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:
parent
2b5d5f72bc
commit
2fe6feb74d
6 changed files with 134 additions and 61 deletions
|
|
@ -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 ---
|
||||
|
|
|
|||
2
tests/fixtures/jsonl-schemas.md
vendored
2
tests/fixtures/jsonl-schemas.md
vendored
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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) ---
|
||||
|
|
|
|||
|
|
@ -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.');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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\}\}/);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue