@lh8ppl/claude-memory-kit 0.1.0
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/bin/cmk-compress-lazy.mjs +59 -0
- package/bin/cmk-daily-distill.mjs +67 -0
- package/bin/cmk-weekly-curate.mjs +56 -0
- package/bin/cmk.mjs +12 -0
- package/package.json +50 -0
- package/src/audit-log.mjs +103 -0
- package/src/auto-extract.mjs +742 -0
- package/src/capture-prompt.mjs +61 -0
- package/src/capture-turn.mjs +273 -0
- package/src/claude-md.mjs +212 -0
- package/src/compress-session.mjs +349 -0
- package/src/compressor.mjs +376 -0
- package/src/conflict-queue.mjs +796 -0
- package/src/cooldown.mjs +61 -0
- package/src/daily-distill.mjs +252 -0
- package/src/doctor.mjs +528 -0
- package/src/forget.mjs +335 -0
- package/src/frontmatter.mjs +73 -0
- package/src/import-anthropic-memory.mjs +266 -0
- package/src/index-db.mjs +154 -0
- package/src/index-rebuild.mjs +597 -0
- package/src/index.mjs +90 -0
- package/src/inject-context.mjs +484 -0
- package/src/install.mjs +327 -0
- package/src/lazy-compress.mjs +326 -0
- package/src/lock-discipline.mjs +166 -0
- package/src/mcp-server.mjs +498 -0
- package/src/memory-write.mjs +565 -0
- package/src/merge-facts.mjs +213 -0
- package/src/observe-edit.mjs +87 -0
- package/src/platform-commands.mjs +138 -0
- package/src/poison-guard.mjs +245 -0
- package/src/privacy.mjs +21 -0
- package/src/provenance.mjs +217 -0
- package/src/register-crons.mjs +354 -0
- package/src/reindex.mjs +134 -0
- package/src/repair.mjs +316 -0
- package/src/result-shapes.mjs +155 -0
- package/src/review-queue.mjs +345 -0
- package/src/roll.mjs +115 -0
- package/src/scratchpad.mjs +335 -0
- package/src/search.mjs +311 -0
- package/src/subcommands.mjs +1252 -0
- package/src/tier-paths.mjs +74 -0
- package/src/transcripts.mjs +234 -0
- package/src/trust.mjs +226 -0
- package/src/weekly-curate.mjs +454 -0
- package/src/write-fact.mjs +205 -0
- package/template/.claude/hooks/pre-tool-memory.js +78 -0
- package/template/.claude/hooks/transcript-capture.js +69 -0
- package/template/.claude/settings.json +27 -0
- package/template/.claude/skills/memory-write/SKILL.md +117 -0
- package/template/.gitignore.fragment +12 -0
- package/template/CLAUDE.md.template +49 -0
- package/template/docs/journey/journey-log.md.template +292 -0
- package/template/local/machine-paths.md.template +37 -0
- package/template/local/overrides.md.template +36 -0
- package/template/project/.index/.gitkeep +0 -0
- package/template/project/MEMORY.md.template +47 -0
- package/template/project/SOUL.md.template +35 -0
- package/template/project/memory/INDEX.md.template +47 -0
- package/template/project/memory/archive/superseded/.gitkeep +0 -0
- package/template/project/memory/archive/tombstones/.gitkeep +0 -0
- package/template/project/queues/.gitkeep +0 -0
- package/template/project/sessions/.gitkeep +0 -0
- package/template/project/transcripts/.gitkeep +0 -0
- package/template/support/cron-jobs/daily-memory-distill.md +15 -0
- package/template/support/cron-jobs/nightly-memsearch-index.md +17 -0
- package/template/support/cron-jobs/weekly-memory-curator.md +15 -0
- package/template/support/milvus-deploy/README.md +57 -0
- package/template/support/milvus-deploy/docker-compose.yml +66 -0
- package/template/support/scripts/auto-extract-memory.sh +102 -0
- package/template/support/scripts/memsearch-index-with-flush.sh +59 -0
- package/template/support/scripts/refresh-distill-timestamp.py +35 -0
- package/template/support/scripts/register-crons.py +242 -0
- package/template/support/scripts/run-daily-distill.sh +67 -0
- package/template/support/scripts/run-weekly-curate.sh +58 -0
- package/template/user/HABITS.md.template +18 -0
- package/template/user/LESSONS.md.template +18 -0
- package/template/user/USER.md.template +18 -0
- package/template/user/fragments/INDEX.md.template +23 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
// review-queue.mjs — Task 26 (T-023). Public boundary for the
|
|
2
|
+
// medium-trust review queue resolver.
|
|
3
|
+
//
|
|
4
|
+
// Per design §6.2:
|
|
5
|
+
// - Auto-extract routes high-trust → MEMORY.md (direct)
|
|
6
|
+
// - Auto-extract routes medium-trust → queues/review.md (this module)
|
|
7
|
+
// - Auto-extract routes low-trust → discarded (logged to extract.log)
|
|
8
|
+
//
|
|
9
|
+
// The medium-trust ROUTING is already done by `routeMedium` in
|
|
10
|
+
// `auto-extract.mjs` (Task 23). This module handles the RESOLVER side:
|
|
11
|
+
// the `cmk queue review` interactive walker that processes pending
|
|
12
|
+
// entries one-at-a-time and accepts `promote` / `discard` / `skip`.
|
|
13
|
+
//
|
|
14
|
+
// Companion to the conflict-queue (Task 25; design §6.8). Same shape
|
|
15
|
+
// (interactive walker + per-entry decisions) but different routing:
|
|
16
|
+
// - Review queue: new medium-trust candidates awaiting blessing
|
|
17
|
+
// - Conflict queue: new writes that contradict existing higher-
|
|
18
|
+
// trust facts
|
|
19
|
+
//
|
|
20
|
+
// Idempotency-convention asymmetry (code-review note): review-queue
|
|
21
|
+
// REMOVES resolved entries from review.md; conflict-queue PRESERVES
|
|
22
|
+
// them with `resolution: <decision>` markers. The asymmetry is
|
|
23
|
+
// intentional — review is transient (once promoted, the candidate's
|
|
24
|
+
// outcome is in MEMORY.md; once discarded, in audit.log), so the
|
|
25
|
+
// queue file shouldn't grow unbounded with resolved entries.
|
|
26
|
+
// Conflict-queue is preservational (file-as-history serves audit).
|
|
27
|
+
//
|
|
28
|
+
// Parser tolerance (code-review note): parseReviewQueue silently
|
|
29
|
+
// skips malformed blocks. Matches kit pattern (lock-discipline,
|
|
30
|
+
// conflict-queue). User manually editing review.md with bad
|
|
31
|
+
// markdown loses the malformed block but doesn't crash the resolver.
|
|
32
|
+
// Trade-off: data-loss risk on bad edits vs. robustness against
|
|
33
|
+
// editor noise. The kit's overall posture is "be tolerant of file
|
|
34
|
+
// edits; never crash the kit on malformed memory state".
|
|
35
|
+
//
|
|
36
|
+
// Public surface:
|
|
37
|
+
// - parseReviewQueue(text) — parses the review.md format into
|
|
38
|
+
// entries (`## <ts> — auto-extract (medium-trust, pending review)`
|
|
39
|
+
// heading + bullet + provenance + blank line)
|
|
40
|
+
// - resolveReviewQueue({tier, projectRoot, userDir, prompter}) —
|
|
41
|
+
// walks pending entries one-at-a-time; on each, the prompter
|
|
42
|
+
// returns 'promote' / 'discard' / 'skip'. Resolved entries are
|
|
43
|
+
// REMOVED from review.md (per task 26.3/26.4). Skipped entries
|
|
44
|
+
// stay. Audit-log entries written for promote + discard.
|
|
45
|
+
//
|
|
46
|
+
// File format (matches routeMedium's output in auto-extract.mjs):
|
|
47
|
+
//
|
|
48
|
+
// ## 2026-05-27T10:00:00Z — auto-extract (medium-trust, pending review)
|
|
49
|
+
// - (P-AAAAAAAA) the candidate text
|
|
50
|
+
// <!-- proposed_trust: medium, write: auto-extract, at: 2026-05-27T10:00:00Z -->
|
|
51
|
+
//
|
|
52
|
+
// ## 2026-05-27T10:01:00Z — auto-extract (medium-trust, pending review)
|
|
53
|
+
// - (P-BBBBBBBB) another candidate
|
|
54
|
+
// <!-- proposed_trust: medium, write: auto-extract, at: 2026-05-27T10:01:00Z -->
|
|
55
|
+
//
|
|
56
|
+
// Uses shared modules per CLAUDE.md "Shared modules" rule.
|
|
57
|
+
|
|
58
|
+
import {
|
|
59
|
+
existsSync,
|
|
60
|
+
readFileSync,
|
|
61
|
+
writeFileSync,
|
|
62
|
+
} from 'node:fs';
|
|
63
|
+
import { join } from 'node:path';
|
|
64
|
+
import { resolveTierRoot, VALID_TIERS } from './tier-paths.mjs';
|
|
65
|
+
import { nowIso, appendAuditEntry, REASON_CODES } from './audit-log.mjs';
|
|
66
|
+
import { ERROR_CATEGORIES, errorResult } from './result-shapes.mjs';
|
|
67
|
+
import { memoryWrite } from './memory-write.mjs';
|
|
68
|
+
|
|
69
|
+
const QUEUE_RELATIVE = ['queues', 'review.md'];
|
|
70
|
+
|
|
71
|
+
// --- Parsing -------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
const HEADING_RE = /^##\s+(.+?)\s+—\s+auto-extract\s+\(medium-trust,\s+pending review\)\s*$/;
|
|
74
|
+
const BULLET_LINE_RE = /^- \(([PUL]-[A-Za-z0-9]{8})\)\s+(.+)$/;
|
|
75
|
+
const PROVENANCE_RE = /^\s+<!--\s*(.+?)\s*-->\s*$/;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Parse the review.md file body into an entry array.
|
|
79
|
+
*
|
|
80
|
+
* Returns: { entries: [{ ts, id, text, provenance, startLine, endLine }],
|
|
81
|
+
* preamble: string[] }
|
|
82
|
+
*
|
|
83
|
+
* Entries are ORDERED — earliest at index 0. preamble holds any text
|
|
84
|
+
* before the first heading (typically empty or comments). The parser
|
|
85
|
+
* is tolerant: any block that doesn't match the heading+bullet+
|
|
86
|
+
* provenance triple is skipped silently (the resolver doesn't lose
|
|
87
|
+
* data — it just doesn't display malformed entries).
|
|
88
|
+
*/
|
|
89
|
+
export function parseReviewQueue(text) {
|
|
90
|
+
const lines = String(text ?? '').split(/\r?\n/);
|
|
91
|
+
const entries = [];
|
|
92
|
+
let preambleEnd = 0;
|
|
93
|
+
let i = 0;
|
|
94
|
+
|
|
95
|
+
// Skip any preamble before the first heading.
|
|
96
|
+
while (i < lines.length && !HEADING_RE.test(lines[i])) {
|
|
97
|
+
i++;
|
|
98
|
+
}
|
|
99
|
+
preambleEnd = i;
|
|
100
|
+
|
|
101
|
+
while (i < lines.length) {
|
|
102
|
+
const headingMatch = lines[i].match(HEADING_RE);
|
|
103
|
+
if (!headingMatch) {
|
|
104
|
+
i++;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const ts = headingMatch[1];
|
|
108
|
+
const startLine = i;
|
|
109
|
+
// Next non-blank line should be the bullet.
|
|
110
|
+
let j = i + 1;
|
|
111
|
+
let bulletLine = -1;
|
|
112
|
+
let provenanceLine = -1;
|
|
113
|
+
while (j < lines.length && j < i + 5) {
|
|
114
|
+
if (BULLET_LINE_RE.test(lines[j])) {
|
|
115
|
+
bulletLine = j;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
j++;
|
|
119
|
+
}
|
|
120
|
+
if (bulletLine === -1) {
|
|
121
|
+
i++;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const bulletMatch = lines[bulletLine].match(BULLET_LINE_RE);
|
|
125
|
+
const id = bulletMatch[1];
|
|
126
|
+
const bulletText = bulletMatch[2];
|
|
127
|
+
// Provenance comment is the next line.
|
|
128
|
+
if (bulletLine + 1 < lines.length && PROVENANCE_RE.test(lines[bulletLine + 1])) {
|
|
129
|
+
provenanceLine = bulletLine + 1;
|
|
130
|
+
}
|
|
131
|
+
// entry ends at the next heading or EOF.
|
|
132
|
+
let endLine = (provenanceLine === -1 ? bulletLine : provenanceLine) + 1;
|
|
133
|
+
while (endLine < lines.length && !HEADING_RE.test(lines[endLine])) {
|
|
134
|
+
// Stop at blank-line boundary if the next non-blank starts a heading.
|
|
135
|
+
if (lines[endLine].trim() === '') {
|
|
136
|
+
endLine++;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
// Non-empty, non-heading line — could be a malformed entry.
|
|
140
|
+
// Conservative: include it in this entry.
|
|
141
|
+
endLine++;
|
|
142
|
+
}
|
|
143
|
+
entries.push({
|
|
144
|
+
ts,
|
|
145
|
+
id,
|
|
146
|
+
text: bulletText,
|
|
147
|
+
provenance: provenanceLine !== -1 ? lines[provenanceLine] : null,
|
|
148
|
+
startLine,
|
|
149
|
+
endLine, // exclusive
|
|
150
|
+
});
|
|
151
|
+
i = endLine;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
entries,
|
|
156
|
+
preamble: lines.slice(0, preambleEnd),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Re-serialize entries back to the review.md file format. Used by
|
|
162
|
+
* the resolver after promote/discard removals to keep the file
|
|
163
|
+
* coherent.
|
|
164
|
+
*/
|
|
165
|
+
function serializeReviewQueue({ preamble, entries }) {
|
|
166
|
+
const lines = [...preamble];
|
|
167
|
+
for (const e of entries) {
|
|
168
|
+
lines.push(`## ${e.ts} — auto-extract (medium-trust, pending review)`);
|
|
169
|
+
lines.push(`- (${e.id}) ${e.text}`);
|
|
170
|
+
if (e.provenance) lines.push(e.provenance);
|
|
171
|
+
lines.push('');
|
|
172
|
+
}
|
|
173
|
+
return lines.join('\n');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// --- Public: resolveReviewQueue ------------------------------------
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Walk pending review.md entries one-at-a-time. The `prompter` is a
|
|
180
|
+
* caller-supplied function: `async ({id, text, ts, provenance}) →
|
|
181
|
+
* 'promote' | 'discard' | 'skip'`. Lets the CLI / tests inject
|
|
182
|
+
* behavior; production wires through the interactive `cmk queue
|
|
183
|
+
* review` verb.
|
|
184
|
+
*
|
|
185
|
+
* Decisions:
|
|
186
|
+
* - 'promote' → invokes memoryWrite({action: 'add', trust: 'high',
|
|
187
|
+
* source: 'review-promote', text, tier, ...}). Removes entry
|
|
188
|
+
* from review.md. Audit-log entry written by memoryWrite (action:
|
|
189
|
+
* 'promoted'/scratchpad-append) AND this module (action: 'promoted'
|
|
190
|
+
* for review-queue tracking).
|
|
191
|
+
* - 'discard' → removes entry from review.md. Audit-log entry:
|
|
192
|
+
* action: 'discarded', reasonCode: REVIEW_DISCARDED.
|
|
193
|
+
* - 'skip' → entry stays in review.md. No audit-log entry.
|
|
194
|
+
* - anything else → treated as 'skip' (defensive).
|
|
195
|
+
*
|
|
196
|
+
* Returns: { promoted: N, discarded: N, skipped: N, errors: [] } on
|
|
197
|
+
* success, OR errorResult on schema failure.
|
|
198
|
+
*
|
|
199
|
+
* Note on the memoryWrite call: this passes `scratchpad: 'MEMORY.md'`
|
|
200
|
+
* + `section: 'Active Threads'` as v0.1 defaults. The kit's
|
|
201
|
+
* memory-write skill picks the section based on heuristics in v0.1.x.
|
|
202
|
+
*/
|
|
203
|
+
// Default `section` is 'Active Threads' — the catchall section in
|
|
204
|
+
// the kit's seed MEMORY.md scaffold (per design §2.1). v0.1.x
|
|
205
|
+
// candidate: per-candidate section routing via heuristics in the
|
|
206
|
+
// memory-write skill (requires `routeMedium` in auto-extract.mjs
|
|
207
|
+
// to capture the target section at write-time, currently it doesn't
|
|
208
|
+
// — review.md format has no section field). For v0.1.0 the catchall
|
|
209
|
+
// works because Active Threads is where transient observations
|
|
210
|
+
// belong by convention; the user can promote into a different
|
|
211
|
+
// section by editing MEMORY.md after promotion.
|
|
212
|
+
export async function resolveReviewQueue({
|
|
213
|
+
tier,
|
|
214
|
+
projectRoot,
|
|
215
|
+
userDir,
|
|
216
|
+
prompter,
|
|
217
|
+
scratchpad = 'MEMORY.md',
|
|
218
|
+
section = 'Active Threads',
|
|
219
|
+
} = {}) {
|
|
220
|
+
if (!VALID_TIERS.has(tier)) {
|
|
221
|
+
return errorResult({
|
|
222
|
+
category: ERROR_CATEGORIES.SCHEMA,
|
|
223
|
+
errors: [`resolveReviewQueue: tier must be one of P/U/L (got ${tier})`],
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
if (typeof prompter !== 'function') {
|
|
227
|
+
return errorResult({
|
|
228
|
+
category: ERROR_CATEGORIES.SCHEMA,
|
|
229
|
+
errors: ['resolveReviewQueue: prompter required (function)'],
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
const tierRoot = resolveTierRoot({ tier, projectRoot, userDir });
|
|
233
|
+
const queuePath = join(tierRoot, ...QUEUE_RELATIVE);
|
|
234
|
+
if (!existsSync(queuePath)) {
|
|
235
|
+
return { promoted: 0, discarded: 0, skipped: 0, errors: [] };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const text = readFileSync(queuePath, 'utf8');
|
|
239
|
+
const { entries, preamble } = parseReviewQueue(text);
|
|
240
|
+
if (entries.length === 0) {
|
|
241
|
+
return { promoted: 0, discarded: 0, skipped: 0, errors: [] };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let promoted = 0;
|
|
245
|
+
let discarded = 0;
|
|
246
|
+
let skipped = 0;
|
|
247
|
+
const errors = [];
|
|
248
|
+
const keep = []; // entries that remain after this pass
|
|
249
|
+
|
|
250
|
+
for (const entry of entries) {
|
|
251
|
+
const decision = await prompter({
|
|
252
|
+
id: entry.id,
|
|
253
|
+
text: entry.text,
|
|
254
|
+
ts: entry.ts,
|
|
255
|
+
provenance: entry.provenance,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
if (decision === 'promote') {
|
|
259
|
+
// Promote via memoryWrite — adds at trust: high. The proposed id
|
|
260
|
+
// from review.md is recomputed by memoryWrite from the canonical
|
|
261
|
+
// text, so it'll match if the text hasn't been edited.
|
|
262
|
+
//
|
|
263
|
+
// Composition note (code-review IMP-1): memoryWrite runs
|
|
264
|
+
// detectConflicts on every add (Task 25 integration). If the
|
|
265
|
+
// promoted text conflicts with an existing high-trust bullet,
|
|
266
|
+
// the result will be `{action: 'queued', ...}` — re-routed to
|
|
267
|
+
// queues/conflicts.md. This is by design (high-trust + similar-
|
|
268
|
+
// to-existing IS a conflict that needs surfacing), not a bug.
|
|
269
|
+
// The resolver reports this explicitly so the user understands
|
|
270
|
+
// where the promoted entry ended up.
|
|
271
|
+
const result = memoryWrite({
|
|
272
|
+
action: 'add',
|
|
273
|
+
tier,
|
|
274
|
+
scratchpad,
|
|
275
|
+
section,
|
|
276
|
+
text: entry.text,
|
|
277
|
+
source: 'review-promote',
|
|
278
|
+
trust: 'high',
|
|
279
|
+
projectRoot,
|
|
280
|
+
userDir,
|
|
281
|
+
now: nowIso(),
|
|
282
|
+
});
|
|
283
|
+
if (result.action === 'error') {
|
|
284
|
+
errors.push({
|
|
285
|
+
id: entry.id,
|
|
286
|
+
decision,
|
|
287
|
+
errors: result.errors,
|
|
288
|
+
});
|
|
289
|
+
keep.push(entry);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
// Detect the re-route case: memoryWrite returned `action: 'queued'`
|
|
293
|
+
// (conflict-queue route), not `action: 'created'` / 'appended'.
|
|
294
|
+
const rerouted = result.action === 'queued';
|
|
295
|
+
appendAuditEntry(tierRoot, {
|
|
296
|
+
ts: nowIso(),
|
|
297
|
+
action: 'promoted',
|
|
298
|
+
tier,
|
|
299
|
+
id: entry.id,
|
|
300
|
+
reasonCode: REASON_CODES.REVIEW_PROMOTED,
|
|
301
|
+
reasonText: rerouted
|
|
302
|
+
? `review-queue: promoted ${entry.id} but re-routed to conflict-queue (collision with ${result.conflictsWith}; resolve via cmk queue conflicts)`
|
|
303
|
+
: `review-queue: promoted ${entry.id} to ${scratchpad} (trust: high)`,
|
|
304
|
+
extra: {
|
|
305
|
+
decision,
|
|
306
|
+
from_queue: 'review',
|
|
307
|
+
original_ts: entry.ts,
|
|
308
|
+
...(rerouted
|
|
309
|
+
? {
|
|
310
|
+
rerouted_to: 'conflicts',
|
|
311
|
+
rerouted_as: result.id,
|
|
312
|
+
conflicts_with: result.conflictsWith,
|
|
313
|
+
}
|
|
314
|
+
: {}),
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
promoted++;
|
|
318
|
+
} else if (decision === 'discard') {
|
|
319
|
+
appendAuditEntry(tierRoot, {
|
|
320
|
+
ts: nowIso(),
|
|
321
|
+
action: 'discarded',
|
|
322
|
+
tier,
|
|
323
|
+
id: entry.id,
|
|
324
|
+
reasonCode: REASON_CODES.REVIEW_DISCARDED,
|
|
325
|
+
reasonText: `review-queue: discarded ${entry.id} (medium-trust auto-extract candidate)`,
|
|
326
|
+
extra: {
|
|
327
|
+
decision,
|
|
328
|
+
from_queue: 'review',
|
|
329
|
+
original_ts: entry.ts,
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
discarded++;
|
|
333
|
+
} else {
|
|
334
|
+
// skip OR unknown → preserve entry as-is, no audit.
|
|
335
|
+
skipped++;
|
|
336
|
+
keep.push(entry);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Rewrite the queue with only the kept (skipped + errored) entries.
|
|
341
|
+
const newBody = serializeReviewQueue({ preamble, entries: keep });
|
|
342
|
+
writeFileSync(queuePath, newBody, 'utf8');
|
|
343
|
+
|
|
344
|
+
return { promoted, discarded, skipped, errors };
|
|
345
|
+
}
|
package/src/roll.mjs
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// `cmk roll` (Task 39, T-033, parts 39.4–39.5).
|
|
2
|
+
//
|
|
3
|
+
// Public boundary:
|
|
4
|
+
// async runRoll({projectRoot, scope: 'now'|'today'|'recent', backend, now})
|
|
5
|
+
// → {action, scope, delegatedTo, result, duration_ms}
|
|
6
|
+
//
|
|
7
|
+
// Manually trigger one of the kit's compression pipelines on demand:
|
|
8
|
+
// - 'now' : task 22 compress-session — compresses now.md to today-{date}.md (DEFAULT)
|
|
9
|
+
// - 'today' : task 33 daily-distill — rolls today-*.md into recent.md
|
|
10
|
+
// - 'recent' : task 34 weekly-curate — archives today-*.md >7d into archive.md
|
|
11
|
+
//
|
|
12
|
+
// All three pipelines already implement the Haiku call + cooldown +
|
|
13
|
+
// audit-log discipline. This dispatcher passes the backend through to
|
|
14
|
+
// the appropriate one. `cooldownMs: 0` override because `cmk roll`
|
|
15
|
+
// is an explicit user-invoked operation — the user opted in, and the
|
|
16
|
+
// shared 120s cooldown shouldn't gate a manual command.
|
|
17
|
+
//
|
|
18
|
+
// Per design §8 + tasks.md 39.4–39.5.
|
|
19
|
+
|
|
20
|
+
import { ERROR_CATEGORIES, errorResult } from './result-shapes.mjs';
|
|
21
|
+
import { nowIso } from './audit-log.mjs';
|
|
22
|
+
|
|
23
|
+
export const ROLL_SCOPES = Object.freeze({
|
|
24
|
+
NOW: 'now',
|
|
25
|
+
TODAY: 'today',
|
|
26
|
+
RECENT: 'recent',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Public boundary: run the roll pipeline.
|
|
31
|
+
*
|
|
32
|
+
* @param {object} opts
|
|
33
|
+
* @param {string} opts.projectRoot
|
|
34
|
+
* @param {string} [opts.scope] 'now' (default) | 'today' | 'recent'
|
|
35
|
+
* @param {object} opts.backend CompressorBackend (HaikuViaAnthropicApi or MockHaikuBackend)
|
|
36
|
+
* @param {string} [opts.now]
|
|
37
|
+
* @returns {Promise<object>}
|
|
38
|
+
*
|
|
39
|
+
* Note: I2 fix (skill-review 2026-05-28) dropped the v0.1.0 `userDir`
|
|
40
|
+
* forward-compat parameter. The underlying pipelines (compress-session,
|
|
41
|
+
* daily-distill, weekly-curate) all operate purely on projectRoot — none
|
|
42
|
+
* take userDir. When user-tier roll operations land (v0.1.x), the param
|
|
43
|
+
* lands at that PR alongside the actual consumer.
|
|
44
|
+
*/
|
|
45
|
+
export async function runRoll({
|
|
46
|
+
projectRoot,
|
|
47
|
+
scope = ROLL_SCOPES.NOW,
|
|
48
|
+
backend,
|
|
49
|
+
now,
|
|
50
|
+
} = {}) {
|
|
51
|
+
const ts = now ?? nowIso();
|
|
52
|
+
const t0 = Date.now();
|
|
53
|
+
if (!projectRoot) {
|
|
54
|
+
return errorResult({
|
|
55
|
+
category: ERROR_CATEGORIES.MISSING_PROJECT_ROOT,
|
|
56
|
+
errors: ['projectRoot is required'],
|
|
57
|
+
duration_ms: Date.now() - t0,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (!backend || typeof backend.compress !== 'function') {
|
|
61
|
+
return errorResult({
|
|
62
|
+
category: ERROR_CATEGORIES.MISSING_BACKEND,
|
|
63
|
+
errors: ['backend (CompressorBackend) is required'],
|
|
64
|
+
duration_ms: Date.now() - t0,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (!Object.values(ROLL_SCOPES).includes(scope)) {
|
|
68
|
+
return errorResult({
|
|
69
|
+
category: ERROR_CATEGORIES.SCHEMA,
|
|
70
|
+
errors: [`invalid scope: ${scope}; expected 'now' | 'today' | 'recent'`],
|
|
71
|
+
duration_ms: Date.now() - t0,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Lazy-load the underlying pipeline modules so this module's
|
|
76
|
+
// public surface stays narrow + import-cost stays low.
|
|
77
|
+
let result;
|
|
78
|
+
let delegatedTo;
|
|
79
|
+
if (scope === ROLL_SCOPES.NOW) {
|
|
80
|
+
const { compressSession } = await import('./compress-session.mjs');
|
|
81
|
+
delegatedTo = 'compress-session';
|
|
82
|
+
result = await compressSession({
|
|
83
|
+
projectRoot,
|
|
84
|
+
backend,
|
|
85
|
+
now: ts,
|
|
86
|
+
cooldownMs: 0, // user-explicit invocation bypasses 120s gate
|
|
87
|
+
});
|
|
88
|
+
} else if (scope === ROLL_SCOPES.TODAY) {
|
|
89
|
+
const { dailyDistill } = await import('./daily-distill.mjs');
|
|
90
|
+
delegatedTo = 'daily-distill';
|
|
91
|
+
result = await dailyDistill({
|
|
92
|
+
projectRoot,
|
|
93
|
+
backend,
|
|
94
|
+
now: ts,
|
|
95
|
+
cooldownMs: 0,
|
|
96
|
+
});
|
|
97
|
+
} else {
|
|
98
|
+
const { weeklyCurate } = await import('./weekly-curate.mjs');
|
|
99
|
+
delegatedTo = 'weekly-curate';
|
|
100
|
+
result = await weeklyCurate({
|
|
101
|
+
projectRoot,
|
|
102
|
+
backend,
|
|
103
|
+
now: ts,
|
|
104
|
+
cooldownMs: 0,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
action: 'completed',
|
|
110
|
+
scope,
|
|
111
|
+
delegatedTo,
|
|
112
|
+
result,
|
|
113
|
+
duration_ms: Date.now() - t0,
|
|
114
|
+
};
|
|
115
|
+
}
|