Compare commits

..

No commits in common. "main" and "v5.10.0" have entirely different histories.

28 changed files with 332 additions and 1596 deletions

View file

@ -1,11 +1,11 @@
{ {
"name": "voyage", "name": "voyage",
"description": "Voyage — brief, research, plan, execute, review, continue. Contract-driven Claude Code pipeline. /trekbrief, /trekplan, and /trekreview each end by building a self-contained operator-annotation HTML (scripts/annotate.mjs, modelled on claude-code-100x): select text or click any element, pick intent (Fiks/Endre/Spørsmål), write comment, copy structured prompt, paste back, Claude revises the .md.", "description": "Voyage — brief, research, plan, execute, review, continue. Contract-driven Claude Code pipeline. /trekbrief, /trekplan, and /trekreview each end by building a self-contained operator-annotation HTML (scripts/annotate.mjs, modelled on claude-code-100x): select text or click any element, pick intent (Fiks/Endre/Spørsmål), write comment, copy structured prompt, paste back, Claude revises the .md.",
"version": "5.10.1", "version": "5.10.0",
"author": { "author": {
"name": "Kjell Tore Guttormsen" "name": "Kjell Tore Guttormsen"
}, },
"homepage": "https://git.fromaitochitta.com/open/voyage", "homepage": "https://git.fromaitochitta.com/open/ktg-plugin-marketplace/src/branch/main/plugins/voyage",
"repository": "https://git.fromaitochitta.com/open/ktg-plugin-marketplace.git", "repository": "https://git.fromaitochitta.com/open/ktg-plugin-marketplace.git",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [

3
.gitignore vendored
View file

@ -48,6 +48,3 @@ NEXT-SESSION-PROMPT*.local.md
# broad `git add -A docs`, which needed a history rewrite to undo. Ignoring the # broad `git add -A docs`, which needed a history rewrite to undo. Ignoring the
# type is cheaper than remembering not to stage it. # type is cheaper than remembering not to stage it.
docs/*.pdf docs/*.pdf
# Page renders of those same documents (SLDC-AI-1_1.png etc). Narrow on purpose:
# legitimate plugin assets may still be .png under docs/.
docs/SLDC-AI*.png

View file

@ -4,111 +4,6 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## v5.10.1 — 2026-09-03 — gemini-bridge dropped; fail-closed review verdicts
### Removed
- **`gemini-bridge` is dropped from the pipeline (operator directive).** The
agent file is deleted and every live surface that could still steer a run
toward it is cleared: `commands/trekresearch.md` (the Bridge-agent launch
block, the `--local` help text, the high-effort always-on pair, the stats
record, the graceful-degradation list), `commands/trekplan.md`,
`commands/trekbrief.md`, `agents/research-orchestrator.md` (mode table,
agent table, prompting block, summary line), `README.md` (feature prose, mode
table, agent prose, the mermaid EXTERNAL node, the `* = conditional` legend,
the optional-MCP requirement, the `--local` section), `CLAUDE.md`,
`settings.json` (the whole `trekresearch.geminiBridge` block),
`templates/research-brief-template.md` (the *Gemini Second Opinion* section
and the sources-table `gemini` type), `templates/trekbrief-template.md`,
`docs/architecture.md`, `docs/command-modes.md`.
- **`/trekplan` high-effort Adversarial Pass 2 degrades explicitly.** It now
emits a `## Adversarial Pass 2 (v5.1.1 high-effort)` section whose status
reads **"unavailable, skipped"**, rather than failing or vanishing: a
high-effort plan carrying no Pass 2 marker is indistinguishable from one whose
Pass 2 crashed. `/trekresearch` high effort forces only `contrarian-researcher`
always-on, which remains the complete adversarial coverage.
- **Not removed:** `gemini_used` survives as a vestigial `/trekresearch` stats
field pinned to `false`. Dropping it would break the observability export
schema for existing consumers, and no consumer asked for that.
- Agent inventory: **24 → 23 files** (20 spawnable + 3 orchestrator reference
docs; `synthesis-agent` still dormant). Counts reconciled in `README.md` and
`CLAUDE.md`; the `<example>`-block floor in
`tests/lib/agent-frontmatter.test.mjs` moves 34 → 32 because an agent
legitimately left the inventory — not because examples went missing from a
surviving one.
- Pinned by 5 new tests in `tests/lib/doc-consistency.test.mjs`, including a
**known-positive control** asserting the historical records *still do* carry
gemini references — so the empty result on live surfaces is a measurement and
not a broken query. CHANGELOG entries and measurement/decision docs keep their
references deliberately: they record what a past version did.
### Fixed
- **`/trekreview` verdicts are fail-closed.** A finding removed by Pass 2 or
Pass 3, and a reviewer whose payload was thrown away or never arrived, were
arithmetically identical to a finding that never existed — all three pushed
the verdict toward `ALLOW`. Measured before the fix: a BLOCKER with a
101-character title → `ALLOW`; a payload carrying one ad-hoc `rule_key` was
skipped whole at ingest, taking a valid BLOCKER sibling with it → `ALLOW`.
`lib/review/coordinator-contract.mjs` now splits every removal two ways —
**dropped** when the test refuted the finding as a claim about this codebase,
**unverified** otherwise — and a non-empty `unverified` bucket, or a reviewer
in `expectedReviewers` that did not report, forbids `ALLOW` (verdict `WARN`,
with `allow_blocked_by` naming why). The rule never *raises* a verdict: the
severity catalogue and the BLOCKER/MAJOR thresholds are untouched. New
exports: `classifySuppression`, `REFUTING_REASONS`, `UNVERIFIED_REASONS`;
`runContract` gains `unverified`, `missing_reviewers`,
`unattributable_payloads` and `allow_blocked_by` (`suppressed` stays the
union, so existing consumers keep their meaning). A payload that fails schema
without carrying a `reviewer` name is counted as unattributable rather than
reported as a reviewer called "unnamed reviewer" — naming one would invent an
agent nobody launched and double-count with `expectedReviewers`.
Mirrored in `agents/review-coordinator.md` (Pass 2/3 fate columns, the new
§*Suppression is two-valued*, Pass 4 threshold table) and
`commands/trekreview.md` (Phase 5 reviewer accounting → STOP; Phase 6).
Driven test-first: 11 new tests in `tests/lib/coordinator-contract.test.mjs`,
incl. a known-positive control proving `ALLOW` is still reachable.
### Docs
- **T1 §6 PoC status corrected — it was already run and declined.**
`docs/T1-cc26-delegated-orchestration.md` §8 item 3 still claimed both the §6
synthesis-agent PoC and the §5 full bake-off were "designed but unbuilt". That
was written in S7 and falsified the same afternoon by S12, which ran the §6
PoC and recorded **Δ main-context (faithful flow) = 0.0%, NEGATIVE**
(`docs/T1-synthesis-poc-results.md`; reproduce with
`node scripts/synthesis-measure.mjs`). The stale wording caused the settled
PoC to be re-ordered as new work on 2026-09-02, so it is struck rather than
deleted and §6 gained an explicit **RUN AND DECLINED** status block. The
finding is structural, not stochastic: `/trekplan` Phase 5 spawns the
exploration swarm **foreground** (`commands/trekplan.md:158,338-341`), so the
610 outputs are already resident in main before Phase 7 begins — delegating
only the Phase-7 digest returns on top of them and evicts nothing. A saving
needs the Phase-5 delivery moved off-main, which is §5.
- **§5's environment premise updated (measured 2026-09-03, CC 2.1.259).** S12
recorded the live bake-off as blocked for want of an `ANTHROPIC_API_KEY`;
`claude -p --output-format stream-json` in fact runs on subscription auth with
no API key and now emits a `subagent_stats` block (spawned / max_depth /
refused) that did not exist in June — a ready-made classifier-interference
counter for §5's feasibility guard. S12's other blocker (the installed plugin
is the cache copy, so a repo-local agent is invisible to `claude -p`) stands
unmeasured. This changes nothing about the §6 verdict; it lowers the cost of §5.
- **`/trekresearch --engine deep-research`: document the real version window.**
Claude Code **2.1.218** changed `/deep-research` to start only when the operator
invokes it; from there the Skill tool refuses a model invocation outright with
`disable-model-invocation` (measured in a real `--engine deep-research` run on
2026-09-01 — the SC3 fallback to `swarm` held). The engine was built against
2.1.196 and the prose still promised a path Claude Code has removed
("requires Claude Code 2.1.154+"). `commands/trekresearch.md` (flag bullet +
pre-gate + fallback reason tokens), `docs/command-modes.md` and `README.md` now
state the closed window `2.1.154 <= CC < 2.1.218` and name
`disable-model-invocation` as the expected fallback reason on any current CC.
The pre-gate gained an upper ceiling (still a numeric comparison, not a string
one). **The flag is kept** as an additive opt-in that never hard-fails; no
`lib/` change, no adapter-contract change, no default change, no version bump.
Pinned by `tests/lib/doc-consistency.test.mjs`.
## v5.10.0 — 2026-08-18 — STORM bounded research loop (default-off) + the Agent-tool `name` spawn defect ## v5.10.0 — 2026-08-18 — STORM bounded research loop (default-off) + the Agent-tool `name` spawn defect
Additive. Every new research mechanism ships **inert**: `VOYAGE_STORM_ENABLED` is Additive. Every new research mechanism ships **inert**: `VOYAGE_STORM_ENABLED` is

View file

@ -65,8 +65,9 @@ Full flag reference for each command (modes, `--gates`, `--profile`, breaking ch
| community-researcher | opus | Community experience: issues, blogs, discussions | | community-researcher | opus | Community experience: issues, blogs, discussions |
| security-researcher | opus | CVEs, audit history, supply chain risks | | security-researcher | opus | CVEs, audit history, supply chain risks |
| contrarian-researcher | opus | Counter-evidence, overlooked alternatives | | contrarian-researcher | opus | Counter-evidence, overlooked alternatives |
| gemini-bridge | opus | Gemini Deep Research second opinion (conditional) |
> **Inventory (S33 reconcile, updated v5.10.1).** 23 agent files = **20 spawnable** (one, `synthesis-agent`, ships **dormant** — Δ≈0, wired to nothing) **+ 3 orchestrator reference docs** (`planning-/research-/review-orchestrator` document the inline `/trek*` workflow, not spawnable capabilities). All 23 stay `model: opus` (operator pin `40d8742`); the glue/mechanical/retrieval/dormant roles were reconsidered for a sonnet downgrade and **kept opus** — decision record: `docs/voyage-vs-cc-balance-analysis.md` §10. > **Inventory (S33 reconcile).** 24 agent files = **21 spawnable** (one, `synthesis-agent`, ships **dormant** — Δ≈0, wired to nothing) **+ 3 orchestrator reference docs** (`planning-/research-/review-orchestrator` document the inline `/trek*` workflow, not spawnable capabilities). All 24 stay `model: opus` (operator pin `40d8742`); the glue/mechanical/retrieval/dormant roles were reconsidered for a sonnet downgrade and **kept opus** — decision record: `docs/voyage-vs-cc-balance-analysis.md` §10.
> **Model & effort.** `opus` = Opus 4.8 (default reasoning effort `high`); `sonnet` = Sonnet 4.6; `fable` = Fable 5 (Mythos-class, above Opus — reasoning effort inherits from the session; xhigh requires a session-level setting). Select agents carry native per-spawn `effort:` (retrieval → `medium`, adversarial-reasoning → `high`) — a different axis from brief `phase_signals.effort` (orchestration shape: which agents/passes run). Per-agent table + axes → `docs/profiles.md` §Model & effort axes. > **Model & effort.** `opus` = Opus 4.8 (default reasoning effort `high`); `sonnet` = Sonnet 4.6; `fable` = Fable 5 (Mythos-class, above Opus — reasoning effort inherits from the session; xhigh requires a session-level setting). Select agents carry native per-spawn `effort:` (retrieval → `medium`, adversarial-reasoning → `high`) — a different axis from brief `phase_signals.effort` (orchestration shape: which agents/passes run). Per-agent table + axes → `docs/profiles.md` §Model & effort axes.

View file

@ -2,7 +2,7 @@
Contract-driven Claude Code pipeline: brief, research, plan, execute, review. Agent swarms, research triangulation, adversarial review, multi-session resumption. Contract-driven Claude Code pipeline: brief, research, plan, execute, review. Agent swarms, research triangulation, adversarial review, multi-session resumption.
![Version](https://img.shields.io/badge/version-5.10.1-blue) ![Version](https://img.shields.io/badge/version-5.10.0-blue)
![License](https://img.shields.io/badge/license-MIT-green) ![License](https://img.shields.io/badge/license-MIT-green)
![Platform](https://img.shields.io/badge/platform-Claude%20Code-purple) ![Platform](https://img.shields.io/badge/platform-Claude%20Code-purple)
@ -195,7 +195,7 @@ If you say "stop" or "enough" during Phase 4, the current review findings are su
Deep, multi-phase research that combines local codebase analysis with external knowledge. Uses specialized agent swarms to investigate multiple dimensions in parallel, then triangulates findings. Deep, multi-phase research that combines local codebase analysis with external knowledge. Uses specialized agent swarms to investigate multiple dimensions in parallel, then triangulates findings.
A parallel swarm of up to 5 local + 4 external agents investigates 38 research dimensions. Findings are triangulated (local vs. external, confidence per dimension, contradictions flagged) and synthesized into a structured research brief. A parallel swarm of up to 5 local + 4 external agents investigates 38 research dimensions, with optional Gemini Deep Research as an independent second opinion. Findings are triangulated (local vs. external, confidence per dimension, contradictions flagged) and synthesized into a structured research brief.
Output: Output:
- With `--project <dir>`: `{dir}/research/{NN}-{slug}.md` (auto-incremented index) - With `--project <dir>`: `{dir}/research/{NN}-{slug}.md` (auto-incremented index)
@ -205,14 +205,14 @@ Output:
| Mode | Usage | Behavior | | Mode | Usage | Behavior |
|------|-------|----------| |------|-------|----------|
| **Default** | `/trekresearch <question>` | Interview + research swarm (local + external), foreground | | **Default** | `/trekresearch <question>` | Interview + research swarm (local + external + Gemini), foreground |
| **Project** | `/trekresearch --project <dir> <question>` | Write brief into `{dir}/research/NN-slug.md` | | **Project** | `/trekresearch --project <dir> <question>` | Write brief into `{dir}/research/NN-slug.md` |
| **Quick** | `/trekresearch --quick <question>` | Interview (short) + inline research, no agent swarm | | **Quick** | `/trekresearch --quick <question>` | Interview (short) + inline research, no agent swarm |
| **Local** | `/trekresearch --local <question>` | Only codebase analysis agents (skip external research) | | **Local** | `/trekresearch --local <question>` | Only codebase analysis agents (skip external + Gemini) |
| **External** | `/trekresearch --external <question>` | Only external research agents (skip codebase analysis) | | **External** | `/trekresearch --external <question>` | Only external research agents (skip codebase analysis) |
| **Foreground** | `/trekresearch --fg <question>` | No-op alias (foreground is default since v2.4.0) | | **Foreground** | `/trekresearch --fg <question>` | No-op alias (foreground is default since v2.4.0) |
| **Profile** | `/trekresearch --profile <name> <question>` | (v4.1.0) Pin model profile for the research phase. See [Profile system](#profile-system-v410). | | **Profile** | `/trekresearch --profile <name> <question>` | (v4.1.0) Pin model profile for the research phase. See [Profile system](#profile-system-v410). |
| **Engine** | `/trekresearch --external --engine deep-research <question>` | Delegate the external phase to Claude Code's built-in `/deep-research` workflow. Only works on `2.1.154 <= CC < 2.1.218`; from **2.1.218** `/deep-research` is operator-invoked only (Skill tool: `disable-model-invocation`), so it always falls back to `swarm`. Never hard-fails. Default `swarm`. | | **Engine** | `/trekresearch --external --engine deep-research <question>` | Delegate the external phase to Claude Code's built-in `/deep-research` workflow; falls back to `swarm` if unavailable. Default `swarm`. |
Flags combine: `--project <dir> --external`. Flags combine: `--project <dir> --external`.
@ -234,7 +234,7 @@ how many turns it has used. Whether the loop becomes the default is decided by a
pre-registered measurement, not by preference: see pre-registered measurement, not by preference: see
[`docs/storm-measurement.md`](docs/storm-measurement.md). [`docs/storm-measurement.md`](docs/storm-measurement.md).
Research uses up to 5 local agents (architecture-mapper, dependency-tracer, task-finder, git-historian, convention-scanner) and 4 external agents (docs-researcher, community-researcher, security-researcher, contrarian-researcher). Per-agent details in [`agents/`](agents/). Research uses up to 5 local agents (architecture-mapper, dependency-tracer, task-finder, git-historian, convention-scanner) and 4 external agents (docs-researcher, community-researcher, security-researcher, contrarian-researcher) plus the optional Gemini bridge for an independent second opinion. Per-agent details in [`agents/`](agents/).
--- ---
@ -657,7 +657,7 @@ flowchart TB
end end
subgraph RES["/trekresearch · Phase 4 — parallel"] subgraph RES["/trekresearch · Phase 4 — parallel"]
RL["LOCAL: architecture-mapper · dependency-tracer<br/>task-finder · git-historian · convention-scanner*"] RL["LOCAL: architecture-mapper · dependency-tracer<br/>task-finder · git-historian · convention-scanner*"]
RE["EXTERNAL: docs-researcher · community-researcher<br/>security-researcher* · contrarian-researcher*"] RE["EXTERNAL: docs-researcher · community-researcher<br/>security-researcher* · contrarian-researcher* · gemini-bridge*"]
end end
subgraph PL["/trekplan"] subgraph PL["/trekplan"]
PLG["Phase 4b · brief-reviewer (gate)"] PLG["Phase 4b · brief-reviewer (gate)"]
@ -676,7 +676,7 @@ flowchart TB
BR --> RES --> PL --> EX --> RV BR --> RES --> PL --> EX --> RV
``` ```
`* = conditional`: convention-scanner / test-strategist on medium+ codebases (50+ files); research-scout for unknown external tech; security-/contrarian-researcher when a leading recommendation forms (or always at `effort=high`); brief-conformance-reviewer skipped under `--quick`. `* = conditional`: convention-scanner / test-strategist on medium+ codebases (50+ files); research-scout for unknown external tech; security-/contrarian-researcher + gemini-bridge when a leading recommendation forms (or always at `effort=high`); brief-conformance-reviewer skipped under `--quick`.
> **Which Claude Code primitive each phase uses — and the alternatives considered (Workflow substrate, delegated orchestrator, dormant synthesis-agent)** → see [docs/architecture.md §Primitives per step](docs/architecture.md#primitives-per-step-decision-matrix). > **Which Claude Code primitive each phase uses — and the alternatives considered (Workflow substrate, delegated orchestrator, dormant synthesis-agent)** → see [docs/architecture.md §Primitives per step](docs/architecture.md#primitives-per-step-decision-matrix).
@ -837,6 +837,7 @@ For per-profile cost estimates, see [`docs/profiles.md`](docs/profiles.md).
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (CLI, desktop app, or web app) - [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (CLI, desktop app, or web app)
- Claude subscription with Opus access (Max plan recommended) - Claude subscription with Opus access (Max plan recommended)
- Optional: [Tavily MCP server](https://github.com/tavily-ai/tavily-mcp) for enhanced external research - Optional: [Tavily MCP server](https://github.com/tavily-ai/tavily-mcp) for enhanced external research
- Optional: a Gemini Deep Research MCP server exposing `gemini_deep_research`, `gemini_get_research_status`, `gemini_get_research_result`, `gemini_research_followup` for an independent second opinion (voyage calls these tools; any server implementing them works)
## Architecture ## Architecture
@ -844,8 +845,8 @@ Top-level layout:
``` ```
trekplan/ trekplan/
├── agents/ 23 specialized agents (all `model: opus`-pinned; per-phase model set by `--profile`) ├── agents/ 24 specialized agents (all `model: opus`-pinned; per-phase model set by `--profile`)
│ └ 20 spawnable (1 dormant: synthesis-agent, Δ≈0) + 3 orchestrator reference docs (not spawned) │ └ 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 ├── commands/ 6 slash commands (trekbrief, trekresearch, trekplan, trekexecute, trekreview, trekcontinue) + trekendsession helper
├── templates/ Frontmatter templates for brief, research, plan, session, launch ├── templates/ Frontmatter templates for brief, research, plan, session, launch
├── hooks/ 8 hooks (pre-bash, pre-write, pre-agent-cap, session-title, post-bash-stats, pre-compact-flush, post-compact-flush, otel-export) ├── hooks/ 8 hooks (pre-bash, pre-write, pre-agent-cap, session-title, post-bash-stats, pre-compact-flush, post-compact-flush, otel-export)
@ -904,8 +905,8 @@ just run `--profile economy`, to put the whole pipeline on Sonnet.
### Disable external research ### Disable external research
`/trekresearch --local` skips Tavily and Microsoft Learn. To make `/trekresearch --local` skips Tavily, Microsoft Learn, and the
`--local` the default, edit the front of Gemini bridge. To make `--local` the default, edit the front of
`commands/trekresearch.md` Phase 1 and flip the default branch `commands/trekresearch.md` Phase 1 and flip the default branch
of the `--local` argument check. Or just always pass `--local` and of the `--local` argument check. Or just always pass `--local` and
document it in your team's CLAUDE.md. document it in your team's CLAUDE.md.

View file

@ -2,14 +2,13 @@
## Reporting a vulnerability ## Reporting a vulnerability
Report privately to <security@fromaitochitta.com> — do not open a public Open a **private** issue on Forgejo:
issue. There is no SLA — this is a solo-maintained plugin — but
acknowledged reports are usually triaged within 7 days.
If you already have an account on the Forgejo instance, you may instead > https://git.fromaitochitta.com/open/ktg-plugin-marketplace
open a **private** issue on the repo, tagged `security`:
> https://git.fromaitochitta.com/open/voyage Tag it `security` and mark it private. Do not file public issues for
unpatched vulnerabilities. There is no SLA — this is a solo-maintained
plugin — but acknowledged reports are usually triaged within 7 days.
## Supported versions ## Supported versions

151
agents/gemini-bridge.md Normal file
View file

@ -0,0 +1,151 @@
---
name: gemini-bridge
description: |
Use this agent when an independent second opinion from Gemini Deep Research is
needed on a technology choice, architectural question, or complex research topic.
Provides triangulation value by running a completely independent research path
that can confirm or challenge findings from other agents.
model: opus
color: magenta
tools: ["mcp__gemini-mcp__gemini_deep_research", "mcp__gemini-mcp__gemini_get_research_status", "mcp__gemini-mcp__gemini_get_research_result", "mcp__gemini-mcp__gemini_research_followup"]
---
You are a bridge to Google Gemini Deep Research. Your role is to obtain an independent,
thorough research result that provides triangulation value — a completely independent
research path that can confirm or challenge findings from other agents.
The value of this agent is INDEPENDENCE. Do not pre-bias Gemini with conclusions from
other agents. Submit the research question cleanly so Gemini's findings stand on their
own merits.
## Workflow
### 1. Check availability
Attempt to call gemini_deep_research. If the tool is not available (MCP server not
connected), return IMMEDIATELY with:
```
## Gemini Bridge Result
**Status:** Unavailable
**Reason:** Gemini MCP server not connected. Proceeding without second opinion.
```
Do NOT error, block, or retry. Unavailability is an expected operational state.
### 2. Formulate query
Take the research question and reformulate it for Gemini to maximize result quality:
- Add context about what dimensions to cover (trade-offs, maturity, ecosystem, operational
concerns, known failure modes, community consensus)
- Use format_instructions to request structured output with clear sections, source citations,
and explicit confidence levels per claim
- Set parameters:
- `research_mode`: "custom"
- `source_tier`: 2
- `research_window_days`: 90
Example format_instructions to include:
> "Structure your response with: Executive Summary, Key Findings (bullet points),
> Trade-offs, Known Issues and Gotchas, Community Consensus, and Sources. For each
> major claim, indicate your confidence level (high/medium/low) and cite the source."
### 3. Submit research
Call `gemini_deep_research` with the reformulated query and parameters.
### 4. Poll for completion
Call `gemini_get_research_status` repeatedly until the research completes:
- Call the status tool, then call it again after it returns — repeat until done
- Do not use bash or sleep commands — use repeated tool calls to simulate waiting
- Continue polling until status is `"completed"` or `"failed"`
- If `"failed"`: report the failure reason and return gracefully — do not retry
- Timeout: if still running after 40 polls (~20 minutes of equivalent wait), report
timeout and return whatever partial result is available
### 5. Retrieve result
Call `gemini_get_research_result` with `include_citations: true`.
### 6. Optional follow-up
If the result has clear gaps on specific dimensions that are directly relevant to the
research question, call `gemini_research_followup` with a targeted follow-up question.
Rules for follow-up:
- Maximum 1 follow-up call
- Only if there is a genuine gap — do not follow up out of habit
- Make the follow-up question narrow and specific, not a re-statement of the original
### 7. Format output
Structure the final result as:
```
## Gemini Bridge Result
**Status:** Completed
**Research duration:** {time taken}
**Sources cited:** {count}
### Key Findings
- {finding 1}
- {finding 2}
- {finding 3}
### Trade-offs and Known Issues
- {trade-off or issue 1}
- {trade-off or issue 2}
### Sources
| # | Source | Relevance |
|---|--------|-----------|
| 1 | {URL} | {one-line relevance} |
### Areas for Triangulation
*Claims that should be cross-checked against local codebase analysis
and other external agents:*
- {claim 1 — check against local architecture}
- {claim 2 — verify with community experience}
- {claim 3 — validate against codebase constraints}
```
## Rules
- **Never block the research pipeline.** If Gemini is slow or unavailable, return what
you have with a clear status note.
- **Do not interpret or editorialize.** Report Gemini's findings as-is, formatted for
integration. Your job is formatting and delivery, not analysis.
- **Flag "Areas for Triangulation"** — claims that the research-orchestrator or other
agents should cross-check against local codebase analysis, team experience, or other
external sources.
- **Independence is the point.** Do not include findings from other agents in your query
to Gemini. The value of a second opinion is that it is uninfluenced by the first.
- **Cite everything.** Every major claim in the output must trace to a source in the
Sources table. Remove claims that Gemini did not support with a source.
- **Graceful degradation at every step.** Unavailable tool, failed research, timeout —
all are handled with a clear status message and immediate return. Never leave the
pipeline hanging.
## When to use — examples
<example>
Context: trekresearch launches gemini-bridge for an independent second opinion on a technology choice
user: "/trekplan Should we use Kafka or NATS for our event streaming layer?"
assistant: "Launching gemini-bridge for an independent second opinion on Kafka vs NATS."
<commentary>
Technology choice with significant architectural implications triggers gemini-bridge
to provide an independent research path alongside local exploration agents.
</commentary>
</example>
<example>
Context: user wants deep research via Gemini on a complex architectural question
user: "Get me a Gemini deep research on event sourcing patterns for distributed systems"
assistant: "I'll use the gemini-bridge agent to run a deep research on event sourcing patterns."
<commentary>
Direct request for Gemini research on a complex architectural question triggers the agent.
</commentary>
</example>

View file

@ -61,16 +61,13 @@ Execute these phases in order. Do not skip phases.
Based on the mode, determine which agent groups to launch: Based on the mode, determine which agent groups to launch:
| Mode | Local agents | External agents | | Mode | Local agents | External agents | Gemini bridge |
|------|-------------|-----------------| |------|-------------|-----------------|---------------|
| `default` | Yes | Yes | | `default` | Yes | Yes | Yes (if enabled in settings) |
| `local` | Yes | No | | `local` | Yes | No | No |
| `external` | No | Yes | | `external` | No | Yes | Yes (if enabled) |
| `quick` | N/A — handled inline by the command, not the orchestrator | | `quick` | N/A — handled inline by the command, not the orchestrator |
> **No bridge agent (v5.10.1).** The third-party second-opinion bridge was dropped
> by operator directive; there is no bridge column to select.
**Local agents** (reuse existing plugin agents with research-focused prompts): **Local agents** (reuse existing plugin agents with research-focused prompts):
| Agent | Purpose in research context | | Agent | Purpose in research context |
@ -90,6 +87,12 @@ Based on the mode, determine which agent groups to launch:
| `security-researcher` | CVEs, audit history, supply chain risks | | `security-researcher` | CVEs, audit history, supply chain risks |
| `contrarian-researcher` | Counter-evidence, overlooked alternatives, reasons to reconsider | | `contrarian-researcher` | Counter-evidence, overlooked alternatives, reasons to reconsider |
**Bridge agent:**
| Agent | Purpose |
|-------|---------|
| `gemini-bridge` | Independent second opinion via Gemini Deep Research |
### Phase 2 — Parallel research ### Phase 2 — Parallel research
Launch ALL selected agents **in parallel** using the Agent tool — one message, Launch ALL selected agents **in parallel** using the Agent tool — one message,
@ -124,6 +127,11 @@ Examples:
Pass the research question, specific dimensions to investigate, and any context from Pass the research question, specific dimensions to investigate, and any context from
the interview about what the user already knows or cares about. the interview about what the user already knows or cares about.
**Prompting gemini-bridge:**
Pass the research question as-is. Do NOT pre-bias with findings from other agents —
the value of Gemini is independence.
### Phase 3 — Targeted follow-ups ### Phase 3 — Targeted follow-ups
Review all agent results. Identify knowledge gaps — areas where findings are thin, Review all agent results. Identify knowledge gaps — areas where findings are thin,
@ -187,7 +195,7 @@ When done, your output message should contain:
**Brief:** {brief path} **Brief:** {brief path}
**Confidence:** {overall confidence 0.0-1.0} **Confidence:** {overall confidence 0.0-1.0}
**Dimensions:** {N} researched **Dimensions:** {N} researched
**Agents:** {N} local + {N} external **Agents:** {N} local + {N} external + {gemini status}
### Key Findings ### Key Findings
- {Finding 1} - {Finding 1}
@ -218,7 +226,7 @@ You can:
- **Sources:** Every claim in the brief must cite a source (URL or file path). - **Sources:** Every claim in the brief must cite a source (URL or file path).
Never invent findings. Never invent findings.
- **Honesty:** If a question is trivially answerable, say so. Don't inflate research. - **Honesty:** If a question is trivially answerable, say so. Don't inflate research.
- **Graceful degradation:** If MCP tools are unavailable (Tavily, MS Learn), proceed - **Graceful degradation:** If MCP tools are unavailable (Tavily, Gemini), proceed
with available tools and note the limitation in the brief metadata. with available tools and note the limitation in the brief metadata.
- **Independence:** Do not pre-bias external agents with local findings or vice versa. - **Independence:** Do not pre-bias external agents with local findings or vice versa.
The value is in independent perspectives that are THEN triangulated. The value is in independent perspectives that are THEN triangulated.

View file

@ -84,48 +84,31 @@ identical `(file, rule_key)` and `line == 0` collide.
### Pass 2 — HubSpot Judge filters (3 criteria) ### Pass 2 — HubSpot Judge filters (3 criteria)
Remove findings that fail ANY of these filters. **The `Removed as` column Drop findings that fail ANY of these filters:
is load-bearing** — see *Suppression is two-valued* below:
| Filter | Test | Fails if | Removed as | | Filter | Test | Drop if |
|--------|------|----------|------------| |--------|------|---------|
| Succinctness | `title.length ≤ 100` and `detail.length ≤ 800` chars | Title is a paragraph or detail is a wall of text | `unverified` (`succinctness:title` / `succinctness:detail`) | | Succinctness | `title.length ≤ 100` and `detail.length ≤ 800` chars | Title is a paragraph or detail is a wall of text |
| Accuracy | `file` resolves under the repo root AND `line` is plausible (≥ 0; ≤ file line count when known) | Path traversal escape, negative line, or impossibly large line number | **dropped** (`accuracy:refuted`) | | Accuracy | `file` resolves under the repo root AND `line` is plausible (≥ 0; ≤ file line count when known) | Path traversal escape, negative line, or impossibly large line number |
| Actionability | `recommended_action` is non-empty AND begins with an imperative verb | Empty action, "consider …" hedges, or restating the title | `unverified` (`actionability:empty`) | | Actionability | `recommended_action` is non-empty AND begins with an imperative verb | Empty action, "consider …" hedges, or restating the title |
Succinctness and Actionability read the finding's *packaging*; neither When dropping a finding, preserve a one-line note in the
examines the claim, so neither can establish the finding is unreal. Accuracy
does: a citation that escapes the repo root refutes the finding as a claim
about this codebase.
When removing a finding, preserve a one-line note in the
`Suppressed Findings` body section so the user knows why the count `Suppressed Findings` body section so the user knows why the count
shrank. shrank.
### Pass 3 — Cloudflare reasonableness (skipped in quick mode) ### Pass 3 — Cloudflare reasonableness (skipped in quick mode)
Remove findings that fail ANY of these tests: Drop findings that fail ANY of these tests:
- **No file:line citation****dropped** (`no-citation`). `file` is empty, - **No file:line citation.** `file` is empty, or `line < 0`. Speculative
or `line < 0`. Speculative "code might break somewhere" findings name no "code might break somewhere" findings have no anchor and are dropped.
location, so they make no checkable claim at all. - **Unknown rule_key.** `rule_key` is not in `RULE_CATALOGUE`. Reviewers
- **Unknown rule_key**`unverified` (`unknown-rule_key`). `rule_key` is not occasionally emit ad-hoc rule keys; the catalogue is the contract.
in `RULE_CATALOGUE`. Reviewers occasionally emit ad-hoc rule keys; the
catalogue is the contract, but a mislabelled finding is not a refuted one.
*(High-effort mode does not reach this branch: Pass 3 is bypassed and the
key is normalised to `PLAN_EXECUTE_DRIFT` and KEPT — see High-effort
normalization below. The two fates never apply to the same input.)*
- **Non-existent file.** `file` does not exist in the working tree AND - **Non-existent file.** `file` does not exist in the working tree AND
the diff does not show it as `(new file)`. Use Glob to verify. **This test the diff does not show it as `(new file)`. Use Glob to verify.
has three outcomes, not two:** Glob resolves and the file is absent from
both tree and diff → **dropped** (`file-existence:refuted`); Glob resolves
and the file is present → keep; **Glob cannot decide** (path outside the
working tree, unreadable, or the tool errored) → `unverified`
(`file-existence:indeterminate`). Never collapse *unresolvable* into
*refuted*.
- **Catalogue severity mismatch.** `severity` does not match the rule's - **Catalogue severity mismatch.** `severity` does not match the rule's
catalogue tier (e.g., `MISSING_TEST` emitted as MINOR). Reset to the catalogue tier (e.g., `MISSING_TEST` emitted as MINOR). Reset to the
catalogue tier; this is a correction, neither a drop nor an unverified. catalogue tier; this is a correction, not a drop.
In `quick` mode, skip this pass entirely. Note the skip in the In `quick` mode, skip this pass entirely. Note the skip in the
Executive Summary so the reader knows reasonableness was not applied. Executive Summary so the reader knows reasonableness was not applied.
@ -142,32 +125,6 @@ purposes. This normalization happens BEFORE writing review.md,
ensuring all `rule_key` values in the final review match the ensuring all `rule_key` values in the final review match the
catalogue. catalogue.
### Suppression is two-valued (fail-closed)
Every removal in Pass 2 and Pass 3 carries one of two fates, and the
distinction decides whether the review may come back clean:
| Fate | Meaning | Weight in Pass 4 |
|------|---------|------------------|
| **dropped** | The test **refuted** the finding as a claim about this codebase. | None. It weighs nothing, correctly. |
| **unverified** | The finding was removed **without** its claim ever being examined or settled. | Forbids `ALLOW`. |
The rule is one sentence: **a removal is `dropped` only when the test
refuted the finding; every other removal is `unverified`.** A reason you
cannot place is `unverified` — the default fails closed.
Why this exists: without it, a finding the coordinator could not
substantiate is arithmetically identical to a finding that never existed,
and both push the verdict toward `ALLOW`. The deterministic mirror of this
rule, including the reason vocabulary, is
`lib/review/coordinator-contract.mjs` (`classifySuppression`,
`REFUTING_REASONS`, `UNVERIFIED_REASONS`) — prose and lib share one
vocabulary on purpose.
**Unverified findings are not counted into a severity tier.** Their severity
is reviewer-asserted and was never substantiated; counting it would let an
unexamined finding *raise* the verdict, which is invention.
### Pass 4 — Compute verdict ### Pass 4 — Compute verdict
Count findings by severity AFTER dedup and filtering. Verdict thresholds: Count findings by severity AFTER dedup and filtering. Verdict thresholds:
@ -176,19 +133,7 @@ Count findings by severity AFTER dedup and filtering. Verdict thresholds:
|--------|---------| |--------|---------|
| `BLOCKER ≥ 1` | `BLOCK` | | `BLOCKER ≥ 1` | `BLOCK` |
| `BLOCKER == 0` AND `MAJOR ≥ 1` | `WARN` | | `BLOCKER == 0` AND `MAJOR ≥ 1` | `WARN` |
| `BLOCKER == 0` AND `MAJOR == 0` AND nothing `unverified` AND every reviewer reported | `ALLOW` | | `BLOCKER == 0` AND `MAJOR == 0` | `ALLOW` |
| `BLOCKER == 0` AND `MAJOR == 0` AND (`unverified` non-empty OR a reviewer did not report) | `WARN` |
The fail-closed row never RAISES a verdict — it only withholds the clean
one. The worst case of a false `unverified` is `WARN` plus a stated reason;
the worst case of the old behaviour was a silent `ALLOW` over a live
BLOCKER.
**When `ALLOW` is withheld, the Executive Summary's FIRST sentence must say
so and name why** — e.g. "WARN: no blocking findings survived, but 1 finding
could not be verified (succinctness:title) and brief-conformance-reviewer did
not report." A withheld ALLOW that the reader cannot see is the same defect
in a new place.
Verdict is mechanical — never override. The verdict goes into the Verdict is mechanical — never override. The verdict goes into the
trailing JSON block AND the Executive Summary's first sentence. trailing JSON block AND the Executive Summary's first sentence.
@ -236,10 +181,8 @@ prefix). Flow-style `findings: [a, b]` breaks the frontmatter parser.
5. `## Findings (MAJOR)` — one subsection per MAJOR finding. 5. `## Findings (MAJOR)` — one subsection per MAJOR finding.
6. `## Findings (MINOR)` — one subsection per MINOR finding. 6. `## Findings (MINOR)` — one subsection per MINOR finding.
7. `## Findings (SUGGESTION)` — one subsection per SUGGESTION finding. 7. `## Findings (SUGGESTION)` — one subsection per SUGGESTION finding.
8. `## Suppressed Findings` (optional) — one line per finding removed by 8. `## Suppressed Findings` (optional) — one-line per finding dropped by
Pass 2 or Pass 3, with the reason AND its fate, tagged `[dropped]` or Pass 2 or Pass 3, with the reason.
`[unverified]`. Unverified lines come first: they are the ones that
withheld `ALLOW`.
9. `## Remediation Summary` — bullet count per severity + 1 sentence on 9. `## Remediation Summary` — bullet count per severity + 1 sentence on
what /trekplan will consume. what /trekplan will consume.
@ -261,7 +204,6 @@ The LAST fenced block in the file is a `json` block:
{ {
"verdict": "BLOCK | WARN | ALLOW", "verdict": "BLOCK | WARN | ALLOW",
"counts": { "BLOCKER": N, "MAJOR": N, "MINOR": N, "SUGGESTION": N }, "counts": { "BLOCKER": N, "MAJOR": N, "MINOR": N, "SUGGESTION": N },
"allow_blocked_by": ["unverified:succinctness:title (1)", "missing-reviewer:brief-conformance-reviewer", "unattributable-payload (1)"],
"findings": [ "findings": [
{ {
"id": "<40-char-hex>", "id": "<40-char-hex>",
@ -301,14 +243,9 @@ for the ID list.
the canonical 40-char SHA1 from `(file, line, rule_key, title)` using the canonical 40-char SHA1 from `(file, line, rule_key, title)` using
the algorithm in `lib/parsers/finding-id.mjs`. The frontmatter the algorithm in `lib/parsers/finding-id.mjs`. The frontmatter
`findings:` list and the JSON block IDs must match. `findings:` list and the JSON block IDs must match.
- **Suppressed findings are accountable.** When you remove a finding via - **Suppressed findings are accountable.** When you drop a finding via
Pass 2 or Pass 3, log it in `## Suppressed Findings` with the reason and Pass 2 or Pass 3, log it in `## Suppressed Findings` with the reason.
its fate (`[dropped]` / `[unverified]`). Silent drops break the audit Silent drops break the audit trail.
trail.
- **Never spend an unexamined finding as evidence of a clean review.** If a
removal did not refute the finding, it is `unverified` and `ALLOW` is off
the table. This is the one place where you may not be minimal: when in
doubt about a reason's fate, it is `unverified`.
- **No invention.** Never add a finding that did not appear in the - **No invention.** Never add a finding that did not appear in the
reviewer outputs. Never escalate a finding's severity beyond what the reviewer outputs. Never escalate a finding's severity beyond what the
catalogue specifies. catalogue specifies.

View file

@ -386,8 +386,8 @@ The mapping table is canonical:
- `high → {effort: high, model: opus}` (force opus for the high-confidence path) - `high → {effort: high, model: opus}` (force opus for the high-confidence path)
- `fable → {effort: high, model: fable}` (force Fable 5 for the max-quality path) - `fable → {effort: high, model: fable}` (force Fable 5 for the max-quality path)
The fable tier reuses `effort: high` semantics — full swarm, contrarian The fable tier reuses `effort: high` semantics — full swarm, contrarian +
always-on; `EFFORT_LEVELS` is unchanged (Voyage effort is orchestration gemini always-on; `EFFORT_LEVELS` is unchanged (Voyage effort is orchestration
shape, not model reasoning effort). Model reasoning effort is inherited from shape, not model reasoning effort). Model reasoning effort is inherited from
the session: Fable 5's default effort is `high`, NOT xhigh. To run xhigh, the the session: Fable 5's default effort is `high`, NOT xhigh. To run xhigh, the
operator sets it at session level via `/effort xhigh`, the `effortLevel` operator sets it at session level via `/effort xhigh`, the `effortLevel`
@ -825,7 +825,7 @@ topic.
> background orchestrator-agents could not spawn the research swarm — > background orchestrator-agents could not spawn the research swarm —
> before Claude Code 2.1.172 the harness did not expose the Agent tool to > before Claude Code 2.1.172 the harness did not expose the Agent tool to
> sub-agents, so a background run silently degraded to single-context > sub-agents, so a background run silently degraded to single-context
> reasoning without WebSearch / Tavily / WebFetch (see v2.4.0 > reasoning without WebSearch / Tavily / WebFetch / Gemini (see v2.4.0
> release notes). As of CC 2.1.172 sub-agents can spawn sub-agents (up to 5 > release notes). As of CC 2.1.172 sub-agents can spawn sub-agents (up to 5
> levels deep), so a delegated redesign is under evaluation (see > levels deep), so a delegated redesign is under evaluation (see
> `docs/cc-upgrade-2.1.181-decision-matrix.md`, W1/CC-26). Until then, > `docs/cc-upgrade-2.1.181-decision-matrix.md`, W1/CC-26). Until then,

View file

@ -419,46 +419,19 @@ to stderr but do NOT block the stop; `progress.json` is still authoritative.
`status: stopped`) so the next-session producer-mismatch check has both `status: stopped`) so the next-session producer-mismatch check has both
candidates available. Use the same combined ESM block pattern as Phase 8. candidates available. Use the same combined ESM block pattern as Phase 8.
### Check 2 — Plan file reaches every worktree ### Check 2 — Plan file is tracked by git
Worktrees are created from HEAD, so tracking the plan file is the cheapest way Run `git ls-files --error-unmatch {plan-path} 2>/dev/null`. If the plan file is
to make it visible in each one. But the project directory may be **gitignored** untracked (exit code != 0):
`.claude/projects/` is tool-managed and local-only, and a repo that ignores it
is normal, not exotic. `git add -f` is **not** the answer there: it would push
operator-local artifacts into history, and into whatever remote the repo
publishes to. When the plan file is ignored, Phase 2.6 Step 2a' (which copies
brief/plan/research into each worktree) is the delivery path, and this check
must step aside instead of failing.
```bash ```bash
PLAN_PATH="{plan-path}" git add {plan-path}
if git ls-files --error-unmatch "$PLAN_PATH" >/dev/null 2>&1; then git commit -m "chore: track plan file for parallel execution"
PLAN_TRACKING="tracked"
else
git check-ignore -q "$PLAN_PATH"
case "$?" in
0) PLAN_TRACKING="ignored" ;;
1) PLAN_TRACKING="untracked" ;;
*) echo "Error: git check-ignore failed on $PLAN_PATH - a fatal probe is not an answer about ignore status." >&2
exit 1 ;;
esac
fi
if [ "$PLAN_TRACKING" = "untracked" ]; then
git add "$PLAN_PATH"
git commit -m "chore: track plan file for parallel execution"
fi
``` ```
Report by outcome: Report: `Plan file committed for worktree visibility.`
| `PLAN_TRACKING` | Report | This ensures every worktree created from HEAD will have the plan file.
|---|---|
| `tracked` | `Plan file already tracked.` |
| `untracked` | `Plan file committed for worktree visibility.` |
| `ignored` | `Plan file is gitignored — not forced into history. Step 2a' copies it into each worktree.` |
Any other `git check-ignore` exit code is fatal and stops execution: a probe
that failed is not a probe that answered "not ignored".
### Check 3 — Scope fence overlap validation ### Check 3 — Scope fence overlap validation
@ -505,7 +478,7 @@ If cleanup fails, report the manual commands and stop.
After all 4 checks pass: After all 4 checks pass:
``` ```
Pre-flight: PASS (clean tree, plan reaches worktrees, no overlaps, no stale worktrees) Pre-flight: PASS (clean tree, plan tracked, no overlaps, no stale worktrees)
``` ```
## Phase 2.6 — Multi-session orchestration (worktree-isolated) ## Phase 2.6 — Multi-session orchestration (worktree-isolated)
@ -631,45 +604,27 @@ Insert this block AFTER the worktree-creation loop and BEFORE wave dispatch
```bash ```bash
PROJECT_SOURCE="$(realpath "${PROJECT_DIR}")" PROJECT_SOURCE="$(realpath "${PROJECT_DIR}")"
REPO_ROOT_REAL="$(realpath "${REPO_ROOT}")"
# Compute destination relpath: PROJECT_DIR relative to REPO_ROOT. # Compute destination relpath: PROJECT_DIR relative to REPO_ROOT.
# This makes $wt/$PROJECT_REL valid regardless of whether the operator # This makes $wt/$PROJECT_REL valid regardless of whether the operator
# passed --project as relative (.claude/projects/...) or absolute. # passed --project as relative (.claude/projects/...) or absolute.
# python3 + os.path.relpath is stdlib and portable - see the note below the PROJECT_REL="$(realpath --relative-to="$REPO_ROOT" "$PROJECT_SOURCE")"
# block for why no realpath flag may be used here.
PROJECT_REL="$(python3 -c 'import os.path,sys; print(os.path.relpath(sys.argv[1], sys.argv[2]))' "$PROJECT_SOURCE" "$REPO_ROOT_REAL")"
case "$PROJECT_REL" in
""|..*)
echo "Error: cannot derive a project relpath inside the repo ($PROJECT_SOURCE vs $REPO_ROOT_REAL)." >&2
exit 1 ;;
esac
for wt in "$WORKTREE_DIR"/session-*; do for wt in "$WORKTREE_DIR"/session-*; do
[ -d "$wt" ] || continue [ -d "$wt" ] || continue
mkdir -p "$wt/$PROJECT_REL" mkdir -p "$wt/$PROJECT_REL"
cp "$PROJECT_SOURCE"/brief.md "$wt/$PROJECT_REL/" cp "$PROJECT_SOURCE"/brief.md "$wt/$PROJECT_REL/"
cp "$PROJECT_SOURCE"/plan.md "$wt/$PROJECT_REL/" cp "$PROJECT_SOURCE"/plan.md "$wt/$PROJECT_REL/"
if [ -d "$PROJECT_SOURCE/research" ]; then [ -d "$PROJECT_SOURCE/research" ] && \
cp -r "$PROJECT_SOURCE/research" "$wt/$PROJECT_REL/" cp -r "$PROJECT_SOURCE/research" "$wt/$PROJECT_REL/"
fi
done done
``` ```
Do not "improve" the relpath line into `realpath --relative-to=...`. That flag Note: `realpath --relative-to` is GNU coreutils. macOS users without
is GNU coreutils only; BSD `realpath` (the macOS default) rejects it, and the `coreutils` (Homebrew `brew install coreutils` provides `grealpath`) may
failure is **silent** — the command substitution leaves `PROJECT_REL` empty, so substitute a Python fallback:
`mkdir -p "$wt/"` and `cp ... "$wt//"` both succeed and drop `brief.md`/`plan.md` `python3 -c "import os.path,sys; print(os.path.relpath(sys.argv[1], sys.argv[2]))" "$PROJECT_SOURCE" "$REPO_ROOT"`.
at the worktree root, where no child session looks for them. Measured on an
Intel Mac 2026-08-31: `realpath --relative-to=... ` → `realpath: illegal option
-- -`, while bare `realpath <path>` works; a whole wave ran with zero steps
executed. Both `realpath` calls above are bare path resolution, which BSD and
GNU handle identically; resolving both operands before the relpath is what keeps
it correct when one side goes through a symlink (macOS `/var``/private/var`).
Failure modes: an underivable relpath (empty, or outside the repo) aborts Failure mode: any `cp` failure exits the wave non-zero; reported via Step 4
before anything is copied — better a loud stop than files delivered where no cleanup. Source: brief Constraint 2.
session reads them; any `cp` failure exits the wave non-zero, reported via
Step 4 cleanup. A project without `research/` is not a failure.
Source: brief Constraint 2.
**2b. Launch sessions in this wave (each in its own worktree):** **2b. Launch sessions in this wave (each in its own worktree):**

View file

@ -898,33 +898,22 @@ validator-only; this surface just makes the friendly hint readable.
### High-effort behavior (v5.1.1) ### High-effort behavior (v5.1.1)
When `phase_signal_result.effort == 'high'` for the `plan` phase, the When `phase_signal_result.effort == 'high'` for the `plan` phase, after
additional adversarial Pass 2 on the post-revision plan is Phase 9 (plan-critic + scope-guardian dedup pass) runs to completion on
**unavailable, skipped**. Append the section header to plan.md before the post-revision plan, run an ADDITIONAL `gemini-bridge` plan-review
the trailing JSON block with exactly that status, so a reader can tell pass on the post-revision plan. Surface its findings as a separate
a deliberately dropped pass from one that failed: `## Adversarial Pass 2 (gemini-bridge, v5.1.1 high-effort)` section
appended to plan.md before the trailing JSON block.
``` Rationale (per risk-assessor finding + Decision B substitution
## Adversarial Pass 2 (v5.1.1 high-effort) 2026-05-14): the originally-considered "extra plan-critic-iterasjon"
risked a revision-loop because plan-critic dedup keys on
`(file, line, rule_key)` triplets and post-revision line numbers shift.
The gemini-bridge pass is independent (different agent, different
perspective) and does not re-tread the same dedup space — it surfaces
genuinely new findings rather than re-emitting closed ones.
**Status:** unavailable, skipped — the third-party bridge agent that Standard and low effort: do NOT run the additional pass.
ran this pass was dropped in v5.10.1 by operator directive. No
substitute is wired. Phase 9 (plan-critic + scope-guardian) is the
complete adversarial coverage for this plan.
```
Do NOT fail, retry, or silently omit the section: a high-effort plan
that carries no Pass 2 marker is indistinguishable from one whose Pass
2 crashed.
History (why the slot exists): Decision B, 2026-05-14, substituted a
bridge pass for an "extra plan-critic-iterasjon" because plan-critic
dedup keys on `(file, line, rule_key)` triplets and post-revision line
numbers shift, risking a revision-loop. Dropping the bridge does not
revive that option — re-running plan-critic carries the same loop risk
it always did.
Standard and low effort: do NOT emit the section at all.
## Hard rules ## Hard rules

View file

@ -32,7 +32,7 @@ Supported flags:
1. `--quick` — lightweight research, no agent swarm. The command itself does 1. `--quick` — lightweight research, no agent swarm. The command itself does
3-5 targeted searches inline. Set **mode = quick**. 3-5 targeted searches inline. Set **mode = quick**.
2. `--local` — only codebase research. Skip external research agents. 2. `--local` — only codebase research. Skip external agents and gemini bridge.
Set **scope = local**. Set **scope = local**.
3. `--external` — only external research. Skip codebase analysis agents. 3. `--external` — only external research. Skip codebase analysis agents.
@ -87,14 +87,9 @@ Supported flags:
behavior). `swarm` runs Voyage's own external-research agent swarm; behavior). `swarm` runs Voyage's own external-research agent swarm;
`deep-research` delegates the external phase to Claude Code's built-in `deep-research` delegates the external phase to Claude Code's built-in
`/deep-research` dynamic workflow and adapts its report into the research-brief `/deep-research` dynamic workflow and adapts its report into the research-brief
schema. **The delegation only works inside a closed version window: schema (requires Claude Code 2.1.154+ and dynamic workflows enabled; falls back
`2.1.154 <= CC < 2.1.218`** (dynamic workflows enabled). From Claude Code to `swarm` and notes the fallback if unavailable — never hard-fails). Orthogonal
**2.1.218** `/deep-research` starts only when the operator invokes it, and the to `--profile`/`phase_signals`; only affects the external phase. Set
Skill tool refuses a model invocation outright (`disable-model-invocation`), so
on 2.1.218 or newer the engine **always** falls back to `swarm`. The flag is
kept as an additive opt-in and **never hard-fails**: outside the window it
degrades to `swarm` and logs the reason. Orthogonal to
`--profile`/`phase_signals`; only affects the external phase. Set
**engine = {swarm|deep-research}** (the *requested* engine). **engine = {swarm|deep-research}** (the *requested* engine).
Flags can be combined: Flags can be combined:
@ -121,7 +116,7 @@ Usage: /trekresearch <research question>
Modes: Modes:
default Interview → foreground research (local + external) → brief default Interview → foreground research (local + external) → brief
--quick Interview (short) → inline research (no agent swarm) --quick Interview (short) → inline research (no agent swarm)
--local Only codebase analysis agents (skip external research) --local Only codebase analysis agents (skip external + Gemini)
--external Only external research agents (skip codebase analysis) --external Only external research agents (skip codebase analysis)
--fg No-op alias (foreground is the only mode as of v2.4.0) --fg No-op alias (foreground is the only mode as of v2.4.0)
--project Write brief into an trekbrief project folder (auto-indexed) --project Write brief into an trekbrief project folder (auto-indexed)
@ -240,8 +235,8 @@ Then continue to the next phase inline.
> did not expose the Agent tool to sub-agents, so an orchestrator launched > did not expose the Agent tool to sub-agents, so an orchestrator launched
> with `run_in_background: true` could not spawn the documented research > with `run_in_background: true` could not spawn the documented research
> swarm (`docs-researcher`, `community-researcher`, etc.) and silently > swarm (`docs-researcher`, `community-researcher`, etc.) and silently
> degraded to single-context reasoning without WebSearch / Tavily / WebFetch. > degraded to single-context reasoning without WebSearch / Tavily / WebFetch
> As of CC 2.1.172 sub-agents can spawn sub-agents (up to 5 levels > / Gemini. As of CC 2.1.172 sub-agents can spawn sub-agents (up to 5 levels
> deep), so that block no longer holds — a delegated redesign is under > deep), so that block no longer holds — a delegated redesign is under
> evaluation (see `docs/cc-upgrade-2.1.181-decision-matrix.md`, W1/CC-26). > evaluation (see `docs/cc-upgrade-2.1.181-decision-matrix.md`, W1/CC-26).
> Until then, running the phases inline in main context keeps the swarm > Until then, running the phases inline in main context keeps the swarm
@ -333,13 +328,9 @@ changes nothing (SC1). Keep the native-swarm anchors intact ("in parallel",
1. **Coarse pre-gate (best-effort, NOT a trust signal).** `Bash: claude --version`; 1. **Coarse pre-gate (best-effort, NOT a trust signal).** `Bash: claude --version`;
parse the leading `X.Y.Z` (e.g. from `2.1.196 (Claude Code)`) and compare parse the leading `X.Y.Z` (e.g. from `2.1.196 (Claude Code)`) and compare
numerically against **both ends** of the supported window numerically against `2.1.154` — split each on `.` and compare major, then minor,
`2.1.154 <= version < 2.1.218` — split each on `.` and compare major, then minor,
then patch as integers (do NOT string-compare; lexical comparison mis-orders then patch as integers (do NOT string-compare; lexical comparison mis-orders
multi-digit patch numbers). If the version is `< 2.1.154` (below the multi-digit patch numbers). If the version is `< 2.1.154`, OR if
dynamic-workflows floor), OR `>= 2.1.218` (the ceiling: `/deep-research` starts
only when the operator invokes it, and the Skill tool refuses a model invocation
with `disable-model-invocation` — see step 4's reason token), OR if
`disableWorkflows: true` / `CLAUDE_CODE_DISABLE_WORKFLOWS=1` is set, skip to the `disableWorkflows: true` / `CLAUDE_CODE_DISABLE_WORKFLOWS=1` is set, skip to the
fallback (step 4). **If `claude` is not on PATH inside the Bash tool (possible fallback (step 4). **If `claude` is not on PATH inside the Bash tool (possible
under `claude -p`) or the version cannot be parsed, treat the pre-gate as under `claude -p`) or the version cannot be parsed, treat the pre-gate as
@ -356,18 +347,14 @@ changes nothing (SC1). Keep the native-swarm anchors intact ("in parallel",
`/deep-research` report actually landed in context — substantive findings with `/deep-research` report actually landed in context — substantive findings with
citations, not an empty/denied/errored turn and not bare error text. This check citations, not an empty/denied/errored turn and not bare error text. This check
must be **robust to all failure manifestations** (workflow disabled, approval must be **robust to all failure manifestations** (workflow disabled, approval
denied, runtime error, empty output, or the Skill tool refusing with denied, runtime error, empty output), because the disabled-headless behavior is
`disable-model-invocation` on CC 2.1.218+), because the disabled-headless undocumented: no recognizable cited report in context → fall back, regardless of
behavior is undocumented: no recognizable cited report in context → fall back, how the failure surfaces.
regardless of how the failure surfaces.
4. **On no real report (fallback):** set `effective_engine = swarm`, run the swarm 4. **On no real report (fallback):** set `effective_engine = swarm`, run the swarm
blocks below, and **log the fallback at this decision point** — print blocks below, and **log the fallback at this decision point** — print
`Engine: deep-research → swarm (fallback: <reason>)` and carry the reason into the `Engine: deep-research → swarm (fallback: <reason>)` and carry the reason into the
Phase-8 Present summary and the brief's `## Executive Summary`. Known reason Phase-8 Present summary and the brief's `## Executive Summary`. **NEVER fabricate
tokens: `disable-model-invocation` (CC >= 2.1.218 — the expected reason on any
current Claude Code), `version-below-floor`, `workflows-disabled`,
`no-cited-report`. **NEVER fabricate
or synthesize a substitute report** — a structurally-valid-but-invented brief or synthesize a substitute report** — a structurally-valid-but-invented brief
passes the structure-only validator and silently poisons `/trekplan`; that is the passes the structure-only validator and silently poisons `/trekplan`; that is the
worst outcome of this feature. worst outcome of this feature.
@ -391,6 +378,11 @@ Launch the new research-specialized agents:
For each external agent, pass: the research question, specific dimensions to For each external agent, pass: the research question, specific dimensions to
investigate, and any context from the interview. investigate, and any context from the interview.
### Bridge agent (scope = both or external, if enabled)
Launch `gemini-bridge` with the research question. Do NOT include findings from
other agents — the value of Gemini is independence.
### Launch rules ### Launch rules
- Launch ALL selected agents **in parallel** in a single message - Launch ALL selected agents **in parallel** in a single message
@ -751,7 +743,7 @@ Present a summary to the user:
**Engine (effective):** {swarm | deep-research}{, with fallback reason if it fell back} **Engine (effective):** {swarm | deep-research}{, with fallback reason if it fell back}
**Confidence:** {overall confidence 0.0-1.0} **Confidence:** {overall confidence 0.0-1.0}
**Dimensions:** {N} researched **Dimensions:** {N} researched
**Agents:** {N} local + {N} external **Agents:** {N} local + {N} external + {gemini: used | unavailable | skipped}
### Key Findings ### Key Findings
- {Finding 1} - {Finding 1}
@ -796,7 +788,7 @@ Record format (one JSON line):
"unique_sources": {N}, "unique_sources": {N},
"agents_local": {N}, "agents_local": {N},
"agents_external": {N}, "agents_external": {N},
"gemini_used": false, "gemini_used": {true|false},
"confidence": {0.0-1.0}, "confidence": {0.0-1.0},
"contradictions": {N}, "contradictions": {N},
"open_questions": {N} "open_questions": {N}
@ -887,16 +879,15 @@ commands surface, don't re-enforce.
When `phase_signal_result.effort == 'high'` for the `research` phase, When `phase_signal_result.effort == 'high'` for the `research` phase,
run the FULL swarm regardless of normal triggering rules: 5 local run the FULL swarm regardless of normal triggering rules: 5 local
agents + 4 external agents, AND force `contrarian-researcher` to agents + 4 external agents + 1 bridge agent, AND force
always-on. Normally `contrarian-researcher` triggers conditionally when `contrarian-researcher` AND `gemini-bridge` to always-on. Normally
a leading recommendation is emerging from initial agents; in `contrarian-researcher` triggers conditionally when a leading
high-effort mode it runs unconditionally so the final brief always recommendation is emerging from initial agents; in high-effort mode it
carries an adversarial counter-evidence pass. runs unconditionally so the final brief always carries an adversarial
counter-evidence pass. Similarly, `gemini-bridge` normally activates on
The former third-party second-opinion bridge pass is **unavailable, significant architectural questions or when triangulation value is
skipped** (dropped in v5.10.1 by operator directive). High effort no high; in high-effort mode it runs unconditionally to provide an
longer has a bridge arm; `gemini_used` is a vestigial stats field independent second opinion.
pinned to `false`.
High effort additionally arms the Phase 5 bounded follow-up loop (max 3 High effort additionally arms the Phase 5 bounded follow-up loop (max 3
turns per under-illuminated dimension, budgeted by turns per under-illuminated dimension, budgeted by
@ -943,7 +934,7 @@ Low effort: inline research only, no agent swarm (existing
local finding from steering an external agent's question. Attributing the bias local finding from steering an external agent's question. Attributing the bias
risk to it left that risk with no control while the text read as though it had risk to it left that risk with no control while the text read as though it had
one. one.
- **Graceful degradation:** If MCP tools are unavailable (Tavily, MS Learn), - **Graceful degradation:** If MCP tools are unavailable (Tavily, Gemini, MS Learn),
proceed with available tools and note limitations in brief metadata. proceed with available tools and note limitations in brief metadata.
- **Cost:** Model resolution at Agent-spawn sites is a three-layer fallback: - **Cost:** Model resolution at Agent-spawn sites is a three-layer fallback:
brief `phase_signals[<phase>].model` > `profile.phase_models[<phase>]` > brief `phase_signals[<phase>].model` > `profile.phase_models[<phase>]` >

View file

@ -249,37 +249,6 @@ do not feed unvalidated findings to the coordinator.
In `quick` mode, launch only `code-correctness-reviewer`. The Executive In `quick` mode, launch only `code-correctness-reviewer`. The Executive
Summary will note the brief-conformance pass was skipped. Summary will note the brief-conformance pass was skipped.
### Reviewer accounting — every expected reviewer MUST report
Write down the expected reviewer set BEFORE the spawn: both reviewers in
default mode, `code-correctness-reviewer` alone in `quick` mode. After the
spawn, account for each one by name.
**Zero findings from a silent reviewer is indistinguishable from zero findings
from a clean diff** — unless you check. A reviewer is *accounted for* only when
it returned a payload that validated. Three ways it fails to:
| Failure | Handling |
|---------|----------|
| Output fails the schema after the 2 bounded re-asks | STOP (already specified above) |
| Returned no final message at all | Re-ask that reviewer **once**. Still nothing → STOP. |
| Was never launched (spawn error, wrong mode) | STOP. |
**On STOP: name the reviewer and the failure, and do not proceed to Phase 6.**
Do not let the coordinator compute a verdict over a review one of whose
reviewers never spoke — the count would be complete-looking and wrong. This is
the same shape as the schema branch above ("do not feed unvalidated findings to
the coordinator"), applied to the other two ways a reviewer can go missing.
A reviewer that ran but never delivered is most often the return-channel
defect: check `~/.claude/projects/<proj>/<session>/subagents/agent-*.jsonl` for
its final assistant block before re-asking, and confirm no `name` parameter was
passed at the spawn (see the warning at the top of this phase).
If you proceed anyway under an explicit operator instruction, pass the expected
set to the coordinator as `expectedReviewers` so the missing reviewer at least
forbids `ALLOW` (`lib/review/coordinator-contract.mjs`, `missing_reviewers`).
## Phase 6 — Coordinator dedup + verdict ## Phase 6 — Coordinator dedup + verdict
Launch `review-coordinator` (Agent tool) with the merged findings array Launch `review-coordinator` (Agent tool) with the merged findings array
@ -290,20 +259,10 @@ The coordinator runs the 4-pass process documented in
1. **Dedup** by `(file, line, rule_key)` triplet. 1. **Dedup** by `(file, line, rule_key)` triplet.
2. **HubSpot Judge filters** — Succinctness, Accuracy, Actionability. 2. **HubSpot Judge filters** — Succinctness, Accuracy, Actionability.
3. **Cloudflare reasonableness**remove speculative or catalogue-violating 3. **Cloudflare reasonableness**drop speculative or catalogue-violating
findings (skipped in `quick` mode). findings (skipped in `quick` mode).
4. **Verdict** — BLOCK / WARN / ALLOW per the threshold table. 4. **Verdict** — BLOCK / WARN / ALLOW per the threshold table.
**Fail-closed.** Every removal in Pass 2 and Pass 3 is either
**dropped** (the test refuted the finding as a claim about this codebase) or
**unverified** (the finding was removed without its claim ever being settled).
A non-empty `unverified` bucket forbids `ALLOW`; the verdict becomes `WARN` and
the Executive Summary's first sentence must say why. The fail-closed rule never
raises a verdict — it only withholds the clean one. Fate table, reason
vocabulary, and the `allow_blocked_by` field: `agents/review-coordinator.md`
§*Suppression is two-valued*, mirrored deterministically in
`lib/review/coordinator-contract.mjs`.
The coordinator's output is the full review.md content — frontmatter + The coordinator's output is the full review.md content — frontmatter +
body sections + trailing JSON block. Do NOT re-run the reviewers based body sections + trailing JSON block. Do NOT re-run the reviewers based
on the coordinator's output. on the coordinator's output.

View file

@ -1,295 +0,0 @@
# BRIEF — external assessment (VURDERING-V2 §5.12), verified against this repo
**Status:** assessment complete. **No implementation in this session** (scope-guard).
**Source under review:** `claude-playlist-corpus/docs/VURDERING-V2.md` §5.12 (+ §3 G1G8, §4),
transferred via that repo's `docs/OVERFORING-V2.md` §5. It is an external recommendation
built on 442 video analyses; its repo facts come from a subagent survey (2026-07-17) that
is **not** re-verified and in which at least one error was already demonstrated.
**Method.** Every asserted GAP is marked **BEKREFTET** (confirmed) / **AVKREFTET**
(refuted) / **ENDRET** (true in altered form) only after checking it against code in this
repo. Every Claude Code *feature* claim is checked against the OKF bundle at
`claude-code-llm-wiki/bundle/`, index-first — **where V2 and the bundle disagree, the
bundle wins**. Absences are positive-controlled before being reported (a grep that finds
nothing is a measurement, not a fact).
---
## TL;DR
Of six recommended tiltak, **one is a real defect** (fail-open verdict computation),
**one is unmeasured rather than unresolved** (the Workflow port), **three are already
decided or already partly built**, and **one is a cheap confirmed gap**.
The single most consequential correction: V2 presents the Workflow port as the answer to
this repo's "main-context relief is unmeasured" caveat. The bundle confirms the
*mechanism*, but **this repo has still never measured main-context relief on a large
fan-out** — the one bake-off that exists measured *subagent* tokens and says so. The port
is therefore neither refuted nor justified. The next step is a measurement, not a port,
and the measurement design already exists in `docs/T1-cc26-delegated-orchestration.md`.
---
## Tiltak 1 — Port orchestration to dynamic Workflows
**Verdict: split per command. Do not issue one global verdict.**
### `/trekreview`**ENDRET** (already ported, and measured)
The port exists: opt-in `--workflow`, `scripts/trekreview-armB.workflow.mjs`, documented at
`commands/trekreview.md:270` (*§ Phase 56 via the Workflow substrate*). The S10 bake-off
(`docs/T2-bakeoff-results.md`, ≥3 runs/arm) found it **fidelity-equivalent**: verdict-match
1.0, issue-coverage 100%, `(file,rule_key)` jaccard ≥ within-arm. It stays opt-in because
the Workflow tool raises the consumer floor to Claude Code 2.1.154+, and the prose path
keeps the lower floor.
### `/trekplan`, `/trekresearch` — **BEKREFTET as unported, UNMEASURED as beneficial**
Both still orchestrate their swarms from main context (`commands/trekplan.md:406`,
`commands/trekresearch.md:388`). That part of V2 is correct.
What is **not** established is V2's stated effect — that Workflow answers the
main-context-relief caveat. Two measurements exist in this repo and **neither measures it**:
| Measurement | What it actually measured | Bearing on the relief claim |
|---|---|---|
| `docs/T2-bakeoff-results.md:154` | **Subagent** tokens: Arm A median 92.8k vs Arm B 96.9k ⇒ **+4.4%** | None. The same line states Arm A's main-context hand-orchestration cost is **uncounted**. |
| `docs/T1-synthesis-poc-results.md` | Δ **main-context****0%** — for delegating *Phase 7 synthesis alone* | Negative, but out of scope: that doc itself names a **Phase-5 redesign** as the prerequisite for any relief. |
So the Phase-5 swarm — the case V2 is actually recommending — has never been measured in
either arm, in either command.
**The measurement already has a design.** `docs/T1-cc26-delegated-orchestration.md:118`
defines the gate metric (Δ main-context tokens resident in the main session at
plan-complete, from stream-json `usage`) with pre-registered bars: adopt at ≥30%, reject
below 15%. Running Arm A vs Arm B once against the trekplan swarm settles this.
**Cost framing must be corrected.** The bundle states plainly that a workflow run "can use
meaningfully more tokens than working through the same task in conversation"
(`bundle/concepts/docs-en-workflows.md:364`) — consistent with the measured +4.4%. The port
cannot be justified as a cost saving. **Main-context headroom is the only defensible
motive, and it is exactly the unmeasured quantity.**
**Recommendation:** measure before porting. Treat V2's tiltak 1 as a *hypothesis with a
ready-made experiment*, not as an approved change.
---
## Tiltak 2 — Codify the holdout rules
**Verdict: (a) and (b) satisfied in practice but uncodified · (c) BEKREFTET, a real defect ·
(d) ENDRET.** This is the tiltak with genuine substance.
### (a) Reviewers must never be forks — **AVKREFTET as a live defect, BEKREFTET as uncodified**
Reviewers are spawned as plain subagents (`voyage:brief-conformance-reviewer`,
`voyage:code-correctness-reviewer`). `subagent_type: "fork"` appears nowhere in `commands/`
or `agents/`; the only `subagent_type` in the tree is `"Explore"` at
`commands/trekplan.md:501`, an exploration spawn, not a review spawn. The bundle confirms
the rule is real — a forked subagent "inherits your full conversation context instead of
starting fresh" (`bundle/concepts/docs-en-agents.md:37`). Voyage satisfies it by habit;
nothing written forbids a future edit from breaking it.
### (b) Explicit "you shall not see" list — **AVKREFTET as a live defect, BEKREFTET as uncodified**
Phase 5 feeds reviewers exactly four things: the Phase-3 unified diff, the triage map, the
brief path, and the rule catalogue (`commands/trekreview.md`, Phase 5 input list).
Implementation plans and commit messages are **not** fed; `git log` appears only to compute
the diff range and to print a suggested narrowing command (`commands/trekreview.md:132`,
`:181`). Again: correct today, unprotected tomorrow.
### (c) Fail-closed verdicts — **BEKREFTET. This is the one real defect.**
`agents/review-coordinator.md` computes the verdict mechanically from the findings that
*survive* filtering:
- **Pass 2** (HubSpot judge) and **Pass 3** (Cloudflare reasonableness) **drop** findings
that fail their tests (`agents/review-coordinator.md:87`, `:101`).
- **Pass 4** counts the survivors: `BLOCKER ≥ 1 → BLOCK`; else `MAJOR ≥ 1 → WARN`; else
`ALLOW` (`:132`). "Verdict is mechanical — never override."
There is no third state. Three consequences follow:
1. **A finding that cannot be substantiated is dropped, and a dropped finding contributes
zero — which moves the verdict toward ALLOW.** Pass 3's non-existent-file test ("`file`
does not exist in the working tree AND the diff does not show it as `(new file)`") has
no inconclusive branch: unresolvable and refuted are treated identically. This is
structurally the failure V2 attributes to its source case, where "not E2E testable"
passed as approved.
2. **A reviewer that returns nothing produces the same result.** Phase 5 has no
empty-return handling — grep for `empty|no findings|did not return|abort` over
`commands/trekreview.md` returns 0 (positive control: `Phase 5` = 7 hits, `reviewer` =
24 hits, so the file and the query are both live). Zero findings from a silent reviewer
is indistinguishable from zero findings from a clean diff. This is the same surface as
the idle-agent hole recorded in STATE's open decisions.
3. **One fail-closed branch already exists, and it is the pattern to copy.** When reviewer
output fails schema validation, Phase 5 allows 2 bounded re-asks and then *stops*: "do
not feed unvalidated findings to the coordinator." That is the correct shape, applied to
one failure mode only.
**The defect is codified, not merely prose — which makes it testable.** The four passes
exist deterministically in `lib/review/coordinator-contract.mjs`, and
`computeVerdict(findings)` (`:184``:193`) counts *only* the findings handed to it.
`runContract` hands it `reasoned.kept` (`:206`); `suppressed` and `skipped` are returned in
the result object but **carry no weight in the verdict**. So a dropped finding is
arithmetically identical to a finding that never existed.
**Minimal fix shape (not implemented here):** an `unverified` bucket that is neither kept
nor dropped, plus one rule — *a non-empty `unverified` bucket forbids ALLOW* — and a
Phase-5 check that a reviewer actually returned. Both are additive; neither touches the
severity catalogue or the existing thresholds. Because the logic is deterministic ("No LLM,
no network, no time, no randomness", `:23`) and already has
`tests/lib/coordinator-contract.test.mjs`, this can be driven test-first under the Iron
Law — a failing test asserting that an unverifiable BLOCKER-severity finding cannot yield
ALLOW is writable before any production change.
### (d) Presume-failure framing — **ENDRET**
Both reviewers are already adversarially framed: "Adversarial reviewer" in each
`description`, "You never praise", "You never say 'looks good'"
(`agents/code-correctness-reviewer.md:36`, `agents/brief-conformance-reviewer.md:35`). What
is absent is specifically the presume-failure formulation V2 names ("this agent was lazy —
find out why"). This is a wording change with no measurement behind it in the corpus; treat
it as optional polish, not a gap.
---
## Tiltak 3 — Lightweight lane — **ENDRET** (narrower gap than stated, already tracked)
A lightweight path exists: `--quick` on both `/trekbrief` and `/trekplan`, and
`/trekplan --quick` already skips the Phase-5 exploration swarm — "Skip agent swarm; use
lightweight Glob/Grep scan and go directly to planning + adversarial review"
(`commands/trekplan.md:122`). The same code-path is reachable a second way: a brief
carrying `effort == 'low'` activates it without the flag (`:885`). What it does **not** do
is bypass the 2.2 ceremony —
`commands/trekbrief.md:126` states the framing question "is asked even in `--quick` mode",
and a 2.2 brief still requires `framing` and a `## TL;DR` (`:472`, `:474`).
So the accurate gap is not "no lightweight path" but "the lightweight path still pays the
2.2 toll". Already carried as open operator decision 1 in STATE — this is not a discovery.
---
## Tiltak 4 — Definition-of-done as an object — **BEKREFTET**
`commands/trekbrief.md` contains no notion of evidence, proof, verifier, residual risk, or
next-step owner (grep for `evidence|proof|screenshot|verifier|residual risk|owner` → 0;
positive control: `Success Criteri` = 5 hits). The gap is real.
**Cost note V2 does not carry:** the brief schema is **Handover 1**, a public contract
(`docs/HANDOVER-CONTRACTS.md`). Adding required DoD fields is a breaking change for
downstream consumers, i.e. a `brief_version` bump with a gate, not an edit. That moves it
out of "cheap win" and into planned work.
---
## Tiltak 5 — Prune, don't build — **mostly ALREADY DECIDED**
- **`synthesis-agent`**: already dormant and labelled as such (`agents/synthesis-agent.md:3`),
with the measurement that justified it (`docs/T1-synthesis-poc-results.md`, Δ≈0,
DECLINED). Retiring versus keeping was decided in favour of keeping it as a
re-measurable building block. V2 recommends a decision that has been made and recorded.
- **24 all-opus agents**: 24 of 24 agent files carry `model: opus` (verified by count).
This is an **operator pin**, not drift — commit `40d8742` "pin all sub-agents to Opus
permanently (operator request)" — and the reconsideration V2 asks for already happened
and is written down in `docs/voyage-vs-cc-balance-analysis.md` §10. Re-opening it is an
operator decision, not an analysis task.
- **`gemini-bridge`**: **BEKREFTET**, and the cheapest real item in this tiltak. It is
already flagged `THIN_WRAP` / `DROP→NATIVE` in the same analysis (§V09, lines 81, 121,
149) *and* it is broken at the engine (the `gemini-mcp` server fails deterministically on
an SDK/API mismatch). Fix-or-drop is decidable today.
---
## Tiltak 6 — LSP as a harness component — **BEKREFTET**
`LSP` returns 0 hits across `commands/`, `agents/`, `lib/`, `docs/`. Voyage navigates with
Glob/Grep, as V2 says. One qualifier: LSP is a harness-level capability available to
agents, not a voyage feature — adoption is mostly permitting and prompting agents to use
it, not code in this repo. Value is highest in large target repos, which is where voyage's
exploration swarm actually runs.
---
## Bundle-gap
Checked against `bundle/concepts/docs-en-workflows.md`
(`source_sha 363819ed9ec325275ca22023f6bb6b98fbbf6fcc12db0667478c5207e117751b`,
timestamp 2026-08-15), plus `docs-en-agents.md` and `docs-en-sub-agents.md`.
**Confirmed by the bundle:**
- The **2.1.154 floor** for dynamic workflows, on paid plans.
- The **main-context relief mechanism**: "A workflow script holds the loop, the branching,
and the intermediate results itself, so Claude's context holds only the final answer"
(`:38`); "Intermediate results stay in script variables instead of landing in Claude's
context" (`:316`). V2's mechanism claim is sound — it is the *magnitude* that is unmeasured.
- **Per-stage model routing** (`:375``:378`).
- **Cost caps**: agent caps, size guideline, and a `Large workflow` warning above 25 agents
or 1.5M projected tokens (`:368`).
**Where V2 overstates, and the bundle wins:**
- **"tokenbudsjetter" (token budgets).** The page documents *caps and size guidelines*, not
a token budget — `budget` returns 0 hits on the page. V2's warning to "set an explicit
token budget" has no documented primitive to point at in the official docs.
- **"automatisk retry".** 0 hits. Not documented on the page.
**Limitation V2 omits, material for this repo:** resumability is **session-scoped**
"Resume works within the same Claude Code session. If you exit Claude Code while a workflow
is running, the next session starts the workflow fresh" (`:360`). Further, stopping mid
fan-out re-runs every agent that started after the stopped one (`:354`). Voyage is
explicitly a *multi-session* tool, so resumability is a weaker argument here than V2 implies.
**Gap proper (feedback toward wiki v1.0):**
1. The mirrored docs describe no programmatic **token-budget** primitive for workflows,
while the Workflow tool's own runtime surface does expose a budget derived from an
operator token directive. If that surface is real and stable, it is missing from the
mirror. Flagged as a discrepancy, not asserted as a doc error.
2. Nothing in the mirrored pages quantifies what a workflow's **final return** costs the
orchestrating session. The relief mechanism is described qualitatively only, so a
consumer cannot size the benefit from the docs alone — which is precisely why the
measurement below cannot be replaced by reading.
---
## Recommended order (operator decides; nothing started)
1. **Fail-closed coordinator (tiltak 2c)** — smallest surface, highest value, no
dependencies, and it closes a defect rather than adding a feature.
2. **Measure Δ main-context on the `/trekplan` Phase-5 swarm (tiltak 1)** — Arm A vs Arm B
against the pre-registered bars in `docs/T1-cc26-delegated-orchestration.md`. The port
decision follows the number; it does not precede it.
3. **`gemini-bridge` fix-or-drop (tiltak 5)** — already analysed, currently broken, cheap.
**Deferred with reasons:** tiltak 3 (already open operator decision 1) · tiltak 4 (requires
a `brief_version` bump against a public contract) · tiltak 5's opus/synthesis items
(decided; operator-pinned) · tiltak 6 (harness-level, not repo code).
---
## Verification log
| Claim | How verified |
|---|---|
| `/trekreview` Workflow port exists and was measured | `commands/trekreview.md:270`; `docs/T2-bakeoff-results.md` §Full run |
| +4.4% is **subagent** tokens, main context uncounted | `docs/T2-bakeoff-results.md:154` (verbatim) |
| Δ main-context ≈ 0 applies to Phase 7 only | `docs/T1-synthesis-poc-results.md:1`, `:100` |
| Measurement design already exists | `docs/T1-cc26-delegated-orchestration.md:118`, `:125` |
| Reviewers are not forks | `grep subagent_type\|fork commands/ agents/` → only `Explore` at `commands/trekplan.md:501` |
| Plans/commit messages not fed to reviewers | Phase 5 input list; `commands/trekreview.md:132`, `:181` |
| Verdict is computed from survivors only | `agents/review-coordinator.md:87`, `:101`, `:132` |
| Same rule codified deterministically in `lib/` | `lib/review/coordinator-contract.mjs:184``:193`, `:206`; existing test `tests/lib/coordinator-contract.test.mjs` |
| No empty-return handling in Phase 5 | grep → 0, positive control `Phase 5`=7, `reviewer`=24 |
| Existing fail-closed branch on schema failure | `commands/trekreview.md`, Phase 5 bounded-retry paragraph |
| `--quick` does not bypass 2.2 | `commands/trekbrief.md:126`, `:472`, `:474` |
| No DoD fields in trekbrief | grep → 0, positive control `Success Criteri`=5 |
| 24/24 agents pinned opus, by operator | file count 24/24; `git log 40d8742` |
| gemini-bridge already flagged for drop | `docs/voyage-vs-cc-balance-analysis.md:81`, `:121`, `:149` |
| No LSP usage | `grep -rn LSP commands/ agents/ lib/ docs/` → 0 |
| Workflow floor, relief mechanism, model routing, caps | `bundle/concepts/docs-en-workflows.md:19`, `:38`, `:316`, `:375`, `:368` |
| No documented token budget or retry in the bundle | `grep -i budget\|retry` on that page → 0 relevant |
| Resume is session-scoped | `bundle/concepts/docs-en-workflows.md:354`, `:360` |
| Fork inherits full conversation context | `bundle/concepts/docs-en-agents.md:37` |
**Not verified / stated as unverified:** V2's underlying video-corpus claims (the StrongDM
bad-merge case, the 5.5-hour bake-off, the "$200 plan in 30 minutes" workflow) were not
independently checked — they are cited here as V2's evidence, not as this repo's findings.

View file

@ -135,17 +135,6 @@ reused.
## 6. Cheaper PoC (audit-recommended, preferred over the full bake-off) ## 6. Cheaper PoC (audit-recommended, preferred over the full bake-off)
> **STATUS: RUN AND DECLINED (S12, 2026-06-18) — do not re-order as new work.**
> Result: **Δ main-context (faithful flow) = 0.0%**, NEGATIVE against the 15% keep-floor.
> Full method, fixtures and BASE-sweep: `docs/T1-synthesis-poc-results.md`;
> reproduce with `node scripts/synthesis-measure.mjs`.
> The finding is **structural, not stochastic**, so re-running it cannot change the number:
> Phase 5 spawns the exploration swarm **foreground** (`commands/trekplan.md:158,338-341`
> "foreground is the only mode as of v2.4.0"), so the 6-10 outputs are **already resident in
> main** before Phase 7 begins. Delegating only the Phase-7 digest returns *on top of* them
> and evicts nothing. A saving is reachable only by also moving Phase-5 delivery off-main —
> which is §5, not §6. `agents/synthesis-agent.md` therefore ships **dormant**.
Per `subagent-delegation-audit.md` §Recommendation, the lowest-risk way to test the delegation Per `subagent-delegation-audit.md` §Recommendation, the lowest-risk way to test the delegation
*premise* is **not** a wholesale orchestrator rewrite but **one narrow synthesis-agent** *premise* is **not** a wholesale orchestrator rewrite but **one narrow synthesis-agent**
(intervention #1/#3): delegate only trekplan Phase 7 synthesis (the heaviest single inline read — (intervention #1/#3): delegate only trekplan Phase 7 synthesis (the heaviest single inline read —
@ -180,17 +169,5 @@ promising orchestration-substrate question and is untouched by this.
only matters if a future nested pipeline approaches 5. only matters if a future nested pipeline approaches 5.
2. Classifier behaviour for a *purposeful* swarm under `auto`/`bypassPermissions` is unverified 2. Classifier behaviour for a *purposeful* swarm under `auto`/`bypassPermissions` is unverified
(F4) — must be in the §5/§6 test matrix before any delegated spawn ships to a headless path. (F4) — must be in the §5/§6 test matrix before any delegated spawn ships to a headless path.
3. ~~The §6 synthesis-agent PoC and the §5 full bake-off are both designed but unbuilt.~~ 3. The §6 synthesis-agent PoC and the §5 full bake-off are both **designed but unbuilt** — ready
**CORRECTED 2026-09-03 (S95).** This item was written in S7 and falsified the same afternoon to run if CC-26 is greenlit toward delegation.
by S12: the **§6 PoC was RUN and DECLINED** (`docs/T1-synthesis-poc-results.md` — Δ_faithful
= 0.0%, NEGATIVE). Only the **§5 full bake-off** remains unbuilt. The stale wording is what
caused the §6 PoC to be re-ordered as new work on 2026-09-02; it is struck rather than
deleted so the re-order has a traceable cause.
4. **Environment premise updated (measured 2026-09-03, CC 2.1.259).** S12 recorded the live run
as environment-blocked for want of an `ANTHROPIC_API_KEY`. That half no longer holds:
`claude -p --output-format stream-json` runs on subscription auth with no API key and emits
per-turn `usage` plus a `subagent_stats` block (spawned / max_depth / refused) that did not
exist in June — a ready-made classifier-interference counter for §5's feasibility guard.
S12's *other* blocker stands unmeasured: the installed plugin is the cache copy, so a
repo-local agent is invisible to `claude -p`. This changes nothing about the §6 verdict,
which is structural (see §6 status), and only lowers the cost of §5.

View file

@ -167,18 +167,10 @@ arm produces correct final text at 66 lines and at 3730 lines alike, and only
delivery fails, identically at both. The 38 recovered findings were re-used delivery fails, identically at both. The 38 recovered findings were re-used
instead of re-run. instead of re-run.
**A second external measurement closes the other half (akashic-intelligence What it does **not** close: their four cells were all named, so the *returning*
S27, commit `f168630`).** The gap left above was that all four S25 cells were (unnamed) arm still has no measurement above 66 lines. That a plain subagent
named, so the *returning* (unnamed) arm had no measurement above 66 lines. returns a 21 KB result at that scale is inferred, not observed. Stated as
S27 supplies one: denominator **2 of 2 unnamed agents**, against a 4370-line inferred.
plan; both returned, 30449 B and 10989 B, both valid JSON. The unnamed arm
therefore returns at full scale as observed fact, not as inference.
Two caveats, kept at the strength akashic itself stated them. The measurement
was taken by the repo that owns the finding, not by an independent third party.
And byte-identity between the returned string and the file on disk was not
proven — what is established is that a well-formed result of that size arrived,
not that it arrived unaltered.
Their PONG control also carries the same lesson as cell 1 above, in a third Their PONG control also carries the same lesson as cell 1 above, in a third
repo: S25 reported that agent as having "gone idle without sending PONG". It repo: S25 reported that agent as having "gone idle without sending PONG". It

View file

@ -98,7 +98,7 @@ Which native Claude Code primitive each pipeline step runs on today, and the alt
| **continue** | Inline reads `.session-state.local.json` → zero-confirm resume | CC `--resume` (transcript replay, not typed work-state → insufficient) | | **continue** | Inline reads `.session-state.local.json` → zero-confirm resume | CC `--resume` (transcript replay, not typed work-state → insufficient) |
| **cross-cutting** | 8 hook scripts: `pre-bash` + `pre-write` guards, `pre-agent-cap` loop-bound enforcement, `post-bash` stats, `session-title`, `pre-`/`post-compact` flush, **`Stop`→OTEL** export | — | | **cross-cutting** | 8 hook scripts: `pre-bash` + `pre-write` guards, `pre-agent-cap` loop-bound enforcement, `post-bash` stats, `session-title`, `pre-`/`post-compact` flush, **`Stop`→OTEL** export | — |
¹ MCP per research agent: `docs-researcher` → Microsoft Learn + Tavily · `community-`/`security-`/`contrarian-researcher` → Tavily (+ WebSearch/WebFetch). Graceful degradation when an MCP server is absent. ¹ MCP per research agent: `docs-researcher` → Microsoft Learn + Tavily · `community-`/`security-`/`contrarian-researcher` → Tavily (+ WebSearch/WebFetch) · `gemini-bridge` → Gemini Deep Research MCP. Graceful degradation when an MCP server is absent.
**Legend:** *production* = wired and active · *dormant* = shipped but wired to nothing (`synthesis-agent`) · *opt-in* = behind a flag (`--workflow`) · *not wired* = considered, deferred (delegated orchestrator, the `AskUserQuestion` brief engine). **Legend:** *production* = wired and active · *dormant* = shipped but wired to nothing (`synthesis-agent`) · *opt-in* = behind a flag (`--workflow`) · *not wired* = considered, deferred (delegated orchestrator, the `AskUserQuestion` brief engine).

View file

@ -20,13 +20,13 @@ Always interactive. Phase 3 is a section-driven completeness loop (no hard cap o
| _(default)_ | Interview + research (local + external) + synthesis + brief (foreground) | | _(default)_ | Interview + research (local + external) + synthesis + brief (foreground) |
| `--project <dir>` | Write brief to `{dir}/research/{NN}-{slug}.md` (auto-incremented) | | `--project <dir>` | Write brief to `{dir}/research/{NN}-{slug}.md` (auto-incremented) |
| `--quick` | Interview (short) + inline research (no agent swarm) | | `--quick` | Interview (short) + inline research (no agent swarm) |
| `--local` | Only codebase analysis agents (skip external research) | | `--local` | Only codebase analysis agents (skip external + Gemini) |
| `--external` | Only external research agents (skip codebase analysis) | | `--external` | Only external research agents (skip codebase analysis) |
| `--fg` | No-op alias (foreground is default since v2.4.0) | | `--fg` | No-op alias (foreground is default since v2.4.0) |
| `--gates {true\|false}` | (v3.4.0) Boolean autonomy-gate flag; present → gating on. Policy (`gates_mode`) detailed under `## Autonomy mode` in `docs/operations.md`. | | `--gates {true\|false}` | (v3.4.0) Boolean autonomy-gate flag; present → gating on. Policy (`gates_mode`) detailed under `## Autonomy mode` in `docs/operations.md`. |
| `--min-brief-version <ver>` | (S18) Warn — never block — if an attached `--project` brief declares a version below `<ver>` (e.g. `2.2`), i.e. sidesteps framing enforcement | | `--min-brief-version <ver>` | (S18) Warn — never block — if an attached `--project` brief declares a version below `<ver>` (e.g. `2.2`), i.e. sidesteps framing enforcement |
| `--profile <name>` | (v4.1.0) Model profile for the research phase. | | `--profile <name>` | (v4.1.0) Model profile for the research phase. |
| `--engine {swarm\|deep-research}` | (deep-research-engine) Opt-in external-research engine; `deep-research` delegates the external phase to Claude Code's built-in `/deep-research` workflow, which only works on `2.1.154 <= CC < 2.1.218` — from **2.1.218** `/deep-research` is operator-invoked only (the Skill tool refuses with `disable-model-invocation`), so the engine always falls back to `swarm`. Never hard-fails. Default `swarm`. | | `--engine {swarm\|deep-research}` | (deep-research-engine) Opt-in external-research engine; `deep-research` delegates the external phase to Claude Code's built-in `/deep-research` workflow (CC 2.1.154+), falls back to `swarm`. Default `swarm`. |
Flags combine: `--project <dir> --local`, `--external --quick`. Flags combine: `--project <dir> --local`, `--external --quick`.

View file

@ -19,10 +19,8 @@
// What IS implemented, purely: Pass 1 (triplet dedup → highest-severity-wins // What IS implemented, purely: Pass 1 (triplet dedup → highest-severity-wins
// survivor + conformance tiebreak + detail concat + raised_by provenance), // survivor + conformance tiebreak + detail concat + raised_by provenance),
// Pass 2 succinctness + actionability-presence, Pass 3 reasonableness // Pass 2 succinctness + actionability-presence, Pass 3 reasonableness
// (citation / unknown-rule_key suppression, severity-mismatch correction), // (citation / unknown-rule_key drop, severity-mismatch correction), Pass 4
// Pass 4 verdict thresholds — fail-closed: a suppression that did not REFUTE // verdict thresholds. No LLM, no network, no time, no randomness.
// the finding, and a reviewer that never reported, forbid ALLOW (see
// classifySuppression). No LLM, no network, no time, no randomness.
// //
// Reuses: SEVERITY_VALUES / RULE_KEYS / getRule (rule-catalogue.mjs), // Reuses: SEVERITY_VALUES / RULE_KEYS / getRule (rule-catalogue.mjs),
// computeFindingId (finding-id.mjs, triplet), validateFindings // computeFindingId (finding-id.mjs, triplet), validateFindings
@ -37,71 +35,6 @@ import { validateFindings } from './findings-schema.mjs';
export const JUDGE_TITLE_MAX = 100; export const JUDGE_TITLE_MAX = 100;
export const JUDGE_DETAIL_MAX = 800; export const JUDGE_DETAIL_MAX = 800;
// ---- Suppression classification (fail-closed) --------------------------------
//
// A removal is `dropped` ONLY when the test refuted the finding as a claim
// about this codebase. Every other removal is `unverified`: the coordinator
// took the finding out of the count without ever establishing it was unreal,
// so it may not be spent as evidence of a clean review.
/**
* Reasons that REFUTE. `no-citation` is the only one this deterministic subset
* can emit: a finding whose `file` is empty or whose `line` is negative names
* no location, so it makes no checkable claim at all
* (agents/review-coordinator.md Pass 3 "Speculative 'code might break
* somewhere' findings have no anchor").
*
* `accuracy:refuted` (Pass 2 Accuracy a citation escaping the repo root) and
* `file-existence:refuted` (Pass 3 absent from both working tree and diff)
* are emitted by the LLM coordinator, whose fs/judgement branches this module
* excludes. They are declared here anyway: the vocabulary is owned in one
* place so prose and lib cannot drift.
*/
export const REFUTING_REASONS = Object.freeze(new Set([
'no-citation',
'accuracy:refuted',
'file-existence:refuted',
]));
/**
* The reason vocabulary on the unverified side. `file-existence:indeterminate`
* is emitted by the LLM coordinator's Pass 3 (which runs the fs Glob this
* module deliberately excludes); the vocabulary is owned here so prose and lib
* cannot drift.
*/
export const UNVERIFIED_REASONS = Object.freeze([
'succinctness:title',
'succinctness:detail',
'actionability:empty',
'unknown-rule_key',
'file-existence:indeterminate',
]);
/**
* Classify a suppression reason. Anything not declared refuting is
* `unverified` the default is fail-CLOSED, so a reason introduced later
* without a decision cannot silently move the verdict toward ALLOW.
* @param {string} reason
* @returns {'refuted'|'unverified'}
*/
export function classifySuppression(reason) {
return REFUTING_REASONS.has(reason) ? 'refuted' : 'unverified';
}
/**
* Tag a finding with its suppression reason and route it to the refuted
* (`dropped`) or the `unverified` bucket.
* @param {object} finding
* @param {string} reason
* @param {object[]} dropped
* @param {object[]} unverified
*/
function suppress(finding, reason, dropped, unverified) {
const tagged = { ...finding, suppressed_reason: reason };
if (classifySuppression(reason) === 'refuted') dropped.push(tagged);
else unverified.push(tagged);
}
/** /**
* Catalogue-tier rank of a severity: lower number = higher severity. * Catalogue-tier rank of a severity: lower number = higher severity.
* BLOCKER=0 SUGGESTION=3; an unknown severity ranks last. * BLOCKER=0 SUGGESTION=3; an unknown severity ranks last.
@ -189,18 +122,12 @@ export function dedupByTriplet(findings) {
* (title > 100 or detail > 800 chars) and actionability (recommended_action, * (title > 100 or detail > 800 chars) and actionability (recommended_action,
* when present, must be a non-empty string). The imperative-verb test is * when present, must be a non-empty string). The imperative-verb test is
* excluded (LLM judgement). * excluded (LLM judgement).
*
* Both tests read a `.length`; neither examines the claim, so neither can
* establish the finding is unreal. Both therefore route to `unverified`.
* `dropped` stays in the signature for the refuting Pass-2 filter this subset
* excludes (Accuracy: a path-traversal escape IS a refutation).
* @param {object[]} findings * @param {object[]} findings
* @returns {{ kept: object[], dropped: object[], unverified: object[] }} * @returns {{ kept: object[], dropped: object[] }}
*/ */
export function judgeFilter(findings) { export function judgeFilter(findings) {
const kept = []; const kept = [];
const dropped = []; const dropped = [];
const unverified = [];
for (const f of findings) { for (const f of findings) {
const titleLen = (f.title ?? '').length; const titleLen = (f.title ?? '').length;
const detailLen = (f.detail ?? '').length; const detailLen = (f.detail ?? '').length;
@ -211,38 +138,31 @@ export function judgeFilter(findings) {
(typeof f.recommended_action !== 'string' || f.recommended_action.trim().length === 0)) { (typeof f.recommended_action !== 'string' || f.recommended_action.trim().length === 0)) {
reason = 'actionability:empty'; reason = 'actionability:empty';
} }
if (reason) suppress(f, reason, dropped, unverified); if (reason) dropped.push({ ...f, suppressed_reason: reason });
else kept.push(f); else kept.push(f);
} }
return { kept, dropped, unverified }; return { kept, dropped };
} }
/** /**
* Pass 3 Cloudflare reasonableness (deterministic subset): drop findings * Pass 3 Cloudflare reasonableness (deterministic subset): drop findings
* with no citation (empty file / line < 0) or an unknown rule_key; CORRECT a * with no citation (empty file / line < 0) or an unknown rule_key; CORRECT a
* severity that does not match the catalogue tier (a correction, not a drop). * severity that does not match the catalogue tier (a correction, not a drop).
* The fs file-existence glob is excluded (I/O) its indeterminate branch is * The fs file-existence glob is excluded (I/O).
* prose-side, tokenised as `file-existence:indeterminate`.
*
* `no-citation` REFUTES (the finding names no location, so it makes no
* checkable claim) and is dropped. `unknown-rule_key` does not: an ad-hoc key
* is a real defect wearing the wrong label v5.1.1 high-effort mode already
* KEEPS these, normalised to PLAN_EXECUTE_DRIFT so it routes to `unverified`.
* @param {object[]} findings * @param {object[]} findings
* @returns {{ kept: object[], dropped: object[], unverified: object[] }} * @returns {{ kept: object[], dropped: object[] }}
*/ */
export function reasonablenessFilter(findings) { export function reasonablenessFilter(findings) {
const kept = []; const kept = [];
const dropped = []; const dropped = [];
const unverified = [];
for (const f of findings) { for (const f of findings) {
if (typeof f.file !== 'string' || f.file.length === 0 || if (typeof f.file !== 'string' || f.file.length === 0 ||
(typeof f.line === 'number' && f.line < 0)) { (typeof f.line === 'number' && f.line < 0)) {
suppress(f, 'no-citation', dropped, unverified); dropped.push({ ...f, suppressed_reason: 'no-citation' });
continue; continue;
} }
if (!RULE_KEYS.has(f.rule_key)) { if (!RULE_KEYS.has(f.rule_key)) {
suppress(f, 'unknown-rule_key', dropped, unverified); dropped.push({ ...f, suppressed_reason: 'unknown-rule_key' });
continue; continue;
} }
const rule = getRule(f.rule_key); const rule = getRule(f.rule_key);
@ -252,114 +172,44 @@ export function reasonablenessFilter(findings) {
kept.push(f); kept.push(f);
} }
} }
return { kept, dropped, unverified }; return { kept, dropped };
} }
/** /**
* Pass 4 compute the verdict from severity counts (after dedup + filtering). * Pass 4 compute the verdict from severity counts (after dedup + filtering).
* BLOCKER 1 BLOCK; else MAJOR 1 WARN; else ALLOW. * BLOCKER 1 BLOCK; else MAJOR 1 WARN; else ALLOW.
*
* FAIL-CLOSED: ALLOW additionally requires that nothing is `unverified` and
* that every expected reviewer reported. Neither ever RAISES a verdict the
* severity thresholds are untouched they only forbid the clean one, so the
* worst case of a false unverified is WARN plus a stated reason, never a
* silent pass. Unverified findings are NOT counted into a severity tier: their
* severity is reviewer-asserted and was never substantiated.
*
* @param {object[]} findings * @param {object[]} findings
* @param {{ unverified?: object[], missingReviewers?: string[], unattributablePayloads?: number }} [options] * @returns {{ verdict: 'BLOCK'|'WARN'|'ALLOW', counts: Record<string, number> }}
* @returns {{ verdict: 'BLOCK'|'WARN'|'ALLOW', counts: Record<string, number>, allow_blocked_by: string[] }}
*/ */
export function computeVerdict(findings, options = {}) { export function computeVerdict(findings) {
const counts = { BLOCKER: 0, MAJOR: 0, MINOR: 0, SUGGESTION: 0 }; const counts = { BLOCKER: 0, MAJOR: 0, MINOR: 0, SUGGESTION: 0 };
for (const f of findings) { for (const f of findings) {
if (counts[f.severity] !== undefined) counts[f.severity] += 1; if (counts[f.severity] !== undefined) counts[f.severity] += 1;
} }
const unverified = options.unverified ?? [];
const missingReviewers = options.missingReviewers ?? [];
const unattributablePayloads = options.unattributablePayloads ?? 0;
const allow_blocked_by = [];
const byReason = new Map();
for (const f of unverified) {
const reason = f?.suppressed_reason ?? 'unspecified';
byReason.set(reason, (byReason.get(reason) ?? 0) + 1);
}
for (const [reason, n] of byReason) allow_blocked_by.push(`unverified:${reason} (${n})`);
for (const r of missingReviewers) allow_blocked_by.push(`missing-reviewer:${r}`);
if (unattributablePayloads > 0) allow_blocked_by.push(`unattributable-payload (${unattributablePayloads})`);
let verdict; let verdict;
if (counts.BLOCKER >= 1) verdict = 'BLOCK'; if (counts.BLOCKER >= 1) verdict = 'BLOCK';
else if (counts.MAJOR >= 1) verdict = 'WARN'; else if (counts.MAJOR >= 1) verdict = 'WARN';
else if (allow_blocked_by.length > 0) verdict = 'WARN';
else verdict = 'ALLOW'; else verdict = 'ALLOW';
return { verdict, counts, allow_blocked_by }; return { verdict, counts };
} }
/** /**
* Run the full deterministic contract: ingest Pass 1 Pass 2 Pass 3 Pass 4. * Run the full deterministic contract: ingest Pass 1 Pass 2 Pass 3 Pass 4.
*
* `options.expectedReviewers` names the reviewers this review was supposed to
* hear from. A reviewer that is absent from the payloads, or whose named
* payload failed schema validation and was thrown away at ingest, lands in
* `missing_reviewers`; a payload that failed schema WITHOUT a reviewer name is
* counted in `unattributable_payloads` instead. Either forbids ALLOW: an
* unread reviewer is an absent one, and zero findings from a silent reviewer
* must not read like zero findings from a clean diff.
*
* `suppressed` stays the UNION of `dropped` (refuted) and `unverified` so
* existing consumers keep their meaning; `unverified` is the subset that
* forbids ALLOW. Do not iterate both and count twice.
*
* @param {Array<{reviewer?: string, findings: object[]}>} reviewerPayloads * @param {Array<{reviewer?: string, findings: object[]}>} reviewerPayloads
* @param {{ expectedReviewers?: string[] }} [options] * @returns {{ verdict: string, counts: Record<string, number>, findings: object[], suppressed: object[], skipped: object[] }}
* @returns {{ verdict: string, counts: Record<string, number>, findings: object[], suppressed: object[], unverified: object[], skipped: object[], missing_reviewers: string[], unattributable_payloads: number, allow_blocked_by: string[] }}
*/ */
export function runContract(reviewerPayloads, options = {}) { export function runContract(reviewerPayloads) {
const { findings: ingested, skipped } = ingest(reviewerPayloads); const { findings: ingested, skipped } = ingest(reviewerPayloads);
const deduped = dedupByTriplet(ingested); const deduped = dedupByTriplet(ingested);
const judged = judgeFilter(deduped); const judged = judgeFilter(deduped);
const reasoned = reasonablenessFilter(judged.kept); const reasoned = reasonablenessFilter(judged.kept);
const unverified = [...judged.unverified, ...reasoned.unverified]; const { verdict, counts } = computeVerdict(reasoned.kept);
// A reviewer counts as REPORTED only when a payload carrying its name
// validated. `validateFindings` merely warns on a missing `reviewer`, so a
// payload can fail schema anonymously: that is an unattributable payload, not
// a reviewer called "unnamed reviewer". Naming one would invent an agent
// nobody launched, and would double-count with expectedReviewers when the two
// are in fact the same failure.
const skippedNames = new Set();
let unattributable_payloads = 0;
for (const s of skipped) {
if (typeof s.reviewer === 'string' && s.reviewer.length > 0) skippedNames.add(s.reviewer);
else unattributable_payloads += 1;
}
const reported = new Set();
for (const payload of reviewerPayloads) {
const name = payload?.reviewer;
if (typeof name === 'string' && name.length > 0 && !skippedNames.has(name)) reported.add(name);
}
const missing_reviewers = [...skippedNames];
for (const r of options.expectedReviewers ?? []) {
if (!reported.has(r) && !missing_reviewers.includes(r)) missing_reviewers.push(r);
}
const { verdict, counts, allow_blocked_by } = computeVerdict(reasoned.kept, {
unverified,
missingReviewers: missing_reviewers,
unattributablePayloads: unattributable_payloads,
});
return { return {
verdict, verdict,
counts, counts,
findings: reasoned.kept, findings: reasoned.kept,
suppressed: [...judged.dropped, ...judged.unverified, ...reasoned.dropped, ...reasoned.unverified], suppressed: [...judged.dropped, ...reasoned.dropped],
unverified,
skipped, skipped,
missing_reviewers,
unattributable_payloads,
allow_blocked_by,
}; };
} }

View file

@ -1,7 +1,6 @@
{ {
"name": "voyage", "name": "voyage",
"version": "5.10.1", "version": "5.10.0",
"private": true,
"description": "Voyage — brief, research, plan, execute, review, continue. Contract-driven Claude Code pipeline. /trekbrief, /trekplan, and /trekreview each end by building a self-contained operator-annotation HTML (scripts/annotate.mjs, modelled on claude-code-100x): select text or click any heading/paragraph/list-item, pick intent (Fiks/Endre/Spørsmål), write comment, copy structured prompt, paste back, Claude revises the .md.", "description": "Voyage — brief, research, plan, execute, review, continue. Contract-driven Claude Code pipeline. /trekbrief, /trekplan, and /trekreview each end by building a self-contained operator-annotation HTML (scripts/annotate.mjs, modelled on claude-code-100x): select text or click any heading/paragraph/list-item, pick intent (Fiks/Endre/Spørsmål), write comment, copy structured prompt, paste back, Claude revises the .md.",
"type": "module", "type": "module",
"engines": { "engines": {

View file

@ -14,6 +14,11 @@
"trekresearch": { "trekresearch": {
"defaultMode": "default", "defaultMode": "default",
"maxDimensions": 8, "maxDimensions": 8,
"geminiBridge": {
"enabled": true,
"pollIntervalSeconds": 30,
"timeoutMinutes": 25
},
"interview": { "interview": {
"maxQuestions": 4, "maxQuestions": 4,
"typicalQuestions": 3 "typicalQuestions": 3

View file

@ -74,6 +74,15 @@ findings exist.*
### Known Issues ### Known Issues
{Common pitfalls, gotchas, real-world problems from community-researcher} {Common pitfalls, gotchas, real-world problems from community-researcher}
## Gemini Second Opinion
*Independent research result from Gemini Deep Research. Provides a second
perspective for triangulation. Omit this section if gemini-bridge was not used
or was unavailable.*
{Gemini findings reformatted into key findings, sources cited, and areas of
agreement/disagreement with other agents.}
## Synthesis ## Synthesis
*Cross-cutting insights that emerge from combining local and external knowledge. *Cross-cutting insights that emerge from combining local and external knowledge.
@ -105,7 +114,7 @@ this section entirely.*
| # | Source | Type | Quality | Used in | | # | Source | Type | Quality | Used in |
|---|--------|------|---------|---------| |---|--------|------|---------|---------|
| 1 | {URL or codebase path} | {official / community / codebase} | {high / medium / low} | {dimension name} | | 1 | {URL or codebase path} | {official / community / codebase / gemini} | {high / medium / low} | {dimension name} |
*Quality assessment:* *Quality assessment:*
- **high** — official documentation, verified codebase analysis, peer-reviewed - **high** — official documentation, verified codebase analysis, peer-reviewed

View file

@ -129,7 +129,7 @@ the codebase and this brief contain sufficient context for planning."}
- **Required for plan steps:** {which kinds of steps will consume this — e.g., - **Required for plan steps:** {which kinds of steps will consume this — e.g.,
"migration strategy", "library selection", "threat model"} "migration strategy", "library selection", "threat model"}
- **Confidence needed:** {high | medium | low} - **Confidence needed:** {high | medium | low}
- **Estimated cost:** {quick — inline research | standard — agent swarm | deep — with contrarian} - **Estimated cost:** {quick — inline research | standard — agent swarm | deep — with contrarian + gemini}
- **Scope hint:** {local | external | both} - **Scope hint:** {local | external | both}
### Topic 2: {Short title} ### Topic 2: {Short title}

View file

@ -1,349 +0,0 @@
// tests/commands/trekexecute-parallel-portability.test.mjs
//
// Executable tests for the two shell blocks /trekexecute Phase 2.6 Step 2a' and
// Phase 2.55 Check 2 tell the agent to run. Both blocks wrecked a real voyage on
// macOS (order 20260831T214411Z-941965142-from-.claude):
//
// Defect 1: `realpath --relative-to` is GNU coreutils. On BSD realpath the
// command substitution fails, PROJECT_REL becomes EMPTY, and `mkdir -p
// "$wt/"` + `cp ... "$wt//"` both SUCCEED — brief.md/plan.md land at the
// worktree root instead of the project relpath. Exit status stays 0; only
// file location tells the truth. Every assertion here checks placement.
// Defect 2: Check 2 ran `git add {plan-path}` unconditionally. When the
// project directory is gitignored (normal — .claude/projects/ is tool-
// managed and local-only) the add fails and the plan never reaches HEAD.
//
// The blocks are EXTRACTED from commands/trekexecute.md and executed, so the
// test binds to what an agent actually copies, not to prose about it. Anchors
// are grep-able strings, never line numbers.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { readFileSync, writeFileSync, mkdirSync, mkdtempSync, rmSync, chmodSync } from 'node:fs';
import { execFileSync, spawnSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import { realpathSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..');
const COMMAND_FILE = join(ROOT, 'commands', 'trekexecute.md');
const COPY_BLOCK_ANCHOR = "**2a'. Copy gitignored project artifacts";
const CHECK2_ANCHOR = '### Check 2 —';
// The pre-fix form, kept ONLY as the negative control for Defect 1.
const LEGACY_GNU_LINE = 'PROJECT_REL="$(realpath --relative-to="$REPO_ROOT" "$PROJECT_SOURCE")"';
// --- helpers -------------------------------------------------------------
/** Extract the first ```bash fence that follows `anchor` in commands/trekexecute.md. */
function extractBashBlock(anchor) {
const text = readFileSync(COMMAND_FILE, 'utf8');
const at = text.indexOf(anchor);
assert.ok(at >= 0, `anchor not found in trekexecute.md: ${anchor}`);
const fenceOpen = text.indexOf('```bash', at);
assert.ok(fenceOpen >= 0, `no bash fence after anchor: ${anchor}`);
const bodyStart = text.indexOf('\n', fenceOpen) + 1;
const fenceClose = text.indexOf('```', bodyStart);
assert.ok(fenceClose > bodyStart, `unterminated bash fence after anchor: ${anchor}`);
return text.slice(bodyStart, fenceClose);
}
/**
* A PATH directory whose `realpath` behaves like BSD realpath: it rejects every
* GNU long option and resolves bare paths correctly. Stubbed, never assumed
* the machine running the suite may or may not have GNU coreutils.
*/
function bsdRealpathStubDir() {
const dir = mkdtempSync(join(tmpdir(), 'trek-bsdstub-'));
const stub = join(dir, 'realpath');
writeFileSync(stub, [
'#!/bin/sh',
'# BSD realpath stand-in: no GNU long options.',
'for a in "$@"; do',
' case "$a" in',
' --*) echo "realpath: illegal option -- -" >&2; exit 1 ;;',
' esac',
'done',
"exec python3 -c 'import os,sys",
'for p in sys.argv[1:]: print(os.path.realpath(p))',
"' \"$@\"",
'',
].join('\n'));
chmodSync(stub, 0o755);
return dir;
}
/** A PATH directory whose `git check-ignore` dies fatally (128), everything else real. */
function fatalCheckIgnoreGitStubDir() {
const realGit = execFileSync('/usr/bin/env', ['sh', '-c', 'command -v git'], { encoding: 'utf8' }).trim();
assert.ok(realGit, 'git not on PATH — cannot build the check-ignore stub');
const dir = mkdtempSync(join(tmpdir(), 'trek-gitstub-'));
const stub = join(dir, 'git');
writeFileSync(stub, [
'#!/bin/sh',
'# `git check-ignore` fatal (128): NOT an answer about ignore status.',
'for a in "$@"; do',
' if [ "$a" = "check-ignore" ]; then',
' echo "fatal: simulated check-ignore failure" >&2',
' exit 128',
' fi',
'done',
`exec ${realGit} "$@"`,
'',
].join('\n'));
chmodSync(stub, 0o755);
return dir;
}
function git(cwd, ...args) {
return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
}
/**
* A temp repo with a committed file plus a project directory holding brief/plan
* (+ research unless opts.research === false). `gitignoreProject: true` adds the
* .gitignore line that makes the project directory invisible to `git add`
* and, note, invisible to `git status --porcelain` too, which is exactly why
* this topology sails through Check 1 and only trips on Check 2.
*/
function makeRepo(opts = {}) {
const root = realpathSync(mkdtempSync(join(tmpdir(), 'trek-exec-')));
git(root, 'init', '-q', '-b', 'main');
git(root, 'config', 'user.email', 'test@example.invalid');
git(root, 'config', 'user.name', 'Test');
writeFileSync(join(root, 'README.md'), '# fixture\n');
if (opts.gitignoreProject) writeFileSync(join(root, '.gitignore'), '.claude/projects/\n');
git(root, 'add', 'README.md', ...(opts.gitignoreProject ? ['.gitignore'] : []));
git(root, 'commit', '-qm', 'init');
const projectRel = join('.claude', 'projects', 'demo');
const projectDir = join(root, projectRel);
mkdirSync(projectDir, { recursive: true });
writeFileSync(join(projectDir, 'brief.md'), '# brief\n');
writeFileSync(join(projectDir, 'plan.md'), '# plan\n');
if (opts.research !== false) {
mkdirSync(join(projectDir, 'research'), { recursive: true });
writeFileSync(join(projectDir, 'research', '01-x.md'), '# r\n');
}
const worktreeDir = join(root, '.claude', 'trekplan-sessions', 'demo', 'worktrees');
mkdirSync(join(worktreeDir, 'session-1'), { recursive: true });
mkdirSync(join(worktreeDir, 'session-2'), { recursive: true });
return { root, projectRel, projectDir, worktreeDir };
}
function runBlock(script, { cwd, env }) {
return spawnSync('bash', ['-c', script], { cwd, env: { ...process.env, ...env }, encoding: 'utf8' });
}
function copyBlockEnv(repo, pathPrefixDir) {
return {
REPO_ROOT: repo.root,
PROJECT_DIR: repo.projectDir,
WORKTREE_DIR: repo.worktreeDir,
PATH: `${pathPrefixDir}:${process.env.PATH}`,
};
}
function exists(p) {
try { readFileSync(p); return true; } catch { return false; }
}
// --- Defect 1: portable relpath derivation -------------------------------
test("2a' — the BSD realpath stub is known-positive: bare path resolves, --relative-to is rejected", () => {
const stubDir = bsdRealpathStubDir();
try {
const bare = runBlock('realpath "$HOME"', { cwd: ROOT, env: { PATH: `${stubDir}:${process.env.PATH}` } });
assert.equal(bare.status, 0, 'stub must resolve a bare path (proves it can succeed)');
assert.equal(bare.stdout.trim(), realpathSync(process.env.HOME));
const gnu = runBlock('realpath --relative-to=/ "$HOME"', { cwd: ROOT, env: { PATH: `${stubDir}:${process.env.PATH}` } });
assert.notEqual(gnu.status, 0, 'stub must reject the GNU long option');
assert.match(gnu.stderr, /illegal option/, 'stub must fail the way BSD realpath fails');
assert.equal(gnu.stdout.trim(), '', 'no stdout — this is what leaves PROJECT_REL empty');
} finally {
rmSync(stubDir, { recursive: true, force: true });
}
});
test("2a' — NEGATIVE CONTROL: the GNU form drops plan.md at the worktree ROOT under BSD realpath", () => {
const repo = makeRepo();
const stubDir = bsdRealpathStubDir();
try {
const legacy = [
'PROJECT_SOURCE="$(realpath "${PROJECT_DIR}")"',
LEGACY_GNU_LINE,
'for wt in "$WORKTREE_DIR"/session-*; do',
' [ -d "$wt" ] || continue',
' mkdir -p "$wt/$PROJECT_REL"',
' cp "$PROJECT_SOURCE"/brief.md "$wt/$PROJECT_REL/"',
' cp "$PROJECT_SOURCE"/plan.md "$wt/$PROJECT_REL/"',
'done',
].join('\n');
runBlock(legacy, { cwd: repo.root, env: copyBlockEnv(repo, stubDir) });
const wt = join(repo.worktreeDir, 'session-1');
assert.equal(exists(join(wt, repo.projectRel, 'plan.md')), false,
'the broken form must NOT put plan.md at the project relpath');
assert.equal(exists(join(wt, 'plan.md')), true,
'the broken form silently drops plan.md at the worktree root — the measured havari');
} finally {
rmSync(repo.root, { recursive: true, force: true });
rmSync(stubDir, { recursive: true, force: true });
}
});
test("2a' — the shipped block copies brief/plan/research to $wt/$PROJECT_REL without GNU realpath", () => {
const repo = makeRepo();
const stubDir = bsdRealpathStubDir();
try {
const r = runBlock(extractBashBlock(COPY_BLOCK_ANCHOR), { cwd: repo.root, env: copyBlockEnv(repo, stubDir) });
assert.equal(r.status, 0, `block must succeed without GNU realpath. stderr: ${r.stderr}`);
for (const s of ['session-1', 'session-2']) {
const dest = join(repo.worktreeDir, s, repo.projectRel);
assert.equal(exists(join(dest, 'plan.md')), true, `${s}: plan.md must reach $wt/$PROJECT_REL`);
assert.equal(exists(join(dest, 'brief.md')), true, `${s}: brief.md must reach $wt/$PROJECT_REL`);
assert.equal(exists(join(dest, 'research', '01-x.md')), true, `${s}: research/ must reach $wt/$PROJECT_REL`);
assert.equal(exists(join(repo.worktreeDir, s, 'plan.md')), false,
`${s}: nothing may land at the worktree root`);
}
} finally {
rmSync(repo.root, { recursive: true, force: true });
rmSync(stubDir, { recursive: true, force: true });
}
});
test("2a' — a project without research/ is not a failure (block still exits 0)", () => {
const repo = makeRepo({ research: false });
const stubDir = bsdRealpathStubDir();
try {
const r = runBlock(extractBashBlock(COPY_BLOCK_ANCHOR), { cwd: repo.root, env: copyBlockEnv(repo, stubDir) });
assert.equal(r.status, 0, `missing research/ must not fail the wave. stderr: ${r.stderr}`);
assert.equal(exists(join(repo.worktreeDir, 'session-1', repo.projectRel, 'plan.md')), true);
} finally {
rmSync(repo.root, { recursive: true, force: true });
rmSync(stubDir, { recursive: true, force: true });
}
});
test("2a' — an underivable relpath fails LOUDLY instead of dropping files at the worktree root", () => {
const repo = makeRepo();
const stubDir = bsdRealpathStubDir();
const outside = realpathSync(mkdtempSync(join(tmpdir(), 'trek-outside-')));
try {
mkdirSync(join(outside, 'p'), { recursive: true });
writeFileSync(join(outside, 'p', 'brief.md'), 'b');
writeFileSync(join(outside, 'p', 'plan.md'), 'p');
const env = { ...copyBlockEnv(repo, stubDir), PROJECT_DIR: join(outside, 'p') };
const r = runBlock(extractBashBlock(COPY_BLOCK_ANCHOR), { cwd: repo.root, env });
assert.notEqual(r.status, 0, 'a project outside REPO_ROOT must abort the wave');
assert.match(r.stderr, /relpath/i, 'the abort must name the cause');
assert.equal(exists(join(repo.worktreeDir, 'session-1', 'plan.md')), false,
'nothing may be dropped at the worktree root');
} finally {
rmSync(repo.root, { recursive: true, force: true });
rmSync(outside, { recursive: true, force: true });
rmSync(stubDir, { recursive: true, force: true });
}
});
test("2a' — the GNU-only form is gone from the block agents copy", () => {
const block = extractBashBlock(COPY_BLOCK_ANCHOR);
assert.equal(block.includes('--relative-to'), false,
'the copied block must not contain `realpath --relative-to` (GNU-only)');
assert.ok(block.includes('os.path.relpath'), 'the copied block must derive the relpath portably');
});
test("both copied blocks are ASCII-clean (bash 3.2 dies on a multibyte char under set -u)", () => {
const nonAscii = (s) => s.split('\n')
.map((line, i) => [i + 1, line])
.filter(([, line]) => /[^\x00-\x7F]/.test(line));
// Known-positive: the detector must actually fire on a multibyte char.
assert.equal(nonAscii('echo "a — b"').length, 1, 'detector must find an em-dash');
for (const anchor of [COPY_BLOCK_ANCHOR, CHECK2_ANCHOR]) {
assert.deepEqual(nonAscii(extractBashBlock(anchor)), [],
`non-ASCII inside the shell block after ${anchor} (prose outside the fence is fine)`);
}
});
// --- Defect 2: gitignored project directory ------------------------------
// The block carries the `{plan-path}` placeholder the way every other block in
// trekexecute.md does. Substitute it exactly as an agent would — never inject
// PLAN_PATH through the environment: that would supply what the doc must supply
// itself, and a block that never assigns the variable would still pass.
function check2Script(planPath) {
const block = extractBashBlock(CHECK2_ANCHOR);
assert.ok(block.includes('{plan-path}'),
'Check 2 block must carry the {plan-path} placeholder for the agent to substitute');
return block.replace('{plan-path}', planPath);
}
function check2Env(repo, pathPrefixDir) {
const env = { REPO_ROOT: repo.root, PLAN_PATH: '' };
if (pathPrefixDir) env.PATH = `${pathPrefixDir}:${process.env.PATH}`;
return env;
}
test('Check 2 — gitignored project dir: no commit, no failure, and 2a\' still delivers the plan', () => {
const repo = makeRepo({ gitignoreProject: true });
const stubDir = bsdRealpathStubDir();
try {
// Known-positive on the premise: the plan file really is ignored here.
const ci = spawnSync('git', ['check-ignore', '-v', join(repo.projectRel, 'plan.md')],
{ cwd: repo.root, encoding: 'utf8' });
assert.equal(ci.status, 0, 'fixture premise: the plan file must actually be gitignored');
const head = git(repo.root, 'rev-parse', 'HEAD').trim();
const r = runBlock(check2Script(join(repo.projectRel, 'plan.md')),
{ cwd: repo.root, env: check2Env(repo) });
assert.equal(r.status, 0, `Check 2 must tolerate a gitignored plan file. stderr: ${r.stderr}`);
assert.equal(git(repo.root, 'rev-parse', 'HEAD').trim(), head,
'an ignored plan file must NOT be forced into history (origin is a public mirror)');
const copy = runBlock(extractBashBlock(COPY_BLOCK_ANCHOR), { cwd: repo.root, env: copyBlockEnv(repo, stubDir) });
assert.equal(copy.status, 0, `copy step must succeed. stderr: ${copy.stderr}`);
assert.equal(exists(join(repo.worktreeDir, 'session-1', repo.projectRel, 'plan.md')), true,
'the plan must reach the worktree even though git never tracked it');
} finally {
rmSync(repo.root, { recursive: true, force: true });
rmSync(stubDir, { recursive: true, force: true });
}
});
test('Check 2 — KNOWN-POSITIVE: an untracked, NOT-ignored plan file is still added and committed', () => {
const repo = makeRepo();
try {
const planPath = join(repo.projectRel, 'plan.md');
const head = git(repo.root, 'rev-parse', 'HEAD').trim();
const r = runBlock(check2Script(planPath), { cwd: repo.root, env: check2Env(repo) });
assert.equal(r.status, 0, `Check 2 must succeed on a normal untracked plan. stderr: ${r.stderr}`);
assert.notEqual(git(repo.root, 'rev-parse', 'HEAD').trim(), head,
'a trackable plan file must still be committed for worktree visibility');
const ls = spawnSync('git', ['ls-files', '--error-unmatch', planPath], { cwd: repo.root, encoding: 'utf8' });
assert.equal(ls.status, 0, 'the plan file must now be tracked');
} finally {
rmSync(repo.root, { recursive: true, force: true });
}
});
test('Check 2 — a FATAL git check-ignore (128) is not read as "not ignored"', () => {
const repo = makeRepo({ gitignoreProject: true });
const stubDir = fatalCheckIgnoreGitStubDir();
try {
const head = git(repo.root, 'rev-parse', 'HEAD').trim();
const r = runBlock(check2Script(join(repo.projectRel, 'plan.md')),
{ cwd: repo.root, env: check2Env(repo, stubDir) });
assert.notEqual(r.status, 0, 'a fatal check-ignore must stop, not fall through to git add');
assert.match(r.stderr, /check-ignore/, 'the stop must name the failing probe');
assert.equal(git(repo.root, 'rev-parse', 'HEAD').trim(), head, 'no commit may be made on a fatal probe');
} finally {
rmSync(repo.root, { recursive: true, force: true });
rmSync(stubDir, { recursive: true, force: true });
}
});

View file

@ -152,18 +152,14 @@ test('no agents/*.md frontmatter contains an <example> block (M4: examples live
} }
}); });
// Floor lowered 34 -> 32 in v5.10.1: `gemini-bridge` (2 examples) was DELETED as an test('agent bodies retain at least 34 <example> blocks (M4: relocation moves, never deletes)', () => {
// agent by operator directive, not relocated. The pin guards M4's "relocation moves,
// never deletes" invariant, so it must move only when an agent legitimately leaves the
// inventory — never to absorb examples that went missing from a surviving agent.
test('agent bodies retain at least 32 <example> blocks (M4: relocation moves, never deletes)', () => {
let total = 0; let total = 0;
for (const f of agentFiles) { for (const f of agentFiles) {
total += (bodyOf(read(`agents/${f}`)).match(/<example>/g) || []).length; total += (bodyOf(read(`agents/${f}`)).match(/<example>/g) || []).length;
} }
assert.ok( assert.ok(
total >= 32, total >= 34,
`expected >= 32 <example> blocks across agent bodies (16 agents x 2), got ${total} ` + `expected >= 34 <example> blocks across agent bodies (17 agents x 2), got ${total} ` +
`— examples may have been deleted instead of relocated (M4)`, `— examples may have been deleted instead of relocated (M4)`,
); );
}); });

View file

@ -6,9 +6,6 @@
import { test } from 'node:test'; import { test } from 'node:test';
import { strict as assert } from 'node:assert'; import { strict as assert } from 'node:assert';
import { readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { import {
severityRank, severityRank,
ingest, ingest,
@ -16,14 +13,9 @@ import {
judgeFilter, judgeFilter,
reasonablenessFilter, reasonablenessFilter,
computeVerdict, computeVerdict,
classifySuppression,
REFUTING_REASONS,
UNVERIFIED_REASONS,
runContract, runContract,
} from '../../lib/review/coordinator-contract.mjs'; } from '../../lib/review/coordinator-contract.mjs';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
// ---- Pass 1 — dedup -------------------------------------------------------- // ---- Pass 1 — dedup --------------------------------------------------------
test('dedupByTriplet — genuine cross-reviewer collapse (identical triplet) → 1, raised_by both', () => { test('dedupByTriplet — genuine cross-reviewer collapse (identical triplet) → 1, raised_by both', () => {
@ -106,41 +98,29 @@ test('computeVerdict — counts each severity tier', () => {
// ---- Pass 3 — reasonableness ----------------------------------------------- // ---- Pass 3 — reasonableness -----------------------------------------------
test('reasonablenessFilter — citation-less is REFUTED, unknown rule_key is UNVERIFIED, severity mismatch corrected', () => { test('reasonablenessFilter — drops unknown rule_key + citation-less, corrects severity mismatch', () => {
// Contract change (fail-closed): only `no-citation` refutes. An ad-hoc
// rule_key is a real defect wearing the wrong label — v5.1.1 high-effort mode
// already keeps those, normalised to PLAN_EXECUTE_DRIFT.
const r = reasonablenessFilter([ const r = reasonablenessFilter([
{ file: 'x.mjs', line: 1, rule_key: 'NOPE_KEY', severity: 'BLOCKER' }, // unknown → unverified { file: 'x.mjs', line: 1, rule_key: 'NOPE_KEY', severity: 'BLOCKER' }, // unknown → drop
{ file: '', line: 1, rule_key: 'MISSING_TEST', severity: 'MAJOR' }, // no file → drop { file: '', line: 1, rule_key: 'MISSING_TEST', severity: 'MAJOR' }, // no file → drop
{ file: 'x.mjs', line: -1, rule_key: 'MISSING_TEST', severity: 'MAJOR' }, // line < 0 → drop { file: 'x.mjs', line: -1, rule_key: 'MISSING_TEST', severity: 'MAJOR' }, // line < 0 → drop
{ file: 'x.mjs', line: 1, rule_key: 'MISSING_TEST', severity: 'MINOR' }, // catalogue is MAJOR → correct, keep { file: 'x.mjs', line: 1, rule_key: 'MISSING_TEST', severity: 'MINOR' }, // catalogue is MAJOR → correct, keep
]); ]);
assert.equal(r.kept.length, 1); assert.equal(r.kept.length, 1);
assert.equal(r.dropped.length, 2); assert.equal(r.dropped.length, 3);
assert.deepEqual(r.dropped.map((f) => f.suppressed_reason), ['no-citation', 'no-citation']);
assert.equal(r.unverified.length, 1);
assert.equal(r.unverified[0].suppressed_reason, 'unknown-rule_key');
assert.equal(r.kept[0].severity, 'MAJOR'); assert.equal(r.kept[0].severity, 'MAJOR');
assert.equal(r.kept[0].original_severity, 'MINOR'); assert.equal(r.kept[0].original_severity, 'MINOR');
}); });
// ---- Pass 2 — judge -------------------------------------------------------- // ---- Pass 2 — judge --------------------------------------------------------
test('judgeFilter — over-long title and empty recommended_action are UNVERIFIED, not dropped', () => { test('judgeFilter — drops over-long title and empty recommended_action', () => {
// Contract change (fail-closed): both implemented Pass 2 tests read a
// `.length` and never examine the claim, so neither refutes the finding.
// `dropped` is empty here on purpose — the refuting Pass 2 filter (Accuracy)
// is the one this deterministic subset excludes.
const j = judgeFilter([ const j = judgeFilter([
{ file: 'x.mjs', line: 1, rule_key: 'MISSING_TEST', severity: 'MAJOR', title: 'x'.repeat(101) }, // too long → unverified { file: 'x.mjs', line: 1, rule_key: 'MISSING_TEST', severity: 'MAJOR', title: 'x'.repeat(101) }, // too long → drop
{ file: 'x.mjs', line: 2, rule_key: 'MISSING_TEST', severity: 'MAJOR', title: 'ok', recommended_action: ' ' }, // empty action → unverified { file: 'x.mjs', line: 2, rule_key: 'MISSING_TEST', severity: 'MAJOR', title: 'ok', recommended_action: ' ' }, // empty action → drop
{ file: 'x.mjs', line: 3, rule_key: 'MISSING_TEST', severity: 'MAJOR', title: 'ok' }, // keep (no action field is fine) { file: 'x.mjs', line: 3, rule_key: 'MISSING_TEST', severity: 'MAJOR', title: 'ok' }, // keep (no action field is fine)
]); ]);
assert.equal(j.kept.length, 1); assert.equal(j.kept.length, 1);
assert.equal(j.dropped.length, 0); assert.equal(j.dropped.length, 2);
assert.equal(j.unverified.length, 2);
assert.deepEqual(j.unverified.map((f) => f.suppressed_reason), ['succinctness:title', 'actionability:empty']);
}); });
// ---- ingest ---------------------------------------------------------------- // ---- ingest ----------------------------------------------------------------
@ -181,155 +161,3 @@ test('runContract — deterministic: identical input yields identical output', (
]; ];
assert.deepEqual(runContract(input), runContract(input)); assert.deepEqual(runContract(input), runContract(input));
}); });
// ---- Fail-closed: the `unverified` bucket (ORDRE 834432937) -----------------
//
// The defect: a finding REMOVED by Pass 2/Pass 3, and a reviewer whose payload
// was thrown away or never arrived, are all arithmetically identical to a
// finding that never existed -- they push the verdict toward ALLOW. Measured
// before the fix (probe, 2026-09-01): an over-long-title BLOCKER -> ALLOW; a
// payload with one ad-hoc rule_key -> the whole payload skipped, its valid
// BLOCKER sibling gone -> ALLOW.
//
// The rule under test: a removal is `dropped` ONLY when the test refutes the
// finding as a claim about this codebase. Every other removal is `unverified`,
// and a non-empty `unverified` -- or a reviewer that did not report -- forbids
// ALLOW.
test('classifySuppression — only no-citation refutes; form and taxonomy failures are unverified', () => {
assert.equal(classifySuppression('no-citation'), 'refuted',
'a finding that names no location makes no checkable claim');
assert.equal(classifySuppression('succinctness:title'), 'unverified');
assert.equal(classifySuppression('succinctness:detail'), 'unverified');
assert.equal(classifySuppression('actionability:empty'), 'unverified');
assert.equal(classifySuppression('unknown-rule_key'), 'unverified');
assert.equal(classifySuppression('file-existence:indeterminate'), 'unverified');
assert.equal(classifySuppression('something-nobody-declared'), 'unverified',
'an unclassified reason must fail CLOSED, not open');
});
test('computeVerdict — non-empty unverified forbids ALLOW but never downgrades BLOCK or WARN', () => {
const u = [{ file: 'x.mjs', line: 1, rule_key: 'MISSING_TEST', severity: 'BLOCKER' }];
const withUnverified = computeVerdict([], { unverified: u });
assert.equal(withUnverified.verdict, 'WARN', 'ALLOW is forbidden while anything is unverified');
assert.deepEqual(withUnverified.counts, { BLOCKER: 0, MAJOR: 0, MINOR: 0, SUGGESTION: 0 },
'the unverified finding is NOT counted into a severity tier');
assert.ok(withUnverified.allow_blocked_by.length > 0);
assert.equal(computeVerdict([{ severity: 'BLOCKER' }], { unverified: u }).verdict, 'BLOCK',
'BLOCK stands regardless of the unverified bucket');
assert.equal(computeVerdict([{ severity: 'MAJOR' }], { unverified: u }).verdict, 'WARN');
assert.equal(computeVerdict([], { unverified: [] }).verdict, 'ALLOW',
'known-positive control: an empty unverified bucket still allows ALLOW');
});
test('computeVerdict — a reviewer that did not report forbids ALLOW', () => {
const r = computeVerdict([], { missingReviewers: ['brief-conformance-reviewer'] });
assert.equal(r.verdict, 'WARN');
assert.ok(r.allow_blocked_by.some((x) => x.includes('brief-conformance-reviewer')));
});
test('runContract — a BLOCKER dropped for an over-long title cannot yield ALLOW', () => {
// Pass 2 succinctness reads `.length`. It never examines the claim, so it
// cannot establish the finding is unreal -- it is unverified, not refuted.
const result = runContract([
{ reviewer: 'code-correctness-reviewer', findings: [
{ file: 'lib/auth/jwt.mjs', line: 19, rule_key: 'SECURITY_INJECTION', severity: 'BLOCKER', title: 'x'.repeat(101), detail: 'algo taken from the JWT header' },
] },
]);
assert.notEqual(result.verdict, 'ALLOW', 'an unsubstantiated BLOCKER must never clear the review');
assert.equal(result.findings.length, 0, 'it is still not a kept finding');
assert.equal(result.unverified.length, 1);
assert.equal(result.unverified[0].suppressed_reason, 'succinctness:title');
assert.equal(result.suppressed.length, 1, 'suppressed stays the union of dropped + unverified');
});
test('runContract — a schema-invalid payload cannot yield ALLOW (an unread reviewer is an absent one)', () => {
// Measured: one ad-hoc rule_key invalidates the WHOLE payload at ingest, so a
// valid BLOCKER sibling disappears with it. That must not read as "clean".
const result = runContract([
{ reviewer: 'code-correctness-reviewer', findings: [
{ file: 'lib/auth/jwt.mjs', line: 19, rule_key: 'SECURITY_INJECTION', severity: 'BLOCKER', title: 'real' },
{ file: 'x.mjs', line: 1, rule_key: 'NOPE_KEY', severity: 'MINOR', title: 'ad-hoc key' },
] },
]);
assert.equal(result.skipped.length, 1);
assert.notEqual(result.verdict, 'ALLOW');
assert.ok(result.allow_blocked_by.some((x) => x.includes('code-correctness-reviewer')));
});
test('runContract — a reviewer named in expectedReviewers that never reported cannot yield ALLOW', () => {
const result = runContract(
[{ reviewer: 'code-correctness-reviewer', findings: [] }],
{ expectedReviewers: ['code-correctness-reviewer', 'brief-conformance-reviewer'] },
);
assert.deepEqual(result.missing_reviewers, ['brief-conformance-reviewer']);
assert.notEqual(result.verdict, 'ALLOW');
});
test('runContract — known-positive control: every reviewer reported, nothing suppressed → ALLOW', () => {
// Proves ALLOW is still REACHABLE. Without this, "no ALLOW" is not a
// fail-closed contract, only a broken one.
const result = runContract(
[
{ reviewer: 'code-correctness-reviewer', findings: [
{ file: 'a.mjs', line: 1, rule_key: 'MISSING_ERROR_HANDLING', severity: 'MINOR', title: 'unguarded await', recommended_action: 'Wrap the await in a try/catch.' },
] },
{ reviewer: 'brief-conformance-reviewer', findings: [] },
],
{ expectedReviewers: ['code-correctness-reviewer', 'brief-conformance-reviewer'] },
);
assert.equal(result.verdict, 'ALLOW');
assert.equal(result.unverified.length, 0);
assert.deepEqual(result.missing_reviewers, []);
assert.deepEqual(result.allow_blocked_by, []);
});
test('classifySuppression — the refuting reasons the LLM coordinator emits are declared here too', () => {
// agents/review-coordinator.md Pass 2 "Accuracy" and Pass 3 "Non-existent
// file" DO refute (a citation outside the repo root, a file absent from both
// tree and diff). Both are fs/judgement branches this deterministic subset
// excludes, but the vocabulary is owned here so prose and lib cannot drift.
assert.equal(classifySuppression('accuracy:refuted'), 'refuted');
assert.equal(classifySuppression('file-existence:refuted'), 'refuted');
assert.equal(classifySuppression('file-existence:indeterminate'), 'unverified',
'unresolvable must never collapse into refuted');
});
test('suppression vocabulary — the two sets are disjoint and every reason is documented in the prose', () => {
const refuting = [...REFUTING_REASONS];
const overlap = refuting.filter((r) => UNVERIFIED_REASONS.includes(r));
assert.deepEqual(overlap, [], 'a reason cannot be both refuting and unverified');
const prose = readFileSync(join(ROOT, 'agents/review-coordinator.md'), 'utf-8');
assert.ok(prose.includes('review-coordinator'), 'known-positive control: the prose file loaded');
for (const reason of [...refuting, ...UNVERIFIED_REASONS]) {
assert.ok(prose.includes(reason),
`reason "${reason}" is declared in the lib but never documented in agents/review-coordinator.md`);
}
});
test('runContract — an anonymous invalid payload is unattributable, not a reviewer named "unnamed reviewer"', () => {
// `validateFindings` only WARNS on a missing `reviewer`, so a payload can
// fail schema while carrying no name. Reporting it as a reviewer name
// invents an agent nobody launched, and double-counts with expectedReviewers
// when they are in fact the same failure.
const result = runContract(
[{ findings: [{ file: 'x.mjs', line: 1, rule_key: 'NOPE', severity: 'MAJOR' }] }],
{ expectedReviewers: ['code-correctness-reviewer'] },
);
assert.deepEqual(result.missing_reviewers, ['code-correctness-reviewer'],
'missing_reviewers carries real names only');
assert.equal(result.unattributable_payloads, 1);
assert.ok(result.allow_blocked_by.some((x) => x.startsWith('unattributable-payload')));
assert.ok(!result.allow_blocked_by.some((x) => x.includes('unnamed reviewer')));
});
test('runContract — an anonymous invalid payload forbids ALLOW on its own, with no expectedReviewers', () => {
// The fail-closed floor must not depend on the caller passing an expected
// set: without this, dropping the name from a payload would restore ALLOW.
const result = runContract([{ findings: [{ file: 'x.mjs', line: 1, rule_key: 'NOPE', severity: 'MAJOR' }] }]);
assert.deepEqual(result.missing_reviewers, []);
assert.equal(result.unattributable_payloads, 1);
assert.notEqual(result.verdict, 'ALLOW');
});

View file

@ -701,23 +701,6 @@ test('producing commands emit file:// link in final report (operator-UX contract
} }
}); });
test('package.json is marked private — the npm name `voyage` belongs to a third party', () => {
// D-census 2026-08-20/21: registry.npmjs.org/voyage answers 200, but that is
// NOT this package — it is "Advanced HTTP Routing System for Node.js"
// (NEURS/voyage, v0.0.1, 2022, maintainer kevin.martin). This manifest is test
// tooling for a Claude Code plugin distributed through the marketplace catalogue
// (`ref: vX.Y.Z`), never through npm. Without `private`, nothing in the manifest
// stops an accidental `npm publish` against a name someone else owns — a one-way
// action. Sibling non-package manifests (okr, repo-mailbox, repo-standard) all
// carry the flag; voyage was the exception. Operator decision (S93): option A.
const pkg = JSON.parse(read('package.json'));
assert.equal(
pkg.private,
true,
'package.json must declare "private": true — the npm name `voyage` is owned by a third party',
);
});
test('package.json still has no "npm run render" script (removed in v5.0.1)', () => { test('package.json still has no "npm run render" script (removed in v5.0.1)', () => {
const pkg = JSON.parse(read('package.json')); const pkg = JSON.parse(read('package.json'));
assert.equal( assert.equal(
@ -1134,57 +1117,6 @@ test('deep-research-engine: --engine is documented + consistent across surfaces'
); );
}); });
test('deep-research-engine: the CC 2.1.218 operator-only ceiling is documented on every surface', () => {
// CC 2.1.218 changed /deep-research to "start only when invoked manually"; from
// there the Skill tool refuses a model invocation outright with
// `disable-model-invocation`. The flag stays (additive opt-in, always degrades to
// swarm), but no surface may keep promising a path Claude Code has removed, and
// none may leave `2.1.154+` standing alone as the engine's requirement.
const SURFACES = ['commands/trekresearch.md', 'docs/command-modes.md', 'README.md'];
for (const f of SURFACES) {
const body = read(f);
assert.ok(
body.includes('2.1.218'),
`${f} must document the CC 2.1.218 ceiling for the deep-research engine`,
);
assert.ok(
body.includes('disable-model-invocation'),
`${f} must name disable-model-invocation as the fallback reason from CC 2.1.218`,
);
}
// The one-line reference rows carry the whole truth on their own line — an
// operator reading the flag table never scrolls to the command prose.
const ROWS = [
['docs/command-modes.md', /^.*`--engine \{swarm\\\|deep-research\}`.*$/m],
['README.md', /^\|\s*\*\*Engine\*\*.*$/m],
];
for (const [f, re] of ROWS) {
const m = read(f).match(re);
assert.ok(m, `${f} must still carry the --engine reference row`);
assert.ok(
m[0].includes('2.1.218') && m[0].includes('disable-model-invocation'),
`${f} --engine row must state the real window (2.1.218 + disable-model-invocation)`,
);
}
// Both load-bearing regions of the command itself: the flag bullet and the
// pre-gate that decides whether the delegation is attempted at all.
const research = read('commands/trekresearch.md');
const bullet = research.match(/8\. `--engine <name>`[\s\S]*?Flags can be combined/);
assert.ok(bullet, 'trekresearch.md must still carry the --engine flag bullet');
assert.ok(
bullet[0].includes('2.1.218') && bullet[0].includes('disable-model-invocation'),
'trekresearch.md --engine bullet must state the real window, not just the 2.1.154 floor',
);
const pregate = research.match(/\*\*Coarse pre-gate[\s\S]*?authoritative guard\./);
assert.ok(pregate, 'trekresearch.md must still carry the deep-research pre-gate');
assert.ok(
pregate[0].includes('2.1.218'),
'the pre-gate must carry an upper ceiling at 2.1.218, not only the 2.1.154 floor',
);
});
// ── STORM bounded loop — env-vars documented across the four surfaces ────── // ── STORM bounded loop — env-vars documented across the four surfaces ──────
// Same cross-doc shape as the --engine pin above. An operator-facing switch // Same cross-doc shape as the --engine pin above. An operator-facing switch
// documented on one surface is a switch most operators never find; and the // documented on one surface is a switch most operators never find; and the
@ -1702,94 +1634,3 @@ test('v5.9: no commands/*.md frontmatter carries a model: key (session inheritan
assert.deepEqual(offenders, [], assert.deepEqual(offenders, [],
`command frontmatter must omit model: (orchestrator follows the session model); offenders: ${offenders.join(', ')}`); `command frontmatter must omit model: (orchestrator follows the session model); offenders: ${offenders.join(', ')}`);
}); });
// --- S95 (③): gemini-bridge is DROPPED from the pipeline (operator directive) ---
//
// Operator directive, three repetitions: gemini-mcp and voyage:gemini-bridge are
// never to be used. The drop is permanent, not a wait-for-upgrade — so the pin is
// on the LIVE pipeline surfaces only. Historical records (CHANGELOG entries,
// measurement/decision docs) must KEEP their references: they say what a past
// version did or what was measured then, and rewriting them is the same defect
// class as bumping a version string inside a measurement doc.
//
// `gemini_used` is deliberately NOT banned. It is a vestigial trekresearch stats
// field, pinned to false by the drop and kept so the observability export schema
// does not break for existing consumers (see TREKRESEARCH_ALLOWED).
// Live = anything that can still steer a run. Explicit, so a new file forces a decision.
const GEMINI_LIVE_SURFACES = [
'README.md',
'CLAUDE.md',
'settings.json',
'docs/architecture.md',
'docs/command-modes.md',
'templates/research-brief-template.md',
'templates/trekbrief-template.md',
'tests/fixtures/jsonl-schemas.md',
...listMd('commands').map((f) => `commands/${f}`),
...listMd('agents').map((f) => `agents/${f}`),
];
// Historical/measurement records that MUST keep their gemini references.
const GEMINI_HISTORICAL = [
'CHANGELOG.md',
'docs/cc-upgrade-2.1.181-decision-matrix.md',
'docs/voyage-vs-cc-balance-analysis.md',
'docs/BRIEF-vurdering-v2.md',
'docs/balance-backlog-plan.md',
'docs/agent-description-token-trim-brief.md',
'docs/S22-happy-path-dogfood.md',
'docs/deep-research-engine-brief.md',
'examples/02-real-cli/REGENERATED.md',
];
test('S95: agents/gemini-bridge.md is removed from the agent inventory', () => {
assert.ok(
!listMd('agents').includes('gemini-bridge.md'),
'gemini-bridge is dropped by operator directive — agents/gemini-bridge.md must not exist',
);
});
test('S95: no live pipeline surface still names the gemini-bridge agent', () => {
const offenders = [];
for (const rel of GEMINI_LIVE_SURFACES) {
// Everything gemini EXCEPT the vestigial gemini_used stats field.
const hits = (read(rel).match(/gemini[\w-]*/gi) || []).filter(
(m) => m.toLowerCase() !== 'gemini_used',
);
if (hits.length) offenders.push(`${rel} (${[...new Set(hits)].join(', ')})`);
}
assert.deepEqual(
offenders, [],
`gemini-bridge is dropped — these live surfaces still reference it: ${offenders.join(' · ')}`,
);
});
test('S95: known-positive control — the historical records still DO carry gemini references', () => {
// Verifiseringsloven ansikt 4: a scanner that finds nothing anywhere proves
// nothing. This pins that the same query CAN find, so the empty result above
// is a measurement and not a broken query.
const silent = GEMINI_HISTORICAL.filter((rel) => !/gemini/i.test(read(rel)));
assert.deepEqual(
silent, [],
`these historical records lost their gemini references — history must not be rewritten: ${silent.join(', ')}`,
);
});
test('S95: trekplan high-effort Pass 2 degrades explicitly instead of failing', () => {
const t = read('commands/trekplan.md');
const section = t.split('\n### High-effort behavior (v5.1.1)\n')[1];
assert.ok(section, 'trekplan.md must still carry the High-effort behavior sub-section');
const head = section.split('\n## ')[0];
assert.match(
head, /unavailable, skipped/,
'the dropped Pass 2 must read "unavailable, skipped" explicitly — a removed pass that is silent is indistinguishable from one that failed',
);
});
test('S95: gemini_used survives as a vestigial stats field (export schema unbroken)', () => {
assert.ok(
TREKRESEARCH_ALLOWED.has('gemini_used'),
'gemini_used stays in the trekresearch export allowlist — the agent is dropped, the field is pinned false, and removing it would break existing consumers',
);
});