feat(voyage): S11 — NW2 part B, integrate opt-in --workflow flag

Make the bake-off-validated Workflow substrate (Arm B) reachable behind an
opt-in --workflow flag for /trekreview. Default Phase 5-6 path stays prose
to preserve the lower portability floor; --workflow raises the consumer
floor to Claude Code 2.1.154+ (the Workflow tool).

- arg-parser: --workflow added to trekreview boolean flags
- commands/trekreview.md: flag row + Phase 5 substrate-routing gate +
  new section 'Phase 5-6 via the Workflow substrate' (invocation contract,
  S10 gotchas, bake-off citation, auto/bypass residual as Known limitation)
- docs/command-modes.md: --workflow row in /trekreview table
- routes to existing scripts/trekreview-armB.workflow.mjs (byte-identical to
  the S10 part-B POSITIVE build); integration is pure routing, no script change

TDD: 8 new tests (arg-parser flag recognition + combine; command/doc prose
pins for route, opt-in posture, 2.1.154+ floor, bake-off evidence).
Suite 662 -> 670 (668 pass / 2 skip / 0 fail). plugin validate clean modulo
known root-CLAUDE.md warning. Resolves W1-narrow-wins-plan.md S11.
This commit is contained in:
Kjell Tore Guttormsen 2026-06-18 17:22:09 +02:00
commit 9fb536e2d8
5 changed files with 130 additions and 1 deletions

View file

@ -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 56 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 56 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 56 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 56 via the Workflow substrate documented in *§ Phase 56 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 56 via the Workflow substrate (opt-in `--workflow`)
Runs **only** when `workflow_substrate == true`. This is the **NW2 port**: it expresses
the SAME Phase 56 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 14 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 `<result>` 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:

View file

@ -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 56 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 <name>` | (v4.1.0) Model profile for the review phase. |
## /trekcontinue modes

View file

@ -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: {},
},

View file

@ -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 56 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 56 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');
});

View file

@ -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');