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.
- package/CHANGELOG.md +18 -0
- package/dist/cli/commands/core/memory-command.js +61 -3
- package/dist/cli/commands/dispatch-commands.js +4 -2
- package/dist/cli/commands/memory-commands.d.ts +35 -0
- package/dist/cli/commands/memory-commands.js +119 -10
- package/dist/services/context/context-schema.d.ts +1 -1
- package/dist/services/context/memory-index-reader.d.ts +26 -0
- package/dist/services/context/memory-index-reader.js +62 -30
- package/dist/services/context/memory-preflight-config.d.ts +33 -0
- package/dist/services/context/memory-preflight-config.js +32 -2
- package/dist/services/context/memory-preflight-service.d.ts +20 -1
- package/dist/services/context/memory-preflight-service.js +198 -31
- package/dist/services/doctor/doctor-service/checks/l3-memory-health.d.ts +19 -2
- package/dist/services/doctor/doctor-service/checks/l3-memory-health.js +143 -19
- package/dist/services/job/job-types.d.ts +3 -3
- package/dist/services/memory/memory-ingest-service.d.ts +79 -0
- package/dist/services/memory/memory-ingest-service.js +225 -0
- package/dist/services/memory/memory-rotate-service.d.ts +88 -0
- package/dist/services/memory/memory-rotate-service.js +373 -0
- package/dist/services/memory/project-memory-service/index/ranking.d.ts +9 -1
- package/dist/services/memory/project-memory-service/index/ranking.js +25 -13
- package/dist/services/memory/project-memory-service/index/reindex.d.ts +75 -0
- package/dist/services/memory/project-memory-service/index/reindex.js +207 -0
- package/dist/services/memory/project-memory-service/index/search.js +14 -24
- package/dist/services/memory/project-memory-service/index.d.ts +7 -3
- package/dist/services/memory/project-memory-service/index.js +6 -2
- package/dist/services/memory/project-memory-service/parsers/frontmatter.d.ts +75 -3
- package/dist/services/memory/project-memory-service/parsers/frontmatter.js +113 -24
- package/dist/services/memory/project-memory-service/types.d.ts +31 -1
- package/dist/services/memory/project-memory-service/types.js +76 -1
- package/dist/services/preferences/preferences-types.d.ts +14 -0
- package/dist/services/preferences/preferences-types.js +8 -0
- package/dist/services/share/run-state-contract.d.ts +1 -1
- package/package.json +5 -5
- package/skills/peaks-code/SKILL.md +1 -1
- package/skills/peaks-code/references/runbook.md +6 -0
- 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
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
190
|
+
kind,
|
|
102
191
|
sourceArtifact: sourceArtifact && sourceArtifact !== 'undefined' ? sourceArtifact : null,
|
|
103
192
|
body,
|
|
104
193
|
filePath
|
|
@@ -1,4 +1,34 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peaks-loop",
|
|
3
|
-
"version": "4.0.
|
|
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.
|
|
105
|
-
"peaks-loop-
|
|
106
|
-
"peaks-loop-shared
|
|
107
|
-
"peaks-loop-
|
|
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).
|