Imported from lmmoreira/ikaro (
.agents/skills/story-discovery/SKILL.md). Install upstream withnpx skills add lmmoreira/ikaro --skill story-discovery. Copyright stays with the author.
Run a structured pre-implementation discovery session for a story or TD. Checks doc clarity, completeness, consistency, dependency artifacts, alignment with the validated UX prototype (frontend stories), and locks in the architectural pattern and a concrete test/e2e coverage plan — asking the user as many questions as needed to resolve every open decision before any code is written. Ends by asking how the user wants to set up the working environment (worktree vs direct branch).
This session is the one deep, front-loaded decision point in the workflow (CLAUDE.md §9): once it returns READY, the entire rest of the implementation — commit, push, /pre-pr, PR, CI-fix, bot-fix — runs autonomously with no further per-step permission asks. That's only safe if every pattern choice, test-strategy decision, and business-rule ambiguity gets resolved here, not deferred to implementation time.
HARD RULE — NO CODE CHANGES: This skill only reads code and updates documentation files (
.mdplan and doc files). It NEVER writes or modifies any.ts,.js, or any source/test/config file. If a gap requires a code change (e.g. enriching an event payload, adding a method to an aggregate), flag it as a recommendation in the readiness verdict and let the user decide when and how to handle it — do NOT make the change.
Argument: $ARGUMENTS — story ID (e.g. M09-S04) or TD ID (e.g. TD13).
Step 0 — Workspace state check
Run these checks before reading any plan or doc files.
1. Detect existing branch for this story:
git branch -a | grep -i "<story-id>"
If a branch already exists → RISK: "Branch <branch-name> already exists for this story — are you resuming interrupted work? Confirm before creating a new branch."
2. Detect dirty working tree:
git status --short
If output is non-empty → RISK: "Working tree has uncommitted changes — stash or commit them before starting a new story."
3. Check if story is already done:
In the plan file (located in Step 1), look for ✅ Done next to the story ID. If found → BLOCKER: "Story <story-id> is already marked ✅ Done — should not be re-implemented. Confirm with user before proceeding."
Surface any findings here immediately. If a BLOCKER is found in Step 0, stop and report it — do not proceed to Step 1 without user confirmation.
Step 1 — Locate the story
Story ID format:
M<N>-S<NN>(e.g.M09-S04) → plan file:plan/<milestone>-*.md— exclude*_IMPLEMENTATION_DETAILS_IA.mdand*_IMPLEMENTATION_DETAILS_DEVELOPER.mdTD<N>(e.g.TD13) → plan file:td/TD<N>-*.md
Exactly one match expected — if zero or more than one file matches, STOP:
Found files matching
<pattern>: . Ambiguous — confirm with the user which file is canonical before proceeding.
Read the file and find ### <story-id> — (full M<milestone>-S<NN> form, e.g. M13-S01 — not a bare S01). For TDs, find the primary heading or section. If not found, stop:
Story
<story-id>not found in<file>. Check the ID and try again.
Extract the story block's fields per the canonical schema, docs/STORY_SCHEMA.md (load it now if not already loaded this session). In particular:
- Files to create/modify — if listed, verify each modified-file path actually exists (same Explore-agent discipline as the dependency-symbol check below); flag a missing declared path as a RISK, not silently
- Prototype references — every
plan/journey/...path listed under a "Prototype references:", "Prototype reference:", or milestone-level "Journey prototype:" line - Acceptance criteria — product / technical — if the story predates the product/technical split (an older milestone or pre-standardization TD), treat its single flat AC list as-is rather than blocking on the missing split; note the gap as a RISK only if it makes verification ambiguous
- Infra-specific fields (IAM/permissions, Live-verification check, PR sequence) — for any
devops-agent story, confirm these are present; a missing Live-verification check on an infra-touching story is a RISK per CLAUDE.md §9 Step 5 item 6, not silently assumed unnecessary - Any mention of: new DB migration/entity, new i18n keys, new env vars, new Pub/Sub topics, feature flags
Also check story status: Look for ✅ Done next to the story heading (Step 0 check #3).
Immediately after extracting story content — spawn one Explore agent for symbol search.
From the Dependencies list and the story description, derive every artifact symbol the story expects (aggregate methods, use-case class names, event names, port names, component names, fetcher function names, page route paths — using the vocabulary rules in Step 3). Spawn an Explore agent with "very thorough" breadth and instruct it to run, for each symbol:
grep -r "<symbol>" apps/ --include="*.ts" --include="*.tsx" -l
The agent should return {symbol, found: true/false, matchingFiles: [...]} for each. Continue to Step 2 immediately without waiting — collect results before Step 4.
Step 2 — Load referenced docs
For each entry in "Docs to load":
- Verify the file path exists.
- If a
§ Sectionis specified, confirm that heading exists inside the file. - Read the relevant content.
Also load unconditionally:
docs/CODE_STANDARDS.mddocs/AGENT_PATTERNS.mddocs/ENGINEERING_RULES_SHARED.mdalways, plusdocs/ENGINEERING_RULES_BACKEND.md/_INFRA.md/_FRONTEND.md/_TESTING.mdfor whichever layer(s) this story touches (TD41-S4 split the former singledocs/ENGINEERING_RULES.md, which is now just a redirect index) — most "critical code invariants" (Transactions, Event Handlers, RequestContext, aggregate-events-outbox, Controller/Route boundaries) live in these files rather than inline in CLAUDE.md §7. A story can no longer be checked against them without loading the relevant file(s) explicitly.docs/DEFINITION_OF_DONE.md— know the full completion bar before writing the story, not just at/pre-prtime (see 4p below).- The matching
plan/<milestone>_IMPLEMENTATION_DETAILS_IA.md(if it exists — older milestones have one; use it to understand established patterns for this milestone)
For each entry in "Prototype references":
- Verify the file path exists.
- Also load that folder's
dev-notes.mdandindex.html— even if not explicitly cited, they're the canonical implementation-handoff and screen-inventory files for the prototype. - Load the parent journey
.mdspec (e.g.plan/journey/staff/agenda.mdfor a prototype understaff/prototypes/agenda/).
Flag any path that doesn't resolve as a BLOCKER.
Step 3 — Dependency symbol check
Collect the results from the Explore agent spawned at the end of Step 1. If the agent has not yet returned, wait for it now.
The symbol vocabulary (used to brief the agent and to interpret its results):
-
backend-ts/bff-tsdependencies: aggregate methods, use-case class names, event names, port interface names, repository method names. -
frontend-ts/web-tsdependencies: component names, page/route file paths, hook names, exported fetcher function names (e.g.DashboardShell,fetchStaffBookings,apps/web/app/dashboard/bookings/page.tsx) — these live underapps/web/, notapps/backend//apps/bff/. -
devopsdependencies (live infra/cloud state): when a dependency story is taggedAgent: devopsand its own Acceptance Criteria describe live cloud state (an org policy, an IAM binding, an enabled API, a DNS record, a provisioned account) rather than committed code, a✅ Donemarker is not sufficient evidence the AC is still true — plan-file status only proves the story was closed out, not that the described state exists today. Run one live, read-only check per such AC line — a real cloud-API read (gcloud ... describe/list, or equivalent) or a refresh-backed Terraform check (terraform plan -refresh-only) — before treating it as a Confirmation. Neverterraform state show: it only reflects what the state file records, not the live provider, so it cannot catch the exact class of drift this rule exists to catch. If the check can't be run yet, or fails, it becomes a BLOCKER — same treatment as a missing code symbol below, never a softer RISK — with the exact command and its actual output (or the reason it couldn't run) noted, so the gap is visible before any code is written. (M17-S14 precedent, 2026-07-17: S07 was marked ✅ Done but its own "project-level org-policy exceptions" AC line had never actually been executed — caught only mid-implementation via a live check that should have run here instead.) -
devopsIAM/binding forward-references (target resource doesn't exist yet): when a devops story's own IAM/permission table includes a binding whose target resource (a specific Cloud Run service, Pub/Sub topic, secret, bucket, etc.) is created by a story that comes later in the dependency chain — or isn't a dependency at all — that binding cannot literally be created by this story; Terraform can't reference a resource that doesn't exist yet. For every binding row in the story's own table, check whether the target resource's owning story appears in this story's own Dependencies list (or is this same story). If not, flag as [ORDERING] RISK and propose the binding be created by whichever story actually owns the target resource instead (which must then depend on this story for the principal to exist) — never attempt to grant IAM on a resource this story's Terraform has no way to reference. (M17-S15/S17 precedent, 2026-07-18: S15's registry module originally described granting reader access to "runtime SAs" that don't exist until S17; S17's own table listedrun.invoker/pubsub.publisherbindings on Cloud Run services and Pub/Sub topics that don't exist until S18/S19 — both caught only during story-discovery, twice in the same session.) -
Shared closed-enum/union extension impact: when a story's own description or acceptance criteria call for adding a new member to a shared closed enum, string union, or Zod
z.enum([...])(e.g.HotsiteModuleType, an error-code union, a status/type discriminator) — check whether that same conceptual value set has more than one independent copy across the codebase before assuming a single-file edit is sufficient. Grep the enum's own member names (not just its declared name) acrosspackages/types/src/,apps/backend/src/contexts/**/domain/,packages/validation/src/, and anyapps/web/exhaustiveRecord<Type, ...>map keyed by it. If more than one copy exists, list every copy found and add an explicit acceptance criterion to update all of them together — and flag whether any consumer (an exhaustiveRecord/switchinapps/web/, an ESLint exhaustiveness rule) would break on an addition to only one copy. Treat as a RISK, not a BLOCKER, unless the check can't be completed. (M20-S01 precedent, 2026-08-24:'LEAD_FORM'was added to the sharedHotsiteModuleTypeunion inpackages/types/src/enums.tswithout checking thatapps/backend/.../hotsite-config.types.tsandpackages/validation/src/hotsite.tseach carry an independent copy of the same conceptual type; the shared-package edit brokeapps/web's exhaustiveRecord<HotsiteModuleType,...>maps and had to be reverted mid-implementation.td/TD37-CI-ARCHITECTURE-VALIDATION-HARDENING.mdStory 21 is mechanizing this as a CI detector, but a story can be discovered before that detector exists — check by hand until it lands.)
Also check: do any dependency stories have status Pending? If a required upstream story is not done → BLOCKER: "Story <dep-id> is a dependency and is not yet marked Done."
If a symbol has found: false → BLOCKER — dependency artifact not found in codebase. The same applies to a failed or unconfirmed live infra-state check above.
Step 4 — Discovery checklist
Run every check silently. Tag each finding as BLOCKER, RISK, or CONFIRMATION.
4a. Doc validity
- Every "Docs to load" path exists and resolves (
docs/archive/refs are always blockers — superseded content) - Every referenced
§ Sectionheading exists in its file
4b. Use case completeness
- The UC section in
docs/04-USE_CASES.mdcovers all flows the story description mentions - Acceptance criteria address the UC's main flow + primary alternative flows
- Every failure mode in acceptance criteria has a named HTTP status code
- Config-driven values name the exact
tenants.settingskey (cross-check againstdocs/21-TENANTS_SETTINGS_SCHEMA.md)
4c. State machine consistency
- Every state transition the story triggers is valid per CLAUDE.md §5
- No criterion references
NO_SHOW(not in MVP) - No reference to UC-014 or UC-015 (superseded by UC-021/UC-022)
4d. Event envelope completeness
- Every event the story emits is defined in
docs/03-DOMAIN_EVENTS.md - Each event carries:
eventId,tenantId,occurredAt,correlationId,eventName,eventVersion,data - Domain-specific payload fields (e.g.
isBusiness,cancelledBy) are documented
4e. Multi-tenancy invariants
- Every query implied by the story filters by
tenant_id - Cross-aggregate references use composite FKs
(tenant_id, id) - No implied
UNIQUE(google_oauth_id)alone for customers
4f. Test coverage readability
- At least one tenant-isolation acceptance criterion (Tenant A data + Tenant B caller → 404/403)
- At least one integration test scenario is specified
- Acceptance criteria are concrete enough to derive test names from
- For a frontend story creating multiple distinct
page.tsxroutes (a list + create + edit + deactivate-or-similar shape), the E2E acceptance criteria must name at least one scenario touching each route, not just the ones with the most obvious business action — a route with zero E2E coverage is a full-stack-wiring gap invisible to unit tests, since jsdom/Testing Library never exercises real routing, the real BFF, or the real backend. Cross-check the story's own "Files to create" list'spage.tsxentries against its E2E AC bullets one-for-one. (M21-S04 precedent, 2026-09-02: the story's own E2E AC named only a create→deactivate→reactivate round trip; the Edit route — its ownpage.tsxat/dashboard/resources/[id]/, editing every field including the LOCATION-hours lock — had zero E2E coverage from the original implementation through 12+ rounds of bot review, found only when the user asked directly whether every flow was covered.)
4g. Cross-context data access
- Data from another context is accessed via events, BFF orchestration, or a named port — not direct repo injection
- Port interfaces referenced in the story are named explicitly
4h. API contract
- BFF endpoint: method, path, auth requirement (role/JWT), and response body fields are all specified
- Shared endpoints (same path, different role) explicitly state how routing/branching works
4i. Configuration / settings
- Every configurable threshold names its exact
tenants.settingspath - No hardcoded business values in use-case steps
4j. Conflicts with project standards
- Story doesn't contradict the relevant
docs/ENGINEERING_RULES_*.mdfile(s),docs/CODE_STANDARDS.md, ordocs/ANTI_PATTERNS.md— these are the primary sources now, not CLAUDE.md §7/§8's excerpts of them - Story doesn't conflict with patterns locked in prior milestones'
_IMPLEMENTATION_DETAILS_IA.md - Any file path the story specifies matches CLAUDE.md §11's domain-slice rules — in particular, an actor-scoped view of another domain's aggregate (e.g. a Customer reading their own Booking/Loyalty data) belongs in the owning domain's slice, never the actor's slice (TD31 Story 11 precedent — this exact mistake already happened once)
- For a new page/route nested under an existing shared layout (e.g. anything under
app/[slug]/): grep that layout file for components rendered unconditionally, outside the{children}slot — these apply to every route beneath it, including the new one, whether or not the story's author was aware of them. If the story's AC assumes uninterrupted access to the new page (e.g. "the customer fills in their phone directly on this form"), check whether any such component could intercept that flow first (a mandatory profile-completion gate, an auth redirect, a maintenance banner) and either fold the interaction into the AC or flag it as a RISK for the user to resolve before implementation. Don't assume a new page starts from a blank slate just because its own component tree looks self-contained (M20-S09 PR #433 precedent, 2026-08-26:InformationCompletionPrompt, rendered unconditionally byapp/[slug]/layout.tsxfor every route, silently blocked the lead-form's own "customer edits their phone inline" AC for any customer with an incomplete profile — found only via live manual testing, well after the story's AC had already been written and implemented) - For a new top-level dashboard section (
app/dashboard/<name>/**): grepapps/web/shells/dashboard/for every existing per-section registry a sibling section is already wired into —Sidebar.tsx's nav list,apps/web/proxy.ts'sMANAGER_ONLY_ROUTES(if role-gated),BottomNav.tsx's hide-on-drilldown route matcher, andtopbar-route.ts'sPAGE_TITLE_KEYS/per-action title resolver — and confirm the story's file list wires the new section into all of them, not just whichever one a specific component happened to need built first. Building a route-matcher module to satisfy one consumer's need doesn't mean every other consumer picked it up automatically. (M21-S04 precedent, 2026-09-02:resource-route.tswas built and correctly wired intoBottomNav.tsx's mobile hide-on-drilldown logic during the original implementation, buttopbar-route.ts/Topbar.tsxwere never updated — the dashboard topbar showed the generic "Dashboard" title on every Resources route instead of "Recursos," through 12+ rounds of automated bot review, found only via live manual testing)
4k. Journey / prototype alignment (frontend stories — Agent: frontend-ts/web-ts, or any story citing a plan/journey/ path)
- Frontend-facing story with no prototype reference at all → RISK — UI wasn't UX-validated via a prototype before this story was written
- Component/route names the story introduces match what
dev-notes.md's file map / per-screen sections call them — no invented names that drift from the prototype's documented components - Exact pt-BR copy/error strings in the acceptance criteria match the prototype's validated copy verbatim — don't let a story re-invent wording the prototype already settled
- Every unhappy-path/variant screen present in the prototype folder (loading, fetch-error, validation-error, empty, success states) has a corresponding acceptance criterion — a screen that exists in the prototype but is silently dropped from the story's AC is a UX regression, not a scope simplification
- Every "Known limitations" bullet in the prototype's
dev-notes.mdis either addressed by this story's AC or explicitly carried into the story's own open questions index.html's dry-run checklist questions are either answered by the story's AC or explicitly left open- Precedent pattern, per action — not just by name. If the story's design (or
dev-notes.md) names a sibling feature as its structural precedent (e.g. "follows Team's list/create/deactivate/reactivate shape"), citing the precedent by folder/name is not enough — read the precedent's actual component code for each action the new story also implements (create, edit, deactivate, reactivate, etc.) and record which concrete UI mechanic it uses (confirmation screen vs. one-click inline row action, a shared component vs. a new one). A precedent cited only at the folder level is exactly how a reactivate flow ends up as a full confirmation screen when the cited precedent actually does it inline with no screen at all. (M21-S04 precedent, 2026-09-02:dev-notes.mdcorrectly namedmanager/equipe.md's "Ativar" as the precedent for reactivation during discovery, and even correctly described it as "same one-click-row-action pattern" — but the shipped implementation still built a full confirmation screen anyway. The written note was right; nothing checked the code against it before or during implementation. Caught only via live manual testing, after 9 rounds of automated bot review missed it too — user feedback: "I want [reactivate] to be really simple as we have in staff screen — we only do it on the grid.") - Domain-model field semantics, for every planned form field. For a create/edit form's field list, cross-check each field against
docs/02-DOMAIN_MODEL.md's own documented semantics for that aggregate — specifically, whether a field described as "denormalized" or "independent of X" is planned as independently editable in the UI, not silently re-derived from whatever it's denormalized from. A field that looks safe to auto-populate from a linked entity may be explicitly documented as an independent, user-owned value. (M21-S04 precedent, 2026-09-02:Resource.nameis documented as "denormalized display name, independent ofStaff.name," but the STAFF picker never got an editable name field at all — every edit silently overwrote it with the linked staff member's current name.)
4l. Infrastructure / environment
- Does the story introduce a new secret, Pub/Sub topic, Cloud Scheduler job, or env var — or touch
infra/terraform/**at all? → Consultinfra/terraform/README.md's "New-resource PR-sequencing playbook" table now, before continuing. Match the story's change against the table's rows and state the required PR count and sequence explicitly as a finding — not a bare "flag it." Carry the same statement into the Scope Summary'sDevops PR sequenceline (Step 5). This table exists specifically because this exact class of gap (foundation/envs split,env-contract's all-or-nothing schema requirement, Cloud Run'ssecret_env_varsdeploy hazard) was independently rediscovered from scratch across M19-S02, S07, and S08 — the whole point is to stop that from happening an S09th time. - If the story adds a new secret and it's ambiguous whether the table's safe-3-PR row or the accept-deploy-risk-2-PR row applies → this is a question for the user in Step 6, never a silent default. State both options' tradeoff (deploy-risk window vs. an extra PR) the way the table does. First check the playbook's rule 3: if this same story also creates a new Pub/Sub topic/subscription, the accept-deploy-risk row is unsafe and the safe-3-PR row is mandatory — not a choice to offer the user at all (TD39 follow-up, M19-S08, 2026-08-14: a failed Cloud Run deploy from the risky row silently prevented a same-PR Pub/Sub subscription from ever being created, since its
push_endpointdepends on that same service's URL — that subscription's own Foundation grant then 404'd for an unrelated-looking reason days apart from its actual cause). - If the story adds a new secret whose consumer is wired into a real running service (
secret_env_vars, any row) → the plan-file text must include, as its own explicit numbered step, "populate the secret's real value viagcloud secrets versions add" immediately after the accessor-grant PR — not folded into a general "populate out-of-band" mention. A zero-version secret fails the exact same way a missing accessor grant does, and skipping this step produces a failure that looks identical to the IAM grant not having worked. - Does the story require a new env var? → RISK: verify naming follows
SNAKE_UPPER_CASE; flag it for.env.exampleupdate - Does the story use a feature flag? → RISK: flag must follow
FEATURE_FLAG_XYZ=trueconvention (CLAUDE.md §1); verify it's not wired to an external system
4m. i18n keys
- Does the story description or acceptance criteria mention UI copy, labels, or error messages? → Check if the story lists the exact
packages/i18n/locales/en/web.json+pt-BR/web.jsonkeys to be added - If UI copy is implied but no i18n keys are specified → RISK: "Story implies new UI copy but doesn't name i18n keys — both locale files must be updated in the same commit"
4n. Migration / entity registration
- Does the story add a new TypeORM entity or database migration? → RISK: "
integration-global-setup.tsmust be updated in the same commit — missing registration causes silent test failures" - Check that the migration follows expand/contract (backward-compatible) — no destructive column drops in a single step
- New/modified migration → RISK: "
docs/13-DATABASE_SCHEMA.md's matching table must be updated in the same commit — same silent-drift risk as theintegration-global-setup.tsregistration above" (a full/docs-auditsweep, 2026-08-04, found 6 tables where this had already drifted)
4o. Engineering discipline — no workarounds, no improvisation, no accumulating machinery
Check the story's proposed design, not just its documentation completeness, against CLAUDE.md §7's 3 NON-NEGOTIABLE principles. This is a design-quality read, not a doc-gap check — findings here are RISKs for discussion with the user, never BLOCKERs:
- No workarounds: does any acceptance criterion describe patching a symptom (suppressing a warning, pinning a version, adding a one-off special case) where a root-cause fix looks available instead?
- No improvisation: if the story cites a specific reference (a library, an existing pattern, a named example), does the design actually use it — or does it describe a bespoke alternative presented as equivalent?
- Mounting complexity: does the story's own description already need multiple stacked safeguards/exceptions/special-cases to work — a sign a structurally simpler approach might need none of it? Before accepting the design as-is, check whether an existing port/adapter/pattern (grep
infrastructure/cross-context/,docs/AGENT_PATTERNS.md's numbered patterns, or a similar existing use case) already solves this without the extra machinery.
4p. Stale-reference sweep anticipation (Definition of Done)
If this story replaces or removes an existing flow/mechanism (an auth pattern, a data model assumption, a transport layer, a dead endpoint) — does the story's own scope explicitly include grepping docs/*.md, other milestones' plan/*_IMPLEMENTATION_DETAILS_*.md, .claude/commands/**, .claude/skills/**, and scripts/** for stale references to the old version? If the story is silent on this, flag it now — docs/DEFINITION_OF_DONE.md makes this mandatory, and catching the gap here is cheaper than at milestone close-out (M13 precedent: 18 such findings across 8 files, found only when the milestone closed).
Inverse case — journey GAP-status drift: if this story's own Prototype references point at a plan/journey/<actor>/<slug>.md that currently marks the relevant screen/flow ❓ GAP, does the story's scope include flipping that status in the same commit? A full /docs-audit sweep (2026-08-04) found this exact pattern in every actor's journeys (28 findings) — dev-notes.md consistently got updated when a gap shipped, the parent journey .md's mermaid/Prototype table consistently didn't. Flag as RISK if the story is silent on it.
4q. Pattern & test-strategy lock-in
- Architectural pattern: does the story's design name the concrete pattern it uses (strategy, factory, builder, plain composition, etc.) and why — or explicitly state that no named pattern applies? A story that's silent on this pushes an undocumented judgment call into implementation time; surface it as a question in Step 6 instead.
- Test/e2e coverage plan: does the story name concrete test scenarios — the specific unit cases, integration flows, and (for frontend stories) e2e/Playwright scenarios — rather than a vague "at least one integration test"? A vague coverage statement here becomes an implementation-time judgment call instead of a discovery-time decision.
- Business-rule ambiguity: does anything in the story's description leave a business rule underspecified (a threshold, an edge case, a precedence between two rules)? Surface each as a question in Step 6 rather than letting the implementation step infer one.
- Ripple effects: does this story's change plausibly affect another existing flow, screen, or use case not explicitly listed in its scope? If so, name it as a RISK — either fold it into this story's scope or explicitly note it's out of scope and why.
4r. Business-logic reference doc (docs/27-BUSINESS_LOGIC_REFERENCE.md)
Does this story introduce or change an algorithm, state machine, or formula that spans multiple use cases or aggregates within its bounded context — the kind of logic a future dev/agent would otherwise have to re-derive from scattered prose across docs/02/docs/04/docs/13? If so, flag as a RISK that the story's own scope should include adding or updating that context's section in docs/27-BUSINESS_LOGIC_REFERENCE.md (a permanent, mermaid-diagrammed reference, additive by bounded context — read its own header before writing). A context with no section yet is normal; a story that meaningfully changes an existing section's algorithm without touching the doc is the actual gap to catch here. Not every story needs this — only genuinely complex, cross-cutting logic, not a single new field or endpoint.
Step 5 — Print findings
Start with a Story scope summary — a quick mental model for the agent before listing findings:
## Story Discovery — <story-id>: <title>
### Scope summary
- **Layers:** <backend | BFF | frontend | full-stack>
- **Core pattern:** <e.g. "new use case + BFF endpoint" / "new React component consuming existing BFF route">
- **Upstream deps:** <N> stories (<list with status>)
- **Migration required:** yes / no
- **i18n keys required:** yes / no
- **Feature flag:** yes (`FEATURE_FLAG_XYZ`) / no
- **Devops PR sequence:** N/A / <N> PRs (<one-line summary — e.g. "1: topic+scheduler job+app code (infra-app-mix-ok); 2: foundation IAM grant follow-up">)
Then list findings:
### Blockers (resolve before writing any code)
1. [DOC-PATH] `docs/03-DOMAIN_EVENTS.md §BookingRescheduled` — section not found
2. [SYMBOL] `booking.reschedule()` — no match in apps/ — M07-S03 may be incomplete
3. [DEP] Story `M09-S02` is a dependency and is not yet marked Done
### Risks (could cause rework mid-story)
1. [COVERAGE] No tenant-isolation acceptance criterion — CI gate will require one
2. [API] BFF routing for shared PATCH endpoint not described — ambiguous which use case is called
3. [JOURNEY] No prototype reference found for this frontend story — UX wasn't validated before the story was written
4. [I18N] Story implies new UI copy but doesn't name i18n keys
5. [MIGRATION] New entity detected — verify `integration-global-setup.ts` is updated in same commit
6. [WORKAROUND] AC #3 suppresses a lint warning instead of fixing the underlying type error — root-cause fix looks available
7. [PATTERN] Story proposes a new Port+Adapter for booking→loyalty reads; `infrastructure/cross-context/` already has one — extend it instead
8. [STALE-SWEEP] Story replaces the legacy invite-link format but doesn't mention checking `docs/*.md`/`.claude/commands/**` for references to the old one
### Confirmations (assumed settled — flag if any are wrong)
1. APPROVED → CANCELLED transition is valid per state machine ✓
2. `cancellation_window_hours` key exists in docs/21-TENANTS_SETTINGS_SCHEMA.md ✓
3. PATCH /v1/bookings/:id/cancel endpoint created in M09-S01 — this story reuses it ✓
4. "Tentar novamente" retry-button copy matches `01e-submit-error.html` exactly ✓
If zero blockers and zero risks, emit:
✅ No issues found. Story is implementation-ready.
and skip Steps 6–7, go directly to Step 8.
Step 6 — Questions to the user (one shot)
Collect every question that requires human input (i.e., the answer isn't derivable from the existing docs) and post them all at once in a single numbered list. Group by theme. Distinguish blockers (must resolve before starting) from risks (can proceed with a stated default).
## Questions before we start
Please answer all at once — I'll wait for one reply before proposing any doc changes.
**Event payload**
1. [BLOCKER] `BookingRescheduled` isn't in `docs/03-DOMAIN_EVENTS.md`. Should `data` carry `previousScheduledAt` + `newScheduledAt` + `rescheduledBy`? Or a different shape?
**BFF routing**
2. [RISK] The story says one `PATCH /v1/bookings/:id/reschedule` endpoint but doesn't say how the BFF picks the backend use case. JWT role only, or also a body flag? (Default assumption: JWT role — confirm or override.)
**Defaults**
3. [CONFIRMATION] I'm reading "cancellation_window_hours absent → fall back to 48h" as the intent. Correct, or should a missing setting be a hard error?
Wait for the user's single reply before continuing.
Step 7 — Propose doc updates
DOCS ONLY. This step updates
.mdfiles only — plan files and docs indocs/. Never touch.ts,.js, migration files, or any source/test/config file.
For every blocker or risk that a doc gap caused (missing event payload field, ambiguous criterion, wrong reference, missing consumer), propose a concrete doc fix. Show the exact content to add/change.
If resolving the gap would also require a code change (e.g. enriching an event class interface or updating an aggregate method), do NOT make the code change. Instead, include it in the readiness verdict under a dedicated section:
### Code changes required before implementation
- `booking-cancelled.event.ts` — add `scheduledAt`, `lineSummary`, `totalPrice` fields to interface
- `booking.aggregate.ts` — update `cancel()` to populate the new fields at emit time
These changes are outside the scope of story-discovery. Implement them on the feature branch before writing story code, or address them in a separate preparatory commit on main.
For EACH doc change, apply §0 permission protocol:
- Summarise what you intend to write.
- Ask: "May I now update
<path>?" - Write only after an explicit yes.
If the user says no to a change, note it and proceed with the current docs.
Once all approved Step-7 edits are written, commit and push them to main together — list the files, ask before the commit and again before the push (§0's gates apply as normal), same as any other commit. Do this before Step 9's worktree decision. This is the default specifically to prevent the orphaned-edit trap described in the Worktree note below: EnterWorktree's default fresh base ref branches from origin/main, so committed-and-pushed Step-7 edits are already present in a freshly created worktree, with nothing to reapply. If the user declines the commit or the push, the edit stays uncommitted in the main checkout — follow the Worktree note's fallback guidance instead.
Worktree note: Step-7 edits are committed and pushed to main immediately (above) specifically so the trap below doesn't happen. But if a push was declined, or a doc edit lands in the main checkout some other way (see the M18-S02 case below), it's real: EnterWorktree's new worktree branches fresh from origin/main, and does not carry forward uncommitted — or committed-but-unpushed — changes sitting in the main checkout. An edit left in that state is silently orphaned unless reapplied inside the worktree (confirmed in M17-S32, 2026-07-19 — before this commit-and-push default existed — docs/24-BFF_ARCHITECTURE.md had to be rewritten a second time after EnterWorktree). If the edit is committed but unpushed in the main checkout, don't retype the content — cherry-pick that exact commit into the worktree (git log to find the SHA, git cherry-pick <sha>), then remove the stray commit from the main checkout (git reset --hard origin/main — destructive, confirm with the user first per the git safety protocol) so a later routine push from there can never publish a duplicate or divergent version. If the edit is uncommitted, redo it inside the worktree and discard the orphaned uncommitted copy per the note below instead — or defer either case to Step 9 entirely and make the edit from inside the worktree in the first place. This isn't limited to Step 7 — the same trap applies to any plan-file doc edit made in the main checkout at any point before EnterWorktree is called and pushed, including drafting the story itself (M18-S02, 2026-07-28: the entire initial story draft was written to the main checkout before story-discovery even started; it landed on origin/main only because the user separately committed and pushed it directly — the PR branch itself still forked before that push and missed a later refinement, caught only when a cross-tool PR review flagged what looked like a code/spec mismatch).
Once the edit is reapplied inside the worktree, discard the orphaned uncommitted copy left in the main checkout (git checkout -- <path> / git restore <path>) — don't just leave it sitting there. An orphaned uncommitted doc edit has no owner and no expiration; anyone who later runs a routine commit in the main checkout can sweep it up and push it to main by mistake, creating a merge conflict with the worktree branch's own (by then further-diverged) version of the same file when the branch is later merged back. (M18-S05 precedent, 2026-07-31: the initial story draft was correctly reapplied inside the worktree per this same note, but the leftover copy in the main checkout was never discarded; it was later committed directly to main as [chore] doc s05, conflicting with the worktree branch's substantially-rewritten version of the same section when merging origin/main before opening the PR.)
If the fix is git merge origin/main into the worktree branch (per the M17-S15/S17 guidance below, for an already-committed main-side edit): this can fail with fatal: refusing to merge unrelated histories, even though the histories are genuinely related. EnterWorktree creates a shallow clone — the shared ancestor exists on the remote but isn't present locally, so git can't see it. Run git fetch --unshallow origin first (safe, non-destructive — it only fetches additional history) and retry the merge.
Gitignored files are a sharper version of the same trap — they don't get orphaned, they don't exist at all. A worktree only ever contains git-tracked content, so a gitignored file that exists in the main checkout (e.g. docs/BOOTSTRAP_LOG.md, an operator-local bootstrap/runbook log) is simply absent from a freshly-created worktree — ls on its expected path returns nothing, and there's no "reapply inside the worktree" option the way there is for a tracked doc, because there's nothing to copy from and no commit will ever carry it. Any edit to a gitignored file must be made directly in the main checkout's copy, regardless of which worktree the rest of the story's work happens in (confirmed in M17-S24, 2026-07-21 — the agent initially reasoned about this file as if normal worktree rules applied, and needed the user to point out it's gitignored before locating it correctly). Before editing any doc referenced by a story, check git check-ignore -v <path> if it doesn't show up where expected — don't assume a missing file means it needs creating.
Step 8 — Readiness verdict
After all questions are answered and doc updates are applied (or declined), emit a final verdict:
## Readiness verdict
✅ READY — all blockers resolved.
What was clarified:
- BookingRescheduled payload: previousScheduledAt + newScheduledAt + rescheduledBy
- BFF routing: JWT role determines use case (STAFF|MANAGER → admin; CUSTOMER → customer)
- Missing cancellation_window_hours → default 48h
- Tenant-isolation criterion added to plan file
or:
## Readiness verdict
❌ NOT READY — N blocker(s) unresolved.
Remaining blockers:
- [SYMBOL] booking.reschedule() not found in codebase — fix dependency before starting
Do not start implementation until all blockers are cleared.
If NOT READY, stop here. Do not proceed to Step 9.
A ✅ READY verdict is the single authorization for the rest of the implementation workflow (CLAUDE.md §9) — commit, push, /pre-pr, PR, CI-fix, and bot-fix all proceed autonomously from here with no further per-step asks; only the final merge review and any stuck condition come back to the user.
Step 9 — Working environment setup
Only reached when Step 8 verdict is ✅ READY.
Ask the user:
## Working environment
How do you want to work on this story?
1. **Worktree** — isolated copy of the repo under `.claude/worktrees/`. Safe for parallel work; requires cleanup after PR merge.
2. **Direct branch** — feature branch in the main working directory. Simpler; no cleanup needed.
Reply with `1` / `worktree` or `2` / `direct`.
Wait for reply, then:
If worktree:
- Use the
EnterWorktreetool with branch namefeat/<story-id-lowercase>-<short-description>(e.g.feat/m09-s04-booking-reschedule). - After
EnterWorktreecompletes, confirm the worktree path and branch to the user. - Cleanup is automatic, not a reminder to the user: CLAUDE.md §9 Step 11 (mark-done) removes the worktree immediately afterward, no permission needed —
Then verify withgit worktree remove .claude/worktrees/<name> --force git branch -D <branch-name> git fetch --prune origingit worktree listandls .claude/worktrees/— don't trust a success message alone.
If direct branch:
- Output the branch creation command for the user to run (per §9 Step 1 of CLAUDE.md):
git checkout -b feat/M<N>-S<NN>-<short-description> - Wait for the user to confirm before any code is written.
Either way, end with:
Ready. Next: implement per §9 Step 2 — write all files from the story spec.
This READY verdict already authorizes the rest of the chain (§9 Steps 3–9) —
commit, push, /pre-pr, PR, CI-fix, and bot-fix all proceed autonomously from
here with no further per-step asks. I'll come back to you only for the merge
review (§9 Step 10) or a stuck condition.