diff --git a/commands/trekreview.md b/commands/trekreview.md index 288072d..a7f593c 100644 --- a/commands/trekreview.md +++ b/commands/trekreview.md @@ -52,6 +52,7 @@ FLAG_SCHEMA `trekreview` entry): | `--validate` | boolean | Schema-only check on existing `{project_dir}/review.md`. No LLM calls. | | `--dry-run` | boolean | Print the discovered scope and triage map. Skip writes. | | `--fg` | boolean | No-op alias (foreground is default). | +| `--workflow` | boolean | **(opt-in, NW2)** Run Phase 5–6 on the bake-off-validated Workflow substrate (`scripts/trekreview-armB.workflow.mjs`) instead of the default prose Agent-tool path. Requires **Claude Code 2.1.154+**. Combines with `--quick`. See *§ Phase 5–6 via the Workflow substrate*. | Resolution: 1. If `--project` is missing, print usage and stop: @@ -75,6 +76,13 @@ Set `mode`: - `quick` if `--quick` is set. - `default` otherwise. +Set `workflow_substrate` (orthogonal to `mode` — a substrate choice, not a behavior mode): +- `true` if `--workflow` is set — Phase 5–6 run on the Workflow substrate (see the Phase 5 + routing gate). The Workflow tool requires **Claude Code 2.1.154+**; if it is unavailable, + fall back to the prose path and note the fallback in the Executive Summary. +- `false` otherwise. **Default stays prose**: the substrate is opt-in, so the lower + portability floor of the prose path is preserved unless the operator opts in. + ## Phase 2 — Validate brief Run the brief validator in soft mode — the brief is upstream context, not @@ -181,6 +189,12 @@ If `mode == dry-run`: print the triage map and exit. ## Phase 5 — Launch parallel reviewers +**Substrate routing (opt-in `--workflow`).** When `workflow_substrate == true`, run +Phases 5–6 via the Workflow substrate documented in *§ Phase 5–6 via the Workflow +substrate* (below Phase 6), then resume at Phase 7 with the returned +`{verdict, findings}`. When `false` (the default), run the prose Agent-tool path +described in the rest of this phase. + Launch two reviewer agents **in parallel** via the Agent tool — one message, multiple tool calls. @@ -240,6 +254,54 @@ The coordinator's output is the full review.md content — frontmatter + body sections + trailing JSON block. Do NOT re-run the reviewers based on the coordinator's output. +## Phase 5–6 via the Workflow substrate (opt-in `--workflow`) + +Runs **only** when `workflow_substrate == true`. This is the **NW2 port**: it expresses +the SAME Phase 5–6 pipeline (parallel reviewers → triplet-dedup → coordinator verdict) as +a single Workflow, reusing the NW1 findings schema. The S10 bake-off found it +**fidelity-equivalent** to the prose path — see `docs/T2-bakeoff-results.md` (verdict +**POSITIVE**: verdict-match 1.0, issue-coverage 100%, `(file,rule_key)` jaccard ≥ +within-arm, tokens +4.4%). It stays **opt-in**, not the default, because the Workflow tool +raises the consumer floor to **Claude Code 2.1.154+** (outward-facing; the prose path +keeps the lower floor). + +Invoke the port via the **Workflow** tool with the Phase 1–4 output pinned into `args`: + +``` +Workflow({ + scriptPath: "${CLAUDE_PLUGIN_ROOT}/scripts/trekreview-armB.workflow.mjs", + args: { + briefPath: "{brief_path}", + diffPath: "{path to the unified diff file from Phase 3}", + triage: "{triage map as 'path → treatment' lines from Phase 4}", + quick: {true if mode == quick, else false} + } +}) +``` + +Contract (verified in S10 part B — follow exactly): +- **Pass `args` as a JSON object.** The script defensively re-parses a JSON *string*, but + the object form is the contract. +- **Reviewers are `StructuredOutput`-schema-forced** — `rule_key` is enum-enforced at the + tool layer (stronger than the prose path's post-hoc NW1 check), so there is no + `JSON.parse`/re-ask dance. +- **Recover the result from the `RESULT_JSON:{…}` line** inside the workflow output logs. + The script returns `{verdict, findings, ...}` AND logs it as that line; the + notification's `` may be truncated, so parse the logged line. +- The reviewer/coordinator agentTypes are namespaced inside the script + (`voyage:brief-conformance-reviewer`, `voyage:code-correctness-reviewer`, + `voyage:review-coordinator`). + +Then continue at **Phase 7** exactly as the prose path does — Phase 7 rendering, Phase 8 +validation, and the operator gate are **shared** and substrate-independent (both paths +return the same `{verdict, findings}` shape). + +**Known limitation (per bake-off §Posture, surfaced not hidden).** Classifier interference +was measured **0** at 9-agent concurrency in the session's default permission mode; an +explicit `auto`/`bypass`-mode re-run was not performed (the permission mode is operator-set, +not settable from within a session). trekreview's small fan-out showed 0 interference in S8 +and S10. The large fan-out case (the trekplan swarm) is out of NW2 scope. + ## Phase 7 — Write review.md Write the coordinator's output verbatim to: diff --git a/docs/command-modes.md b/docs/command-modes.md index f9e3256..2ff0c7e 100644 --- a/docs/command-modes.md +++ b/docs/command-modes.md @@ -72,6 +72,7 @@ If `{project_dir}/architecture/overview.md` exists (typically produced by an opt | `--validate` | Schema-only check on existing `{dir}/review.md`. No LLM calls | | `--dry-run` | Print discovered scope + triage map; skip writes | | `--fg` | No-op alias (foreground is default) | +| `--workflow` | (opt-in, NW2) Run Phase 5–6 on the bake-off-validated Workflow substrate (`scripts/trekreview-armB.workflow.mjs`) instead of the default prose path. Default stays prose; requires **Claude Code 2.1.154+** (raises the consumer floor — opt-in for portability). Fidelity-equivalent per `docs/T2-bakeoff-results.md` | | `--profile ` | (v4.1.0) Model profile for the review phase. | ## /trekcontinue modes diff --git a/lib/parsers/arg-parser.mjs b/lib/parsers/arg-parser.mjs index c843eb1..ab27e55 100644 --- a/lib/parsers/arg-parser.mjs +++ b/lib/parsers/arg-parser.mjs @@ -27,7 +27,7 @@ const FLAG_SCHEMA = { aliases: {}, }, trekreview: { - boolean: ['--quick', '--fg', '--dry-run', '--validate'], + boolean: ['--quick', '--fg', '--dry-run', '--validate', '--workflow'], valued: ['--project', '--since', '--profile'], aliases: {}, }, diff --git a/tests/commands/trekreview.test.mjs b/tests/commands/trekreview.test.mjs index e66ef00..bf00ab6 100644 --- a/tests/commands/trekreview.test.mjs +++ b/tests/commands/trekreview.test.mjs @@ -13,9 +13,11 @@ import { parseDocument } from '../../lib/util/frontmatter.mjs'; const HERE = dirname(fileURLToPath(import.meta.url)); const ROOT = join(HERE, '..', '..'); const COMMAND_FILE = join(ROOT, 'commands', 'trekreview.md'); +const MODES_DOC = join(ROOT, 'docs', 'command-modes.md'); const PHASE = 'review'; function read() { return readFileSync(COMMAND_FILE, 'utf8'); } +function readModes() { return readFileSync(MODES_DOC, 'utf8'); } function readFixture(name) { return readFileSync(join(ROOT, 'tests', 'fixtures', name), 'utf8'); } function frontmatterOf(text) { const doc = parseDocument(text); @@ -72,3 +74,50 @@ test('trekreview — SC7: brief_version 2.1 + no phase_signals + no partial → `sequencing gate must fire; errors=${JSON.stringify(r.errors)}`, ); }); + +// --- S11 (NW2 part B) — opt-in --workflow substrate routing --- + +test('trekreview — Phase 1 flag table documents the --workflow opt-in flag', () => { + const text = read(); + assert.match(text, /--workflow/, + 'commands/trekreview.md must document the --workflow opt-in flag (NW2 part B)'); +}); + +test('trekreview — --workflow routes Phase 5–6 to the validated NW2 Workflow port script', () => { + const text = read(); + assert.ok(text.includes('scripts/trekreview-armB.workflow.mjs'), + 'the --workflow route must reference the bake-off-validated NW2 Workflow port script'); +}); + +test('trekreview — Workflow path is opt-in; default stays prose (portability floor)', () => { + const text = read(); + assert.match(text, /opt-in/i, '--workflow must be described as opt-in'); + assert.ok(text.includes('Default stays prose'), + 'the command must state the default Phase 5–6 path stays prose (not Workflow)'); +}); + +test('trekreview — Workflow opt-in documents the Claude Code 2.1.154+ floor', () => { + const text = read(); + assert.match(text, /2\.1\.154/, + 'the --workflow path raises the consumer floor and must document Claude Code 2.1.154+'); +}); + +test('trekreview — bake-off evidence (fidelity-equivalent / T2 results) is cited for the opt-in', () => { + const text = read(); + assert.ok( + text.includes('T2-bakeoff-results.md') && /fidelity-equivalent/i.test(text), + 'the opt-in must cite the S10 bake-off (docs/T2-bakeoff-results.md) fidelity-equivalence verdict', + ); +}); + +test('command-modes.md — /trekreview table documents --workflow + its CC floor', () => { + const text = readModes(); + const start = text.indexOf('## /trekreview modes'); + assert.ok(start >= 0, 'command-modes.md missing "## /trekreview modes" section'); + const end = text.indexOf('\n## ', start + 1); + const section = text.slice(start, end === -1 ? undefined : end); + assert.match(section, /--workflow/, + 'command-modes.md /trekreview table must list the --workflow opt-in flag'); + assert.match(section, /2\.1\.154/, + 'command-modes.md /trekreview --workflow row must note the Claude Code 2.1.154+ floor'); +}); diff --git a/tests/lib/arg-parser.test.mjs b/tests/lib/arg-parser.test.mjs index 0a04956..f59c0b8 100644 --- a/tests/lib/arg-parser.test.mjs +++ b/tests/lib/arg-parser.test.mjs @@ -97,6 +97,23 @@ test('trekreview — unknown flag goes to unknown[]', () => { assert.ok(r.unknown.includes('--mystery')); }); +// --- S11 (NW2 part B) — opt-in --workflow substrate flag --- + +test('trekreview — --workflow boolean flag (NW2 opt-in substrate)', () => { + const r = parseArgs('--workflow', 'trekreview'); + assert.equal(r.flags['--workflow'], true); + assert.ok(!r.unknown.includes('--workflow'), + '--workflow must be a recognized trekreview flag, not unknown[]'); +}); + +test('trekreview — --workflow combines with --quick + --project', () => { + const r = parseArgs('--workflow --quick --project .claude/projects/x', 'trekreview'); + assert.equal(r.flags['--workflow'], true); + assert.equal(r.flags['--quick'], true); + assert.equal(r.flags['--project'], '.claude/projects/x'); + assert.deepEqual(r.unknown, []); +}); + test('trekcontinue — empty args produce no flags and no positional', () => { const r = parseArgs('', 'trekcontinue'); assert.equal(r.command, 'trekcontinue');