fix(execute): every /trekexecute telemetry write lands through a writer

Phase 9 said 'append to ${CLAUDE_PLUGIN_DATA}/trekexecute-stats.jsonl; if it is not set, skip silently' - the Bash tool env never carries it. It now runs stats-append.mjs trekexecute with the record on stdin, the /trekbrief Phase 7 shape, and reports a written:false. The gates_mode_flag_override record was prose with no writer; it now has an event-emit block (fixed literal payload: --gates is a boolean flag) and a documented shape {ts, event, known_event, payload:{flag, signal_effort}}. Phase 8's three event blocks spliced ${PLAN_PATH} (set in an earlier Bash call that does not survive) and ${WAVE_COUNT} (never assigned) into a double-quoted payload, so a literal run printed payload-not-json, and the decline reason was operator free text in a shell argument. Payloads are now single-quoted with {plan-path} / {wave-count} placeholders and the literal label hold-for-review; each call runs on its own and a written:false is printed and passed. docs/architecture.md and the jsonl-schemas rows (writer, measured line_refs) follow.

Red: 7 of 7 new tests fail on the pre-fix text (Phase 9 writer + no 'skip silently', Phase 9 record lands, no bare ${CLAUDE_PLUGIN_DATA}/...jsonl target, no shell expansion in --payload, the three Phase 8 blocks land known events when run as written in a fresh shell, the written:false sentence, the override writer and shape).
Verify 198/198 (stats-landing, main-merge-gate, gates-flag-coverage, profile-stats-fields, plugin-root-resolution, doc-consistency, no-claude-code-launch). Suite 1383 (1381/0/2).

Voyage order 20261005T104513Z-1250514346, plan step 3/5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-05 17:37:19 +02:00
commit d5d121150e
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
4 changed files with 144 additions and 16 deletions

View file

@ -1640,9 +1640,21 @@ boundary:
```bash
node ${CLAUDE_PLUGIN_ROOT}/lib/stats/event-emit.mjs \
--event main-merge-gate \
--payload "{\"plan\":\"${PLAN_PATH}\",\"wave_count\":${WAVE_COUNT}}"
--payload '{"plan":"{plan-path}","wave_count":{wave-count}}'
```
Fill in the placeholders yourself; the payload is single-quoted, so the shell
expands nothing in it (shell state from an earlier Bash call does not survive,
and an expanded variable is how this event used to print `payload-not-json`).
`{plan-path}` is the repo-relative plan path — a Voyage plan path
(`.claude/projects/<date>-<slug>/plan.md` or
`.claude/plans/trekplan-<date>-<slug>.md`) holds no quote character.
`{wave-count}` is the number of merged waves, `1` on the single-session path.
Run each event call as its own command, never chained with `&&`. The writer
prints `{"written": true, ...}`. On `"written": false` (exit 1), print its
`reason` in one line and continue — an event failure never blocks the gate.
Pause for operator confirmation via `AskUserQuestion`:
**Question:** "All waves merged. Ready to proceed with the main-merge step
@ -1662,7 +1674,7 @@ On confirm:
```bash
node ${CLAUDE_PLUGIN_ROOT}/lib/stats/event-emit.mjs \
--event main-merge-approved \
--payload "{\"plan\":\"${PLAN_PATH}\"}"
--payload '{"plan":"{plan-path}"}'
```
On the multi-session wave path (Phase 2.6), and only when `next-wave` said
@ -1678,9 +1690,13 @@ On decline:
```bash
node ${CLAUDE_PLUGIN_ROOT}/lib/stats/event-emit.mjs \
--event main-merge-declined \
--payload "{\"plan\":\"${PLAN_PATH}\",\"reason\":\"${reason}\"}"
--payload '{"plan":"{plan-path}","reason":"hold-for-review"}'
```
The reason is the literal label of the one decline option, never the
operator's free text (free text in a shell argument breaks the JSON and can be
expanded by the shell).
On decline, nothing is cleared: the merged-waves record is kept.
**Recovery surface:** on the single-session path, if declined,
@ -1865,9 +1881,13 @@ wave instead.
## Phase 9 — Stats tracking
Append one record to `${CLAUDE_PLUGIN_DATA}/trekexecute-stats.jsonl`:
Append one record to `trekexecute-stats.jsonl` in the plugin data directory.
Run it exactly like this. The record goes on stdin, and the writer resolves
the directory: `CLAUDE_PLUGIN_DATA`, which the Bash tool env does not carry,
else the directory `scripts/yardstick.mjs` reads.
```json
```bash
node ${CLAUDE_PLUGIN_ROOT}/lib/stats/stats-append.mjs trekexecute <<'JSON'
{
"ts": "{ISO-8601}",
"plan": "{filename only}",
@ -1880,10 +1900,14 @@ Append one record to `${CLAUDE_PLUGIN_DATA}/trekexecute-stats.jsonl`:
"steps_skipped": 0,
"failed_at_step": null
}
JSON
```
If `${CLAUDE_PLUGIN_DATA}` is not set or not writable, skip silently.
Never let stats failures block the workflow.
`steps_passed` counts the steps whose status is `completed`.
The writer prints `{"written": true, ...}`. On `"written": false` (exit 1),
print its `reason` in one line and continue. A stats failure never blocks the
workflow, but it is reported, not skipped silently.
## Profile (v4.1)
@ -1968,9 +1992,16 @@ 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 to
`${CLAUDE_PLUGIN_DATA}/trekexecute-stats.jsonl` as
`{event: 'gates_mode_flag_override', flag: 'open', signal_effort: 'high'}`.
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.
Standard effort (or absent): use `gates_mode = 'adaptive'` (the
default). Low effort: `gates_mode = 'open'` plus sequential-only

View file

@ -129,11 +129,13 @@ Stats:
- Research stats: `${CLAUDE_PLUGIN_DATA}/trekresearch-stats.jsonl`
- Continue stats: `${CLAUDE_PLUGIN_DATA}/trekcontinue-stats.jsonl`
The Bash tool env carries no `CLAUDE_PLUGIN_DATA`. Brief and plan stats are
written by `lib/stats/stats-append.mjs`, which falls back to
The Bash tool env carries no `CLAUDE_PLUGIN_DATA`. Brief, plan and execute
stats are written by `lib/stats/stats-append.mjs`, which falls back to
`~/.claude/plugins/data/voyage-ktg-plugin-marketplace/`, the directory
`scripts/yardstick.mjs` reads. A failed write is reported, not skipped.
The other commands' stats phases still name the variable in prose only.
`scripts/yardstick.mjs` reads. `/trekexecute`'s lifecycle events go through
`lib/stats/event-emit.mjs`, which resolves the same directory. A failed write
is reported, not skipped. The research, review and continue stats phases still
name the variable in prose only.
A brief record also carries `order_id`, `order_claimed_at` and `order_reason`:
the coord-queue order the run is working on, added by the writer from the
queue's claimed orders (`lib/stats/claimed-order.mjs`), never by the model.

View file

@ -22,8 +22,8 @@
| 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:961-976 | 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 | commands/trekresearch.md (orchestrator-emit Stats tracking) | trekresearch.md:634-676 | 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 | commands/trekplan.md (orchestrator-emit Phase 12) | trekplan.md:805-826 | 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 | commands/trekexecute.md (orchestrator-emit Phase 9) | trekexecute.md:1479-1494 | profile, phase_models, profile_source | none |
| trekexecute-stats (autonomy events) | ts, event, known_event, payload | lib/stats/event-emit.mjs `emit()` | event-emit.mjs:64-86 | payload.profile, payload.phase_models, payload.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:1882-1903 | profile, phase_models, profile_source | none |
| trekexecute-stats (autonomy events) | ts, event, known_event, payload | lib/stats/event-emit.mjs `emit()` | event-emit.mjs:77-98 | 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 | commands/trekreview.md (orchestrator-emit Phase 8) | trekreview.md:255 | profile, phase_models, profile_source | none |
| trekcontinue-stats | ts, project, next_session_label, status | commands/trekcontinue.md (orchestrator-emit Phase 5) | trekcontinue.md:289 | profile, profile_source | none |

View file

@ -336,3 +336,98 @@ test('CLI: stats-append writes when its path contains a space', () => {
assert.ok(existsSync(join(home, DATA_REL, 'trekplan-stats.jsonl')), `no record landed; stdout: "${r.stdout}"`);
} finally { rmSync(home, { recursive: true, force: true }); rmSync(spaced, { recursive: true, force: true }); }
});
// ---- /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
// 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.
const TE = 'commands/trekexecute.md';
/** The fenced bash blocks in `sec` that contain `needle`. */
function fencedBlocksWith(sec, needle) {
const blocks = [];
const re = /```bash\n([\s\S]*?)```/g;
let m;
while ((m = re.exec(sec)) !== null) if (m[1].includes(needle)) blocks.push(m[1]);
return blocks;
}
/** Fill the prose placeholders an executor fills in, then run the block. */
function runBlock(block, home) {
const cmd = block.split('${CLAUDE_PLUGIN_ROOT}').join(ROOT)
.split('{plan-path}').join('.claude/projects/2026-10-05-x/plan.md')
.split('{wave-count}').join('1');
return spawnSync('bash', ['-c', cmd], {
encoding: 'utf8',
env: { ...baseEnv(), CLAUDE_PLUGIN_DATA: '', HOME: home },
});
}
test('M6: /trekexecute Phase 9 writes through stats-append (code), never "skip silently" (prose)', () => {
appendLine(section(TE, 'Phase 9'), 'trekexecute', TE);
assert.doesNotMatch(read(TE), /skip (tracking )?silently/i, 'a silent skip is how the record never landed');
});
test('M6: with CLAUDE_PLUGIN_DATA empty the trekexecute record lands where the yardstick reads', () => {
const home = mkdtempSync(join(tmpdir(), 'stats-landing-'));
try {
const line = appendLine(section(TE, 'Phase 9'), 'trekexecute', TE);
const r = runLine(line, { ts: '2026-10-05T10:00:00Z', plan: 'plan.md', result: 'completed' }, home);
assert.equal(r.status, 0, r.stdout + r.stderr);
assert.equal(records(join(home, DATA_REL, 'trekexecute-stats.jsonl')).length, 1);
} finally { rmSync(home, { recursive: true, force: true }); }
});
test('trekexecute.md names no bare ${CLAUDE_PLUGIN_DATA}/...jsonl write target', () => {
const hits = read(TE).split('\n')
.map((l, i) => [i + 1, l]).filter(([, l]) => /\$\{CLAUDE_PLUGIN_DATA\}\/[\w-]+\.jsonl/.test(l));
assert.deepEqual(hits, [], 'every stats or event write goes through a writer that resolves the directory');
});
test('no --payload in trekexecute.md expands a shell variable', () => {
const hits = read(TE).split('\n')
.map((l, i) => [i + 1, l]).filter(([, l]) => /--payload\b.*\$\{/.test(l));
assert.deepEqual(hits, [], 'a payload built from shell state that does not survive between Bash calls never parses');
});
test('Phase 8: each event block, run as written, lands one known event', () => {
const blocks = fencedBlocksWith(section(TE, 'Phase 8'), 'event-emit.mjs');
assert.equal(blocks.length, 3, 'main-merge-gate, main-merge-approved, main-merge-declined');
for (const block of blocks) {
const home = mkdtempSync(join(tmpdir(), 'stats-landing-'));
try {
const r = runBlock(block, home);
assert.equal(r.status, 0, `${block}\n${r.stdout}${r.stderr}`);
assert.equal(JSON.parse(r.stdout.trim()).written, true, r.stdout);
const [rec] = records(join(home, DATA_REL, 'trekexecute-stats.jsonl'));
assert.equal(rec.known_event, true, rec.event);
} finally { rmSync(home, { recursive: true, force: true }); }
}
});
test('Phase 8: a written:false is printed in one line and the run continues', () => {
const sec = section(TE, 'Phase 8');
const flat = sec.replace(/\s+/g, ' ');
assert.match(flat, /On `"written": false` \(exit 1\), print its `reason` in one line and continue/);
assert.match(flat, /never chained with `&&`/);
});
test('gates_mode_flag_override is written through event-emit with the documented shape', () => {
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 }); }
});