peaks-loop 4.0.27 → 4.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/dist/cli/commands/dispatch-commands.js +25 -1
- package/dist/cli/commands/job-commands.js +14 -1
- package/dist/cli/commands/request-commands.js +13 -1
- package/dist/services/codegraph/codegraph-autorefresh.d.ts +22 -0
- package/dist/services/codegraph/codegraph-autorefresh.js +91 -0
- package/dist/services/codegraph/codegraph-preflight-service.d.ts +53 -0
- package/dist/services/codegraph/codegraph-preflight-service.js +226 -0
- package/dist/services/codegraph/codegraph-service.d.ts +16 -42
- package/dist/services/codegraph/codegraph-service.js +23 -66
- package/dist/services/context/build-dispatch-system-prompt.d.ts +26 -0
- package/dist/services/context/build-dispatch-system-prompt.js +35 -3
- package/dist/services/doctor/doctor-service/checks/codegraph-capability.d.ts +4 -5
- package/dist/services/doctor/doctor-service/checks/codegraph-capability.js +16 -32
- package/dist/services/doctor/doctor-service/types.d.ts +11 -15
- package/dist/services/ide/adapters/claude-code-adapter.d.ts +7 -0
- package/dist/services/ide/adapters/claude-code-adapter.js +57 -4
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.0.29 — 2026-09-03 (codegraph 生命周期闭环 — pre-read + auto-refresh)
|
|
4
|
+
|
|
5
|
+
**3 atomic commits from session 2026-09-03-session-d49394** (user feedback #3):
|
|
6
|
+
|
|
7
|
+
- `7b9fe627` feat(codegraph): auto-refresh index after slice completion (CLI-internal trigger)
|
|
8
|
+
- `b6599b56` feat(codegraph): preflight project structure into RD dispatch prompts
|
|
9
|
+
- `dfbd229e` docs(memory): sediment codegraph lifecycle closure (pre-read + auto-refresh)
|
|
10
|
+
|
|
11
|
+
**Highlights**:
|
|
12
|
+
|
|
13
|
+
1. **slice/request 完成后自动刷新 codegraph** — `job checkpoint --state done` / `request transition --state qa-handoff` 成功后自动 `peaks codegraph index`。CLI-internal 触发(vendor-neutral、不可绕过、恰在 slice 边界 fire 一次),best-effort fail-silent(codegraph 失败不阻塞 checkpoint)。16 个新 BDD 测试。
|
|
14
|
+
|
|
15
|
+
2. **RD 规划前强制读 codegraph 结构** — peaks-code dispatch RD 前 preflight:确保 index(无则 init+index,fresh 则跳过,foreign-schema fail-soft)+ 读有界 `codegraph files --json` 结构(cap 40 dirs / 12 root files)渲染 `## Codegraph structure` 块注入 RD prompt。codegraph 不可用 → 优雅降级 note,dispatch 照常。13 个新 BDD 测试。
|
|
16
|
+
|
|
17
|
+
3. 闭环成型:规划前 pre-read(Slice B)→ 实现 → 完成后 auto-refresh(Slice A)→ 下一 slice 读到新图。
|
|
18
|
+
|
|
19
|
+
## 4.0.28 — 2026-09-03 (codegraph 目录回根 + env-first 1M 窗口识别)
|
|
20
|
+
|
|
21
|
+
**3 atomic commits from session 2026-09-03-session-d49394** (user feedback batch):
|
|
22
|
+
|
|
23
|
+
- `c0721d3a` fix(codegraph): revert data dir to root `.codegraph/` — drop `.peaks/.codegraph` handling
|
|
24
|
+
- `7011e64c` fix(context): env-first model window resolution — `[1M]`-suffixed model → 1M context
|
|
25
|
+
- `5289a9b3` docs(memory): sediment codegraph root revert + env-first 1M window fix
|
|
26
|
+
|
|
27
|
+
**Highlights**:
|
|
28
|
+
|
|
29
|
+
1. **codegraph 数据目录完全回根** — rid-CG-003(4.0.20)引入的 `.peaks/.codegraph/` preferred 位置被移除,数据目录回到根 `.codegraph/`(上游默认语义)。删除 `PREFERRED_CODEGRAPH_DIR` + preferred/legacy/fresh-preferred 三态 union;`resolveCodegraphProjectRoot` 纯路径数学返回 root;doctor messaging / `.gitignore` / 3 测试同步。用户明确"完全回根,去掉 .peaks 路径"。
|
|
30
|
+
|
|
31
|
+
2. **env-first 模型上下文窗口识别(1M vs 200K)** — 之前只从 transcript `message.model` 读模型 id,而 Claude Code / deepseek 等的 transcript 记录**裸模型名**(`deepseek-v4-flash`,不带 `[1M]`),导致 1M 上下文模型被误判为 200K 并在 ~50% 处 false soft-warn。新增 `resolveClaudeModelFromEnv` 优先读 env 模型标记(`ANTHROPIC_MODEL` → `ANTHROPIC_DEFAULT_OPUS/SONNET/HAIKU/FABLE_MODEL` → `CLAUDE_CODE_SUBAGENT_MODEL`,`[1M]`/`[1m]` 后缀即 1M),transcript 兜底。实测修复后 `context-now` 从 `capacityTokens: 200000, ratio 0.88` 变为 `capacityTokens: 1000000, ratio 0.19`。
|
|
32
|
+
|
|
3
33
|
## 4.0.27 — 2026-09-01 (auto-compact context probe fix)
|
|
4
34
|
|
|
5
35
|
**1 atomic commit from session 2026-09-01-session-fdd7aa**:
|
|
@@ -371,6 +371,26 @@ export function registerDispatchCommand(parent, io) {
|
|
|
371
371
|
// pure-function builder.
|
|
372
372
|
const preflightService = new MemoryPreflightService(projectRoot, projectPrefs);
|
|
373
373
|
const memoryBlock = await preflightService.fetchBlock(role);
|
|
374
|
+
// Slice 2026-09-03-codegraph-preread (Option A): pre-dispatch
|
|
375
|
+
// codegraph preflight for RD planning. BEFORE the RD sub-agent's
|
|
376
|
+
// prompt is composed, ensure the codegraph index exists (init +
|
|
377
|
+
// index best-effort when `.codegraph/` is absent; skip when already
|
|
378
|
+
// fresh) and read a BOUNDED project-structure summary. The block is
|
|
379
|
+
// only requested for the `rd` role — other roles keep the legacy
|
|
380
|
+
// prompt byte-identical. Fail-soft: any codegraph failure degrades
|
|
381
|
+
// to a null block (builder renders the "codegraph unavailable" note)
|
|
382
|
+
// and the dispatch proceeds; we never hard-block RD dispatch here.
|
|
383
|
+
let codegraphBlock;
|
|
384
|
+
if (role === 'rd') {
|
|
385
|
+
try {
|
|
386
|
+
const { buildCodegraphPreflightBlock } = await import('../../services/codegraph/codegraph-preflight-service.js');
|
|
387
|
+
const preflight = await buildCodegraphPreflightBlock(projectRoot);
|
|
388
|
+
codegraphBlock = preflight.available ? preflight.block : null;
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
codegraphBlock = null; // fail-soft: never block RD dispatch
|
|
392
|
+
}
|
|
393
|
+
}
|
|
374
394
|
// Slice 2026-07-29-context-evaluation-accuracy: capture the
|
|
375
395
|
// authoritative context-fill probe before composing the
|
|
376
396
|
// dispatch prompt. The probe is token-counted (IDE adapter's
|
|
@@ -400,7 +420,11 @@ export function registerDispatchCommand(parent, io) {
|
|
|
400
420
|
taskTitle: role,
|
|
401
421
|
taskBody: options.prompt,
|
|
402
422
|
memoryBlock,
|
|
403
|
-
contextProbe
|
|
423
|
+
contextProbe,
|
|
424
|
+
// exactOptionalPropertyTypes: only set codegraphBlock when the rd
|
|
425
|
+
// preflight actually produced a value (null = attempted-unavailable,
|
|
426
|
+
// undefined = not requested → legacy prompt unchanged).
|
|
427
|
+
...(codegraphBlock !== undefined ? { codegraphBlock } : {})
|
|
404
428
|
});
|
|
405
429
|
// Part 2.C: when --isolation worktree, prepend an isolation envelope
|
|
406
430
|
// block so the sub-agent sees the lease id + worktree path. The block
|
|
@@ -13,6 +13,7 @@ import { JobInitInputSchema, JobCheckpointInputSchema, JobBlockInputSchema, } fr
|
|
|
13
13
|
import { getCurrentSessionId } from '../../services/skills/skill-presence-service.js';
|
|
14
14
|
import { buildCostCheckEnvelope, runKarpathyCostCheck, } from '../../services/karpathy-cost/karpathy-cost-check-service.js';
|
|
15
15
|
import { read24hState } from '../../services/24h-mode/store.js';
|
|
16
|
+
import { refreshCodegraphAfterSlice, } from '../../services/codegraph/codegraph-autorefresh.js';
|
|
16
17
|
function projectRoot(opts) {
|
|
17
18
|
// Reuse the workspace root resolver from peaks CLI; for now, CWD as a safe placeholder.
|
|
18
19
|
return opts.project ?? process.cwd();
|
|
@@ -172,6 +173,10 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
|
|
|
172
173
|
const jobRoot = resolveJobStateRoot(opts);
|
|
173
174
|
const store = new JobStateStore(jobRoot.rootDir);
|
|
174
175
|
const orch = new JobOrchestrator(store);
|
|
176
|
+
// 2026-09-03-codegraph-autorefresh: set on --state done so the ok
|
|
177
|
+
// envelope carries a non-blocking `codegraph` result; null for
|
|
178
|
+
// failed/skipped (no slice-complete boundary).
|
|
179
|
+
let codegraph = null;
|
|
175
180
|
if (parsed.data.state === 'done') {
|
|
176
181
|
await orch.checkpointDone({ jobId: parsed.data.jobId, sliceId: parsed.data.sliceId, ...(parsed.data.commitSha ? { commitSha: parsed.data.commitSha } : {}) });
|
|
177
182
|
// v3.1.2: after each --state done, mirror slice progress to
|
|
@@ -188,6 +193,14 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
|
|
|
188
193
|
lastCommitSha: parsed.data.commitSha ?? null,
|
|
189
194
|
updatedAt: new Date().toISOString()
|
|
190
195
|
});
|
|
196
|
+
// Auto codegraph refresh at the slice-complete boundary. Best-effort
|
|
197
|
+
// and fail-silent: a refresh failure must never fail the checkpoint.
|
|
198
|
+
try {
|
|
199
|
+
codegraph = await refreshCodegraphAfterSlice(project);
|
|
200
|
+
}
|
|
201
|
+
catch (e) {
|
|
202
|
+
codegraph = { refreshed: false, reason: 'unavailable', note: `auto codegraph refresh failed: ${e instanceof Error ? e.message : String(e)}` };
|
|
203
|
+
}
|
|
191
204
|
}
|
|
192
205
|
else if (parsed.data.state === 'skipped') {
|
|
193
206
|
await orch.checkpointSkipped({ jobId: parsed.data.jobId, sliceId: parsed.data.sliceId, reason: parsed.data.reason });
|
|
@@ -195,7 +208,7 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
|
|
|
195
208
|
else {
|
|
196
209
|
await orch.checkpointFailed({ jobId: parsed.data.jobId, sliceId: parsed.data.sliceId, reason: parsed.data.reason });
|
|
197
210
|
}
|
|
198
|
-
printResult(io, ok('checkpoint', { sliceId: parsed.data.sliceId, status: parsed.data.state }), opts);
|
|
211
|
+
printResult(io, ok('checkpoint', { sliceId: parsed.data.sliceId, status: parsed.data.state, codegraph }), opts);
|
|
199
212
|
});
|
|
200
213
|
addJsonOption(job.commands.find(c => c.name() === 'checkpoint'));
|
|
201
214
|
job
|
|
@@ -6,6 +6,7 @@ import { lintRequestArtifact } from '../../services/artifacts/artifact-lint-serv
|
|
|
6
6
|
import { getRepairCycleStatus } from '../../services/artifacts/repair-cycle-service.js';
|
|
7
7
|
import { fail, ok } from 'peaks-loop-shared/result';
|
|
8
8
|
import { triggerBestPracticeScan } from '../../services/prd/best-practice-auto-trigger.js';
|
|
9
|
+
import { refreshCodegraphAfterSlice, } from '../../services/codegraph/codegraph-autorefresh.js';
|
|
9
10
|
import { formatMdCompact } from '../../shared/format-md-compact.js';
|
|
10
11
|
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
11
12
|
/**
|
|
@@ -353,6 +354,10 @@ export function registerRequestCommands(program, io) {
|
|
|
353
354
|
// unchanged — the existing auto-compact orchestrator continues to
|
|
354
355
|
// refuse dispatch at ratio ≥ 0.95.
|
|
355
356
|
let preCompact = null;
|
|
357
|
+
// 2026-09-03-codegraph-autorefresh: auto codegraph re-index on the
|
|
358
|
+
// RD → QA slice-complete boundary. Set only for rd:qa-handoff; null
|
|
359
|
+
// otherwise. Best-effort and fail-silent — never blocks the transition.
|
|
360
|
+
let codegraphRefresh = null;
|
|
356
361
|
if (role === 'rd' && newState === 'qa-handoff') {
|
|
357
362
|
try {
|
|
358
363
|
const { maybePreCompactCheckpoint } = await import('../../services/compact/request-transition-hook.js');
|
|
@@ -369,6 +374,13 @@ export function registerRequestCommands(program, io) {
|
|
|
369
374
|
// The hook is best-effort; never block the transition.
|
|
370
375
|
preCompact = null;
|
|
371
376
|
}
|
|
377
|
+
try {
|
|
378
|
+
codegraphRefresh = await refreshCodegraphAfterSlice(options.project);
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
// The refresh is best-effort; never block the transition.
|
|
382
|
+
codegraphRefresh = { refreshed: false, reason: 'unavailable', note: 'auto codegraph refresh failed after transition' };
|
|
383
|
+
}
|
|
372
384
|
}
|
|
373
385
|
// v2.13.2 AC-4 — auto-regen prd/handoff.md on prd:handed-off success.
|
|
374
386
|
// Only fires when the handoff is missing; existing handoffs are not overwritten.
|
|
@@ -410,7 +422,7 @@ export function registerRequestCommands(program, io) {
|
|
|
410
422
|
// transition is not a slice boundary), preCompact is null and
|
|
411
423
|
// we emit a `preCompactCheckpoint: null` field so LLM callers
|
|
412
424
|
// can branch on it without re-deriving zone membership.
|
|
413
|
-
printResult(io, ok('request.transition', { ...result, preCompactCheckpoint: preCompact?.data ?? null }, preCompact !== null
|
|
425
|
+
printResult(io, ok('request.transition', { ...result, preCompactCheckpoint: preCompact?.data ?? null, codegraphRefresh }, preCompact !== null
|
|
414
426
|
? [
|
|
415
427
|
`Pre-compact checkpoint written at ratio=${typeof preCompact.data === 'object' && preCompact.data !== null && 'ratio' in preCompact.data
|
|
416
428
|
? String(preCompact.data.ratio)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type CodegraphProcessRunner } from './codegraph-service.js';
|
|
2
|
+
export type CodegraphAutorefreshResult = {
|
|
3
|
+
refreshed: true;
|
|
4
|
+
} | {
|
|
5
|
+
refreshed: false;
|
|
6
|
+
reason: 'no-codegraph-dir' | 'index-failed' | 'unavailable';
|
|
7
|
+
note: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* True when `<projectRoot>/.codegraph/` exists and is a directory.
|
|
11
|
+
* Pure fs probe; never throws.
|
|
12
|
+
*/
|
|
13
|
+
export declare function isCodegraphPresent(projectRoot: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Run a best-effort `codegraph index` refresh for `projectRoot` after a
|
|
16
|
+
* slice-complete boundary. NEVER throws — every failure path returns a
|
|
17
|
+
* non-refreshed result so the caller keeps its ok envelope.
|
|
18
|
+
*
|
|
19
|
+
* The optional `runner` is a test seam mirroring
|
|
20
|
+
* `CodegraphProcessRunner`; when omitted the real process runner is used.
|
|
21
|
+
*/
|
|
22
|
+
export declare function refreshCodegraphAfterSlice(projectRoot: string, runner?: CodegraphProcessRunner): Promise<CodegraphAutorefreshResult>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// src/services/codegraph/codegraph-autorefresh.ts
|
|
2
|
+
//
|
|
3
|
+
// Slice 2026-09-03-codegraph-autorefresh — Option 1: CLI-internal
|
|
4
|
+
// auto codegraph refresh at the slice-complete boundary.
|
|
5
|
+
//
|
|
6
|
+
// `peaks codegraph index` is incremental + idempotent, so re-running it
|
|
7
|
+
// after a slice that changed code is cheap and safe. Rather than rely on
|
|
8
|
+
// the orchestrator LLM to remember the prose rule in
|
|
9
|
+
// `skills/peaks-code/references/codegraph-orchestration.md` ("MUST
|
|
10
|
+
// proactively run `peaks codegraph index --project <path>` after each
|
|
11
|
+
// slice"), the checkpoint/transition command itself triggers the refresh
|
|
12
|
+
// right before it returns its ok envelope. This is the vendor-neutral
|
|
13
|
+
// CLI-internal form of "hook on slice-complete": it is un-bypassable
|
|
14
|
+
// (fires even when the LLM dispatches the command through any IDE / no
|
|
15
|
+
// hook install surface needed), fires exactly once at the true slice
|
|
16
|
+
// boundary, and needs no IDE hook plumbing.
|
|
17
|
+
//
|
|
18
|
+
// The refresh is best-effort and FAIL-SILENT — it never throws and never
|
|
19
|
+
// blocks the checkpoint/transition ok envelope:
|
|
20
|
+
// - No `<projectRoot>/.codegraph/` directory → skip (codegraph was
|
|
21
|
+
// never initialized for this project; `peaks codegraph init` is a
|
|
22
|
+
// one-time setup the orchestrator owns).
|
|
23
|
+
// - The upstream index exits non-zero → return `index-failed` with a
|
|
24
|
+
// human-readable note.
|
|
25
|
+
// - Any unexpected error → return `unavailable` with a note.
|
|
26
|
+
//
|
|
27
|
+
// We deliberately do NOT auto-init: `codegraph init` can prompt / take a
|
|
28
|
+
// long time on first run, which would make the background side-effect
|
|
29
|
+
// block the slice boundary. Projects that want auto-refresh first run
|
|
30
|
+
// `peaks codegraph init` once (per the orchestration doc).
|
|
31
|
+
import { statSync } from 'node:fs';
|
|
32
|
+
import { join } from 'node:path';
|
|
33
|
+
import { CODEGRAPH_DIR_NAME, createCodegraphInvocation, executeCodegraphInvocation, } from './codegraph-service.js';
|
|
34
|
+
/**
|
|
35
|
+
* True when `<projectRoot>/.codegraph/` exists and is a directory.
|
|
36
|
+
* Pure fs probe; never throws.
|
|
37
|
+
*/
|
|
38
|
+
export function isCodegraphPresent(projectRoot) {
|
|
39
|
+
try {
|
|
40
|
+
return statSync(join(projectRoot, CODEGRAPH_DIR_NAME)).isDirectory();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function errorMessage(error) {
|
|
47
|
+
return error instanceof Error ? error.message : String(error);
|
|
48
|
+
}
|
|
49
|
+
function firstMeaningfulLine(text) {
|
|
50
|
+
const trimmed = text.trim();
|
|
51
|
+
if (trimmed.length === 0)
|
|
52
|
+
return 'no upstream output';
|
|
53
|
+
const first = trimmed.split(/\r?\n/)[0];
|
|
54
|
+
return first !== undefined ? first.slice(0, 200) : 'no upstream output';
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Run a best-effort `codegraph index` refresh for `projectRoot` after a
|
|
58
|
+
* slice-complete boundary. NEVER throws — every failure path returns a
|
|
59
|
+
* non-refreshed result so the caller keeps its ok envelope.
|
|
60
|
+
*
|
|
61
|
+
* The optional `runner` is a test seam mirroring
|
|
62
|
+
* `CodegraphProcessRunner`; when omitted the real process runner is used.
|
|
63
|
+
*/
|
|
64
|
+
export async function refreshCodegraphAfterSlice(projectRoot, runner) {
|
|
65
|
+
if (!isCodegraphPresent(projectRoot)) {
|
|
66
|
+
return {
|
|
67
|
+
refreshed: false,
|
|
68
|
+
reason: 'no-codegraph-dir',
|
|
69
|
+
note: `auto codegraph refresh skipped: no ${CODEGRAPH_DIR_NAME} directory at ${join(projectRoot, CODEGRAPH_DIR_NAME)}. Run \`peaks codegraph init\` once to enable post-slice auto-refresh.`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const invocation = createCodegraphInvocation({ subcommand: 'index', project: projectRoot, quiet: true });
|
|
74
|
+
const result = await executeCodegraphInvocation(invocation, runner);
|
|
75
|
+
if (result.exitCode !== 0) {
|
|
76
|
+
return {
|
|
77
|
+
refreshed: false,
|
|
78
|
+
reason: 'index-failed',
|
|
79
|
+
note: `auto codegraph refresh failed (exit ${String(result.exitCode)}): ${firstMeaningfulLine(result.stderr || result.stdout)}`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return { refreshed: true };
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
return {
|
|
86
|
+
refreshed: false,
|
|
87
|
+
reason: 'unavailable',
|
|
88
|
+
note: `auto codegraph refresh unavailable: ${errorMessage(error)}`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type CodegraphProcessRunner } from './codegraph-service.js';
|
|
2
|
+
export type CodegraphPreflightResult = {
|
|
3
|
+
available: true;
|
|
4
|
+
block: string;
|
|
5
|
+
fileCount: number;
|
|
6
|
+
truncated: boolean;
|
|
7
|
+
} | {
|
|
8
|
+
available: false;
|
|
9
|
+
note: string;
|
|
10
|
+
};
|
|
11
|
+
/** Cap for the directory histogram in the rendered structure block. */
|
|
12
|
+
export declare const CODEGRAPH_STRUCTURE_MAX_DIRS = 40;
|
|
13
|
+
/** Cap for bare root files listed in the rendered structure block. */
|
|
14
|
+
export declare const CODEGRAPH_STRUCTURE_MAX_ROOT_FILES = 12;
|
|
15
|
+
export interface CodegraphStructureFileEntry {
|
|
16
|
+
readonly path: string;
|
|
17
|
+
}
|
|
18
|
+
export interface CodegraphStructureRenderOptions {
|
|
19
|
+
readonly maxDirs?: number;
|
|
20
|
+
readonly maxRootFiles?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface CodegraphStructureSummary {
|
|
23
|
+
/** Full `## Codegraph structure` markdown block, ending on its own paragraph. */
|
|
24
|
+
block: string;
|
|
25
|
+
total: number;
|
|
26
|
+
truncated: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Pure renderer: turn the codegraph `files --json` payload into a bounded
|
|
30
|
+
* `## Codegraph structure` block. Files are aggregated into a directory
|
|
31
|
+
* histogram (immediate parent dir; bare root files bucket separately),
|
|
32
|
+
* sorted by file count descending then name ascending, and capped at
|
|
33
|
+
* `maxDirs` rows / `maxRootFiles` root entries. Exported separately so the
|
|
34
|
+
* bounded-output contract is unit-testable without a filesystem.
|
|
35
|
+
*/
|
|
36
|
+
export declare function renderCodegraphStructureBlock(files: readonly CodegraphStructureFileEntry[], options?: CodegraphStructureRenderOptions): CodegraphStructureSummary;
|
|
37
|
+
/**
|
|
38
|
+
* Pre-dispatch codegraph preflight. Returns a `## Codegraph structure`
|
|
39
|
+
* block when the index is (or becomes) readable; otherwise an
|
|
40
|
+
* `{ available: false, note }` result. NEVER throws — the caller must be
|
|
41
|
+
* able to degrade gracefully on every failure path.
|
|
42
|
+
*
|
|
43
|
+
* Behavior matrix (acceptance criteria):
|
|
44
|
+
* - `.codegraph/` absent → init + index (best-effort), then read.
|
|
45
|
+
* - `.codegraph/` present with peaks-loop marker → skip init/index
|
|
46
|
+
* (fresh), read directly. No redundant re-index on every dispatch.
|
|
47
|
+
* - `.codegraph/` present WITHOUT marker (foreign schema) → fail-soft;
|
|
48
|
+
* never clobber a foreign store.
|
|
49
|
+
*
|
|
50
|
+
* The optional `runner` mirrors `CodegraphProcessRunner` and is the ONLY
|
|
51
|
+
* injected boundary (tests fake the upstream binary).
|
|
52
|
+
*/
|
|
53
|
+
export declare function buildCodegraphPreflightBlock(projectRoot: string, runner?: CodegraphProcessRunner): Promise<CodegraphPreflightResult>;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// src/services/codegraph/codegraph-preflight-service.ts
|
|
2
|
+
//
|
|
3
|
+
// Slice 2026-09-03-codegraph-preread (Option A) — pre-dispatch codegraph
|
|
4
|
+
// preflight for RD planning. peaks-code's RD dispatch path calls
|
|
5
|
+
// `buildCodegraphPreflightBlock` BEFORE composing the RD sub-agent prompt
|
|
6
|
+
// (src/cli/commands/dispatch-commands.ts) so the RD plans against the real
|
|
7
|
+
// module/file topology in the codegraph index, not LLM memory.
|
|
8
|
+
//
|
|
9
|
+
// The service does three things, all best-effort and fail-soft (it NEVER
|
|
10
|
+
// throws — every failure returns `{ available: false, note }` so the caller
|
|
11
|
+
// degrades to a "codegraph unavailable — proceeding on project-scan only"
|
|
12
|
+
// note and dispatch proceeds):
|
|
13
|
+
//
|
|
14
|
+
// 1. Ensure the schema exists: when `<projectRoot>/.codegraph/` is
|
|
15
|
+
// absent, run `codegraph init` + `codegraph index` (best-effort).
|
|
16
|
+
// 2. Skip-when-fresh: when `.codegraph/` already carries the
|
|
17
|
+
// peaks-loop marker, do NOT re-init / re-index on every dispatch
|
|
18
|
+
// (index is incremental; the sibling rid-2026-09-03-codegraph-autorefresh
|
|
19
|
+
// owns post-slice refresh). A foreign-schema `.codegraph/` is never
|
|
20
|
+
// clobbered — we fail-soft instead.
|
|
21
|
+
// 3. Read a BOUNDED project-structure summary from the index
|
|
22
|
+
// (`codegraph files --json`) and render it as a `## Codegraph
|
|
23
|
+
// structure` markdown block.
|
|
24
|
+
//
|
|
25
|
+
// Bounded-output contract: the rendered block never exceeds a safe prompt
|
|
26
|
+
// budget. The pure renderer `renderCodegraphStructureBlock` caps the
|
|
27
|
+
// directory histogram at CODEGRAPH_STRUCTURE_MAX_DIRS rows and the root-file
|
|
28
|
+
// listing at CODEGRAPH_STRUCTURE_MAX_ROOT_FILES entries; anything beyond
|
|
29
|
+
// those caps is summarized with a "… and N more" line and a `truncated: true`
|
|
30
|
+
// flag. Documented caps:
|
|
31
|
+
// - CODEGRAPH_STRUCTURE_MAX_DIRS = 40 directory rows
|
|
32
|
+
// - CODEGRAPH_STRUCTURE_MAX_ROOT_FILES = 12 bare root files
|
|
33
|
+
import { mkdirSync } from 'node:fs';
|
|
34
|
+
import { CODEGRAPH_DIR_NAME, createCodegraphInvocation, defaultCodegraphInitGuard, executeCodegraphInvocation, writeCodegraphMarker, } from './codegraph-service.js';
|
|
35
|
+
import { defaultCodegraphProcessRunner } from './codegraph-process-runner.js';
|
|
36
|
+
/** Cap for the directory histogram in the rendered structure block. */
|
|
37
|
+
export const CODEGRAPH_STRUCTURE_MAX_DIRS = 40;
|
|
38
|
+
/** Cap for bare root files listed in the rendered structure block. */
|
|
39
|
+
export const CODEGRAPH_STRUCTURE_MAX_ROOT_FILES = 12;
|
|
40
|
+
/** Strip a leading `./` (upstream codegraph paths may carry it). */
|
|
41
|
+
function normalizeCodegraphPath(path) {
|
|
42
|
+
return path.startsWith('./') ? path.slice(2) : path;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Pure renderer: turn the codegraph `files --json` payload into a bounded
|
|
46
|
+
* `## Codegraph structure` block. Files are aggregated into a directory
|
|
47
|
+
* histogram (immediate parent dir; bare root files bucket separately),
|
|
48
|
+
* sorted by file count descending then name ascending, and capped at
|
|
49
|
+
* `maxDirs` rows / `maxRootFiles` root entries. Exported separately so the
|
|
50
|
+
* bounded-output contract is unit-testable without a filesystem.
|
|
51
|
+
*/
|
|
52
|
+
export function renderCodegraphStructureBlock(files, options = {}) {
|
|
53
|
+
const maxDirs = options.maxDirs ?? CODEGRAPH_STRUCTURE_MAX_DIRS;
|
|
54
|
+
const maxRootFiles = options.maxRootFiles ?? CODEGRAPH_STRUCTURE_MAX_ROOT_FILES;
|
|
55
|
+
const dirCounts = new Map();
|
|
56
|
+
const rootFiles = [];
|
|
57
|
+
let total = 0;
|
|
58
|
+
for (const file of files) {
|
|
59
|
+
const raw = normalizeCodegraphPath(file.path).replace(/\\/g, '/');
|
|
60
|
+
if (raw.length === 0)
|
|
61
|
+
continue;
|
|
62
|
+
total += 1;
|
|
63
|
+
const slash = raw.lastIndexOf('/');
|
|
64
|
+
if (slash === -1) {
|
|
65
|
+
rootFiles.push(raw);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const dir = raw.slice(0, slash);
|
|
69
|
+
dirCounts.set(dir, (dirCounts.get(dir) ?? 0) + 1);
|
|
70
|
+
}
|
|
71
|
+
const sortedDirs = [...dirCounts.entries()]
|
|
72
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
73
|
+
const shownDirs = sortedDirs.slice(0, maxDirs);
|
|
74
|
+
const truncatedDirs = sortedDirs.length > maxDirs;
|
|
75
|
+
const sortedRoot = [...rootFiles].sort((a, b) => a.localeCompare(b));
|
|
76
|
+
const shownRoot = sortedRoot.slice(0, maxRootFiles);
|
|
77
|
+
const truncatedRoot = sortedRoot.length > maxRootFiles;
|
|
78
|
+
const lines = ['## Codegraph structure', ''];
|
|
79
|
+
lines.push(total === 0
|
|
80
|
+
? 'No files are indexed yet. Run `peaks codegraph index` before dispatching planning work for symbol-accurate structure.'
|
|
81
|
+
: `${total} file${total === 1 ? '' : 's'} indexed in the codegraph index:`);
|
|
82
|
+
for (const [dir, count] of shownDirs) {
|
|
83
|
+
lines.push(`- \`${dir}/\` — ${count} file${count === 1 ? '' : 's'}`);
|
|
84
|
+
}
|
|
85
|
+
if (truncatedDirs) {
|
|
86
|
+
lines.push(`- … and ${sortedDirs.length - maxDirs} more director${sortedDirs.length - maxDirs === 1 ? 'y' : 'ies'}`);
|
|
87
|
+
}
|
|
88
|
+
if (sortedRoot.length > 0) {
|
|
89
|
+
lines.push(`- (root) — ${shownRoot.map((f) => `\`${f}\``).join(', ')}${truncatedRoot ? ' …' : ''}`);
|
|
90
|
+
}
|
|
91
|
+
const block = lines.join('\n').replace(/\s+$/, '') + '\n\n';
|
|
92
|
+
return { block, total, truncated: truncatedDirs || truncatedRoot };
|
|
93
|
+
}
|
|
94
|
+
function errorMessage(error) {
|
|
95
|
+
return error instanceof Error ? error.message : String(error);
|
|
96
|
+
}
|
|
97
|
+
function firstMeaningfulLine(text) {
|
|
98
|
+
const trimmed = text.trim();
|
|
99
|
+
if (trimmed.length === 0)
|
|
100
|
+
return 'no upstream output';
|
|
101
|
+
const first = trimmed.split(/\r?\n/)[0];
|
|
102
|
+
return first !== undefined ? first.slice(0, 200) : 'no upstream output';
|
|
103
|
+
}
|
|
104
|
+
function parseFilesPayload(stdout) {
|
|
105
|
+
try {
|
|
106
|
+
const parsed = JSON.parse(stdout);
|
|
107
|
+
if (Array.isArray(parsed)) {
|
|
108
|
+
const entries = parsed
|
|
109
|
+
.filter((entry) => typeof entry === 'object' && entry !== null && 'path' in entry)
|
|
110
|
+
.map((entry) => ({ path: typeof entry.path === 'string' ? entry.path : '' }))
|
|
111
|
+
.filter((entry) => entry.path.length > 0);
|
|
112
|
+
return { ok: true, entries };
|
|
113
|
+
}
|
|
114
|
+
// JSON but not an array — not the shape we expect.
|
|
115
|
+
return { ok: false, entries: [] };
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Not JSON at all — e.g. the upstream text path ("No files indexed…").
|
|
119
|
+
return { ok: false, entries: [] };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async function readStructure(projectRoot, runner) {
|
|
123
|
+
let result;
|
|
124
|
+
try {
|
|
125
|
+
const invocation = createCodegraphInvocation({ subcommand: 'files', project: projectRoot, json: true });
|
|
126
|
+
result = await executeCodegraphInvocation(invocation, runner);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
return { available: false, note: `codegraph files unavailable: ${errorMessage(error)}` };
|
|
130
|
+
}
|
|
131
|
+
if (result.exitCode !== 0) {
|
|
132
|
+
return {
|
|
133
|
+
available: false,
|
|
134
|
+
note: `codegraph files failed (exit ${String(result.exitCode)}): ${firstMeaningfulLine(result.stderr || result.stdout)}`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const { ok, entries } = parseFilesPayload(result.stdout);
|
|
138
|
+
if (!ok) {
|
|
139
|
+
return {
|
|
140
|
+
available: false,
|
|
141
|
+
note: 'codegraph files returned no parseable structure — run `peaks codegraph index` before dispatch for symbol-accurate structure.',
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (entries.length === 0) {
|
|
145
|
+
return {
|
|
146
|
+
available: false,
|
|
147
|
+
note: 'codegraph index has no files — run `peaks codegraph index` before dispatch for symbol-accurate structure.',
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
const summary = renderCodegraphStructureBlock(entries);
|
|
151
|
+
return {
|
|
152
|
+
available: true,
|
|
153
|
+
block: summary.block,
|
|
154
|
+
fileCount: summary.total,
|
|
155
|
+
truncated: summary.truncated,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Pre-dispatch codegraph preflight. Returns a `## Codegraph structure`
|
|
160
|
+
* block when the index is (or becomes) readable; otherwise an
|
|
161
|
+
* `{ available: false, note }` result. NEVER throws — the caller must be
|
|
162
|
+
* able to degrade gracefully on every failure path.
|
|
163
|
+
*
|
|
164
|
+
* Behavior matrix (acceptance criteria):
|
|
165
|
+
* - `.codegraph/` absent → init + index (best-effort), then read.
|
|
166
|
+
* - `.codegraph/` present with peaks-loop marker → skip init/index
|
|
167
|
+
* (fresh), read directly. No redundant re-index on every dispatch.
|
|
168
|
+
* - `.codegraph/` present WITHOUT marker (foreign schema) → fail-soft;
|
|
169
|
+
* never clobber a foreign store.
|
|
170
|
+
*
|
|
171
|
+
* The optional `runner` mirrors `CodegraphProcessRunner` and is the ONLY
|
|
172
|
+
* injected boundary (tests fake the upstream binary).
|
|
173
|
+
*/
|
|
174
|
+
export async function buildCodegraphPreflightBlock(projectRoot, runner) {
|
|
175
|
+
const processRunner = runner ?? defaultCodegraphProcessRunner;
|
|
176
|
+
const guard = defaultCodegraphInitGuard(projectRoot);
|
|
177
|
+
if (guard.status === 'conflict-foreign-schema') {
|
|
178
|
+
return {
|
|
179
|
+
available: false,
|
|
180
|
+
note: `codegraph unavailable: ${CODEGRAPH_DIR_NAME}/ exists with a non-peaks-loop schema and was not touched. Move or rename the foreign directory, then re-run \`peaks codegraph init\` to enable pre-dispatch structure reads.`,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
if (guard.status === 'fresh') {
|
|
184
|
+
// 1. init (best-effort). Upstream creates the `.codegraph/` dir; we
|
|
185
|
+
// stamp the peaks-loop marker afterwards so the NEXT dispatch hits
|
|
186
|
+
// the noop (skip-when-fresh) branch.
|
|
187
|
+
try {
|
|
188
|
+
const initResult = await executeCodegraphInvocation(createCodegraphInvocation({ subcommand: 'init', project: projectRoot }), processRunner);
|
|
189
|
+
if (initResult.exitCode !== 0) {
|
|
190
|
+
return {
|
|
191
|
+
available: false,
|
|
192
|
+
note: `codegraph init failed (exit ${String(initResult.exitCode)}): ${firstMeaningfulLine(initResult.stderr || initResult.stdout)}`,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
// Upstream init creates `.codegraph/`; mkdir is a no-op in the real
|
|
197
|
+
// path and lets the marker write succeed even when the runner is
|
|
198
|
+
// faked (tests). Best-effort: failure must not undo the init.
|
|
199
|
+
mkdirSync(guard.codegraphDir, { recursive: true });
|
|
200
|
+
writeCodegraphMarker(guard.codegraphDir);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// Best-effort: a marker-write failure must not undo the init.
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
return { available: false, note: `codegraph init unavailable: ${errorMessage(error)}` };
|
|
208
|
+
}
|
|
209
|
+
// 2. index (best-effort).
|
|
210
|
+
try {
|
|
211
|
+
const indexResult = await executeCodegraphInvocation(createCodegraphInvocation({ subcommand: 'index', project: projectRoot, quiet: true }), processRunner);
|
|
212
|
+
if (indexResult.exitCode !== 0) {
|
|
213
|
+
return {
|
|
214
|
+
available: false,
|
|
215
|
+
note: `codegraph index failed (exit ${String(indexResult.exitCode)}): ${firstMeaningfulLine(indexResult.stderr || indexResult.stdout)}`,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
return { available: false, note: `codegraph index unavailable: ${errorMessage(error)}` };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// guard.status === 'noop-already-peaks-loop' (or we just initialized):
|
|
224
|
+
// the schema exists — do NOT re-index, go straight to the bounded read.
|
|
225
|
+
return readStructure(projectRoot, processRunner);
|
|
226
|
+
}
|
|
@@ -41,17 +41,13 @@ export type CodegraphProcessRunner = (invocation: CodegraphInvocation) => Promis
|
|
|
41
41
|
export declare function createCodegraphInvocation(options: CodegraphInvocationOptions): CodegraphInvocation;
|
|
42
42
|
export declare function executeCodegraphInvocation(invocation: CodegraphInvocation, runner?: CodegraphProcessRunner): Promise<CodegraphExecutionResult>;
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
44
|
+
* Single managed codegraph data directory name, relative to the
|
|
45
|
+
* project root. peaks-loop always reads/writes
|
|
46
|
+
* `<projectRoot>/.codegraph/` and spawns the upstream binary with
|
|
47
|
+
* `cwd = <projectRoot>` so its default discovery lands on the same
|
|
48
|
+
* directory.
|
|
47
49
|
*/
|
|
48
50
|
export declare const CODEGRAPH_DIR_NAME = ".codegraph";
|
|
49
|
-
/**
|
|
50
|
-
* Slice rid-CG-003 — preferred managed location. Going forward
|
|
51
|
-
* peaks-loop writes here; legacy root `.codegraph/` is only used
|
|
52
|
-
* when it pre-exists AND `.peaks/.codegraph/` does not.
|
|
53
|
-
*/
|
|
54
|
-
export declare const PREFERRED_CODEGRAPH_DIR = ".peaks/.codegraph";
|
|
55
51
|
/**
|
|
56
52
|
* Marker file peaks-loop writes inside the resolved codegraph dir
|
|
57
53
|
* after a successful upstream init. Its presence distinguishes
|
|
@@ -61,37 +57,18 @@ export declare const PREFERRED_CODEGRAPH_DIR = ".peaks/.codegraph";
|
|
|
61
57
|
export declare const CODEGRAPH_MARKER_NAME = ".peaks-loop-marker";
|
|
62
58
|
export declare const CODEGRAPH_INIT_CONFLICT_EXIT_CODE = 73;
|
|
63
59
|
export type ResolvedCodegraphLocation = {
|
|
64
|
-
readonly source: '
|
|
65
|
-
|
|
66
|
-
readonly codegraphDir: string;
|
|
67
|
-
} | {
|
|
68
|
-
readonly source: 'legacy';
|
|
69
|
-
readonly cwd: string;
|
|
70
|
-
readonly codegraphDir: string;
|
|
71
|
-
} | {
|
|
72
|
-
readonly source: 'fresh-preferred';
|
|
60
|
+
readonly source: 'root';
|
|
61
|
+
/** Absolute path the upstream binary should be spawned with. */
|
|
73
62
|
readonly cwd: string;
|
|
63
|
+
/** Absolute path to the data directory codegraph reads/writes. */
|
|
74
64
|
readonly codegraphDir: string;
|
|
75
65
|
};
|
|
76
66
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* 1. `<projectRoot>/.peaks/.codegraph/` exists → use it
|
|
83
|
-
* (cwd = `<projectRoot>/.peaks`, codegraphDir = preferred path)
|
|
84
|
-
* 2. `<projectRoot>/.codegraph/` exists → fall back to legacy
|
|
85
|
-
* (cwd = `<projectRoot>`, codegraphDir = legacy path)
|
|
86
|
-
* 3. neither exists → default to the preferred path so the next
|
|
87
|
-
* `peaks codegraph init` lands in `.peaks/.codegraph/`
|
|
88
|
-
* (cwd = `<projectRoot>/.peaks`, codegraphDir = preferred path,
|
|
89
|
-
* `source: 'fresh-preferred'`)
|
|
90
|
-
*
|
|
91
|
-
* The cwd is the directory the upstream binary treats as "project
|
|
92
|
-
* root" — the binary's default codegraph discovery reads
|
|
93
|
-
* `<cwd>/.codegraph/`, so we always set cwd to the PARENT of the
|
|
94
|
-
* resolved codegraph dir. Pure fs check; no IO beyond `existsSync`.
|
|
67
|
+
* Root-only resolver: peaks-loop always manages
|
|
68
|
+
* `<projectRoot>/.codegraph/`. Returns the cwd the upstream codegraph
|
|
69
|
+
* binary should be spawned with (= project root, so the binary's
|
|
70
|
+
* default discovery reads `<projectRoot>/.codegraph/`) and the
|
|
71
|
+
* absolute data-dir path. Pure path computation; no fs IO.
|
|
95
72
|
*/
|
|
96
73
|
export declare function resolveCodegraphProjectRoot(projectRoot: string): ResolvedCodegraphLocation;
|
|
97
74
|
export type CodegraphInitGuardResult = {
|
|
@@ -112,12 +89,9 @@ export declare class CodegraphInitConflictError extends Error {
|
|
|
112
89
|
}
|
|
113
90
|
export type CodegraphInitGuard = (projectRoot: string) => CodegraphInitGuardResult;
|
|
114
91
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* when the preferred path is absent. The 'fresh' case returns the
|
|
119
|
-
* preferred path so the next `peaks codegraph init` lands inside
|
|
120
|
-
* `.peaks/` instead of polluting the project root.
|
|
92
|
+
* Root-only init guard: probes `<projectRoot>/.codegraph/`. 'fresh'
|
|
93
|
+
* is returned when the directory does not exist yet, so the next
|
|
94
|
+
* `peaks codegraph init` creates the root `.codegraph/` directory.
|
|
121
95
|
*/
|
|
122
96
|
export declare function defaultCodegraphInitGuard(projectRoot: string): CodegraphInitGuardResult;
|
|
123
97
|
/**
|
|
@@ -143,10 +143,8 @@ function buildCommandArgs(options, projectRoot) {
|
|
|
143
143
|
export function createCodegraphInvocation(options) {
|
|
144
144
|
assertSupportedSubcommand(options.subcommand);
|
|
145
145
|
const projectRoot = resolveProjectRoot(options.project);
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
// discovery lands on `.peaks/.codegraph/` (preferred) or
|
|
149
|
-
// `.codegraph/` (legacy fallback).
|
|
146
|
+
// Spawn the upstream binary with the project root as cwd so its
|
|
147
|
+
// default `.codegraph/` discovery reads `<projectRoot>/.codegraph/`.
|
|
150
148
|
const location = resolveCodegraphProjectRoot(projectRoot);
|
|
151
149
|
assertSupportedOptions(options);
|
|
152
150
|
assertRequiredOptions(options);
|
|
@@ -165,21 +163,16 @@ export async function executeCodegraphInvocation(invocation, runner = defaultCod
|
|
|
165
163
|
return runner(invocation);
|
|
166
164
|
}
|
|
167
165
|
/* ──────────────────────────────────────────────────────────────────────
|
|
168
|
-
*
|
|
169
|
-
* root `.codegraph/` fallback (spike follow-up #1).
|
|
166
|
+
* codegraph data-directory resolution — root `.codegraph/` only.
|
|
170
167
|
* ────────────────────────────────────────────────────────────────────── */
|
|
171
168
|
/**
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
169
|
+
* Single managed codegraph data directory name, relative to the
|
|
170
|
+
* project root. peaks-loop always reads/writes
|
|
171
|
+
* `<projectRoot>/.codegraph/` and spawns the upstream binary with
|
|
172
|
+
* `cwd = <projectRoot>` so its default discovery lands on the same
|
|
173
|
+
* directory.
|
|
175
174
|
*/
|
|
176
175
|
export const CODEGRAPH_DIR_NAME = '.codegraph';
|
|
177
|
-
/**
|
|
178
|
-
* Slice rid-CG-003 — preferred managed location. Going forward
|
|
179
|
-
* peaks-loop writes here; legacy root `.codegraph/` is only used
|
|
180
|
-
* when it pre-exists AND `.peaks/.codegraph/` does not.
|
|
181
|
-
*/
|
|
182
|
-
export const PREFERRED_CODEGRAPH_DIR = '.peaks/.codegraph';
|
|
183
176
|
/**
|
|
184
177
|
* Marker file peaks-loop writes inside the resolved codegraph dir
|
|
185
178
|
* after a successful upstream init. Its presence distinguishes
|
|
@@ -189,46 +182,17 @@ export const PREFERRED_CODEGRAPH_DIR = '.peaks/.codegraph';
|
|
|
189
182
|
export const CODEGRAPH_MARKER_NAME = '.peaks-loop-marker';
|
|
190
183
|
export const CODEGRAPH_INIT_CONFLICT_EXIT_CODE = 73;
|
|
191
184
|
/**
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
* 1. `<projectRoot>/.peaks/.codegraph/` exists → use it
|
|
198
|
-
* (cwd = `<projectRoot>/.peaks`, codegraphDir = preferred path)
|
|
199
|
-
* 2. `<projectRoot>/.codegraph/` exists → fall back to legacy
|
|
200
|
-
* (cwd = `<projectRoot>`, codegraphDir = legacy path)
|
|
201
|
-
* 3. neither exists → default to the preferred path so the next
|
|
202
|
-
* `peaks codegraph init` lands in `.peaks/.codegraph/`
|
|
203
|
-
* (cwd = `<projectRoot>/.peaks`, codegraphDir = preferred path,
|
|
204
|
-
* `source: 'fresh-preferred'`)
|
|
205
|
-
*
|
|
206
|
-
* The cwd is the directory the upstream binary treats as "project
|
|
207
|
-
* root" — the binary's default codegraph discovery reads
|
|
208
|
-
* `<cwd>/.codegraph/`, so we always set cwd to the PARENT of the
|
|
209
|
-
* resolved codegraph dir. Pure fs check; no IO beyond `existsSync`.
|
|
185
|
+
* Root-only resolver: peaks-loop always manages
|
|
186
|
+
* `<projectRoot>/.codegraph/`. Returns the cwd the upstream codegraph
|
|
187
|
+
* binary should be spawned with (= project root, so the binary's
|
|
188
|
+
* default discovery reads `<projectRoot>/.codegraph/`) and the
|
|
189
|
+
* absolute data-dir path. Pure path computation; no fs IO.
|
|
210
190
|
*/
|
|
211
191
|
export function resolveCodegraphProjectRoot(projectRoot) {
|
|
212
|
-
const preferredDir = join(projectRoot, PREFERRED_CODEGRAPH_DIR);
|
|
213
|
-
const legacyDir = join(projectRoot, CODEGRAPH_DIR_NAME);
|
|
214
|
-
if (existsSync(preferredDir)) {
|
|
215
|
-
return {
|
|
216
|
-
source: 'preferred',
|
|
217
|
-
cwd: join(projectRoot, '.peaks'),
|
|
218
|
-
codegraphDir: preferredDir
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
if (existsSync(legacyDir)) {
|
|
222
|
-
return {
|
|
223
|
-
source: 'legacy',
|
|
224
|
-
cwd: projectRoot,
|
|
225
|
-
codegraphDir: legacyDir
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
192
|
return {
|
|
229
|
-
source: '
|
|
230
|
-
cwd:
|
|
231
|
-
codegraphDir:
|
|
193
|
+
source: 'root',
|
|
194
|
+
cwd: projectRoot,
|
|
195
|
+
codegraphDir: join(projectRoot, CODEGRAPH_DIR_NAME)
|
|
232
196
|
};
|
|
233
197
|
}
|
|
234
198
|
export class CodegraphInitConflictError extends Error {
|
|
@@ -264,23 +228,16 @@ function inspectCandidateCodegraphDir(codegraphDir) {
|
|
|
264
228
|
return { status: 'conflict-foreign-schema', codegraphDir };
|
|
265
229
|
}
|
|
266
230
|
/**
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
* when the preferred path is absent. The 'fresh' case returns the
|
|
271
|
-
* preferred path so the next `peaks codegraph init` lands inside
|
|
272
|
-
* `.peaks/` instead of polluting the project root.
|
|
231
|
+
* Root-only init guard: probes `<projectRoot>/.codegraph/`. 'fresh'
|
|
232
|
+
* is returned when the directory does not exist yet, so the next
|
|
233
|
+
* `peaks codegraph init` creates the root `.codegraph/` directory.
|
|
273
234
|
*/
|
|
274
235
|
export function defaultCodegraphInitGuard(projectRoot) {
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
return inspectCandidateCodegraphDir(preferredDir);
|
|
279
|
-
}
|
|
280
|
-
if (existsSync(legacyDir)) {
|
|
281
|
-
return inspectCandidateCodegraphDir(legacyDir);
|
|
236
|
+
const codegraphDir = join(projectRoot, CODEGRAPH_DIR_NAME);
|
|
237
|
+
if (!existsSync(codegraphDir)) {
|
|
238
|
+
return { status: 'fresh', codegraphDir };
|
|
282
239
|
}
|
|
283
|
-
return
|
|
240
|
+
return inspectCandidateCodegraphDir(codegraphDir);
|
|
284
241
|
}
|
|
285
242
|
/**
|
|
286
243
|
* Pure-fs helper that stamps the peaks-loop marker AFTER a
|
|
@@ -28,6 +28,20 @@ export interface DispatchPromptInput {
|
|
|
28
28
|
* a byte-counted estimate.
|
|
29
29
|
*/
|
|
30
30
|
contextProbe?: ContextPercentProbe | null;
|
|
31
|
+
/**
|
|
32
|
+
* Slice 2026-09-03-codegraph-preread (Option A): pre-composed
|
|
33
|
+
* `## Codegraph structure` markdown block, read from the codegraph
|
|
34
|
+
* index BEFORE the RD sub-agent's task body is composed so the RD
|
|
35
|
+
* plans against real module/file topology, not LLM memory.
|
|
36
|
+
*
|
|
37
|
+
* - `undefined` → no codegraph block (legacy callers unchanged).
|
|
38
|
+
* - `null` → codegraph was attempted but is unavailable; the composer
|
|
39
|
+
* renders a fixed "codegraph unavailable — proceeding on project-scan
|
|
40
|
+
* only" note (fail-soft; never hard-blocks a dispatch).
|
|
41
|
+
* - `string` → the block, rendered verbatim between the context window
|
|
42
|
+
* and the memory/task content.
|
|
43
|
+
*/
|
|
44
|
+
codegraphBlock?: string | null;
|
|
31
45
|
}
|
|
32
46
|
/**
|
|
33
47
|
* Slice 2026-07-29-worktree-l1: Layer 1 of the 3-layer worktree governance
|
|
@@ -88,6 +102,10 @@ export declare const LIFECYCLE_RULES = "## Sub-agent lifecycle rules (locked 202
|
|
|
88
102
|
* `${formatTestToolDetection()}\n\n${taskBody}`. Today's pre-change behavior
|
|
89
103
|
* produced the same string from `src/cli/commands/dispatch-commands.ts:220`,
|
|
90
104
|
* so the unavailable branch MUST return `taskBody` (NOT a `# title\n\n` wrap).
|
|
105
|
+
* The contract holds for callers that do not pass `codegraphBlock` (all
|
|
106
|
+
* non-RD roles). Slice 2026-09-03-codegraph-preread deliberately inserts a
|
|
107
|
+
* codegraph structure block (or its fail-soft unavailable note) for RD
|
|
108
|
+
* dispatches between the context window and the memory/task content.
|
|
91
109
|
*
|
|
92
110
|
* Available branch prepends the memory block before the `## Task` heading so
|
|
93
111
|
* `## Project memory …` always sits above the task brief (never pushed below
|
|
@@ -99,3 +117,11 @@ export declare const LIFECYCLE_RULES = "## Sub-agent lifecycle rules (locked 202
|
|
|
99
117
|
* refusal is in scope before any task-specific prose arrives.
|
|
100
118
|
*/
|
|
101
119
|
export declare function buildDispatchSystemPrompt(input: DispatchPromptInput): string;
|
|
120
|
+
/**
|
|
121
|
+
* Slice 2026-09-03-codegraph-preread: fixed degradation string emitted
|
|
122
|
+
* when the RD dispatch preflight requested a codegraph structure read but
|
|
123
|
+
* the index could not be resolved (absent + init failure, foreign schema,
|
|
124
|
+
* unparseable output). Kept as a constant so the unavailable branch is
|
|
125
|
+
* byte-stable and trivially testable.
|
|
126
|
+
*/
|
|
127
|
+
export declare const CODEGRAPH_UNAVAILABLE_BLOCK = "## Codegraph structure\n\ncodegraph unavailable \u2014 proceeding on project-scan only.\n";
|
|
@@ -83,6 +83,10 @@ export const LIFECYCLE_RULES = `## Sub-agent lifecycle rules (locked 2026-08-01)
|
|
|
83
83
|
* `${formatTestToolDetection()}\n\n${taskBody}`. Today's pre-change behavior
|
|
84
84
|
* produced the same string from `src/cli/commands/dispatch-commands.ts:220`,
|
|
85
85
|
* so the unavailable branch MUST return `taskBody` (NOT a `# title\n\n` wrap).
|
|
86
|
+
* The contract holds for callers that do not pass `codegraphBlock` (all
|
|
87
|
+
* non-RD roles). Slice 2026-09-03-codegraph-preread deliberately inserts a
|
|
88
|
+
* codegraph structure block (or its fail-soft unavailable note) for RD
|
|
89
|
+
* dispatches between the context window and the memory/task content.
|
|
86
90
|
*
|
|
87
91
|
* Available branch prepends the memory block before the `## Task` heading so
|
|
88
92
|
* `## Project memory …` always sits above the task brief (never pushed below
|
|
@@ -94,12 +98,40 @@ export const LIFECYCLE_RULES = `## Sub-agent lifecycle rules (locked 2026-08-01)
|
|
|
94
98
|
* refusal is in scope before any task-specific prose arrives.
|
|
95
99
|
*/
|
|
96
100
|
export function buildDispatchSystemPrompt(input) {
|
|
97
|
-
const { taskBody, memoryBlock, contextProbe } = input;
|
|
101
|
+
const { taskBody, memoryBlock, contextProbe, codegraphBlock } = input;
|
|
98
102
|
const contextBlock = renderContextBlock(contextProbe ?? null);
|
|
103
|
+
const codegraphText = renderCodegraphBlock(codegraphBlock);
|
|
99
104
|
if (memoryBlock.available === true && typeof memoryBlock.block === 'string') {
|
|
100
|
-
return `${L1_WORKTREE_GOVERNANCE_BLOCK}\n${LIFECYCLE_RULES}\n${contextBlock}${memoryBlock.block}\n## Task\n${taskBody}`;
|
|
105
|
+
return `${L1_WORKTREE_GOVERNANCE_BLOCK}\n${LIFECYCLE_RULES}\n${contextBlock}${codegraphText}${memoryBlock.block}\n## Task\n${taskBody}`;
|
|
101
106
|
}
|
|
102
|
-
return `${L1_WORKTREE_GOVERNANCE_BLOCK}\n${LIFECYCLE_RULES}\n${contextBlock}${taskBody}`;
|
|
107
|
+
return `${L1_WORKTREE_GOVERNANCE_BLOCK}\n${LIFECYCLE_RULES}\n${contextBlock}${codegraphText}${taskBody}`;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Slice 2026-09-03-codegraph-preread: fixed degradation string emitted
|
|
111
|
+
* when the RD dispatch preflight requested a codegraph structure read but
|
|
112
|
+
* the index could not be resolved (absent + init failure, foreign schema,
|
|
113
|
+
* unparseable output). Kept as a constant so the unavailable branch is
|
|
114
|
+
* byte-stable and trivially testable.
|
|
115
|
+
*/
|
|
116
|
+
export const CODEGRAPH_UNAVAILABLE_BLOCK = '## Codegraph structure\n\ncodegraph unavailable — proceeding on project-scan only.\n';
|
|
117
|
+
/**
|
|
118
|
+
* Render the codegraph insertion for a dispatch prompt.
|
|
119
|
+
*
|
|
120
|
+
* - `undefined` → empty (the composer is byte-identical to the legacy
|
|
121
|
+
* shape for callers that did not opt into a codegraph pre-read).
|
|
122
|
+
* - `null` → the fixed CODEGRAPH_UNAVAILABLE_BLOCK note.
|
|
123
|
+
* - `string` → the pre-composed block from the codegraph preflight
|
|
124
|
+
* service, verbatim.
|
|
125
|
+
*
|
|
126
|
+
* Every non-empty variant is normalized to end on its own paragraph
|
|
127
|
+
* (`\n\n`) so the following block (project memory or task body) starts
|
|
128
|
+
* cleanly regardless of the caller's trailing newline habits.
|
|
129
|
+
*/
|
|
130
|
+
function renderCodegraphBlock(codegraphBlock) {
|
|
131
|
+
if (codegraphBlock === undefined)
|
|
132
|
+
return '';
|
|
133
|
+
const text = codegraphBlock === null ? CODEGRAPH_UNAVAILABLE_BLOCK : codegraphBlock;
|
|
134
|
+
return `${text.replace(/\s+$/, '')}\n\n`;
|
|
103
135
|
}
|
|
104
136
|
/**
|
|
105
137
|
* Slice 2026-07-29-context-evaluation-accuracy: emit a
|
|
@@ -6,12 +6,11 @@
|
|
|
6
6
|
* Fails when the version drifts, when the binary is missing, or
|
|
7
7
|
* when the package is not resolvable at all.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
* codegraph
|
|
11
|
-
* or root `.codegraph/` (legacy) — via the injected `managedPathProbe`.
|
|
9
|
+
* The check also reports the managed codegraph data directory in use —
|
|
10
|
+
* always the root `.codegraph/` — via the injected `managedPathProbe`.
|
|
12
11
|
* The CG-007 yarn-pnp fallback is preserved as the package-resolution
|
|
13
|
-
* default; the managed-path probe defaults to the same
|
|
14
|
-
*
|
|
12
|
+
* default; the managed-path probe defaults to the same root-only
|
|
13
|
+
* resolver and is independently injectable for tests.
|
|
15
14
|
*
|
|
16
15
|
* The probe is injected so tests do not depend on the real
|
|
17
16
|
* `node_modules` resolution; the default probe uses
|
|
@@ -6,12 +6,11 @@
|
|
|
6
6
|
* Fails when the version drifts, when the binary is missing, or
|
|
7
7
|
* when the package is not resolvable at all.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
* codegraph
|
|
11
|
-
* or root `.codegraph/` (legacy) — via the injected `managedPathProbe`.
|
|
9
|
+
* The check also reports the managed codegraph data directory in use —
|
|
10
|
+
* always the root `.codegraph/` — via the injected `managedPathProbe`.
|
|
12
11
|
* The CG-007 yarn-pnp fallback is preserved as the package-resolution
|
|
13
|
-
* default; the managed-path probe defaults to the same
|
|
14
|
-
*
|
|
12
|
+
* default; the managed-path probe defaults to the same root-only
|
|
13
|
+
* resolver and is independently injectable for tests.
|
|
15
14
|
*
|
|
16
15
|
* The probe is injected so tests do not depend on the real
|
|
17
16
|
* `node_modules` resolution; the default probe uses
|
|
@@ -100,45 +99,30 @@ function defaultCodegraphProbe() {
|
|
|
100
99
|
version,
|
|
101
100
|
binaryPath,
|
|
102
101
|
binaryExists,
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
// project (cwd has neither `.peaks/.codegraph/` nor
|
|
107
|
-
// `.codegraph/`) we still report `fresh-preferred` so the check
|
|
108
|
-
// message names the canonical future location.
|
|
102
|
+
// Resolve the managed codegraph directory relative to the cwd the
|
|
103
|
+
// doctor itself was invoked from. Root-only: always names
|
|
104
|
+
// `<cwd>/.codegraph/`.
|
|
109
105
|
managedPath: detectManagedCodegraphPath(process.cwd())
|
|
110
106
|
};
|
|
111
107
|
return result;
|
|
112
108
|
}
|
|
113
109
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* the cwd does not look like a peaks project root).
|
|
110
|
+
* Pure wrapper over `resolveCodegraphProjectRoot` that returns a
|
|
111
|
+
* probe-shaped managed-path payload for the root `.codegraph/`.
|
|
117
112
|
*/
|
|
118
113
|
function detectManagedCodegraphPath(cwd) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
catch {
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
114
|
+
const location = resolveCodegraphProjectRoot(cwd);
|
|
115
|
+
return {
|
|
116
|
+
source: location.source,
|
|
117
|
+
codegraphDir: location.codegraphDir,
|
|
118
|
+
cwd: location.cwd
|
|
119
|
+
};
|
|
130
120
|
}
|
|
131
121
|
function renderManagedPathSuffix(managedPath) {
|
|
132
122
|
if (!managedPath) {
|
|
133
123
|
return '';
|
|
134
124
|
}
|
|
135
|
-
|
|
136
|
-
return `; managed path: ${managedPath.codegraphDir} (preferred .peaks/.codegraph/)`;
|
|
137
|
-
}
|
|
138
|
-
if (managedPath.source === 'legacy') {
|
|
139
|
-
return `; managed path: ${managedPath.codegraphDir} (legacy root .codegraph/ — consider moving to .peaks/.codegraph/)`;
|
|
140
|
-
}
|
|
141
|
-
return `; managed path: ${managedPath.codegraphDir} (fresh-preferred — no codegraph dir yet; next init will create it)`;
|
|
125
|
+
return `; managed path: ${managedPath.codegraphDir}`;
|
|
142
126
|
}
|
|
143
127
|
function runCheck(probe, managedPathProbe) {
|
|
144
128
|
try {
|
|
@@ -57,7 +57,7 @@ export type DoctorReport = {
|
|
|
57
57
|
};
|
|
58
58
|
};
|
|
59
59
|
export type CodegraphManagedPathInfo = {
|
|
60
|
-
source: '
|
|
60
|
+
source: 'root';
|
|
61
61
|
codegraphDir: string;
|
|
62
62
|
cwd: string;
|
|
63
63
|
};
|
|
@@ -67,14 +67,11 @@ export type CodegraphCapabilityProbe = {
|
|
|
67
67
|
binaryPath: string;
|
|
68
68
|
binaryExists: boolean;
|
|
69
69
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* `source: 'preferred'` — `.peaks/.codegraph/` exists
|
|
76
|
-
* `source: 'legacy'` — only `.codegraph/` exists
|
|
77
|
-
* `source: 'fresh-preferred'` — neither exists; defaults to preferred
|
|
70
|
+
* Root-only managed-path resolution result. The check resolves the
|
|
71
|
+
* single codegraph data directory, `<cwd>/.codegraph/`, relative to
|
|
72
|
+
* the cwd the doctor was invoked from; null only when resolution is
|
|
73
|
+
* unavailable (e.g. the operator invoked `peaks doctor` outside a
|
|
74
|
+
* resolvable directory).
|
|
78
75
|
*/
|
|
79
76
|
managedPath: CodegraphManagedPathInfo | null;
|
|
80
77
|
};
|
|
@@ -207,12 +204,11 @@ export type DoctorOptions = {
|
|
|
207
204
|
skillsBaseDir?: string;
|
|
208
205
|
codegraphProbe?: () => CodegraphCapabilityProbe;
|
|
209
206
|
/**
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
* outcomes without monkey-patching `process.cwd()`.
|
|
207
|
+
* Optional override for the managed-codegraph path detection inside
|
|
208
|
+
* the `capability:codegraph` check. When omitted, the check uses
|
|
209
|
+
* the default resolver (`resolveCodegraphProjectRoot(process.cwd())`).
|
|
210
|
+
* Tests inject a custom probe without monkey-patching
|
|
211
|
+
* `process.cwd()`.
|
|
216
212
|
*/
|
|
217
213
|
codegraphManagedPathProbe?: () => CodegraphManagedPathInfo | null;
|
|
218
214
|
skillPresenceProbe?: () => DoctorSkillPresence | null;
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import type { IdeAdapter } from '../ide-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the currently-active Claude Code model id from a runtime env map.
|
|
4
|
+
* Returns the first non-empty `CLAUDE_CODE_MODEL_ENV_VARS` value (trimmed), or
|
|
5
|
+
* `undefined` when none is present. Pure + exported for tests; the caller falls
|
|
6
|
+
* back to the transcript `message.model` when this returns undefined.
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveClaudeModelFromEnv(env: NodeJS.ProcessEnv | undefined): string | undefined;
|
|
2
9
|
/**
|
|
3
10
|
* Model-aware context-window size in tokens.
|
|
4
11
|
*
|
|
@@ -103,6 +103,50 @@ const TRANSCRIPT_SCAN_CHUNK_BYTES = 64 * 1024;
|
|
|
103
103
|
* variant is 1M-context.
|
|
104
104
|
*/
|
|
105
105
|
const ONE_MILLION_CONTEXT_MODELS = ['claude-opus-4', 'claude-sonnet-4'];
|
|
106
|
+
/**
|
|
107
|
+
* Claude Code runtime env vars that may carry the currently-active model id,
|
|
108
|
+
* in documented precedence order (first non-empty value wins):
|
|
109
|
+
* 1. ANTHROPIC_MODEL — explicit per-run model override
|
|
110
|
+
* 2. ANTHROPIC_DEFAULT_OPUS_MODEL — default Opus fallback
|
|
111
|
+
* 3. ANTHROPIC_DEFAULT_SONNET_MODEL — default Sonnet fallback
|
|
112
|
+
* 4. ANTHROPIC_DEFAULT_HAIKU_MODEL — default Haiku fallback
|
|
113
|
+
* 5. ANTHROPIC_DEFAULT_FABLE_MODEL — default Fable fallback
|
|
114
|
+
* 6. CLAUDE_CODE_SUBAGENT_MODEL — sub-agent model (used when the others
|
|
115
|
+
* are absent, e.g. a sub-agent-only env)
|
|
116
|
+
*
|
|
117
|
+
* Why env-first: the transcript's `message.model` often drops Claude Code's
|
|
118
|
+
* `[1M]` / `[200K]` suffix marker (observed: `deepseek-v4-flash`), while the
|
|
119
|
+
* runtime env vars above carry it (`deepseek-v4-flash[1M]`). Reading them
|
|
120
|
+
* first lets the window resolver see the true context window. This family is
|
|
121
|
+
* vendor-specific, so it lives ONLY in the claude-code adapter.
|
|
122
|
+
*/
|
|
123
|
+
const CLAUDE_CODE_MODEL_ENV_VARS = [
|
|
124
|
+
'ANTHROPIC_MODEL',
|
|
125
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
126
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
127
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
128
|
+
'ANTHROPIC_DEFAULT_FABLE_MODEL',
|
|
129
|
+
'CLAUDE_CODE_SUBAGENT_MODEL',
|
|
130
|
+
];
|
|
131
|
+
/**
|
|
132
|
+
* Resolve the currently-active Claude Code model id from a runtime env map.
|
|
133
|
+
* Returns the first non-empty `CLAUDE_CODE_MODEL_ENV_VARS` value (trimmed), or
|
|
134
|
+
* `undefined` when none is present. Pure + exported for tests; the caller falls
|
|
135
|
+
* back to the transcript `message.model` when this returns undefined.
|
|
136
|
+
*/
|
|
137
|
+
export function resolveClaudeModelFromEnv(env) {
|
|
138
|
+
if (!env)
|
|
139
|
+
return undefined;
|
|
140
|
+
for (const name of CLAUDE_CODE_MODEL_ENV_VARS) {
|
|
141
|
+
const value = env[name];
|
|
142
|
+
if (typeof value === 'string') {
|
|
143
|
+
const trimmed = value.trim();
|
|
144
|
+
if (trimmed.length > 0)
|
|
145
|
+
return trimmed;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
106
150
|
/**
|
|
107
151
|
* Model-aware context-window size in tokens.
|
|
108
152
|
*
|
|
@@ -251,8 +295,12 @@ function resolveContextWindowTokens(model, contextTokens) {
|
|
|
251
295
|
* `bytes / 256KB` (which over-fired because the transcript grows unboundedly).
|
|
252
296
|
* Tagged `'transcript-estimate'` (v2.14.0) so callers know it is a real
|
|
253
297
|
* signal, NOT a hard gate.
|
|
298
|
+
*
|
|
299
|
+
* Window model resolution is env-first: when `envModel` is present, its id
|
|
300
|
+
* (which Claude Code stamps with the `[1M]` / `[200K]` suffix) drives the
|
|
301
|
+
* window; otherwise the transcript `message.model` is used.
|
|
254
302
|
*/
|
|
255
|
-
function readClaudeTranscriptEstimate(outerSessionId) {
|
|
303
|
+
function readClaudeTranscriptEstimate(outerSessionId, envModel) {
|
|
256
304
|
const projectsDir = join(homedir(), '.claude', 'projects');
|
|
257
305
|
const path = findTranscriptJsonl(projectsDir, outerSessionId);
|
|
258
306
|
if (path === null)
|
|
@@ -260,7 +308,8 @@ function readClaudeTranscriptEstimate(outerSessionId) {
|
|
|
260
308
|
const latest = findLatestTranscriptUsage(path);
|
|
261
309
|
if (latest === null)
|
|
262
310
|
return null;
|
|
263
|
-
const
|
|
311
|
+
const model = envModel !== undefined && envModel.trim().length > 0 ? envModel : latest.model;
|
|
312
|
+
const contextWindowTokens = resolveContextWindowTokens(model, latest.contextTokens);
|
|
264
313
|
const ratio = Math.min(1, latest.contextTokens / contextWindowTokens);
|
|
265
314
|
return { ratio, contextTokens: latest.contextTokens, contextWindowTokens };
|
|
266
315
|
}
|
|
@@ -274,7 +323,10 @@ function readClaudeTranscriptEstimate(outerSessionId) {
|
|
|
274
323
|
* `~/.claude/projects/<hash>/...` using the OUTER session id (Claude
|
|
275
324
|
* names its transcript by the outer session UUID, not the peaks sid),
|
|
276
325
|
* and estimates `contextTokens / contextWindowTokens` from the LATEST
|
|
277
|
-
* `message.usage` entry (token-based + model-aware).
|
|
326
|
+
* `message.usage` entry (token-based + model-aware). The window model
|
|
327
|
+
* resolves env-first via `resolveClaudeModelFromEnv(input.env)` (which
|
|
328
|
+
* carries the `[1M]` suffix the transcript often drops), falling back to
|
|
329
|
+
* the transcript `message.model` when env is empty.
|
|
278
330
|
* Returns `null` when neither yields a signal → the reader emits
|
|
279
331
|
* `conservative-fallback`.
|
|
280
332
|
*/
|
|
@@ -291,7 +343,8 @@ function readContextPercentFallback(input) {
|
|
|
291
343
|
return { ratio: statusline, source: 'statusline-poll', capacityBytes, ide, capturedAt };
|
|
292
344
|
}
|
|
293
345
|
if (typeof input.outerSessionId === 'string' && input.outerSessionId.length > 0) {
|
|
294
|
-
const
|
|
346
|
+
const envModel = resolveClaudeModelFromEnv(input.env);
|
|
347
|
+
const estimate = readClaudeTranscriptEstimate(input.outerSessionId, envModel);
|
|
295
348
|
if (estimate !== null) {
|
|
296
349
|
return {
|
|
297
350
|
ratio: estimate.ratio,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peaks-loop",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.29",
|
|
4
4
|
"description": "Loop Engineering CLI — workflow primitive / loop guards / evaluators / slice orchestration",
|
|
5
5
|
"author": "SquabbyZ",
|
|
6
6
|
"keywords": [
|
|
@@ -101,10 +101,10 @@
|
|
|
101
101
|
"fzf": "^0.5.2",
|
|
102
102
|
"yaml": "^2.9.0",
|
|
103
103
|
"zod": "^4.4.3",
|
|
104
|
-
"peaks-loop-
|
|
105
|
-
"peaks-loop-
|
|
106
|
-
"peaks-loop-shared": "0.0.
|
|
107
|
-
"peaks-loop-shared
|
|
104
|
+
"peaks-loop-internal-runtime": "0.0.14",
|
|
105
|
+
"peaks-loop-mut": "0.1.27",
|
|
106
|
+
"peaks-loop-shared-channel": "0.0.31",
|
|
107
|
+
"peaks-loop-shared": "0.0.63"
|
|
108
108
|
},
|
|
109
109
|
"devDependencies": {
|
|
110
110
|
"@changesets/cli": "2.31.1",
|