feat(trekresearch): add Phase 4.5 dimension discovery and amend Independence rule

This commit is contained in:
Kjell Tore Guttormsen 2026-08-09 14:58:48 +02:00
commit d9cba9c6ea
3 changed files with 121 additions and 5 deletions

View file

@ -7,12 +7,20 @@ tools: ["Read", "Glob", "Grep", "Write", "Edit", "Bash"]
--- ---
<!-- Phase mapping: orchestrator → command <!-- Phase mapping: orchestrator → command
Corrected in v5.10: every row below was off by one, and the old last row
pointed at a ninth command phase that does not exist — the command ends
at Phase 8.
Orchestrator Phase 1 = Command Phase 4 (Agent group selection) Orchestrator Phase 1 = Command Phase 4 (Agent group selection)
Orchestrator Phase 2 = Command Phase 5 (Parallel research) Orchestrator Phase 2 = Command Phase 4 (Parallel research — same
Orchestrator Phase 3 = Command Phase 6 (Targeted follow-ups) command phase; the orchestrator
Orchestrator Phase 4 = Command Phase 7 (Triangulation) splits selection from launch)
Orchestrator Phase 5 = Command Phase 8 (Synthesis + write brief) (no orchestrator phase)= Command Phase 4.5 (Dimension discovery,
Orchestrator Phase 6 = Command Phase 9 (Completion) high effort only — v5.10)
Orchestrator Phase 3 = Command Phase 5 (Targeted follow-ups; bounded
conversation loop at high effort)
Orchestrator Phase 4 = Command Phase 6 (Triangulation)
Orchestrator Phase 5 = Command Phase 7 (Synthesis + write brief)
Orchestrator Phase 6 = Command Phase 8 (Present and track / completion)
As of v2.4.0, /trekresearch runs these phases inline in main As of v2.4.0, /trekresearch runs these phases inline in main
context instead of spawning this agent. Keep this file as the canonical context instead of spawning this agent. Keep this file as the canonical
reference for what those phases do. --> reference for what those phases do. -->

View file

@ -391,6 +391,34 @@ other agents — the value of Gemini is independence.
small = halved, medium/large = default small = halved, medium/large = default
- convention-scanner: medium+ codebases only (50+ files) - convention-scanner: medium+ codebases only (50+ files)
## Phase 4.5 — Dimension discovery
**Skip this phase entirely unless `phase_signal_result.effort == 'high'`.**
Phase 4 retrieves more than the interview knew to ask for. This phase mines
that surplus: findings that were **retrieved but unintegrated** — material an
agent surfaced that no interview dimension claims.
1. **Mine.** Walk the Phase-4 agent results and collect findings that map to
no existing dimension.
2. **Rerank.** Order candidates by relevance to the research question **and**
dissimilarity to the dimensions already on the list. A candidate that
restates an existing dimension is not a discovery.
3. **Augment under the existing ceiling.** Append candidates to the dimension
list only while the **whole** list (interview + discovered) stays at or
below `maxDimensions: 8` (`settings.json:16`). The ceiling is **not**
raised here, so the documented 38 dimension range stays true and the
README prose about it stays untouched. If the interview already produced 8
dimensions, this phase discovers nothing and says so.
4. **Record the baseline.** Keep the interview-derived count as
`dimensions_baseline` so the discovered delta is machine-readable against
the final `dimensions` (Phase 8 stats).
Every outbound query generated from a discovered dimension passes
`query-privacy-gate.mjs` before it leaves the machine — see the per-turn
protocol in Phase 5. That gate is the compensating control for the
Independence crossing this phase makes (see Hard rules → Independence).
## Phase 5 — Targeted follow-ups ## Phase 5 — Targeted follow-ups
Review all agent results. Identify knowledge gaps — dimensions where findings Review all agent results. Identify knowledge gaps — dimensions where findings
@ -727,6 +755,16 @@ Low effort: inline research only, no agent swarm (existing
- **Sources required:** Every claim must cite a source. No unsourced findings. - **Sources required:** Every claim must cite a source. No unsourced findings.
- **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.
Triangulate AFTER independent research. Triangulate AFTER independent research.
**Amended (v5.10) for Phase 4.5:** dimension discovery deliberately crosses this
rule. Its candidate dimensions are mined from the Phase-4 result set, which
contains output from the five local codebase agents, so a discovered dimension
can carry local context into an external query. The crossing is bounded to
Phase 4.5 and the Phase 5 loop it feeds, it never applies to the initial
external swarm (which stays blind to local findings), and the compensating
control is `query-privacy-gate.mjs`: every outbound query is inspected before
it leaves the machine, with a hard-block tier for secret-shaped strings that no
operator flag can override. Triangulation still happens AFTER independent
research.
- **Graceful degradation:** If MCP tools are unavailable (Tavily, Gemini, 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:

View file

@ -166,6 +166,76 @@ test('trekresearch — High-effort behavior keeps the standard/low effort senten
); );
}); });
// --- Step 8: Phase 4.5 dimension discovery + Independence amendment ---
const ORCHESTRATOR_FILE = join(ROOT, 'agents', 'research-orchestrator.md');
function readOrchestrator() { return readFileSync(ORCHESTRATOR_FILE, 'utf8'); }
test('trekresearch — Phase 4.5 exists between Phase 4 and Phase 5 with the effort skip-guard', () => {
const text = read();
const p45 = text.indexOf('## Phase 4.5 —');
assert.ok(p45 >= 0, 'Phase 4.5 heading missing');
const p4 = text.indexOf('## Phase 4 —');
const p5 = text.indexOf('## Phase 5 —');
assert.ok(p4 >= 0 && p5 > p45 && p45 > p4, 'Phase 4.5 must sit between Phase 4 and Phase 5');
const slice = text.slice(p45, p5);
assert.match(
slice,
/\*\*Skip this phase entirely unless `phase_signal_result\.effort == 'high'`\.\*\*/,
'Phase 4.5 must carry the bolded skip-guard in the Phase 3.5 form',
);
assert.match(slice, /query-privacy-gate\.mjs/,
'Phase 4.5 must name the privacy gate as its compensating control');
assert.match(slice, /maxDimensions: 8|maxDimensions` *: *8/,
'Phase 4.5 must augment under the existing maxDimensions ceiling, not raise it');
});
test('trekresearch — Independence hard rule carries an explicit Phase 4.5 amendment', () => {
const text = read();
const rulesIdx = text.indexOf('## Hard rules');
assert.ok(rulesIdx >= 0, 'Hard rules section missing');
const rules = text.slice(rulesIdx);
const indIdx = rules.indexOf('**Independence:**');
assert.ok(indIdx >= 0, 'Independence hard rule missing');
// Bound the rule at the next bullet so the amendment must live inside it.
const nextBullet = rules.indexOf('\n- **', indIdx);
const independence = nextBullet > indIdx ? rules.slice(indIdx, nextBullet) : rules.slice(indIdx);
assert.match(independence, /Amend(ed|ment)/i,
'Independence must be explicitly amended, not silently contradicted');
assert.match(independence, /Phase 4\.5/, 'the amendment must name Phase 4.5 as the crossing');
assert.match(independence, /query-privacy-gate\.mjs/,
'the amendment must name the compensating control');
});
test('trekresearch — orchestrator phase map is correct, has no Phase 9, and carries Phase 4.5', () => {
const doc = readOrchestrator();
const start = doc.indexOf('<!-- Phase mapping');
assert.ok(start >= 0, 'phase mapping comment missing');
const end = doc.indexOf('-->', start);
assert.ok(end > start, 'phase mapping comment not terminated');
const map = doc.slice(start, end);
assert.doesNotMatch(map, /Command Phase 9/,
'the command ends at Phase 8 — a Command Phase 9 row is a fiction');
// Six orchestrator rows, each pointing at the phase the command actually has.
const expected = [
[1, '4'],
[2, '4'],
[3, '5'],
[4, '6'],
[5, '7'],
[6, '8'],
];
for (const [orch, cmd] of expected) {
const re = new RegExp(`Orchestrator Phase ${orch}\\s+= Command Phase ${cmd.replace('.', '\\.')}\\b`);
assert.match(map, re, `map row for Orchestrator Phase ${orch} must point at Command Phase ${cmd}`);
}
assert.match(map, /Command Phase 4\.5/, 'the map must carry the new Phase 4.5 row');
});
// --- v5.1.1 runtime SC4 + SC7 --- // --- v5.1.1 runtime SC4 + SC7 ---
test('trekresearch — SC4: low-effort fixture → resolver returns {effort: low, model: sonnet}', () => { test('trekresearch — SC4: low-effort fixture → resolver returns {effort: low, model: sonnet}', () => {