From 9f4c13caaf0b534e8e43137cc0219ddea36e914d Mon Sep 17 00:00:00 2001 From: Lee Overy Date: Mon, 27 Jul 2026 14:08:52 +0100 Subject: [PATCH] =?UTF-8?q?feat(prose-tests):=20conduct=20=E2=80=94=20play?= =?UTF-8?q?ing=20a=20person=20where=20a=20script=20cannot=20reach?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A scripted answer covers a question with one right response. Some prose does not work that way: symptom gathering explores, and keeps exploring until it judges it has enough, so there is no fixed number of turns to script. Every case so far has stopped at the edge of such a stretch, which is why seven processing skills had no coverage past their gates. conduct is the third per-case input, beside answers and stubs: a description of the person on the other side, not a queue of replies. The walker answers as that person would, in their words, for as long as the prose keeps asking. A case may carry both — the script covers the discrete gates, the description covers the open stretches. Two guards keep it from becoming a back door through the P4 boundary. The template states it describes the user and never the walk: it cannot name an arm, end a step, or say what the prose ought to do. And the walker's standing instruction now says that playing the user is not steering the walk — a described user who would happily stop talking is still not permission to cut a loop the prose has not ended. The first case to need it walks investigation through its symptom interview: the fix in the commit below this one made the interview run, and this case is the one that fails on the prose before that fix — the finding landing twice, as the project requires. Its user is relaying the bug rather than living it: concrete about what they saw, plain about what they do not know, volunteering nothing past the question asked. Co-Authored-By: Claude Opus 5 --- .claude/agents/prose-walker.md | 15 ++++-- .../investigation-gathers-symptoms/act.md | 4 ++ .../investigation-gathers-symptoms/assert.md | 37 +++++++++++++ .../investigation-gathers-symptoms/case.json | 32 ++++++++++++ .../fixture-state.cjs | 14 +++++ .../investigation-gathers-symptoms/fixture.md | 6 +++ .../fixture/.claude/settings.json | 9 ++++ .../fixture/.recipe-hash | 1 + .../fixture/.workflows/.state/migrations | 52 +++++++++++++++++++ .../fixture/.workflows/_gitignore.fixture | 2 + .../discovery/sessions/session-001.md | 40 ++++++++++++++ .../.workflows/crash-fix/manifest.json | 8 +++ .../fixture/.workflows/manifest.json | 7 +++ tests/prose/lib/cases.cjs | 8 ++- tests/prose/lib/prompts.cjs | 3 +- tests/prose/prompts/walker.md | 17 ++++++ tests/prose/run.cjs | 1 + 17 files changed, 251 insertions(+), 5 deletions(-) create mode 100644 tests/prose/cases/investigation-gathers-symptoms/act.md create mode 100644 tests/prose/cases/investigation-gathers-symptoms/assert.md create mode 100644 tests/prose/cases/investigation-gathers-symptoms/case.json create mode 100644 tests/prose/cases/investigation-gathers-symptoms/fixture-state.cjs create mode 100644 tests/prose/cases/investigation-gathers-symptoms/fixture.md create mode 100644 tests/prose/cases/investigation-gathers-symptoms/fixture/.claude/settings.json create mode 100644 tests/prose/cases/investigation-gathers-symptoms/fixture/.recipe-hash create mode 100644 tests/prose/cases/investigation-gathers-symptoms/fixture/.workflows/.state/migrations create mode 100644 tests/prose/cases/investigation-gathers-symptoms/fixture/.workflows/_gitignore.fixture create mode 100644 tests/prose/cases/investigation-gathers-symptoms/fixture/.workflows/crash-fix/discovery/sessions/session-001.md create mode 100644 tests/prose/cases/investigation-gathers-symptoms/fixture/.workflows/crash-fix/manifest.json create mode 100644 tests/prose/cases/investigation-gathers-symptoms/fixture/.workflows/manifest.json diff --git a/.claude/agents/prose-walker.md b/.claude/agents/prose-walker.md index 63c768459..cf0132881 100644 --- a/.claude/agents/prose-walker.md +++ b/.claude/agents/prose-walker.md @@ -56,9 +56,18 @@ any harness substitutions. Follow it exactly. - Follow the prose literally, step by step, arm by arm. Where it names an engine or knowledge call, run it from the project directory and use the real response to decide which arm applies. Never predict a response. -- You also play the user, from the fixed script in the payload. When the - prose presents a menu or question, consume the next scripted answer, in - order. +- You also play the user. Where the payload gives scripted answers, + consume the next one in order as each menu or question arrives. Where + it instead describes how the user behaves, the prose has no fixed + number of questions to script — answer as that person would, in their + words, for as long as it keeps asking. A payload may carry both: the + script covers the discrete gates, the description covers the open + stretches. +- **Playing the user is not steering the walk.** However the payload + describes them, it says nothing about which arm to take or when a step + is finished — those you derive from the prose, exactly as before. A + described user who would happily stop talking is still not permission + to cut a loop the prose has not ended. - **Never silently repair, reinterpret, or improve the prose.** Execute what is written, even where it looks wrong. A broken instruction is the finding — the single most damaging thing you can do is quietly do the diff --git a/tests/prose/cases/investigation-gathers-symptoms/act.md b/tests/prose/cases/investigation-gathers-symptoms/act.md new file mode 100644 index 000000000..428e34278 --- /dev/null +++ b/tests/prose/cases/investigation-gathers-symptoms/act.md @@ -0,0 +1,4 @@ +Execute skills/workflow-investigation-entry/SKILL.md with arguments +$0=bugfix, $1=crash-fix, and continue into the processing skill it hands +off to. Stop once the prose turns from the user to the knowledge base — +do not query it. diff --git a/tests/prose/cases/investigation-gathers-symptoms/assert.md b/tests/prose/cases/investigation-gathers-symptoms/assert.md new file mode 100644 index 000000000..a94040837 --- /dev/null +++ b/tests/prose/cases/investigation-gathers-symptoms/assert.md @@ -0,0 +1,37 @@ +The prose should have taken this path: + +1. no investigation exists, so this is a fresh start — no resume choice + is put to the user +2. initialisation reads the work's seed and writes the investigation + file, placing what discovery already recorded into its Symptoms + section before anything is asked +3. **the interview happens anyway.** The section carrying the carrier's + account is not treated as symptoms already gathered — the seed is + where the questioning starts, not a reason to skip it +4. questioning opens broad and narrows on what the answers give back, + rather than putting the whole bank to the user at once +5. what the user says is written into the investigation file as it is + gathered, and committed +6. the questioning ends and the prose turns to the knowledge base, which + is where this walk stops + +Further claims: + +- nothing the user already said in discovery is put back to them as a + question — the carrier is read, not re-asked +- the user's ignorance is recorded as such: where they had no error + tracking, no logs, and no better date than last week, the file says so + rather than leaving those parts of the template as placeholders +- the analysis is untouched: no hypothesis, no code trace, no root cause. + Nothing has been investigated yet, only described + +EXPECTED WORLD — from a work unit whose investigation had not begun: + +- an investigation file at `.workflows/crash-fix/investigation/crash-fix.md` + carrying the template's structure +- its Symptoms section holding both what discovery captured and what the + interview added — the digital-only basket, the staging reproduction, + the absence of logs or a tracking link +- its Analysis and Fix Direction sections still unwritten +- the investigation registered as in progress on the work unit +- no other phase touched diff --git a/tests/prose/cases/investigation-gathers-symptoms/case.json b/tests/prose/cases/investigation-gathers-symptoms/case.json new file mode 100644 index 000000000..271ac42d0 --- /dev/null +++ b/tests/prose/cases/investigation-gathers-symptoms/case.json @@ -0,0 +1,32 @@ +{ + "origin": "the interview the carrier was suppressing — symptom gathering must run even though initialisation has already written into the section its guard reads", + "entry": "workflow-investigation-entry", + "world": "claims", + "files": [ + "skills/workflow-investigation-entry/SKILL.md", + "skills/workflow-investigation-entry/references/invoke-skill.md", + "skills/workflow-investigation-process/SKILL.md", + "skills/workflow-investigation-process/references/initialize-investigation.md", + "skills/workflow-investigation-process/references/template.md", + "skills/workflow-investigation-process/references/symptom-gathering.md", + "skills/workflow-shared/references/seed-context.md", + "skills/workflow-knowledge/references/knowledge-usage.md" + ], + "conduct": "The user is relaying this bug rather than living it. Two customers hit it last week and they reproduced it once on staging with a digital-only basket; beyond that they are working from the report. They answer concretely about what they have actually seen, and say plainly when they do not know — they have no error-tracking link, no logs to hand, and nothing better than \"sometime last week\" for when it started. They do not volunteer detail past the question asked, and they do not speculate about the cause; that is what they think the investigation is for.", + "invariants": { + "engine_before_write": true, + "calls_include": [ + "manifest get crash-fix.investigation.crash-fix status", + "topic start crash-fix investigation crash-fix" + ], + "calls_in_order": [ + "manifest get crash-fix.investigation.crash-fix status", + "topic start crash-fix investigation crash-fix" + ], + "calls_exclude": [ + "topic complete", + "knowledge.cjs index", + "topic reopen" + ] + } +} diff --git a/tests/prose/cases/investigation-gathers-symptoms/fixture-state.cjs b/tests/prose/cases/investigation-gathers-symptoms/fixture-state.cjs new file mode 100644 index 000000000..de3d3ec54 --- /dev/null +++ b/tests/prose/cases/investigation-gathers-symptoms/fixture-state.cjs @@ -0,0 +1,14 @@ +'use strict'; + +// The bug is captured and the investigation has not begun — the same +// starting world the initialisation case uses. This one carries on +// through the interview that follows. + +const m = require('../../mainlines/bugfix.cjs'); + +module.exports = { + build(h) { + m.init(h); + m.create(h); + }, +}; diff --git a/tests/prose/cases/investigation-gathers-symptoms/fixture.md b/tests/prose/cases/investigation-gathers-symptoms/fixture.md new file mode 100644 index 000000000..957efa34c --- /dev/null +++ b/tests/prose/cases/investigation-gathers-symptoms/fixture.md @@ -0,0 +1,6 @@ +A bugfix shaped in discovery and routed to investigation. The work unit +exists with its session log; nothing in the investigation phase has been +started, and no investigation file exists. + +The bridge has just written its plan file and the context was cleared — +this session opens cold at the entry skill. diff --git a/tests/prose/cases/investigation-gathers-symptoms/fixture/.claude/settings.json b/tests/prose/cases/investigation-gathers-symptoms/fixture/.claude/settings.json new file mode 100644 index 000000000..ae019ede5 --- /dev/null +++ b/tests/prose/cases/investigation-gathers-symptoms/fixture/.claude/settings.json @@ -0,0 +1,9 @@ +{ + "showClearContextOnPlanAccept": true, + "permissions": { + "allow": [ + "Edit(.workflows/**)", + "Bash(mv .workflows/:*)" + ] + } +} diff --git a/tests/prose/cases/investigation-gathers-symptoms/fixture/.recipe-hash b/tests/prose/cases/investigation-gathers-symptoms/fixture/.recipe-hash new file mode 100644 index 000000000..887dd3c5c --- /dev/null +++ b/tests/prose/cases/investigation-gathers-symptoms/fixture/.recipe-hash @@ -0,0 +1 @@ +abfb1370246e172ddcb3fce5ec26fde9e10bd78028e98a3d2aaad9796adf1d86 diff --git a/tests/prose/cases/investigation-gathers-symptoms/fixture/.workflows/.state/migrations b/tests/prose/cases/investigation-gathers-symptoms/fixture/.workflows/.state/migrations new file mode 100644 index 000000000..3c7162acc --- /dev/null +++ b/tests/prose/cases/investigation-gathers-symptoms/fixture/.workflows/.state/migrations @@ -0,0 +1,52 @@ +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/investigation-gathers-symptoms/fixture/.workflows/_gitignore.fixture b/tests/prose/cases/investigation-gathers-symptoms/fixture/.workflows/_gitignore.fixture new file mode 100644 index 000000000..779cbf28a --- /dev/null +++ b/tests/prose/cases/investigation-gathers-symptoms/fixture/.workflows/_gitignore.fixture @@ -0,0 +1,2 @@ +.cache/ +.manifest.json.*.tmp diff --git a/tests/prose/cases/investigation-gathers-symptoms/fixture/.workflows/crash-fix/discovery/sessions/session-001.md b/tests/prose/cases/investigation-gathers-symptoms/fixture/.workflows/crash-fix/discovery/sessions/session-001.md new file mode 100644 index 000000000..6dc56d4e3 --- /dev/null +++ b/tests/prose/cases/investigation-gathers-symptoms/fixture/.workflows/crash-fix/discovery/sessions/session-001.md @@ -0,0 +1,40 @@ +# 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/investigation-gathers-symptoms/fixture/.workflows/crash-fix/manifest.json b/tests/prose/cases/investigation-gathers-symptoms/fixture/.workflows/crash-fix/manifest.json new file mode 100644 index 000000000..eaaa56199 --- /dev/null +++ b/tests/prose/cases/investigation-gathers-symptoms/fixture/.workflows/crash-fix/manifest.json @@ -0,0 +1,8 @@ +{ + "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/investigation-gathers-symptoms/fixture/.workflows/manifest.json b/tests/prose/cases/investigation-gathers-symptoms/fixture/.workflows/manifest.json new file mode 100644 index 000000000..1cd498ed5 --- /dev/null +++ b/tests/prose/cases/investigation-gathers-symptoms/fixture/.workflows/manifest.json @@ -0,0 +1,7 @@ +{ + "work_units": { + "crash-fix": { + "work_type": "bugfix" + } + } +} diff --git a/tests/prose/lib/cases.cjs b/tests/prose/lib/cases.cjs index 1293e452c..014bad3cb 100644 --- a/tests/prose/lib/cases.cjs +++ b/tests/prose/lib/cases.cjs @@ -9,7 +9,8 @@ // // case.json the values code branches on: // { origin, entry, files[], answers[], -// stubs{name: trigger}, world: "claims" (optional), +// conduct (optional), 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 @@ -111,6 +112,7 @@ function loadCase(id) { dir, rel: path.relative(ROOT, dir), metaError, + rawMeta: meta, origin: meta.origin || null, entry: meta.entry || null, files: (meta.files || []).map((spec) => { @@ -120,6 +122,7 @@ function loadCase(id) { : { path: spec.slice(0, hash).trim(), anchor: spec.slice(hash + 1).trim() }; }), answers: meta.answers || [], + conduct: meta.conduct || null, worldMode: meta.world || null, invariants: meta.invariants || null, stubs: Object.entries(meta.stubs || {}).map(([name, trigger]) => ({ name, trigger })), @@ -220,6 +223,9 @@ function validateCorpus(cases) { errors.push(`${at}: no ${FILES.fixtureState} — a case with no world records no ` + 'actions and produces no delta, so nothing it claims can be answered'); } + if ('conduct' in (c.rawMeta || {}) && !c.conduct) { + errors.push(`${at}: ${FILES.meta} conduct is empty — describe the user or omit it`); + } if (!c.act) errors.push(`${at}: no ${FILES.act}`); if (!c.assert) errors.push(`${at}: no ${FILES.assert} — the expected trace is what catches a silent repair`); diff --git a/tests/prose/lib/prompts.cjs b/tests/prose/lib/prompts.cjs index bbc70688c..40c306da8 100644 --- a/tests/prose/lib/prompts.cjs +++ b/tests/prose/lib/prompts.cjs @@ -55,12 +55,13 @@ function indent(text, by = ' ') { return text.split('\n').map((l) => (l.length ? by + l : l)).join('\n'); } -function walkerPrompt({ worldDir, situation, task, scope, stubs, answers }) { +function walkerPrompt({ worldDir, situation, task, scope, stubs, answers, conduct }) { const t = loadTemplate('walker'); const parts = [fill(t.world, { world_dir: worldDir })]; if (situation) parts.push(fill(t.situation, { situation })); parts.push(fill(t.task, { task, scope })); if (answers) parts.push(fill(t.answers, { answers })); + if (conduct) parts.push(fill(t.conduct, { conduct })); if (stubs.length) { const entries = stubs.map((s) => fill(t['stub-entry'], { name: s.name, diff --git a/tests/prose/prompts/walker.md b/tests/prose/prompts/walker.md index f1017f38d..cd0fe5173 100644 --- a/tests/prose/prompts/walker.md +++ b/tests/prose/prompts/walker.md @@ -33,6 +33,23 @@ SCOPE — the prose under walk: SCRIPTED USER ANSWERS — consume in order, one per question the prose asks: {{answers}} +=== conduct === + +PLAYING THE USER — how they behave where the prose stops being scripted. + +A scripted answer covers a question with one right response. Some prose +does not work that way: it explores, and keeps exploring until it judges +it has enough. There is no fixed number of turns to script, so this says +what kind of person is on the other side instead. Answer as they would, +in their words, for as long as the prose keeps asking. + +It describes the user, never the walk. It does not tell you which arm to +take, when to stop, or what the prose ought to do — those are yours to +derive as always, and nothing here relieves you of following the prose +literally. + +{{conduct}} + === stubs === HARNESS SUBSTITUTIONS — these are NOT part of the process you are walking. diff --git a/tests/prose/run.cjs b/tests/prose/run.cjs index 9af456393..115267144 100644 --- a/tests/prose/run.cjs +++ b/tests/prose/run.cjs @@ -120,6 +120,7 @@ function cmdPrompt(argv) { .join('\n'), stubs: c.stubs.map((s) => ({ ...s, ...cases.readStub(s.name) })), answers: c.answers.map((a, i) => ` ${i + 1}. ${a}`).join('\n'), + conduct: c.conduct, })); }