@ngockhoale/ukit 3.0.8 → 3.0.9
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 +14 -1
- package/manifests/documentation.yaml +11 -0
- package/package.json +1 -1
- package/scripts/audit/decision-coverage.mjs +29 -2
- package/scripts/bench/data-foundation.mjs +52 -3
- package/scripts/bench/decision-runtime-baseline.mjs +427 -0
- package/scripts/bench/decision-runtime-metrics.mjs +67 -0
- package/scripts/bench/decision-runtime-variant.mjs +626 -0
- package/scripts/bench/memory-ablation.mjs +495 -0
- package/scripts/bench/memory-baseline.mjs +596 -0
- package/scripts/bench/memory-bench.mjs +661 -0
- package/scripts/bench/memory-canary.mjs +321 -0
- package/scripts/bench/memory-corpus.mjs +354 -0
- package/scripts/bench/memory-gate.mjs +389 -0
- package/scripts/bench/memory-metrics.mjs +179 -0
- package/scripts/bench/parallel-agents.mjs +33 -11
- package/scripts/bench/recorder-overhead.mjs +204 -0
- package/scripts/bench/sqlite-spike.mjs +451 -0
- package/scripts/measure-decision-gateway.mjs +306 -0
- package/scripts/perf/audit-perf.mjs +35 -17
- package/src/bug/triageBug.js +4 -3
- package/src/cli/commands/memory.js +357 -63
- package/src/context/detectProjectContext.js +11 -1
- package/src/core/agentRuntime/adapters.js +254 -0
- package/src/core/agentRuntime/artifacts.js +192 -0
- package/src/core/agentRuntime/completionGate.js +176 -0
- package/src/core/agentRuntime/context.js +149 -0
- package/src/core/agentRuntime/contract.js +247 -0
- package/src/core/agentRuntime/diagnostics.js +244 -0
- package/src/core/agentRuntime/evaluation.js +163 -0
- package/src/core/agentRuntime/eventStore.js +404 -0
- package/src/core/agentRuntime/liveness.js +60 -0
- package/src/core/agentRuntime/planCompiler.js +322 -0
- package/src/core/agentRuntime/promotion.js +53 -0
- package/src/core/agentRuntime/qualityComparison.js +112 -0
- package/src/core/agentRuntime/recovery.js +266 -0
- package/src/core/agentRuntime/resourcePolicy.js +78 -0
- package/src/core/agentRuntime/runtimeSupport.js +237 -0
- package/src/core/agentRuntime/supervisor.js +565 -0
- package/src/core/agentRuntime/vmEngine.js +621 -0
- package/src/core/codeintel/analogy.js +3 -2
- package/src/core/experiments/dynamicWorkflow.js +17 -2
- package/src/core/fileOps.js +21 -3
- package/src/core/memory/deltaOverlays.js +75 -30
- package/src/core/memory/learningCandidates.js +93 -48
- package/src/core/memory/memoryFlags.js +83 -0
- package/src/core/memory/memoryFreshness.js +190 -0
- package/src/core/memory/memoryHit.js +144 -0
- package/src/core/memory/migrate.js +69 -189
- package/src/core/memory/migrateMapping.js +232 -0
- package/src/core/memory/mutateMemory.js +323 -0
- package/src/core/memory/policy.js +96 -0
- package/src/core/memory/projectIdentity.js +266 -0
- package/src/core/memory/recordIndex.js +178 -0
- package/src/core/memory/recordStore.js +133 -20
- package/src/core/memory/records.js +144 -6
- package/src/core/memory/retrieval.js +259 -125
- package/src/core/memory/store.js +16 -5
- package/src/core/memory/storeBackup.js +226 -0
- package/src/core/memory/storeV2.js +63 -26
- package/src/core/memory/storeV2Loader.js +30 -12
- package/src/core/memory/userMemory.js +38 -20
- package/src/core/memory/writeClassification.js +161 -0
- package/src/core/memory/writeGuard.js +129 -0
- package/src/core/observability/adapters/hookTelemetryAdapter.js +90 -0
- package/src/core/observability/analytics/cohorts.js +148 -0
- package/src/core/observability/analytics/storeDigest.js +163 -0
- package/src/core/observability/evaluation/experimentPlan.js +95 -0
- package/src/core/observability/evaluation/findings.js +99 -0
- package/src/core/observability/evaluation/optimizationKnowledge.js +10 -1
- package/src/core/observability/evaluation/perturbation.js +273 -0
- package/src/core/observability/evaluation/replay.js +7 -1
- package/src/core/observability/evaluation/scorecard.js +23 -3
- package/src/core/observability/rollout.js +11 -7
- package/src/core/observability/schema/compatibility.js +135 -0
- package/src/core/observability/schema/registry.js +99 -0
- package/src/core/observability/schema/validate.js +7 -0
- package/src/core/observability/support/import.js +53 -9
- package/src/core/observability/support/paths.js +13 -3
- package/src/core/observability/support/projector.js +148 -12
- package/src/core/output/index.js +12 -2
- package/src/core/runtimeConfig.js +83 -0
- package/src/core/runtimePaths.js +3 -0
- package/src/core/sensitiveValueScanner.js +40 -0
- package/src/core/token/index.js +40 -3
- package/src/decision/client.js +37 -13
- package/src/decision/protocol.js +1 -1
- package/src/decision/registry.js +5 -3
- package/src/decision/runtimeDecide.js +242 -0
- package/src/decision/runtimeFilter.js +150 -0
- package/src/decision/runtimeScheduler.js +239 -0
- package/src/index/buildIndex.js +13 -12
- package/src/index/queryIndex.js +35 -14
- package/src/index/relatedTests.js +50 -8
- package/src/index/resolveContext.js +9 -4
- package/src/manifest/selectItems.js +7 -3
- package/src/render/instructionRenderer.js +17 -5
- package/template_project/.claude/ukit/index/lib/index-core.mjs +94 -39
- package/template_project/.claude/ukit/index/route-task.mjs +121 -19
- package/template_project/.claude/ukit/index/unic-decision.mjs +28 -13
- package/template_project/.claude/ukit/runtime/memory-flags.mjs +51 -0
- package/template_project/.claude/ukit/runtime/memory-freshness.mjs +155 -0
- package/template_project/.claude/ukit/runtime/memory-policy.mjs +286 -0
- package/template_project/.claude/ukit/runtime/output-compression.mjs +3 -0
- package/template_project/.claude/ukit/runtime/reinject-context.mjs +145 -14
|
@@ -8,25 +8,25 @@ import {
|
|
|
8
8
|
runProjectHygiene,
|
|
9
9
|
} from '../../core/memory/store.js';
|
|
10
10
|
import {
|
|
11
|
-
addRecord,
|
|
12
11
|
getRecord,
|
|
13
12
|
loadRecords,
|
|
14
13
|
queryRecords,
|
|
15
14
|
stats as v2Stats,
|
|
16
|
-
updateRecord,
|
|
17
15
|
} from '../../core/memory/storeV2.js';
|
|
18
16
|
import {
|
|
19
|
-
addUserRecord,
|
|
20
17
|
getUserRecord,
|
|
21
18
|
queryUserRecords,
|
|
22
|
-
removeUserRecord,
|
|
23
|
-
updateUserRecord,
|
|
24
19
|
userMemoryStats,
|
|
25
20
|
} from '../../core/memory/userMemory.js';
|
|
26
21
|
import { runMigration } from '../../core/memory/migrate.js';
|
|
27
|
-
import {
|
|
22
|
+
import { createBackup, restoreBackup, diagnoseStore } from '../../core/memory/storeBackup.js';
|
|
23
|
+
import { mutateMemory } from '../../core/memory/mutateMemory.js';
|
|
24
|
+
import { redactWritePayload } from '../../core/memory/writeGuard.js';
|
|
25
|
+
import { deletePromptCacheEntries } from '../../core/token/index.js';
|
|
26
|
+
import { expand, getContextInjection, search } from '../../core/memory/retrieval.js';
|
|
28
27
|
import { inspectRuntimeConfig } from '../../core/runtimeConfig.js';
|
|
29
28
|
import { buildRuntimePaths } from '../../core/runtimePaths.js';
|
|
29
|
+
import { buildUserPaths } from '../../core/userPaths.js';
|
|
30
30
|
import { pathExists } from '../../core/fileOps.js';
|
|
31
31
|
import { detectProjectContext } from '../../context/detectProjectContext.js';
|
|
32
32
|
import { listLedgerFiles, LEDGER_DIR_REL } from '../../diagnostics/ledgerFiles.js';
|
|
@@ -94,6 +94,37 @@ async function listAllPendingPatternCandidates(projectRoot, runtimePaths) {
|
|
|
94
94
|
}
|
|
95
95
|
return results;
|
|
96
96
|
}
|
|
97
|
+
// FR-025: guard rejections surface labels only — never the matched value or
|
|
98
|
+
// record body. Returns true when the result was a handled rejection.
|
|
99
|
+
function printGuardRejection(res) {
|
|
100
|
+
const labels = Array.isArray(res.labels) && res.labels.length > 0
|
|
101
|
+
? res.labels.join(', ')
|
|
102
|
+
: 'unknown';
|
|
103
|
+
console.log(`[UKit] rejected: ${res.reason ?? 'secret-detected'} (${labels})`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Shared mutateMemory error mapping for CLI write ops: conflict/not-found/
|
|
107
|
+
// corrupt become thrown errors; rejected prints labels and returns quietly.
|
|
108
|
+
function handleMutationResult(res, { notFoundMessage }) {
|
|
109
|
+
if (res.status === 'ok') return res;
|
|
110
|
+
if (res.status === 'not-found') throw new Error(notFoundMessage);
|
|
111
|
+
if (res.status === 'conflict') {
|
|
112
|
+
throw new Error(`Write conflict (${res.reason ?? 'revision-mismatch'}) — a concurrent writer won; re-run the command.`);
|
|
113
|
+
}
|
|
114
|
+
// M06 rollout flag (SPEC §9): writer stage 'off'/killSwitch surfaces as a
|
|
115
|
+
// typed disabled message, not a failure — the write lane is intentionally
|
|
116
|
+
// gated, exit-code class unchanged.
|
|
117
|
+
if (res.status === 'disabled' && res.reason === 'rollout-stage') {
|
|
118
|
+
console.log('[UKit] memory writer disabled by rollout stage');
|
|
119
|
+
return res;
|
|
120
|
+
}
|
|
121
|
+
if (res.status === 'rejected') {
|
|
122
|
+
printGuardRejection(res);
|
|
123
|
+
return res;
|
|
124
|
+
}
|
|
125
|
+
throw new Error(`Memory write failed: ${res.status}${res.reason ? ` — ${res.reason}` : ''}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
97
128
|
|
|
98
129
|
// ---- `ukit memory v2` ops (SPEC §11) ----
|
|
99
130
|
|
|
@@ -151,13 +182,29 @@ async function runMemoryV2(projectRoot, args) {
|
|
|
151
182
|
throw new Error(`Cannot promote ${record.type} records (only derived_fact or episode).`);
|
|
152
183
|
}
|
|
153
184
|
// Explicit approval only — no auto-promotion (SPEC §11,
|
|
154
|
-
// memoryV2.promotion.episodeToRuleRequiresApproval).
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
185
|
+
// memoryV2.promotion.episodeToRuleRequiresApproval). The write goes
|
|
186
|
+
// through mutateMemory with expectedRevision so a concurrent writer
|
|
187
|
+
// surfaces as a conflict instead of a silent clobber (FR-023).
|
|
188
|
+
const res = await mutateMemory(
|
|
189
|
+
{ kind: 'project', projectRoot },
|
|
190
|
+
{
|
|
191
|
+
op: 'update',
|
|
192
|
+
payload: {
|
|
193
|
+
id,
|
|
194
|
+
patch: {
|
|
195
|
+
type: 'project_rule',
|
|
196
|
+
scope: 'repo',
|
|
197
|
+
provenance: `${record.provenance ?? 'manual'};promoted-from:${record.id}`,
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
expectedRevision: record.revision,
|
|
201
|
+
},
|
|
202
|
+
);
|
|
203
|
+
const handled = handleMutationResult(res, {
|
|
204
|
+
notFoundMessage: `Memory v2 record not found: ${id}`,
|
|
159
205
|
});
|
|
160
|
-
|
|
206
|
+
if (handled.status !== 'ok') return;
|
|
207
|
+
console.log(`[UKit] Promoted ${handled.record.id} → project_rule.`);
|
|
161
208
|
return;
|
|
162
209
|
}
|
|
163
210
|
|
|
@@ -166,10 +213,22 @@ async function runMemoryV2(projectRoot, args) {
|
|
|
166
213
|
if (!id) {
|
|
167
214
|
throw new Error('Missing record id. Usage: ukit memory v2 stale <id>');
|
|
168
215
|
}
|
|
169
|
-
const
|
|
170
|
-
if (!
|
|
216
|
+
const record = await getRecord(projectRoot, id);
|
|
217
|
+
if (!record) {
|
|
171
218
|
throw new Error(`Memory v2 record not found: ${id}`);
|
|
172
219
|
}
|
|
220
|
+
const res = await mutateMemory(
|
|
221
|
+
{ kind: 'project', projectRoot },
|
|
222
|
+
{
|
|
223
|
+
op: 'update',
|
|
224
|
+
payload: { id, patch: { status: 'stale' } },
|
|
225
|
+
expectedRevision: record.revision,
|
|
226
|
+
},
|
|
227
|
+
);
|
|
228
|
+
const handled = handleMutationResult(res, {
|
|
229
|
+
notFoundMessage: `Memory v2 record not found: ${id}`,
|
|
230
|
+
});
|
|
231
|
+
if (handled.status !== 'ok') return;
|
|
173
232
|
console.log(`[UKit] Marked ${id} as stale.`);
|
|
174
233
|
return;
|
|
175
234
|
}
|
|
@@ -178,9 +237,19 @@ async function runMemoryV2(projectRoot, args) {
|
|
|
178
237
|
const dryRun = rest.includes('--dry-run');
|
|
179
238
|
const result = await runMigration(projectRoot, { dryRun });
|
|
180
239
|
if (dryRun) {
|
|
181
|
-
|
|
240
|
+
// Per-record plan: counts per action + ids only — never record bodies.
|
|
241
|
+
const counts = {};
|
|
242
|
+
for (const entry of result.plan) {
|
|
243
|
+
counts[entry.action] = (counts[entry.action] ?? 0) + 1;
|
|
244
|
+
}
|
|
245
|
+
const countText = Object.entries(counts).map(([a, n]) => `${a}=${n}`).join(' ') || 'none';
|
|
246
|
+
console.log(`[UKit] migrate dry-run: ${countText} (migrated=${result.migrated} skipped=${result.skipped})`);
|
|
247
|
+
for (const entry of result.plan) {
|
|
248
|
+
console.log(`[UKit] ${entry.action} ${entry.id} (${entry.source}) — ${entry.reason}`);
|
|
249
|
+
}
|
|
182
250
|
return;
|
|
183
251
|
}
|
|
252
|
+
|
|
184
253
|
if (result.migrated === 0) {
|
|
185
254
|
console.log('[UKit] Nothing to migrate (marker present or no legacy memory).');
|
|
186
255
|
return;
|
|
@@ -191,17 +260,16 @@ async function runMemoryV2(projectRoot, args) {
|
|
|
191
260
|
console.log(`[UKit] v2 store: ${stats.total} record(s) — ${JSON.stringify(stats.byType)}`);
|
|
192
261
|
return;
|
|
193
262
|
}
|
|
194
|
-
|
|
195
263
|
throw new Error(`Unknown memory v2 op: ${op}. Expected list|show|promote|stale|migrate.`);
|
|
196
264
|
}
|
|
197
265
|
|
|
198
266
|
// ---- `ukit memory --user` — user-level store at ~/.ukit (FR-011) ----
|
|
199
267
|
// Same v2 record document, no v1→v2 migration (the user layer starts at v2).
|
|
200
|
-
// Ops: list|add|get|update|forget|stats; an optional `v2` token is accepted so
|
|
268
|
+
// Ops: list|add|get|update|forget|purge|stats; an optional `v2` token is accepted so
|
|
201
269
|
// `memory --user v2 add` and `memory --user add` are equivalent. Any other
|
|
202
270
|
// subcommand under --user is a misuse error — v1 lanes stay project-level.
|
|
203
271
|
|
|
204
|
-
const USER_MEMORY_OPS = new Set(['list', 'add', 'get', 'update', 'forget', 'stats']);
|
|
272
|
+
const USER_MEMORY_OPS = new Set(['list', 'add', 'get', 'update', 'forget', 'purge', 'stats']);
|
|
205
273
|
|
|
206
274
|
async function runUserMemory(homeDir, args) {
|
|
207
275
|
const rest = (args[0] ?? '').toLowerCase() === 'v2' ? args.slice(1) : args;
|
|
@@ -210,7 +278,7 @@ async function runUserMemory(homeDir, args) {
|
|
|
210
278
|
|
|
211
279
|
if (!USER_MEMORY_OPS.has(op)) {
|
|
212
280
|
throw new Error(
|
|
213
|
-
`--user only supports v2 ops (list|add|get|update|forget|stats); got "${op}".`,
|
|
281
|
+
`--user only supports v2 ops (list|add|get|update|forget|purge|stats); got "${op}".`,
|
|
214
282
|
);
|
|
215
283
|
}
|
|
216
284
|
|
|
@@ -242,15 +310,32 @@ async function runUserMemory(homeDir, args) {
|
|
|
242
310
|
if (!text) {
|
|
243
311
|
throw new Error('Missing record text. Usage: ukit memory --user add [--type <type>] [--scope <scope>] <text>');
|
|
244
312
|
}
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
313
|
+
const res = await mutateMemory(
|
|
314
|
+
{ kind: 'user', homeDir },
|
|
315
|
+
{
|
|
316
|
+
op: 'add',
|
|
317
|
+
payload: {
|
|
318
|
+
type: typeFlag.value ?? 'derived_fact',
|
|
319
|
+
scope: scopeFlag.value ?? 'user',
|
|
320
|
+
text,
|
|
321
|
+
provenance: provenanceFlag.value ?? 'cli',
|
|
322
|
+
createdBy: 'cli',
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
);
|
|
326
|
+
if (res.status === 'ok') {
|
|
327
|
+
console.log(`[UKit] Added user record ${res.record.id}.`);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (res.status === 'duplicate') {
|
|
331
|
+
console.log(`[UKit] User record already exists${res.record ? ` (${res.record.id})` : ''}.`);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (res.status === 'rejected') {
|
|
335
|
+
printGuardRejection(res);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
throw new Error(`User memory add failed: ${res.status}${res.reason ? ` — ${res.reason}` : ''}`);
|
|
254
339
|
}
|
|
255
340
|
|
|
256
341
|
if (op === 'get') {
|
|
@@ -283,11 +368,15 @@ async function runUserMemory(homeDir, args) {
|
|
|
283
368
|
if (Object.keys(patch).length === 0) {
|
|
284
369
|
throw new Error('Nothing to update. Pass --status/--type/--scope or new text.');
|
|
285
370
|
}
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
371
|
+
const res = await mutateMemory(
|
|
372
|
+
{ kind: 'user', homeDir },
|
|
373
|
+
{ op: 'update', payload: { id, patch } },
|
|
374
|
+
);
|
|
375
|
+
const handled = handleMutationResult(res, {
|
|
376
|
+
notFoundMessage: `User memory record not found: ${id}`,
|
|
377
|
+
});
|
|
378
|
+
if (handled.status !== 'ok') return;
|
|
379
|
+
console.log(`[UKit] Updated user record ${handled.record.id}.`);
|
|
291
380
|
return;
|
|
292
381
|
}
|
|
293
382
|
|
|
@@ -296,14 +385,40 @@ async function runUserMemory(homeDir, args) {
|
|
|
296
385
|
if (!id) {
|
|
297
386
|
throw new Error('Missing record id. Usage: ukit memory --user forget <id>');
|
|
298
387
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
388
|
+
// forget = archive semantics (SPEC G5) — the record stays in the store
|
|
389
|
+
// with status:'archived' so tombstone/review invariants hold.
|
|
390
|
+
const res = await mutateMemory(
|
|
391
|
+
{ kind: 'user', homeDir },
|
|
392
|
+
{ op: 'forget', payload: { id } },
|
|
393
|
+
);
|
|
394
|
+
const handled = handleMutationResult(res, {
|
|
395
|
+
notFoundMessage: `User memory record not found: ${id}`,
|
|
396
|
+
});
|
|
397
|
+
if (handled.status !== 'ok') return;
|
|
303
398
|
console.log(`[UKit] Forgot user record ${id}.`);
|
|
304
399
|
return;
|
|
305
400
|
}
|
|
306
401
|
|
|
402
|
+
if (op === 'purge') {
|
|
403
|
+
const confirm = opArgs.includes('--confirm');
|
|
404
|
+
const id = opArgs.filter((a) => a !== '--confirm').join(' ').trim();
|
|
405
|
+
if (!id) {
|
|
406
|
+
throw new Error('Missing record id. Usage: ukit memory --user purge <id> --confirm');
|
|
407
|
+
}
|
|
408
|
+
if (!confirm) {
|
|
409
|
+
throw new Error(`Refusing to purge user record ${id} — re-run with --confirm.`);
|
|
410
|
+
}
|
|
411
|
+
const res = await mutateMemory(
|
|
412
|
+
{ kind: 'user', homeDir },
|
|
413
|
+
{ op: 'purge', payload: { id }, confirmScope: 'user' },
|
|
414
|
+
);
|
|
415
|
+
const handled = handleMutationResult(res, {
|
|
416
|
+
notFoundMessage: `User memory record not found: ${id}`,
|
|
417
|
+
});
|
|
418
|
+
if (handled.status !== 'ok') return;
|
|
419
|
+
console.log(`[UKit] Purged ${id} (tombstone recorded).`);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
307
422
|
// op === 'stats'
|
|
308
423
|
const stats = await userMemoryStats({ homeDir });
|
|
309
424
|
console.log(`[UKit] User memory: total=${stats.total} byType=${JSON.stringify(stats.byType)} byStatus=${JSON.stringify(stats.byStatus)} invalidSkipped=${stats.invalidSkipped}`);
|
|
@@ -482,28 +597,116 @@ async function runMemoryEpisode(projectRoot, args) {
|
|
|
482
597
|
|
|
483
598
|
const ttlDays = Number(config?.memoryV2?.episodeTtlDays) || 90;
|
|
484
599
|
const projectId = (await detectProjectContext(projectRoot)).project.name;
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
600
|
+
// FR-023: the add goes through mutateMemory with the ledger key as the
|
|
601
|
+
// idempotency key — a hook-invoked rerun is a journal 'duplicate' even if
|
|
602
|
+
// the meta.ledgerKey fast-path above is bypassed.
|
|
603
|
+
const res = await mutateMemory(
|
|
604
|
+
{ kind: 'project', projectRoot },
|
|
605
|
+
{
|
|
606
|
+
op: 'add',
|
|
607
|
+
idempotencyKey: ledgerKey,
|
|
608
|
+
payload: {
|
|
609
|
+
type: 'episode',
|
|
610
|
+
scope: 'session',
|
|
611
|
+
text,
|
|
612
|
+
provenance: 'exec-ledger',
|
|
613
|
+
confidence: 0.6,
|
|
614
|
+
createdBy: 'episode-hook',
|
|
615
|
+
projectId,
|
|
616
|
+
validUntil: Date.now() + ttlDays * 24 * 60 * 60 * 1000,
|
|
617
|
+
meta: { sessionId, ledgerKey },
|
|
618
|
+
},
|
|
619
|
+
},
|
|
620
|
+
);
|
|
621
|
+
if (res.status === 'duplicate') {
|
|
622
|
+
console.log('[UKit] episode: already recorded');
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
if (res.status === 'rejected') {
|
|
626
|
+
printGuardRejection(res);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
if (res.status !== 'ok') {
|
|
630
|
+
throw new Error(`episode write failed: ${res.status}${res.reason ? ` — ${res.reason}` : ''}`);
|
|
631
|
+
}
|
|
632
|
+
console.log(`[UKit] episode: recorded ${res.record.id} for session ${sessionId}`);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// ---- `ukit memory backup|restore|doctor` — operator lane (SPEC §9, FR-011) ----
|
|
636
|
+
// Output is counts/ids/paths only — never record bodies. `doctor` is strictly
|
|
637
|
+
// read-only; `--dry-run` flags write nothing. `--user` targets the user store
|
|
638
|
+
// at ~/.ukit instead of the project store (dispatch happens before the generic
|
|
639
|
+
// --user lane so these subcommands keep their own flag semantics).
|
|
640
|
+
|
|
641
|
+
async function runMemoryStoreOps(subcommand, { projectRoot, homeDir, isUser, rest }) {
|
|
642
|
+
if (subcommand === 'backup') {
|
|
643
|
+
const outFlag = extractFlag(rest, '--out');
|
|
644
|
+
const { backupDir, manifest } = await createBackup({
|
|
645
|
+
projectRoot: isUser ? null : projectRoot,
|
|
646
|
+
homeDir: isUser ? homeDir : undefined,
|
|
647
|
+
outDir: outFlag.value ?? undefined,
|
|
648
|
+
includeUser: isUser,
|
|
649
|
+
});
|
|
650
|
+
console.log(`[UKit] Backup written: ${backupDir}`);
|
|
651
|
+
for (const store of manifest.stores) {
|
|
652
|
+
console.log(
|
|
653
|
+
`[UKit] ${store.kind}: state=${store.state} records=${store.recordCount} tombstones=${store.tombstoneCount} file=${store.file ?? 'none'}`,
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (subcommand === 'restore') {
|
|
660
|
+
const dryRun = rest.includes('--dry-run');
|
|
661
|
+
const positional = rest.filter((a) => a !== '--dry-run');
|
|
662
|
+
const backupDir = positional.join(' ').trim();
|
|
663
|
+
if (!backupDir) {
|
|
664
|
+
throw new Error('Missing backup dir. Usage: ukit memory restore <backupDir> [--dry-run] [--user]');
|
|
665
|
+
}
|
|
666
|
+
const result = await restoreBackup(backupDir, {
|
|
667
|
+
projectRoot: isUser ? null : projectRoot,
|
|
668
|
+
homeDir,
|
|
669
|
+
dryRun,
|
|
670
|
+
});
|
|
671
|
+
for (const entry of result.plan) {
|
|
672
|
+
const detail = entry.reason ?? (entry.records != null ? `records=${entry.records}` : `added=${entry.added ?? 0}`);
|
|
673
|
+
console.log(`[UKit] ${entry.kind}: ${entry.action} ${detail}`);
|
|
674
|
+
}
|
|
675
|
+
console.log(
|
|
676
|
+
dryRun
|
|
677
|
+
? `[UKit] restore dry-run: ${result.plan.length} plan entr(ies), restored=0 skipped=${result.skipped} — nothing written.`
|
|
678
|
+
: `[UKit] Restored ${result.restored} record(s), skipped ${result.skipped}.`,
|
|
679
|
+
);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (subcommand === 'doctor') {
|
|
684
|
+
const { stores } = await diagnoseStore({
|
|
685
|
+
projectRoot: isUser ? null : projectRoot,
|
|
686
|
+
homeDir: isUser ? homeDir : undefined,
|
|
687
|
+
});
|
|
688
|
+
for (const store of stores) {
|
|
689
|
+
console.log(
|
|
690
|
+
`[UKit] ${store.kind}: state=${store.state} generation=${store.generation} records=${store.recordCount} tombstones=${store.tombstoneCount} quarantine=${store.quarantine.length}`,
|
|
691
|
+
);
|
|
692
|
+
if (store.state !== 'ok') {
|
|
693
|
+
const paths = isUser ? buildUserPaths({ homeDir }) : buildRuntimePaths(projectRoot);
|
|
694
|
+
const quarantineDir = path.join(path.dirname(paths.memoryV2RecordsPath), 'quarantine');
|
|
695
|
+
console.log(`[UKit] quarantine-dir: ${quarantineDir}`);
|
|
696
|
+
for (const q of store.quarantine) {
|
|
697
|
+
console.log(`[UKit] quarantined: ${q}`);
|
|
698
|
+
}
|
|
699
|
+
if (store.hint) console.log(`[UKit] hint: ${store.hint}`);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
497
704
|
}
|
|
498
705
|
|
|
499
706
|
export async function runMemory({ projectRoot, homeDir = os.homedir(), argv = [] }) {
|
|
500
707
|
const userFlagIndex = argv.indexOf('--user');
|
|
501
708
|
const isUser = userFlagIndex >= 0;
|
|
502
709
|
const runtimePaths = buildRuntimePaths(projectRoot);
|
|
503
|
-
// --user operates on ~/.ukit and does not require the project runtime.
|
|
504
|
-
if (!isUser && !(await pathExists(runtimePaths.runtimeRoot))) {
|
|
505
|
-
throw new Error('Shared UKit runtime not found. Run `ukit install` first.');
|
|
506
|
-
}
|
|
507
710
|
|
|
508
711
|
const argvRest = isUser
|
|
509
712
|
? [...argv.slice(0, userFlagIndex), ...argv.slice(userFlagIndex + 1)]
|
|
@@ -516,6 +719,23 @@ export async function runMemory({ projectRoot, homeDir = os.homedir(), argv = []
|
|
|
516
719
|
return;
|
|
517
720
|
}
|
|
518
721
|
|
|
722
|
+
// Operator lane runs without the runtime gate: doctor must report
|
|
723
|
+
// state=missing on a project with no store yet, and backup/restore are
|
|
724
|
+
// exactly the tools used before/without a full install.
|
|
725
|
+
if (subcommand === 'backup' || subcommand === 'restore' || subcommand === 'doctor') {
|
|
726
|
+
await runMemoryStoreOps(subcommand, { projectRoot, homeDir, isUser, rest });
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// --user operates on ~/.ukit and does not require the project runtime.
|
|
731
|
+
if (!isUser && !(await pathExists(runtimePaths.runtimeRoot))) {
|
|
732
|
+
throw new Error('Shared UKit runtime not found. Run `ukit install` first.');
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
|
|
519
739
|
if (isUser) {
|
|
520
740
|
await runUserMemory(homeDir, argvRest);
|
|
521
741
|
return;
|
|
@@ -659,15 +879,57 @@ export async function runMemory({ projectRoot, homeDir = os.homedir(), argv = []
|
|
|
659
879
|
throw new Error('Missing query. Usage: ukit memory search <query>');
|
|
660
880
|
}
|
|
661
881
|
|
|
662
|
-
const
|
|
882
|
+
const projectContext = await detectProjectContext(projectRoot, { homeDir });
|
|
883
|
+
const results = await search(query, 'all', 10, {
|
|
884
|
+
projectRoot,
|
|
885
|
+
// Stable registry id (M01-05) — never the display name. Null/ambiguous
|
|
886
|
+
// is still passed through: policy denies repo-bound records rather than
|
|
887
|
+
// silently widening to cross-repo results.
|
|
888
|
+
projectId: projectContext.project.id ?? null,
|
|
889
|
+
projectAliases: projectContext.project.aliases ?? [],
|
|
890
|
+
});
|
|
663
891
|
if (results.length === 0) {
|
|
664
892
|
console.log('[UKit] No memory matches found.');
|
|
665
893
|
return;
|
|
666
894
|
}
|
|
667
895
|
|
|
668
896
|
for (const result of results) {
|
|
669
|
-
|
|
897
|
+
// FR-007: `<id> — <snippet> [<freshness≠fresh>] (<kind>:<locator>)` —
|
|
898
|
+
// tag only when the resolved state is not fresh; citation only when the
|
|
899
|
+
// hit carries one. Ids/locators/labels only — never meta or bodies.
|
|
900
|
+
const snippet = result.snippet ?? result.summary;
|
|
901
|
+
const tag = result.freshness && result.freshness.state !== 'fresh'
|
|
902
|
+
? ` [${result.freshness.state}]`
|
|
903
|
+
: '';
|
|
904
|
+
const cite = result.citation
|
|
905
|
+
? ` (${result.citation.kind}:${result.citation.locator})`
|
|
906
|
+
: '';
|
|
907
|
+
console.log(`${result.id} — ${snippet}${tag}${cite}`);
|
|
908
|
+
}
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
if (subcommand === 'expand') {
|
|
913
|
+
const id = rest.join(' ').trim();
|
|
914
|
+
if (!id) {
|
|
915
|
+
throw new Error('Missing memory id. Usage: ukit memory expand <id>');
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const projectContext = await detectProjectContext(projectRoot, { homeDir });
|
|
919
|
+
const result = await expand(id, {
|
|
920
|
+
projectRoot,
|
|
921
|
+
projectId: projectContext.project.id ?? null,
|
|
922
|
+
projectAliases: projectContext.project.aliases ?? [],
|
|
923
|
+
});
|
|
924
|
+
// FR-007: the hit block carries citation/freshness/reasons — provenance
|
|
925
|
+
// surface only, never record meta. Legacy lane has no hit; its
|
|
926
|
+
// fullContent is the pre-existing expansion surface.
|
|
927
|
+
if (result.hit) {
|
|
928
|
+
console.log(JSON.stringify(result.hit, null, 2));
|
|
929
|
+
} else {
|
|
930
|
+
console.log(result.fullContent);
|
|
670
931
|
}
|
|
932
|
+
console.log(`relatedItems: ${(result.relatedItems ?? []).join(', ') || 'none'}`);
|
|
671
933
|
return;
|
|
672
934
|
}
|
|
673
935
|
|
|
@@ -687,8 +949,9 @@ export async function runMemory({ projectRoot, homeDir = os.homedir(), argv = []
|
|
|
687
949
|
throw new Error('Memory recall is disabled in .ukit/storage/config.json.');
|
|
688
950
|
}
|
|
689
951
|
|
|
690
|
-
const projectContext = await detectProjectContext(projectRoot);
|
|
691
|
-
const injection = await getContextInjection(query, projectContext.project.
|
|
952
|
+
const projectContext = await detectProjectContext(projectRoot, { homeDir });
|
|
953
|
+
const injection = await getContextInjection(query, projectContext.project.id ?? null, {
|
|
954
|
+
projectAliases: projectContext.project.aliases ?? [],
|
|
692
955
|
projectRoot,
|
|
693
956
|
maxTokens: runtimeConfig.memory.maxInjectionTokens,
|
|
694
957
|
promptCache: Boolean(runtimeConfig.tokenPipeline?.promptCache),
|
|
@@ -729,25 +992,53 @@ export async function runMemory({ projectRoot, homeDir = os.homedir(), argv = []
|
|
|
729
992
|
return;
|
|
730
993
|
}
|
|
731
994
|
|
|
995
|
+
if (subcommand === 'purge') {
|
|
996
|
+
const memoryId = rest.join(' ').trim();
|
|
997
|
+
if (!memoryId) {
|
|
998
|
+
throw new Error('Missing memory id. Usage: ukit memory purge <id>');
|
|
999
|
+
}
|
|
1000
|
+
// FR-023/§9: purge removes the record, writes a tombstone, then sweeps
|
|
1001
|
+
// prompt-cache entries that embedded the purged record's text.
|
|
1002
|
+
const res = await mutateMemory(
|
|
1003
|
+
{ kind: 'project', projectRoot },
|
|
1004
|
+
{ op: 'purge', payload: { id: memoryId } },
|
|
1005
|
+
);
|
|
1006
|
+
const handled = handleMutationResult(res, {
|
|
1007
|
+
notFoundMessage: `Memory record not found: ${memoryId}`,
|
|
1008
|
+
});
|
|
1009
|
+
if (handled.status !== 'ok') return;
|
|
1010
|
+
await deletePromptCacheEntries(projectRoot, { selectedIds: [memoryId] });
|
|
1011
|
+
console.log(`[UKit] Purged ${memoryId} (tombstone recorded).`);
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
732
1015
|
if (subcommand === 'export') {
|
|
733
1016
|
const data = await exportMemory(projectRoot);
|
|
734
|
-
|
|
1017
|
+
// FR-023/025: defense-in-depth redaction at the CLI boundary too —
|
|
1018
|
+
// store.js exportMemory already redacts, this catches any future lane.
|
|
1019
|
+
const { config } = await inspectRuntimeConfig(projectRoot);
|
|
1020
|
+
const { payload: safe } = redactWritePayload({ meta: data }, { config });
|
|
1021
|
+
console.log(JSON.stringify(safe.meta, null, 2));
|
|
735
1022
|
return;
|
|
736
1023
|
}
|
|
737
1024
|
|
|
1025
|
+
|
|
1026
|
+
|
|
738
1027
|
throw new Error(`Unknown memory subcommand: ${subcommand}. Run "ukit memory help".`);
|
|
739
1028
|
}
|
|
740
1029
|
|
|
741
1030
|
export function printMemoryHelp() {
|
|
742
1031
|
console.log('UKit Memory Commands');
|
|
743
|
-
console.log('Usage: ukit memory <list|search|recall|forget|export|propose|approve|reject|learn|promote|episode|hygiene> [args]');
|
|
1032
|
+
console.log('Usage: ukit memory <list|search|expand|recall|forget|purge|export|propose|approve|reject|learn|promote|episode|hygiene|backup|restore|doctor> [args]');
|
|
744
1033
|
console.log('');
|
|
745
1034
|
console.log('Subcommands:');
|
|
746
1035
|
console.log(' list List memory items in shared .ukit/storage/memory');
|
|
747
1036
|
console.log(' list --pending List pending pattern candidates awaiting approval');
|
|
748
|
-
console.log(' search <query> Search memory summaries and content');
|
|
1037
|
+
console.log(' search <query> Search memory summaries and content (prints freshness tag + citation)');
|
|
1038
|
+
console.log(' expand <id> Print the MemoryHit block (citation/freshness/reasons) + related ids');
|
|
749
1039
|
console.log(' recall <task> Print a compact previous-context block for the current task');
|
|
750
|
-
console.log(' forget <id>
|
|
1040
|
+
console.log(' forget <id> Archive one memory item by id');
|
|
1041
|
+
console.log(' purge <id> Remove a v2 record + tombstone it, and sweep prompt-cache entries');
|
|
751
1042
|
console.log(' export Print all memory as JSON');
|
|
752
1043
|
console.log(' propose "<text>" [--category <name>] [--project <id>] Propose a project convention for human approval');
|
|
753
1044
|
console.log(' approve <id> [--project <id>] Approve a pending pattern candidate into project conventions');
|
|
@@ -756,6 +1047,9 @@ export function printMemoryHelp() {
|
|
|
756
1047
|
console.log(' promote [--dry-run] Render approved rules/procedures into the ## ukit-learned block in docs/MEMORY.md');
|
|
757
1048
|
console.log(' episode [--dry-run] [--session <id>] Write a session episode record from the exec-ledger');
|
|
758
1049
|
console.log(' hygiene [--project <id>] Run decision-conflict resolution + session archiving now');
|
|
759
|
-
console.log('
|
|
760
|
-
console.log(' --
|
|
1050
|
+
console.log(' backup [--out <dir>] [--user] Snapshot v2 store(s) + sha256 manifest into a backup dir');
|
|
1051
|
+
console.log(' restore <dir> [--dry-run] [--user] Restore store(s) from a backup dir (plan only with --dry-run)');
|
|
1052
|
+
console.log(' doctor [--user] Diagnose v2 store state + counts + quarantine (read-only)');
|
|
1053
|
+
console.log(' v2 <list|show|promote|stale|migrate> Operate on the project v2 record store (migrate supports --dry-run plan output)');
|
|
1054
|
+
console.log(' --user [v2] <list|add|get|update|forget|purge|stats> Operate on the user-level store at ~/.ukit/storage/memory/v2/records.json (purge requires --confirm)');
|
|
761
1055
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { readJsonIfExists } from '../core/fileOps.js';
|
|
3
3
|
import { detectPackageManager } from '../core/packageManager.js';
|
|
4
|
+
import { resolveProjectIdentity } from '../core/memory/projectIdentity.js';
|
|
4
5
|
|
|
5
6
|
function inferProjectName(projectRoot, packageJson) {
|
|
6
7
|
if (typeof packageJson?.name === 'string' && packageJson.name.trim() !== '') {
|
|
@@ -10,16 +11,25 @@ function inferProjectName(projectRoot, packageJson) {
|
|
|
10
11
|
return path.basename(projectRoot);
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
export async function detectProjectContext(projectRoot) {
|
|
14
|
+
export async function detectProjectContext(projectRoot, { homeDir } = {}) {
|
|
14
15
|
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
15
16
|
// A corrupt package.json must degrade to basename naming, not crash the
|
|
16
17
|
// status/memory CLIs with a raw SyntaxError.
|
|
17
18
|
const packageJson = await readJsonIfExists(packageJsonPath).catch(() => null);
|
|
18
19
|
|
|
20
|
+
// Identity resolution must never break context detection: any resolver
|
|
21
|
+
// failure degrades to `id: null` (SPEC §10 — deny-by-default downstream).
|
|
22
|
+
const identity = await resolveProjectIdentity(projectRoot, { homeDir })
|
|
23
|
+
.catch(() => null);
|
|
24
|
+
|
|
19
25
|
return {
|
|
20
26
|
project: {
|
|
21
27
|
root: projectRoot,
|
|
22
28
|
name: inferProjectName(projectRoot, packageJson),
|
|
29
|
+
id: identity?.projectId ?? null,
|
|
30
|
+
// Registry-attested display names — lets the read path match migrated
|
|
31
|
+
// v1 records whose project_id is still the legacy name (M01-05).
|
|
32
|
+
aliases: Array.isArray(identity?.aliases) ? identity.aliases : [],
|
|
23
33
|
},
|
|
24
34
|
runtime: {
|
|
25
35
|
packageManager: await detectPackageManager(projectRoot, packageJson),
|