peaks-loop 4.0.34 → 4.0.35

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 (37) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/cli/commands/core/memory-command.js +61 -3
  3. package/dist/cli/commands/dispatch-commands.js +4 -2
  4. package/dist/cli/commands/memory-commands.d.ts +35 -0
  5. package/dist/cli/commands/memory-commands.js +119 -10
  6. package/dist/services/context/context-schema.d.ts +1 -1
  7. package/dist/services/context/memory-index-reader.d.ts +26 -0
  8. package/dist/services/context/memory-index-reader.js +62 -30
  9. package/dist/services/context/memory-preflight-config.d.ts +33 -0
  10. package/dist/services/context/memory-preflight-config.js +32 -2
  11. package/dist/services/context/memory-preflight-service.d.ts +20 -1
  12. package/dist/services/context/memory-preflight-service.js +198 -31
  13. package/dist/services/doctor/doctor-service/checks/l3-memory-health.d.ts +19 -2
  14. package/dist/services/doctor/doctor-service/checks/l3-memory-health.js +143 -19
  15. package/dist/services/job/job-types.d.ts +3 -3
  16. package/dist/services/memory/memory-ingest-service.d.ts +79 -0
  17. package/dist/services/memory/memory-ingest-service.js +225 -0
  18. package/dist/services/memory/memory-rotate-service.d.ts +88 -0
  19. package/dist/services/memory/memory-rotate-service.js +373 -0
  20. package/dist/services/memory/project-memory-service/index/ranking.d.ts +9 -1
  21. package/dist/services/memory/project-memory-service/index/ranking.js +25 -13
  22. package/dist/services/memory/project-memory-service/index/reindex.d.ts +75 -0
  23. package/dist/services/memory/project-memory-service/index/reindex.js +207 -0
  24. package/dist/services/memory/project-memory-service/index/search.js +14 -24
  25. package/dist/services/memory/project-memory-service/index.d.ts +7 -3
  26. package/dist/services/memory/project-memory-service/index.js +6 -2
  27. package/dist/services/memory/project-memory-service/parsers/frontmatter.d.ts +75 -3
  28. package/dist/services/memory/project-memory-service/parsers/frontmatter.js +113 -24
  29. package/dist/services/memory/project-memory-service/types.d.ts +31 -1
  30. package/dist/services/memory/project-memory-service/types.js +76 -1
  31. package/dist/services/preferences/preferences-types.d.ts +14 -0
  32. package/dist/services/preferences/preferences-types.js +8 -0
  33. package/dist/services/share/run-state-contract.d.ts +1 -1
  34. package/package.json +5 -5
  35. package/skills/peaks-code/SKILL.md +1 -1
  36. package/skills/peaks-code/references/runbook.md +6 -0
  37. package/skills/peaks-code/references/step-11-memory-sediment.md +35 -0
@@ -12,22 +12,18 @@
12
12
  // are stored as standard YAML frontmatter (name / description /
13
13
  // metadata.type / metadata.sourceArtifact) followed by the body.
14
14
  //
15
- // Both parsers share the 8-kind `VALID_MEMORY_KINDS` allow-list and the
15
+ // Both parsers share the `VALID_MEMORY_KINDS` allow-list (derived from the
16
+ // canonical `PROJECT_MEMORY_KINDS` tuple in `../types.ts`) and the
16
17
  // `slugify` helper used to derive filenames from titles.
17
18
  // ---------------------------------------------------------------------------
18
- export const VALID_MEMORY_KINDS = new Set([
19
- 'project',
20
- 'rule',
21
- 'decision',
22
- 'reference',
23
- 'feedback',
24
- 'convention',
25
- 'module',
26
- 'lesson'
27
- ]);
28
- /** Exported for guard tests + tooling that needs to enumerate the valid
29
- * set without duplicating the literal. Single source of truth. */
30
- export const VALID_PROJECT_MEMORY_KINDS = Array.from(VALID_MEMORY_KINDS);
19
+ import { basename } from 'node:path';
20
+ import { PROJECT_MEMORY_KINDS } from '../types.js';
21
+ /** Accepted-kind set, derived from the canonical `PROJECT_MEMORY_KINDS`
22
+ * tuple so the parser cannot drift from the union type / tier map. */
23
+ export const VALID_MEMORY_KINDS = new Set(PROJECT_MEMORY_KINDS);
24
+ /** Exported for guard tests + tooling that needs to enumerate the accepted
25
+ * set (CLI help text, `--kind` validation) without duplicating the literal. */
26
+ export const VALID_PROJECT_MEMORY_KINDS = PROJECT_MEMORY_KINDS;
31
27
  export function slugify(title) {
32
28
  const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
33
29
  return slug.length > 0 ? slug : 'project-memory';
@@ -69,36 +65,129 @@ export function renderMemoryFile(memory) {
69
65
  ''
70
66
  ].join('\n');
71
67
  }
72
- export function parseStoredMemoryFile(content, filePath) {
68
+ /**
69
+ * Resolve the peaks memory kind from a stored memory file's frontmatter.
70
+ *
71
+ * Resolution order (first *valid* kind wins):
72
+ * 1. nested `metadata.type` — the canonical peaks contract
73
+ * 2. top-level `kind:` — the legacy alias (was silently dropped before)
74
+ * 3. top-level `type:` — tolerated by the pre-existing trim-based reader
75
+ * 4. `none` — reported as unclassified; never invented
76
+ *
77
+ * Slice 2026-09-09-memory-system-overhaul (B): before this helper, files
78
+ * using a top-level `kind:` were silently dropped by the reader (defect
79
+ * #2). Falling through to `kind:` / `type:` is a strict superset of the
80
+ * old behaviour — no previously-indexed file changes kind.
81
+ */
82
+ export function resolveMemoryKind(content) {
83
+ const parsed = parseMemoryFrontmatter(content);
84
+ return parsed.kind;
85
+ }
86
+ /**
87
+ * Single parse surface for stored memory frontmatter. Both
88
+ * `parseStoredMemoryFile` (read path) and the reindex / ingest / doctor
89
+ * classifiers consume this so there is exactly one kind-resolution rule
90
+ * in the codebase.
91
+ */
92
+ export function parseMemoryFrontmatter(content) {
73
93
  const normalized = content.replace(/\r\n/g, '\n');
74
- if (!normalized.startsWith('---\n'))
75
- return null;
94
+ if (!normalized.startsWith('---\n')) {
95
+ return { hasFrontmatter: false, kind: { kind: null, source: 'none', rawKind: null }, frontmatter: '', body: normalized.trim() };
96
+ }
76
97
  const endIndex = normalized.indexOf('\n---\n', 4);
77
- if (endIndex < 0)
78
- return null;
98
+ if (endIndex < 0) {
99
+ return { hasFrontmatter: false, kind: { kind: null, source: 'none', rawKind: null }, frontmatter: '', body: normalized.trim() };
100
+ }
79
101
  const frontmatter = normalized.slice(4, endIndex);
80
102
  const body = normalized.slice(endIndex + '\n---\n'.length).trim();
81
103
  let name;
104
+ let titleField;
82
105
  let description;
83
- let kind;
84
106
  let sourceArtifact;
107
+ let nestedType;
108
+ let topType;
109
+ let kindField;
110
+ let inMetadata = false;
85
111
  for (const rawLine of frontmatter.split('\n')) {
112
+ const indented = /^\s/.test(rawLine);
86
113
  const line = rawLine.trim();
114
+ if (!indented) {
115
+ inMetadata = line === 'metadata:';
116
+ }
87
117
  if (line.startsWith('name:'))
88
118
  name = line.slice('name:'.length).trim();
119
+ else if (line.startsWith('title:')) {
120
+ if (!indented)
121
+ titleField = line.slice('title:'.length).trim();
122
+ }
89
123
  else if (line.startsWith('description:'))
90
124
  description = line.slice('description:'.length).trim();
91
- else if (line.startsWith('type:'))
92
- kind = line.slice('type:'.length).trim();
125
+ else if (line.startsWith('type:')) {
126
+ const value = line.slice('type:'.length).trim();
127
+ if (indented || inMetadata)
128
+ nestedType ??= value;
129
+ else
130
+ topType ??= value;
131
+ }
132
+ else if (line.startsWith('kind:'))
133
+ kindField ??= line.slice('kind:'.length).trim();
93
134
  else if (line.startsWith('sourceArtifact:'))
94
135
  sourceArtifact = line.slice('sourceArtifact:'.length).trim();
95
136
  }
96
- if (!name || !kind || !VALID_MEMORY_KINDS.has(kind) || body.length === 0)
137
+ const candidates = [
138
+ ['metadata.type', nestedType],
139
+ ['kind', kindField],
140
+ ['type', topType]
141
+ ];
142
+ let kind = { kind: null, source: 'none', rawKind: null };
143
+ for (const [source, raw] of candidates) {
144
+ if (raw === undefined || raw === '')
145
+ continue;
146
+ if (kind.rawKind === null)
147
+ kind = { kind: null, source: 'none', rawKind: raw };
148
+ if (VALID_MEMORY_KINDS.has(raw)) {
149
+ kind = { kind: raw, source, rawKind: raw };
150
+ break;
151
+ }
152
+ }
153
+ return { hasFrontmatter: true, frontmatter, ...(name !== undefined ? { name } : {}), ...(titleField !== undefined ? { title: titleField } : {}), ...(description !== undefined ? { description } : {}), ...(sourceArtifact !== undefined ? { sourceArtifact } : {}), kind, body };
154
+ }
155
+ /**
156
+ * Deterministic name fallback chain for the read path:
157
+ *
158
+ * 1. `name:` — the canonical field written by `renderMemoryFile`
159
+ * 2. `title:` — hand-written / legacy files (the 5 on-disk files
160
+ * this slice fixes carried only `title:` + `kind:`)
161
+ * 3. filename stem — last resort, so a well-formed memory with a valid
162
+ * kind is never dropped just for missing a name
163
+ *
164
+ * Empty values are skipped rather than accepted: a `name:` of `''` still
165
+ * falls through, and a file whose stem is also empty resolves to null so the
166
+ * caller's validation is preserved (never invents a name).
167
+ */
168
+ export function resolveMemoryName(parsed, filePath) {
169
+ if (parsed.name !== undefined && parsed.name.length > 0)
170
+ return { name: parsed.name, source: 'name' };
171
+ if (parsed.title !== undefined && parsed.title.length > 0)
172
+ return { name: parsed.title, source: 'title' };
173
+ const stem = basename(filePath, '.md');
174
+ if (stem.length > 0)
175
+ return { name: stem, source: 'stem' };
176
+ return { name: null, source: 'none' };
177
+ }
178
+ export function parseStoredMemoryFile(content, filePath) {
179
+ const parsed = parseMemoryFrontmatter(content);
180
+ if (!parsed.hasFrontmatter)
181
+ return null;
182
+ const { description, sourceArtifact, body } = parsed;
183
+ const kind = parsed.kind.kind;
184
+ const { name } = resolveMemoryName(parsed, filePath);
185
+ if (name === null || kind === null || body.length === 0)
97
186
  return null;
98
187
  return {
99
188
  name,
100
189
  title: description ?? name,
101
- kind: kind,
190
+ kind,
102
191
  sourceArtifact: sourceArtifact && sourceArtifact !== 'undefined' ? sourceArtifact : null,
103
192
  body,
104
193
  filePath
@@ -1,4 +1,34 @@
1
- export type ProjectMemoryKind = 'project' | 'rule' | 'decision' | 'reference' | 'feedback' | 'convention' | 'module' | 'lesson';
1
+ /**
2
+ * Canonical memory-kind vocabulary — the single source of truth.
3
+ *
4
+ * The union type, the accepted-kind set in the frontmatter parser, the
5
+ * hot/warm tier map below, `KIND_ORDER` in the reindex view, and the CLI
6
+ * `--kind` help text all derive from this tuple. Adding a kind here is the
7
+ * only edit required; TypeScript then forces `MEMORY_KIND_TIER` to cover it.
8
+ *
9
+ * Slice 2026-09-10-memory-vocab-and-rotate (E): the original 8 kinds are
10
+ * unchanged. The 13 appended kinds were observed on disk with real values
11
+ * that the index schema rejected (`peaks memory reindex` reported them as
12
+ * `unrecognized kind value`). Accepting them moves those files into the
13
+ * index; files with no kind field at all stay reported as unclassified.
14
+ */
15
+ export declare const PROJECT_MEMORY_KINDS: readonly ["project", "rule", "decision", "reference", "feedback", "convention", "module", "lesson", "bug", "investigation", "technical-pattern", "project-rule", "design", "handoff", "session-handoff", "project-todo", "publish-closure", "project-closure", "slice-closure", "slice-pilot-findings", "sediment"];
16
+ export type ProjectMemoryKind = (typeof PROJECT_MEMORY_KINDS)[number];
17
+ /** Hot kinds keep their body in the always-available index; warm kinds are
18
+ * indexed with the same entry shape but read on demand. */
19
+ export type MemoryKindTier = 'hot' | 'warm';
20
+ /**
21
+ * Hot/warm tier per kind. Insertion order IS the deterministic section order
22
+ * used by the generated `MEMORY.md` (`KIND_ORDER` in `index/reindex.ts`),
23
+ * so this object's key order is load-bearing — append new kinds at the end
24
+ * of their tier block rather than reshuffling existing entries.
25
+ *
26
+ * The `Record<ProjectMemoryKind, MemoryKindTier>` annotation makes a missing
27
+ * tier a compile error whenever `PROJECT_MEMORY_KINDS` grows.
28
+ */
29
+ export declare const MEMORY_KIND_TIER: Record<ProjectMemoryKind, MemoryKindTier>;
30
+ export declare const HOT_MEMORY_KINDS: readonly ProjectMemoryKind[];
31
+ export declare const WARM_MEMORY_KINDS: readonly ProjectMemoryKind[];
2
32
  export type ExtractedProjectMemory = {
3
33
  title: string;
4
34
  kind: ProjectMemoryKind;
@@ -6,4 +6,79 @@
6
6
  // from here, and the top-level `index.ts` re-exports them for back-compat
7
7
  // with downstream callers (CLI commands, audit writers, presence service).
8
8
  // ---------------------------------------------------------------------------
9
- export {};
9
+ /**
10
+ * Canonical memory-kind vocabulary — the single source of truth.
11
+ *
12
+ * The union type, the accepted-kind set in the frontmatter parser, the
13
+ * hot/warm tier map below, `KIND_ORDER` in the reindex view, and the CLI
14
+ * `--kind` help text all derive from this tuple. Adding a kind here is the
15
+ * only edit required; TypeScript then forces `MEMORY_KIND_TIER` to cover it.
16
+ *
17
+ * Slice 2026-09-10-memory-vocab-and-rotate (E): the original 8 kinds are
18
+ * unchanged. The 13 appended kinds were observed on disk with real values
19
+ * that the index schema rejected (`peaks memory reindex` reported them as
20
+ * `unrecognized kind value`). Accepting them moves those files into the
21
+ * index; files with no kind field at all stay reported as unclassified.
22
+ */
23
+ export const PROJECT_MEMORY_KINDS = [
24
+ // Original 8 (pre-2026-09-10).
25
+ 'project',
26
+ 'rule',
27
+ 'decision',
28
+ 'reference',
29
+ 'feedback',
30
+ 'convention',
31
+ 'module',
32
+ 'lesson',
33
+ // Slice E expansion — observed on-disk values, hot tier.
34
+ 'bug',
35
+ 'investigation',
36
+ 'technical-pattern',
37
+ 'project-rule',
38
+ // Slice E expansion — observed on-disk values, warm tier.
39
+ 'design',
40
+ 'handoff',
41
+ 'session-handoff',
42
+ 'project-todo',
43
+ 'publish-closure',
44
+ 'project-closure',
45
+ 'slice-closure',
46
+ 'slice-pilot-findings',
47
+ 'sediment'
48
+ ];
49
+ /**
50
+ * Hot/warm tier per kind. Insertion order IS the deterministic section order
51
+ * used by the generated `MEMORY.md` (`KIND_ORDER` in `index/reindex.ts`),
52
+ * so this object's key order is load-bearing — append new kinds at the end
53
+ * of their tier block rather than reshuffling existing entries.
54
+ *
55
+ * The `Record<ProjectMemoryKind, MemoryKindTier>` annotation makes a missing
56
+ * tier a compile error whenever `PROJECT_MEMORY_KINDS` grows.
57
+ */
58
+ export const MEMORY_KIND_TIER = {
59
+ // hot — full body kept in the index
60
+ feedback: 'hot',
61
+ decision: 'hot',
62
+ rule: 'hot',
63
+ convention: 'hot',
64
+ module: 'hot',
65
+ lesson: 'hot',
66
+ bug: 'hot',
67
+ investigation: 'hot',
68
+ 'technical-pattern': 'hot',
69
+ 'project-rule': 'hot',
70
+ // warm — same entry shape, read on demand
71
+ project: 'warm',
72
+ reference: 'warm',
73
+ design: 'warm',
74
+ handoff: 'warm',
75
+ 'session-handoff': 'warm',
76
+ 'project-todo': 'warm',
77
+ 'publish-closure': 'warm',
78
+ 'project-closure': 'warm',
79
+ 'slice-closure': 'warm',
80
+ 'slice-pilot-findings': 'warm',
81
+ sediment: 'warm'
82
+ };
83
+ export const HOT_MEMORY_KINDS = PROJECT_MEMORY_KINDS.filter((kind) => MEMORY_KIND_TIER[kind] === 'hot');
84
+ export const WARM_MEMORY_KINDS = PROJECT_MEMORY_KINDS.filter((kind) => MEMORY_KIND_TIER[kind] === 'warm');
@@ -127,9 +127,23 @@ export interface ProjectPreferences {
127
127
  */
128
128
  readonly memoryPreflight?: {
129
129
  readonly enabled?: boolean;
130
+ /** Back-compat token cap; converted to bytes as `maxTokens * 4`. */
130
131
  readonly maxTokens?: number;
132
+ /** Back-compat hot item cap; `hotItemCap` overrides it when set. */
131
133
  readonly listCap?: number;
132
134
  readonly contentCacheBytes?: number;
135
+ /** Slice 2026-09-09-memory-retrieval: hard byte cap on the block. */
136
+ readonly maxBytes?: number;
137
+ /** Slice 2026-09-09-memory-retrieval: max hot items (default 10). */
138
+ readonly hotItemCap?: number;
139
+ /** Slice 2026-09-09-memory-retrieval: max warm items (default 4; 0 disables). */
140
+ readonly warmItemCap?: number;
141
+ /** Slice 2026-09-09-memory-retrieval: min task-token hits for warm eligibility. */
142
+ readonly warmMinTokenHits?: number;
143
+ /** Slice 2026-09-09-memory-retrieval: soft selection wall-clock budget (ms). */
144
+ readonly selectionTimeBudgetMs?: number;
145
+ /** Slice 2026-09-09-memory-retrieval: inline memo bodies (default false). */
146
+ readonly includeBodies?: boolean;
133
147
  };
134
148
  }
135
149
  export type FanoutMode = 'fan-out';
@@ -48,5 +48,13 @@ export const DEFAULT_PREFERENCES = {
48
48
  maxTokens: 1200,
49
49
  listCap: 12,
50
50
  contentCacheBytes: 6000,
51
+ // Slice 2026-09-09-memory-retrieval tiered budget (see
52
+ // memory-preflight-config.ts::DEFAULTS).
53
+ maxBytes: 4800,
54
+ hotItemCap: 10,
55
+ warmItemCap: 4,
56
+ warmMinTokenHits: 1,
57
+ selectionTimeBudgetMs: 200,
58
+ includeBodies: false,
51
59
  },
52
60
  };
@@ -64,8 +64,8 @@ export declare const RunStateContractSchema: z.ZodObject<{
64
64
  status: z.ZodEnum<{
65
65
  blocked: "blocked";
66
66
  failed: "failed";
67
- running: "running";
68
67
  done: "done";
68
+ running: "running";
69
69
  paused: "paused";
70
70
  }>;
71
71
  current_step: z.ZodString;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-loop",
3
- "version": "4.0.34",
3
+ "version": "4.0.35",
4
4
  "description": "Loop Engineering CLI — workflow primitive / loop guards / evaluators / slice orchestration",
5
5
  "author": "SquabbyZ",
6
6
  "keywords": [
@@ -101,10 +101,10 @@
101
101
  "fzf": "^0.5.2",
102
102
  "yaml": "^2.9.0",
103
103
  "zod": "^4.4.3",
104
- "peaks-loop-internal-runtime": "0.0.19",
105
- "peaks-loop-shared": "0.0.68",
106
- "peaks-loop-shared-channel": "0.0.36",
107
- "peaks-loop-mut": "0.1.32"
104
+ "peaks-loop-internal-runtime": "0.0.20",
105
+ "peaks-loop-mut": "0.1.33",
106
+ "peaks-loop-shared": "0.0.69",
107
+ "peaks-loop-shared-channel": "0.0.37"
108
108
  },
109
109
  "devDependencies": {
110
110
  "@changesets/cli": "2.31.1",
@@ -296,7 +296,7 @@ After final validation, refresh project-local standards via `peaks standards ini
296
296
 
297
297
  ## Peaks-Loop Step 11: Memory sediment (BLOCKING on workflow complete)
298
298
 
299
- > **Hard rule.** Code MUST NOT declare a workflow complete until Step 11 has produced ≥ 1 file in `.peaks/memory/` OR the user has explicitly approved a no-sediment outcome via AskUserQuestion. Canonical CLI: `peaks memory extract --project <repo> --artifact .peaks/_runtime/<sessionId>/txt/handoff.md --apply --json` (the artifact-scoped extract; the batch-scoped sibling `peaks project memories:extract` is for non-handoff flows only). Substeps 11a/11b/11c/11d (Gate A/B/C), D-010 fix root cause check → `references/step-11-memory-sediment.md` + `references/runbook.md` §Step 11.
299
+ > **Hard rule.** Code MUST NOT declare a workflow complete until Step 11 has produced ≥ 1 file in `.peaks/memory/` OR the user has explicitly approved a no-sediment outcome via AskUserQuestion. Canonical CLI: `peaks memory extract --project <repo> --artifact .peaks/_runtime/<sessionId>/txt/handoff.md --apply --json` (the artifact-scoped extract; the batch-scoped sibling `peaks project memories:extract` is for non-handoff flows only). **Single authority:** inside a peaks-code workflow, sedimenting memory writes to `.peaks/memory/`; the IDE-side memory dir (`~/.claude/projects/<hash>/memory/`) is a session note, not the authority and is read-only for peaks — pull any session note in with `peaks memory ingest --apply`, and keep the index in sync with `peaks memory reindex --apply`. Substeps 11a/11b/11c/11d (Gate A/B/C), 11e (index hygiene), 11f (IDE-side ingest), D-010 fix root cause check → `references/step-11-memory-sediment.md` + `references/runbook.md` §Step 11.
300
300
 
301
301
  ## Peaks-Loop External references and lifecycle
302
302
 
@@ -215,6 +215,12 @@ peaks memory extract --project <repo> --artifact .peaks/_runtime/<id>/txt/handof
215
215
  # --apply is REQUIRED to write .peaks/memory/; without it the command only
216
216
  # previews. The extract regenerates index.json in the same call.
217
217
 
218
+ # 10d. Index hygiene (single authority: .peaks/memory/ is peaks-owned):
219
+ peaks memory reindex --project <repo> --json # drift report (unclassified / orphans both ways)
220
+ peaks memory reindex --project <repo> --apply --json # rebuild index.json + regenerate MEMORY.md
221
+ peaks memory ingest --project <repo> --json # preview IDE-side session notes
222
+ peaks memory ingest --project <repo> --apply --json # import them into .peaks/memory (IDE side stays read-only)
223
+
218
224
  # 11. Peaks-Loop Final snapshot
219
225
  peaks project dashboard --project <repo> --json
220
226
  peaks skill doctor --json
@@ -6,6 +6,15 @@ Companion to `SKILL.md` §"Peaks-Loop Step 11". This file holds the substep-by-s
6
6
 
7
7
  Code MUST NOT declare a workflow complete until Step 11 has produced ≥ 1 file in `.peaks/memory/` OR the user has explicitly approved a no-sediment outcome via AskUserQuestion. Applies to **all modes** including `assisted` and `strict`.
8
8
 
9
+ ## Single authority (2026-09-09)
10
+
11
+ **Inside a peaks-code workflow, sedimenting memory writes to `.peaks/memory/`. The IDE-side memory dir is a session note, not the authority.**
12
+
13
+ - `.peaks/memory/` is peaks-owned and authoritative. Every sediment action ends there.
14
+ - Claude Code's per-project memory dir (`~/.claude/projects/<project-hash>/memory/*.md`) is the IDE's own scratch memory. It is **read-only** for peaks — never write there (same rule as `~/.claude/agents/`).
15
+ - If a session note was written to the IDE-side dir instead of `.peaks/memory/`, pull it in with `peaks memory ingest --apply` (LLM-run; read-only on the IDE side, normalizes frontmatter to `metadata.type`, idempotent by filename stem). Conflicts leave both copies in place — resolve by hand.
16
+ - Never let a memory exist only in the IDE-side dir when the workflow claims the memory was sedimented.
17
+
9
18
  ## Substeps
10
19
 
11
20
  ### 11a — Gate A (txt/ inventory)
@@ -42,6 +51,32 @@ If `extractedCount === 0` after 11c, fire AskUserQuestion:
42
51
 
43
52
  > **D-010 fix root cause check:** When firing 11d, first inspect whether the block has the YAML frontmatter (`title:` + `kind:` + `---`). If the `<!-- peaks-memory:start -->` exists but no `title:` line follows, fix the block format and re-run 11c — don't ask the user yet. Default option = (a). Code MUST NOT silently accept (b) without user pick.
44
53
 
54
+ ### 11e — Index hygiene (run after any sediment)
55
+
56
+ ```bash
57
+ peaks memory reindex --project <repo> --json # drift report (dry run)
58
+ peaks memory reindex --project <repo> --apply --json # rebuild index.json + MEMORY.md
59
+ ```
60
+
61
+ `reindex` re-scans **every** `.peaks/memory/*.md` (including `archived/`), resolves each file's kind as `metadata.type` → top-level `kind:` → top-level `type:`, rebuilds `index.json` deterministically, and regenerates `MEMORY.md` (generated banner; do not hand-edit). It reports, never silently drops:
62
+
63
+ - `unclassified[]` — files with no resolvable kind (add `metadata.type` to fix);
64
+ - `orphanIndex[]` — index entries whose `sourcePath` no longer exists;
65
+ - `orphanDisk[]` — files on disk the rebuilt index does not contain.
66
+
67
+ Run it whenever Step 11 wrote or imported memories, so the machine index and `MEMORY.md` never drift apart again.
68
+
69
+ ### 11f — Pull in IDE-side session notes (only when needed)
70
+
71
+ If a memory was written to Claude Code's own memory dir during the session instead of `.peaks/memory/`:
72
+
73
+ ```bash
74
+ peaks memory ingest --project <repo> --json # preview
75
+ peaks memory ingest --project <repo> --apply --json # write into .peaks/memory
76
+ ```
77
+
78
+ Source defaults to `~/.claude/projects/<project-hash>/memory/` (read-only). Import is idempotent by filename stem; differing destinations are reported as conflicts and both copies are left in place. Files whose kind cannot be resolved are reported as needing classification — never invented.
79
+
45
80
  ## Why Step 11 exists
46
81
 
47
82
  Audit 2026-07-03 confirmed 2 consecutive sessions produced zero `.peaks/memory/` files despite completing RD + QA + handoff artifacts; `assisted` mode silently skipped runbook Step 10 (no STOP condition).