peaks-loop 4.0.35 → 4.0.37

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 (128) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README-en.md +1 -1
  3. package/README.md +1 -1
  4. package/bin/peaks.js +71 -1
  5. package/dist/cli/cli-helpers.js +7 -0
  6. package/dist/cli/commands/_register.js +2 -0
  7. package/dist/cli/commands/best-practice-scan-command.d.ts +14 -1
  8. package/dist/cli/commands/best-practice-scan-command.js +67 -9
  9. package/dist/cli/commands/code-runtime-commands.d.ts +5 -2
  10. package/dist/cli/commands/code-runtime-commands.js +78 -7
  11. package/dist/cli/commands/core/doctor-command.d.ts +8 -0
  12. package/dist/cli/commands/core/doctor-command.js +44 -2
  13. package/dist/cli/commands/core/memory-command.js +5 -1
  14. package/dist/cli/commands/dispatch-commands.js +15 -3
  15. package/dist/cli/commands/dispatch-from-dag.js +17 -0
  16. package/dist/cli/commands/hooks-commands.js +10 -1
  17. package/dist/cli/commands/job-commands.js +107 -25
  18. package/dist/cli/commands/memory-commands.d.ts +24 -0
  19. package/dist/cli/commands/memory-commands.js +77 -10
  20. package/dist/cli/commands/request-commands.d.ts +8 -0
  21. package/dist/cli/commands/request-commands.js +23 -2
  22. package/dist/cli/commands/scan-commands.js +1 -1
  23. package/dist/cli/commands/sub-agent-commands.js +2 -0
  24. package/dist/cli/commands/wave-plan-commands.d.ts +24 -0
  25. package/dist/cli/commands/wave-plan-commands.js +93 -0
  26. package/dist/cli/commands/web-commands.d.ts +28 -0
  27. package/dist/cli/commands/web-commands.js +327 -0
  28. package/dist/cli/commands/web-lifecycle-commands.d.ts +49 -0
  29. package/dist/cli/commands/web-lifecycle-commands.js +321 -0
  30. package/dist/services/best-practice/scan-orchestrator.d.ts +22 -0
  31. package/dist/services/best-practice/scan-orchestrator.js +14 -5
  32. package/dist/services/code/orchestrator-can-do.js +27 -4
  33. package/dist/services/context/build-dispatch-system-prompt.d.ts +66 -9
  34. package/dist/services/context/build-dispatch-system-prompt.js +132 -17
  35. package/dist/services/context/context-audit-hint.d.ts +79 -0
  36. package/dist/services/context/context-audit-hint.js +150 -0
  37. package/dist/services/context/context-audit.d.ts +100 -0
  38. package/dist/services/context/context-audit.js +322 -0
  39. package/dist/services/context/summary-view.d.ts +54 -0
  40. package/dist/services/context/summary-view.js +114 -0
  41. package/dist/services/dispatch/file-overlap-wave-planner.d.ts +70 -0
  42. package/dist/services/dispatch/file-overlap-wave-planner.js +119 -0
  43. package/dist/services/dispatch/session-capsule.d.ts +23 -0
  44. package/dist/services/dispatch/session-capsule.js +56 -0
  45. package/dist/services/dispatch/slice-dag.d.ts +9 -0
  46. package/dist/services/dispatch/slice-dag.js +9 -1
  47. package/dist/services/dispatch/test-tool-detection.d.ts +12 -1
  48. package/dist/services/dispatch/test-tool-detection.js +14 -13
  49. package/dist/services/hooks/auto-compact-hook-install.js +10 -1
  50. package/dist/services/hooks/write-gate.js +88 -0
  51. package/dist/services/ide/adapters/claude-code-adapter.d.ts +10 -0
  52. package/dist/services/ide/adapters/claude-code-adapter.js +20 -1
  53. package/dist/services/ide/ide-types.d.ts +15 -0
  54. package/dist/services/lint/detect-eslint.d.ts +2 -0
  55. package/dist/services/lint/detect-eslint.js +23 -9
  56. package/dist/services/lint/npx-resolver.d.ts +6 -0
  57. package/dist/services/lint/npx-resolver.js +38 -14
  58. package/dist/services/memory/project-memory-service/parsers/frontmatter.d.ts +5 -0
  59. package/dist/services/memory/project-memory-service/parsers/frontmatter.js +55 -5
  60. package/dist/services/release/version-precheck-service.js +9 -2
  61. package/dist/services/scan/file-size-scan.d.ts +29 -0
  62. package/dist/services/scan/file-size-scan.js +63 -0
  63. package/dist/services/session/caller-binding-service.d.ts +24 -0
  64. package/dist/services/session/caller-binding-service.js +34 -0
  65. package/dist/services/session/getSessionDir.js +15 -10
  66. package/dist/services/skills/hooks-codegate-superpowers.d.ts +33 -0
  67. package/dist/services/skills/hooks-codegate-superpowers.js +34 -3
  68. package/dist/services/skills/hooks-settings-service.d.ts +10 -0
  69. package/dist/services/skills/hooks-settings-service.js +152 -61
  70. package/dist/services/slice/slice-check-service.d.ts +14 -0
  71. package/dist/services/slice/slice-check-service.js +110 -50
  72. package/dist/services/slice/slice-check-types.d.ts +12 -7
  73. package/dist/services/slice/slice-check-types.js +8 -3
  74. package/dist/services/slice/slice-decompose-runners.js +24 -21
  75. package/dist/services/sop/sop-check-service.js +12 -1
  76. package/dist/services/web/bounded-output.d.ts +34 -0
  77. package/dist/services/web/bounded-output.js +68 -0
  78. package/dist/services/web/browser-acquire.d.ts +14 -0
  79. package/dist/services/web/browser-acquire.js +84 -0
  80. package/dist/services/web/browser-session-manager.d.ts +111 -0
  81. package/dist/services/web/browser-session-manager.js +413 -0
  82. package/dist/services/web/daemon-entry.d.ts +1 -0
  83. package/dist/services/web/daemon-entry.js +65 -0
  84. package/dist/services/web/daemon-registry.d.ts +42 -0
  85. package/dist/services/web/daemon-registry.js +164 -0
  86. package/dist/services/web/daemon-supervisor.d.ts +144 -0
  87. package/dist/services/web/daemon-supervisor.js +455 -0
  88. package/dist/services/web/playwright-loader.d.ts +89 -0
  89. package/dist/services/web/playwright-loader.js +253 -0
  90. package/dist/services/web/snapshot-pruner.d.ts +48 -0
  91. package/dist/services/web/snapshot-pruner.js +241 -0
  92. package/dist/services/web/untrusted-envelope.d.ts +27 -0
  93. package/dist/services/web/untrusted-envelope.js +44 -0
  94. package/dist/services/web/web-artifact-paths.d.ts +79 -0
  95. package/dist/services/web/web-artifact-paths.js +163 -0
  96. package/dist/services/web/web-client.d.ts +19 -0
  97. package/dist/services/web/web-client.js +55 -0
  98. package/dist/services/web/web-daemon-service.d.ts +38 -0
  99. package/dist/services/web/web-daemon-service.js +416 -0
  100. package/dist/services/web/web-fallback.d.ts +70 -0
  101. package/dist/services/web/web-fallback.js +121 -0
  102. package/dist/services/web/web-install-service.d.ts +91 -0
  103. package/dist/services/web/web-install-service.js +346 -0
  104. package/dist/services/web/web-login-profile.d.ts +89 -0
  105. package/dist/services/web/web-login-profile.js +612 -0
  106. package/dist/services/web/web-login-staging.d.ts +27 -0
  107. package/dist/services/web/web-login-staging.js +173 -0
  108. package/dist/services/web/web-protocol.d.ts +58 -0
  109. package/dist/services/web/web-protocol.js +58 -0
  110. package/dist/services/web/web-status-report.d.ts +33 -0
  111. package/dist/services/web/web-status-report.js +47 -0
  112. package/dist/services/workspace/claude-settings-template.d.ts +41 -5
  113. package/dist/services/workspace/claude-settings-template.js +116 -64
  114. package/dist/services/workspace/workspace-claude-settings-materializer.js +5 -1
  115. package/dist/services/workspace/workspace-service.js +33 -0
  116. package/package.json +5 -5
  117. package/scripts/copy-templates.mjs +12 -0
  118. package/scripts/sync-version.mjs +20 -0
  119. package/skills/bee/peaks-qa/SKILL.md +2 -0
  120. package/skills/bee/peaks-qa/references/qa-sub-agent-dispatch.md +12 -0
  121. package/skills/bee/peaks-rd/SKILL.md +2 -0
  122. package/skills/bee/peaks-rd/references/rd-sub-agent-dispatch.md +14 -0
  123. package/skills/bee/peaks-txt/SKILL.md +2 -0
  124. package/skills/bee/peaks-ui/SKILL.md +2 -0
  125. package/skills/peaks-code/SKILL.md +18 -0
  126. package/skills/peaks-code/references/browser-workflow.md +10 -1
  127. package/skills/peaks-code/references/context-governance.md +29 -0
  128. package/skills/peaks-doctor/SKILL.md +2 -0
@@ -35,9 +35,10 @@ import { DEFAULT_PREFERENCES } from '../../services/preferences/preferences-type
35
35
  import { writeLogEntry } from '../../services/log/logger.js';
36
36
  import { PROMPT_LIMIT_BYTES, RECOMMENDED_ROLES, validateRole } from './sub-agent-shared.js';
37
37
  import { runDispatchFromDag } from './dispatch-from-dag.js';
38
- import { TEST_TOOL_DETECTION_BLOCK, formatTestToolDetection } from '../../services/dispatch/test-tool-detection.js';
38
+ import { TEST_TOOL_DETECTION_BLOCK } from '../../services/dispatch/test-tool-detection.js';
39
39
  import { MemoryPreflightService, deriveMemoryQuery } from '../../services/context/memory-preflight-service.js';
40
40
  import { buildDispatchSystemPrompt } from '../../services/context/build-dispatch-system-prompt.js';
41
+ import { readSessionCapsule } from '../../services/dispatch/session-capsule.js';
41
42
  import { computeUiLibraryDispatchBlock } from '../../services/standards/ui-library-dispatch-block.js';
42
43
  import { readFreshContextBlock } from '../../services/fresh-context/fresh-context-block.js';
43
44
  import { createDispatchProvenanceToken, DISPATCH_PROVENANCE_ENV, writeDispatchProvenance, } from '../../services/worktree/dispatch-provenance.js';
@@ -443,10 +444,17 @@ export function registerDispatchCommand(parent, io) {
443
444
  // "no probe available" hint instead of failing the
444
445
  // dispatch.
445
446
  }
447
+ // Slice 2026-09-10-dispatch-token-and-swarm §4: advisory background
448
+ // capsule published by the orchestrator via `peaks sub-agent share`.
449
+ // Absent capsule → no pointer and no precedence line (byte-identical
450
+ // legacy prompt).
451
+ const capsuleRef = readSessionCapsule({ projectRoot, sid, rid });
446
452
  const memoryAugmentedBody = buildDispatchSystemPrompt({
447
453
  taskTitle: role,
448
454
  taskBody: options.prompt,
449
455
  memoryBlock,
456
+ // 2026-09-10-dispatch-block-d (Option D): the composer owns the ONE
457
+ // unified Test Tool Detection injection for every role.
450
458
  contextProbe,
451
459
  // exactOptionalPropertyTypes: only set codegraphBlock when the rd
452
460
  // preflight actually produced a value (null = attempted-unavailable,
@@ -459,7 +467,11 @@ export function registerDispatchCommand(parent, io) {
459
467
  // Same optionality contract for the fresh-context block: set only
460
468
  // when the rd/prd read ran (null = no block on disk → no block,
461
469
  // undefined = non-rd/prd role → legacy prompt unchanged).
462
- ...(freshContextBlock !== undefined ? { freshContextBlock } : {})
470
+ ...(freshContextBlock !== undefined ? { freshContextBlock } : {}),
471
+ // §4: advisory session capsule pointer + precedence line.
472
+ ...(capsuleRef !== null
473
+ ? { capsule: { batchId: capsuleRef.batchId, key: capsuleRef.key, bytes: capsuleRef.bytes } }
474
+ : {})
463
475
  });
464
476
  // Part 2.C: when --isolation worktree, prepend an isolation envelope
465
477
  // block so the sub-agent sees the lease id + worktree path. The block
@@ -498,7 +510,7 @@ export function registerDispatchCommand(parent, io) {
498
510
  `confirm the file exists. Anti-fake-green rule (sediment 2026-08-11-rid-001-redo-fake-green-recovery-closure §Lesson 1): ` +
499
511
  `if the file does not exist, your verdict MUST be \`status: "blocked"\` with reason "must_ls_files_failed". Do NOT silently skip this step.\n`;
500
512
  }
501
- const effectivePrompt = `${formatTestToolDetection()}\n\n${memoryAugmentedBody}${isolationBlock}${mustLsFilesBlock}`;
513
+ const effectivePrompt = `${memoryAugmentedBody}${isolationBlock}${mustLsFilesBlock}`;
502
514
  const warnings = [...decision.warnings];
503
515
  let toolCall;
504
516
  try {
@@ -32,6 +32,7 @@ import { fail, ok } from 'peaks-loop-shared/result';
32
32
  import { detectInstalledIde } from '../../services/ide/ide-detector.js';
33
33
  import { getAdapter } from '../../services/ide/ide-registry.js';
34
34
  import { getCurrentSessionId } from '../../services/skills/skill-presence-service.js';
35
+ import { planFileOverlapWaves } from '../../services/dispatch/file-overlap-wave-planner.js';
35
36
  export async function runDispatchFromDag(role, options, asJson, io) {
36
37
  if (!options.fromDag)
37
38
  return;
@@ -135,6 +136,19 @@ export async function runDispatchFromDag(role, options, asJson, io) {
135
136
  // end-to-end, but they are NOT surfaced in the CLI envelope (the LLM
136
137
  // sees them only after re-invoking with fresh level-1 contracts).
137
138
  const firstLevelIds = new Set(levelArr[0] ?? []);
139
+ // Slice 2026-09-10-dispatch-token-and-swarm §3: when EVERY first-level
140
+ // node declares `files`, refine the level into file-overlap waves so the
141
+ // LLM fans out without serializing on a shared file. Emitted additively
142
+ // in the envelope; the topological dispatch flow itself is unchanged.
143
+ // Nodes without `files` → `firstLevelWaves: null` (legacy envelope).
144
+ const firstLevelDescriptors = [...firstLevelIds].map((id) => ({
145
+ id,
146
+ files: dag.nodes.find((n) => n.id === id)?.files ?? []
147
+ }));
148
+ const firstLevelWaves = firstLevelDescriptors.length > 0
149
+ && firstLevelDescriptors.every((d) => d.files.length > 0)
150
+ ? planFileOverlapWaves(firstLevelDescriptors).waves
151
+ : null;
138
152
  const emittedToolCalls = [];
139
153
  const emittedSliceIds = [];
140
154
  // CLI runner: emit the per-slice `buildToolCall` envelope, then return
@@ -240,6 +254,9 @@ export async function runDispatchFromDag(role, options, asJson, io) {
240
254
  dispatchCount: emittedSliceIds.length,
241
255
  levelsTotal: levelArr.length,
242
256
  firstLevel: emittedSliceIds,
257
+ // §3: file-overlap wave plan for the first level (null when any
258
+ // first-level node omits `files`). Additive field.
259
+ firstLevelWaves,
243
260
  toolCalls: emittedToolCalls,
244
261
  existingContractCount: existingContracts.length,
245
262
  expectedCompletionSeconds: 60,
@@ -300,6 +300,12 @@ export function registerHooksCommands(program, io) {
300
300
  // install left behind.
301
301
  const settingsPath = status.settingsPath;
302
302
  const settings = existsSync(settingsPath) ? readJsonObjectFile(settingsPath) : {};
303
+ // The gate-enforce entry is materialized into the machine-local
304
+ // settings file (see `resolveHookTargets`), so the on-disk entry list
305
+ // must be read from both files or `status` would report it missing.
306
+ const localSettings = status.localSettingsPath !== undefined && existsSync(status.localSettingsPath)
307
+ ? readJsonObjectFile(status.localSettingsPath)
308
+ : {};
303
309
  // Slice 2026-07-29-worktree-layer3-deny: report the Layer 3 deny
304
310
  // entries actually on disk. We read the existing settings.json
305
311
  // (above) and surface its `permissions.deny` block. Any entry
@@ -311,7 +317,10 @@ export function registerHooksCommands(program, io) {
311
317
  printResult(io, ok('hooks.status', {
312
318
  ...status,
313
319
  ide,
314
- entries: readInstalledEntriesFromSettings(settings, ide),
320
+ entries: [
321
+ ...readInstalledEntriesFromSettings(settings, ide),
322
+ ...readInstalledEntriesFromSettings(localSettings, ide)
323
+ ],
315
324
  permissionsDenyEntries: listSuperpowersDenyEntries(),
316
325
  permissionsDenyOnDisk: onDiskDeny
317
326
  }), options.json);
@@ -1,4 +1,5 @@
1
1
  // src/cli/commands/job-commands.ts
2
+ import { existsSync, readdirSync } from 'node:fs';
2
3
  import { join } from 'node:path';
3
4
  import { Command } from 'commander';
4
5
  import { fail, ok } from 'peaks-loop-shared/result';
@@ -18,6 +19,30 @@ function projectRoot(opts) {
18
19
  // Reuse the workspace root resolver from peaks CLI; for now, CWD as a safe placeholder.
19
20
  return opts.project ?? process.cwd();
20
21
  }
22
+ /**
23
+ * D6: every job subcommand resolves a job root, so every one of them must be
24
+ * able to name the session that holds the job. Same precedence as the rest of
25
+ * the CLI (`peaks sub-agent dispatch`, `peaks web *`, `peaks share *`), except
26
+ * that job state has no "unknown-sid" location to land in — an unresolvable
27
+ * session is an error, not a silent fallback.
28
+ */
29
+ const SESSION_ID_HELP = 'session id (default: resolve from .peaks/_runtime/session.json; falls back to PEAKS_SESSION_ID env var; final fallback: NO_ACTIVE_SESSION error)';
30
+ /**
31
+ * The session (a direct child of `<project>/.peaks/_runtime/`) that holds
32
+ * `jobId`, or null. Only used to explain a miss: a job that lives in another
33
+ * session must be reported by name so the caller can re-run with --session-id.
34
+ */
35
+ function findSessionHoldingJob(project, jobId) {
36
+ const runtimeDir = join(project, '.peaks', '_runtime');
37
+ if (!existsSync(runtimeDir))
38
+ return null;
39
+ for (const entry of readdirSync(runtimeDir, { withFileTypes: true })) {
40
+ if (entry.isDirectory() && existsSync(join(runtimeDir, entry.name, 'job', jobId, 'state.json'))) {
41
+ return entry.name;
42
+ }
43
+ }
44
+ return null;
45
+ }
21
46
  /**
22
47
  * Resolves the on-disk root for Job state files.
23
48
  *
@@ -27,20 +52,50 @@ function projectRoot(opts) {
27
52
  * The `JobStateStore` itself only knows its `rootDir` + `jobId` and joins them. We
28
53
  * compute the canonical root here (per-call) so the store can stay layout-agnostic.
29
54
  *
30
- * Resolution order:
55
+ * Resolution order (D6 — a job must stay addressable while the single per-project
56
+ * `.peaks/_runtime/session.json` binding points at another session):
31
57
  * 1. `--session-id` flag (explicit override)
32
- * 2. `getCurrentSessionId(project)` — reads `.peaks/_runtime/session.json` per peaks-code
33
- * 3. Error (NO_ACTIVE_SESSION) — must never silently fall back to a random uuid
58
+ * 2. `PEAKS_SESSION_ID` env var
59
+ * 3. `getCurrentSessionId(project)` — reads `.peaks/_runtime/session.json`
60
+ * 4. Error (NO_ACTIVE_SESSION) — must never silently fall back to a random uuid
61
+ *
62
+ * When `jobId` is passed and it is absent from the resolved session, the thrown
63
+ * error names the session that does hold it (if any), instead of leaving the
64
+ * caller with a bare "no state for <job> at <other-sid>" path.
34
65
  */
35
- function resolveJobStateRoot(opts) {
66
+ function resolveJobStateRoot(opts, jobId) {
36
67
  const project = projectRoot(opts);
37
- const sessionId = opts.sessionId ?? getCurrentSessionId(project);
68
+ const sessionId = opts.sessionId ?? process.env.PEAKS_SESSION_ID ?? getCurrentSessionId(project);
38
69
  if (!sessionId) {
39
70
  throw new Error('NO_ACTIVE_SESSION: peaks job requires --session-id or an active peaks-code session via peaks workspace init');
40
71
  }
41
72
  const rootDir = join(project, '.peaks', '_runtime', sessionId, 'job');
73
+ if (jobId && !existsSync(join(rootDir, jobId, 'state.json'))) {
74
+ const other = findSessionHoldingJob(project, jobId);
75
+ throw new Error(other
76
+ ? `JOB_NOT_IN_SESSION: no job "${jobId}" in session "${sessionId}"; it lives in session "${other}" — re-run with --session-id ${other}`
77
+ : `JOB_NOT_IN_SESSION: no job "${jobId}" in session "${sessionId}" (and no other session under .peaks/_runtime/ has it)`);
78
+ }
42
79
  return { rootDir, sessionId, projectRoot: project };
43
80
  }
81
+ /**
82
+ * D7: slices are keyed `slice-NNN`, but `peaks job init --slice-list "S1,…"`
83
+ * takes labels, so the natural string to pass back to `--slice-id` is that same
84
+ * label. Resolve an exact sliceId OR an exact label to the canonical sliceId;
85
+ * anything else is a hard error listing the valid ids, so a mistyped
86
+ * `slice-04` can never be accepted as a silent no-op that leaves the slice
87
+ * pending.
88
+ */
89
+ function resolveSliceId(store, jobId, sliceId) {
90
+ const slices = store.load(jobId).slices;
91
+ const hit = slices.find((sl) => sl.sliceId === sliceId || sl.label === sliceId);
92
+ if (hit)
93
+ return { sliceId: hit.sliceId };
94
+ return {
95
+ message: `no slice "${sliceId}" in job ${jobId}; valid ids: ${slices.map((sl) => `${sl.sliceId} (${sl.label})`).join(', ')}`,
96
+ validSliceIds: slices.map((sl) => sl.sliceId),
97
+ };
98
+ }
44
99
  export function registerJobCommands(program, io = { stdout: (t) => process.stdout.write(t), stderr: (t) => process.stderr.write(t) }) {
45
100
  const job = new Command('job').description('Drive long multi-slice work as one Job (peaks-code Step 0.8+)');
46
101
  job
@@ -51,14 +106,14 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
51
106
  .option('--exit-policy <strict|best-effort>', 'strict')
52
107
  .option('--main-loop-strategy <single|rotating>', 'rotating')
53
108
  .option('--rotate-every <n>', 'rotate every N slices (rotating mode)', '3')
54
- .option('--session-id <sid>', 'session id (default: read from .peaks/_runtime/session.json; required to land in the 2.7.1 single-scope-axis layout)')
109
+ .option('--session-id <sid>', SESSION_ID_HELP)
55
110
  .option('--project <repo>')
56
111
  .action(async (opts) => {
57
112
  const project = projectRoot(opts);
58
- // Resolve sessionId: explicit flag > canonical session binding > FAIL.
113
+ // Resolve sessionId: explicit flag > PEAKS_SESSION_ID > canonical session binding > FAIL.
59
114
  // Per spec §3.3, Job state lives at .peaks/_runtime/<sessionId>/job/<jobId>/state.json —
60
115
  // a random UUID would scatter state across dirs and break resume/auto-compact.
61
- let sessionId = opts.sessionId ?? getCurrentSessionId(project);
116
+ let sessionId = opts.sessionId ?? process.env.PEAKS_SESSION_ID ?? getCurrentSessionId(project);
62
117
  if (!sessionId) {
63
118
  return printResult(io, fail('init', 'NO_ACTIVE_SESSION', 'peaks job init requires --session-id (or an active peaks-code session via peaks workspace init)', { project }, [
64
119
  'Re-run with --session-id <sid>',
@@ -105,9 +160,10 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
105
160
  .requiredOption('--job-id <jid>')
106
161
  .option('--watch', 'poll every 3s')
107
162
  .option('--show-cost', 'overlay cost from peaks budget')
163
+ .option('--session-id <sid>', SESSION_ID_HELP)
108
164
  .option('--project <repo>')
109
165
  .action(async (opts) => {
110
- const store = new JobStateStore(resolveJobStateRoot(opts).rootDir);
166
+ const store = new JobStateStore(resolveJobStateRoot(opts, opts.jobId).rootDir);
111
167
  const orch = new JobOrchestrator(store);
112
168
  const s = orch.status(opts.jobId);
113
169
  if (opts.watch) {
@@ -134,9 +190,10 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
134
190
  // M4.2: wire rotate-now to JobRotation (session-rotate callbacks are stubs pending M6.5 batch-fix).
135
191
  job.command('rotate-now')
136
192
  .requiredOption('--job-id <jid>')
193
+ .option('--session-id <sid>', SESSION_ID_HELP)
137
194
  .option('--project <repo>')
138
195
  .action(async (opts) => {
139
- const store = new JobStateStore(resolveJobStateRoot(opts).rootDir);
196
+ const store = new JobStateStore(resolveJobStateRoot(opts, opts.jobId).rootDir);
140
197
  const rotation = new JobRotation(store, async (_jid) => { /* delegate to peaks session rotate — implementation wired in M6.5 batch-fix */ return { rotated: true }; }, async (jid) => ({ jobId: jid, cycle: 0 }));
141
198
  const r = await rotation.rotateNow(opts.jobId);
142
199
  printResult(io, ok('rotate-now', r), opts);
@@ -146,9 +203,10 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
146
203
  .requiredOption('--job-id <jid>')
147
204
  .requiredOption('--batch-id <bid>')
148
205
  .option('--force')
206
+ .option('--session-id <sid>', SESSION_ID_HELP)
149
207
  .option('--project <repo>')
150
208
  .action(async (opts) => {
151
- const wrapper = new SubAgentJobWrapper(new JobStateStore(resolveJobStateRoot(opts).rootDir), async () => ({ batchId: opts.batchId }));
209
+ const wrapper = new SubAgentJobWrapper(new JobStateStore(resolveJobStateRoot(opts, opts.jobId).rootDir), async () => ({ batchId: opts.batchId }));
152
210
  const r = await wrapper.cleanup({ jobId: opts.jobId, batchId: opts.batchId, force: !!opts.force });
153
211
  printResult(io, ok('subagent-cleanup', r), opts);
154
212
  });
@@ -161,6 +219,7 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
161
219
  .requiredOption('--state <done|failed|skipped>')
162
220
  .option('--commit-sha <sha>')
163
221
  .option('--reason <text>')
222
+ .option('--session-id <sid>', SESSION_ID_HELP)
164
223
  .option('--project <repo>')
165
224
  .action(async (opts) => {
166
225
  const parsed = JobCheckpointInputSchema.safeParse({
@@ -170,15 +229,25 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
170
229
  });
171
230
  if (!parsed.success)
172
231
  return printResult(io, fail('checkpoint', 'INVALID_CHECKPOINT', parsed.error.message, {}), opts);
173
- const jobRoot = resolveJobStateRoot(opts);
232
+ const jobRoot = resolveJobStateRoot(opts, opts.jobId);
174
233
  const store = new JobStateStore(jobRoot.rootDir);
234
+ // D7: `--slice-id` accepts the canonical `slice-NNN` or the slice's label
235
+ // ("S1"); an id that matches no slice is rejected here, BEFORE any write,
236
+ // so progress.json is never touched by a checkpoint that matched nothing.
237
+ const slice = resolveSliceId(store, parsed.data.jobId, parsed.data.sliceId);
238
+ if ('message' in slice) {
239
+ return printResult(io, fail('checkpoint', 'SLICE_NOT_FOUND', slice.message, {
240
+ jobId: parsed.data.jobId, sliceId: parsed.data.sliceId, validSliceIds: slice.validSliceIds,
241
+ }, ['Re-run with one of the valid slice ids']), opts);
242
+ }
243
+ const sliceId = slice.sliceId;
175
244
  const orch = new JobOrchestrator(store);
176
245
  // 2026-09-03-codegraph-autorefresh: set on --state done so the ok
177
246
  // envelope carries a non-blocking `codegraph` result; null for
178
247
  // failed/skipped (no slice-complete boundary).
179
248
  let codegraph = null;
180
249
  if (parsed.data.state === 'done') {
181
- await orch.checkpointDone({ jobId: parsed.data.jobId, sliceId: parsed.data.sliceId, ...(parsed.data.commitSha ? { commitSha: parsed.data.commitSha } : {}) });
250
+ await orch.checkpointDone({ jobId: parsed.data.jobId, sliceId, ...(parsed.data.commitSha ? { commitSha: parsed.data.commitSha } : {}) });
182
251
  // v3.1.2: after each --state done, mirror slice progress to
183
252
  // .peaks/_runtime/<sessionId>/job/<jid>/progress.json so the
184
253
  // next LLM turn (or peaks code gate-step-08 hook) can read it.
@@ -203,12 +272,12 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
203
272
  }
204
273
  }
205
274
  else if (parsed.data.state === 'skipped') {
206
- await orch.checkpointSkipped({ jobId: parsed.data.jobId, sliceId: parsed.data.sliceId, reason: parsed.data.reason });
275
+ await orch.checkpointSkipped({ jobId: parsed.data.jobId, sliceId, reason: parsed.data.reason });
207
276
  }
208
277
  else {
209
- await orch.checkpointFailed({ jobId: parsed.data.jobId, sliceId: parsed.data.sliceId, reason: parsed.data.reason });
278
+ await orch.checkpointFailed({ jobId: parsed.data.jobId, sliceId, reason: parsed.data.reason });
210
279
  }
211
- printResult(io, ok('checkpoint', { sliceId: parsed.data.sliceId, status: parsed.data.state, codegraph }), opts);
280
+ printResult(io, ok('checkpoint', { sliceId, status: parsed.data.state, codegraph }), opts);
212
281
  });
213
282
  addJsonOption(job.commands.find(c => c.name() === 'checkpoint'));
214
283
  job
@@ -216,6 +285,7 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
216
285
  .requiredOption('--job-id <jid>')
217
286
  .requiredOption('--slice-id <rid>')
218
287
  .requiredOption('--reason <text>')
288
+ .option('--session-id <sid>', SESSION_ID_HELP)
219
289
  .option('--project <repo>')
220
290
  .action(async (opts) => {
221
291
  const parsed = JobBlockInputSchema.safeParse({
@@ -224,18 +294,27 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
224
294
  });
225
295
  if (!parsed.success)
226
296
  return printResult(io, fail('block', 'INVALID_BLOCK', parsed.error.message, {}), opts);
227
- const store = new JobStateStore(resolveJobStateRoot(opts).rootDir);
297
+ const store = new JobStateStore(resolveJobStateRoot(opts, opts.jobId).rootDir);
298
+ // D7 (same silent no-op as checkpoint): resolve label → sliceId, reject a
299
+ // miss before any write.
300
+ const slice = resolveSliceId(store, parsed.data.jobId, parsed.data.sliceId);
301
+ if ('message' in slice) {
302
+ return printResult(io, fail('block', 'SLICE_NOT_FOUND', slice.message, {
303
+ jobId: parsed.data.jobId, sliceId: parsed.data.sliceId, validSliceIds: slice.validSliceIds,
304
+ }, ['Re-run with one of the valid slice ids']), opts);
305
+ }
228
306
  const orch = new JobOrchestrator(store);
229
- await orch.blockSlice(parsed.data);
230
- printResult(io, ok('block', { blocked: parsed.data.sliceId, reason: parsed.data.reason }), opts);
307
+ await orch.blockSlice({ ...parsed.data, sliceId: slice.sliceId });
308
+ printResult(io, ok('block', { blocked: slice.sliceId, reason: parsed.data.reason }), opts);
231
309
  });
232
310
  addJsonOption(job.commands.find(c => c.name() === 'block'));
233
311
  job
234
312
  .command('continue')
235
313
  .requiredOption('--job-id <jid>')
314
+ .option('--session-id <sid>', SESSION_ID_HELP)
236
315
  .option('--project <repo>')
237
316
  .action(async (opts) => {
238
- const store = new JobStateStore(resolveJobStateRoot(opts).rootDir);
317
+ const store = new JobStateStore(resolveJobStateRoot(opts, opts.jobId).rootDir);
239
318
  const orch = new JobOrchestrator(store);
240
319
  const r = orch.continueNow(opts.jobId);
241
320
  printResult(io, ok('continue', r), opts);
@@ -244,9 +323,10 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
244
323
  job
245
324
  .command('resume')
246
325
  .requiredOption('--job-id <jid>')
326
+ .option('--session-id <sid>', SESSION_ID_HELP)
247
327
  .option('--project <repo>')
248
328
  .action(async (opts) => {
249
- const store = new JobStateStore(resolveJobStateRoot(opts).rootDir);
329
+ const store = new JobStateStore(resolveJobStateRoot(opts, opts.jobId).rootDir);
250
330
  const orch = new JobOrchestrator(store);
251
331
  const s = orch.status(opts.jobId);
252
332
  printResult(io, ok('resume', { resumed: opts.jobId, ...s }), opts);
@@ -261,11 +341,12 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
261
341
  .description('v3.1.2: read the on-disk slice progress mirror (.peaks/_runtime/<sid>/job/<jid>/progress.json). ' +
262
342
  'Returns { jobId, done, total, currentSlice, lastCommitSha, updatedAt }.')
263
343
  .requiredOption('--job-id <jid>')
344
+ .option('--session-id <sid>', SESSION_ID_HELP)
264
345
  .option('--project <repo>')
265
346
  .option('--allow-missing', 'return done=0/total=0 envelope instead of failing when progress.json is absent')
266
347
  .action(async (opts) => {
267
348
  try {
268
- const jobRoot = resolveJobStateRoot(opts);
349
+ const jobRoot = resolveJobStateRoot(opts, opts.jobId);
269
350
  const sessId = jobRoot.sessionId;
270
351
  const project = projectRoot(opts);
271
352
  const progress = opts.allowMissing === true
@@ -292,9 +373,10 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
292
373
  job
293
374
  .command('handoff')
294
375
  .requiredOption('--job-id <jid>')
376
+ .option('--session-id <sid>', SESSION_ID_HELP)
295
377
  .option('--project <repo>')
296
378
  .action(async (opts) => {
297
- const store = new JobStateStore(resolveJobStateRoot(opts).rootDir);
379
+ const store = new JobStateStore(resolveJobStateRoot(opts, opts.jobId).rootDir);
298
380
  const orch = new JobOrchestrator(store);
299
381
  const s = orch.status(opts.jobId);
300
382
  printResult(io, ok('handoff', { handoffFor: opts.jobId, ...s }), opts);
@@ -305,10 +387,10 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
305
387
  .description('Read the slice\'s rd/karpathy-review.md and decide whether to downgrade a block gateAction to warn (slice 2026-07-30-karpathy-cost-self-review).')
306
388
  .requiredOption('--review-file <path>', 'path to rd/karpathy-review.md (or its .json sibling if the file is JSON)')
307
389
  .option('--project <repo>')
308
- .option('--session-id <sid>')
390
+ .option('--session-id <sid>', SESSION_ID_HELP)
309
391
  .action(async (opts) => {
310
392
  const project = projectRoot(opts);
311
- const sessionId = opts.sessionId ?? getCurrentSessionId(project);
393
+ const sessionId = opts.sessionId ?? process.env.PEAKS_SESSION_ID ?? getCurrentSessionId(project);
312
394
  if (!sessionId) {
313
395
  return printResult(io, fail('karpathy-cost-check', 'NO_ACTIVE_SESSION', 'karpathy-cost-check requires --session-id (or an active peaks-code session)', { project }, [
314
396
  'Re-run with --session-id <sid>',
@@ -1,3 +1,5 @@
1
+ import { type MemoryIndexEntry, type MemoryIndexSnapshot, type ProjectMemoryKind } from '../../services/memory/memory-search-service.js';
2
+ import { type MemoryReindexReport } from '../../services/memory/project-memory-service.js';
1
3
  import { type ProgramIO } from '../cli-helpers.js';
2
4
  export interface MemorySearchCommandOptions {
3
5
  query: string;
@@ -12,13 +14,35 @@ export interface MemoryListCommandOptions {
12
14
  fzfBin?: string;
13
15
  project?: string;
14
16
  json?: boolean;
17
+ /** Slice B: emit counts + names-of-first-N instead of the full entry array. */
18
+ summary?: boolean;
15
19
  }
16
20
  export interface MemoryReindexCommandOptions {
17
21
  project?: string;
18
22
  dryRun?: boolean;
19
23
  apply?: boolean;
20
24
  json?: boolean;
25
+ /** Slice B: emit counts + names-of-first-N instead of the full arrays. */
26
+ summary?: boolean;
21
27
  }
28
+ /**
29
+ * Slice 2026-09-10-context-audit-and-discipline (Slice B): bounded view of
30
+ * `memory reindex`. The full report's arrays stay on disk / in the default
31
+ * envelope; this replaces them with `{count, names}` views (≤ 2 KB).
32
+ */
33
+ export declare function buildMemoryReindexSummary(report: MemoryReindexReport): Record<string, unknown>;
34
+ /**
35
+ * Slice B: bounded view of `memory list`. `count` is the true total; `names`
36
+ * carries `name (kind)` labels for the first N entries.
37
+ */
38
+ export declare function buildMemoryListSummary(data: {
39
+ snapshot: MemoryIndexSnapshot;
40
+ entries: readonly MemoryIndexEntry[];
41
+ kindFilter: ProjectMemoryKind | undefined;
42
+ pickedEntries: readonly MemoryIndexEntry[];
43
+ pickedOutputPath: string | null;
44
+ fzfVersion: string | null;
45
+ }): Record<string, unknown>;
22
46
  export interface MemoryIngestCommandOptions {
23
47
  project?: string;
24
48
  sourceDir?: string;
@@ -2,6 +2,7 @@ import { findProjectRoot } from '../../services/config/config-safety.js';
2
2
  import { resolveCanonicalProjectRoot } from '../../services/config/config-service.js';
3
3
  import { loadMemoryIndex, searchMemory } from '../../services/memory/memory-search-service.js';
4
4
  import { executeMemoryReindex, VALID_PROJECT_MEMORY_KINDS } from '../../services/memory/project-memory-service.js';
5
+ import { boundedNames, fitSummaryToBytes } from '../../services/context/summary-view.js';
5
6
  import { executeMemoryIngest } from '../../services/memory/memory-ingest-service.js';
6
7
  import { executeMemoryRotate } from '../../services/memory/memory-rotate-service.js';
7
8
  import { pickFromList } from '../../services/fuzzy-matching/fzf-pick-service.js';
@@ -9,6 +10,53 @@ import { fail, ok } from 'peaks-loop-shared/result';
9
10
  import { getErrorMessage, printResult } from '../cli-helpers.js';
10
11
  import { join } from 'node:path';
11
12
  const VALID_KINDS = VALID_PROJECT_MEMORY_KINDS;
13
+ /**
14
+ * Slice 2026-09-10-context-audit-and-discipline (Slice B): bounded view of
15
+ * `memory reindex`. The full report's arrays stay on disk / in the default
16
+ * envelope; this replaces them with `{count, names}` views (≤ 2 KB).
17
+ */
18
+ export function buildMemoryReindexSummary(report) {
19
+ const view = {
20
+ view: 'summary',
21
+ apply: report.apply,
22
+ projectRoot: report.projectRoot,
23
+ memoryDir: report.memoryDir,
24
+ indexPath: report.indexPath,
25
+ memoryMdPath: report.memoryMdPath,
26
+ scannedFiles: report.scannedFiles,
27
+ indexed: report.indexed,
28
+ indexedByKind: report.indexedByKind,
29
+ unclassified: boundedNames(report.unclassified.map((u) => `${u.name}${u.rawKind === null ? '' : ` (${u.rawKind})`}`)),
30
+ nameConflicts: boundedNames(report.nameConflicts.map((c) => c.name)),
31
+ orphanIndex: boundedNames(report.orphanIndex.map((o) => o.name)),
32
+ orphanDisk: boundedNames(report.orphanDisk.map((p) => p.split(/[\\/]/).pop() ?? p)),
33
+ memoryMd: report.memoryMd,
34
+ writtenFiles: boundedNames(report.writtenFiles.map((p) => p.split(/[\\/]/).pop() ?? p)),
35
+ };
36
+ return fitSummaryToBytes(view);
37
+ }
38
+ /**
39
+ * Slice B: bounded view of `memory list`. `count` is the true total; `names`
40
+ * carries `name (kind)` labels for the first N entries.
41
+ */
42
+ export function buildMemoryListSummary(data) {
43
+ const label = (e) => `${e.name} (${e.kind})`;
44
+ const view = {
45
+ view: 'summary',
46
+ indexPath: data.snapshot.indexPath,
47
+ version: data.snapshot.version,
48
+ updatedAt: data.snapshot.updatedAt,
49
+ total: data.entries.length,
50
+ kindFilter: data.kindFilter ?? null,
51
+ entries: boundedNames(data.entries.map(label)),
52
+ };
53
+ if (data.pickedOutputPath !== null) {
54
+ view.picked = boundedNames(data.pickedEntries.map(label));
55
+ view.pickedOutputPath = data.pickedOutputPath;
56
+ view.fzfVersion = data.fzfVersion;
57
+ }
58
+ return fitSummaryToBytes(view);
59
+ }
12
60
  function resolveMemoryProjectRoot(project) {
13
61
  return project !== undefined
14
62
  ? resolveCanonicalProjectRoot(project)
@@ -63,15 +111,28 @@ export async function runMemoryList(io, options) {
63
111
  if (entries.length === 0) {
64
112
  nextActions.push('No entries match; run `peaks memory extract` to build the index from memory/*.md files.');
65
113
  }
66
- printResult(io, ok('memory.list', {
67
- indexPath: snapshot.indexPath,
68
- version: snapshot.version,
69
- updatedAt: snapshot.updatedAt,
70
- total: entries.length,
71
- kindFilter: kindFilter ?? null,
72
- entries,
73
- ...(options.pick === true ? { picked: pickedEntries, pickedOutputPath, fzfVersion } : {})
74
- }, warnings, nextActions), options.json);
114
+ // Slice B: `--summary` swaps the full entry array for a bounded
115
+ // `{count, names}` view. The default (no flag) envelope is byte-identical
116
+ // to before — the flag is strictly opt-in.
117
+ const data = options.summary === true
118
+ ? buildMemoryListSummary({
119
+ snapshot,
120
+ entries,
121
+ kindFilter,
122
+ pickedEntries,
123
+ pickedOutputPath,
124
+ fzfVersion,
125
+ })
126
+ : {
127
+ indexPath: snapshot.indexPath,
128
+ version: snapshot.version,
129
+ updatedAt: snapshot.updatedAt,
130
+ total: entries.length,
131
+ kindFilter: kindFilter ?? null,
132
+ entries,
133
+ ...(options.pick === true ? { picked: pickedEntries, pickedOutputPath, fzfVersion } : {})
134
+ };
135
+ printResult(io, ok('memory.list', data, warnings, nextActions), options.json);
75
136
  }
76
137
  catch (error) {
77
138
  const message = getErrorMessage(error);
@@ -149,7 +210,13 @@ export async function runMemoryReindex(io, options) {
149
210
  if (report.nameConflicts.length > 0) {
150
211
  nextActions.push(`${report.nameConflicts.length} name collision(s) across files; both entries are kept — rename one file to disambiguate.`);
151
212
  }
152
- printResult(io, ok('memory.reindex', report, [], nextActions), options.json);
213
+ // Slice B: `--summary` keeps the scalar drift counts + names-of-first-N;
214
+ // the full report (with every unclassified/orphan path) stays available
215
+ // by omitting the flag. Default shape is unchanged.
216
+ const data = options.summary === true
217
+ ? buildMemoryReindexSummary(report)
218
+ : report;
219
+ printResult(io, ok('memory.reindex', data, [], nextActions), options.json);
153
220
  }
154
221
  catch (error) {
155
222
  const message = getErrorMessage(error);
@@ -1,3 +1,11 @@
1
1
  import { Command } from 'commander';
2
+ import { type RequestArtifactSummary } from '../../services/artifacts/request-artifact-service.js';
2
3
  import { type ProgramIO } from '../cli-helpers.js';
4
+ /**
5
+ * Slice 2026-09-10-context-audit-and-discipline (Slice B): bounded view of
6
+ * `request list`. `count` is the true total; `names` carries
7
+ * `role/requestId (state)` labels for the first N entries. The full `items`
8
+ * array (with paths + timestamps) is one flag away — omit `--summary`.
9
+ */
10
+ export declare function buildRequestListSummary(items: readonly RequestArtifactSummary[]): Record<string, unknown>;
3
11
  export declare function registerRequestCommands(program: Command, io: ProgramIO): void;
@@ -1,5 +1,6 @@
1
1
  import { InvalidArgumentError } from 'commander';
2
2
  import { createRequestArtifact, listRequestArtifacts, showRequestArtifact, transitionRequestArtifact, PrerequisitesNotSatisfiedError, LintGateError, TypeSanityViolationError, FileSizeViolationError, VALID_REQUEST_TYPES } from '../../services/artifacts/request-artifact-service.js';
3
+ import { boundedNames, fitSummaryToBytes } from '../../services/context/summary-view.js';
3
4
  import { applyPerArtifactFormat, inferArtifactName, parseRequestType, parseRole, parseStateForRole, resolveDefaultFormat, VALID_ROLES, } from './request-format-helpers.js';
4
5
  import { ConfirmationRequiredError } from '../../services/mode/mode-enforcement.js';
5
6
  import { recordBypass, isBypassLimitReached, MAX_BYPASSES_PER_SESSION } from '../../services/mode/bypass-tracker.js';
@@ -9,6 +10,20 @@ import { fail, ok } from 'peaks-loop-shared/result';
9
10
  import { triggerBestPracticeScan } from '../../services/prd/best-practice-auto-trigger.js';
10
11
  import { refreshCodegraphAfterSlice, } from '../../services/codegraph/codegraph-autorefresh.js';
11
12
  import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
13
+ /**
14
+ * Slice 2026-09-10-context-audit-and-discipline (Slice B): bounded view of
15
+ * `request list`. `count` is the true total; `names` carries
16
+ * `role/requestId (state)` labels for the first N entries. The full `items`
17
+ * array (with paths + timestamps) is one flag away — omit `--summary`.
18
+ */
19
+ export function buildRequestListSummary(items) {
20
+ const view = {
21
+ view: 'summary',
22
+ count: items.length,
23
+ items: boundedNames(items.map((i) => `${i.role}/${i.requestId} (${i.state})`)),
24
+ };
25
+ return fitSummaryToBytes(view);
26
+ }
12
27
  export function registerRequestCommands(program, io) {
13
28
  const request = program.command('request').description('Manage per-request Peaks role artifacts (PRD / UI / RD / QA)');
14
29
  addJsonOption(request
@@ -87,7 +102,8 @@ export function registerRequestCommands(program, io) {
87
102
  .description('List per-request artifacts under a project workspace')
88
103
  .requiredOption('--project <path>', 'target project root')
89
104
  .option('--session-id <session>', 'limit to a specific session id')
90
- .option('--role <role>', `limit to a single role (${VALID_ROLES.join(' | ')})`, parseRole)).action(async (options) => {
105
+ .option('--role <role>', `limit to a single role (${VALID_ROLES.join(' | ')})`, parseRole)
106
+ .option('--summary', 'emit counts + names-of-first-N only (≤ 2 KB) instead of the full item array; the default envelope is unchanged')).action(async (options) => {
91
107
  try {
92
108
  const listOptions = { projectRoot: options.project };
93
109
  if (options.sessionId !== undefined) {
@@ -97,7 +113,12 @@ export function registerRequestCommands(program, io) {
97
113
  listOptions.role = options.role;
98
114
  }
99
115
  const items = await listRequestArtifacts(listOptions);
100
- printResult(io, ok('request.list', { count: items.length, items }), options.json);
116
+ // Slice B: `--summary` is opt-in; the default `{count, items}` shape is
117
+ // byte-identical to before.
118
+ const data = options.summary === true
119
+ ? buildRequestListSummary(items)
120
+ : { count: items.length, items };
121
+ printResult(io, ok('request.list', data), options.json);
101
122
  }
102
123
  catch (error) {
103
124
  printResult(io, fail('request.list', 'REQUEST_LIST_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Check project path before retrying']), options.json);
@@ -198,7 +198,7 @@ export function registerScanCommands(program, io) {
198
198
  });
199
199
  addJsonOption(scan
200
200
  .command('file-size')
201
- .description('Check git diff for files exceeding a line count threshold (karpathy-skills "Simplicity First")')
201
+ .description('Check git diff for files exceeding a line count threshold (karpathy-skills "Simplicity First"; tool output, lockfiles and append-only records such as CHANGELOG.md excluded)')
202
202
  .requiredOption('--project <path>', 'target project root')
203
203
  .option('--base-ref <ref>', 'compare working tree against this git ref (default: HEAD)')
204
204
  .option('--threshold <n>', `line count threshold (default: ${DEFAULT_FILE_SIZE_THRESHOLD})`)).action((options) => {