peaks-loop 4.0.29 → 4.0.31

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 (75) hide show
  1. package/CHANGELOG.md +27 -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 +35 -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/request-commands.js +2 -96
  10. package/dist/cli/commands/request-format-helpers.d.ts +19 -0
  11. package/dist/cli/commands/request-format-helpers.js +102 -0
  12. package/dist/cli/commands/workspace/init-command.js +27 -14
  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.js +44 -6
  22. package/dist/services/codegraph/codegraph-preflight-service.d.ts +5 -2
  23. package/dist/services/codegraph/codegraph-preflight-service.js +13 -6
  24. package/dist/services/codegraph/codegraph-service.d.ts +17 -2
  25. package/dist/services/codegraph/codegraph-service.js +27 -2
  26. package/dist/services/context/build-dispatch-system-prompt.d.ts +26 -0
  27. package/dist/services/context/build-dispatch-system-prompt.js +38 -3
  28. package/dist/services/dispatch/dispatch-record-types.d.ts +278 -0
  29. package/dist/services/dispatch/dispatch-record-types.js +15 -0
  30. package/dist/services/dispatch/dispatch-record-upgrade.d.ts +5 -0
  31. package/dist/services/dispatch/dispatch-record-upgrade.js +230 -0
  32. package/dist/services/dispatch/dispatch-record-writer.d.ts +3 -278
  33. package/dist/services/dispatch/dispatch-record-writer.js +3 -245
  34. package/dist/services/dispatch/dispatch-sub-agent.d.ts +43 -0
  35. package/dist/services/dispatch/dispatch-sub-agent.js +55 -0
  36. package/dist/services/dispatch/isolation-lease.d.ts +43 -0
  37. package/dist/services/dispatch/isolation-lease.js +129 -0
  38. package/dist/services/evidence/evidence-generator.d.ts +26 -0
  39. package/dist/services/evidence/evidence-generator.js +349 -0
  40. package/dist/services/fresh-context/config.d.ts +1 -0
  41. package/dist/services/fresh-context/config.js +13 -0
  42. package/dist/services/fresh-context/fresh-context-block.d.ts +12 -0
  43. package/dist/services/fresh-context/fresh-context-block.js +40 -0
  44. package/dist/services/fresh-context/trigger-scan.d.ts +30 -0
  45. package/dist/services/fresh-context/trigger-scan.js +29 -0
  46. package/dist/services/skills/hooks-codegate-superpowers.d.ts +65 -0
  47. package/dist/services/skills/hooks-codegate-superpowers.js +204 -0
  48. package/dist/services/skills/hooks-settings-service.d.ts +2 -64
  49. package/dist/services/skills/hooks-settings-service.js +2 -215
  50. package/dist/services/skills/skill-statusline-renderer.d.ts +2 -34
  51. package/dist/services/skills/skill-statusline-renderer.js +1 -184
  52. package/dist/services/skills/statusline-palette.d.ts +62 -0
  53. package/dist/services/skills/statusline-palette.js +190 -0
  54. package/dist/services/slice/slice-decompose-import-edges.d.ts +8 -0
  55. package/dist/services/slice/slice-decompose-import-edges.js +102 -0
  56. package/dist/services/slice/slice-decompose-service.js +3 -198
  57. package/dist/services/slice/slice-decompose-tarjan.d.ts +8 -0
  58. package/dist/services/slice/slice-decompose-tarjan.js +108 -0
  59. package/dist/services/standards/project-standards-service.d.ts +1 -9
  60. package/dist/services/standards/project-standards-service.js +3 -232
  61. package/dist/services/standards/standards-render.d.ts +23 -0
  62. package/dist/services/standards/standards-render.js +238 -0
  63. package/dist/services/standards/ui-library-dispatch-block.d.ts +27 -0
  64. package/dist/services/standards/ui-library-dispatch-block.js +48 -0
  65. package/dist/services/workspace/reconcile-migrate.d.ts +77 -0
  66. package/dist/services/workspace/reconcile-migrate.js +230 -0
  67. package/dist/services/workspace/reconcile-service.d.ts +0 -72
  68. package/dist/services/workspace/reconcile-service.js +3 -220
  69. package/dist/services/workspace/workspace-service.js +13 -2
  70. package/dist/shared/incrementing-number.d.ts +11 -0
  71. package/dist/shared/incrementing-number.js +16 -3
  72. package/package.json +5 -5
  73. package/skills/peaks-code/SKILL.md +6 -2
  74. package/skills/peaks-code/references/fresh-context-preflight.md +81 -0
  75. package/skills/peaks-code/references/sub-agent-dispatch.md +27 -8
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.0.31 — 2026-09-08 (codegraph auto-refresh 悬空 marker 修复)
4
+
5
+ **Highlights**:
6
+
7
+ 1. **codegraph 自动刷新失效修复** — 根因:`peaks workspace init` auto-stake 只写 `.peaks-loop-marker` 不跑上游 `codegraph init`,留下无 `codegraph.db` 的悬空 `.codegraph/`;下游把"有 marker"误判为"已初始化",post-slice 自动刷新跑 `index` 静默失败("CodeGraph not initialized")。
8
+
9
+ - 新增 `isCodegraphInitialized()` 按 `.codegraph/codegraph.db` 判真实初始化(对齐上游 `isInitialized`)。
10
+ - `workspace init` auto-stake 改为真跑上游 `codegraph init`(快、离线安全)。
11
+ - auto-refresh / preflight 发现悬空 marker(有 marker 无 db)时自愈:先 `init` 再 `index`。
12
+ - init guard 对"marker 有但 db 无"返回 `fresh`,`peaks codegraph init` 可重跑恢复。
13
+
14
+ 验证:scoped 51 测试 + codegraph/workspace 73 测试全过,tsc 干净,现场自测从悬空态成功建出 `codegraph.db`。
15
+
16
+ ## 4.0.30 — 2026-09-07 (搜索优先 preflight + 超行文件拆分 + CLI 漂移修复)
17
+
18
+ **Highlights**:
19
+
20
+ 1. **搜索优先 preflight(fresh-context)** — 编排层新增信号触发的「先搜再答」:`peaks fresh-context preflight` 做确定性信号扫描,命中则搜 Context7+WebSearch,合成 ≤5 条「绑定指令」(用 X / 别用 Y / 因为 Z)注入 PRD/RD 派发提示词,对冲模型训练数据 2 个月滞后。带权威框架 + 全局 kill-switch + fail-soft 降级 + job 内缓存。
21
+
22
+ 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。
23
+
24
+ 3. **CLI 漂移修复** — 修复 request-id 大小写敏感(buildNumberedFilename 小写化 slug vs lookup case-sensitive)与子代理覆盖 session 绑定两个 bug;补全 `peaks sub-agent dispatch` 强制 `--graph-node` 的文档。
25
+
26
+ 4. **`peaks evidence generate`** — 一键生成 slice 的 11 个证据工件,收敛编排仪式开销。
27
+
28
+ 5. **UI 组件库优先注入** — RD/UI 派发提示词直接注入检测到的组件库(`## Project stack` 块),前端开发优先用已装组件库而非原生 DOM。
29
+
3
30
  ## 4.0.29 — 2026-09-03 (codegraph 生命周期闭环 — pre-read + auto-refresh)
4
31
 
5
32
  **3 atomic commits from session 2026-09-03-session-d49394** (user feedback #3):
@@ -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')
@@ -391,6 +394,28 @@ export function registerDispatchCommand(parent, io) {
391
394
  codegraphBlock = null; // fail-soft: never block RD dispatch
392
395
  }
393
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
+ }
394
419
  // Slice 2026-07-29-context-evaluation-accuracy: capture the
395
420
  // authoritative context-fill probe before composing the
396
421
  // dispatch prompt. The probe is token-counted (IDE adapter's
@@ -424,7 +449,15 @@ export function registerDispatchCommand(parent, io) {
424
449
  // exactOptionalPropertyTypes: only set codegraphBlock when the rd
425
450
  // preflight actually produced a value (null = attempted-unavailable,
426
451
  // undefined = not requested → legacy prompt unchanged).
427
- ...(codegraphBlock !== undefined ? { codegraphBlock } : {})
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 } : {})
428
461
  });
429
462
  // Part 2.C: when --isolation worktree, prepend an isolation envelope
430
463
  // block so the sub-agent sees the lease id + worktree path. The block
@@ -701,186 +734,3 @@ export function registerDispatchCommand(parent, io) {
701
734
  * module loads (slice-dag / dag-orchestrator / contract-store) to the
702
735
  * --from-dag codepath only.
703
736
  */
704
- /**
705
- * Part 2.C (slice 2026-07-29-worktree-l2-extended) — spawn a worktree
706
- * lease by shelling out to `peaks worktree spawn` (avoid re-implementing
707
- * the lease-write + git-worktree-add sequence in this file). The CLI
708
- * does the lease write, the git worktree add, AND the error handling;
709
- * we just parse the JSON envelope and surface the leaseId + path.
710
- *
711
- * Throws on spawn failure; the caller converts the error to a
712
- * ISOLATION_SPAWN_FAILED envelope. Synchronous wait is acceptable: the
713
- * dispatch is already async, the lease is a few hundred ms of FS work,
714
- * and we need the leaseId before we build the dispatch record.
715
- */
716
- function spawnWorktreeLease(args) {
717
- return new Promise((resolve, reject) => {
718
- const child = childProcessSpawn(process.execPath, [
719
- // The compiled CLI lives in dist/cli/peaks.js. We pass the entry
720
- // through node so the test suite (which also runs on the same
721
- // process) and the production binary share the same path. When
722
- // the binary is invoked as `peaks`, the package bin stub does
723
- // this for us; here we explicitly use process.execPath + the
724
- // resolved entry to avoid PATH surprises.
725
- process.argv[1] ?? '',
726
- 'worktree', 'spawn',
727
- '--rid', args.rid,
728
- '--role', args.role,
729
- '--purpose', args.purpose,
730
- '--project', args.projectRoot,
731
- '--session', args.sessionId,
732
- '--json'
733
- ], { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, detached: true });
734
- let stdout = '';
735
- let stderr = '';
736
- child.stdout.on('data', (d) => { stdout += d.toString('utf8'); });
737
- child.stderr.on('data', (d) => { stderr += d.toString('utf8'); });
738
- child.on('error', (err) => reject(new Error(`worktree spawn subprocess failed: ${err.message}`)));
739
- child.on('close', (code) => {
740
- if (code !== 0) {
741
- reject(new Error(`peaks worktree spawn exited ${code}; stderr: ${stderr.trim() || '(empty)'}`));
742
- return;
743
- }
744
- let parsed;
745
- try {
746
- parsed = JSON.parse(stdout);
747
- }
748
- catch (err) {
749
- reject(new Error(`peaks worktree spawn produced unparseable JSON: ${err.message}; stdout: ${stdout.slice(0, 400)}`));
750
- return;
751
- }
752
- if (typeof parsed !== 'object' || parsed === null) {
753
- reject(new Error('peaks worktree spawn envelope is not an object'));
754
- return;
755
- }
756
- const env = parsed;
757
- if (env.ok !== true || !env.data?.lease) {
758
- reject(new Error(`peaks worktree spawn envelope missing lease; got: ${stdout.slice(0, 200)}`));
759
- return;
760
- }
761
- resolve({
762
- leaseId: env.data.lease.leaseId,
763
- path: env.data.lease.path,
764
- branch: env.data.lease.branch,
765
- expiresAt: env.data.lease.expiresAt
766
- });
767
- // Part 47: unref via setImmediate so the close handler
768
- // finishes first and Node's stdio 'end' events drain the
769
- // stdout/stderr buffers before the parent releases the
770
- // child handle. Without this microtask defer, the unref
771
- // races the buffered stdout close and the test receives
772
- // an empty JSON envelope.
773
- setImmediate(() => { child.unref(); });
774
- });
775
- });
776
- }
777
- /**
778
- * Slice 2026-07-29-worktree-l2-extended Part 12: container
779
- * isolation bridge. Shells out to `peaks container spawn` to
780
- * run `docker run` + write the container lease. Returns the
781
- * leaseId the dispatch record needs to persist. The shape is
782
- * a subset of the spawnWorktreeLease return (just leaseId);
783
- * we do not need the path/branch/expiresAt for the container
784
- * path because the envelope surfaces a different set of
785
- * fields (image + containerId; see container-lease.ts).
786
- */
787
- function spawnContainerLease(args) {
788
- return new Promise((resolve, reject) => {
789
- const child = childProcessSpawn(process.execPath, [
790
- process.argv[1] ?? '',
791
- 'container', 'spawn',
792
- '--rid', args.rid,
793
- '--role', args.role,
794
- '--purpose', args.purpose,
795
- '--project', args.projectRoot,
796
- '--session', args.sessionId,
797
- '--json'
798
- ], { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, detached: true });
799
- let stdout = '';
800
- let stderr = '';
801
- child.stdout.on('data', (d) => { stdout += d.toString('utf8'); });
802
- child.stderr.on('data', (d) => { stderr += d.toString('utf8'); });
803
- child.on('error', (err) => reject(new Error(`container spawn subprocess failed: ${err.message}`)));
804
- child.on('close', (code) => {
805
- if (code !== 0) {
806
- reject(new Error(`peaks container spawn exited ${code}; stderr: ${stderr.trim() || '(empty)'}`));
807
- return;
808
- }
809
- let parsed;
810
- try {
811
- parsed = JSON.parse(stdout);
812
- }
813
- catch (err) {
814
- reject(new Error(`peaks container spawn produced unparseable JSON: ${err.message}; stdout: ${stdout.slice(0, 400)}`));
815
- return;
816
- }
817
- if (typeof parsed !== 'object' || parsed === null) {
818
- reject(new Error('peaks container spawn envelope is not an object'));
819
- return;
820
- }
821
- const env = parsed;
822
- if (env.ok !== true || !env.data?.lease) {
823
- reject(new Error(`peaks container spawn envelope missing lease; got: ${stdout.slice(0, 200)}`));
824
- return;
825
- }
826
- resolve({ leaseId: env.data.lease.leaseId });
827
- });
828
- // See spawnWorktreeLease above for the rationale.
829
- child.unref();
830
- });
831
- }
832
- /**
833
- * F5 follow-up (sediment 2026-08-11-rid-001-redo-fake-green-recovery-closure
834
- * §Lesson 1): synchronous anti-fake-green file-existence gate. Runs
835
- * `git ls-files <glob>` against `projectRoot` and returns the matching
836
- * tracked file paths (relative to projectRoot). Empty array when no
837
- * files match (e.g. untracked new file, wrong glob, not a git repo).
838
- *
839
- * Why `git ls-files` and not `fs.glob`: the anti-fake-green contract
840
- * is "the file the sub-agent claims to have written must ACTUALLY be
841
- * tracked by git" — `git ls-files` enforces that contract; `fs.glob`
842
- * would happily return untracked-but-on-disk files (false-positive
843
- * for the fake-green gate).
844
- *
845
- * Failure modes (best-effort, never throws):
846
- * - git not on PATH → empty array (`ENOENT` swallowed)
847
- * - not a git repo → empty array (git exits non-zero)
848
- * - glob matches zero tracked files → empty array
849
- *
850
- * Exported for unit-test access (`tests/unit/sub-agent/must-ls-files-flag.test.ts`).
851
- * The export is intentional — the helper has zero side effects and
852
- * keeps the dispatch action handler small.
853
- */
854
- export function runGitLsFiles(projectRoot, glob) {
855
- try {
856
- const { execFileSync } = require('node:child_process');
857
- const stdout = execFileSync('git', ['ls-files', '--', glob], { cwd: projectRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
858
- return stdout.split('\n').filter((line) => line.length > 0);
859
- }
860
- catch {
861
- return [];
862
- }
863
- }
864
- /* ---------- Slice 4.0.8 RD §4 D4c: programmatic dispatcher ---------- */
865
- /**
866
- * `dispatchSubAgent` is the thin programmatic wrapper the integration
867
- * test (`tests/integration/sub-agent-graph-binding.test.ts`) imports.
868
- * It enforces --graph-node required BEFORE any record write so a
869
- * caller can't bypass the CLI's requiredOption guard. Throws a typed
870
- * error with `code = PEAKS_GRAPH_NODE_REQUIRED` when missing.
871
- */
872
- export async function dispatchSubAgent(input) {
873
- if (typeof input.graphNode !== 'string' || input.graphNode.length === 0) {
874
- const err = new Error('PEAKS_GRAPH_NODE_REQUIRED: --graph-node is required (RD §4 D4c)');
875
- err.code = 'PEAKS_GRAPH_NODE_REQUIRED';
876
- throw err;
877
- }
878
- // The integration test only checks the rejection path; the success
879
- // path is exercised by the existing CLI command. Return a minimal
880
- // stub so any future programmatic caller has a stable surface.
881
- return {
882
- role: input.role,
883
- toolCall: null,
884
- dispatchRecordPath: null,
885
- };
886
- }
@@ -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
+ }
@@ -1,5 +1,6 @@
1
1
  import { InvalidArgumentError } from 'commander';
2
- import { allowedStatesForRole, createRequestArtifact, listRequestArtifacts, showRequestArtifact, transitionRequestArtifact, PrerequisitesNotSatisfiedError, LintGateError, TypeSanityViolationError, FileSizeViolationError, VALID_REQUEST_TYPES, isRequestType } from '../../services/artifacts/request-artifact-service.js';
2
+ import { createRequestArtifact, listRequestArtifacts, showRequestArtifact, transitionRequestArtifact, PrerequisitesNotSatisfiedError, LintGateError, TypeSanityViolationError, FileSizeViolationError, VALID_REQUEST_TYPES } from '../../services/artifacts/request-artifact-service.js';
3
+ import { applyPerArtifactFormat, inferArtifactName, parseRequestType, parseRole, parseStateForRole, resolveDefaultFormat, VALID_ROLES, } from './request-format-helpers.js';
3
4
  import { ConfirmationRequiredError } from '../../services/mode/mode-enforcement.js';
4
5
  import { recordBypass, isBypassLimitReached, MAX_BYPASSES_PER_SESSION } from '../../services/mode/bypass-tracker.js';
5
6
  import { lintRequestArtifact } from '../../services/artifacts/artifact-lint-service.js';
@@ -7,102 +8,7 @@ import { getRepairCycleStatus } from '../../services/artifacts/repair-cycle-serv
7
8
  import { fail, ok } from 'peaks-loop-shared/result';
8
9
  import { triggerBestPracticeScan } from '../../services/prd/best-practice-auto-trigger.js';
9
10
  import { refreshCodegraphAfterSlice, } from '../../services/codegraph/codegraph-autorefresh.js';
10
- import { formatMdCompact } from '../../shared/format-md-compact.js';
11
11
  import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
12
- /**
13
- * Per-artifact default format. Only PRD and tech-doc are user-review
14
- * surfaces; all other RD/QA/TXT artifacts default to compact. The
15
- * `--pretty` / `--compact` flags override uniformly. See tech-doc
16
- * §1.3.
17
- */
18
- const DEFAULT_FORMAT_BY_ARTIFACT = {
19
- prd: 'pretty',
20
- 'tech-doc': 'pretty',
21
- 'code-review': 'compact',
22
- 'security-review': 'compact',
23
- 'perf-baseline': 'compact',
24
- 'bug-analysis': 'compact',
25
- 'test-cases': 'compact',
26
- 'test-reports': 'compact',
27
- 'security-findings': 'compact',
28
- 'performance-findings': 'compact',
29
- handoff: 'compact'
30
- };
31
- function resolveDefaultFormat(artifactName) {
32
- return DEFAULT_FORMAT_BY_ARTIFACT[artifactName] ?? 'compact';
33
- }
34
- function applyPerArtifactFormat(envelope, override) {
35
- if (override === null || envelope === null || typeof envelope !== 'object')
36
- return envelope;
37
- // The service returns `{ id, sessionId, role, body, ... }` for a
38
- // single-artifact show. The per-artifact `body` field is the only
39
- // thing that changes; we attach a `format` field to surface the
40
- // choice to the caller. Slice 023 (R3) AC6 / AC7.
41
- const obj = envelope;
42
- if (typeof obj.body === 'string') {
43
- return {
44
- ...obj,
45
- body: override === 'compact' ? formatMdCompact(obj.body) : obj.body,
46
- format: override
47
- };
48
- }
49
- return envelope;
50
- }
51
- /**
52
- * Map a request-artifact envelope to a `DEFAULT_FORMAT_BY_ARTIFACT` key.
53
- * The service returns the path of the artifact in `path`; the role +
54
- * filename stem gives us the artifact name. Falls back to the role
55
- * itself when no `path` field is present.
56
- */
57
- function inferArtifactName(envelope, role) {
58
- if (envelope === null || typeof envelope !== 'object')
59
- return role;
60
- const obj = envelope;
61
- const pathField = typeof obj.path === 'string' ? obj.path : '';
62
- // Path looks like `.peaks/_runtime/<sid>/<role>/requests/<file>.md`.
63
- // The role is the second-to-last directory; the file stem is the
64
- // last segment. For RD role, the artifact is one of {code-review,
65
- // security-review, perf-baseline, bug-analysis, tech-doc}; the file
66
- // stem usually carries the artifact name (e.g. `tech-doc.md`,
67
- // `code-review-002.md`). We strip the trailing `-<digits>` suffix
68
- // when present.
69
- const stem = pathField.split(/[\\/]/).pop()?.replace(/\.md$/, '') ?? '';
70
- if (stem.length > 0) {
71
- // Try the stem verbatim first.
72
- if (DEFAULT_FORMAT_BY_ARTIFACT[stem] !== undefined)
73
- return stem;
74
- // Strip a trailing -<digits> (e.g. code-review-002 -> code-review).
75
- const trimmed = stem.replace(/-\d+$/, '');
76
- if (DEFAULT_FORMAT_BY_ARTIFACT[trimmed] !== undefined)
77
- return trimmed;
78
- // Last-ditch: match by prefix.
79
- for (const key of Object.keys(DEFAULT_FORMAT_BY_ARTIFACT)) {
80
- if (stem.startsWith(key))
81
- return key;
82
- }
83
- }
84
- return role;
85
- }
86
- const VALID_ROLES = ['prd', 'ui', 'rd', 'qa', 'sc'];
87
- function parseRole(value) {
88
- if (!VALID_ROLES.includes(value)) {
89
- throw new InvalidArgumentError(`must be one of ${VALID_ROLES.join(', ')}`);
90
- }
91
- return value;
92
- }
93
- function parseStateForRole(role, value) {
94
- const allowed = allowedStatesForRole(role);
95
- if (!allowed.includes(value)) {
96
- throw new InvalidArgumentError(`must be one of ${allowed.join(', ')} for role ${role}`);
97
- }
98
- return value;
99
- }
100
- function parseRequestType(value) {
101
- if (!isRequestType(value)) {
102
- throw new InvalidArgumentError(`must be one of ${VALID_REQUEST_TYPES.join(', ')}`);
103
- }
104
- return value;
105
- }
106
12
  export function registerRequestCommands(program, io) {
107
13
  const request = program.command('request').description('Manage per-request Peaks role artifacts (PRD / UI / RD / QA)');
108
14
  addJsonOption(request
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Format / parse helpers extracted from `request-commands.ts`
3
+ * (slice 2026-09-06-split-batch-b) so the command file stays under the
4
+ * 800-line cap. Behaviour-preserving verbatim move.
5
+ */
6
+ import { type RequestArtifactRole, type RequestArtifactState, type RequestType } from '../../services/artifacts/request-artifact-service.js';
7
+ export declare function resolveDefaultFormat(artifactName: string): 'compact' | 'pretty';
8
+ export declare function applyPerArtifactFormat(envelope: unknown, override: 'pretty' | 'compact' | null): unknown;
9
+ /**
10
+ * Map a request-artifact envelope to a `DEFAULT_FORMAT_BY_ARTIFACT` key.
11
+ * The service returns the path of the artifact in `path`; the role +
12
+ * filename stem gives us the artifact name. Falls back to the role
13
+ * itself when no `path` field is present.
14
+ */
15
+ export declare function inferArtifactName(envelope: unknown, role: string): string;
16
+ export declare const VALID_ROLES: ReadonlyArray<RequestArtifactRole>;
17
+ export declare function parseRole(value: string): RequestArtifactRole;
18
+ export declare function parseStateForRole(role: RequestArtifactRole, value: string): RequestArtifactState;
19
+ export declare function parseRequestType(value: string): RequestType;