From 6a797c8eac44d30d55967f22f9dd411d980412a8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 28 Sep 2026 16:38:17 +0000 Subject: [PATCH] Include persisted stage stderr in the saved-case review. aas inspect and the Markdown review dropped the child diagnostic the run report already stored. --- CHANGELOG.md | 3 +++ bin/case-review.mjs | 6 +++--- test/stack.test.mjs | 23 +++++++++++++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9853f33..1db3447 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ project adheres to [Semantic Versioning](https://semver.org/). - Playwright setup note in the Quick start section of `README.md`. ### Fixed +- Saved-case review now includes persisted stage stderr. `aas inspect` and + the Markdown review omitted the diagnostic that the run report already + stored, so a failed act looked like it had no child output. - `runAct` now rejects a domain other than `refund` or `inventory` before it spawns the rail CLI. Any other value was passed through as the demo command. diff --git a/bin/case-review.mjs b/bin/case-review.mjs index f10848c..5111827 100644 --- a/bin/case-review.mjs +++ b/bin/case-review.mjs @@ -1,7 +1,7 @@ import { createHash } from "node:crypto"; import { readdirSync } from "node:fs"; import { join } from "node:path"; -import { assertRunsDirectory, DEFAULT_PATHS, UsageError, exportRunBundle, isValidRunId, summarizeRun } from "./aas.mjs"; +import { assertRunsDirectory, clipChildStderr, DEFAULT_PATHS, UsageError, exportRunBundle, isValidRunId, summarizeRun } from "./aas.mjs"; export function listCasePage({ outputRoot = DEFAULT_PATHS.outputRoot, before = null, limit = 25, domain = null, outcome = null, search = null } = {}) { if (!Number.isInteger(limit) || limit < 1 || limit > 50) throw new Error("History limit must be an integer from 1 to 50."); @@ -66,7 +66,7 @@ export function inspectCase(runId, options = {}) { created_at: bundle.manifest.created_at ?? null, domain: report.domain ?? null, requested_options: report.requested_options ?? null, policy_failures: policyFailures, - stages: ['decide','act','prove'].map(name => ({ name, status: bundle.manifest.stages[name]?.status ?? 'unknown', reason: bundle.manifest.stages[name]?.reason ?? null, code: bundle.manifest.stages[name]?.code ?? null, artifact_available: Object.hasOwn(bundle.stages,name) })), + stages: ['decide','act','prove'].map(name => ({ name, status: bundle.manifest.stages[name]?.status ?? 'unknown', reason: bundle.manifest.stages[name]?.reason ?? null, code: bundle.manifest.stages[name]?.code ?? null, stderr: clipChildStderr(bundle.manifest.stages[name]?.stderr) || null, artifact_available: Object.hasOwn(bundle.stages,name) })), policy_id: report.stages?.decide?.policy_id ?? null, action_id: bundle.stages.act?.action_id ?? null, outcome: report.stages?.act?.outcome ?? null, review_verdict: review?.verdict ?? null, @@ -104,7 +104,7 @@ export function renderCaseMarkdown(review) { for (const rule of review.policy_failures?.rules ?? []) lines.push('- '+markdownText(rule.rule_id)+': '+markdownText(rule.path)+'; '+markdownText(rule.kind)+'; '+markdownText(rule.reason_code)); if (review.policy_failures?.omitted) lines.push(review.policy_failures.omitted+' additional failures omitted; inspect the decide artifact for all records.'); lines.push('', ...stageHeading); - for(const stage of review.stages??[]) lines.push('- '+markdownText(stage.name)+': '+markdownText(stage.status)+'; artifact '+(stage.artifact_available?'present':'absent')+'; reason '+markdownText(stage.reason)+'; code '+markdownText(stage.code)); + for(const stage of review.stages??[]) lines.push('- '+markdownText(stage.name)+': '+markdownText(stage.status)+'; artifact '+(stage.artifact_available?'present':'absent')+'; reason '+markdownText(stage.reason)+'; code '+markdownText(stage.code)+(stage.stderr?'; stderr '+markdownText(stage.stderr):'')); lines.push('', '## Evidence binding', '', '- Recorded digest: '+markdownText(review.recorded_evidence_digest), '- Recomputed digest: '+markdownText(review.recomputed_evidence_digest), '- Digests match: '+markdownText(review.digest_matches), '', '## Component revisions', ''); for(const entry of review.component_provenance??[]) lines.push('- '+markdownText(entry.name)+': '+markdownText(entry.commit)); lines.push('', '## Verification next step', '', '- Readiness: '+markdownText(review.verification_readiness?.state), '- Reason: '+markdownText(review.verification_readiness?.reason), '- Next step: '+markdownText(review.verification_readiness?.next_step)); diff --git a/test/stack.test.mjs b/test/stack.test.mjs index b45cc80..8638c4f 100644 --- a/test/stack.test.mjs +++ b/test/stack.test.mjs @@ -2077,6 +2077,29 @@ test('case review handoffs include requested settings and bounded policy failure const markdown=renderCaseMarkdown(review);assert.match(markdown,/## Requested settings/);assert.match(markdown,/domain: inventory/);assert.match(markdown,/not\\_equal/);assert.match(markdown,/5 additional failures omitted/);assert.doesNotMatch(markdown,/OMIT_RAW/); }); +test("case review includes persisted stage stderr", async () => { + const { inspectCase, renderCaseMarkdown } = await import("../bin/case-review.mjs"); + const outputRoot = tempRoot(); + const runId = "stderr-review"; + persistRunBundle({ + outputRoot, + runId, + report: { run_id: runId, stages: {} }, + stages: { + act: { status: "failed", stderr: "crctl: ", raw: { outcome: null } }, + }, + componentProvenance: [], + exitCode: 1, + }); + const review = inspectCase(runId, { outputRoot }); + const act = review.stages.find((stage) => stage.name === "act"); + assert.equal(act.stderr, "crctl: "); + const markdown = renderCaseMarkdown(review); + assert.match(markdown, /stderr crctl: <boom>/); + assert.doesNotMatch(markdown, //); + assert.equal(review.stages.find((stage) => stage.name === "decide").stderr, null); +}); + test('case reviews explain verification readiness without claiming receipt verification', async () => { const {inspectCase,renderCaseMarkdown}=await import('../bin/case-review.mjs'); const outputRoot=tempRoot(),rail={synthetic:true},digest='sha256:'+createHash('sha256').update(JSON.stringify(rail)).digest('hex');