fix(voyage): S23 — make /trekplan Phase 9 dedup executable (defect #1)
Phase 9's dedup hand-off was broken on two layers, both surfaced by the S22
dogfood: (a) plan-critic + scope-guardian (Read/Glob/Grep, no Write) were told
to write /tmp/*-out.json the dedup helper reads — they cannot; (b) even with
Write, their Output format emits markdown, not the helper's
{agent,findings:[{file,line,rule_key,text}]} schema. readJsonOrNull then
swallowed the absent files -> a silent empty merge that discarded every finding.
Fix (operator-chosen A'): keep the reviewers read-only; make the hand-off run.
- plan-review-dedup.mjs gains a --stdin mode reading {plan_critic,scope_guardian};
malformed stdin exits non-zero so a broken hand-off surfaces loudly instead of
collapsing into a silent empty merge. File mode + its tests are untouched.
- plan-critic.md + scope-guardian.md now emit a trailing machine-readable `json`
findings block (the inline hand-off; no Write tool needed).
- trekplan.md + planning-orchestrator.md Phase 9 rewritten in lockstep: extract
both blocks, pipe via heredoc into --stdin. No temp files, portable, no
pathguard dependency.
TDD: malformed-stdin test failed first (CLI ignored stdin -> exit 0 = the bug),
green after impl. New S23 doc-pin asserts both docs use --stdin (not the dead
/tmp paths) and both agents declare the json block. Suite 724 (722/2/0); live
HEAD baseline was 720, not the stale 705 STATE carried forward.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqBYc8Ltrk7LipyJmGxXiB
This commit is contained in:
parent
a366e332b7
commit
b4edc12bec
7 changed files with 196 additions and 30 deletions
|
|
@ -9,12 +9,18 @@
|
|||
//
|
||||
// Provenance is preserved on the surviving finding's `raised_by` array.
|
||||
//
|
||||
// CLI shim:
|
||||
// node lib/review/plan-review-dedup.mjs \
|
||||
// --plan-critic /tmp/x.json --scope-guardian /tmp/y.json
|
||||
// CLI shim — two input modes:
|
||||
// file mode: node lib/review/plan-review-dedup.mjs \
|
||||
// --plan-critic /tmp/x.json --scope-guardian /tmp/y.json
|
||||
// stdin mode: node lib/review/plan-review-dedup.mjs --stdin (reads fd 0:
|
||||
// one object {plan_critic, scope_guardian} of agent payloads)
|
||||
// → stdout: deduped JSON, exit 0 on success.
|
||||
//
|
||||
// Empty / missing inputs are tolerated (single-agent review still works).
|
||||
// File mode tolerates empty / missing inputs (single-agent review still works).
|
||||
// stdin mode is the Phase-9 path: the read-only reviewers (plan-critic,
|
||||
// scope-guardian) cannot write temp files, so /trekplan pipes their inline JSON
|
||||
// blocks here. Malformed stdin exits NON-ZERO — a broken hand-off must surface
|
||||
// loudly, never collapse into a silent empty merge.
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { jaccardSimilarity, meetsThreshold } from '../parsers/jaccard.mjs';
|
||||
|
|
@ -138,6 +144,7 @@ function parseArgs(argv) {
|
|||
if (a === '--plan-critic') out.planCritic = argv[++i];
|
||||
else if (a === '--scope-guardian') out.scopeGuardian = argv[++i];
|
||||
else if (a === '--threshold') out.threshold = Number(argv[++i]);
|
||||
else if (a === '--stdin') out.stdin = true;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
|
@ -151,12 +158,46 @@ function readJsonOrNull(path) {
|
|||
}
|
||||
}
|
||||
|
||||
// --stdin mode reads ONE object {plan_critic, scope_guardian} from fd 0. Unlike
|
||||
// the file path mode (which tolerates absent files so single-agent review still
|
||||
// works), malformed stdin is a hard error: --stdin means the caller intended to
|
||||
// pipe both inline review blocks, so a parse failure is a broken hand-off that
|
||||
// must surface loudly — not collapse into a silent empty merge (the original
|
||||
// Phase-9 defect: read-only reviewers never wrote the temp files, the helper
|
||||
// swallowed the absence, and the dedup ran on nothing).
|
||||
function readStdinSourcesOrExit() {
|
||||
let raw;
|
||||
try {
|
||||
raw = readFileSync(0, 'utf-8');
|
||||
} catch (err) {
|
||||
process.stderr.write(`plan-review-dedup --stdin: cannot read stdin: ${err.message}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch (err) {
|
||||
process.stderr.write(`plan-review-dedup --stdin: malformed JSON on stdin: ${err.message}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
process.stderr.write('plan-review-dedup --stdin: expected an object {plan_critic, scope_guardian}\n');
|
||||
process.exit(1);
|
||||
}
|
||||
return [
|
||||
{ agent: 'plan-critic', payload: parsed.plan_critic ?? null },
|
||||
{ agent: 'scope-guardian', payload: parsed.scope_guardian ?? null },
|
||||
];
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const sources = [
|
||||
{ agent: 'plan-critic', payload: readJsonOrNull(args.planCritic) },
|
||||
{ agent: 'scope-guardian', payload: readJsonOrNull(args.scopeGuardian) },
|
||||
];
|
||||
const sources = args.stdin
|
||||
? readStdinSourcesOrExit()
|
||||
: [
|
||||
{ agent: 'plan-critic', payload: readJsonOrNull(args.planCritic) },
|
||||
{ agent: 'scope-guardian', payload: readJsonOrNull(args.scopeGuardian) },
|
||||
];
|
||||
const opts = {};
|
||||
if (Number.isFinite(args.threshold)) opts.threshold = args.threshold;
|
||||
const result = dedupFindings(sources, opts);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue