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
@@ -27,7 +27,7 @@ import { copyFileSync, mkdirSync, readFileSync } from 'node:fs';
27
27
  import { dirname, join, relative } from 'node:path';
28
28
  import { isInsidePath, resolveInputPath, stablePath, stableRealPath } from '../../../../shared/path-utils.js';
29
29
  import { renderMemoryFile, slugify } from '../parsers/frontmatter.js';
30
- import { extractStableProjectMemories, summarizeBackupResult, summarizeExtractResult } from '../parsers/markdown-pure.js';
30
+ import { extractStableProjectMemoriesWithDiagnostics, summarizeBackupResult, summarizeExtractResult } from '../parsers/markdown-pure.js';
31
31
  import { assertInsideProject, assertSafeProjectMemoryDir, assertSafeSessionDir, normalizeRoot, realPathOrThrow } from '../store/paths.js';
32
32
  import { assertSafeMemoryFileContent, writeNewFile } from '../store/atomic-write.js';
33
33
  import { generateMemoryIndexFile, readStoredMemoryNames } from './ranking.js';
@@ -35,11 +35,18 @@ import { listMarkdownFiles } from './search.js';
35
35
  export function createProjectMemoryExtractPlan(options) {
36
36
  const projectRoot = normalizeRoot(options.projectRoot);
37
37
  const primaryMemoryDir = assertSafeProjectMemoryDir(projectRoot);
38
- const extractedMemories = options.artifactPaths.flatMap((artifactPath) => {
38
+ const extractedMemories = [];
39
+ const droppedBlocks = [];
40
+ for (const artifactPath of options.artifactPaths) {
39
41
  const safeArtifactPath = assertInsideProject(artifactPath, projectRoot);
40
42
  const relativeArtifactPath = relative(projectRoot, safeArtifactPath).replaceAll('\\', '/');
41
- return extractStableProjectMemories(readFileSync(safeArtifactPath, 'utf8'), relativeArtifactPath);
42
- }).sort((left, right) => slugify(left.title).localeCompare(slugify(right.title)));
43
+ const extracted = extractStableProjectMemoriesWithDiagnostics(readFileSync(safeArtifactPath, 'utf8'), relativeArtifactPath);
44
+ // Push order + the sort below are the pre-existing ordering contract:
45
+ // artifacts in argument order, memories sorted by slug.
46
+ extractedMemories.push(...extracted.memories);
47
+ droppedBlocks.push(...extracted.dropped);
48
+ }
49
+ extractedMemories.sort((left, right) => slugify(left.title).localeCompare(slugify(right.title)));
43
50
  const slugCounts = new Map();
44
51
  for (const memory of extractedMemories) {
45
52
  const slug = slugify(memory.title);
@@ -60,7 +67,8 @@ export function createProjectMemoryExtractPlan(options) {
60
67
  primaryMemoryDir,
61
68
  backupPolicy: 'project-memory-primary-artifact-backup',
62
69
  extractedMemories,
63
- plannedWrites
70
+ plannedWrites,
71
+ droppedBlocks
64
72
  };
65
73
  }
66
74
  export function executeProjectMemoryExtract(options) {
@@ -177,22 +185,45 @@ export function extractSessionMemories(options) {
177
185
  scannedFiles: 0,
178
186
  extractedCount: 0,
179
187
  writtenFiles: [],
180
- updatedIndex: false
188
+ updatedIndex: false,
189
+ droppedBlocks: [],
190
+ scanFailures: []
181
191
  };
182
192
  }
183
193
  throw error;
184
194
  }
185
195
  const scannedFiles = listMarkdownFiles(sessionDir, { maxDepth: 6, skipDotfiles: true });
186
196
  const allExtracted = [];
197
+ // Symmetric with `createProjectMemoryExtractPlan`, which has reported the
198
+ // rejected blocks since M2. This path used to call the diagnostic-free
199
+ // projection, so a session handoff whose blocks were all malformed returned
200
+ // `extractedCount: 0` with nothing anywhere saying a block had been found.
201
+ const droppedBlocks = [];
202
+ const scanFailures = [];
187
203
  for (const filePath of scannedFiles) {
204
+ const relativePath = relative(projectRoot, filePath).replaceAll('\\', '/');
188
205
  try {
189
206
  const content = readFileSync(filePath, 'utf8');
190
- const relativePath = relative(projectRoot, filePath).replaceAll('\\', '/');
191
- const extracted = extractStableProjectMemories(content, relativePath);
192
- allExtracted.push(...extracted);
207
+ const extracted = extractStableProjectMemoriesWithDiagnostics(content, relativePath);
208
+ allExtracted.push(...extracted.memories);
209
+ droppedBlocks.push(...extracted.dropped);
193
210
  }
194
- catch { // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
195
- // skip unreadable files
211
+ catch (error) {
212
+ // G2 resolution for this site: this was a bare `catch {}` carrying the
213
+ // repo-wide G2 grace marker that `scripts/lint/silent-warning-detector.mjs`
214
+ // reads (anti-pattern #1, `empty-catch`). The throw is no longer
215
+ // swallowed — the file is named in the same `warnings` channel as the
216
+ // block drops, which is what the grace period was deferring, so the
217
+ // marker is gone from this line. Still non-fatal: one unreadable artifact
218
+ // must not abort the scan of the rest, so the catch keeps its control
219
+ // flow and only gains a channel.
220
+ //
221
+ // `relativePath` is computed above the `try` so the catch can name the
222
+ // file. That is safe rather than convenient: `path.relative` is pure
223
+ // string arithmetic on two already-validated absolute path strings and
224
+ // does not throw for them, so hoisting it cannot turn a path-join problem
225
+ // into a fatal error that the old swallow would have absorbed.
226
+ scanFailures.push({ file: relativePath, detail: error instanceof Error ? error.message : String(error) });
196
227
  }
197
228
  }
198
229
  if (allExtracted.length === 0) {
@@ -205,7 +236,9 @@ export function extractSessionMemories(options) {
205
236
  scannedFiles: scannedFiles.length,
206
237
  extractedCount: 0,
207
238
  writtenFiles: [],
208
- updatedIndex: false
239
+ updatedIndex: false,
240
+ droppedBlocks,
241
+ scanFailures
209
242
  };
210
243
  }
211
244
  const slugCounts = new Map();
@@ -248,6 +281,8 @@ export function extractSessionMemories(options) {
248
281
  scannedFiles: scannedFiles.length,
249
282
  extractedCount: allExtracted.length,
250
283
  writtenFiles,
251
- updatedIndex: apply && writtenFiles.length > 0
284
+ updatedIndex: apply && writtenFiles.length > 0,
285
+ droppedBlocks,
286
+ scanFailures
252
287
  };
253
288
  }
@@ -1,8 +1,10 @@
1
- export type { BackupPlanOptions, ExtractedProjectMemory, MemoryKindTier, ExtractPlanOptions, ExtractSessionMemoriesOptions, ExtractSessionMemoriesResult, MemoryIndex, MemoryIndexEntry, ProjectMemoryBackupPlan, ProjectMemoryBackupResult, ProjectMemoryBackupSummary, ProjectMemoryCopy, ProjectMemoryExtractPlan, ProjectMemoryExtractResult, ProjectMemoryExtractSummary, ProjectMemoryKind, ProjectMemoryReadResult, ProjectMemoryShowResult, ProjectMemoryWrite, StoredProjectMemory } from './types.js';
1
+ export type { BackupPlanOptions, ExtractedProjectMemory, MemoryBlockDrop, MemoryBlockDropReason, MemoryBlockParse, MemoryKindTier, ExtractPlanOptions, ExtractSessionMemoriesOptions, ExtractSessionMemoriesResult, MemoryIndex, MemoryIndexEntry, ProjectMemoryBackupPlan, ProjectMemoryBackupResult, ProjectMemoryBackupSummary, ProjectMemoryCopy, ProjectMemoryExtractPlan, ProjectMemoryExtractResult, ProjectMemoryExtractSummary, ProjectMemoryKind, ProjectMemoryReadResult, ProjectMemoryShowResult, ProjectMemoryWrite, StoredProjectMemory } from './types.js';
2
2
  export { VALID_PROJECT_MEMORY_KINDS } from './parsers/frontmatter.js';
3
3
  export { HOT_MEMORY_KINDS, MEMORY_KIND_TIER, PROJECT_MEMORY_KINDS, WARM_MEMORY_KINDS } from './types.js';
4
- export { summarizeBackupResult, summarizeExtractResult, summarizeMemoryBody, extractStableProjectMemories, END_MARKER, START_MARKER } from './parsers/markdown-pure.js';
5
- export { parseBlock, parseMemoryFrontmatter, parseStoredMemoryFile, renderMemoryFile, resolveMemoryKind, resolveMemoryName, slugify } from './parsers/frontmatter.js';
4
+ export { describeMemoryBlockDrops, describeSessionScanFailures, summarizeBackupResult, summarizeExtractResult, summarizeMemoryBody, extractStableProjectMemories, extractStableProjectMemoriesWithDiagnostics, END_MARKER, START_MARKER } from './parsers/markdown-pure.js';
5
+ export type { SessionScanFailure } from './types.js';
6
+ export type { ExtractedMemoryBlocks } from './parsers/markdown-pure.js';
7
+ export { parseBlock, parseBlockResult, parseMemoryFrontmatter, parseStoredMemoryFile, renderMemoryFile, resolveMemoryKind, resolveMemoryName, slugify } from './parsers/frontmatter.js';
6
8
  export type { MemoryKindResolution, MemoryKindSource, MemoryNameResolution, MemoryNameSource, ParsedMemoryFrontmatter } from './parsers/frontmatter.js';
7
9
  export { assertInsideProject, assertSafeProjectMemoryDir, assertSafeSessionDir, normalizeRealRoot, normalizeRoot, realPathOrThrow, resolveProjectPath, safeRealpath } from './store/paths.js';
8
10
  export { assertSafeMemory, assertSafeMemoryFileContent, hasSensitiveMemoryContent, writeNewFile } from './store/atomic-write.js';
@@ -14,9 +14,9 @@ export { VALID_PROJECT_MEMORY_KINDS } from './parsers/frontmatter.js';
14
14
  // Canonical kind vocabulary + hot/warm tier map (slice E)
15
15
  export { HOT_MEMORY_KINDS, MEMORY_KIND_TIER, PROJECT_MEMORY_KINDS, WARM_MEMORY_KINDS } from './types.js';
16
16
  // Pure markdown helpers
17
- export { summarizeBackupResult, summarizeExtractResult, summarizeMemoryBody, extractStableProjectMemories, END_MARKER, START_MARKER } from './parsers/markdown-pure.js';
17
+ export { describeMemoryBlockDrops, describeSessionScanFailures, summarizeBackupResult, summarizeExtractResult, summarizeMemoryBody, extractStableProjectMemories, extractStableProjectMemoriesWithDiagnostics, END_MARKER, START_MARKER } from './parsers/markdown-pure.js';
18
18
  // Frontmatter parser + renderer
19
- export { parseBlock, parseMemoryFrontmatter, parseStoredMemoryFile, renderMemoryFile, resolveMemoryKind, resolveMemoryName, slugify } from './parsers/frontmatter.js';
19
+ export { parseBlock, parseBlockResult, parseMemoryFrontmatter, parseStoredMemoryFile, renderMemoryFile, resolveMemoryKind, resolveMemoryName, slugify } from './parsers/frontmatter.js';
20
20
  // Store: path safety + sensitive content
21
21
  export { assertInsideProject, assertSafeProjectMemoryDir, assertSafeSessionDir, normalizeRealRoot, normalizeRoot, realPathOrThrow, resolveProjectPath, safeRealpath } from './store/paths.js';
22
22
  export { assertSafeMemory, assertSafeMemoryFileContent, hasSensitiveMemoryContent, writeNewFile } from './store/atomic-write.js';
@@ -1,4 +1,4 @@
1
- import type { ExtractedProjectMemory, ProjectMemoryKind, StoredProjectMemory } from '../types.js';
1
+ import type { ExtractedProjectMemory, MemoryBlockParse, ProjectMemoryKind, StoredProjectMemory } from '../types.js';
2
2
  /** Accepted-kind set, derived from the canonical `PROJECT_MEMORY_KINDS`
3
3
  * tuple so the parser cannot drift from the union type / tier map. */
4
4
  export declare const VALID_MEMORY_KINDS: ReadonlySet<ProjectMemoryKind>;
@@ -6,6 +6,20 @@ export declare const VALID_MEMORY_KINDS: ReadonlySet<ProjectMemoryKind>;
6
6
  * set (CLI help text, `--kind` validation) without duplicating the literal. */
7
7
  export declare const VALID_PROJECT_MEMORY_KINDS: readonly ProjectMemoryKind[];
8
8
  export declare function slugify(title: string): string;
9
+ /**
10
+ * Single implementation of the extract-path block parse. Returns a
11
+ * discriminated result so the caller can say WHY a found block was rejected.
12
+ *
13
+ * The precondition order mirrors the original combined `if` exactly
14
+ * (separator → title → kind present → kind valid → body non-empty), so the
15
+ * accepted set is bit-for-bit what it always was; only the explanation is new.
16
+ */
17
+ export declare function parseBlockResult(block: string, sourceArtifact: string): MemoryBlockParse;
18
+ /**
19
+ * `null`-on-failure projection of `parseBlockResult`. Kept because callers
20
+ * (and the parser's own contract) consume a plain nullable value; both views
21
+ * come from the one implementation above, so they cannot diverge.
22
+ */
9
23
  export declare function parseBlock(block: string, sourceArtifact: string): ExtractedProjectMemory | null;
10
24
  export declare function renderMemoryFile(memory: ExtractedProjectMemory): string;
11
25
  /**
@@ -31,11 +31,20 @@ export function slugify(title) {
31
31
  const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
32
32
  return slug.length > 0 ? slug : 'project-memory';
33
33
  }
34
- export function parseBlock(block, sourceArtifact) {
34
+ /**
35
+ * Single implementation of the extract-path block parse. Returns a
36
+ * discriminated result so the caller can say WHY a found block was rejected.
37
+ *
38
+ * The precondition order mirrors the original combined `if` exactly
39
+ * (separator → title → kind present → kind valid → body non-empty), so the
40
+ * accepted set is bit-for-bit what it always was; only the explanation is new.
41
+ */
42
+ export function parseBlockResult(block, sourceArtifact) {
35
43
  const normalizedBlock = block.replace(/\r\n/g, '\n');
36
44
  const separatorIndex = normalizedBlock.indexOf('\n---\n');
37
- if (separatorIndex < 0)
38
- return null;
45
+ if (separatorIndex < 0) {
46
+ return { ok: false, reason: 'missing-separator', detail: "block header is not followed by a '---' separator line" };
47
+ }
39
48
  const header = normalizedBlock.slice(0, separatorIndex).trim();
40
49
  const body = normalizedBlock.slice(separatorIndex + '\n---\n'.length).trim();
41
50
  const fields = new Map();
@@ -49,9 +58,28 @@ export function parseBlock(block, sourceArtifact) {
49
58
  }
50
59
  const title = fields.get('title')?.trim();
51
60
  const kind = fields.get('kind')?.trim();
52
- if (!title || !kind || !VALID_MEMORY_KINDS.has(kind) || body.length === 0)
53
- return null;
54
- return { title, kind, body, sourceArtifact };
61
+ if (!title) {
62
+ return { ok: false, reason: 'missing-title', detail: "block header has no non-empty 'title:' field" };
63
+ }
64
+ if (!kind) {
65
+ return { ok: false, reason: 'missing-kind', detail: "block header has no non-empty 'kind:' field" };
66
+ }
67
+ if (!VALID_MEMORY_KINDS.has(kind)) {
68
+ return { ok: false, reason: 'unknown-kind', detail: `block declares kind '${kind}', which is not an accepted memory kind` };
69
+ }
70
+ if (body.length === 0) {
71
+ return { ok: false, reason: 'empty-body', detail: 'block body is empty' };
72
+ }
73
+ return { ok: true, memory: { title, kind, body, sourceArtifact } };
74
+ }
75
+ /**
76
+ * `null`-on-failure projection of `parseBlockResult`. Kept because callers
77
+ * (and the parser's own contract) consume a plain nullable value; both views
78
+ * come from the one implementation above, so they cannot diverge.
79
+ */
80
+ export function parseBlock(block, sourceArtifact) {
81
+ const parsed = parseBlockResult(block, sourceArtifact);
82
+ return parsed.ok ? parsed.memory : null;
55
83
  }
56
84
  export function renderMemoryFile(memory) {
57
85
  const name = slugify(memory.title);
@@ -1,7 +1,33 @@
1
- import type { ExtractedProjectMemory, ProjectMemoryBackupResult, ProjectMemoryBackupSummary, ProjectMemoryExtractResult, ProjectMemoryExtractSummary } from '../types.js';
1
+ import type { ExtractedProjectMemory, MemoryBlockDrop, ProjectMemoryBackupResult, ProjectMemoryBackupSummary, ProjectMemoryExtractResult, ProjectMemoryExtractSummary, SessionScanFailure } from '../types.js';
2
2
  export declare const START_MARKER = "<!-- peaks-memory:start -->";
3
3
  export declare const END_MARKER = "<!-- peaks-memory:end -->";
4
4
  export declare function summarizeMemoryBody(body: string): string;
5
+ /** Extracted memories plus the blocks that were found and rejected. */
6
+ export type ExtractedMemoryBlocks = {
7
+ memories: ExtractedProjectMemory[];
8
+ dropped: MemoryBlockDrop[];
9
+ };
10
+ /**
11
+ * Same scan as `extractStableProjectMemories`, but also reports the blocks
12
+ * that were found between the markers and rejected by the parser.
13
+ *
14
+ * `extractStableProjectMemories` is the `.memories` projection of this, so the
15
+ * extracted set is identical by construction — the diagnostics cannot change
16
+ * which blocks are accepted.
17
+ */
18
+ export declare function extractStableProjectMemoriesWithDiagnostics(content: string, sourceArtifact: string): ExtractedMemoryBlocks;
5
19
  export declare function extractStableProjectMemories(content: string, sourceArtifact: string): ExtractedProjectMemory[];
20
+ /**
21
+ * Render one warning line per rejected block, for the CLI envelope's
22
+ * `warnings` channel. `unknown-kind` additionally names the accepted
23
+ * vocabulary, because that is the failure whose remedy is a value change.
24
+ */
25
+ export declare function describeMemoryBlockDrops(dropped: ReadonlyArray<MemoryBlockDrop>): string[];
26
+ /**
27
+ * Render one warning line per session artifact that could not be read. The
28
+ * sibling of `describeMemoryBlockDrops`, for the failure one level coarser: a
29
+ * whole file that never yielded blocks at all.
30
+ */
31
+ export declare function describeSessionScanFailures(failures: ReadonlyArray<SessionScanFailure>): string[];
6
32
  export declare function summarizeExtractResult(result: ProjectMemoryExtractResult): ProjectMemoryExtractSummary;
7
33
  export declare function summarizeBackupResult(result: ProjectMemoryBackupResult): ProjectMemoryBackupSummary;
@@ -20,9 +20,32 @@
20
20
  // No filesystem imports. Pure functions only — easy to unit-test.
21
21
  // ---------------------------------------------------------------------------
22
22
  import { assertSafeMemory } from '../store/atomic-write.js';
23
- import { parseBlock, slugify } from './frontmatter.js';
23
+ import { parseBlockResult, slugify, VALID_MEMORY_KINDS } from './frontmatter.js';
24
24
  export const START_MARKER = '<!-- peaks-memory:start -->';
25
25
  export const END_MARKER = '<!-- peaks-memory:end -->';
26
+ /**
27
+ * A marker-shaped HTML comment: `<!--` then horizontal space then
28
+ * `peaks-memory:start` / `:end`, then anything up to the closing `-->`.
29
+ *
30
+ * This deliberately matches the exact markers TOO — the caller skips those by
31
+ * comparing against `START_MARKER` / `END_MARKER`, so the two literals stay the
32
+ * single source of truth instead of being re-spelled here.
33
+ *
34
+ * Anchoring on `peaks-memory:` immediately after the comment open is what keeps
35
+ * this from firing on prose: a comment that merely *mentions* the marker
36
+ * (`<!-- see the peaks-memory:start docs -->`) does not match, because the text
37
+ * after `<!--` is `see`, not `peaks-memory:`. Only an attempted marker matches.
38
+ */
39
+ const MARKER_SHAPED_COMMENT = /<!--[ \t]*peaks-memory:(start|end)\b[^>]*-->/g;
40
+ /**
41
+ * Why a marker-shaped comment was not usable, as a warning line. Names both the
42
+ * text that was found and the literal the locator wanted, because the whole
43
+ * failure is "these two differ in a way nothing told you about".
44
+ */
45
+ function describeUnrecognizedMarker(found) {
46
+ const wanted = found.includes(':end') ? END_MARKER : START_MARKER;
47
+ return `found ${JSON.stringify(found)}, which is not the exact marker ${JSON.stringify(wanted)} — the locator searches for that literal, so any block this opens is never found`;
48
+ }
26
49
  // Length bounds for index entry descriptions. The numbers were chosen when
27
50
  // summarizeMemoryBody was first introduced; locking them in as named
28
51
  // constants is a doc-as-code move so the truncation rule is no longer
@@ -47,8 +70,21 @@ export function summarizeMemoryBody(body) {
47
70
  }
48
71
  return first.slice(0, MAX_DESCRIPTION_LENGTH - ELLIPSIS_RESERVE) + '...';
49
72
  }
50
- export function extractStableProjectMemories(content, sourceArtifact) {
73
+ /**
74
+ * Same scan as `extractStableProjectMemories`, but also reports the blocks
75
+ * that were found between the markers and rejected by the parser.
76
+ *
77
+ * `extractStableProjectMemories` is the `.memories` projection of this, so the
78
+ * extracted set is identical by construction — the diagnostics cannot change
79
+ * which blocks are accepted.
80
+ */
81
+ export function extractStableProjectMemoriesWithDiagnostics(content, sourceArtifact) {
51
82
  const memories = [];
83
+ // Kept as `{index, drop}` pairs so located-block drops and near-miss-marker
84
+ // drops can be reported in DOCUMENT order through one channel. The index is
85
+ // ordering metadata only — it never reaches the caller.
86
+ const drops = [];
87
+ const locatedRanges = [];
52
88
  let searchStart = 0;
53
89
  while (searchStart < content.length) {
54
90
  const start = content.indexOf(START_MARKER, searchStart);
@@ -58,14 +94,63 @@ export function extractStableProjectMemories(content, sourceArtifact) {
58
94
  const end = content.indexOf(END_MARKER, bodyStart);
59
95
  if (end < 0)
60
96
  break;
61
- const memory = parseBlock(content.slice(bodyStart, end).trim(), sourceArtifact);
62
- if (memory) {
63
- assertSafeMemory(memory);
64
- memories.push(memory);
97
+ locatedRanges.push([start, end + END_MARKER.length]);
98
+ const parsed = parseBlockResult(content.slice(bodyStart, end).trim(), sourceArtifact);
99
+ if (parsed.ok) {
100
+ assertSafeMemory(parsed.memory);
101
+ memories.push(parsed.memory);
102
+ }
103
+ else {
104
+ drops.push({ index: start, drop: { sourceArtifact, reason: parsed.reason, detail: parsed.detail } });
65
105
  }
66
106
  searchStart = end + END_MARKER.length;
67
107
  }
68
- return memories.sort((left, right) => slugify(left.title).localeCompare(slugify(right.title)));
108
+ // Near-miss markers. These are invisible to the loop above by construction —
109
+ // it navigates by exact `indexOf` — so before this pass an artifact written
110
+ // with, say, an attribute inside the marker reported `extractedCount: 0` with
111
+ // `warnings: []`, indistinguishable from an artifact that had no blocks at
112
+ // all. Nothing is extracted here: this pass only names what was not found.
113
+ //
114
+ // A near-miss INSIDE a located block's span is the block's own body text, not
115
+ // an attempted marker, so it is skipped rather than reported.
116
+ for (const match of content.matchAll(MARKER_SHAPED_COMMENT)) {
117
+ const found = match[0];
118
+ if (found === START_MARKER || found === END_MARKER)
119
+ continue;
120
+ const index = match.index ?? 0;
121
+ if (locatedRanges.some(([from, to]) => index >= from && index < to))
122
+ continue;
123
+ drops.push({ index, drop: { sourceArtifact, reason: 'unrecognized-marker', detail: describeUnrecognizedMarker(found) } });
124
+ }
125
+ drops.sort((left, right) => left.index - right.index);
126
+ return {
127
+ memories: memories.sort((left, right) => slugify(left.title).localeCompare(slugify(right.title))),
128
+ dropped: drops.map((entry) => entry.drop)
129
+ };
130
+ }
131
+ export function extractStableProjectMemories(content, sourceArtifact) {
132
+ return extractStableProjectMemoriesWithDiagnostics(content, sourceArtifact).memories;
133
+ }
134
+ /**
135
+ * Render one warning line per rejected block, for the CLI envelope's
136
+ * `warnings` channel. `unknown-kind` additionally names the accepted
137
+ * vocabulary, because that is the failure whose remedy is a value change.
138
+ */
139
+ export function describeMemoryBlockDrops(dropped) {
140
+ return dropped.map((drop) => {
141
+ const hint = drop.reason === 'unknown-kind'
142
+ ? ` Accepted kinds: ${[...VALID_MEMORY_KINDS].join(', ')}.`
143
+ : '';
144
+ return `Skipped a memory block in ${drop.sourceArtifact}: ${drop.detail}.${hint}`;
145
+ });
146
+ }
147
+ /**
148
+ * Render one warning line per session artifact that could not be read. The
149
+ * sibling of `describeMemoryBlockDrops`, for the failure one level coarser: a
150
+ * whole file that never yielded blocks at all.
151
+ */
152
+ export function describeSessionScanFailures(failures) {
153
+ return failures.map((failure) => `Could not read the session artifact ${failure.file}: ${failure.detail}.`);
69
154
  }
70
155
  export function summarizeExtractResult(result) {
71
156
  return {
@@ -35,6 +35,50 @@ export type ExtractedProjectMemory = {
35
35
  body: string;
36
36
  sourceArtifact: string;
37
37
  };
38
+ /**
39
+ * Why a `<!-- peaks-memory:start -->` block that was FOUND was not extracted.
40
+ *
41
+ * One value per precondition in `parseBlockResult` (the extract path's block
42
+ * parser), in evaluation order. Before this existed, every one of these
43
+ * conditions collapsed into a bare `null` and the caller reported nothing, so
44
+ * `extractedCount: N` was indistinguishable from "the block was never there".
45
+ * Naming the cause is the whole point: a memory block that disappears must say
46
+ * why it disappeared.
47
+ *
48
+ * Diagnostics only — this type does not change WHICH blocks are accepted.
49
+ */
50
+ export type MemoryBlockDropReason = 'missing-separator' | 'missing-title' | 'missing-kind' | 'unknown-kind' | 'empty-body'
51
+ /**
52
+ * NOT a `parseBlockResult` precondition — this one comes from the scanner.
53
+ * A marker-shaped comment that is not the exact literal the locator searches
54
+ * for. The block it opens is therefore never found: it is not "rejected",
55
+ * it is invisible. Reporting it is the whole point, because nothing else in
56
+ * the pipeline can see it.
57
+ */
58
+ | 'unrecognized-marker';
59
+ /** A found-but-not-extracted memory block, with the precondition that failed. */
60
+ export type MemoryBlockDrop = {
61
+ /** Artifact path the block was found in (project-relative when extracted). */
62
+ sourceArtifact: string;
63
+ reason: MemoryBlockDropReason;
64
+ /** Human-readable explanation of the failed precondition. */
65
+ detail: string;
66
+ };
67
+ /**
68
+ * Result of parsing one memory block, as a discriminated union.
69
+ *
70
+ * `parseBlock` is the `null`-on-failure projection of this; both are produced
71
+ * by the same single implementation so they cannot disagree about which blocks
72
+ * are accepted.
73
+ */
74
+ export type MemoryBlockParse = {
75
+ ok: true;
76
+ memory: ExtractedProjectMemory;
77
+ } | {
78
+ ok: false;
79
+ reason: MemoryBlockDropReason;
80
+ detail: string;
81
+ };
38
82
  export type ProjectMemoryWrite = {
39
83
  memory: ExtractedProjectMemory;
40
84
  filePath: string;
@@ -47,6 +91,12 @@ export type ProjectMemoryExtractPlan = {
47
91
  backupPolicy: 'project-memory-primary-artifact-backup';
48
92
  extractedMemories: ExtractedProjectMemory[];
49
93
  plannedWrites: ProjectMemoryWrite[];
94
+ /**
95
+ * Blocks that were found between the markers but rejected by the parser.
96
+ * Reported to the user through the CLI envelope's `warnings` channel.
97
+ * Purely informational — this array does not influence extraction.
98
+ */
99
+ droppedBlocks: MemoryBlockDrop[];
50
100
  };
51
101
  export type ProjectMemoryExtractResult = ProjectMemoryExtractPlan & {
52
102
  writtenFiles: string[];
@@ -123,6 +173,22 @@ export type ExtractSessionMemoriesOptions = {
123
173
  sessionId: string;
124
174
  apply?: boolean;
125
175
  };
176
+ /**
177
+ * A session artifact that could not be read at all, so its blocks were never
178
+ * even candidates.
179
+ *
180
+ * Distinct from `MemoryBlockDrop`: that one describes a BLOCK that was found and
181
+ * rejected, and needs the artifact to have been readable in the first place.
182
+ * Folding a whole-file read failure into it would make `sourceArtifact` mean two
183
+ * different things, so this rides its own field and is rendered by its own
184
+ * `describeSessionScanFailures`.
185
+ */
186
+ export type SessionScanFailure = {
187
+ /** Project-relative path of the artifact that could not be read. */
188
+ file: string;
189
+ /** The underlying error message. Never invented; taken from the throw. */
190
+ detail: string;
191
+ };
126
192
  export type ExtractSessionMemoriesResult = {
127
193
  apply: boolean;
128
194
  projectRoot: string;
@@ -133,6 +199,26 @@ export type ExtractSessionMemoriesResult = {
133
199
  extractedCount: number;
134
200
  writtenFiles: string[];
135
201
  updatedIndex: boolean;
202
+ /**
203
+ * Blocks that were FOUND between the markers in this session's artifacts but
204
+ * rejected by the parser. Same diagnostic contract as
205
+ * `ProjectMemoryExtractPlan.droppedBlocks` — the sibling `peaks memory
206
+ * extract` path has carried this since M2, and this one silently dropped
207
+ * them, so `extractedCount: 1` out of three blocks was indistinguishable
208
+ * from "there was one block".
209
+ *
210
+ * Purely informational: this array does not influence which blocks are
211
+ * extracted, and it adds no field to the CLI's `data` payload — the reasons
212
+ * ride the existing envelope `warnings` channel.
213
+ */
214
+ droppedBlocks: MemoryBlockDrop[];
215
+ /**
216
+ * Session artifacts that could not be read, so their blocks were never
217
+ * candidates. Previously swallowed by a bare `catch {}`; reported now through
218
+ * the same CLI `warnings` channel. Diagnostic only — an unreadable artifact
219
+ * is still not an error, and the scan is otherwise unchanged.
220
+ */
221
+ scanFailures: SessionScanFailure[];
136
222
  };
137
223
  export type ProjectMemoryShowResult = {
138
224
  projectRoot: string;
@@ -69,7 +69,7 @@ export type SliceCheckResult = {
69
69
  };
70
70
  export type SliceCheckOptions = {
71
71
  projectRoot: string;
72
- /** When omitted, slice check inspects `.peaks/_runtime/current-change` to find the active rid. */
72
+ /** REQUIRED. The `.peaks/_runtime/current-change` binding file is gone (slice 2026-06-29-change-id-root-removal); when omitted, slice check throws rather than guessing. */
73
73
  rid?: string;
74
74
  /**
75
75
  * When true, re-run the 3-way review fan-out (peaks-rd's code-review +
@@ -0,0 +1,91 @@
1
+ /**
2
+ * The canonical top-level layout of `.peaks/_runtime/`.
3
+ *
4
+ * WHY THIS MODULE EXISTS
5
+ *
6
+ * `.peaks/_runtime/` holds two populations whose names look identical:
7
+ *
8
+ * 1. SESSION DIRS — `<YYYY-MM-DD>-session-<hex>`, one per peaks session.
9
+ * Anything else that *looks* like a session id but is not one is a
10
+ * defect (a test that passed `--session-id x`, a typo, an old
11
+ * `unknown-sid` bucket).
12
+ * 2. SYSTEM ENTRIES — dirs and files the code itself writes there on
13
+ * purpose: `callers/`, `change/`, `benchmarks/`, `session.json`, …
14
+ *
15
+ * The doctor check `L3:l3-orphan-sessions` must flag (1) and tolerate (2),
16
+ * and the only thing that can tell them apart is a list of the system
17
+ * entries. That list used to live inside the check as a hand-maintained
18
+ * `new Set(['change'])` — and it had already drifted: `callers/` is a
19
+ * designed location (`caller-binding-service.ts` stores
20
+ * `.peaks/_runtime/callers/<callerId>.json`), was never added, and made
21
+ * `peaks doctor` exit non-zero on a clean workspace *permanently*
22
+ * (`4 orphan session(s) …: callers, cli, unknown-sid, x`). The check's own
23
+ * doc comment asked future maintainers to keep a *second* list — the prose
24
+ * `RUNTIME_SYSTEM_SUBDIRS_DOC` — in sync by hand. Prose cannot enforce
25
+ * itself, so the two lists diverged exactly as you would expect.
26
+ *
27
+ * WHY DERIVATION WAS REJECTED AND A REGISTRY IS THE SOURCE OF TRUTH
28
+ *
29
+ * A derivation would have to classify a bare name as "system" or "bogus"
30
+ * without a list. There is no signal to derive from: `callers` (system) and
31
+ * `x` (bogus) are the same shape — a lowercase word. The session-id regex
32
+ * separates them from `2026-09-16-session-5bcf09`, not from each other. So
33
+ * the alternatives were (a) keep a literal in the check, or (b) put the
34
+ * literal *and its consumers* in one place. This module is (b): the check
35
+ * imports the set instead of re-declaring it, and
36
+ * `tests/unit/workspace/runtime-layout-drift-guard.test.ts` parses `src/**`
37
+ * and fails when the code writes a `_runtime` child that is not registered
38
+ * here. The registry can no longer drift silently — it can only drift loudly,
39
+ * as a red test.
40
+ *
41
+ * The guard parses ASTs rather than grepping, because this repository
42
+ * documents the layout in prose constantly: `migrate-1-4-1-service.ts` has an
43
+ * array whose two adjacent string literals are `'_runtime', '_sub_agents'`
44
+ * (they are two *separate* SKIP entries), and `evidence-generator.ts`
45
+ * describes a past escape as `` `.peaks/_runtime/pwned.md` `` inside a
46
+ * comment. Both are false positives for a text scan and invisible to an AST
47
+ * walk. See `tests/unit/runtime/no-runtime-input-guard.test.ts` for the same
48
+ * decision made the same way.
49
+ */
50
+ /**
51
+ * `dir` entries are scanned by `L3:l3-orphan-sessions` and must not be
52
+ * flagged. `file` entries are never flagged by that check — it only reads
53
+ * directories — and are registered so the drift guard can classify them and
54
+ * so this module stays an honest census of the tree rather than half of one.
55
+ */
56
+ export type RuntimeEntryKind = 'dir' | 'file';
57
+ export interface RuntimeSystemEntry {
58
+ /** The exact top-level name under `.peaks/_runtime/`. */
59
+ readonly name: string;
60
+ readonly kind: RuntimeEntryKind;
61
+ /** What writes it and why it is legitimate. */
62
+ readonly purpose: string;
63
+ }
64
+ /**
65
+ * Reserved top-level dir for session-less command output.
66
+ *
67
+ * `peaks baseline audit` runs with no bound session (it is the only scorer
68
+ * that can run inside the secretless OIDC publish gate), so it has no
69
+ * `<sid>` to write under. It used to pass the literal `'cli'` as its
70
+ * sessionId, which made `.peaks/_runtime/cli/capability-audit/*.json` look
71
+ * exactly like a session dir that failed validation — `peaks doctor` has
72
+ * been red on this repository ever since. The name is underscore-prefixed to
73
+ * match the tree's existing convention for "machinery, not a session"
74
+ * (`_runtime`, `_sub_agents`) and to make it impossible to mistake for a
75
+ * session id.
76
+ */
77
+ export declare const RUNTIME_SESSIONLESS_SCOPE = "_audit";
78
+ /**
79
+ * Every legitimate top-level entry under `.peaks/_runtime/`.
80
+ *
81
+ * This is the list `L3:l3-orphan-sessions` tolerates. Adding an entry that
82
+ * nothing writes is a red test (the drift guard's reverse direction); writing
83
+ * an entry that is not here is also a red test. Both directions are pinned in
84
+ * `tests/unit/workspace/runtime-layout-drift-guard.test.ts`.
85
+ */
86
+ export declare const RUNTIME_SYSTEM_ENTRIES: readonly RuntimeSystemEntry[];
87
+ /**
88
+ * The set `L3:l3-orphan-sessions` filters with. Derived from the registry
89
+ * above so the check and the census cannot disagree about the same name.
90
+ */
91
+ export declare const RUNTIME_SYSTEM_SUBDIRS: ReadonlySet<string>;