@ngockhoale/ukit 3.0.12 → 3.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -0
  3. package/manifests/documentation.yaml +12 -0
  4. package/package.json +1 -1
  5. package/scripts/bench/data-foundation.mjs +368 -50
  6. package/src/cli/commands/doctor.js +232 -3
  7. package/src/cli/commands/feedback.js +64 -1
  8. package/src/cli/commands/install.js +18 -0
  9. package/src/cli/commands/memory.js +42 -37
  10. package/src/cli/commands/telemetry.js +460 -0
  11. package/src/cli/index.js +7 -0
  12. package/src/core/agentRuntime/adapters.js +83 -2
  13. package/src/core/agentRuntime/diagnostics.js +104 -0
  14. package/src/core/agentRuntime/supervisor.js +137 -0
  15. package/src/core/agentRuntime/telemetry.js +204 -0
  16. package/src/core/memory/memoryEmit.js +131 -0
  17. package/src/core/memory/memoryHit.js +1 -1
  18. package/src/core/memory/migrate.js +18 -11
  19. package/src/core/memory/migrateMapping.js +15 -7
  20. package/src/core/memory/mutateMemory.js +22 -4
  21. package/src/core/memory/recordIndex.js +10 -3
  22. package/src/core/memory/recordStore.js +28 -3
  23. package/src/core/memory/retrieval.js +79 -38
  24. package/src/core/memory/store.js +37 -37
  25. package/src/core/memory/storeV2.js +28 -25
  26. package/src/core/memory/storeV2Loader.js +2 -2
  27. package/src/core/observability/adapters/ingest.js +576 -0
  28. package/src/core/observability/analytics/anomalies.js +415 -0
  29. package/src/core/observability/analytics/summary.js +16 -1
  30. package/src/core/observability/emit/config.js +69 -1
  31. package/src/core/observability/emit/crash.js +434 -0
  32. package/src/core/observability/emit/lifecycle.js +349 -0
  33. package/src/core/observability/emit/recorder.js +135 -9
  34. package/src/core/observability/evaluation/aiPacket.js +52 -10
  35. package/src/core/observability/evaluation/outcomes.js +95 -0
  36. package/src/core/observability/evaluation/runner.js +225 -0
  37. package/src/core/observability/privacy/allowlist.js +23 -3
  38. package/src/core/observability/schema/compatibility.js +48 -3
  39. package/src/core/observability/schema/constants.js +5 -0
  40. package/src/core/observability/schema/registry.js +57 -0
  41. package/src/core/observability/schema/validate.js +68 -6
  42. package/src/core/observability/segments/internal.js +42 -8
  43. package/src/core/observability/segments/readSegments.js +35 -1
  44. package/src/core/observability/segments/recovery.js +3 -2
  45. package/src/core/observability/segments/retention.js +137 -33
  46. package/src/core/observability/support/projector.js +88 -18
  47. package/src/core/observability/support/provision.js +160 -0
  48. package/src/core/observability/support/renderer.js +2 -2
  49. package/src/core/observability/support/schedule.js +174 -0
  50. package/template_project/.claude/hooks/auto-allow-bash.sh +7 -1
  51. package/template_project/.claude/hooks/auto-prune-bash.sh +16 -7
  52. package/template_project/.claude/hooks/verification-guard.sh +13 -4
  53. package/template_project/.claude/ukit/runtime/async-lock.mjs +26 -0
@@ -51,24 +51,24 @@ function extractFlag(args, flag) {
51
51
  };
52
52
  }
53
53
 
54
- async function listAllPendingPatternCandidates(projectRoot, runtimePaths) {
54
+ async function listAllPendingPatternCandidates(projectRoot, runtimePaths, { homeDir } = {}) {
55
55
  // v2 mode: pending pattern candidates live as derived_fact records with
56
56
  // meta.legacyStatus === 'pending' (facade handles the conversion).
57
57
  // Respect memoryV2.enabled — without this check loadRecords would run the
58
58
  // lazy v1→v2 migration even when the whole v2 lane is disabled.
59
59
  let v2Enabled = true;
60
60
  try {
61
- const { config } = await inspectRuntimeConfig(projectRoot);
61
+ const { config } = await inspectRuntimeConfig(projectRoot, { homeDir });
62
62
  if (config?.memoryV2?.enabled === false) v2Enabled = false;
63
63
  } catch {
64
64
  // unreadable config → default enabled
65
65
  }
66
- const records = v2Enabled ? await loadRecords(projectRoot) : [];
66
+ const records = v2Enabled ? await loadRecords(projectRoot, { homeDir }) : [];
67
67
  if (records.length > 0) {
68
68
  const projectIds = new Set(records.map((r) => r.project_id).filter(Boolean));
69
69
  const results = [];
70
70
  for (const projectId of projectIds) {
71
- const pending = await listPendingPatternCandidates(projectRoot, projectId);
71
+ const pending = await listPendingPatternCandidates(projectRoot, projectId, { homeDir });
72
72
  for (const candidate of pending) {
73
73
  results.push({ ...candidate, projectId });
74
74
  }
@@ -87,7 +87,7 @@ async function listAllPendingPatternCandidates(projectRoot, runtimePaths) {
87
87
  for (const entry of entries) {
88
88
  if (!entry.isFile() || !entry.name.endsWith('.json')) continue;
89
89
  const projectId = entry.name.replace(/\.json$/, '');
90
- const pending = await listPendingPatternCandidates(projectRoot, projectId);
90
+ const pending = await listPendingPatternCandidates(projectRoot, projectId, { homeDir });
91
91
  for (const candidate of pending) {
92
92
  results.push({ ...candidate, projectId });
93
93
  }
@@ -128,7 +128,7 @@ function handleMutationResult(res, { notFoundMessage }) {
128
128
 
129
129
  // ---- `ukit memory v2` ops (SPEC §11) ----
130
130
 
131
- async function runMemoryV2(projectRoot, args) {
131
+ async function runMemoryV2(projectRoot, homeDir, args) {
132
132
  const [opRaw, ...rest] = args;
133
133
  const op = (opRaw ?? 'list').toLowerCase();
134
134
 
@@ -140,6 +140,7 @@ async function runMemoryV2(projectRoot, args) {
140
140
  type: typeFlag.value ?? undefined,
141
141
  scope: scopeFlag.value ?? undefined,
142
142
  status: statusFlag.value ?? undefined,
143
+ homeDir,
143
144
  });
144
145
  if (records.length === 0) {
145
146
  console.log('[UKit] No memory v2 records found.');
@@ -157,7 +158,7 @@ async function runMemoryV2(projectRoot, args) {
157
158
  if (!id) {
158
159
  throw new Error('Missing record id. Usage: ukit memory v2 show <id>');
159
160
  }
160
- const record = await getRecord(projectRoot, id);
161
+ const record = await getRecord(projectRoot, id, { homeDir });
161
162
  if (!record) {
162
163
  throw new Error(`Memory v2 record not found: ${id}`);
163
164
  }
@@ -170,7 +171,7 @@ async function runMemoryV2(projectRoot, args) {
170
171
  if (!id) {
171
172
  throw new Error('Missing record id. Usage: ukit memory v2 promote <id>');
172
173
  }
173
- const record = await getRecord(projectRoot, id);
174
+ const record = await getRecord(projectRoot, id, { homeDir });
174
175
  if (!record) {
175
176
  throw new Error(`Memory v2 record not found: ${id}`);
176
177
  }
@@ -186,7 +187,7 @@ async function runMemoryV2(projectRoot, args) {
186
187
  // through mutateMemory with expectedRevision so a concurrent writer
187
188
  // surfaces as a conflict instead of a silent clobber (FR-023).
188
189
  const res = await mutateMemory(
189
- { kind: 'project', projectRoot },
190
+ { kind: 'project', projectRoot, homeDir },
190
191
  {
191
192
  op: 'update',
192
193
  payload: {
@@ -216,12 +217,12 @@ async function runMemoryV2(projectRoot, args) {
216
217
  if (!id) {
217
218
  throw new Error('Missing record id. Usage: ukit memory v2 stale <id>');
218
219
  }
219
- const record = await getRecord(projectRoot, id);
220
+ const record = await getRecord(projectRoot, id, { homeDir });
220
221
  if (!record) {
221
222
  throw new Error(`Memory v2 record not found: ${id}`);
222
223
  }
223
224
  const res = await mutateMemory(
224
- { kind: 'project', projectRoot },
225
+ { kind: 'project', projectRoot, homeDir },
225
226
  {
226
227
  op: 'update',
227
228
  payload: { id, patch: { status: 'stale' } },
@@ -238,7 +239,7 @@ async function runMemoryV2(projectRoot, args) {
238
239
 
239
240
  if (op === 'migrate') {
240
241
  const dryRun = rest.includes('--dry-run');
241
- const result = await runMigration(projectRoot, { dryRun });
242
+ const result = await runMigration(projectRoot, { dryRun, homeDir });
242
243
  if (dryRun) {
243
244
  // Per-record plan: counts per action + ids only — never record bodies.
244
245
  const counts = {};
@@ -259,7 +260,7 @@ async function runMemoryV2(projectRoot, args) {
259
260
  }
260
261
  console.log(`[UKit] Migrated ${result.migrated} record(s) to v2.`);
261
262
  console.log(`[UKit] Backup: ${result.backupDir ?? 'none'} — marker: ${result.markerPath}`);
262
- const stats = await v2Stats(projectRoot);
263
+ const stats = await v2Stats(projectRoot, { homeDir });
263
264
  console.log(`[UKit] v2 store: ${stats.total} record(s) — ${JSON.stringify(stats.byType)}`);
264
265
  return;
265
266
  }
@@ -436,8 +437,8 @@ const MEM_REF_RE = /<!--\s*mem:(mem_[0-9a-f]{12})\s*-->/g;
436
437
  // Approved promotable records: active project_rule/procedure belonging to the
437
438
  // project, not bulk-migrated v1 leftovers (created_by 'migration'), and not
438
439
  // still-pending candidates (meta.legacyStatus 'pending').
439
- async function collectPromotableRecords(projectRoot, projectId) {
440
- const records = await loadRecords(projectRoot);
440
+ async function collectPromotableRecords(projectRoot, projectId, { homeDir } = {}) {
441
+ const records = await loadRecords(projectRoot, { homeDir });
441
442
  return records.filter((r) =>
442
443
  (r.type === 'project_rule' || r.type === 'procedure')
443
444
  && r.status === 'active'
@@ -478,9 +479,9 @@ async function writeTextAtomic(filePath, text) {
478
479
  await fs.rename(tmp, filePath);
479
480
  }
480
481
 
481
- async function runMemoryPromote(projectRoot, args) {
482
+ async function runMemoryPromote(projectRoot, homeDir, args) {
482
483
  const dryRun = args.includes('--dry-run');
483
- const projectId = (await detectProjectContext(projectRoot)).project.name;
484
+ const projectId = (await detectProjectContext(projectRoot, { homeDir })).project.name;
484
485
  const memoryMdPath = path.join(projectRoot, 'docs', 'MEMORY.md');
485
486
 
486
487
  let existing = '';
@@ -491,7 +492,7 @@ async function runMemoryPromote(projectRoot, args) {
491
492
  }
492
493
 
493
494
  const outsideIds = externallyReferencedIds(existing);
494
- const promotable = (await collectPromotableRecords(projectRoot, projectId))
495
+ const promotable = (await collectPromotableRecords(projectRoot, projectId, { homeDir }))
495
496
  .filter((r) => !outsideIds.has(r.id));
496
497
 
497
498
  const block = renderLearnedBlock(promotable);
@@ -567,11 +568,11 @@ function episodeText(ledger, sessionId) {
567
568
  return text.slice(0, 300);
568
569
  }
569
570
 
570
- async function runMemoryEpisode(projectRoot, args) {
571
+ async function runMemoryEpisode(projectRoot, homeDir, args) {
571
572
  const dryRun = args.includes('--dry-run');
572
573
  const sessionFlag = extractFlag(args, '--session').value;
573
574
 
574
- const { config } = await inspectRuntimeConfig(projectRoot);
575
+ const { config } = await inspectRuntimeConfig(projectRoot, { homeDir });
575
576
  if (config?.memoryV2?.enabled === false) {
576
577
  console.log('[UKit] episode: skipped (memoryV2 disabled)');
577
578
  return;
@@ -592,19 +593,19 @@ async function runMemoryEpisode(projectRoot, args) {
592
593
  return;
593
594
  }
594
595
 
595
- const existing = await loadRecords(projectRoot);
596
+ const existing = await loadRecords(projectRoot, { homeDir });
596
597
  if (existing.some((r) => r.meta?.ledgerKey === ledgerKey)) {
597
598
  console.log('[UKit] episode: already recorded');
598
599
  return;
599
600
  }
600
601
 
601
602
  const ttlDays = Number(config?.memoryV2?.episodeTtlDays) || 90;
602
- const projectId = (await detectProjectContext(projectRoot)).project.name;
603
+ const projectId = (await detectProjectContext(projectRoot, { homeDir })).project.name;
603
604
  // FR-023: the add goes through mutateMemory with the ledger key as the
604
605
  // idempotency key — a hook-invoked rerun is a journal 'duplicate' even if
605
606
  // the meta.ledgerKey fast-path above is bypassed.
606
607
  const res = await mutateMemory(
607
- { kind: 'project', projectRoot },
608
+ { kind: 'project', projectRoot, homeDir },
608
609
  {
609
610
  op: 'add',
610
611
  idempotencyKey: ledgerKey,
@@ -745,7 +746,7 @@ export async function runMemory({ projectRoot, homeDir = os.homedir(), argv = []
745
746
  }
746
747
 
747
748
  if (subcommand === 'v2') {
748
- await runMemoryV2(projectRoot, rest);
749
+ await runMemoryV2(projectRoot, homeDir, rest);
749
750
  return;
750
751
  }
751
752
 
@@ -763,7 +764,7 @@ export async function runMemory({ projectRoot, homeDir = os.homedir(), argv = []
763
764
  return;
764
765
  }
765
766
 
766
- const items = await listMemoryItems(projectRoot);
767
+ const items = await listMemoryItems(projectRoot, { homeDir });
767
768
  if (items.length === 0) {
768
769
  console.log('[UKit] No memory items found.');
769
770
  return;
@@ -783,12 +784,12 @@ export async function runMemory({ projectRoot, homeDir = os.homedir(), argv = []
783
784
  throw new Error('Missing pattern text. Usage: ukit memory propose "<text>" [--category <name>] [--project <id>]');
784
785
  }
785
786
 
786
- const projectId = projectFlag.value ?? (await detectProjectContext(projectRoot)).project.name;
787
+ const projectId = projectFlag.value ?? (await detectProjectContext(projectRoot, { homeDir })).project.name;
787
788
  const candidate = await proposePatternCandidate(projectRoot, projectId, {
788
789
  text,
789
790
  category: categoryFlag.value,
790
791
  detectedFrom: 'cli',
791
- });
792
+ }, { homeDir });
792
793
 
793
794
  console.log(`[UKit] Proposed ${candidate.id} (${candidate.status}) for project ${projectId}.`);
794
795
  console.log('[UKit] Review with `ukit memory list --pending`, then `ukit memory approve <id>`.');
@@ -809,7 +810,7 @@ export async function runMemory({ projectRoot, homeDir = os.homedir(), argv = []
809
810
  if (minFlag.value != null && (!Number.isFinite(minCount) || minCount < 1)) {
810
811
  throw new Error(`Invalid --min value: ${minFlag.value}`);
811
812
  }
812
- const projectId = projectFlag.value ?? (await detectProjectContext(projectRoot)).project.name;
813
+ const projectId = projectFlag.value ?? (await detectProjectContext(projectRoot, { homeDir })).project.name;
813
814
 
814
815
  const { proposeFromPatterns } = await import('../../learning/patternProposals.js');
815
816
  const result = await proposeFromPatterns(projectRoot, projectId, {
@@ -853,12 +854,13 @@ export async function runMemory({ projectRoot, homeDir = os.homedir(), argv = []
853
854
  throw new Error(`Missing candidate id. Usage: ukit memory ${subcommand} <candidateId> [--project <id>]`);
854
855
  }
855
856
 
856
- const projectId = explicitProjectId ?? (await detectProjectContext(projectRoot)).project.name;
857
+ const projectId = explicitProjectId ?? (await detectProjectContext(projectRoot, { homeDir })).project.name;
857
858
  const resolved = await resolvePatternCandidate(
858
859
  projectRoot,
859
860
  projectId,
860
861
  candidateId,
861
862
  subcommand === 'approve' ? 'approve' : 'reject',
863
+ { homeDir },
862
864
  );
863
865
 
864
866
  console.log(`[UKit] ${resolved.status === 'approved' ? 'Approved' : 'Rejected'} ${candidateId}.`);
@@ -868,7 +870,7 @@ export async function runMemory({ projectRoot, homeDir = os.homedir(), argv = []
868
870
  if (subcommand === 'hygiene') {
869
871
  const projectFlagIndex = rest.indexOf('--project');
870
872
  const explicitProjectId = projectFlagIndex >= 0 ? rest[projectFlagIndex + 1] : null;
871
- const projectId = explicitProjectId ?? (await detectProjectContext(projectRoot)).project.name;
873
+ const projectId = explicitProjectId ?? (await detectProjectContext(projectRoot, { homeDir })).project.name;
872
874
 
873
875
  const result = await runProjectHygiene(projectRoot, projectId);
874
876
  console.log(`[UKit] Ran hygiene for project ${projectId}.`);
@@ -890,6 +892,7 @@ export async function runMemory({ projectRoot, homeDir = os.homedir(), argv = []
890
892
  // silently widening to cross-repo results.
891
893
  projectId: projectContext.project.id ?? null,
892
894
  projectAliases: projectContext.project.aliases ?? [],
895
+ homeDir,
893
896
  });
894
897
  if (results.length === 0) {
895
898
  console.log('[UKit] No memory matches found.');
@@ -923,6 +926,7 @@ export async function runMemory({ projectRoot, homeDir = os.homedir(), argv = []
923
926
  projectRoot,
924
927
  projectId: projectContext.project.id ?? null,
925
928
  projectAliases: projectContext.project.aliases ?? [],
929
+ homeDir,
926
930
  });
927
931
  // FR-007: the hit block carries citation/freshness/reasons — provenance
928
932
  // surface only, never record meta. Legacy lane has no hit; its
@@ -942,7 +946,7 @@ export async function runMemory({ projectRoot, homeDir = os.homedir(), argv = []
942
946
  throw new Error('Missing task/query. Usage: ukit memory recall <task>');
943
947
  }
944
948
 
945
- const runtimeConfigInspection = await inspectRuntimeConfig(projectRoot);
949
+ const runtimeConfigInspection = await inspectRuntimeConfig(projectRoot, { homeDir });
946
950
  if (!runtimeConfigInspection.valid) {
947
951
  throw new Error('Runtime config is invalid. Run `ukit doctor` or rerun `ukit install`.');
948
952
  }
@@ -959,6 +963,7 @@ export async function runMemory({ projectRoot, homeDir = os.homedir(), argv = []
959
963
  maxTokens: runtimeConfig.memory.maxInjectionTokens,
960
964
  promptCache: Boolean(runtimeConfig.tokenPipeline?.promptCache),
961
965
  inputCompression: Boolean(runtimeConfig.tokenPipeline?.inputCompression),
966
+ homeDir,
962
967
  });
963
968
 
964
969
  if (!injection) {
@@ -971,12 +976,12 @@ export async function runMemory({ projectRoot, homeDir = os.homedir(), argv = []
971
976
  }
972
977
 
973
978
  if (subcommand === 'promote') {
974
- await runMemoryPromote(projectRoot, rest);
979
+ await runMemoryPromote(projectRoot, homeDir, rest);
975
980
  return;
976
981
  }
977
982
 
978
983
  if (subcommand === 'episode') {
979
- await runMemoryEpisode(projectRoot, rest);
984
+ await runMemoryEpisode(projectRoot, homeDir, rest);
980
985
  return;
981
986
  }
982
987
 
@@ -986,7 +991,7 @@ export async function runMemory({ projectRoot, homeDir = os.homedir(), argv = []
986
991
  throw new Error('Missing memory id. Usage: ukit memory forget <id>');
987
992
  }
988
993
 
989
- const result = await forgetMemoryItem(projectRoot, memoryId);
994
+ const result = await forgetMemoryItem(projectRoot, memoryId, { homeDir });
990
995
  if (!result.removed) {
991
996
  throw new Error(`Memory item not found: ${memoryId}`);
992
997
  }
@@ -1003,7 +1008,7 @@ export async function runMemory({ projectRoot, homeDir = os.homedir(), argv = []
1003
1008
  // FR-023/§9: purge removes the record, writes a tombstone, then sweeps
1004
1009
  // prompt-cache entries that embedded the purged record's text.
1005
1010
  const res = await mutateMemory(
1006
- { kind: 'project', projectRoot },
1011
+ { kind: 'project', projectRoot, homeDir },
1007
1012
  { op: 'purge', payload: { id: memoryId } },
1008
1013
  );
1009
1014
  const handled = handleMutationResult(res, {
@@ -1016,10 +1021,10 @@ export async function runMemory({ projectRoot, homeDir = os.homedir(), argv = []
1016
1021
  }
1017
1022
 
1018
1023
  if (subcommand === 'export') {
1019
- const data = await exportMemory(projectRoot);
1024
+ const data = await exportMemory(projectRoot, { homeDir });
1020
1025
  // FR-023/025: defense-in-depth redaction at the CLI boundary too —
1021
1026
  // store.js exportMemory already redacts, this catches any future lane.
1022
- const { config } = await inspectRuntimeConfig(projectRoot);
1027
+ const { config } = await inspectRuntimeConfig(projectRoot, { homeDir });
1023
1028
  const { payload: safe } = redactWritePayload({ meta: data }, { config });
1024
1029
  console.log(JSON.stringify(safe.meta, null, 2));
1025
1030
  return;