peaks-loop 4.0.28 → 4.0.30

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.
Files changed (74) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/cli/commands/_register.js +4 -0
  3. package/dist/cli/commands/dispatch-commands.d.ts +5 -41
  4. package/dist/cli/commands/dispatch-commands.js +59 -185
  5. package/dist/cli/commands/evidence-commands.d.ts +12 -0
  6. package/dist/cli/commands/evidence-commands.js +43 -0
  7. package/dist/cli/commands/fresh-context-commands.d.ts +15 -0
  8. package/dist/cli/commands/fresh-context-commands.js +24 -0
  9. package/dist/cli/commands/job-commands.js +14 -1
  10. package/dist/cli/commands/request-commands.js +15 -97
  11. package/dist/cli/commands/request-format-helpers.d.ts +19 -0
  12. package/dist/cli/commands/request-format-helpers.js +102 -0
  13. package/dist/cli/commands/worktree-auth-commands.js +2 -472
  14. package/dist/cli/commands/worktree-lease-commands.d.ts +21 -0
  15. package/dist/cli/commands/worktree-lease-commands.js +500 -0
  16. package/dist/services/artifacts/request-artifact-service.js +13 -3
  17. package/dist/services/code/auto-compact-lifecycle.d.ts +102 -0
  18. package/dist/services/code/auto-compact-lifecycle.js +235 -0
  19. package/dist/services/code/auto-compact-orchestrator.d.ts +1 -1
  20. package/dist/services/code/auto-compact-orchestrator.js +1 -222
  21. package/dist/services/codegraph/codegraph-autorefresh.d.ts +22 -0
  22. package/dist/services/codegraph/codegraph-autorefresh.js +91 -0
  23. package/dist/services/codegraph/codegraph-preflight-service.d.ts +53 -0
  24. package/dist/services/codegraph/codegraph-preflight-service.js +226 -0
  25. package/dist/services/context/build-dispatch-system-prompt.d.ts +52 -0
  26. package/dist/services/context/build-dispatch-system-prompt.js +70 -3
  27. package/dist/services/dispatch/dispatch-record-types.d.ts +278 -0
  28. package/dist/services/dispatch/dispatch-record-types.js +15 -0
  29. package/dist/services/dispatch/dispatch-record-upgrade.d.ts +5 -0
  30. package/dist/services/dispatch/dispatch-record-upgrade.js +230 -0
  31. package/dist/services/dispatch/dispatch-record-writer.d.ts +3 -278
  32. package/dist/services/dispatch/dispatch-record-writer.js +3 -245
  33. package/dist/services/dispatch/dispatch-sub-agent.d.ts +43 -0
  34. package/dist/services/dispatch/dispatch-sub-agent.js +55 -0
  35. package/dist/services/dispatch/isolation-lease.d.ts +43 -0
  36. package/dist/services/dispatch/isolation-lease.js +129 -0
  37. package/dist/services/evidence/evidence-generator.d.ts +26 -0
  38. package/dist/services/evidence/evidence-generator.js +349 -0
  39. package/dist/services/fresh-context/config.d.ts +1 -0
  40. package/dist/services/fresh-context/config.js +13 -0
  41. package/dist/services/fresh-context/fresh-context-block.d.ts +12 -0
  42. package/dist/services/fresh-context/fresh-context-block.js +40 -0
  43. package/dist/services/fresh-context/trigger-scan.d.ts +30 -0
  44. package/dist/services/fresh-context/trigger-scan.js +29 -0
  45. package/dist/services/skills/hooks-codegate-superpowers.d.ts +65 -0
  46. package/dist/services/skills/hooks-codegate-superpowers.js +204 -0
  47. package/dist/services/skills/hooks-settings-service.d.ts +2 -64
  48. package/dist/services/skills/hooks-settings-service.js +2 -215
  49. package/dist/services/skills/skill-statusline-renderer.d.ts +2 -34
  50. package/dist/services/skills/skill-statusline-renderer.js +1 -184
  51. package/dist/services/skills/statusline-palette.d.ts +62 -0
  52. package/dist/services/skills/statusline-palette.js +190 -0
  53. package/dist/services/slice/slice-decompose-import-edges.d.ts +8 -0
  54. package/dist/services/slice/slice-decompose-import-edges.js +102 -0
  55. package/dist/services/slice/slice-decompose-service.js +3 -198
  56. package/dist/services/slice/slice-decompose-tarjan.d.ts +8 -0
  57. package/dist/services/slice/slice-decompose-tarjan.js +108 -0
  58. package/dist/services/standards/project-standards-service.d.ts +1 -9
  59. package/dist/services/standards/project-standards-service.js +3 -232
  60. package/dist/services/standards/standards-render.d.ts +23 -0
  61. package/dist/services/standards/standards-render.js +238 -0
  62. package/dist/services/standards/ui-library-dispatch-block.d.ts +27 -0
  63. package/dist/services/standards/ui-library-dispatch-block.js +48 -0
  64. package/dist/services/workspace/reconcile-migrate.d.ts +77 -0
  65. package/dist/services/workspace/reconcile-migrate.js +230 -0
  66. package/dist/services/workspace/reconcile-service.d.ts +0 -72
  67. package/dist/services/workspace/reconcile-service.js +3 -220
  68. package/dist/services/workspace/workspace-service.js +13 -2
  69. package/dist/shared/incrementing-number.d.ts +11 -0
  70. package/dist/shared/incrementing-number.js +16 -3
  71. package/package.json +5 -5
  72. package/skills/peaks-code/SKILL.md +6 -2
  73. package/skills/peaks-code/references/fresh-context-preflight.md +81 -0
  74. package/skills/peaks-code/references/sub-agent-dispatch.md +27 -8
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.0.30 — 2026-09-07 (搜索优先 preflight + 超行文件拆分 + CLI 漂移修复)
4
+
5
+ **Highlights**:
6
+
7
+ 1. **搜索优先 preflight(fresh-context)** — 编排层新增信号触发的「先搜再答」:`peaks fresh-context preflight` 做确定性信号扫描,命中则搜 Context7+WebSearch,合成 ≤5 条「绑定指令」(用 X / 别用 Y / 因为 Z)注入 PRD/RD 派发提示词,对冲模型训练数据 2 个月滞后。带权威框架 + 全局 kill-switch + fail-soft 降级 + job 内缓存。
8
+
9
+ 2. **9 个超行文件拆分** — `src/` 全部源文件降至 ≤800 行(Karpathy §2 红线):dispatch-record-writer 1376→774、dispatch-commands 961→752、worktree-auth-commands 1032→312、slice-decompose-service 1014→765、auto-compact-orchestrator 999→756、hooks-settings-service 875→642、skill-statusline-renderer 873→646、project-standards-service 862→644、reconcile-service 861→649、request-commands 806→712。
10
+
11
+ 3. **CLI 漂移修复** — 修复 request-id 大小写敏感(buildNumberedFilename 小写化 slug vs lookup case-sensitive)与子代理覆盖 session 绑定两个 bug;补全 `peaks sub-agent dispatch` 强制 `--graph-node` 的文档。
12
+
13
+ 4. **`peaks evidence generate`** — 一键生成 slice 的 11 个证据工件,收敛编排仪式开销。
14
+
15
+ 5. **UI 组件库优先注入** — RD/UI 派发提示词直接注入检测到的组件库(`## Project stack` 块),前端开发优先用已装组件库而非原生 DOM。
16
+
17
+ ## 4.0.29 — 2026-09-03 (codegraph 生命周期闭环 — pre-read + auto-refresh)
18
+
19
+ **3 atomic commits from session 2026-09-03-session-d49394** (user feedback #3):
20
+
21
+ - `7b9fe627` feat(codegraph): auto-refresh index after slice completion (CLI-internal trigger)
22
+ - `b6599b56` feat(codegraph): preflight project structure into RD dispatch prompts
23
+ - `dfbd229e` docs(memory): sediment codegraph lifecycle closure (pre-read + auto-refresh)
24
+
25
+ **Highlights**:
26
+
27
+ 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 测试。
28
+
29
+ 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 测试。
30
+
31
+ 3. 闭环成型:规划前 pre-read(Slice B)→ 实现 → 完成后 auto-refresh(Slice A)→ 下一 slice 读到新图。
32
+
3
33
  ## 4.0.28 — 2026-09-03 (codegraph 目录回根 + env-first 1M 窗口识别)
4
34
 
5
35
  **3 atomic commits from session 2026-09-03-session-d49394** (user feedback batch):
@@ -19,10 +19,12 @@ import { registerDocCommands } from './doc-commands.js';
19
19
  import { registerE2EVerifyCommand } from './e2e-verify.js';
20
20
  import { registerEccCommands } from './ecc-commands.js';
21
21
  import { registerEvolutionCommands } from './evolution-commands.js';
22
+ import { registerEvidenceCommands } from './evidence-commands.js';
22
23
  import { registerFeedbackCommands } from './feedback-commands.js';
23
24
  import { registerFinalReviewCommands } from './final-review-commands.js';
24
25
  import { registerFixtureCommands } from './fixture-commands.js';
25
26
  import { registerForkCommands } from './fork-commands.js';
27
+ import { registerFreshContextCommands } from './fresh-context-commands.js';
26
28
  import { registerGateCommands } from './gate-commands.js';
27
29
  import { registerClassifyCommands, registerContractCommands } from './governance-classify-contract-commands.js';
28
30
  import { registerHookHandleCommand } from './hook-handle.js';
@@ -117,6 +119,8 @@ const REGISTRATIONS = [
117
119
  ['skill-loop-engineering-readiness-commands', registerSkillLoopEngineeringReadinessCommands],
118
120
  ['loop-commands', registerLoopCommands], ['loop-eval-commands', registerWorkflowEvalCommands],
119
121
  ['evolution-commands', registerEvolutionCommands], ['asset-commands', registerAssetCommands],
122
+ ['evidence-commands', registerEvidenceCommands],
123
+ ['fresh-context-commands', registerFreshContextCommands],
120
124
  ['bee-commands', registerBeeCommands], ['ecc-commands', registerEccCommands],
121
125
  ['baseline-commands', registerBaselineCommands],
122
126
  ['upgrade-commands', registerUpgradeCommands], ['code-review-commands', registerCodeReviewCommands],
@@ -2,45 +2,9 @@ import type { Command } from 'commander';
2
2
  import { type ProgramIO } from '../cli-helpers.js';
3
3
  export declare function registerDispatchCommand(parent: Command, io: ProgramIO): void;
4
4
  /**
5
- * F5 follow-up (sediment 2026-08-11-rid-001-redo-fake-green-recovery-closure
6
- * §Lesson 1): synchronous anti-fake-green file-existence gate. Runs
7
- * `git ls-files <glob>` against `projectRoot` and returns the matching
8
- * tracked file paths (relative to projectRoot). Empty array when no
9
- * files match (e.g. untracked new file, wrong glob, not a git repo).
10
- *
11
- * Why `git ls-files` and not `fs.glob`: the anti-fake-green contract
12
- * is "the file the sub-agent claims to have written must ACTUALLY be
13
- * tracked by git" — `git ls-files` enforces that contract; `fs.glob`
14
- * would happily return untracked-but-on-disk files (false-positive
15
- * for the fake-green gate).
16
- *
17
- * Failure modes (best-effort, never throws):
18
- * - git not on PATH → empty array (`ENOENT` swallowed)
19
- * - not a git repo → empty array (git exits non-zero)
20
- * - glob matches zero tracked files → empty array
21
- *
22
- * Exported for unit-test access (`tests/unit/sub-agent/must-ls-files-flag.test.ts`).
23
- * The export is intentional — the helper has zero side effects and
24
- * keeps the dispatch action handler small.
5
+ * 2.7.0 slice-dag-dispatcher MVP — see `dispatch-from-dag.ts`.
6
+ * The function was pulled out of this file in slice 2026-06-23-audit-3rd
7
+ * #7 to honor the 800-line file cap and isolate the three heavy
8
+ * module loads (slice-dag / dag-orchestrator / contract-store) to the
9
+ * --from-dag codepath only.
25
10
  */
26
- export declare function runGitLsFiles(projectRoot: string, glob: string): readonly string[];
27
- /**
28
- * `dispatchSubAgent` is the thin programmatic wrapper the integration
29
- * test (`tests/integration/sub-agent-graph-binding.test.ts`) imports.
30
- * It enforces --graph-node required BEFORE any record write so a
31
- * caller can't bypass the CLI's requiredOption guard. Throws a typed
32
- * error with `code = PEAKS_GRAPH_NODE_REQUIRED` when missing.
33
- */
34
- export declare function dispatchSubAgent(input: {
35
- projectRoot: string;
36
- role: string;
37
- prompt: string;
38
- sessionId?: string;
39
- graphNode?: string;
40
- workflowId?: string;
41
- graphRef?: string;
42
- }): Promise<{
43
- role: string;
44
- toolCall: unknown;
45
- dispatchRecordPath: string | null;
46
- }>;
@@ -16,7 +16,6 @@
16
16
  */
17
17
  import { existsSync } from 'node:fs';
18
18
  import { randomUUID } from 'node:crypto';
19
- import { spawn as childProcessSpawn } from 'node:child_process';
20
19
  import { fail, getErrorMessage, ok } from 'peaks-loop-shared/result';
21
20
  import { addJsonOption, printResult } from '../cli-helpers.js';
22
21
  import { detectInstalledIde } from '../../services/ide/ide-detector.js';
@@ -39,7 +38,11 @@ import { runDispatchFromDag } from './dispatch-from-dag.js';
39
38
  import { TEST_TOOL_DETECTION_BLOCK, formatTestToolDetection } from '../../services/dispatch/test-tool-detection.js';
40
39
  import { MemoryPreflightService } from '../../services/context/memory-preflight-service.js';
41
40
  import { buildDispatchSystemPrompt } from '../../services/context/build-dispatch-system-prompt.js';
41
+ import { computeUiLibraryDispatchBlock } from '../../services/standards/ui-library-dispatch-block.js';
42
+ import { readFreshContextBlock } from '../../services/fresh-context/fresh-context-block.js';
42
43
  import { createDispatchProvenanceToken, DISPATCH_PROVENANCE_ENV, writeDispatchProvenance, } from '../../services/worktree/dispatch-provenance.js';
44
+ import { spawnWorktreeLease, spawnContainerLease } from '../../services/dispatch/isolation-lease.js';
45
+ import { runGitLsFiles } from '../../services/dispatch/dispatch-sub-agent.js';
43
46
  export function registerDispatchCommand(parent, io) {
44
47
  addJsonOption(parent
45
48
  .command('dispatch')
@@ -371,6 +374,48 @@ export function registerDispatchCommand(parent, io) {
371
374
  // pure-function builder.
372
375
  const preflightService = new MemoryPreflightService(projectRoot, projectPrefs);
373
376
  const memoryBlock = await preflightService.fetchBlock(role);
377
+ // Slice 2026-09-03-codegraph-preread (Option A): pre-dispatch
378
+ // codegraph preflight for RD planning. BEFORE the RD sub-agent's
379
+ // prompt is composed, ensure the codegraph index exists (init +
380
+ // index best-effort when `.codegraph/` is absent; skip when already
381
+ // fresh) and read a BOUNDED project-structure summary. The block is
382
+ // only requested for the `rd` role — other roles keep the legacy
383
+ // prompt byte-identical. Fail-soft: any codegraph failure degrades
384
+ // to a null block (builder renders the "codegraph unavailable" note)
385
+ // and the dispatch proceeds; we never hard-block RD dispatch here.
386
+ let codegraphBlock;
387
+ if (role === 'rd') {
388
+ try {
389
+ const { buildCodegraphPreflightBlock } = await import('../../services/codegraph/codegraph-preflight-service.js');
390
+ const preflight = await buildCodegraphPreflightBlock(projectRoot);
391
+ codegraphBlock = preflight.available ? preflight.block : null;
392
+ }
393
+ catch {
394
+ codegraphBlock = null; // fail-soft: never block RD dispatch
395
+ }
396
+ }
397
+ // Slice 2026-09-06-ui-lib-dispatch-priority: surface the detected
398
+ // component library (+ CSS framework + build tool) in the RD/UI
399
+ // dispatch system prompt so the sub-agent prefers the library over
400
+ // hand-rolled native DOM. Only injected for frontend-generating roles
401
+ // (rd + ui) — qa/sc/txt/prd keep the legacy prompt byte-identical.
402
+ // Fail-soft inside the wrapper: a detection error degrades to null.
403
+ let projectStackBlock;
404
+ if (role === 'rd' || role === 'ui') {
405
+ projectStackBlock = computeUiLibraryDispatchBlock(projectRoot);
406
+ }
407
+ // Slice 2026-09-07-search-first-preflight: read the orchestrator-
408
+ // synthesized fresh-context block for rd + prd dispatches. The block
409
+ // was written to .peaks/_runtime/<sid>/fresh-context.md when the
410
+ // preflight triggered; it carries ≤5 binding directives that hedge
411
+ // model training-data lag. Fail-soft: a missing / unreadable file, or
412
+ // a file without the `## Fresh context` heading, degrades to null so
413
+ // the prompt stays byte-identical to the legacy shape. Other roles
414
+ // (qa/sc/txt/ui) keep the legacy prompt unchanged.
415
+ let freshContextBlock;
416
+ if (role === 'rd' || role === 'prd') {
417
+ freshContextBlock = readFreshContextBlock(projectRoot, sid);
418
+ }
374
419
  // Slice 2026-07-29-context-evaluation-accuracy: capture the
375
420
  // authoritative context-fill probe before composing the
376
421
  // dispatch prompt. The probe is token-counted (IDE adapter's
@@ -400,7 +445,19 @@ export function registerDispatchCommand(parent, io) {
400
445
  taskTitle: role,
401
446
  taskBody: options.prompt,
402
447
  memoryBlock,
403
- contextProbe
448
+ contextProbe,
449
+ // exactOptionalPropertyTypes: only set codegraphBlock when the rd
450
+ // preflight actually produced a value (null = attempted-unavailable,
451
+ // undefined = not requested → legacy prompt unchanged).
452
+ ...(codegraphBlock !== undefined ? { codegraphBlock } : {}),
453
+ // Same optionality contract for the project-stack block: set only
454
+ // when the rd/ui detection ran (null = no library → no block,
455
+ // undefined = non-frontend role → legacy prompt unchanged).
456
+ ...(projectStackBlock !== undefined ? { projectStackBlock } : {}),
457
+ // Same optionality contract for the fresh-context block: set only
458
+ // when the rd/prd read ran (null = no block on disk → no block,
459
+ // undefined = non-rd/prd role → legacy prompt unchanged).
460
+ ...(freshContextBlock !== undefined ? { freshContextBlock } : {})
404
461
  });
405
462
  // Part 2.C: when --isolation worktree, prepend an isolation envelope
406
463
  // block so the sub-agent sees the lease id + worktree path. The block
@@ -677,186 +734,3 @@ export function registerDispatchCommand(parent, io) {
677
734
  * module loads (slice-dag / dag-orchestrator / contract-store) to the
678
735
  * --from-dag codepath only.
679
736
  */
680
- /**
681
- * Part 2.C (slice 2026-07-29-worktree-l2-extended) — spawn a worktree
682
- * lease by shelling out to `peaks worktree spawn` (avoid re-implementing
683
- * the lease-write + git-worktree-add sequence in this file). The CLI
684
- * does the lease write, the git worktree add, AND the error handling;
685
- * we just parse the JSON envelope and surface the leaseId + path.
686
- *
687
- * Throws on spawn failure; the caller converts the error to a
688
- * ISOLATION_SPAWN_FAILED envelope. Synchronous wait is acceptable: the
689
- * dispatch is already async, the lease is a few hundred ms of FS work,
690
- * and we need the leaseId before we build the dispatch record.
691
- */
692
- function spawnWorktreeLease(args) {
693
- return new Promise((resolve, reject) => {
694
- const child = childProcessSpawn(process.execPath, [
695
- // The compiled CLI lives in dist/cli/peaks.js. We pass the entry
696
- // through node so the test suite (which also runs on the same
697
- // process) and the production binary share the same path. When
698
- // the binary is invoked as `peaks`, the package bin stub does
699
- // this for us; here we explicitly use process.execPath + the
700
- // resolved entry to avoid PATH surprises.
701
- process.argv[1] ?? '',
702
- 'worktree', 'spawn',
703
- '--rid', args.rid,
704
- '--role', args.role,
705
- '--purpose', args.purpose,
706
- '--project', args.projectRoot,
707
- '--session', args.sessionId,
708
- '--json'
709
- ], { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, detached: true });
710
- let stdout = '';
711
- let stderr = '';
712
- child.stdout.on('data', (d) => { stdout += d.toString('utf8'); });
713
- child.stderr.on('data', (d) => { stderr += d.toString('utf8'); });
714
- child.on('error', (err) => reject(new Error(`worktree spawn subprocess failed: ${err.message}`)));
715
- child.on('close', (code) => {
716
- if (code !== 0) {
717
- reject(new Error(`peaks worktree spawn exited ${code}; stderr: ${stderr.trim() || '(empty)'}`));
718
- return;
719
- }
720
- let parsed;
721
- try {
722
- parsed = JSON.parse(stdout);
723
- }
724
- catch (err) {
725
- reject(new Error(`peaks worktree spawn produced unparseable JSON: ${err.message}; stdout: ${stdout.slice(0, 400)}`));
726
- return;
727
- }
728
- if (typeof parsed !== 'object' || parsed === null) {
729
- reject(new Error('peaks worktree spawn envelope is not an object'));
730
- return;
731
- }
732
- const env = parsed;
733
- if (env.ok !== true || !env.data?.lease) {
734
- reject(new Error(`peaks worktree spawn envelope missing lease; got: ${stdout.slice(0, 200)}`));
735
- return;
736
- }
737
- resolve({
738
- leaseId: env.data.lease.leaseId,
739
- path: env.data.lease.path,
740
- branch: env.data.lease.branch,
741
- expiresAt: env.data.lease.expiresAt
742
- });
743
- // Part 47: unref via setImmediate so the close handler
744
- // finishes first and Node's stdio 'end' events drain the
745
- // stdout/stderr buffers before the parent releases the
746
- // child handle. Without this microtask defer, the unref
747
- // races the buffered stdout close and the test receives
748
- // an empty JSON envelope.
749
- setImmediate(() => { child.unref(); });
750
- });
751
- });
752
- }
753
- /**
754
- * Slice 2026-07-29-worktree-l2-extended Part 12: container
755
- * isolation bridge. Shells out to `peaks container spawn` to
756
- * run `docker run` + write the container lease. Returns the
757
- * leaseId the dispatch record needs to persist. The shape is
758
- * a subset of the spawnWorktreeLease return (just leaseId);
759
- * we do not need the path/branch/expiresAt for the container
760
- * path because the envelope surfaces a different set of
761
- * fields (image + containerId; see container-lease.ts).
762
- */
763
- function spawnContainerLease(args) {
764
- return new Promise((resolve, reject) => {
765
- const child = childProcessSpawn(process.execPath, [
766
- process.argv[1] ?? '',
767
- 'container', 'spawn',
768
- '--rid', args.rid,
769
- '--role', args.role,
770
- '--purpose', args.purpose,
771
- '--project', args.projectRoot,
772
- '--session', args.sessionId,
773
- '--json'
774
- ], { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, detached: true });
775
- let stdout = '';
776
- let stderr = '';
777
- child.stdout.on('data', (d) => { stdout += d.toString('utf8'); });
778
- child.stderr.on('data', (d) => { stderr += d.toString('utf8'); });
779
- child.on('error', (err) => reject(new Error(`container spawn subprocess failed: ${err.message}`)));
780
- child.on('close', (code) => {
781
- if (code !== 0) {
782
- reject(new Error(`peaks container spawn exited ${code}; stderr: ${stderr.trim() || '(empty)'}`));
783
- return;
784
- }
785
- let parsed;
786
- try {
787
- parsed = JSON.parse(stdout);
788
- }
789
- catch (err) {
790
- reject(new Error(`peaks container spawn produced unparseable JSON: ${err.message}; stdout: ${stdout.slice(0, 400)}`));
791
- return;
792
- }
793
- if (typeof parsed !== 'object' || parsed === null) {
794
- reject(new Error('peaks container spawn envelope is not an object'));
795
- return;
796
- }
797
- const env = parsed;
798
- if (env.ok !== true || !env.data?.lease) {
799
- reject(new Error(`peaks container spawn envelope missing lease; got: ${stdout.slice(0, 200)}`));
800
- return;
801
- }
802
- resolve({ leaseId: env.data.lease.leaseId });
803
- });
804
- // See spawnWorktreeLease above for the rationale.
805
- child.unref();
806
- });
807
- }
808
- /**
809
- * F5 follow-up (sediment 2026-08-11-rid-001-redo-fake-green-recovery-closure
810
- * §Lesson 1): synchronous anti-fake-green file-existence gate. Runs
811
- * `git ls-files <glob>` against `projectRoot` and returns the matching
812
- * tracked file paths (relative to projectRoot). Empty array when no
813
- * files match (e.g. untracked new file, wrong glob, not a git repo).
814
- *
815
- * Why `git ls-files` and not `fs.glob`: the anti-fake-green contract
816
- * is "the file the sub-agent claims to have written must ACTUALLY be
817
- * tracked by git" — `git ls-files` enforces that contract; `fs.glob`
818
- * would happily return untracked-but-on-disk files (false-positive
819
- * for the fake-green gate).
820
- *
821
- * Failure modes (best-effort, never throws):
822
- * - git not on PATH → empty array (`ENOENT` swallowed)
823
- * - not a git repo → empty array (git exits non-zero)
824
- * - glob matches zero tracked files → empty array
825
- *
826
- * Exported for unit-test access (`tests/unit/sub-agent/must-ls-files-flag.test.ts`).
827
- * The export is intentional — the helper has zero side effects and
828
- * keeps the dispatch action handler small.
829
- */
830
- export function runGitLsFiles(projectRoot, glob) {
831
- try {
832
- const { execFileSync } = require('node:child_process');
833
- const stdout = execFileSync('git', ['ls-files', '--', glob], { cwd: projectRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
834
- return stdout.split('\n').filter((line) => line.length > 0);
835
- }
836
- catch {
837
- return [];
838
- }
839
- }
840
- /* ---------- Slice 4.0.8 RD §4 D4c: programmatic dispatcher ---------- */
841
- /**
842
- * `dispatchSubAgent` is the thin programmatic wrapper the integration
843
- * test (`tests/integration/sub-agent-graph-binding.test.ts`) imports.
844
- * It enforces --graph-node required BEFORE any record write so a
845
- * caller can't bypass the CLI's requiredOption guard. Throws a typed
846
- * error with `code = PEAKS_GRAPH_NODE_REQUIRED` when missing.
847
- */
848
- export async function dispatchSubAgent(input) {
849
- if (typeof input.graphNode !== 'string' || input.graphNode.length === 0) {
850
- const err = new Error('PEAKS_GRAPH_NODE_REQUIRED: --graph-node is required (RD §4 D4c)');
851
- err.code = 'PEAKS_GRAPH_NODE_REQUIRED';
852
- throw err;
853
- }
854
- // The integration test only checks the rejection path; the success
855
- // path is exercised by the existing CLI command. Return a minimal
856
- // stub so any future programmatic caller has a stable surface.
857
- return {
858
- role: input.role,
859
- toolCall: null,
860
- dispatchRecordPath: null,
861
- };
862
- }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * peaks evidence * CLI surface — mechanical per-slice evidence generation.
3
+ *
4
+ * `peaks evidence generate` writes the ~11 per-slice evidence artifacts under
5
+ * `.peaks/_runtime/<sid>/` so the orchestrator no longer hand-writes them for
6
+ * a mechanical file-split (or similar) slice. The body builders live in
7
+ * `services/evidence/evidence-generator.ts`; this module only wires the
8
+ * Commander surface + project/session resolution + JSON envelope.
9
+ */
10
+ import type { Command } from 'commander';
11
+ import { type ProgramIO } from '../cli-helpers.js';
12
+ export declare function registerEvidenceCommands(program: Command, io: ProgramIO): void;
@@ -0,0 +1,43 @@
1
+ import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
2
+ import { fail, ok } from 'peaks-loop-shared/result';
3
+ import { generateEvidence, parseFiles, parseLineCounts } from '../../services/evidence/evidence-generator.js';
4
+ import { getSessionId } from '../../services/session/session-manager.js';
5
+ import { findProjectRoot } from '../../services/config/config-safety.js';
6
+ export function registerEvidenceCommands(program, io) {
7
+ const evidence = program.command('evidence').description('Generate per-slice evidence artifacts for mechanical slices');
8
+ addJsonOption(evidence
9
+ .command('generate')
10
+ .description('Write the ~11 rd/qa/audit/prd evidence artifacts for a mechanical file-split slice under .peaks/_runtime/<sid>/')
11
+ .requiredOption('--rid <rid>', 'request id (e.g. 2026-09-06-split-dispatch-commands)')
12
+ .requiredOption('--title <title>', 'one-line description of the slice')
13
+ .requiredOption('--files <list>', 'comma-separated changed file paths')
14
+ .requiredOption('--line-counts <list>', 'semicolon-separated file=lines pairs (e.g. "f=120;g=300")')
15
+ .option('--session-id <sid>', 'session id; defaults to the project-bound session')).action(async (options) => {
16
+ try {
17
+ const projectRoot = findProjectRoot(process.cwd()) ?? process.cwd();
18
+ const sessionId = options.sessionId ?? getSessionId(projectRoot);
19
+ if (sessionId === null || sessionId === undefined || sessionId.length === 0) {
20
+ printResult(io, fail('evidence.generate', 'SESSION_NOT_BOUND', 'No --session-id was provided and no peaks session is bound for this project.', { projectRoot }, ['Pass --session-id <sid>, or run `peaks workspace init` first so a session directory exists.']), options.json);
21
+ process.exitCode = 1;
22
+ return;
23
+ }
24
+ const files = parseFiles(options.files ?? '');
25
+ const lineCounts = parseLineCounts(options.lineCounts ?? '');
26
+ const result = await generateEvidence({
27
+ projectRoot,
28
+ rid: options.rid ?? '',
29
+ title: options.title ?? '',
30
+ files,
31
+ lineCounts,
32
+ sessionId
33
+ });
34
+ printResult(io, ok('evidence.generate', result, [], [
35
+ `Wrote ${result.writtenFiles.length} evidence files under ${result.sessionRoot}`
36
+ ]), options.json);
37
+ }
38
+ catch (error) {
39
+ printResult(io, fail('evidence.generate', 'EVIDENCE_GENERATE_FAILED', getErrorMessage(error), {}, ['Verify the target project is writable.']), options.json);
40
+ process.exitCode = 1;
41
+ }
42
+ });
43
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `peaks fresh-context preflight` CLI — deterministic trigger scan for the
3
+ * search-first preflight (slice 2026-09-07-search-first-preflight).
4
+ *
5
+ * This command is the shared, deterministic surface every peaks-* orchestrator
6
+ * calls at orchestration-start, BEFORE the first planning action, to decide
7
+ * whether to search fresh context (Context7 → WebSearch) and synthesize binding
8
+ * directives. The actual search + synthesis is done by the LLM per the shared
9
+ * `skills/peaks-code/references/fresh-context-preflight.md`; this command only
10
+ * runs the pure trigger scan (`services/fresh-context/trigger-scan.ts`) and the
11
+ * kill-switch read (`services/fresh-context/config.ts`).
12
+ */
13
+ import type { Command } from 'commander';
14
+ import { type ProgramIO } from '../cli-helpers.js';
15
+ export declare function registerFreshContextCommands(program: Command, io: ProgramIO): void;
@@ -0,0 +1,24 @@
1
+ import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
2
+ import { fail, ok } from 'peaks-loop-shared/result';
3
+ import { scanFreshContextTrigger } from '../../services/fresh-context/trigger-scan.js';
4
+ import { isFreshContextEnabled } from '../../services/fresh-context/config.js';
5
+ export function registerFreshContextCommands(program, io) {
6
+ const freshContext = program
7
+ .command('fresh-context')
8
+ .description('Search-first preflight for the orchestration layer (hedge training-data lag)');
9
+ addJsonOption(freshContext
10
+ .command('preflight')
11
+ .description('Deterministic signal scan: report whether the prompt triggers a fresh-context search')
12
+ .requiredOption('--prompt <text>', 'the user request text to scan for trigger keywords')
13
+ .option('--project <root>', 'target project root (accepted for surface parity; the scan is text + config only)')).action((options) => {
14
+ try {
15
+ const enabled = isFreshContextEnabled();
16
+ const result = scanFreshContextTrigger(options.prompt ?? '', enabled);
17
+ printResult(io, ok('fresh-context.preflight', result), options.json);
18
+ }
19
+ catch (error) {
20
+ printResult(io, fail('fresh-context.preflight', 'PREFLIGHT_FAILED', getErrorMessage(error), {}, ['Verify ~/.peaks/config.json is valid JSON']), options.json);
21
+ process.exitCode = 1;
22
+ }
23
+ });
24
+ }
@@ -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