docs(voyage): S33 — reconcile agent inventory (21 spawnable + 3 reference docs) + record D1–D3 considered-and-kept

Balance-backlog S33 (DOC, non-breaking). The D1–D3 forks resolved conservatively
(balance-backlog-plan.md), collapsing the model + observability work into a
documentation record. Three deliverables, all doc-only, no code/frontmatter change:

- V35 (doc half): the "24 agents" headline is reconciled to its honest split —
  21 spawnable (one dormant: synthesis-agent, Δ≈0) + 3 orchestrator reference
  docs (planning-/research-/review-orchestrator document the inline /trek*
  workflow; not spawned). Each orchestrator header now declares itself a
  "reference document, not a spawnable capability". README + CLAUDE.md state the
  split; counts in the new pins are DERIVED from agents/ so they survive reword.
- D2 (V32 rationale): docs/observability.md gains a "Why direct export rather
  than a native collector" section — direct export keeps the path / SSRF /
  field-allowlist guards in audited in-process code (the S21 hardening) instead
  of re-hosting a collector; textfile mode remains the collector escape hatch.
- D3 (kept-opus): docs/voyage-vs-cc-balance-analysis.md §10 decision record —
  opus on V09 (glue), V35 (dormant), V11 (retrieval), V16 (mechanical), V08
  (researchers) was reconsidered for sonnet and KEPT (pin 40d8742 firm).

No agent frontmatter changed — tests/lib/agent-frontmatter.test.mjs is the
structural model source-of-truth and is untouched (diff is description-only:
model: opus + tools lists unchanged, no Agent tool granted). No Handover-1
change; no exporter/gemini-bridge removal. Non-breaking, no version bump.

tests/lib/doc-consistency.test.mjs: +5 S33 pins (inventory split derived from
agents/; synthesis-agent dormant; orchestrator relabel; observability D2
rationale; analysis-doc D1–D3 record). Tests 739 (737 pass / 2 skip / 0 fail),
bar `node --test`; `claude plugin validate` green (1 accepted warning).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqBYc8Ltrk7LipyJmGxXiB
This commit is contained in:
Kjell Tore Guttormsen 2026-06-20 09:49:11 +02:00
commit 2df0cbb372
8 changed files with 160 additions and 6 deletions

View file

@ -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

View file

@ -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 S31S34.
| 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.