8.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 --gates [true|false|closed|open]. A bare --gates, --gates true or --gates closed turns gating on; --gates false, --gates open or no flag (the default) leaves it off. The next token is the flag's value only when it is one of those four; any other token stays the command's own argument, so /trekplan --gates --project <dir> and /trekexecute --gates plan.md are a bare --gates. Each command maps the value once through lib/util/autonomy-gate.mjs; a shim that fails falls back to the same mapping in prose, and the run goes on. With gating on, /trekbrief pauses before its auto chain, /trekresearch after each topic, and /trekplan after exploration, synthesis and adversarial review. /trekexecute accepts the flag and pauses on nothing: its one stop is the merge into main, which always asks (below).
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 is bare, true or closed, 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; the commands run only its start transition, to map the flag. Separately, lib/stats/event-emit.mjs records named lifecycle events (brief-approved, main-merge-gate, main-merge-approved, main-merge-declined, 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.
The main-merge gate is a lock, and the guard asks the one question (operator decision 2026-10-08). The gate opens only when the run is completed and the review's verdict gate (lib/review/verdict-gate.mjs, K7) exits 0; BLOCK (1) and "could not decide" (2) keep it closed. When it opens, /trekexecute runs the merge into main itself and pre-bash-executor answers "ask" before it runs, in auto mode too, so that question is the only one. In the permission mode where Claude Code shows no prompts, the guard refuses the merge instead, and every other call it would ask about (push, tag or publish, destructive git), since no one can be asked (operator decisions 2026-10-09). The executor no longer asks "ready to merge?" first, and --gates no longer adds a pause before the wave merge-back: the guard asks there as well, once per session, because the push and the merge go in one call. Before this, one merge cost two questions in three places: the --gates pause before the wave merge-back, the gate's own question after the waves were merged, and the gate's question on the single-session path followed by the guard's at the merge itself.
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
--allowedToolsper child to recover cross-phase cache sharing): REJECTED. Inverts the security model; plugin hooks don't fire reliably inclaude -p(research/06 GH #36071). - Path B — sequential
--no-ffparallel waves with manifest-driven failure recovery: CHOSEN. Ships in v3.4.0. Phase 2.6 of/trekexecuteruns 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_SUBAGENTdoes 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):
/trekexecuteno 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>.
| 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
- Explicit
--profile <name>flag passed to the command - Plan-file frontmatter
profile:(when resuming via/trekexecute --resumeor/trekcontinue) VOYAGE_PROFILEenvironment variable- 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).