diff --git a/CLAUDE.md b/CLAUDE.md index c76990ced..a041451b6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -210,7 +210,7 @@ Add or update a test alongside any change to engine scripts, adapters, migration ## Prose Tests -`tests/prose/` — end-to-end tests for the prose logic (design: `design/prose-tests.md`). A case is a directory under `tests/prose/cases/{case-id}/`: `case.json` (values code branches on), `fixture.md` + `fixture-state.cjs` (the starting world), `act.md` (the coarse instruction a walker agent follows through the real skills), `assert.md` (the expected path, seen only by the asserting agent), optional `assertion-state.cjs` (the world the walk should produce; absent = unchanged), and the two generated snapshots. Nothing is parsed and prose never shares a file with code. Shared pipeline stages live in `tests/prose/mainlines/`. Walks cost tokens — run on command via the `/prose-test` dev skill, never as part of a routine gate. The deterministic perimeter runs under `npm test`: corpus validation plus snapshot goldens — every snapshot must rebuild byte-identical from its recipe under the frozen clock, skipped when nothing feeding it changed. When an engine change moves a world, regenerate with `node tests/prose/run.cjs snap {case-id}` and land the snapshot diff in the same PR; never hand-edit a snapshot. Every review finding lands twice: the fix, and the case that would have caught it. At the end of a PR that touches skill prose, run `node tests/prose/run.cjs select --diff main` and suggest running the intersecting cases. +`tests/prose/` — end-to-end tests for the prose logic (design: `design/prose-tests.md`). A case is a directory under `tests/prose/cases/{case-id}/`: `case.json` (values code branches on, including the required `entry` and any deterministic `invariants`), `fixture.md` + `fixture-state.cjs` (the starting world), `act.md` (the coarse instruction a walker agent follows through the real skills), `assert.md` (the expected path, seen only by the asserting agent), optional `assertion-state.cjs` (the world the walk should produce; absent = unchanged), and the two generated snapshots. Nothing is parsed and prose never shares a file with code. **A case starts where a session starts and runs until state lands**: `entry` must name `workflow-start`, a `workflow-*-entry` skill, or `workflow-discovery` — never a navigation or processing skill, never a reference — because a walk begun mid-session lacks the context the prose was written for, and one that stops at a gate leaves an empty delta and nothing but display claims to judge. Claims that code can settle belong in `invariants` (`engine_before_write`, `calls_include`, `calls_exclude`), which run before the asserter and whose verdicts it cannot overturn. Shared pipeline stages live in `tests/prose/mainlines/`. Walks cost tokens — run on command via the `/prose-test` dev skill, never as part of a routine gate. The deterministic perimeter runs under `npm test`: corpus validation plus snapshot goldens — every snapshot must rebuild byte-identical from its recipe under the frozen clock, skipped when nothing feeding it changed. When an engine change moves a world, regenerate with `node tests/prose/run.cjs snap {case-id}` and land the snapshot diff in the same PR; never hand-edit a snapshot. Every review finding lands twice: the fix, and the case that would have caught it. At the end of a PR that touches skill prose, run `node tests/prose/run.cjs select --diff main` and suggest running the intersecting cases. ## Knowledge Base Subsystem diff --git a/design/prose-tests.md b/design/prose-tests.md index 7307f07ce..dba57d890 100644 --- a/design/prose-tests.md +++ b/design/prose-tests.md @@ -170,6 +170,30 @@ testing. A failure is a finished result; diagnosing it is a separate, human-led act. This is stated in prose in each definition rather than relied on from tool restrictions, which an agent may hold regardless. +- **P9 — a case starts where a session starts, and runs until state + lands.** The only real openings are `workflow-start` (the user's), a + `workflow-*-entry` skill (what a bridge plan file invokes after a + context clear), and `workflow-discovery` (epic continuation). Navigation + and processing skills are always reached mid-session; a reference is + never entered directly. A walk carries only the context it accumulates, + so one begun in the middle is judged under conditions the prose was + never written for, and its verdict means nothing in either direction. + Enforced in `lib/cases.cjs` by the required `entry` field, not left to + authoring discipline. The same rule ends a case: stop at a gate and the + delta is empty, the claims turn into descriptions of a display, and the + asserter is left comparing the walker's account of what it showed + against the case's account of what it should have shown — with no + independent ground truth anywhere in the loop. + +- **P10 — what code can decide, code decides.** A case declares + invariants over the recorded actions (`lib/invariants.cjs`) that run + before the asserter sees anything, and whose verdicts it may not + overturn. They exist because the world delta proves the outcome but not + that it was earned — a walk that ignored every instruction and wrote + the expected files lands the same state. Not a pinned call sequence: + recording and replaying one would freeze whatever the walker happened + to do, certifying broken prose instead of catching it. + - **P7 — a failing case is a finding either way.** Either the prose broke, or the world/design moved and the case is stale. The adjudication is the point; only the typo-class staleness is @@ -218,6 +242,29 @@ testing. ## Log +- 2026-07-26 — Two findings that dissolved a long-running mystery, and + the rules that follow from them. First: we were judging a walk by the + single message an agent returns, while the walk itself happens across + dozens of turns. Every step narrated at the time and compressed out of + the closing summary read as a step never taken — the "under-reporting" + blamed first on the walker, then on the model, was ours. A Sonnet walk + of continue-feature was marked down for skipping the selection menu; + its transcript shows it emitted the display, emitted the menu verbatim, + stopped, consumed the scripted answer and validated the selection. An + Opus walk of the same case made byte-identical tool calls. The runtime + writes every turn already and the stop payload names the file, so the + hook now lifts them into the world (P6e). Second, and underneath it: + four cases started where no session ever starts — two at navigation + skills, one at a processing skill, one inside a reference. They are + retired, and the rule is enforced rather than remembered (P9). The + residue is now decided in code where it can be (P10). Along the way the + recorder was found to have been reading `tool_output` when the field is + `tool_response`, so every successful call had recorded its status and + nothing else, and the stop hook had never once fired — its world is + resolved from the transcript now, which also stamps the model on every + verdict, since an unreloaded definition can otherwise run a walk on a + model nobody chose. + - 2026-07-26 — The first round that found more prose than instrument. Recording finally live (the blocker was `hasTrustDialogAccepted`, which gates frontmatter hooks in project subagents and fails silently), five diff --git a/tests/prose/cases/continue-bugfix-routes-to-investigation/act.md b/tests/prose/cases/continue-bugfix-routes-to-investigation/act.md deleted file mode 100644 index 0f11a5af9..000000000 --- a/tests/prose/cases/continue-bugfix-routes-to-investigation/act.md +++ /dev/null @@ -1,4 +0,0 @@ -Execute skills/workflow-continue-bugfix/SKILL.md with no arguments, as -it is invoked when the caller has no work unit pre-selected. Follow it -through selection and the bugfix's state display. Stop once the state -and its menu have been shown — do not invoke the route. diff --git a/tests/prose/cases/continue-bugfix-routes-to-investigation/assert.md b/tests/prose/cases/continue-bugfix-routes-to-investigation/assert.md deleted file mode 100644 index f975a31a2..000000000 --- a/tests/prose/cases/continue-bugfix-routes-to-investigation/assert.md +++ /dev/null @@ -1,10 +0,0 @@ -The prose should have taken this path: - -1. initialisation is casing only — boot and migrations belong to the - entry skill -2. shows the selection display and menu rather than auto-selecting the - only bugfix -3. validates the selection, then shows the bugfix's pipeline state -4. the state shows no phase started, and the continue action routes to - investigation entry — a bugfix's first phase is investigation, never - discussion or specification diff --git a/tests/prose/cases/continue-bugfix-routes-to-investigation/case.json b/tests/prose/cases/continue-bugfix-routes-to-investigation/case.json deleted file mode 100644 index 7a221fd7f..000000000 --- a/tests/prose/cases/continue-bugfix-routes-to-investigation/case.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "origin": "bugfix mainline — a fresh bug routes to investigation, not discussion", - "files": [ - "skills/workflow-continue-bugfix/SKILL.md", - "skills/workflow-continue-bugfix/references/select-bugfix.md", - "skills/workflow-continue-bugfix/references/bugfix-display-and-menu.md" - ], - "answers": [ - "the number listed against `crash-fix`" - ] -} diff --git a/tests/prose/cases/continue-bugfix-routes-to-investigation/fixture-state.cjs b/tests/prose/cases/continue-bugfix-routes-to-investigation/fixture-state.cjs deleted file mode 100644 index 6cd47642e..000000000 --- a/tests/prose/cases/continue-bugfix-routes-to-investigation/fixture-state.cjs +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -// The bug is captured and nothing has been started. - -const m = require('../../mainlines/bugfix.cjs'); - -module.exports = { - build(h) { - m.init(h); - m.create(h); - }, -}; diff --git a/tests/prose/cases/continue-bugfix-routes-to-investigation/fixture/.claude/settings.json b/tests/prose/cases/continue-bugfix-routes-to-investigation/fixture/.claude/settings.json deleted file mode 100644 index ae019ede5..000000000 --- a/tests/prose/cases/continue-bugfix-routes-to-investigation/fixture/.claude/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "showClearContextOnPlanAccept": true, - "permissions": { - "allow": [ - "Edit(.workflows/**)", - "Bash(mv .workflows/:*)" - ] - } -} diff --git a/tests/prose/cases/continue-bugfix-routes-to-investigation/fixture/.recipe-hash b/tests/prose/cases/continue-bugfix-routes-to-investigation/fixture/.recipe-hash deleted file mode 100644 index 259e11ddc..000000000 --- a/tests/prose/cases/continue-bugfix-routes-to-investigation/fixture/.recipe-hash +++ /dev/null @@ -1 +0,0 @@ -f2d22a09d2680d82822f6042b87baf77dacbf41d4d8cb2b6d26a33d5c4129ff9 diff --git a/tests/prose/cases/continue-bugfix-routes-to-investigation/fixture/.workflows/.state/migrations b/tests/prose/cases/continue-bugfix-routes-to-investigation/fixture/.workflows/.state/migrations deleted file mode 100644 index 3c7162acc..000000000 --- a/tests/prose/cases/continue-bugfix-routes-to-investigation/fixture/.workflows/.state/migrations +++ /dev/null @@ -1,52 +0,0 @@ -001 -002 -003 -004 -005 -006 -007 -008 -009 -010 -011 -012 -013 -014 -015 -016 -017 -018 -019 -020 -021 -022 -023 -024 -025 -026 -027 -028 -029 -030 -031 -032 -033 -034 -035 -036 -037 -038 -039 -040 -041 -042 -043 -044 -045 -046 -047 -048 -049 -050 -051 -052 diff --git a/tests/prose/cases/continue-bugfix-routes-to-investigation/fixture/.workflows/_gitignore.fixture b/tests/prose/cases/continue-bugfix-routes-to-investigation/fixture/.workflows/_gitignore.fixture deleted file mode 100644 index 779cbf28a..000000000 --- a/tests/prose/cases/continue-bugfix-routes-to-investigation/fixture/.workflows/_gitignore.fixture +++ /dev/null @@ -1,2 +0,0 @@ -.cache/ -.manifest.json.*.tmp diff --git a/tests/prose/cases/continue-bugfix-routes-to-investigation/fixture/.workflows/crash-fix/discovery/sessions/session-001.md b/tests/prose/cases/continue-bugfix-routes-to-investigation/fixture/.workflows/crash-fix/discovery/sessions/session-001.md deleted file mode 100644 index 6dc56d4e3..000000000 --- a/tests/prose/cases/continue-bugfix-routes-to-investigation/fixture/.workflows/crash-fix/discovery/sessions/session-001.md +++ /dev/null @@ -1,40 +0,0 @@ -# Discovery Session 001 - -Date: 2026-01-01 -Work unit: crash-fix - -## Description (as of session) - -Checkout crashes when an order has no shipping address. - -## Seed - -(none) - -## Imports - -(none) - -## Map State at Start - -(n/a — single-topic work) - -## Exploration - -Reported by two users this week: the checkout page 500s at the -payment step for orders with no shipping address. Reproducible on -staging with a digital-only basket. No error surfaces to the user — -the page just fails. Confirmed as a bugfix; no design question here, -so it routes straight to investigation. - -## Edits - -(none) - -## Topics Identified - -(none) - -## Conclusion - -(none) diff --git a/tests/prose/cases/continue-bugfix-routes-to-investigation/fixture/.workflows/crash-fix/manifest.json b/tests/prose/cases/continue-bugfix-routes-to-investigation/fixture/.workflows/crash-fix/manifest.json deleted file mode 100644 index eaaa56199..000000000 --- a/tests/prose/cases/continue-bugfix-routes-to-investigation/fixture/.workflows/crash-fix/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "crash-fix", - "work_type": "bugfix", - "status": "in-progress", - "created": "2026-01-01", - "description": "Checkout crashes when an order has no shipping address", - "phases": {} -} diff --git a/tests/prose/cases/continue-bugfix-routes-to-investigation/fixture/.workflows/manifest.json b/tests/prose/cases/continue-bugfix-routes-to-investigation/fixture/.workflows/manifest.json deleted file mode 100644 index 1cd498ed5..000000000 --- a/tests/prose/cases/continue-bugfix-routes-to-investigation/fixture/.workflows/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "work_units": { - "crash-fix": { - "work_type": "bugfix" - } - } -} diff --git a/tests/prose/cases/continue-feature-routes-to-discussion/act.md b/tests/prose/cases/continue-feature-routes-to-discussion/act.md deleted file mode 100644 index 93e469eef..000000000 --- a/tests/prose/cases/continue-feature-routes-to-discussion/act.md +++ /dev/null @@ -1,4 +0,0 @@ -Execute skills/workflow-continue-feature/SKILL.md with no arguments, as -it is invoked when the caller has no work unit pre-selected. Follow it -through selection and the feature's state display. Stop once the state -and its menu have been shown — do not invoke the route. diff --git a/tests/prose/cases/continue-feature-routes-to-discussion/assert.md b/tests/prose/cases/continue-feature-routes-to-discussion/assert.md deleted file mode 100644 index b789b6b49..000000000 --- a/tests/prose/cases/continue-feature-routes-to-discussion/assert.md +++ /dev/null @@ -1,13 +0,0 @@ -The prose should have taken this path: - -1. initialisation is casing only — no boot and no migrations, which the - entry skill has already guaranteed -2. shows the selection display and menu even though `pay` is the only - feature — never auto-selects -3. validates the selection, then shows the feature's pipeline state -4. the state shows no phase started, and the menu's continue action - routes to discussion entry for work type feature, work unit pay - -Further claims: - -- navigation only reads: no phase is started by looking at it diff --git a/tests/prose/cases/continue-feature-routes-to-discussion/case.json b/tests/prose/cases/continue-feature-routes-to-discussion/case.json deleted file mode 100644 index 33c362b36..000000000 --- a/tests/prose/cases/continue-feature-routes-to-discussion/case.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "origin": "feature mainline — navigation shows state and routes to the next phase", - "files": [ - "skills/workflow-continue-feature/SKILL.md", - "skills/workflow-continue-feature/references/select-feature.md", - "skills/workflow-continue-feature/references/feature-display-and-menu.md" - ], - "answers": [ - "the number listed against `pay`" - ] -} diff --git a/tests/prose/cases/continue-feature-routes-to-discussion/fixture-state.cjs b/tests/prose/cases/continue-feature-routes-to-discussion/fixture-state.cjs deleted file mode 100644 index e4b78cd71..000000000 --- a/tests/prose/cases/continue-feature-routes-to-discussion/fixture-state.cjs +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -// The feature exists and nothing has been started. - -const m = require('../../mainlines/feature.cjs'); - -module.exports = { - build(h) { - m.init(h); - m.create(h); - }, -}; diff --git a/tests/prose/cases/continue-feature-routes-to-discussion/fixture/.claude/settings.json b/tests/prose/cases/continue-feature-routes-to-discussion/fixture/.claude/settings.json deleted file mode 100644 index ae019ede5..000000000 --- a/tests/prose/cases/continue-feature-routes-to-discussion/fixture/.claude/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "showClearContextOnPlanAccept": true, - "permissions": { - "allow": [ - "Edit(.workflows/**)", - "Bash(mv .workflows/:*)" - ] - } -} diff --git a/tests/prose/cases/continue-feature-routes-to-discussion/fixture/.recipe-hash b/tests/prose/cases/continue-feature-routes-to-discussion/fixture/.recipe-hash deleted file mode 100644 index 92519878f..000000000 --- a/tests/prose/cases/continue-feature-routes-to-discussion/fixture/.recipe-hash +++ /dev/null @@ -1 +0,0 @@ -5b0a7d802b3a436a4656bc000122451bb015f2250f8fef47b1d05f3ea51ba96b diff --git a/tests/prose/cases/continue-feature-routes-to-discussion/fixture/.workflows/.state/migrations b/tests/prose/cases/continue-feature-routes-to-discussion/fixture/.workflows/.state/migrations deleted file mode 100644 index 3c7162acc..000000000 --- a/tests/prose/cases/continue-feature-routes-to-discussion/fixture/.workflows/.state/migrations +++ /dev/null @@ -1,52 +0,0 @@ -001 -002 -003 -004 -005 -006 -007 -008 -009 -010 -011 -012 -013 -014 -015 -016 -017 -018 -019 -020 -021 -022 -023 -024 -025 -026 -027 -028 -029 -030 -031 -032 -033 -034 -035 -036 -037 -038 -039 -040 -041 -042 -043 -044 -045 -046 -047 -048 -049 -050 -051 -052 diff --git a/tests/prose/cases/continue-feature-routes-to-discussion/fixture/.workflows/_gitignore.fixture b/tests/prose/cases/continue-feature-routes-to-discussion/fixture/.workflows/_gitignore.fixture deleted file mode 100644 index 779cbf28a..000000000 --- a/tests/prose/cases/continue-feature-routes-to-discussion/fixture/.workflows/_gitignore.fixture +++ /dev/null @@ -1,2 +0,0 @@ -.cache/ -.manifest.json.*.tmp diff --git a/tests/prose/cases/continue-feature-routes-to-discussion/fixture/.workflows/manifest.json b/tests/prose/cases/continue-feature-routes-to-discussion/fixture/.workflows/manifest.json deleted file mode 100644 index 987f6c236..000000000 --- a/tests/prose/cases/continue-feature-routes-to-discussion/fixture/.workflows/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "work_units": { - "pay": { - "work_type": "feature" - } - } -} diff --git a/tests/prose/cases/continue-feature-routes-to-discussion/fixture/.workflows/pay/discovery/sessions/session-001.md b/tests/prose/cases/continue-feature-routes-to-discussion/fixture/.workflows/pay/discovery/sessions/session-001.md deleted file mode 100644 index 2aab96961..000000000 --- a/tests/prose/cases/continue-feature-routes-to-discussion/fixture/.workflows/pay/discovery/sessions/session-001.md +++ /dev/null @@ -1,39 +0,0 @@ -# Discovery Session 001 - -Date: 2026-01-01 -Work unit: pay - -## Description (as of session) - -Accept card payments at checkout. - -## Seed - -(none) - -## Imports - -(none) - -## Map State at Start - -(n/a — single-topic work) - -## Exploration - -Shaped as a single feature: accept card payments at checkout using -the existing gateway account. Card-only for v1 came up early and was -softly agreed; wallet support was noted as a likely deferral. No -research need surfaced — routed straight to discussion. - -## Edits - -(none) - -## Topics Identified - -(none) - -## Conclusion - -(none) diff --git a/tests/prose/cases/continue-feature-routes-to-discussion/fixture/.workflows/pay/manifest.json b/tests/prose/cases/continue-feature-routes-to-discussion/fixture/.workflows/pay/manifest.json deleted file mode 100644 index 1cf8afb0e..000000000 --- a/tests/prose/cases/continue-feature-routes-to-discussion/fixture/.workflows/pay/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "pay", - "work_type": "feature", - "status": "in-progress", - "created": "2026-01-01", - "description": "Accept card payments at checkout", - "phases": {} -} diff --git a/tests/prose/cases/discussion-entry-seeds-from-carrier/case.json b/tests/prose/cases/discussion-entry-seeds-from-carrier/case.json index 85b404f92..514a9adfa 100644 --- a/tests/prose/cases/discussion-entry-seeds-from-carrier/case.json +++ b/tests/prose/cases/discussion-entry-seeds-from-carrier/case.json @@ -1,5 +1,6 @@ { "origin": "feature mainline \u2014 single-phase work seeds from its durable carrier", + "entry": "workflow-discussion-entry", "files": [ "skills/workflow-discussion-entry/SKILL.md", "skills/workflow-shared/references/ensure-discovery-item.md" diff --git a/tests/prose/cases/discussion-session-substituted/act.md b/tests/prose/cases/discussion-session-substituted/act.md deleted file mode 100644 index 09ae57f9a..000000000 --- a/tests/prose/cases/discussion-session-substituted/act.md +++ /dev/null @@ -1,5 +0,0 @@ -Execute skills/workflow-discussion-process/SKILL.md from its first step, -as the entry skill has just invoked it for work unit `pay`, topic `pay`. -Follow it through initialisation. Stop once the session loop is behind -you and the prose moves on to reviewing the discussion — do not begin -the review, dispatch any agent, or conclude the phase. diff --git a/tests/prose/cases/discussion-session-substituted/assert.md b/tests/prose/cases/discussion-session-substituted/assert.md deleted file mode 100644 index a45e704a3..000000000 --- a/tests/prose/cases/discussion-session-substituted/assert.md +++ /dev/null @@ -1,25 +0,0 @@ -The prose should have taken this path: - -1. resume detection finds no discussion file and treats this as a fresh - discussion — no resume choice is offered -2. initialisation registers the discussion in the manifest through the - engine before any map command, since the map requires the item to - exist -3. it creates the discussion file from the template, including the - terminal Triage section seeded as `(none)` -4. it derives initial subtopics from the context available and records - each on the map through the engine, each landing `pending` -5. it commits the initialised discussion -6. the session loop is where the conversation would happen, and the walk - resumes after it to reach the review step, which it does not enter - -Further claims: - -- the subtopic names are derived by the walker from the seed and context, - so which names appear and how many is expected to vary between runs. - What matters is that at least one was recorded through the engine, that - each began `pending`, and that by the end none remain `pending`. -- the discussion file's working sections hold the substituted content, - and its Triage section reads `(none)` -- the discussion item exists in the manifest with its subtopics; no other - phase has been touched diff --git a/tests/prose/cases/discussion-session-substituted/case.json b/tests/prose/cases/discussion-session-substituted/case.json deleted file mode 100644 index c024e03f7..000000000 --- a/tests/prose/cases/discussion-session-substituted/case.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "origin": "framework — the mid-flow substitution: an unscripted conversation stood in for, so the steps around it can be walked", - "world": "claims", - "files": [ - "skills/workflow-discussion-process/SKILL.md", - "skills/workflow-discussion-process/references/initialize-discussion.md", - "skills/workflow-discussion-process/references/template.md" - ], - "stubs": { - "discussion-pay-decided": "once the discussion file has been created from the template, the map seeded, and the initialisation commit made — the moment the prose would enter the session loop" - } -} diff --git a/tests/prose/cases/discussion-session-substituted/fixture-state.cjs b/tests/prose/cases/discussion-session-substituted/fixture-state.cjs deleted file mode 100644 index 546391b87..000000000 --- a/tests/prose/cases/discussion-session-substituted/fixture-state.cjs +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -// A feature routed to discussion with nothing started — the discussion -// file does not exist, so initialisation runs for real. - -const m = require('../../mainlines/feature.cjs'); - -module.exports = { - build(h) { - m.init(h); - m.create(h); - }, -}; diff --git a/tests/prose/cases/discussion-session-substituted/fixture.md b/tests/prose/cases/discussion-session-substituted/fixture.md deleted file mode 100644 index 9f3a2c546..000000000 --- a/tests/prose/cases/discussion-session-substituted/fixture.md +++ /dev/null @@ -1,3 +0,0 @@ -The feature exists and no phase has started. Discovery shaped it and -routed it to discussion; the discussion entry skill has already run and -handed over, so the processing skill is starting from its first step. diff --git a/tests/prose/cases/discussion-session-substituted/fixture/.claude/settings.json b/tests/prose/cases/discussion-session-substituted/fixture/.claude/settings.json deleted file mode 100644 index ae019ede5..000000000 --- a/tests/prose/cases/discussion-session-substituted/fixture/.claude/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "showClearContextOnPlanAccept": true, - "permissions": { - "allow": [ - "Edit(.workflows/**)", - "Bash(mv .workflows/:*)" - ] - } -} diff --git a/tests/prose/cases/discussion-session-substituted/fixture/.recipe-hash b/tests/prose/cases/discussion-session-substituted/fixture/.recipe-hash deleted file mode 100644 index af934cf8a..000000000 --- a/tests/prose/cases/discussion-session-substituted/fixture/.recipe-hash +++ /dev/null @@ -1 +0,0 @@ -9b48d4c996d773219dc60a44fcc014b1f534adc4d22537741a96575da66d2e46 diff --git a/tests/prose/cases/discussion-session-substituted/fixture/.workflows/.state/migrations b/tests/prose/cases/discussion-session-substituted/fixture/.workflows/.state/migrations deleted file mode 100644 index 3c7162acc..000000000 --- a/tests/prose/cases/discussion-session-substituted/fixture/.workflows/.state/migrations +++ /dev/null @@ -1,52 +0,0 @@ -001 -002 -003 -004 -005 -006 -007 -008 -009 -010 -011 -012 -013 -014 -015 -016 -017 -018 -019 -020 -021 -022 -023 -024 -025 -026 -027 -028 -029 -030 -031 -032 -033 -034 -035 -036 -037 -038 -039 -040 -041 -042 -043 -044 -045 -046 -047 -048 -049 -050 -051 -052 diff --git a/tests/prose/cases/discussion-session-substituted/fixture/.workflows/_gitignore.fixture b/tests/prose/cases/discussion-session-substituted/fixture/.workflows/_gitignore.fixture deleted file mode 100644 index 779cbf28a..000000000 --- a/tests/prose/cases/discussion-session-substituted/fixture/.workflows/_gitignore.fixture +++ /dev/null @@ -1,2 +0,0 @@ -.cache/ -.manifest.json.*.tmp diff --git a/tests/prose/cases/discussion-session-substituted/fixture/.workflows/manifest.json b/tests/prose/cases/discussion-session-substituted/fixture/.workflows/manifest.json deleted file mode 100644 index 987f6c236..000000000 --- a/tests/prose/cases/discussion-session-substituted/fixture/.workflows/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "work_units": { - "pay": { - "work_type": "feature" - } - } -} diff --git a/tests/prose/cases/discussion-session-substituted/fixture/.workflows/pay/discovery/sessions/session-001.md b/tests/prose/cases/discussion-session-substituted/fixture/.workflows/pay/discovery/sessions/session-001.md deleted file mode 100644 index 2aab96961..000000000 --- a/tests/prose/cases/discussion-session-substituted/fixture/.workflows/pay/discovery/sessions/session-001.md +++ /dev/null @@ -1,39 +0,0 @@ -# Discovery Session 001 - -Date: 2026-01-01 -Work unit: pay - -## Description (as of session) - -Accept card payments at checkout. - -## Seed - -(none) - -## Imports - -(none) - -## Map State at Start - -(n/a — single-topic work) - -## Exploration - -Shaped as a single feature: accept card payments at checkout using -the existing gateway account. Card-only for v1 came up early and was -softly agreed; wallet support was noted as a likely deferral. No -research need surfaced — routed straight to discussion. - -## Edits - -(none) - -## Topics Identified - -(none) - -## Conclusion - -(none) diff --git a/tests/prose/cases/discussion-session-substituted/fixture/.workflows/pay/manifest.json b/tests/prose/cases/discussion-session-substituted/fixture/.workflows/pay/manifest.json deleted file mode 100644 index 1cf8afb0e..000000000 --- a/tests/prose/cases/discussion-session-substituted/fixture/.workflows/pay/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "pay", - "work_type": "feature", - "status": "in-progress", - "created": "2026-01-01", - "description": "Accept card payments at checkout", - "phases": {} -} diff --git a/tests/prose/cases/implementation-picks-first-task/case.json b/tests/prose/cases/implementation-picks-first-task/case.json index cf3393720..f5c58f453 100644 --- a/tests/prose/cases/implementation-picks-first-task/case.json +++ b/tests/prose/cases/implementation-picks-first-task/case.json @@ -1,5 +1,6 @@ { "origin": "feature mainline \u2014 entry through resume detection to the first task", + "entry": "workflow-implementation-entry", "files": [ "skills/workflow-implementation-entry/SKILL.md", "skills/workflow-implementation-entry/references/validate-phase.md", diff --git a/tests/prose/cases/investigation-entry-seeds-from-carrier/case.json b/tests/prose/cases/investigation-entry-seeds-from-carrier/case.json index 71758ea1b..8f64940f7 100644 --- a/tests/prose/cases/investigation-entry-seeds-from-carrier/case.json +++ b/tests/prose/cases/investigation-entry-seeds-from-carrier/case.json @@ -1,5 +1,6 @@ { "origin": "bugfix mainline \u2014 a bug shaped in discovery is never re-interrogated", + "entry": "workflow-investigation-entry", "files": [ "skills/workflow-investigation-entry/SKILL.md", "skills/workflow-investigation-entry/references/invoke-skill.md" diff --git a/tests/prose/cases/planning-entry-asks-for-late-context/case.json b/tests/prose/cases/planning-entry-asks-for-late-context/case.json index 83b632fd8..8ff061076 100644 --- a/tests/prose/cases/planning-entry-asks-for-late-context/case.json +++ b/tests/prose/cases/planning-entry-asks-for-late-context/case.json @@ -1,5 +1,6 @@ { "origin": "feature mainline \u2014 planning offers a last chance to add context", + "entry": "workflow-planning-entry", "files": [ "skills/workflow-planning-entry/SKILL.md", "skills/workflow-planning-entry/references/validate-spec.md", diff --git a/tests/prose/cases/review-entry-fresh/case.json b/tests/prose/cases/review-entry-fresh/case.json index e19d4dfa4..a365e2cd2 100644 --- a/tests/prose/cases/review-entry-fresh/case.json +++ b/tests/prose/cases/review-entry-fresh/case.json @@ -1,5 +1,6 @@ { "origin": "feature mainline \u2014 review refuses to start until delivery is complete", + "entry": "workflow-review-entry", "files": [ "skills/workflow-review-entry/SKILL.md", "skills/workflow-review-entry/references/validate-phase.md" diff --git a/tests/prose/cases/root-cause-validation-clean-verdict/act.md b/tests/prose/cases/root-cause-validation-clean-verdict/act.md deleted file mode 100644 index c3225285c..000000000 --- a/tests/prose/cases/root-cause-validation-clean-verdict/act.md +++ /dev/null @@ -1,2 +0,0 @@ -Walk root-cause-validation.md from the top, as the processing skill -would. Stop when the reference returns to its caller. diff --git a/tests/prose/cases/root-cause-validation-clean-verdict/assert.md b/tests/prose/cases/root-cause-validation-clean-verdict/assert.md deleted file mode 100644 index 291c65e4b..000000000 --- a/tests/prose/cases/root-cause-validation-clean-verdict/assert.md +++ /dev/null @@ -1,20 +0,0 @@ -The prose should have taken this path: - -1. the scripted `yes` is consumed at the run-or-skip choice, so the walk - takes the run arm — a `skip` would have returned to the caller with no - dispatch at all -2. the dispatch is recorded through the engine, and the report lands at - the path that response returned — the prose does not invent a path or - pre-create the file -3. exactly one validation agent stands behind that dispatch: one dispatch - recorded, one report written, no second of either -4. once the report has landed, the row is promoted by a scan and closed - by an incorporate — the verdict is consumed whole, never surfaced - finding by finding -5. the report is read and, the verdict being validated, the reference - returns to its caller — the gap-handling choice is never reached, so - no gaps are recorded and the investigation file is never edited - -Further claims: - -- nothing is committed: the validated path has no gaps to record diff --git a/tests/prose/cases/root-cause-validation-clean-verdict/assertion-state.cjs b/tests/prose/cases/root-cause-validation-clean-verdict/assertion-state.cjs deleted file mode 100644 index 12c381e52..000000000 --- a/tests/prose/cases/root-cause-validation-clean-verdict/assertion-state.cjs +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -// The fixture state plus what the walk should have done: the dispatch -// recorded, the agent's report landed at the path the dispatch returned, -// scan promoting the row and incorporate closing it. The report content -// comes from the same stub the case arms, so the bytes the walk writes -// and the bytes expected here cannot drift apart. - -const fs = require('fs'); -const path = require('path'); - -const fixture = require('./fixture-state.cjs'); -const m = require('../../mainlines/bugfix.cjs'); -const { readStub } = require('../../lib/cases.cjs'); - -module.exports = { - build(h) { - fixture.build(h); - - const dispatch = JSON.parse(h.engine( - 'agent', 'dispatch', m.WU, 'investigation', m.WU, '--kind', 'root-cause-validation')); - fs.mkdirSync(path.dirname(path.join(h.dir, dispatch.file)), { recursive: true }); - h.write(dispatch.file, readStub('root-cause-validated').content); - h.engine('agent', 'scan', m.WU, 'investigation', m.WU); - h.engine('agent', 'incorporate', m.WU, 'investigation', m.WU, dispatch.id); - }, -}; diff --git a/tests/prose/cases/root-cause-validation-clean-verdict/assertion/.claude/settings.json b/tests/prose/cases/root-cause-validation-clean-verdict/assertion/.claude/settings.json deleted file mode 100644 index ae019ede5..000000000 --- a/tests/prose/cases/root-cause-validation-clean-verdict/assertion/.claude/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "showClearContextOnPlanAccept": true, - "permissions": { - "allow": [ - "Edit(.workflows/**)", - "Bash(mv .workflows/:*)" - ] - } -} diff --git a/tests/prose/cases/root-cause-validation-clean-verdict/assertion/.recipe-hash b/tests/prose/cases/root-cause-validation-clean-verdict/assertion/.recipe-hash deleted file mode 100644 index d38ba55a6..000000000 --- a/tests/prose/cases/root-cause-validation-clean-verdict/assertion/.recipe-hash +++ /dev/null @@ -1 +0,0 @@ -b65c43b1fced1a2d5742f96a2d6d1a8278f67d44c143552dbb42bee06badded6 diff --git a/tests/prose/cases/root-cause-validation-clean-verdict/assertion/.workflows/.cache/crash-fix/investigation/crash-fix/root-cause-validation-001.md b/tests/prose/cases/root-cause-validation-clean-verdict/assertion/.workflows/.cache/crash-fix/investigation/crash-fix/root-cause-validation-001.md deleted file mode 100644 index ce1c9b4c4..000000000 --- a/tests/prose/cases/root-cause-validation-clean-verdict/assertion/.workflows/.cache/crash-fix/investigation/crash-fix/root-cause-validation-001.md +++ /dev/null @@ -1,10 +0,0 @@ -# Root Cause Validation - -The tax context is built before the payment intent and reads the shipping -address unconditionally. Traced fresh: an address-less order fails at that -read, which matches the documented root cause. - -STATUS: validated -CONFIDENCE: high -GAPS_COUNT: 0 -SUMMARY: Root cause confirmed by an independent trace. diff --git a/tests/prose/cases/root-cause-validation-clean-verdict/assertion/.workflows/.cache/crash-fix/investigation/crash-fix/state.json b/tests/prose/cases/root-cause-validation-clean-verdict/assertion/.workflows/.cache/crash-fix/investigation/crash-fix/state.json deleted file mode 100644 index 7eb5a78f1..000000000 --- a/tests/prose/cases/root-cause-validation-clean-verdict/assertion/.workflows/.cache/crash-fix/investigation/crash-fix/state.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "agents": { - "root-cause-validation-001": { - "id": "root-cause-validation-001", - "kind": "root-cause-validation", - "phase": "investigation", - "topic": "crash-fix", - "set": "001", - "status": "incorporated", - "announced": false, - "findings": [], - "surfaced": [], - "created": "2026-01-01T00:00:00.000Z" - } - } -} diff --git a/tests/prose/cases/root-cause-validation-clean-verdict/assertion/.workflows/.state/migrations b/tests/prose/cases/root-cause-validation-clean-verdict/assertion/.workflows/.state/migrations deleted file mode 100644 index 3c7162acc..000000000 --- a/tests/prose/cases/root-cause-validation-clean-verdict/assertion/.workflows/.state/migrations +++ /dev/null @@ -1,52 +0,0 @@ -001 -002 -003 -004 -005 -006 -007 -008 -009 -010 -011 -012 -013 -014 -015 -016 -017 -018 -019 -020 -021 -022 -023 -024 -025 -026 -027 -028 -029 -030 -031 -032 -033 -034 -035 -036 -037 -038 -039 -040 -041 -042 -043 -044 -045 -046 -047 -048 -049 -050 -051 -052 diff --git a/tests/prose/cases/root-cause-validation-clean-verdict/assertion/.workflows/_gitignore.fixture b/tests/prose/cases/root-cause-validation-clean-verdict/assertion/.workflows/_gitignore.fixture deleted file mode 100644 index 779cbf28a..000000000 --- a/tests/prose/cases/root-cause-validation-clean-verdict/assertion/.workflows/_gitignore.fixture +++ /dev/null @@ -1,2 +0,0 @@ -.cache/ -.manifest.json.*.tmp diff --git a/tests/prose/cases/root-cause-validation-clean-verdict/assertion/.workflows/crash-fix/discovery/sessions/session-001.md b/tests/prose/cases/root-cause-validation-clean-verdict/assertion/.workflows/crash-fix/discovery/sessions/session-001.md deleted file mode 100644 index 6dc56d4e3..000000000 --- a/tests/prose/cases/root-cause-validation-clean-verdict/assertion/.workflows/crash-fix/discovery/sessions/session-001.md +++ /dev/null @@ -1,40 +0,0 @@ -# Discovery Session 001 - -Date: 2026-01-01 -Work unit: crash-fix - -## Description (as of session) - -Checkout crashes when an order has no shipping address. - -## Seed - -(none) - -## Imports - -(none) - -## Map State at Start - -(n/a — single-topic work) - -## Exploration - -Reported by two users this week: the checkout page 500s at the -payment step for orders with no shipping address. Reproducible on -staging with a digital-only basket. No error surfaces to the user — -the page just fails. Confirmed as a bugfix; no design question here, -so it routes straight to investigation. - -## Edits - -(none) - -## Topics Identified - -(none) - -## Conclusion - -(none) diff --git a/tests/prose/cases/root-cause-validation-clean-verdict/assertion/.workflows/crash-fix/investigation/crash-fix.md b/tests/prose/cases/root-cause-validation-clean-verdict/assertion/.workflows/crash-fix/investigation/crash-fix.md deleted file mode 100644 index d5fc263a9..000000000 --- a/tests/prose/cases/root-cause-validation-clean-verdict/assertion/.workflows/crash-fix/investigation/crash-fix.md +++ /dev/null @@ -1,88 +0,0 @@ -# Investigation: Checkout Crash On Missing Shipping Address - -## Symptoms - -### Problem Description - -Checkout returns a 500 at the payment step when the order has no -shipping address. No error is surfaced to the user. - -### Manifestation - -Server error on POST to the payment step; the page fails blank. - -### Reproduction Steps - -1. Add a digital-only product to the basket. -2. Proceed to checkout without entering a shipping address. -3. Continue to the payment step — the request 500s. - -### Environment - -Reproduced on staging; reported twice in production this week. - -### Impact - -Digital-only orders cannot complete checkout. - -### References - -(none) - -## Analysis - -### Hypotheses - -- The payment step assumes a shipping address is always present. -- Tax calculation may be the first consumer of the missing address. - -### Code Trace - -The payment step builds a tax context from the order before creating -the payment intent. The tax context reads the shipping address -unconditionally; for a digital-only order the address is absent, so -the read fails and the request aborts before any handler catches it. - -### Root Cause - -The tax context treats the shipping address as mandatory. Digital-only -orders legitimately have none, so the assumption is wrong rather than -the data being wrong. - -### Contributing Factors - -Digital-only baskets were introduced after the tax context was written. - -### Why It Wasn't Caught - -No test covers a basket with no shippable line items. - -### Blast Radius - -Any flow building a tax context from an address-less order. - -## Fix Direction - -### Chosen Approach - -(pending) - -### Options Explored - -(none yet) - -### Discussion - -(none yet) - -### Testing Recommendations - -(pending) - -### Risk Assessment - -(pending) - -## Notes - -(none) diff --git a/tests/prose/cases/root-cause-validation-clean-verdict/assertion/.workflows/crash-fix/manifest.json b/tests/prose/cases/root-cause-validation-clean-verdict/assertion/.workflows/crash-fix/manifest.json deleted file mode 100644 index 88bf183ff..000000000 --- a/tests/prose/cases/root-cause-validation-clean-verdict/assertion/.workflows/crash-fix/manifest.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "crash-fix", - "work_type": "bugfix", - "status": "in-progress", - "created": "2026-01-01", - "description": "Checkout crashes when an order has no shipping address", - "phases": { - "investigation": { - "items": { - "crash-fix": { - "status": "in-progress" - } - } - } - } -} diff --git a/tests/prose/cases/root-cause-validation-clean-verdict/assertion/.workflows/manifest.json b/tests/prose/cases/root-cause-validation-clean-verdict/assertion/.workflows/manifest.json deleted file mode 100644 index 1cd498ed5..000000000 --- a/tests/prose/cases/root-cause-validation-clean-verdict/assertion/.workflows/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "work_units": { - "crash-fix": { - "work_type": "bugfix" - } - } -} diff --git a/tests/prose/cases/root-cause-validation-clean-verdict/case.json b/tests/prose/cases/root-cause-validation-clean-verdict/case.json deleted file mode 100644 index aab675685..000000000 --- a/tests/prose/cases/root-cause-validation-clean-verdict/case.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "origin": "bugfix mainline — the validation agent's lifecycle on a clean verdict", - "files": [ - "skills/workflow-investigation-process/references/root-cause-validation.md" - ], - "answers": [ - "yes — run root cause validation" - ], - "stubs": { - "root-cause-validated": "when the engine records a dispatch of kind root-cause-validation" - } -} diff --git a/tests/prose/cases/root-cause-validation-clean-verdict/fixture-state.cjs b/tests/prose/cases/root-cause-validation-clean-verdict/fixture-state.cjs deleted file mode 100644 index 67d91fdbf..000000000 --- a/tests/prose/cases/root-cause-validation-clean-verdict/fixture-state.cjs +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -// The investigation is open with its root cause documented, and no agent has run. - -const m = require('../../mainlines/bugfix.cjs'); - -module.exports = { - build(h) { - m.init(h); - m.create(h); - m.investigateToRootCause(h); - }, -}; diff --git a/tests/prose/cases/root-cause-validation-clean-verdict/fixture.md b/tests/prose/cases/root-cause-validation-clean-verdict/fixture.md deleted file mode 100644 index 95d92d942..000000000 --- a/tests/prose/cases/root-cause-validation-clean-verdict/fixture.md +++ /dev/null @@ -1,3 +0,0 @@ -The investigation is open with its symptoms gathered and its root cause -documented. The processing skill has worked down to its root-cause -validation step; nothing has been validated yet and no agent has run. diff --git a/tests/prose/cases/root-cause-validation-clean-verdict/fixture/.claude/settings.json b/tests/prose/cases/root-cause-validation-clean-verdict/fixture/.claude/settings.json deleted file mode 100644 index ae019ede5..000000000 --- a/tests/prose/cases/root-cause-validation-clean-verdict/fixture/.claude/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "showClearContextOnPlanAccept": true, - "permissions": { - "allow": [ - "Edit(.workflows/**)", - "Bash(mv .workflows/:*)" - ] - } -} diff --git a/tests/prose/cases/root-cause-validation-clean-verdict/fixture/.recipe-hash b/tests/prose/cases/root-cause-validation-clean-verdict/fixture/.recipe-hash deleted file mode 100644 index d38ba55a6..000000000 --- a/tests/prose/cases/root-cause-validation-clean-verdict/fixture/.recipe-hash +++ /dev/null @@ -1 +0,0 @@ -b65c43b1fced1a2d5742f96a2d6d1a8278f67d44c143552dbb42bee06badded6 diff --git a/tests/prose/cases/root-cause-validation-clean-verdict/fixture/.workflows/.state/migrations b/tests/prose/cases/root-cause-validation-clean-verdict/fixture/.workflows/.state/migrations deleted file mode 100644 index 3c7162acc..000000000 --- a/tests/prose/cases/root-cause-validation-clean-verdict/fixture/.workflows/.state/migrations +++ /dev/null @@ -1,52 +0,0 @@ -001 -002 -003 -004 -005 -006 -007 -008 -009 -010 -011 -012 -013 -014 -015 -016 -017 -018 -019 -020 -021 -022 -023 -024 -025 -026 -027 -028 -029 -030 -031 -032 -033 -034 -035 -036 -037 -038 -039 -040 -041 -042 -043 -044 -045 -046 -047 -048 -049 -050 -051 -052 diff --git a/tests/prose/cases/root-cause-validation-clean-verdict/fixture/.workflows/_gitignore.fixture b/tests/prose/cases/root-cause-validation-clean-verdict/fixture/.workflows/_gitignore.fixture deleted file mode 100644 index 779cbf28a..000000000 --- a/tests/prose/cases/root-cause-validation-clean-verdict/fixture/.workflows/_gitignore.fixture +++ /dev/null @@ -1,2 +0,0 @@ -.cache/ -.manifest.json.*.tmp diff --git a/tests/prose/cases/root-cause-validation-clean-verdict/fixture/.workflows/crash-fix/discovery/sessions/session-001.md b/tests/prose/cases/root-cause-validation-clean-verdict/fixture/.workflows/crash-fix/discovery/sessions/session-001.md deleted file mode 100644 index 6dc56d4e3..000000000 --- a/tests/prose/cases/root-cause-validation-clean-verdict/fixture/.workflows/crash-fix/discovery/sessions/session-001.md +++ /dev/null @@ -1,40 +0,0 @@ -# Discovery Session 001 - -Date: 2026-01-01 -Work unit: crash-fix - -## Description (as of session) - -Checkout crashes when an order has no shipping address. - -## Seed - -(none) - -## Imports - -(none) - -## Map State at Start - -(n/a — single-topic work) - -## Exploration - -Reported by two users this week: the checkout page 500s at the -payment step for orders with no shipping address. Reproducible on -staging with a digital-only basket. No error surfaces to the user — -the page just fails. Confirmed as a bugfix; no design question here, -so it routes straight to investigation. - -## Edits - -(none) - -## Topics Identified - -(none) - -## Conclusion - -(none) diff --git a/tests/prose/cases/root-cause-validation-clean-verdict/fixture/.workflows/crash-fix/investigation/crash-fix.md b/tests/prose/cases/root-cause-validation-clean-verdict/fixture/.workflows/crash-fix/investigation/crash-fix.md deleted file mode 100644 index d5fc263a9..000000000 --- a/tests/prose/cases/root-cause-validation-clean-verdict/fixture/.workflows/crash-fix/investigation/crash-fix.md +++ /dev/null @@ -1,88 +0,0 @@ -# Investigation: Checkout Crash On Missing Shipping Address - -## Symptoms - -### Problem Description - -Checkout returns a 500 at the payment step when the order has no -shipping address. No error is surfaced to the user. - -### Manifestation - -Server error on POST to the payment step; the page fails blank. - -### Reproduction Steps - -1. Add a digital-only product to the basket. -2. Proceed to checkout without entering a shipping address. -3. Continue to the payment step — the request 500s. - -### Environment - -Reproduced on staging; reported twice in production this week. - -### Impact - -Digital-only orders cannot complete checkout. - -### References - -(none) - -## Analysis - -### Hypotheses - -- The payment step assumes a shipping address is always present. -- Tax calculation may be the first consumer of the missing address. - -### Code Trace - -The payment step builds a tax context from the order before creating -the payment intent. The tax context reads the shipping address -unconditionally; for a digital-only order the address is absent, so -the read fails and the request aborts before any handler catches it. - -### Root Cause - -The tax context treats the shipping address as mandatory. Digital-only -orders legitimately have none, so the assumption is wrong rather than -the data being wrong. - -### Contributing Factors - -Digital-only baskets were introduced after the tax context was written. - -### Why It Wasn't Caught - -No test covers a basket with no shippable line items. - -### Blast Radius - -Any flow building a tax context from an address-less order. - -## Fix Direction - -### Chosen Approach - -(pending) - -### Options Explored - -(none yet) - -### Discussion - -(none yet) - -### Testing Recommendations - -(pending) - -### Risk Assessment - -(pending) - -## Notes - -(none) diff --git a/tests/prose/cases/root-cause-validation-clean-verdict/fixture/.workflows/crash-fix/manifest.json b/tests/prose/cases/root-cause-validation-clean-verdict/fixture/.workflows/crash-fix/manifest.json deleted file mode 100644 index 88bf183ff..000000000 --- a/tests/prose/cases/root-cause-validation-clean-verdict/fixture/.workflows/crash-fix/manifest.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "crash-fix", - "work_type": "bugfix", - "status": "in-progress", - "created": "2026-01-01", - "description": "Checkout crashes when an order has no shipping address", - "phases": { - "investigation": { - "items": { - "crash-fix": { - "status": "in-progress" - } - } - } - } -} diff --git a/tests/prose/cases/root-cause-validation-clean-verdict/fixture/.workflows/manifest.json b/tests/prose/cases/root-cause-validation-clean-verdict/fixture/.workflows/manifest.json deleted file mode 100644 index 1cd498ed5..000000000 --- a/tests/prose/cases/root-cause-validation-clean-verdict/fixture/.workflows/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "work_units": { - "crash-fix": { - "work_type": "bugfix" - } - } -} diff --git a/tests/prose/cases/smoke-start-boot-structure/case.json b/tests/prose/cases/smoke-start-boot-structure/case.json index 07b9e0424..c47d0f93f 100644 --- a/tests/prose/cases/smoke-start-boot-structure/case.json +++ b/tests/prose/cases/smoke-start-boot-structure/case.json @@ -1,5 +1,6 @@ { - "origin": "framework smoke — a structure-only walk, no world", + "origin": "framework smoke \u2014 a structure-only walk, no world", + "entry": "workflow-start", "files": [ "skills/workflow-start/SKILL.md#Boot" ] diff --git a/tests/prose/cases/spec-entry-from-discussion/case.json b/tests/prose/cases/spec-entry-from-discussion/case.json index f7f4aa938..95045863f 100644 --- a/tests/prose/cases/spec-entry-from-discussion/case.json +++ b/tests/prose/cases/spec-entry-from-discussion/case.json @@ -1,5 +1,6 @@ { "origin": "feature mainline \u2014 a feature's spec draws on its discussion", + "entry": "workflow-specification-entry", "files": [ "skills/workflow-specification-entry/SKILL.md", "skills/workflow-specification-entry/references/validate-source.md", diff --git a/tests/prose/cases/spec-entry-from-investigation/case.json b/tests/prose/cases/spec-entry-from-investigation/case.json index e0849f435..c48f2332e 100644 --- a/tests/prose/cases/spec-entry-from-investigation/case.json +++ b/tests/prose/cases/spec-entry-from-investigation/case.json @@ -1,5 +1,6 @@ { "origin": "bugfix mainline \u2014 the same entry skill, sourced from an investigation", + "entry": "workflow-specification-entry", "files": [ "skills/workflow-specification-entry/SKILL.md", "skills/workflow-specification-entry/references/validate-source.md", diff --git a/tests/prose/cases/start-lists-active-work/case.json b/tests/prose/cases/start-lists-active-work/case.json index c4b7667c2..8cd90c53f 100644 --- a/tests/prose/cases/start-lists-active-work/case.json +++ b/tests/prose/cases/start-lists-active-work/case.json @@ -1,5 +1,6 @@ { "origin": "feature mainline \u2014 the entry skill surfaces work without touching it", + "entry": "workflow-start", "files": [ "skills/workflow-start/SKILL.md" ], diff --git a/tests/prose/lib/cases.cjs b/tests/prose/lib/cases.cjs index e26d82620..371f8cc37 100644 --- a/tests/prose/lib/cases.cjs +++ b/tests/prose/lib/cases.cjs @@ -8,8 +8,8 @@ // modules. // // case.json the values code branches on: -// { origin, files[], answers[], stubs{name: trigger}, -// world: "claims" (optional), +// { origin, entry, files[], answers[], +// stubs{name: trigger}, world: "claims" (optional), // invariants{} (optional) — see lib/invariants.cjs } // fixture.md optional. Prose describing the starting world — // what has already happened, where the session @@ -35,6 +35,20 @@ // variability is genuinely the model's to decide; a world a recipe can // build must be built. // +// `entry` names where the walk starts, and it may only be somewhere a +// real session starts: `workflow-start` (the user's way in), a +// `workflow-*-entry` skill (what the bridge's plan file invokes after a +// context clear), or `workflow-discovery` (epic continuation). Nothing +// else is a legitimate opening — navigation and processing skills are +// always invoked mid-session, and a reference is never entered directly. +// +// This matters because a walk carries only the context it accumulates. +// Start one in the middle and the world may be right while the reading +// that got there is missing, so the prose is judged under conditions it +// was never written for and the verdict means nothing either way. The +// cases that broke this rule were the ones producing findings that +// dissolved on inspection. +// // act.md and assert.md are separate files because that is the P4 // boundary: the walker must never see the expected trace, and a file // boundary enforces it structurally rather than by convention. @@ -98,6 +112,7 @@ function loadCase(id) { rel: path.relative(ROOT, dir), metaError, origin: meta.origin || null, + entry: meta.entry || null, files: (meta.files || []).map((spec) => { const hash = spec.indexOf('#'); return hash === -1 @@ -160,6 +175,27 @@ function headingExists(absPath, anchor) { }); } +/** + * Where a walk may begin. A session starts at workflow-start, or at the + * entry skill a bridge plan file names after a context clear, or at + * discovery for an epic continuation — never anywhere else. + */ +function entryErrors(entry) { + if (!entry) return ['has no entry — name the skill the walk starts at']; + const allowed = entry === 'workflow-start' + || entry === 'workflow-discovery' + || /^workflow-[a-z-]+-entry$/.test(entry); + if (!allowed) { + return [`entry "${entry}" is not somewhere a session starts — use workflow-start, ` + + 'a workflow-*-entry skill, or workflow-discovery. Navigation and processing ' + + 'skills are only ever reached mid-session, and a reference never directly']; + } + if (!fs.existsSync(path.join(ROOT, 'skills', entry, 'SKILL.md'))) { + return [`entry "${entry}" is not a skill in skills/`]; + } + return []; +} + function validateCorpus(cases) { const errors = []; const stubs = new Set(listStubs()); @@ -169,6 +205,7 @@ function validateCorpus(cases) { if (c.metaError) { errors.push(`${at}: ${c.metaError}`); continue; } if (!/^[a-z0-9][a-z0-9-]*$/.test(c.id)) errors.push(`${at}: case directory is not kebab-case`); if (!c.origin) errors.push(`${at}: ${FILES.meta} has no origin`); + for (const e of entryErrors(c.entry)) errors.push(`${at}: ${FILES.meta} ${e}`); if (c.files.length === 0) errors.push(`${at}: ${FILES.meta} scopes no files`); for (const f of c.files) { @@ -225,5 +262,6 @@ function validateCorpus(cases) { module.exports = { ROOT, PROSE_DIR, CASES_DIR, STUBS_DIR, FILES, SNAPSHOTS, - listCaseIds, loadCase, loadAllCases, requireState, listStubs, readStub, validateCorpus, + listCaseIds, loadCase, loadAllCases, requireState, listStubs, readStub, + validateCorpus, entryErrors, }; diff --git a/tests/scripts/test-prose-invariants.cjs b/tests/scripts/test-prose-invariants.cjs index 4d79cd855..8179495f1 100644 --- a/tests/scripts/test-prose-invariants.cjs +++ b/tests/scripts/test-prose-invariants.cjs @@ -189,3 +189,45 @@ describe('declaration validation', () => { assert.match(invariants.declarationErrors(['engine_before_write'])[0], /must be an object/); }); }); + +describe('entry points — where a walk may begin', () => { + const cases = require('../prose/lib/cases.cjs'); + + it('accepts the user entry point', () => { + assert.deepEqual(cases.entryErrors('workflow-start'), []); + }); + + it('accepts an entry skill, which a bridge plan invokes after a context clear', () => { + assert.deepEqual(cases.entryErrors('workflow-implementation-entry'), []); + assert.deepEqual(cases.entryErrors('workflow-specification-entry'), []); + }); + + it('accepts discovery, the one continuation that is not an entry skill', () => { + assert.deepEqual(cases.entryErrors('workflow-discovery'), []); + }); + + it('rejects a navigation skill — always invoked by workflow-start, never cold', () => { + const [error] = cases.entryErrors('workflow-continue-feature'); + assert.match(error, /not somewhere a session starts/); + }); + + it('rejects a processing skill — always invoked by its entry skill', () => { + assert.match(cases.entryErrors('workflow-discussion-process')[0], /not somewhere a session starts/); + }); + + it('rejects a reference, which is never entered directly', () => { + assert.match(cases.entryErrors('root-cause-validation.md')[0], /not somewhere a session starts/); + }); + + it('rejects a plausible name that is not a skill on disk', () => { + assert.match(cases.entryErrors('workflow-imaginary-entry')[0], /not a skill in skills\//); + }); + + it('requires one at all', () => { + assert.match(cases.entryErrors(null)[0], /has no entry/); + }); + + it('holds for the live corpus', () => { + assert.deepEqual(cases.validateCorpus(cases.loadAllCases()), []); + }); +});