Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .claude/agents/prose-asserter.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ the actions.

Return exactly this and nothing else:

0. **Model** — the model named on the recorded `SubagentStop` line, quoted
as recorded. If no such line exists, say `unrecorded`. A verdict is
only as trustworthy as the model that produced the walk, so it is
stated before anything is judged, never inferred, and never omitted.
1. **Path** — one line per expected step: PASS or FAIL, each PASS quoting
the transcript line that shows it, each FAIL stating what is missing
or contradicting.
Expand Down
3 changes: 2 additions & 1 deletion .claude/agents/prose-orchestrator.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: prose-orchestrator
description: Runs one prose-test case end to end — builds the world, dispatches the walker, computes the delta, dispatches the asserter, escalates a failure to Opus, destroys the world — and returns just the verdict. Dispatched by the /prose-test skill, one per case.
description: Runs one prose-test case end to end — builds the world, dispatches the walker, computes the delta, dispatches the asserter, reruns a failure from a fresh world to confirm it, destroys the world — and returns just the verdict. Dispatched by the /prose-test skill, one per case.
tools: Bash, Read, Agent
model: sonnet
hooks:
Expand Down Expand Up @@ -90,6 +90,7 @@ Return exactly this and nothing else:

```
CASE: <case-id>
MODEL: <the model the asserter reported, or `unrecorded`>
VERDICT: PASS | FAIL | FLAKY | INVALID | HARNESS ERROR
PATH: <n>/<total> steps passed
WORLD: PASS | FAIL
Expand Down
10 changes: 8 additions & 2 deletions .claude/skills/prose-test/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,14 @@ about an expected result can leak into a later dispatch.
## Step 3: Collate

Report a verdict table from what the orchestrators returned: case id,
verdict, path steps passed, world, markers. Quote the evidence line for
every failure.
model, verdict, path steps passed, world, markers. Quote the evidence
line for every failure.

The model column comes from the harness record, not from any agent's
say-so. An edited agent definition does not reach a running session until
its plugins are reloaded, so a run can silently use the previous model —
if the column disagrees with what the agent definitions declare, say so
and treat every verdict in the run as unproven.

A `FLAKY` verdict means a failure did not reproduce on a second run from
a fresh world — surface both runs, resolve neither.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"build": "node build/knowledge.build.js",
"knowledge:repl": "node scripts/knowledge-repl.js",
"typecheck": "tsc -p tsconfig.json",
"test": "node --test tests/scripts/test-render.cjs tests/scripts/test-engine-gateway.cjs tests/scripts/test-engine-epic-projections.cjs tests/scripts/test-engine-start-projections.cjs tests/scripts/test-engine-workunit-projections.cjs tests/scripts/test-engine-discussion-map.cjs tests/scripts/test-engine-discovery-map.cjs tests/scripts/test-engine-tasks.cjs tests/scripts/test-engine-render-surfaces.cjs tests/scripts/test-engine-transactions.cjs tests/scripts/test-engine-workunit-create.cjs tests/scripts/test-engine-workunit-pivot.cjs tests/scripts/test-engine-workunit-absorb.cjs tests/scripts/test-engine-workunit-promote.cjs tests/scripts/test-engine-discovery-session.cjs tests/scripts/test-engine-cache.cjs tests/scripts/test-engine-manifest-io.cjs tests/scripts/test-engine-manifest-fields.cjs tests/scripts/test-engine-boot.cjs tests/scripts/test-migration-orchestrator.cjs tests/scripts/test-migration-047.cjs tests/scripts/test-migration-048.cjs tests/scripts/test-migration-049.cjs tests/scripts/test-migration-050.cjs tests/scripts/test-migration-051.cjs tests/scripts/test-migration-052.cjs tests/scripts/test-engine-specification-projections.cjs tests/scripts/test-engine-discovery-projections.cjs tests/scripts/test-gateway-for-start.cjs tests/scripts/test-gateway-for-specification.cjs tests/scripts/test-gateway-for-workflow-continue-epic.cjs tests/scripts/test-gateway-for-workflow-continue-feature.cjs tests/scripts/test-gateway-for-workflow-continue-bugfix.cjs tests/scripts/test-gateway-for-workflow-continue-quickfix.cjs tests/scripts/test-gateway-for-workflow-continue-cross-cutting.cjs tests/scripts/test-gateway-for-discovery-skill.cjs tests/scripts/test-gateway-for-bridge.cjs tests/scripts/test-reads-derivations.cjs tests/scripts/test-ensure-discovery-item.cjs tests/scripts/test-legacy-research-split.cjs tests/scripts/test-knowledge-chunker.cjs tests/scripts/test-knowledge-config.cjs tests/scripts/test-knowledge-discovery.cjs tests/scripts/test-knowledge-embeddings.cjs tests/scripts/test-knowledge-integration.cjs tests/scripts/test-knowledge-openai.cjs tests/scripts/test-knowledge-openai-compatible.cjs tests/scripts/test-knowledge-openai-integration.cjs tests/scripts/test-knowledge-progress.cjs tests/scripts/test-knowledge-rerank.cjs tests/scripts/test-knowledge-retry.cjs tests/scripts/test-knowledge-setup-forms.cjs tests/scripts/test-knowledge-store.cjs tests/scripts/test-conventions-lint.cjs tests/scripts/test-pipeline-simulation.cjs tests/scripts/test-engine-agent-state.cjs tests/scripts/test-prose-cases.cjs tests/scripts/test-prose-snapshots.cjs",
"test": "node --test tests/scripts/test-render.cjs tests/scripts/test-engine-gateway.cjs tests/scripts/test-engine-epic-projections.cjs tests/scripts/test-engine-start-projections.cjs tests/scripts/test-engine-workunit-projections.cjs tests/scripts/test-engine-discussion-map.cjs tests/scripts/test-engine-discovery-map.cjs tests/scripts/test-engine-tasks.cjs tests/scripts/test-engine-render-surfaces.cjs tests/scripts/test-engine-transactions.cjs tests/scripts/test-engine-workunit-create.cjs tests/scripts/test-engine-workunit-pivot.cjs tests/scripts/test-engine-workunit-absorb.cjs tests/scripts/test-engine-workunit-promote.cjs tests/scripts/test-engine-discovery-session.cjs tests/scripts/test-engine-cache.cjs tests/scripts/test-engine-manifest-io.cjs tests/scripts/test-engine-manifest-fields.cjs tests/scripts/test-engine-boot.cjs tests/scripts/test-migration-orchestrator.cjs tests/scripts/test-migration-047.cjs tests/scripts/test-migration-048.cjs tests/scripts/test-migration-049.cjs tests/scripts/test-migration-050.cjs tests/scripts/test-migration-051.cjs tests/scripts/test-migration-052.cjs tests/scripts/test-engine-specification-projections.cjs tests/scripts/test-engine-discovery-projections.cjs tests/scripts/test-gateway-for-start.cjs tests/scripts/test-gateway-for-specification.cjs tests/scripts/test-gateway-for-workflow-continue-epic.cjs tests/scripts/test-gateway-for-workflow-continue-feature.cjs tests/scripts/test-gateway-for-workflow-continue-bugfix.cjs tests/scripts/test-gateway-for-workflow-continue-quickfix.cjs tests/scripts/test-gateway-for-workflow-continue-cross-cutting.cjs tests/scripts/test-gateway-for-discovery-skill.cjs tests/scripts/test-gateway-for-bridge.cjs tests/scripts/test-reads-derivations.cjs tests/scripts/test-ensure-discovery-item.cjs tests/scripts/test-legacy-research-split.cjs tests/scripts/test-knowledge-chunker.cjs tests/scripts/test-knowledge-config.cjs tests/scripts/test-knowledge-discovery.cjs tests/scripts/test-knowledge-embeddings.cjs tests/scripts/test-knowledge-integration.cjs tests/scripts/test-knowledge-openai.cjs tests/scripts/test-knowledge-openai-compatible.cjs tests/scripts/test-knowledge-openai-integration.cjs tests/scripts/test-knowledge-progress.cjs tests/scripts/test-knowledge-rerank.cjs tests/scripts/test-knowledge-retry.cjs tests/scripts/test-knowledge-setup-forms.cjs tests/scripts/test-knowledge-store.cjs tests/scripts/test-conventions-lint.cjs tests/scripts/test-pipeline-simulation.cjs tests/scripts/test-engine-agent-state.cjs tests/scripts/test-prose-record-action.cjs tests/scripts/test-prose-cases.cjs tests/scripts/test-prose-snapshots.cjs",
"test:migrations": "for t in tests/scripts/test-migration-*.sh; do bash $t || exit 1; done",
"test:cli": "for t in tests/scripts/test-engine-manifest.sh tests/scripts/test-inbox-promotion.sh tests/scripts/test-knowledge-cli.sh tests/scripts/test-knowledge-build.sh; do bash $t || exit 1; done"
},
Expand Down
72 changes: 61 additions & 11 deletions tests/prose/lib/record-action.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,24 @@
//
// Every event is captured, not just the call: the intent before it
// (PreToolUse), the result after it (PostToolUse, with output), the
// failures (PostToolUseFailure), and the finish (Stop). Logs are
// throwaway — they live in the disposable world and die with it — so
// there is no reason to record less than everything.
// failures (PostToolUseFailure), and the finish (SubagentStop, carrying
// the model the walk actually ran on). Logs are throwaway — they live in
// the disposable world and die with it — so there is no reason to record
// less than everything.
//
// The model matters because an edited agent definition does not reach a
// running session until its plugins are reloaded. A walk can therefore
// run on a model nobody intended, and a verdict trusted at the wrong one
// is worse than no verdict. Recording it makes that visible in the
// result rather than something to remember.
//
// Self-scoping: a prose world lives at a temp path containing
// `prose-world-`. The payload is scanned for one, and anything happening
// outside a world is ignored. The asserter is the exception: it is
// contracted to use NO tools, so any tool call it makes is a contract
// violation and lands in a repo-local log instead of a world.
// outside a world is ignored. A stop event names no file, so its world
// and its model both come from the agent transcript the payload points
// at. The asserter is the exception: it is contracted to use NO tools,
// so any tool call it makes is a contract violation and lands in a
// repo-local log instead of a world.
//
// The log is written to <world>/.walk-actions.log, which collectTree()
// excludes, so recording never shows up as a world difference.
Expand Down Expand Up @@ -56,6 +65,34 @@ function summarise(input) {
return flatten(raw, 200);
}

/**
* The agent's own harness transcript — written by the runtime, not the
* agent. Authority on the model it ran on, and on which world it walked,
* neither of which a stop payload states.
*/
function fromTranscript(transcriptPath) {
const blank = { world: null, model: '' };
if (!transcriptPath) return blank;
let raw;
try {
raw = fs.readFileSync(transcriptPath, 'utf8');
} catch {
return blank;
}
const models = new Set();
for (const line of raw.split('\n')) {
try {
const model = JSON.parse(line)?.message?.model;
if (model) models.add(model);
} catch { /* a partial line is not worth failing the record over */ }
}
const found = raw.match(WORLD);
return {
world: found ? found[2].replace(/\\+/g, '') : null,
model: [...models].join(',') || '',
};
}

function main() {
const payload = read();
if (!payload) return;
Expand All @@ -71,26 +108,39 @@ function main() {
const projectDir = process.env.CLAUDE_PROJECT_DIR;
if (projectDir) {
try {
fs.appendFileSync(path.join(projectDir, VIOLATIONS),
const file = path.join(projectDir, VIOLATIONS);
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.appendFileSync(file,
`${agent}\t${event}\t${tool}\t${summarise(payload.tool_input)}\n`);
} catch { /* a hook must never break what it observes */ }
}
return;
}

// A stop event carries no tool input, so the world path that scopes
// every other event is absent from it — resolve it from the transcript.
const stop = event === 'Stop' || event === 'SubagentStop';
const traced = stop ? fromTranscript(payload.agent_transcript_path) : null;
const found = JSON.stringify(payload).match(WORLD);
if (!found) return;
const world = found[2].replace(/\\+/g, '');
if (!fs.existsSync(world)) return;
const world = found ? found[2].replace(/\\+/g, '') : traced && traced.world;
if (!world || !fs.existsSync(world)) return;

const parts = [event, tool, summarise(payload.tool_input).split(world).join('.')];
const parts = [
event,
tool,
stop
? traced.model || 'model-unknown'
: summarise(payload.tool_input).split(world).join('.'),
];

if (event === 'PostToolUse') {
parts.push(payload.tool_output_is_error ? 'ERROR' : 'ok');
parts.push(flatten(payload.tool_output, MAX_OUTPUT).split(world).join('.'));
} else if (event === 'PostToolUseFailure') {
parts.push('FAILED');
parts.push(flatten(payload.tool_output ?? payload.error, MAX_OUTPUT));
} else if (stop) {
parts.push(flatten(payload.last_assistant_message, MAX_OUTPUT).split(world).join('.'));
}

try {
Expand Down
Loading