diff --git a/CLAUDE.md b/CLAUDE.md index ede50c5..499c825 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,6 +53,8 @@ Full flag reference for each command (modes, `--gates`, `--profile`, breaking ch | contrarian-researcher | opus | Counter-evidence, overlooked alternatives | | gemini-bridge | opus | Gemini Deep Research second opinion (conditional) | +> **Inventory (S33 reconcile).** 24 agent files = **21 spawnable** (one of which, `synthesis-agent`, ships **dormant** — Δ≈0, wired to nothing, see `docs/T1-synthesis-poc-results.md`) **+ 3 orchestrator reference docs** (`planning-/research-/review-orchestrator` document the inline `/trek*` workflow; they are reference documentation, not spawnable capabilities). All 24 stay `model: opus` (operator pin `40d8742`); the glue/mechanical/retrieval/dormant roles (V09 gemini-bridge, V16 session-decomposer, V11 retrieval agents, V08 researchers, V35 synthesis) were reconsidered for a sonnet downgrade and **kept opus** — document-only, no frontmatter change (the decision record lives in `docs/voyage-vs-cc-balance-analysis.md` §10). + > **Model & effort (CC 2.1.154+).** `opus` resolves to **Opus 4.8** (default reasoning effort `high`); `sonnet` to Sonnet 4.6. Select agents carry native `effort:` — retrieval agents (`task-finder`, `git-historian`, `dependency-tracer`, `architecture-mapper`) at `medium`, adversarial-reasoning agents (`plan-critic`, `risk-assessor`, `contrarian-researcher`, `review-coordinator`) at `high`. This native per-spawn **reasoning** effort is a different axis from brief `phase_signals.effort` (orchestration shape — which agents/passes run). See `docs/profiles.md` §Model & effort axes. ## Reference docs (read on demand) diff --git a/README.md b/README.md index 3c31d96..57c899f 100644 --- a/README.md +++ b/README.md @@ -802,6 +802,7 @@ Top-level layout: ``` trekplan/ ├── agents/ 24 specialized agents (all `model: opus`-pinned; per-phase model set by `--profile`) +│ └ 21 spawnable (1 dormant: synthesis-agent, Δ≈0) + 3 orchestrator reference docs (not spawned) ├── commands/ 6 slash commands (trekbrief, trekresearch, trekplan, trekexecute, trekreview, trekcontinue) + trekendsession helper ├── templates/ Frontmatter templates for brief, research, plan, session, launch ├── hooks/ 7 hooks (pre-bash, pre-write, session-title, post-bash-stats, pre-compact-flush, post-compact-flush, otel-export) diff --git a/agents/planning-orchestrator.md b/agents/planning-orchestrator.md index bc2ff90..1603930 100644 --- a/agents/planning-orchestrator.md +++ b/agents/planning-orchestrator.md @@ -2,8 +2,10 @@ name: planning-orchestrator description: | Inline reference (v2.4.0) — documents the planning workflow that - /trekplan executes in main context. Historically not spawned as a - sub-agent: before Claude Code 2.1.172 the harness did not expose the + /trekplan executes in main context. + This file is a reference document, not a spawnable capability. + Historically not spawned as a sub-agent: before Claude Code 2.1.172 + the harness did not expose the Agent tool to sub-agents, so a background orchestrator could not spawn the exploration swarm (architecture-mapper, task-finder, plan-critic, etc.). As of CC 2.1.172 sub-agents can spawn sub-agents (up to 5 levels diff --git a/agents/research-orchestrator.md b/agents/research-orchestrator.md index 51cd682..04603c2 100644 --- a/agents/research-orchestrator.md +++ b/agents/research-orchestrator.md @@ -2,8 +2,10 @@ name: research-orchestrator description: | Inline reference (v2.4.0) — documents the research workflow that - /trekresearch executes in main context. Historically not spawned as - a sub-agent: before Claude Code 2.1.172 the harness did not expose the + /trekresearch executes in main context. + This file is a reference document, not a spawnable capability. + Historically not spawned as a sub-agent: before Claude Code 2.1.172 + the harness did not expose the Agent tool to sub-agents, so a background orchestrator could not spawn the research swarm. As of CC 2.1.172 sub-agents can spawn sub-agents (up to 5 levels deep), so a delegated-orchestration redesign is under diff --git a/agents/review-orchestrator.md b/agents/review-orchestrator.md index aa1e8bf..0751191 100644 --- a/agents/review-orchestrator.md +++ b/agents/review-orchestrator.md @@ -2,8 +2,10 @@ name: review-orchestrator description: | Inline reference (v3.2.0) — documents the review workflow that - /trekreview executes in main context. Historically not spawned - as a sub-agent: before Claude Code 2.1.172 the harness did not expose + /trekreview executes in main context. + This file is a reference document, not a spawnable capability. + Historically not spawned as a sub-agent: before Claude Code 2.1.172 + the harness did not expose the Agent tool to sub-agents, so a background orchestrator could not spawn the reviewer swarm (brief-conformance-reviewer, code-correctness-reviewer, review-coordinator). As of CC 2.1.172 diff --git a/docs/observability.md b/docs/observability.md index 1eea08b..cc0fe36 100644 --- a/docs/observability.md +++ b/docs/observability.md @@ -114,6 +114,40 @@ The exporter is hardened against three CWE classes: | `prom/node-exporter` | `1.10.2` | textfile collector path normalization | | `grafana/grafana` | `11.4.0` | datasource provisioning hardening | +## Why direct export rather than a native collector + +A balance review (`docs/voyage-vs-cc-balance-analysis.md` §4, V32) asked +whether the custom exporters should be dropped in favour of pointing the +standard `OTEL_*` environment variables at a co-located OTLP collector, +letting that collector own egress and field selection. The operator +decision (D2, 2026-06-20) is to **keep direct export**. The rationale is +the security boundary, not a preference for re-hosting a collector: + +- **The three guards run in-process, before any byte leaves Voyage.** + `path-validator.mjs` (CWE-22), `endpoint-validator.mjs` (CWE-918 / SSRF), + and `field-allowlist.mjs` (CWE-212) are applied inside `otel-export.mjs` + and covered by `tests/hooks/otel-export-validators.test.mjs`. The + records carry operator-private data (paths, prompts, brief content); + the allowlist drops everything not explicitly named before export. +- **A native-collector design moves that boundary out of audited code.** + Handing raw JSONL to a sidecar collector means either re-expressing the + field allowlist in collector config (a second source of truth that can + drift) or shipping un-allowlisted private fields and trusting the + collector's egress rules. The S21 SSRF hardening — 169.254.169.254 + permanently blocked, loopback/RFC1918 gated behind + `VOYAGE_OTEL_ALLOW_PRIVATE` — is a property of `endpoint-validator.mjs` + and would have to be re-created in collector configuration to be + preserved. +- **The collector path is still available, by design.** Operators who + want collector semantics (retry, persistence, relabelling) use + `textfile` mode and scrape `voyage.prom` with node-exporter / vector / + otel-collector. Direct export is the minimal default, not a rejection + of collectors — it keeps the data-sanitization boundary in Voyage's + own validated code for the common case. + +This is a deliberate direct-export-over-collector choice; the custom +exporters and their guards are kept, not pruned. + ## Limitations - **Stop-hook is normal-exit only.** If Claude Code crashes or is killed diff --git a/docs/voyage-vs-cc-balance-analysis.md b/docs/voyage-vs-cc-balance-analysis.md index 3b0ef21..a938c18 100644 --- a/docs/voyage-vs-cc-balance-analysis.md +++ b/docs/voyage-vs-cc-balance-analysis.md @@ -194,3 +194,34 @@ All items are **operator-gated** (scope-guard: analysis only). Tagged **[non-bre ## 9. Meta-note (dogfooding the Workflow tool) Running this analysis *on* the CC Workflow tool produced first-hand evidence for the audit's own V27/V29 rows: the `pipeline()` shape gave clean coverage (35/35, no row dropped), schema-forced output eliminated JSON-parse fragility (the CC-27 F2 win, reconfirmed), and the conditional adversarial stage worked. Costs also reconfirmed: ~2.97M subagent tokens / 96 agents / 4.6 min for a 35×3 fan-out — Workflow is a good substrate for *bounded, verifiable, parallel* fan-out, exactly the ~20% core CC-27 identified, and a poor fit for the ~80% judgment-heavy glue (scout + synthesis, which ran inline in main context here). This is dogfooding of the **Workflow tool**, not of Voyage's pipeline (the latter was declined by the operator). + +--- + +## 10. Decision record — resolved forks (operator, 2026-06-20) + +§4/§6 deferred three forks to the operator. All three resolved to the +**conservative** option (preserve the capability / the security code / the +deliberate pin). The model and observability work therefore collapses from +code-deletion to a documentation record; the only real code changes in the +backlog are V15 (export trim, S31) and V30 (economy calibration, S34). These +decisions are now the implemented baseline of the multi-session backlog plan +(`docs/balance-backlog-plan.md`), shipped across S31–S34. + +| Fork | Item | Resolution | What changed | +|------|------|------------|--------------| +| **D1** | V09 `gemini-bridge` | **Keep it as an agent.** | No removal / inline-rewiring. The capability count stays 24; its model is governed by D3 (stays opus). | +| **D2** | V32 observability export | **Keep `lib/exporters/*` + `otel-export.mjs`; document the direct-export rationale.** | Doc-only — no deletion of S21 security code. The rationale (preserve the in-process path / SSRF / field-allowlist guards rather than re-host a collector) is recorded in `docs/observability.md` §"Why direct export rather than a native collector". | +| **D3** | 24-agent `model: opus` pin (`40d8742`) | **Keep the pin firm.** | Document-only. opus on V09 (glue — does not reason), V35 (dormant `synthesis-agent`), V11 (retrieval agents, already `effort:medium`), V16 (`session-decomposer`, mechanical parsing), and V08 (researchers) was reconsidered for a sonnet downgrade and **kept opus**. **No agent frontmatter is changed** — `tests/lib/agent-frontmatter.test.mjs` remains the structural source-of-truth and is untouched. | + +**Inventory framing (V35 doc half, reconciled).** The "24 agents" headline is +**21 spawnable + 3 orchestrator reference docs**. The three orchestrators +(`planning-/research-/review-orchestrator`) document the inline `/trek*` +workflow and are not spawned as sub-agents. Of the 21 spawnable, one — +`synthesis-agent` — ships **dormant** (Δ≈0, wired to nothing; +`docs/T1-synthesis-poc-results.md`). Reconciled across `README.md`, +`CLAUDE.md`, and this doc; pinned in `tests/lib/doc-consistency.test.mjs` (S33). + +**Still explicitly NOT done** (out of scope, per the plan): no `gemini-bridge` +removal (D1), no exporter deletion (D2), no model downgrade (D3), no Handover-1 +change, no Workflow substrate swap (CC-27 declined), no delegated-orchestration +redesign (CC-26 lean-NO), no reopening of the premium-default / framing-gate pins. diff --git a/tests/lib/doc-consistency.test.mjs b/tests/lib/doc-consistency.test.mjs index 4474a72..450845e 100644 --- a/tests/lib/doc-consistency.test.mjs +++ b/tests/lib/doc-consistency.test.mjs @@ -1100,3 +1100,83 @@ test('S32: trekbrief Phase 3 delegates Q&A turn-taking to AskUserQuestion (V01 'question to ask (policy); AskUserQuestion does the asking (engine). No hand-rolled menu loop.', ); }); + +// ── S33 — documentation consolidation: record the considered-and-kept decisions ── +// +// The D1–D3 forks (balance-backlog-plan.md) resolved conservatively, collapsing +// the model + observability work into a documentation record. These pins make +// the three deliverables enforced doc-truth: +// • V35 (doc half) — the "24 agents" framing is reconciled to its honest split +// (21 spawnable + 3 reference orchestrators, of which synthesis-agent is the +// one dormant spawnable). Counts are DERIVED from agents/ so they survive +// reword and track new files. +// • D2 (V32 rationale) — observability.md documents why direct export rather +// than a native collector (preserves the S21 in-process guards). +// • D3 (kept-opus) — the analysis doc carries a decision record citing the pin +// commit; the model frontmatter is NOT changed (agent-frontmatter.test.mjs +// stays the structural source-of-truth and is untouched by S33). +// Fix the SOURCE a pin guards, not the test. + +test('S33: README + CLAUDE.md reconcile the agent inventory (spawnable + reference orchestrators = file count)', () => { + const agentFiles = listMd('agents'); + const total = agentFiles.length; + const orchestrators = agentFiles.filter((f) => /-orchestrator\.md$/.test(f)); + const refCount = orchestrators.length; + const spawnable = total - refCount; + assert.equal(refCount, 3, `expected exactly 3 *-orchestrator reference docs, got ${refCount}`); + for (const [name, body] of [['README.md', read('README.md')], ['CLAUDE.md', read('CLAUDE.md')]]) { + assert.ok( + body.includes(`${spawnable} spawnable`), + `${name} must state "${spawnable} spawnable" agents (derived: ${total} files − ${refCount} reference orchestrators)`, + ); + assert.ok( + body.includes(`${refCount} orchestrator reference doc`), + `${name} must label the ${refCount} orchestrators as reference docs (not spawnable capabilities)`, + ); + } +}); + +test('S33: the inventory framing names synthesis-agent as the one dormant spawnable', () => { + // 1 of the spawnable agents ships dormant (Δ≈0, wired to nothing). The framing + // must say so, so the headline count is honest about live vs. dormant capability. + for (const [name, body] of [['README.md', read('README.md')], ['CLAUDE.md', read('CLAUDE.md')]]) { + assert.ok( + /synthesis-agent/.test(body) && /dormant/i.test(body), + `${name} inventory framing must name synthesis-agent as the dormant spawnable`, + ); + } +}); + +test('S33: the 3 orchestrator files declare themselves reference docs, not spawnable capabilities (V35 relabel)', () => { + for (const f of listMd('agents').filter((x) => /-orchestrator\.md$/.test(x))) { + assert.ok( + /reference document, not a spawnable capability/i.test(read(`agents/${f}`)), + `agents/${f} must declare "reference document, not a spawnable capability" (V35 doc-half relabel)`, + ); + } +}); + +test('S33: docs/observability.md documents the direct-export-vs-collector rationale (D2)', () => { + const t = read('docs/observability.md'); + assert.ok( + /direct[- ]export/i.test(t) && /collector/i.test(t), + 'observability.md must document why Voyage exports directly rather than via a native collector (D2)', + ); + // The rationale must anchor on the in-process security boundary it preserves + // (the S21 hardening), not read as an unjustified re-host. + assert.ok( + /allowlist/i.test(t) && /(SSRF|CWE-918|CWE-212)/.test(t), + 'the direct-export rationale must cite the in-process guards it preserves (field allowlist + SSRF/path)', + ); +}); + +test('S33: the analysis doc records the D1–D3 resolved forks (considered-and-kept, pin firm)', () => { + const t = read('docs/voyage-vs-cc-balance-analysis.md'); + assert.ok(/Decision record/i.test(t), 'analysis doc must carry a "Decision record" addendum for the resolved forks'); + for (const d of ['D1', 'D2', 'D3']) { + assert.ok(new RegExp(`\\b${d}\\b`).test(t), `analysis-doc decision record must name fork ${d}`); + } + // D3 kept the model pin firm — the record must cite the pin commit so a future + // reader sees the downgrade was reconsidered-and-declined, not overlooked. + assert.ok(/40d8742/.test(t), 'decision record must cite the model-pin commit 40d8742 (D3 kept firm, document-only)'); +});