peaks-loop 4.0.50 → 4.0.52

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 (127) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README-en.md +1 -1
  3. package/README.md +1 -1
  4. package/dist/cli/commands/baseline-commands.js +11 -1
  5. package/dist/cli/commands/codegraph-command-runtime.d.ts +28 -0
  6. package/dist/cli/commands/codegraph-command-runtime.js +72 -0
  7. package/dist/cli/commands/codegraph-commands.d.ts +2 -11
  8. package/dist/cli/commands/codegraph-commands.js +173 -228
  9. package/dist/cli/commands/codegraph-status-command.d.ts +22 -0
  10. package/dist/cli/commands/codegraph-status-command.js +299 -0
  11. package/dist/cli/commands/core/memory-command.js +6 -2
  12. package/dist/cli/commands/job-commands.js +121 -30
  13. package/dist/cli/commands/project-commands.js +13 -3
  14. package/dist/cli/commands/request-commands.js +19 -8
  15. package/dist/cli/commands/share-commands.js +85 -18
  16. package/dist/cli/commands/slice-commands.js +2 -2
  17. package/dist/services/artifacts/artifact-prerequisites.js +23 -1
  18. package/dist/services/codegraph/codegraph-autorefresh.d.ts +16 -0
  19. package/dist/services/codegraph/codegraph-autorefresh.js +51 -5
  20. package/dist/services/codegraph/codegraph-config-repair-writer.d.ts +88 -0
  21. package/dist/services/codegraph/codegraph-config-repair-writer.js +322 -0
  22. package/dist/services/codegraph/codegraph-exclude-integrity.d.ts +20 -2
  23. package/dist/services/codegraph/codegraph-exclude-integrity.js +24 -3
  24. package/dist/services/codegraph/codegraph-exclude-reconciler.d.ts +23 -2
  25. package/dist/services/codegraph/codegraph-exclude-reconciler.js +123 -12
  26. package/dist/services/codegraph/codegraph-exclude-repair.d.ts +109 -55
  27. package/dist/services/codegraph/codegraph-exclude-repair.js +249 -195
  28. package/dist/services/codegraph/codegraph-include-reconciler.d.ts +10 -0
  29. package/dist/services/codegraph/codegraph-include-reconciler.js +160 -0
  30. package/dist/services/codegraph/codegraph-index-integrity.d.ts +268 -0
  31. package/dist/services/codegraph/codegraph-index-integrity.js +471 -0
  32. package/dist/services/codegraph/codegraph-service.d.ts +54 -0
  33. package/dist/services/codegraph/codegraph-service.js +84 -1
  34. package/dist/services/dispatch/sub-agent-dispatcher.d.ts +11 -30
  35. package/dist/services/dispatch/sub-agent-dispatcher.js +5 -48
  36. package/dist/services/doctor/doctor-service/checks/codegraph-exclude-integrity.js +19 -4
  37. package/dist/services/doctor/doctor-service/checks/codegraph-index-integrity.d.ts +54 -0
  38. package/dist/services/doctor/doctor-service/checks/codegraph-index-integrity.js +151 -0
  39. package/dist/services/doctor/doctor-service/checks/l3-orphan-sessions.js +10 -10
  40. package/dist/services/doctor/doctor-service/plugin-registry.js +2 -0
  41. package/dist/services/doctor/doctor-service/types.d.ts +25 -0
  42. package/dist/services/ide/adapters/claude-code-adapter.js +0 -1
  43. package/dist/services/ide/adapters/codex-adapter.js +1 -2
  44. package/dist/services/ide/adapters/cursor-adapter.js +1 -2
  45. package/dist/services/ide/adapters/hermes-adapter.js +1 -2
  46. package/dist/services/ide/adapters/openclaw-adapter.js +1 -2
  47. package/dist/services/ide/adapters/qoder-adapter.js +1 -2
  48. package/dist/services/ide/adapters/tongyi-lingma-adapter.js +1 -2
  49. package/dist/services/ide/adapters/trae-adapter.js +1 -2
  50. package/dist/services/ide/adapters/zcode-adapter.js +0 -1
  51. package/dist/services/ide/ide-types.d.ts +0 -2
  52. package/dist/services/memory/project-memory-service/index/kind-dispatch.js +48 -13
  53. package/dist/services/memory/project-memory-service/index.d.ts +5 -3
  54. package/dist/services/memory/project-memory-service/index.js +2 -2
  55. package/dist/services/memory/project-memory-service/parsers/frontmatter.d.ts +15 -1
  56. package/dist/services/memory/project-memory-service/parsers/frontmatter.js +34 -6
  57. package/dist/services/memory/project-memory-service/parsers/markdown-pure.d.ts +27 -1
  58. package/dist/services/memory/project-memory-service/parsers/markdown-pure.js +92 -7
  59. package/dist/services/memory/project-memory-service/types.d.ts +86 -0
  60. package/dist/services/slice/slice-check-types.d.ts +1 -1
  61. package/dist/services/workspace/runtime-layout.d.ts +91 -0
  62. package/dist/services/workspace/runtime-layout.js +148 -0
  63. package/dist/services/workspace/workspace-claude-settings-materializer.js +14 -0
  64. package/package.json +6 -6
  65. package/scripts/clean-dist.mjs +15 -3
  66. package/scripts/sync-version.mjs +26 -4
  67. package/skills/bee/peaks-perf-audit/SKILL.md +2 -2
  68. package/skills/bee/peaks-perf-audit/references/audit-protocol.md +1 -1
  69. package/skills/bee/peaks-prd/SKILL.md +4 -4
  70. package/skills/bee/peaks-prd/references/prd-for-multi-pass.md +1 -1
  71. package/skills/bee/peaks-prd/references/workflow.md +1 -1
  72. package/skills/bee/peaks-qa/SKILL.md +6 -6
  73. package/skills/bee/peaks-qa/references/external-capability-guidance.md +1 -1
  74. package/skills/bee/peaks-qa/references/qa-fanout-contract.md +1 -1
  75. package/skills/bee/peaks-qa/references/qa-skill-presence.md +1 -1
  76. package/skills/bee/peaks-qa/references/reading-handoff-frontmatter.md +2 -2
  77. package/skills/bee/peaks-rd/SKILL.md +2 -2
  78. package/skills/bee/peaks-rd/references/code-reviewer-4dim-hint.md +1 -1
  79. package/skills/bee/peaks-rd/references/external-references.md +1 -1
  80. package/skills/bee/peaks-rd/references/mandatory-perf-baseline.md +1 -1
  81. package/skills/bee/peaks-rd/references/ocr-multilang-1.8.md +2 -2
  82. package/skills/bee/peaks-rd/references/parallel-review-fanout.md +2 -2
  83. package/skills/bee/peaks-rd/references/rd-fanout-contracts.md +11 -8
  84. package/skills/bee/peaks-rd/references/rd-runbook.md +1 -1
  85. package/skills/bee/peaks-rd/references/rd-sub-agent-dispatch.md +7 -7
  86. package/skills/bee/peaks-rd/references/rd-transition-gates.md +1 -1
  87. package/skills/bee/peaks-rd/references/reading-v2-slice-results.md +1 -1
  88. package/skills/bee/peaks-rd/references/skill-presence-and-title.md +1 -1
  89. package/skills/bee/peaks-rd/references/v2-12-fanout-collapse.md +7 -5
  90. package/skills/bee/peaks-rd/references/writing-handoff-frontmatter.md +3 -3
  91. package/skills/bee/peaks-reviewer/SKILL.md +1 -1
  92. package/skills/bee/peaks-sc/SKILL.md +1 -1
  93. package/skills/bee/peaks-security-audit/SKILL.md +3 -3
  94. package/skills/bee/peaks-security-audit/references/audit-protocol.md +1 -1
  95. package/skills/bee/peaks-txt/SKILL.md +3 -3
  96. package/skills/bee/peaks-txt/references/context-capsule.md +1 -1
  97. package/skills/bee/peaks-ui/SKILL.md +1 -1
  98. package/skills/peaks-audit/SKILL.md +1 -1
  99. package/skills/peaks-code/SKILL.md +9 -9
  100. package/skills/peaks-code/references/context-governance.md +1 -1
  101. package/skills/peaks-code/references/dag-orchestrator.md +3 -4
  102. package/skills/peaks-code/references/external-references.md +1 -1
  103. package/skills/peaks-code/references/external-skill-invocation.md +2 -2
  104. package/skills/peaks-code/references/fanout-mandatory.md +3 -3
  105. package/skills/peaks-code/references/frontend-only-mode.md +2 -2
  106. package/skills/peaks-code/references/gstack-integration.md +1 -1
  107. package/skills/peaks-code/references/micro-cycle.md +1 -1
  108. package/skills/peaks-code/references/periodic-checkpoint.md +2 -2
  109. package/skills/peaks-code/references/project-memory-loading.md +19 -1
  110. package/skills/peaks-code/references/project-scan-checklist.md +1 -1
  111. package/skills/peaks-code/references/resume-detection.md +1 -1
  112. package/skills/peaks-code/references/runbook.md +3 -3
  113. package/skills/peaks-code/references/session-overload-signal-index.md +2 -2
  114. package/skills/peaks-code/references/startup-sequence.md +16 -16
  115. package/skills/peaks-code/references/step-11-memory-sediment.md +3 -3
  116. package/skills/peaks-code/references/sub-agent-dispatch.md +7 -6
  117. package/skills/peaks-code/references/swarm-dispatch-contract.md +1 -1
  118. package/skills/peaks-code/references/workflow-gates-and-types.md +3 -3
  119. package/skills/peaks-code/references/worktree-governance.md +1 -1
  120. package/skills/peaks-final-review/SKILL.md +3 -3
  121. package/skills/peaks-ide/references/audit-log-helper.md +5 -4
  122. package/skills/peaks-resume/SKILL.md +1 -1
  123. package/skills/peaks-slice-decompose/SKILL.md +4 -4
  124. package/skills/peaks-slice-decompose/references/cross-pass-edge-interpretation.md +1 -1
  125. package/skills/peaks-slice-decompose/references/granularity-decision.md +1 -1
  126. package/skills/peaks-slice-decompose/references/v2-schema.md +2 -2
  127. package/skills/peaks-solo/SKILL.md +1 -2
@@ -1,11 +1,13 @@
1
1
  import { loadProjectDashboard } from '../../services/dashboard/project-dashboard-service.js';
2
2
  import { generateProjectContext, readProjectContext } from '../../services/memory/project-context-service.js';
3
- import { extractSessionMemories, readMemoryIndex, readProjectMemories, readProjectMemoryBody } from '../../services/memory/project-memory-service.js';
3
+ import { describeMemoryBlockDrops, describeSessionScanFailures, extractSessionMemories, readMemoryIndex, readProjectMemories, readProjectMemoryBody, VALID_PROJECT_MEMORY_KINDS } from '../../services/memory/project-memory-service.js';
4
4
  import { readBusinessKnowledge } from '../../services/prd/project-scan-reader.js';
5
5
  import { applyStalePolicy, DEFAULT_STALE_DAYS } from '../../shared/stale-policy.js';
6
6
  import { formatMdCompact } from '../../shared/format-md-compact.js';
7
7
  import { fail, ok } from 'peaks-loop-shared/result';
8
8
  import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
9
+ /** Derived from the canonical kind vocabulary — never hand-maintain a list here. */
10
+ const KIND_HELP = VALID_PROJECT_MEMORY_KINDS.join(', ');
9
11
  export function registerProjectCommands(program, io) {
10
12
  const project = program.command('project').description('Aggregate Peaks state for a target project (read-only)');
11
13
  addJsonOption(project
@@ -111,7 +113,15 @@ export function registerProjectCommands(program, io) {
111
113
  writtenFiles: result.writtenFiles,
112
114
  memoryDir: result.primaryMemoryDir,
113
115
  indexUpdated: result.updatedIndex
114
- }), options.json);
116
+ }, [
117
+ // Two drop axes, one channel. `droppedBlocks` = a block was found and
118
+ // rejected (or a marker-shaped comment was not findable at all);
119
+ // `scanFailures` = the whole artifact could not be read, so its blocks
120
+ // were never candidates. `data` is unchanged by either — same contract
121
+ // as `memory.extract`.
122
+ ...describeMemoryBlockDrops(result.droppedBlocks),
123
+ ...describeSessionScanFailures(result.scanFailures)
124
+ ]), options.json);
115
125
  }
116
126
  catch (error) {
117
127
  printResult(io, fail('project.memories:extract', 'MEMORY_EXTRACT_FAILED', getErrorMessage(error), { sessionId: options.sessionId, projectRoot: options.project }, ['Check the session-id and project path']), options.json);
@@ -141,7 +151,7 @@ export function registerProjectCommands(program, io) {
141
151
  .command('memories')
142
152
  .description('Read durable project memories (decisions, conventions, modules, rules) from .peaks/memory for LLM consumption')
143
153
  .requiredOption('--project <path>', 'target project root')
144
- .option('--kind <kind>', 'filter by kind: project, rule, decision, reference, feedback, convention, module, lesson')).action((options) => {
154
+ .option('--kind <kind>', `filter by memory kind (one of: ${KIND_HELP})`)).action((options) => {
145
155
  try {
146
156
  const result = readProjectMemories(options.project);
147
157
  if (options.kind) {
@@ -8,7 +8,7 @@ import { lintRequestArtifact } from '../../services/artifacts/artifact-lint-serv
8
8
  import { getRepairCycleStatus } from '../../services/artifacts/repair-cycle-service.js';
9
9
  import { fail, ok } from 'peaks-loop-shared/result';
10
10
  import { triggerBestPracticeScan } from '../../services/prd/best-practice-auto-trigger.js';
11
- import { refreshCodegraphAfterSlice, } from '../../services/codegraph/codegraph-autorefresh.js';
11
+ import { codegraphRefreshNotice, refreshCodegraphAfterSlice, } from '../../services/codegraph/codegraph-autorefresh.js';
12
12
  import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
13
13
  import { isUnsafePathInput } from '../../shared/path-safety.js';
14
14
  /**
@@ -296,6 +296,9 @@ export function registerRequestCommands(program, io) {
296
296
  // RD → QA slice-complete boundary. Set only for rd:qa-handoff; null
297
297
  // otherwise. Best-effort and fail-silent — never blocks the transition.
298
298
  let codegraphRefresh = null;
299
+ // A2 (2026-09-17): the human-visible half of the refresh outcome. null
300
+ // when the refresh succeeded or when no codegraph store was in use.
301
+ let codegraphWarning = null;
299
302
  if (role === 'rd' && newState === 'qa-handoff') {
300
303
  try {
301
304
  const { maybePreCompactCheckpoint } = await import('../../services/compact/request-transition-hook.js');
@@ -319,6 +322,11 @@ export function registerRequestCommands(program, io) {
319
322
  // The refresh is best-effort; never block the transition.
320
323
  codegraphRefresh = { refreshed: false, reason: 'unavailable', note: 'auto codegraph refresh failed after transition' };
321
324
  }
325
+ // A2 (2026-09-17): never BLOCKING is kept; never VISIBLE is not. A
326
+ // non-refresh while a codegraph store is in use becomes a warning
327
+ // line beside the transition's own notes. See
328
+ // `codegraphRefreshNotice` for why `no-codegraph-dir` stays silent.
329
+ codegraphWarning = codegraphRefreshNotice(codegraphRefresh);
322
330
  }
323
331
  // v2.13.2 AC-4 — auto-regen prd/handoff.md on prd:handed-off success.
324
332
  // Only fires when the handoff is missing; existing handoffs are not overwritten.
@@ -360,13 +368,16 @@ export function registerRequestCommands(program, io) {
360
368
  // transition is not a slice boundary), preCompact is null and
361
369
  // we emit a `preCompactCheckpoint: null` field so LLM callers
362
370
  // can branch on it without re-deriving zone membership.
363
- printResult(io, ok('request.transition', { ...result, preCompactCheckpoint: preCompact?.data ?? null, codegraphRefresh }, preCompact !== null
364
- ? [
365
- `Pre-compact checkpoint written at ratio=${typeof preCompact.data === 'object' && preCompact.data !== null && 'ratio' in preCompact.data
366
- ? String(preCompact.data.ratio)
367
- : 'unknown'} (zone=pre-compact)`
368
- ]
369
- : []), options.json);
371
+ printResult(io, ok('request.transition', { ...result, preCompactCheckpoint: preCompact?.data ?? null, codegraphRefresh }, [
372
+ ...(preCompact !== null
373
+ ? [
374
+ `Pre-compact checkpoint written at ratio=${typeof preCompact.data === 'object' && preCompact.data !== null && 'ratio' in preCompact.data
375
+ ? String(preCompact.data.ratio)
376
+ : 'unknown'} (zone=pre-compact)`
377
+ ]
378
+ : []),
379
+ ...(codegraphWarning === null ? [] : [codegraphWarning])
380
+ ]), options.json);
370
381
  }
371
382
  catch (error) {
372
383
  if (error instanceof InvalidArgumentError) {
@@ -1,5 +1,5 @@
1
- import { realpathSync as realpathSyncNative } from 'node:fs';
2
- import { resolve } from 'node:path';
1
+ import { existsSync, readdirSync, realpathSync as realpathSyncNative } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
3
  import { fail, getErrorMessage, ok } from 'peaks-loop-shared/result';
4
4
  import { addJsonOption, printResult } from '../cli-helpers.js';
5
5
  import { readSharedChannel, writeSharedEntry, SHARED_CHANNEL_SOFT_VALUE_WARN } from 'peaks-loop-shared-channel';
@@ -282,19 +282,39 @@ export function registerAwaitCommand(parent, io) {
282
282
  process.exitCode = 1;
283
283
  return;
284
284
  }
285
- // No record-path index is kept for DAG-dispatched batches yet; the caller
286
- // is expected to have a single shared record directory. We pass the empty
287
- // list — the runner tracks outcomes through its own contract-store writes,
288
- // so the dispatcher reaches no slot and reports no results (see the
289
- // `recordPaths: []` case in tests/unit/services/dispatch/
290
- // sub-agent-dispatchers.test.ts, which pins that empty shape).
291
- const input = {
292
- batchId: options.batch,
293
- dispatchCount: 1,
294
- recordPaths: [],
295
- ...(timeoutMs !== undefined ? { timeoutMs } : {})
296
- };
297
285
  try {
286
+ // Slice 2026-09-16-n1-await-reports: resolve the batch's records from the
287
+ // session's dispatch directory. `recordPaths` used to be hardcoded to
288
+ // `[]`, and `awaitBatch` short-circuits on an empty list
289
+ // (await-batch.ts:134) — so `await` reported zero results and exited 0
290
+ // for every batch, forever. A batch we cannot locate is now a failure,
291
+ // not a success with nothing in it.
292
+ const { readRecord } = await import('../../services/dispatch/dispatch-record-writer.js');
293
+ const scan = resolveBatchRecords({
294
+ projectRoot,
295
+ sessionId: sid,
296
+ batchId: options.batch,
297
+ readOne: readRecord
298
+ });
299
+ if (scan.recordPaths.length === 0) {
300
+ const unreadableNote = scan.unreadable.length > 0
301
+ ? ` ${scan.unreadable.length} dispatch record(s) there could not be read, so they could not be matched to this batch: ${scan.unreadable.join(', ')}`
302
+ : '';
303
+ printResult(io, fail('sub-agent.await', 'NO_DISPATCH_RECORDS', `No dispatch record with batchId=${options.batch} under ${scan.sessionDir}.${unreadableNote}`, {
304
+ ok: false,
305
+ batchId: options.batch,
306
+ sessionDir: scan.sessionDir,
307
+ unreadableRecords: scan.unreadable
308
+ }, [awaitErrorNextActions('NO_DISPATCH_RECORDS')]), asJson);
309
+ process.exitCode = 1;
310
+ return;
311
+ }
312
+ const input = {
313
+ batchId: options.batch,
314
+ dispatchCount: scan.recordPaths.length,
315
+ recordPaths: scan.recordPaths,
316
+ ...(timeoutMs !== undefined ? { timeoutMs } : {})
317
+ };
298
318
  const results = await dispatcher.awaitBatch(input);
299
319
  const summary = summarizeBatchResults(results);
300
320
  printResult(io, ok('sub-agent.await', {
@@ -303,15 +323,18 @@ export function registerAwaitCommand(parent, io) {
303
323
  batchId: options.batch,
304
324
  ide: dispatcher.label,
305
325
  results,
306
- summary
307
- }, [], [
326
+ summary,
327
+ unreadableRecords: scan.unreadable
328
+ }, scan.unreadable.length > 0
329
+ ? [`${scan.unreadable.length} unreadable dispatch record(s) in this session were skipped and are NOT part of the results: ${scan.unreadable.join(', ')}`]
330
+ : [], [
308
331
  // Slice 2026-09-15-s9: corrected. This used to tell users that the
309
332
  // four non-Claude IDEs would report `awaitByLlm: <ide> 1.2 fallback`,
310
333
  // the slice-1.2 marker that slice 1.3 replaced with a real
311
334
  // file-polling await. The text survived because nothing tested it —
312
335
  // no adapter produces that note any more (asserted in
313
- // sub-agent-dispatchers.test.ts), and the only code that still emits
314
- // it, `awaitByLlmFallback`, has no callers.
336
+ // sub-agent-dispatchers.test.ts), and the emitter that produced it,
337
+ // `awaitByLlmFallback`, has since been removed.
315
338
  `Each non-claude-code IDE labels its own results (see the \`note\` field), so a timed-out slot is attributable to the adapter it came from.`
316
339
  ]), asJson);
317
340
  }
@@ -331,6 +354,9 @@ function awaitErrorNextActions(code) {
331
354
  if (code === 'IDE_NOT_SUPPORTED') {
332
355
  return 'Switch to claude-code, or rely on LLM-side await for non-claude-code IDEs in slice 1.3.';
333
356
  }
357
+ if (code === 'NO_DISPATCH_RECORDS') {
358
+ return 'Check --session-id / --project: records live under .peaks/_sub_agents/<sessionId>/. Use the batchId exactly as printed by the dispatch envelope.';
359
+ }
334
360
  return 'See error message; check that --batch matches the dispatch envelope and --timeout is a positive integer ms.';
335
361
  }
336
362
  /**
@@ -351,6 +377,47 @@ function safeRecordPath(p) {
351
377
  return p;
352
378
  }
353
379
  }
380
+ /**
381
+ * Slice 2026-09-16-n1-await-reports: which on-disk records belong to a batch.
382
+ *
383
+ * Reuses the conventions already in the repo instead of inventing a new one:
384
+ * - records live at `.peaks/_sub_agents/<sid>/dispatch-<rid>-<ts>.json`
385
+ * (`dispatchRecordPath`, src/services/security/safe-settings-path.ts);
386
+ * - a record's batch is its own `batchId` field, written by
387
+ * `writeInitialDispatchRecord`. The `--batch` branch of `finalize` (below)
388
+ * and `findBatchRecords` in heartbeat-watch-command.ts resolve a batch the
389
+ * same way: scan the session dir, filter `dispatch-*.json`, compare field.
390
+ *
391
+ * `unreadable` lists the `dispatch-*.json` candidates whose batch could NOT be
392
+ * determined. The caller must not fold them into "no such batch": a record it
393
+ * cannot read is not evidence that the batch is empty.
394
+ */
395
+ function resolveBatchRecords(input) {
396
+ const sessionDir = resolve(input.projectRoot, '.peaks', '_sub_agents', input.sessionId);
397
+ const recordPaths = [];
398
+ const unreadable = [];
399
+ if (!existsSync(sessionDir))
400
+ return { sessionDir, recordPaths, unreadable };
401
+ for (const name of readdirSync(sessionDir)) {
402
+ // `active-dispatches.json` (the index) and `batch-<uuid>.counter.json` are
403
+ // not records; neither carries a `version`, so `readRecord` would reject
404
+ // them as "version mismatch". Same filter as the `--batch` branch below.
405
+ if (!name.startsWith('dispatch-') || !name.endsWith('.json'))
406
+ continue;
407
+ const recordPath = safeRecordPath(join(sessionDir, name));
408
+ let batchId;
409
+ try {
410
+ batchId = input.readOne(recordPath).batchId;
411
+ }
412
+ catch {
413
+ unreadable.push(recordPath);
414
+ continue;
415
+ }
416
+ if (batchId === input.batchId)
417
+ recordPaths.push(recordPath);
418
+ }
419
+ return { sessionDir, recordPaths, unreadable };
420
+ }
354
421
  export function registerFinalizeCommand(parent, io) {
355
422
  addJsonOption(parent
356
423
  .command('finalize')
@@ -35,7 +35,7 @@ export function registerSliceCommands(program, io) {
35
35
  'Each stage reports pass / fail / skipped. ' +
36
36
  'Exit 0 only if every stage passes or is skipped.')
37
37
  .option('--project <path>', 'target project root', '.')
38
- .option('--rid <rid>', 'request id; defaults to the active current-change binding')
38
+ .option('--rid <rid>', 'request id; REQUIRED — there is no binding to fall back to, and slice check fails without it')
39
39
  .option('--refresh-fanout', 're-run the 3-way review fan-out (peaks-rd) even if the review files already exist', false)
40
40
  .option('--run-tests', 'opt in to the FULL test suite at the boundary (default is the changed-only suite via `vitest run --changed`); use the peaks-test skill to run the full suite standalone', false)
41
41
  .option('--skip-tests', 'skip the unit-test stage entirely (e.g. docs-only slices); use the peaks-test skill to run the full suite manually if you want a separate check', false)
@@ -60,7 +60,7 @@ export function registerSliceCommands(program, io) {
60
60
  }
61
61
  }
62
62
  catch (error) {
63
- printResult(io, fail('slice.check', 'SLICE_CHECK_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Verify the project path is a peaks repo, --rid is correct, and .peaks/_runtime/current-change is valid']), options.json ?? false);
63
+ printResult(io, fail('slice.check', 'SLICE_CHECK_FAILED', getErrorMessage(error), { projectRoot: options.project }, [`Verify the project path is a peaks repo and --rid names a slice (letters, digits, dots, underscores or dashes): ${options.rid ?? '(no --rid given)'}`]), options.json ?? false);
64
64
  process.exitCode = 1;
65
65
  }
66
66
  });
@@ -237,10 +237,32 @@ const PRD_CONTENT = {
237
237
  description: 'PRD artifact must contain Goal and Acceptance criteria sections before handoff',
238
238
  mustContain: ['## Goals', '## Acceptance']
239
239
  };
240
+ // R10 (2026-09-16): the second marker used to be the bare `test(` inside
241
+ // `mustContain` — i.e. ALL markers required. That made the gate satisfiable
242
+ // only by MENTIONING the literal string in prose, never by honest test code,
243
+ // because this repo's own BDD Test Style Contract
244
+ // (`skills/bee/peaks-rd/references/rd-sub-agent-dispatch.md:170`) names BOTH
245
+ // idioms — "The first string-literal argument of `it()` / `test()` MUST
246
+ // describe business behavior" — and `it(` is the dominant one by ~8x.
247
+ // Measured 2026-09-16 (`grep -rho '\bit(' tests | wc -l`, same for `test(`):
248
+ // `it(` 3015 vs `test(` 372 across `tests/`; the three test files slice
249
+ // 2026-09-16-codegraph-index-integrity added contain `it(` 58 / `test(` 0.
250
+ // The gate was in fact passing on artifacts whose ONLY `test(` was prose —
251
+ // including this repo's own generated template `buildTestCases`
252
+ // (`src/services/evidence/evidence-generator.ts`), which literally reads
253
+ // "no new tests; behavior preserved" and satisfied the marker anyway.
254
+ // Both idioms are therefore accepted via `mustContainAny` — an existing
255
+ // field on this type, already used by PERF_BASELINE / AUDIT_SECURITY /
256
+ // MUT_REPORT — while `## Test cases` stays a hard `mustContain`.
257
+ // Do NOT "tighten" this back to a single idiom in `mustContain`: that
258
+ // restores exactly the pass-by-mentioning hole `headingMustContain` was
259
+ // introduced to close (see the field doc comment above), and no honest test
260
+ // file in this repo could then satisfy the gate.
240
261
  const UNIT_TESTS = {
241
262
  relativePath: 'qa/test-cases/<rid>.md',
242
263
  description: 'Unit test files for the implemented changes (enforces peaks-rd Gate B2)',
243
- mustContain: ['## Test cases', 'test(']
264
+ mustContain: ['## Test cases'],
265
+ mustContainAny: ['test(', 'it(']
244
266
  };
245
267
  const QA_INITIATED = {
246
268
  relativePath: 'qa/.initiated',
@@ -6,6 +6,22 @@ export type CodegraphAutorefreshResult = {
6
6
  reason: 'no-codegraph-dir' | 'index-failed' | 'unavailable';
7
7
  note: string;
8
8
  };
9
+ /**
10
+ * The operator-facing line for a refresh that did not happen, or `null` when
11
+ * there is nothing to report. The ONE place the report/no-report rule lives,
12
+ * shared by both slice-boundary call sites (`peaks job checkpoint --state
13
+ * done` and `peaks request transition rd:qa-handoff`) so the two cannot
14
+ * drift into disagreeing about what counts as visible.
15
+ *
16
+ * Returns the result's own `note` verbatim rather than rebuilding a
17
+ * sentence: that note is where the reason (exit code / upstream error line)
18
+ * and the remedy live, and a second wording here would be a second truth.
19
+ *
20
+ * `no-codegraph-dir` is deliberately SILENT — see the header's A2 note. A
21
+ * store that exists and did not refresh is a warning; a project that never
22
+ * set codegraph up is not a defect to report at every slice boundary.
23
+ */
24
+ export declare function codegraphRefreshNotice(result: CodegraphAutorefreshResult): string | null;
9
25
  /**
10
26
  * True when `<projectRoot>/.codegraph/` exists and is a directory.
11
27
  * Pure fs probe; never throws.
@@ -15,8 +15,8 @@
15
15
  // hook install surface needed), fires exactly once at the true slice
16
16
  // boundary, and needs no IDE hook plumbing.
17
17
  //
18
- // The refresh is best-effort and FAIL-SILENT — it never throws and never
19
- // blocks the checkpoint/transition ok envelope:
18
+ // The refresh is best-effort and never throws, and it never blocks the
19
+ // checkpoint/transition ok envelope:
20
20
  // - No `<projectRoot>/.codegraph/` directory → skip (codegraph was
21
21
  // never initialized for this project; `peaks codegraph init` is a
22
22
  // one-time setup the orchestrator owns).
@@ -29,6 +29,24 @@
29
29
  // human-readable note.
30
30
  // - Any unexpected error → return `unavailable` with a note.
31
31
  //
32
+ // A2 (`2026-09-17-codegraph-msg-and-refresh`). The old header said
33
+ // "FAIL-SILENT", and it was: both call sites discarded this result's `note`,
34
+ // so a refresh that DID NOT HAPPEN and one that did were indistinguishable
35
+ // to the operator — the same silent-failure class this job exists to close.
36
+ // "Never blocks the caller" is the correct half and is KEPT; "never tells
37
+ // anyone" was not.
38
+ //
39
+ // The two halves are split by whether a refresh was EXPECTED:
40
+ // - `no-codegraph-dir` → the project has no codegraph store (or has a
41
+ // foreign, never-initialized one). Nothing was expected, so nothing is
42
+ // reported: warning on every slice boundary of every project that never
43
+ // opted in is noise that trains the reader to skip the line that
44
+ // matters. The note is still in the JSON envelope.
45
+ // - `index-failed` / `unavailable` → a store EXISTS and is in use, so the
46
+ // refresh was expected and did not happen. That is a real failure and
47
+ // `codegraphRefreshNotice` turns it into an operator-visible warning
48
+ // naming the reason and the remedy.
49
+ //
32
50
  // We do NOT auto-init a genuinely fresh (no `.codegraph/` dir) project:
33
51
  // the orchestrator owns that one-time setup. The dangling self-heal above
34
52
  // IS an auto-init, but `codegraph init` (WITHOUT `--index`) is fast (~1 s)
@@ -39,6 +57,28 @@ import { existsSync, statSync } from 'node:fs';
39
57
  import { join } from 'node:path';
40
58
  import { CODEGRAPH_DIR_NAME, CODEGRAPH_MARKER_NAME, createCodegraphInvocation, executeCodegraphInvocation, isCodegraphInitialized, } from './codegraph-service.js';
41
59
  import { repairCodegraphExcludeFromProject } from './codegraph-exclude-repair.js';
60
+ /**
61
+ * The operator-facing line for a refresh that did not happen, or `null` when
62
+ * there is nothing to report. The ONE place the report/no-report rule lives,
63
+ * shared by both slice-boundary call sites (`peaks job checkpoint --state
64
+ * done` and `peaks request transition rd:qa-handoff`) so the two cannot
65
+ * drift into disagreeing about what counts as visible.
66
+ *
67
+ * Returns the result's own `note` verbatim rather than rebuilding a
68
+ * sentence: that note is where the reason (exit code / upstream error line)
69
+ * and the remedy live, and a second wording here would be a second truth.
70
+ *
71
+ * `no-codegraph-dir` is deliberately SILENT — see the header's A2 note. A
72
+ * store that exists and did not refresh is a warning; a project that never
73
+ * set codegraph up is not a defect to report at every slice boundary.
74
+ */
75
+ export function codegraphRefreshNotice(result) {
76
+ if (result.refreshed)
77
+ return null;
78
+ if (result.reason === 'no-codegraph-dir')
79
+ return null;
80
+ return result.note;
81
+ }
42
82
  /**
43
83
  * True when `<projectRoot>/.codegraph/` exists and is a directory.
44
84
  * Pure fs probe; never throws.
@@ -62,6 +102,12 @@ function isCodegraphPeaksLoopManaged(projectRoot) {
62
102
  function errorMessage(error) {
63
103
  return error instanceof Error ? error.message : String(error);
64
104
  }
105
+ // A2: the remedy half of an operator-visible failure note. Phrased like the
106
+ // existing follow-up-index warning in `codegraph-exclude-repair.ts` so the
107
+ // two read as one voice; it names the command the LLM/orchestrator re-runs,
108
+ // never a verb the user is asked to type (Human-NL-Choice-Only — the same
109
+ // posture that warning already ships with).
110
+ const REFRESH_REMEDY = 'Run `peaks codegraph index --project <root>` to refresh the codegraph index.';
65
111
  function firstMeaningfulLine(text) {
66
112
  const trimmed = text.trim();
67
113
  if (trimmed.length === 0)
@@ -105,7 +151,7 @@ export async function refreshCodegraphAfterSlice(projectRoot, runner) {
105
151
  return {
106
152
  refreshed: false,
107
153
  reason: 'index-failed',
108
- note: `auto codegraph refresh self-heal init failed (exit ${String(initResult.exitCode)}): ${firstMeaningfulLine(initResult.stderr || initResult.stdout)}`,
154
+ note: `auto codegraph refresh self-heal init failed (exit ${String(initResult.exitCode)}): ${firstMeaningfulLine(initResult.stderr || initResult.stdout)}. ${REFRESH_REMEDY}`,
109
155
  };
110
156
  }
111
157
  // That init just wrote upstream's 99-rule default `exclude`
@@ -126,7 +172,7 @@ export async function refreshCodegraphAfterSlice(projectRoot, runner) {
126
172
  return {
127
173
  refreshed: false,
128
174
  reason: 'index-failed',
129
- note: `auto codegraph refresh failed (exit ${String(result.exitCode)}): ${firstMeaningfulLine(result.stderr || result.stdout)}`,
175
+ note: `auto codegraph refresh failed (exit ${String(result.exitCode)}): ${firstMeaningfulLine(result.stderr || result.stdout)}. ${REFRESH_REMEDY}`,
130
176
  };
131
177
  }
132
178
  return { refreshed: true };
@@ -135,7 +181,7 @@ export async function refreshCodegraphAfterSlice(projectRoot, runner) {
135
181
  return {
136
182
  refreshed: false,
137
183
  reason: 'unavailable',
138
- note: `auto codegraph refresh unavailable: ${errorMessage(error)}`,
184
+ note: `auto codegraph refresh unavailable: ${errorMessage(error)}. ${REFRESH_REMEDY}`,
139
185
  };
140
186
  }
141
187
  }
@@ -0,0 +1,88 @@
1
+ /** Suffix of the byte-exact pre-repair copy kept next to the config. */
2
+ export declare const CODEGRAPH_CONFIG_BACKUP_SUFFIX = ".bak";
3
+ export type CodegraphExcludeRepairPlan = {
4
+ /** True when at least one rule would actually be dropped. */
5
+ readonly changed: boolean;
6
+ /** The `exclude` array after the removal. */
7
+ readonly exclude: readonly string[];
8
+ /** Rules actually present in `exclude` and dropped, in config order. */
9
+ readonly removedRules: readonly string[];
10
+ };
11
+ /**
12
+ * Pure: given the current `exclude` list and the rules to drop, return
13
+ * the new list. No fs, no clock, no serialization.
14
+ *
15
+ * A rule named in `rulesToRemove` but absent from `exclude` is NOT
16
+ * invented — the result is a subset of the input, so a caller can
17
+ * never add a rule by accident. Removing an already-absent rule is a
18
+ * no-op, which is what makes the whole repair idempotent: feeding the
19
+ * repaired list back in yields `changed: false`.
20
+ */
21
+ export declare function repairCodegraphExclude(input: {
22
+ readonly exclude: readonly string[];
23
+ readonly rulesToRemove: readonly string[];
24
+ }): CodegraphExcludeRepairPlan;
25
+ /**
26
+ * Pure: given the current `include` list and the patterns to append, return
27
+ * the new list. The mirror of `repairCodegraphExclude`, in the other
28
+ * direction — a SUPERSET operation instead of a subset one.
29
+ *
30
+ * A pattern already present is not appended twice: the include reconciler
31
+ * already guarantees that, but this function is the writer's own last line
32
+ * of defence, and a duplicated glob in a third-party config would be a
33
+ * visible defect even though it changes no matching behaviour.
34
+ */
35
+ export declare function repairCodegraphInclude(input: {
36
+ readonly include: readonly string[];
37
+ readonly patternsToAdd: readonly string[];
38
+ }): {
39
+ readonly changed: boolean;
40
+ readonly include: readonly string[];
41
+ readonly addedPatterns: readonly string[];
42
+ };
43
+ export type CodegraphConfigRepairPlan = {
44
+ /** Rules from the exclude reconciler to drop. */
45
+ readonly rulesToRemove: readonly string[];
46
+ /** Patterns from the include reconciler to append. */
47
+ readonly includePatternsToAdd: readonly string[];
48
+ };
49
+ export type CodegraphConfigRepairOutcome = {
50
+ readonly applied: false;
51
+ readonly reason: 'nothing-to-repair';
52
+ readonly removedRules: readonly string[];
53
+ readonly addedIncludePatterns: readonly string[];
54
+ } | {
55
+ readonly applied: true;
56
+ readonly configPath: string;
57
+ readonly backupPath: string;
58
+ readonly removedRules: readonly string[];
59
+ readonly addedIncludePatterns: readonly string[];
60
+ readonly excludeCountBefore: number;
61
+ readonly excludeCountAfter: number;
62
+ readonly includeCountBefore: number;
63
+ readonly includeCountAfter: number;
64
+ };
65
+ /**
66
+ * Apply BOTH config repairs to `<projectRoot>/.codegraph/config.json` in one
67
+ * atomic rewrite.
68
+ *
69
+ * No-op (and no write, no mtime change, no backup) when both lists are
70
+ * empty. Otherwise: back up the original bytes to `config.json.bak`, then
71
+ * rewrite the file with `exclude` reduced by exactly the rules that were
72
+ * both requested and present, and `include` extended by exactly the
73
+ * patterns that were both requested and absent.
74
+ *
75
+ * ONE rewrite, not two. A caller that widened `include` in one write and
76
+ * dropped the newly-offending `exclude` rules in a second would leave a
77
+ * window in which the config on disk is worse than it started (the widened
78
+ * include admits a file that a surviving rule then hides from the index),
79
+ * and would need two backups to stay rollback-exact. One rewrite through
80
+ * the same-directory temp file has neither property.
81
+ *
82
+ * Throws only on real fs/parse failures and on the containment refusal
83
+ * above — the caller decides whether that is fatal (`repair-exclude` /
84
+ * `repair-index` → non-zero exit) or a surfaced warning (`init` → keep
85
+ * going, the init itself already succeeded). Both are refusals BEFORE
86
+ * anything is written, so a throw can never half-apply.
87
+ */
88
+ export declare function applyCodegraphConfigRepair(projectRoot: string, repair: CodegraphConfigRepairPlan): CodegraphConfigRepairOutcome;