voyage/docs/operations.md
Kjell Tore Guttormsen 27d586841a
fix(stats): event-emit lands without the env var and reports every failure
event-emit returned {written:false, reason:'CLAUDE_PLUGIN_DATA unset'} whenever the harness did not export the variable - the normal case for the Bash tool - so /trekexecute's Phase 8 events never landed. It now resolves the directory through stats-append's resolveStatsDir (one resolver; opts.path still wins, so intent-approval is unchanged). The CLI uses the realpath isMain() guard (the file:// guard was a silent no-op under a symlink or a path with a space) and exits 1 on every written:false - usage error, payload-not-json, failed write - instead of always 0. KNOWN_EVENTS gains main-merge-approved, main-merge-declined and gates_mode_flag_override, which /trekexecute emits by design. Header, intent-approval docstring (comment only), docs/operations.md and the README autonomy sentence (wrong state names, 'records each transition') updated.

Red: 10 of 20 tests fail on the pre-fix code - 7 new (CLI lands with CLAUDE_PLUGIN_DATA empty / absent, unwritable target exit 1, usage error exit 1, symlink, space path, known events) and 3 inverted contracts. Regression guards (green on arrival): never throws when unwritable; no second copy of the fallback directory name.

Inverted (the contract this changes, not deleted): emit with no CLAUDE_PLUGIN_DATA (silent skip) -> lands under a pinned HOME; never throws when unwritable -> a genuinely unwritable target (env {} now resolves to the real home); resolveStatsPath({}) === null -> the fallback path; malformed --payload exit 0 -> exit 1.

Readers checked: otel-export keys on 'event', yardstick counts only user_input, cache-analyzer and research-loop-cap name event-emit only in comments. Verify 268/268 (stats-event-emit, intent-approval, doc-consistency, otel-export-validators, yardstick). Suite 1376 (1374/0/2); non-hook lines in the real trekexecute-stats.jsonl 60 before and after.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:35:11 +02:00

7.5 KiB

Voyage — Autonomy gates, profile system, observability

Imported from CLAUDE.md via pointer.

Autonomy mode (--gates, v3.4.0)

All four pipeline commands accept a boolean --gates {true|false} flag. Presence (--gates true, or bare --gates) turns gating on — the run pauses at autonomy boundaries for operator confirmation. Absence (or --gates false, the default) runs phases continuously without pausing.

/trekexecute additionally refines how strict the gating is via a gates_mode policy derived from the brief's effort signal (an explicit operator --gates flag takes precedence over the brief signal — see commands/trekexecute.md § High-effort behavior (v5.1.1)):

gates_mode Derived from (effort) Behavior
adaptive standard / absent (default) Stop only at meaningful boundaries (manifest-audit FAIL, plan-critic BLOCKER, main-merge gate)
closed high Stop at every autonomy boundary; operator confirms each transition
open low Skip optional checkpoints; trust manifests + verify gates only

Under the hood: lib/util/autonomy-gate.mjs runs the state machine idle → gates_on | auto_running → paused_for_gate → completed. start routes to gates_on when --gates true, else auto_running; a phase_boundary from gates_on pauses at paused_for_gate (awaiting resume); finish reaches the terminal completed. The module is pure data with no I/O. Separately, lib/stats/event-emit.mjs records named lifecycle events (brief-approved, main-merge-gate, main-merge-approved, main-merge-declined, gates_mode_flag_override, user_input) to trekexecute-stats.jsonl in the plugin data directory (CLAUDE_PLUGIN_DATA, else ~/.claude/plugins/data/voyage-ktg-plugin-marketplace/, the directory scripts/yardstick.mjs reads); a write that fails is reported, never skipped. The main-merge gate is the final autonomy boundary before HEAD lands on main.

Path A/B/C decision (v3.4.0; Path C closed 2026-05-05)

Three architectural options were considered for the speedup work:

  • Path A — cache-first (drop --allowedTools per child to recover cross-phase cache sharing): REJECTED. Inverts the security model; plugin hooks don't fire reliably in claude -p (research/06 GH #36071).
  • Path B — sequential --no-ff parallel waves with manifest-driven failure recovery: CHOSEN. Ships in v3.4.0. Phase 2.6 of /trekexecute runs the wave executor with hardenings for plugin-in-monorepo + gitignored-state topology.
  • Path C — hybrid (cache-warm sentinel + identical-tool parallel): CLOSED 2026-05-05. Q3 experiment measured median cache_creation_input_tokens = 163,903 across 3 fork-children at 186K parent context (CC v2.1.128, Sonnet 4.6). Master-plan thresholds: ≤ 1,500 POSITIVE / ≥ 3,500 NEGATIVE. Result is solidly NEGATIVE — CLAUDE_CODE_FORK_SUBAGENT does not preserve cache prefix across identical-tool children at our context size. Path C migration is deferred indefinitely; reassessment is appropriate when CC v2.2.xxx ships fork-cache-relevant features. Harness: scripts/q3-cache-prefix-experiment.mjs. Companion analyser: lib/stats/cache-analyzer.mjs.

Status (6.0.0): /trekexecute no longer starts wave children. Phase 2.6 prepares one wave at a time and hands each session over as an order the user runs in a session they open (CHANGELOG, 6.0.0). The Q3 harness was removed in 6.0.0 because it started the binary; its result above stands, and the script remains in git history before that tag.

A revived Path C (post-v2.2.xxx) would require: (1) re-architecting tool-list to be identical across all wave children, (2) cache-telemetry analysis confirming the new fork-cache behaviour holds, (3) prompt-level deny re-enablement to compensate for tool scoping rollback.

Profile system (--profile, v4.1.0)

Four built-in model profiles plus operator-defined <custom>.yaml. Each profile pins phase_models for the six pipeline phases (brief, research, plan, execute, review, continue). Profile is recorded in plan.md frontmatter as profile: <name> and emitted to ${CLAUDE_PLUGIN_DATA}/trek*-stats.jsonl for cost-attribution.

Profile Brief Research Plan Execute Review Continue Use case
economy sonnet sonnet sonnet sonnet sonnet sonnet ⚠ Experimental (uncalibrated Jaccard floor) — lowest cost; high-confidence small-scope tasks (operator-opt-in via --profile economy)
balanced sonnet sonnet opus sonnet opus sonnet Mixed — opus where reasoning depth pays off (operator-opt-in via --profile balanced)
premium (default) opus opus opus opus opus opus Maximum quality — Opus on every phase. Default since 2026-05-13 operator request; also the hardcoded resolver default returned by resolveProfile() in lib/profiles/resolver.mjs
fable fable fable fable fable fable fable Max quality — Fable 5.1 (the most capable model in Claude Code) on every phase (operator-opt-in via --profile fable); reasoning effort inherits from the session — see docs/profiles.md §Model & effort axes

Lookup order

  1. Explicit --profile <name> flag passed to the command
  2. Plan-file frontmatter profile: (when resuming via /trekexecute --resume or /trekcontinue)
  3. VOYAGE_PROFILE environment variable
  4. Default premium

Custom profiles

Create voyage-profiles/<custom>.yaml in the repo root (or ~/.claude/voyage-profiles/<custom>.yaml) to define a new tier — the name must not be a built-in. The validator (lib/validators/profile-validator.mjs) enforces: every phase_models[].phase must be a known phase enum; every phase_models[].model must exactly match an entry in BASE_ALLOWED_MODELS (['sonnet', 'opus', 'fable']; haiku only with VOYAGE_ALLOW_HAIKU=1). findProfilePath (lib/profiles/resolver.mjs) resolves built-in first (lib/profiles/<name>.yaml for economy/balanced/premium/fable), then repo-root voyage-profiles/, then ~/.claude/voyage-profiles/. A custom file named after a built-in therefore cannot shadow it (custom profiles must use new names); for the same custom name, repo-root takes precedence over home.

Drift between plan-frontmatter profile: and step-manifest profile_used: emits a MANIFEST_PROFILE_DRIFT warning from plan-validator --strict (Step 20). Plan remains valid; the warning surfaces accidental tier-mismatch.

Observability (Stop hook, v4.1.0)

The Stop hook in hooks/hooks.json runs hooks/scripts/otel-export.mjs at session-end. The hook is opt-in — when VOYAGE_EXPORT_MODE is unset or off, no work is done.

Mode Output Endpoint env-var
off (default) (no export) —
textfile voyage.prom (Prometheus exposition format) VOYAGE_TEXTFILE_DIR
otlp OTLP/JSON POST VOYAGE_OTEL_ENDPOINT

Endpoint validation: VOYAGE_OTEL_ALLOW_PRIVATE=1 is required to send to loopback or RFC1918 destinations (CWE-918 SSRF mitigation). Allowlist lib/exporters/field-allowlist.mjs redacts records before export (CWE-212). Path validation (lib/exporters/path-validator.mjs) rejects symlink + traversal (CWE-22).

Local Docker Compose stack: examples/observability/. Operator docs: docs/observability.md. Both pin minimum versions per CVE history (prom/prometheus:v3.0.1, grafana/grafana:11.4.0, otel/opentelemetry-collector-contrib:0.115.0).