fix(execute): run the brief check in Phase 1, and say what effort really changes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-22 16:07:27 +02:00
commit 2fe6feb74d
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
6 changed files with 134 additions and 61 deletions

View file

@ -48,6 +48,22 @@ Parse `$ARGUMENTS` for mode flags:
use that path but keep `project_dir` set (user may be pointing at a
recovery session spec inside the project).
- Continue parsing remaining flags (they combine with `--project`).
- Brief check, once every flag below is parsed: when `{dir}/brief.md`
exists, run the two lines `/trekplan` runs on it (its Phase 1), for the
execute phase; append `--profile {profile}` when the operator passed
`--profile`:
```bash
node ${CLAUDE_PLUGIN_ROOT}/lib/validators/brief-validator.mjs --soft --json "{dir}/brief.md"
node ${CLAUDE_PLUGIN_ROOT}/lib/profiles/resolver.mjs --resolve-phase-model --phase execute --brief-path "{dir}/brief.md" [--profile {profile}] --json
```
The validator exits 1 when its `errors` are not empty; read its JSON
either way. Halt only when the validator's `errors` hold
`BRIEF_V51_MISSING_SIGNALS`, with the message from "Sequencing gate
surface" below. Any other error is printed in one line and the run
continues. A validator or resolver that cannot run (no JSON) is "not
measured" in one line, and the run continues. Capture the resolver's
JSON as `phase_signal_result` (Composition rule below). No
`{dir}/brief.md`: skip both lines.
2. If arguments contain `--fg`: extract the file path. Set **mode = foreground**.
3. If arguments contain `--resume`: extract the file path. Set **mode = resume**.
4. If arguments contain `--dry-run`: extract the file path. Set **mode = dry-run**.
@ -449,6 +465,7 @@ Determine how to execute this plan:
- `--step N` mode
- `--resume` mode
- `--session N` mode (runs only that session's steps, foreground)
- `phase_signal_result.effort == 'low'` → one session, steps in order — never while a wave is in flight (`WAVE_IN_FLIGHT`)
- Plan has no `## Execution Strategy` section
- Plan has Execution Strategy with only 1 session
@ -2986,7 +3003,7 @@ model_for_phase = brief.phase_signals[<phase>]?.model ?? profile.phase_models[
The brief signal wins per-phase when present; the profile fills any
gaps. Both fields are mechanically resolved by the single composed CLI,
invoked in Phase 2.4 alongside the sequencing-gate brief-validator call:
invoked in Phase 1 alongside the sequencing-gate brief-validator call:
```bash
# v5.9 — composed phase-model resolution (brief > profile > default) for the
@ -2998,21 +3015,18 @@ node ${CLAUDE_PLUGIN_ROOT}/lib/profiles/resolver.mjs --resolve-phase-model --pha
```
`phase_signal_result.effort` (absent = `standard`) is consumed when picking the execution
strategy (gates auto-escalation, parallel-wave choice — see High-effort
behavior below). The resolver does NOT control the orchestrator's own
strategy (the parallel-wave choice, Phase 2.5). The resolver does NOT control the orchestrator's own
model — that is fixed at invocation time (command frontmatter omits
`model:`, so it follows the session model) and cannot be switched mid-turn.
`/trekexecute` spawns no swarm: one step helper at a time and one controller
(Hard Rule 10), and both run `opus` — the step agent's pin and the
controller's `model: "opus"` — so `phase_signal_result.model` still has no
spawn site here; it is returned for cross-command uniformity and stats.
spawn site here; it is returned for cross-command uniformity; no stats record carries it.
For `/trekexecute` specifically: `effort == 'low'` activates `--gates open`
+ sequential-only execution (no worktree-isolated parallel waves — runs
all sessions in a single foreground loop). `effort == 'standard'` (or
absent) → no change (default execution strategy applies). `effort == 'high'`
activates the high-effort behavior documented under `### High-effort
behavior (v5.1.1)` below.
For `/trekexecute` specifically: `effort == 'low'` runs the plan as one
session, step after step (Phase 2.5). `effort == 'standard'` (or absent) →
no change (default execution strategy applies). `effort == 'high'`: see
`### High-effort behavior (v5.1.1)` below.
### Sequencing gate surface
@ -3026,32 +3040,9 @@ commands surface, don't re-enforce.
### High-effort behavior (v5.1.1)
When `phase_signal_result.effort == 'high'` for the `execute` phase,
set `gates_mode = 'closed'` automatically (equivalent to passing
`--gates closed` on the command line). All autonomy boundaries become
operator-stopping points: manifest-audit FAIL halts immediately, the
main-merge gate stays a lock whose one question is the guard's (Phase 8), and Phase 7.5
manifest-audit runs in its strictest form (fails on ANY mismatch, not
just deltas above threshold). Operator explicit `--gates` flag still
wins over the brief signal — if the operator passes `--gates open` on
the command line while `phase_signal_result.effort == 'high'`, the
flag takes precedence and the override is logged:
```bash
node ${CLAUDE_PLUGIN_ROOT}/lib/stats/event-emit.mjs \
--event gates_mode_flag_override --payload '{"flag":"--gates","signal_effort":"high"}'
```
The record lands in `trekexecute-stats.jsonl` in the plugin data directory as
`{ts, event, known_event, payload:{flag, signal_effort}}`. The two payload keys
stay in the jsonl; the OTel export allowlist drops them by omission. On
`"written": false` (exit 1), print its `reason` in one line and continue — the
override record never blocks the run.
Standard effort (or absent): use `gates_mode = 'adaptive'` (the
default). Low effort: `gates_mode = 'open'` plus sequential-only
execution (no parallel waves) — existing `--quick`-equivalent
code-path.
High effort changes nothing in `/trekexecute`: the step helper and the
controller already run opus, and every gate already runs. No `gates_mode` is
set from the effort.
## Hard rules

View file

@ -1,6 +1,7 @@
// tests/commands/trekexecute.test.mjs
// v5.1 prose-pin tests + v5.1.1 runtime SC4 + SC7 tests for /trekexecute.
// Plan Assumption 2 locks low-effort to --gates open + sequential-only.
// Low effort runs the plan as one session, step after step; it sets no --gates
// (fix round 4-5a, #33).
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
@ -33,13 +34,14 @@ test('trekexecute — sequencing-gate surface mentions BRIEF_V51_MISSING_SIGNALS
'/trekexecute must reference phase_signals (v5.1 composition rule)');
});
test('trekexecute — low-effort path references --gates open + sequential', () => {
test('trekexecute — low-effort path runs one session, step after step, and sets no --gates open', () => {
const text = read();
const compIdx = text.indexOf('## Composition rule (v5.1)');
assert.ok(compIdx >= 0, 'Composition rule (v5.1) section missing');
const section = text.slice(compIdx, compIdx + 2000);
assert.match(section, /--gates open/, 'Low-effort path must mention --gates open');
assert.match(section, /sequential/, 'Low-effort path must mention sequential-only execution');
assert.doesNotMatch(section, /--gates open/, 'Low effort must not set --gates open');
assert.match(section.replace(/\s+/g, ' '), /`effort == 'low'` runs the plan as one session, step after step \(Phase 2\.5\)/,
'Low-effort path must run the plan as one session, step after step');
});
// --- v5.1.1 runtime SC4 + SC7 ---

View file

@ -22,7 +22,7 @@
| trekbrief-stats | ts, task, slug, mode, interview_turns, review_iterations, brief_quality, research_topics, auto_research, auto_result, intent_approved, project_dir, order_id, order_claimed_at, order_reason | commands/trekbrief.md (orchestrator-emit Phase 7; order_* added by lib/stats/stats-append.mjs) | trekbrief.md:1140-1164 | profile, phase_models, profile_source | none |
| trekresearch-stats | ts, question, mode, scope, engine, slug, project_dir, brief_path, dimensions, dimensions_baseline, dimensions_baseline_preserved, effort, conv_turns, empty_turns, unique_sources, agents_local, agents_external, gemini_used, confidence, contradictions, open_questions | lib/stats/stats-append.mjs (trekresearch.md Stats tracking) | trekresearch.md:806-839 | profile, phase_models, parallel_agents, external_research_enabled, profile_source | none |
| trekplan-stats | ts, task, mode, slug, brief_path, project_dir, codebase_size, codebase_files, agents_deployed, deep_dives, research_briefs_used, research_scout_used, critic_verdict, guardian_verdict, outcome | lib/stats/stats-append.mjs (trekplan.md Phase 12) | trekplan.md:1305-1333 | profile, phase_models, parallel_agents, profile_source | none |
| trekexecute-stats (Phase 9 record) | ts, plan, plan_type, mode, result, steps_total, steps_passed, steps_failed, steps_skipped, failed_at_step | lib/stats/stats-append.mjs (trekexecute.md Phase 9) | trekexecute.md:2926-2948 | profile, phase_models, profile_source | none |
| trekexecute-stats (Phase 9 record) | ts, plan, plan_type, mode, result, steps_total, steps_passed, steps_failed, steps_skipped, failed_at_step | lib/stats/stats-append.mjs (trekexecute.md Phase 9) | trekexecute.md:2943-2965 | profile, phase_models, profile_source | none |
| trekexecute-stats (autonomy events) | ts, event, known_event, payload | lib/stats/event-emit.mjs `emit()` | event-emit.mjs:78-99 | payload.profile, payload.phase_models, payload.profile_source | none |
| trekexecute-stats (PostToolUse Bash) | ts, session_id, command_excerpt, duration_ms, success | hooks/scripts/post-bash-stats.mjs (Bash PostToolUse) | post-bash-stats.mjs:42-54 | none (hook is plugin-level, not profile-aware) | command_excerpt (CWE-212) |
| trekreview-stats | ts, slug, verdict, counts (BLOCKER/MAJOR/MINOR/SUGGESTION), reviewed_files_count, mode, duration_ms | lib/stats/stats-append.mjs (trekreview.md Phase 8) | trekreview.md:636-686 | profile, phase_models, profile_source | none |

View file

@ -849,7 +849,7 @@ test('v5.1.1 — commands/trekreview.md contains ### High-effort behavior (v5.1.
test('v5.1.1 — commands/trekexecute.md contains ### High-effort behavior (v5.1.1) sub-section', () => {
const t = read('commands/trekexecute.md');
assert.match(t, /^### High-effort behavior \(v5\.1\.1\)$/m,
'trekexecute.md must contain ### High-effort behavior (v5.1.1) sub-section under Composition rule (gates_mode = closed)');
'trekexecute.md must contain ### High-effort behavior (v5.1.1) sub-section under Composition rule (high effort changes nothing in /trekexecute)');
});
// --- v5.4 — brief.md formalized as PUBLIC CONTRACT (Handover 1) ---

View file

@ -17,7 +17,8 @@
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { existsSync, readFileSync, readdirSync } from 'node:fs';
import { copyFileSync, existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process';
@ -698,3 +699,89 @@ test('[#30] sibling: README.md /trekexecute Profile row keeps the helper and the
assert.doesNotMatch(rows[0], /Pin model profile for execution/);
assert.ok(rows[0].includes(`| (v4.1.0) Profile for this run's resolver; ${SAME_ON_OPUS}.`), rows[0]);
});
// --- #33 ---------------------------------------------------------------------------
// /trekexecute promised a brief check and an effort-driven gates mode that no
// line ran. Phase 1's --project branch now runs the two lines /trekplan runs,
// for the execute phase, and halts only on BRIEF_V51_MISSING_SIGNALS (a brief
// edited since planning does not stop a --resume on any other error). Low
// effort means one session, step after step; high effort changes nothing.
/** Phase 1's rule 1 (the --project branch), as lines. */
function projectRule() {
const sec = section(read(EXEC), '## Phase 1 — Parse mode and validate input').split('\n');
const start = sec.findIndex((l) => l.startsWith('1. If arguments contain `--project <dir>`'));
assert.ok(start !== -1, 'Phase 1 must still open with the --project rule');
const end = sec.findIndex((l, i) => i > start && /^2\. /.test(l));
assert.ok(end !== -1, 'Phase 1 must still have a rule 2');
return sec.slice(start, end).join('\n');
}
test('[#33] Phase 1 --project runs brief-validator --soft --json and the execute resolver, and halts only on BRIEF_V51_MISSING_SIGNALS', () => {
const rule = projectRule();
const validator = line(rule, 'validators/brief-validator.mjs', '--soft --json');
const resolver = line(rule, 'profiles/resolver.mjs', '--resolve-phase-model --phase execute');
assert.equal(validator, 'node ${CLAUDE_PLUGIN_ROOT}/lib/validators/brief-validator.mjs --soft --json "{dir}/brief.md"');
assert.equal(resolver,
'node ${CLAUDE_PLUGIN_ROOT}/lib/profiles/resolver.mjs --resolve-phase-model --phase execute --brief-path "{dir}/brief.md" [--profile {profile}] --json');
const f = flat(rule);
for (const s of [
'The validator exits 1 when its `errors` are not empty; read its JSON either way.',
'Halt only when the validator\'s `errors` hold `BRIEF_V51_MISSING_SIGNALS`, with the message from "Sequencing gate surface" below.',
'Any other error is printed in one line and the run continues.',
'A validator or resolver that cannot run (no JSON) is "not measured" in one line, and the run continues.',
'Capture the resolver\'s JSON as `phase_signal_result` (Composition rule below).',
'No `{dir}/brief.md`: skip both lines.',
]) assert.ok(f.includes(s), `the --project rule is missing: ${s}`);
// The two lines run as printed: the validator names the one halting code for a
// 2.1 brief without signals, and the resolver hands Phase 2.5 its low effort.
const dir = mkdtempSync(join(tmpdir(), 'fix-33-'));
try {
copyFileSync(join(ROOT, 'tests', 'fixtures', 'brief-v21-no-signals.md'), join(dir, 'brief.md'));
const v = bash(validator.split('{dir}').join(dir), ROOT);
assert.equal(v.status, 1, `validator: ${v.stdout}${v.stderr}`);
assert.ok(JSON.parse(v.stdout).errors.some((e) => e.code === 'BRIEF_V51_MISSING_SIGNALS'), v.stdout);
copyFileSync(join(ROOT, 'tests', 'fixtures', 'brief-effort-low.md'), join(dir, 'brief.md'));
const r = bash(resolver.split('{dir}').join(dir).replace(' [--profile {profile}]', ''), ROOT);
assert.equal(r.status, 0, `resolver: ${r.stdout}${r.stderr}`);
assert.equal(JSON.parse(r.stdout).effort, 'low');
} finally { rmSync(dir, { recursive: true, force: true }); }
});
test('[#33] the Composition rule says the resolver runs in Phase 1, and no line says Phase 2.4 runs it', () => {
assert.doesNotMatch(flat(read(EXEC)), /invoked in Phase 2\.4/);
assert.ok(flat(section(read(EXEC), '## Composition rule (v5.1)')).includes('invoked in Phase 1 alongside the sequencing-gate brief-validator call'),
'the Composition rule does not say the resolver is invoked in Phase 1');
});
test('[#33] sibling: Composition rule has no "uniformity and stats" and no gates auto-escalation', () => {
const f = flat(section(read(EXEC), '## Composition rule (v5.1)'));
assert.doesNotMatch(f, /uniformity and stats/, 'the Composition rule still says the model is returned for stats');
assert.ok(f.includes('it is returned for cross-command uniformity; no stats record carries it.'),
'the Composition rule does not say no stats record carries the model');
assert.doesNotMatch(f, /gates auto-escalation/);
});
test('[#33] Phase 2.5 names low effort as one session, steps in order, never while a wave is in flight', () => {
const sec = section(read(EXEC), '## Phase 2.5 — Execution strategy decision').split('\n');
const start = sec.findIndex((l) => l.startsWith('**Run as single session (foreground)** when ANY of these are true:'));
assert.ok(start !== -1, 'Phase 2.5 must still list when it runs as a single session');
const end = sec.findIndex((l, i) => i > start && l.trim() === '');
const bullets = sec.slice(start + 1, end);
assert.ok(bullets.includes("- `phase_signal_result.effort == 'low'` → one session, steps in order — never while a wave is in flight (`WAVE_IN_FLIGHT`)"),
`the single-session list does not name low effort: ${bullets.join(' | ')}`);
});
test('[#33] no gates_mode = \'closed\', "strictest" or --event gates_mode_flag_override is left, and high effort says it changes nothing', () => {
const text = read(EXEC);
const f = flat(text);
assert.doesNotMatch(f, /gates_mode = 'closed'/);
assert.doesNotMatch(f, /strictest/);
assert.doesNotMatch(f, /halts immediately/);
assert.doesNotMatch(text, /--event gates_mode_flag_override/);
assert.doesNotMatch(f, /gates_mode = '(adaptive|open)'/);
assert.equal(flat(section(text, '### High-effort behavior (v5.1.1)')),
'### High-effort behavior (v5.1.1) High effort changes nothing in `/trekexecute`: the step helper and the controller '
+ 'already run opus, and every gate already runs. No `gates_mode` is set from the effort.');
});

View file

@ -339,9 +339,9 @@ test('CLI: stats-append writes when its path contains a space', () => {
});
// ---- /trekexecute: every telemetry write lands through a writer ----------------
// Phase 8's three event-emit calls, Phase 9's stats record and the
// gates_mode_flag_override record. Phase 9 said "skip silently"; the override was
// prose with no writer; Phase 8's payloads spliced ${PLAN_PATH} (set in an earlier
// Phase 8's three event-emit calls and Phase 9's stats record; the
// gates_mode_flag_override record is gone, since no effort sets gates_mode (#33).
// Phase 9 said "skip silently"; Phase 8's payloads spliced ${PLAN_PATH} (set in an earlier
// Bash call that does not survive) and ${WAVE_COUNT} (never assigned) into JSON,
// so a literal run printed payload-not-json. Every block runs here in a fresh
// shell with NOTHING exported, CLAUDE_PLUGIN_DATA empty and HOME pinned.
@ -431,9 +431,10 @@ test('Phase 8: a plan path with a quote character lands literally and the shell
}
});
test('gates_mode_flag_override: a written:false is printed in one line and the run continues', () => {
test('gates_mode_flag_override: the Composition rule holds no override record to report on', () => {
const flat = section(TE, 'Composition rule (v5.1)').replace(/\s+/g, ' ');
assert.match(flat, /On `"written": false` \(exit 1\), print its `reason` in one line and continue/);
assert.doesNotMatch(flat, /--event gates_mode_flag_override/, 'no effort sets gates_mode, so there is no override to log');
assert.doesNotMatch(flat, /override record never blocks the run/);
});
test('Phase 8: a written:false is printed in one line and the run continues', () => {
@ -443,18 +444,10 @@ test('Phase 8: a written:false is printed in one line and the run continues', ()
assert.match(flat, /never chained with `&&`/);
});
test('gates_mode_flag_override is written through event-emit with the documented shape', () => {
test('gates_mode_flag_override: /trekexecute holds no --event gates_mode_flag_override block', () => {
const sec = section(TE, 'Composition rule (v5.1)');
const blocks = fencedBlocksWith(sec, '--event gates_mode_flag_override');
assert.equal(blocks.length, 1, 'the override needs a writer, not a prose target');
assert.match(sec, /\{ts, event, known_event, payload:\{flag, signal_effort\}\}/);
const home = mkdtempSync(join(tmpdir(), 'stats-landing-'));
try {
const r = runBlock(blocks[0], home);
assert.equal(r.status, 0, r.stdout + r.stderr);
const [rec] = records(join(home, DATA_REL, 'trekexecute-stats.jsonl'));
assert.deepEqual(Object.keys(rec).sort(), ['event', 'known_event', 'payload', 'ts']);
assert.equal(rec.known_event, true);
assert.deepEqual(Object.keys(rec.payload).sort(), ['flag', 'signal_effort']);
} finally { rmSync(home, { recursive: true, force: true }); }
assert.deepEqual(fencedBlocksWith(sec, '--event gates_mode_flag_override'), [],
'high effort sets no gates_mode, so no override is written');
assert.deepEqual(fencedBlocksWith(read(TE), 'gates_mode_flag_override'), []);
assert.doesNotMatch(sec, /\{ts, event, known_event, payload:\{flag, signal_effort\}\}/);
});