@ngockhoale/ukit 2.7.12 → 2.8.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 (46) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/manifests/documentation.yaml +11 -0
  3. package/manifests/platform.full.yaml +182 -0
  4. package/manifests/platform.user.yaml +53 -0
  5. package/package.json +3 -1
  6. package/src/cli/commands/diff.js +4 -2
  7. package/src/cli/commands/doctor.js +22 -1
  8. package/src/cli/commands/install.js +10 -0
  9. package/src/cli/commands/memory.js +142 -3
  10. package/src/cli/commands/playbook.js +53 -0
  11. package/src/cli/index.js +7 -0
  12. package/src/core/memory/recordStore.js +81 -0
  13. package/src/core/memory/storeV2.js +16 -52
  14. package/src/core/memory/userMemory.js +111 -0
  15. package/src/core/paths.js +1 -0
  16. package/src/core/runInstallPipeline.js +96 -3
  17. package/src/core/runtimeConfig.js +170 -5
  18. package/src/core/userPaths.js +21 -0
  19. package/src/core/userPlaybooks.js +185 -0
  20. package/src/index/taskRouting.js +422 -21
  21. package/src/index/verificationPlan.js +17 -0
  22. package/src/manifest/validateManifest.js +19 -0
  23. package/templates/.claude/config/providers.md +1 -3
  24. package/templates/.claude/skills/principle-attack-the-premise/SKILL.md +16 -0
  25. package/templates/.claude/skills/principle-boundary-discipline/SKILL.md +16 -0
  26. package/templates/.claude/skills/principle-encode-lessons-in-structure/SKILL.md +16 -0
  27. package/templates/.claude/skills/principle-fix-root-causes/SKILL.md +18 -0
  28. package/templates/.claude/skills/principle-foundational-thinking/SKILL.md +17 -0
  29. package/templates/.claude/skills/principle-guard-the-context-window/SKILL.md +16 -0
  30. package/templates/.claude/skills/principle-laziness-protocol/SKILL.md +17 -0
  31. package/templates/.claude/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +16 -0
  32. package/templates/.claude/skills/principle-minimize-reader-load/SKILL.md +17 -0
  33. package/templates/.claude/skills/principle-model-the-domain/SKILL.md +16 -0
  34. package/templates/.claude/skills/principle-never-block-on-the-human/SKILL.md +16 -0
  35. package/templates/.claude/skills/principle-prove-it-works/SKILL.md +18 -0
  36. package/templates/.claude/skills/principle-sequence-verifiable-units/SKILL.md +16 -0
  37. package/templates/.claude/skills/principle-subtract-before-you-add/SKILL.md +16 -0
  38. package/templates/.claude/skills/principle-test-behavior-not-implementation/SKILL.md +18 -0
  39. package/templates/.claude/ukit/index/route-task.mjs +652 -28
  40. package/templates/.claude/ukit/runtime/execution-ledger.mjs +238 -9
  41. package/templates/ukit/README.md +31 -0
  42. package/templates/ukit/storage/config.json +10 -0
  43. package/templates/user/README.md +21 -0
  44. package/templates/user/playbooks/bug-fix.md +18 -0
  45. package/templates/user/playbooks/issue-implementation.md +14 -0
  46. package/templates/user/storage/config.json +16 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,19 @@
3
3
  All notable changes to UKit are documented here.
4
4
 
5
5
 
6
+ ## 2.8.0 - 2026-09-23
7
+
8
+ - **User-Level `.ukit` layer (cycle C46)**: new `~/.ukit/` layer resolved via `os.homedir()` (correct on Windows/Linux/macOS), immutable on `ukit update` (`level: "user"` manifests + `mergeStrategy: skip`).
9
+ - Canonical API: `src/core/userPaths.js`, `src/core/memory/{recordStore,userMemory}.js`, `src/core/userPlaybooks.js`.
10
+ - Merge-per-key precedence: project wins config keys, user wins personal data; `loadRuntimeConfig(projectRoot, {homeDir})` deep-merges defaults←user←project; invalid user-only config warns and falls back to defaults.
11
+ - Install seeds the user layer (`manifests/platform.user.yaml` + `templates/user/`); router + `route-task.mjs` mirror are user-layer aware; new `ukit playbook` CLI; `ukit memory --user`; doctor user-layer section.
12
+ - **v3 pstack-derived specs**: playbook policies, principle index (15 leaf skills), `modelRoles`, typed verdicts + patch-id currency, decision log (`docs/pstack/SPEC-*.md`, `V3_RESHAPE.md`).
13
+ - **M01.1**: additive `ResolvedTaskRoute` v1 behind `routing.routeSchema.stage` (default `off` = byte-identical legacy route).
14
+
15
+ ## 2.7.13 - 2026-09-22
16
+
17
+ - Same content as 2.7.12 (npm staged-publish never completed; re-published as 2.7.13).
18
+
6
19
  ## 2.7.12 - 2026-09-22
7
20
 
8
21
  - **Token & latency diet (cycle C45)**:
@@ -330,6 +330,17 @@ entries:
330
330
  validation: [manual]
331
331
  archive_policy: never
332
332
  notes: dir entry — covers all files beneath (handoff lane reports, perf findings)
333
+ - id: docs-pstack
334
+ path: docs/pstack/
335
+ class: canonical
336
+ audience: [agent, maintainer]
337
+ owner: product
338
+ source_of_truth: docs/pstack/
339
+ merge_strategy: none
340
+ load_policy: task-routed
341
+ validation: [manual]
342
+ archive_policy: never
343
+ notes: dir entry — pstack→UKit v3 integration corpus, specs, and roadmaps
333
344
  - id: docs-archive
334
345
  path: docs/archive/
335
346
  class: archive
@@ -845,6 +845,188 @@ items:
845
845
  packs:
846
846
  - core
847
847
 
848
+ # SPEC-principle-index §2.2: 15 principle leaf skills — reference material only
849
+ # (disable-model-invocation), lazy-loaded by name from the emitted principle index.
850
+ - id: principle-laziness-protocol-skill
851
+ type: skill
852
+ sourceTemplate: .claude/skills/principle-laziness-protocol/SKILL.md
853
+ targetPath: .claude/skills/principle-laziness-protocol/SKILL.md
854
+ requires:
855
+ - core-skill-delivery
856
+ mergeStrategy: overwrite_with_backup
857
+ variables: []
858
+ enabledByDefault: true
859
+ packs:
860
+ - core
861
+
862
+ - id: principle-foundational-thinking-skill
863
+ type: skill
864
+ sourceTemplate: .claude/skills/principle-foundational-thinking/SKILL.md
865
+ targetPath: .claude/skills/principle-foundational-thinking/SKILL.md
866
+ requires:
867
+ - core-skill-delivery
868
+ mergeStrategy: overwrite_with_backup
869
+ variables: []
870
+ enabledByDefault: true
871
+ packs:
872
+ - core
873
+
874
+ - id: principle-subtract-before-you-add-skill
875
+ type: skill
876
+ sourceTemplate: .claude/skills/principle-subtract-before-you-add/SKILL.md
877
+ targetPath: .claude/skills/principle-subtract-before-you-add/SKILL.md
878
+ requires:
879
+ - core-skill-delivery
880
+ mergeStrategy: overwrite_with_backup
881
+ variables: []
882
+ enabledByDefault: true
883
+ packs:
884
+ - core
885
+
886
+ - id: principle-minimize-reader-load-skill
887
+ type: skill
888
+ sourceTemplate: .claude/skills/principle-minimize-reader-load/SKILL.md
889
+ targetPath: .claude/skills/principle-minimize-reader-load/SKILL.md
890
+ requires:
891
+ - core-skill-delivery
892
+ mergeStrategy: overwrite_with_backup
893
+ variables: []
894
+ enabledByDefault: true
895
+ packs:
896
+ - core
897
+
898
+ - id: principle-attack-the-premise-skill
899
+ type: skill
900
+ sourceTemplate: .claude/skills/principle-attack-the-premise/SKILL.md
901
+ targetPath: .claude/skills/principle-attack-the-premise/SKILL.md
902
+ requires:
903
+ - core-skill-delivery
904
+ mergeStrategy: overwrite_with_backup
905
+ variables: []
906
+ enabledByDefault: true
907
+ packs:
908
+ - core
909
+
910
+ - id: principle-model-the-domain-skill
911
+ type: skill
912
+ sourceTemplate: .claude/skills/principle-model-the-domain/SKILL.md
913
+ targetPath: .claude/skills/principle-model-the-domain/SKILL.md
914
+ requires:
915
+ - core-skill-delivery
916
+ mergeStrategy: overwrite_with_backup
917
+ variables: []
918
+ enabledByDefault: true
919
+ packs:
920
+ - core
921
+
922
+ - id: principle-boundary-discipline-skill
923
+ type: skill
924
+ sourceTemplate: .claude/skills/principle-boundary-discipline/SKILL.md
925
+ targetPath: .claude/skills/principle-boundary-discipline/SKILL.md
926
+ requires:
927
+ - core-skill-delivery
928
+ mergeStrategy: overwrite_with_backup
929
+ variables: []
930
+ enabledByDefault: true
931
+ packs:
932
+ - core
933
+
934
+ - id: principle-migrate-callers-then-delete-legacy-apis-skill
935
+ type: skill
936
+ sourceTemplate: .claude/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md
937
+ targetPath: .claude/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md
938
+ requires:
939
+ - core-skill-delivery
940
+ mergeStrategy: overwrite_with_backup
941
+ variables: []
942
+ enabledByDefault: true
943
+ packs:
944
+ - core
945
+
946
+ - id: principle-prove-it-works-skill
947
+ type: skill
948
+ sourceTemplate: .claude/skills/principle-prove-it-works/SKILL.md
949
+ targetPath: .claude/skills/principle-prove-it-works/SKILL.md
950
+ requires:
951
+ - core-skill-delivery
952
+ mergeStrategy: overwrite_with_backup
953
+ variables: []
954
+ enabledByDefault: true
955
+ packs:
956
+ - core
957
+
958
+ - id: principle-fix-root-causes-skill
959
+ type: skill
960
+ sourceTemplate: .claude/skills/principle-fix-root-causes/SKILL.md
961
+ targetPath: .claude/skills/principle-fix-root-causes/SKILL.md
962
+ requires:
963
+ - core-skill-delivery
964
+ mergeStrategy: overwrite_with_backup
965
+ variables: []
966
+ enabledByDefault: true
967
+ packs:
968
+ - core
969
+
970
+ - id: principle-sequence-verifiable-units-skill
971
+ type: skill
972
+ sourceTemplate: .claude/skills/principle-sequence-verifiable-units/SKILL.md
973
+ targetPath: .claude/skills/principle-sequence-verifiable-units/SKILL.md
974
+ requires:
975
+ - core-skill-delivery
976
+ mergeStrategy: overwrite_with_backup
977
+ variables: []
978
+ enabledByDefault: true
979
+ packs:
980
+ - core
981
+
982
+ - id: principle-test-behavior-not-implementation-skill
983
+ type: skill
984
+ sourceTemplate: .claude/skills/principle-test-behavior-not-implementation/SKILL.md
985
+ targetPath: .claude/skills/principle-test-behavior-not-implementation/SKILL.md
986
+ requires:
987
+ - core-skill-delivery
988
+ mergeStrategy: overwrite_with_backup
989
+ variables: []
990
+ enabledByDefault: true
991
+ packs:
992
+ - core
993
+
994
+ - id: principle-guard-the-context-window-skill
995
+ type: skill
996
+ sourceTemplate: .claude/skills/principle-guard-the-context-window/SKILL.md
997
+ targetPath: .claude/skills/principle-guard-the-context-window/SKILL.md
998
+ requires:
999
+ - core-skill-delivery
1000
+ mergeStrategy: overwrite_with_backup
1001
+ variables: []
1002
+ enabledByDefault: true
1003
+ packs:
1004
+ - core
1005
+
1006
+ - id: principle-never-block-on-the-human-skill
1007
+ type: skill
1008
+ sourceTemplate: .claude/skills/principle-never-block-on-the-human/SKILL.md
1009
+ targetPath: .claude/skills/principle-never-block-on-the-human/SKILL.md
1010
+ requires:
1011
+ - core-skill-delivery
1012
+ mergeStrategy: overwrite_with_backup
1013
+ variables: []
1014
+ enabledByDefault: true
1015
+ packs:
1016
+ - core
1017
+
1018
+ - id: principle-encode-lessons-in-structure-skill
1019
+ type: skill
1020
+ sourceTemplate: .claude/skills/principle-encode-lessons-in-structure/SKILL.md
1021
+ targetPath: .claude/skills/principle-encode-lessons-in-structure/SKILL.md
1022
+ requires:
1023
+ - core-skill-delivery
1024
+ mergeStrategy: overwrite_with_backup
1025
+ variables: []
1026
+ enabledByDefault: true
1027
+ packs:
1028
+ - core
1029
+
848
1030
  - id: agent-handoff-planner
849
1031
  type: agent
850
1032
  sourceTemplate: .claude/agents/handoff-planner.md
@@ -0,0 +1,53 @@
1
+ version: 1
2
+ name: ukit-user-layer
3
+ level: user
4
+ autoDiscoverTemplates: false
5
+ description: >
6
+ UKit user-level manifest. Seeds ~/.ukit/ (resolved via os.homedir()) with personal
7
+ defaults: ownership README, minimal config, and playbook seeds. Every item is
8
+ mergeStrategy: skip — seed-once semantics; UKit never overwrites user files.
9
+
10
+ items:
11
+ - id: user-readme
12
+ type: config
13
+ sourceTemplate: user/README.md
14
+ targetPath: README.md
15
+ requires: []
16
+ mergeStrategy: skip
17
+ variables: []
18
+ enabledByDefault: true
19
+ packs:
20
+ - core
21
+
22
+ - id: user-config
23
+ type: config
24
+ sourceTemplate: user/storage/config.json
25
+ targetPath: storage/config.json
26
+ requires: []
27
+ mergeStrategy: skip
28
+ variables: []
29
+ enabledByDefault: true
30
+ packs:
31
+ - core
32
+
33
+ - id: user-playbook-bugfix
34
+ type: config
35
+ sourceTemplate: user/playbooks/bug-fix.md
36
+ targetPath: playbooks/bug-fix.md
37
+ requires: []
38
+ mergeStrategy: skip
39
+ variables: []
40
+ enabledByDefault: true
41
+ packs:
42
+ - core
43
+
44
+ - id: user-playbook-issue
45
+ type: config
46
+ sourceTemplate: user/playbooks/issue-implementation.md
47
+ targetPath: playbooks/issue-implementation.md
48
+ requires: []
49
+ mergeStrategy: skip
50
+ variables: []
51
+ enabledByDefault: true
52
+ packs:
53
+ - core
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngockhoale/ukit",
3
- "version": "2.7.12",
3
+ "version": "2.8.0",
4
4
  "description": "Install/update an index-first AI workspace for Claude Code, OpenAI Codex and omp (Oh My Pi).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -31,6 +31,8 @@
31
31
  "src/",
32
32
  "scripts/",
33
33
  "templates/",
34
+ "!templates/**/.smbdelete*",
35
+ "!templates/.smbdelete*",
34
36
  "CHANGELOG.md",
35
37
  "LICENSE",
36
38
  "templates/CLAUDE.md",
@@ -36,8 +36,10 @@ export async function runDiff({ packageRoot, projectRoot, packageVersion, argv =
36
36
  console.log(`[UKit] Detected packs: ${result.stackContext.detectedPacks.join(', ')}`);
37
37
 
38
38
  for (const row of result.rows) {
39
- const relPath = path.relative(projectRoot, row.targetPath);
40
- console.log(`- [${row.action}] ${row.id} -> ${relPath}`);
39
+ // User-layer rows live under ~/.ukit — outside projectRoot — so a relative
40
+ // path would be a noisy ../../… chain; print the absolute target instead.
41
+ const displayPath = row.layer === 'user' ? row.targetPath : path.relative(projectRoot, row.targetPath);
42
+ console.log(`- [${row.action}] ${row.id} -> ${displayPath}`);
41
43
  }
42
44
 
43
45
  console.log(
@@ -27,6 +27,9 @@ import { inspectUnattendedMode } from '../../core/unattendedDoctor.js';
27
27
  import { inspectPermissions } from '../../core/permissionDoctor.js';
28
28
  import { inspectHookChainHealth } from '../../core/hookChainDoctor.js';
29
29
  import { findOpencodeArtifacts, opencodeSteerMessage } from '../../core/opencodeSteer.js';
30
+ import { buildUserPaths } from '../../core/userPaths.js';
31
+ import { loadPlaybooks } from '../../core/userPlaybooks.js';
32
+ import { userMemoryStats } from '../../core/memory/userMemory.js';
30
33
 
31
34
  export const DOCTOR_HELP_FLAGS = new Set(['--help', '-h']);
32
35
  const KNOWN_FLAGS = new Set([...DOCTOR_HELP_FLAGS, '--skills', '--gateway', '--docs', '--permissions', '--json']);
@@ -94,7 +97,7 @@ export async function runDoctor({ packageRoot, projectRoot, argv = [], homeDir =
94
97
  const stack = await detectStack(projectRoot);
95
98
  const providers = await detectProviders(projectRoot);
96
99
  const installMeta = await readJsonIfExists(pathConfig.installMetaPath);
97
- const runtimeConfigInspection = await inspectRuntimeConfig(projectRoot);
100
+ const runtimeConfigInspection = await inspectRuntimeConfig(projectRoot, { homeDir });
98
101
 
99
102
  const trackedPaths = Array.isArray(installMeta?.files)
100
103
  ? installMeta.files
@@ -288,6 +291,24 @@ export async function runDoctor({ packageRoot, projectRoot, argv = [], homeDir =
288
291
  }
289
292
  console.log(`[UKit] ${ok(checks.allProvidersConfigured)} All providers configured`);
290
293
 
294
+ // FR-011 — user-layer health (~/.ukit). Advisory only: a missing or broken
295
+ // user layer never fails the project report.
296
+ const userPaths = buildUserPaths({ homeDir });
297
+ if (!(await pathExists(userPaths.userRoot))) {
298
+ console.log('[UKit] User layer: not seeded (run ukit install)');
299
+ } else {
300
+ const userConfigState = !runtimeConfigInspection.user.exists
301
+ ? 'absent'
302
+ : runtimeConfigInspection.user.parseError
303
+ ? 'invalid'
304
+ : 'ok';
305
+ const userPlaybooks = await loadPlaybooks(userPaths.playbooksDir);
306
+ const userStats = await userMemoryStats({ homeDir });
307
+ console.log(
308
+ `[UKit] User layer: present (${userPaths.userRoot}) | config: ${userConfigState} | playbooks: ${userPlaybooks.size} | memory records: ${userStats.total}`,
309
+ );
310
+ }
311
+
291
312
  console.log('[UKit] Project rules checks:');
292
313
  for (const check of projectChecks) {
293
314
  if (check.applicable === false) continue;
@@ -284,6 +284,16 @@ export async function runInstall({ packageRoot, projectRoot, packageVersion, arg
284
284
  );
285
285
  }
286
286
 
287
+ if (result.userLayer) {
288
+ if (result.userLayer.error) {
289
+ console.warn(`[UKit] Warning: user-layer seeding skipped — ${result.userLayer.error}`);
290
+ } else {
291
+ console.log(
292
+ `[UKit] User layer: seeded=${result.userLayer.seeded} kept=${result.userLayer.kept} (${result.userLayer.userRoot})`,
293
+ );
294
+ }
295
+ }
296
+
287
297
  const providerNames = Object.keys(result.providerContext.providers);
288
298
  const providerStatus = providerNames
289
299
  .map((name) => `${name}=${result.providerContext.providers[name]?.supported ?? 'unknown'}`)
@@ -15,6 +15,15 @@ import {
15
15
  stats as v2Stats,
16
16
  updateRecord,
17
17
  } from '../../core/memory/storeV2.js';
18
+ import {
19
+ addUserRecord,
20
+ getUserRecord,
21
+ loadUserRecords,
22
+ queryUserRecords,
23
+ saveUserRecords,
24
+ updateUserRecord,
25
+ userMemoryStats,
26
+ } from '../../core/memory/userMemory.js';
18
27
  import { runMigration } from '../../core/memory/migrate.js';
19
28
  import { getContextInjection, search } from '../../core/memory/retrieval.js';
20
29
  import { inspectRuntimeConfig } from '../../core/runtimeConfig.js';
@@ -23,6 +32,7 @@ import { pathExists } from '../../core/fileOps.js';
23
32
  import { detectProjectContext } from '../../context/detectProjectContext.js';
24
33
  import { listLedgerFiles, LEDGER_DIR_REL } from '../../diagnostics/ledgerFiles.js';
25
34
  import fs from 'node:fs/promises';
35
+ import os from 'node:os';
26
36
  import path from 'node:path';
27
37
 
28
38
  const HELP_FLAGS = new Set(['--help', '-h', 'help']);
@@ -180,6 +190,122 @@ async function runMemoryV2(projectRoot, args) {
180
190
  throw new Error(`Unknown memory v2 op: ${op}. Expected list|show|promote|stale|migrate.`);
181
191
  }
182
192
 
193
+ // ---- `ukit memory --user` — user-level store at ~/.ukit (FR-011) ----
194
+ // Same v2 record document, no v1→v2 migration (the user layer starts at v2).
195
+ // Ops: list|add|get|update|forget|stats; an optional `v2` token is accepted so
196
+ // `memory --user v2 add` and `memory --user add` are equivalent. Any other
197
+ // subcommand under --user is a misuse error — v1 lanes stay project-level.
198
+
199
+ const USER_MEMORY_OPS = new Set(['list', 'add', 'get', 'update', 'forget', 'stats']);
200
+
201
+ async function runUserMemory(homeDir, args) {
202
+ const rest = (args[0] ?? '').toLowerCase() === 'v2' ? args.slice(1) : args;
203
+ const op = (rest[0] ?? 'list').toLowerCase();
204
+ const opArgs = rest.slice(1);
205
+
206
+ if (!USER_MEMORY_OPS.has(op)) {
207
+ throw new Error(
208
+ `--user only supports v2 ops (list|add|get|update|forget|stats); got "${op}".`,
209
+ );
210
+ }
211
+
212
+ if (op === 'list') {
213
+ const typeFlag = extractFlag(opArgs, '--type');
214
+ const scopeFlag = extractFlag(typeFlag.rest, '--scope');
215
+ const statusFlag = extractFlag(scopeFlag.rest, '--status');
216
+ const records = await queryUserRecords({
217
+ type: typeFlag.value ?? undefined,
218
+ scope: scopeFlag.value ?? undefined,
219
+ status: statusFlag.value ?? undefined,
220
+ }, { homeDir });
221
+ if (records.length === 0) {
222
+ console.log('[UKit] No user memory records found.');
223
+ return;
224
+ }
225
+ for (const record of records) {
226
+ const scopeLabel = record.project_id ?? record.scope;
227
+ console.log(`${record.id} [${record.type}/${scopeLabel}/${record.status}] — ${record.text}`);
228
+ }
229
+ return;
230
+ }
231
+
232
+ if (op === 'add') {
233
+ const typeFlag = extractFlag(opArgs, '--type');
234
+ const scopeFlag = extractFlag(typeFlag.rest, '--scope');
235
+ const provenanceFlag = extractFlag(scopeFlag.rest, '--provenance');
236
+ const text = provenanceFlag.rest.join(' ').trim();
237
+ if (!text) {
238
+ throw new Error('Missing record text. Usage: ukit memory --user add [--type <type>] [--scope <scope>] <text>');
239
+ }
240
+ const record = await addUserRecord({
241
+ type: typeFlag.value ?? 'derived_fact',
242
+ scope: scopeFlag.value ?? 'user',
243
+ text,
244
+ provenance: provenanceFlag.value ?? 'cli',
245
+ createdBy: 'cli',
246
+ }, { homeDir });
247
+ console.log(`[UKit] Added user record ${record.id}.`);
248
+ return;
249
+ }
250
+
251
+ if (op === 'get') {
252
+ const id = opArgs.join(' ').trim();
253
+ if (!id) {
254
+ throw new Error('Missing record id. Usage: ukit memory --user get <id>');
255
+ }
256
+ const record = await getUserRecord(id, { homeDir });
257
+ if (!record) {
258
+ throw new Error(`User memory record not found: ${id}`);
259
+ }
260
+ console.log(JSON.stringify(record, null, 2));
261
+ return;
262
+ }
263
+
264
+ if (op === 'update') {
265
+ const id = opArgs[0];
266
+ if (!id) {
267
+ throw new Error('Missing record id. Usage: ukit memory --user update <id> [--status <s>] [--type <t>] [--scope <s>] [text]');
268
+ }
269
+ const statusFlag = extractFlag(opArgs.slice(1), '--status');
270
+ const typeFlag = extractFlag(statusFlag.rest, '--type');
271
+ const scopeFlag = extractFlag(typeFlag.rest, '--scope');
272
+ const text = scopeFlag.rest.join(' ').trim();
273
+ const patch = {};
274
+ if (statusFlag.value != null) patch.status = statusFlag.value;
275
+ if (typeFlag.value != null) patch.type = typeFlag.value;
276
+ if (scopeFlag.value != null) patch.scope = scopeFlag.value;
277
+ if (text) patch.text = text;
278
+ if (Object.keys(patch).length === 0) {
279
+ throw new Error('Nothing to update. Pass --status/--type/--scope or new text.');
280
+ }
281
+ const updated = await updateUserRecord(id, patch, { homeDir });
282
+ if (!updated) {
283
+ throw new Error(`User memory record not found: ${id}`);
284
+ }
285
+ console.log(`[UKit] Updated user record ${updated.id}.`);
286
+ return;
287
+ }
288
+
289
+ if (op === 'forget') {
290
+ const id = opArgs.join(' ').trim();
291
+ if (!id) {
292
+ throw new Error('Missing record id. Usage: ukit memory --user forget <id>');
293
+ }
294
+ const records = await loadUserRecords({ homeDir });
295
+ const remaining = records.filter((r) => r.id !== id);
296
+ if (remaining.length === records.length) {
297
+ throw new Error(`User memory record not found: ${id}`);
298
+ }
299
+ await saveUserRecords(remaining, { homeDir });
300
+ console.log(`[UKit] Forgot user record ${id}.`);
301
+ return;
302
+ }
303
+
304
+ // op === 'stats'
305
+ const stats = await userMemoryStats({ homeDir });
306
+ console.log(`[UKit] User memory: total=${stats.total} byType=${JSON.stringify(stats.byType)} byStatus=${JSON.stringify(stats.byStatus)} invalidSkipped=${stats.invalidSkipped}`);
307
+ }
308
+
183
309
  // ---- `ukit memory promote` — MEMORY.md `## ukit-learned` lane (SPEC §7a) ----
184
310
 
185
311
  const LEARNED_START = '<!-- ukit-learned:start -->';
@@ -366,13 +492,19 @@ async function runMemoryEpisode(projectRoot, args) {
366
492
  console.log(`[UKit] episode: recorded ${record.id} for session ${sessionId}`);
367
493
  }
368
494
 
369
- export async function runMemory({ projectRoot, argv = [] }) {
495
+ export async function runMemory({ projectRoot, homeDir = os.homedir(), argv = [] }) {
496
+ const userFlagIndex = argv.indexOf('--user');
497
+ const isUser = userFlagIndex >= 0;
370
498
  const runtimePaths = buildRuntimePaths(projectRoot);
371
- if (!(await pathExists(runtimePaths.runtimeRoot))) {
499
+ // --user operates on ~/.ukit and does not require the project runtime.
500
+ if (!isUser && !(await pathExists(runtimePaths.runtimeRoot))) {
372
501
  throw new Error('Shared UKit runtime not found. Run `ukit install` first.');
373
502
  }
374
503
 
375
- const [subcommandRaw, ...rest] = argv;
504
+ const argvRest = isUser
505
+ ? [...argv.slice(0, userFlagIndex), ...argv.slice(userFlagIndex + 1)]
506
+ : argv;
507
+ const [subcommandRaw, ...rest] = argvRest;
376
508
  const subcommand = (subcommandRaw ?? 'list').toLowerCase();
377
509
 
378
510
  if (HELP_FLAGS.has(subcommand)) {
@@ -380,6 +512,11 @@ export async function runMemory({ projectRoot, argv = [] }) {
380
512
  return;
381
513
  }
382
514
 
515
+ if (isUser) {
516
+ await runUserMemory(homeDir, argvRest);
517
+ return;
518
+ }
519
+
383
520
  if (subcommand === 'v2') {
384
521
  await runMemoryV2(projectRoot, rest);
385
522
  return;
@@ -615,4 +752,6 @@ export function printMemoryHelp() {
615
752
  console.log(' promote [--dry-run] Render approved rules/procedures into the ## ukit-learned block in docs/MEMORY.md');
616
753
  console.log(' episode [--dry-run] [--session <id>] Write a session episode record from the exec-ledger');
617
754
  console.log(' hygiene [--project <id>] Run decision-conflict resolution + session archiving now');
755
+ console.log(' v2 <list|show|promote|stale|migrate> Operate on the project v2 record store');
756
+ console.log(' --user [v2] <list|add|get|update|forget|stats> Operate on the user-level store at ~/.ukit/storage/memory/v2/records.json');
618
757
  }
@@ -0,0 +1,53 @@
1
+ import { listPlaybooks, resolvePlaybook } from '../../core/userPlaybooks.js';
2
+
3
+ const HELP_FLAGS = new Set(['--help', '-h', 'help']);
4
+
5
+ export function printPlaybookHelp() {
6
+ console.log('UKit Playbook Commands');
7
+ console.log('Usage: ukit playbook <list|show> [args]');
8
+ console.log('');
9
+ console.log('Subcommands:');
10
+ console.log(' list List resolved playbooks (id | source | lanes)');
11
+ console.log(' show <id> Print the resolved playbook body and its source level');
12
+ console.log('');
13
+ console.log('Resolution precedence: project (.ukit/playbooks) > user (~/.ukit/playbooks) > builtin.');
14
+ }
15
+
16
+ export async function runPlaybook({ projectRoot, homeDir, argv = [], builtins } = {}) {
17
+ const [subcommandRaw, ...rest] = argv;
18
+ const subcommand = (subcommandRaw ?? 'list').toLowerCase();
19
+
20
+ if (HELP_FLAGS.has(subcommand)) {
21
+ printPlaybookHelp();
22
+ return;
23
+ }
24
+
25
+ if (subcommand === 'list') {
26
+ const rows = await listPlaybooks({ projectRoot, homeDir, builtins });
27
+ if (rows.length === 0) {
28
+ console.log('No playbooks found (built-ins: bug-fix, issue-implementation)');
29
+ return;
30
+ }
31
+ console.log('id | source | lanes');
32
+ for (const row of rows) {
33
+ console.log(`${row.id} | ${row.source} | ${row.lanes.join(', ') || '-'}`);
34
+ }
35
+ return;
36
+ }
37
+
38
+ if (subcommand === 'show') {
39
+ const id = rest[0];
40
+ if (!id) {
41
+ throw new Error('Missing playbook id. Usage: ukit playbook show <id>');
42
+ }
43
+ const playbook = await resolvePlaybook(id, { projectRoot, homeDir, builtins });
44
+ if (playbook === null) {
45
+ throw new Error(`Unknown playbook: ${id}`);
46
+ }
47
+ console.log(`source: ${playbook.source}`);
48
+ console.log(playbook.body);
49
+ return;
50
+ }
51
+
52
+ throw new Error(`Unknown playbook subcommand: ${subcommand}. Run ukit playbook --help for usage.`);
53
+ }
package/src/cli/index.js CHANGED
@@ -9,6 +9,7 @@ import { runUpdate } from './commands/update.js';
9
9
  import { runCode } from './commands/code.js';
10
10
  import { runMetrics } from './commands/metrics.js';
11
11
  import { runFeedback } from './commands/feedback.js';
12
+ import { runPlaybook } from './commands/playbook.js';
12
13
 
13
14
  const GLOBAL_FLAGS = new Set(['--help', '-h', '--version', '-v']);
14
15
 
@@ -67,6 +68,11 @@ export async function runCli({ argv, packageRoot, projectRoot, packageVersion })
67
68
  return;
68
69
  }
69
70
 
71
+ if (command === 'playbook') {
72
+ await runPlaybook({ projectRoot, argv: commandArgv });
73
+ return;
74
+ }
75
+
70
76
  if (command === 'metrics') {
71
77
  await runMetrics({ projectRoot, argv: commandArgv });
72
78
  return;
@@ -110,6 +116,7 @@ export async function runCli({ argv, packageRoot, projectRoot, packageVersion })
110
116
  console.log(' code Context compiler surface (peek/search/context/impact)');
111
117
  console.log(' status Show UKit runtime status');
112
118
  console.log(' memory Inspect shared UKit memory');
119
+ console.log(' playbook List/show playbooks (project > user > builtin)');
113
120
  console.log(' metrics Telemetry roll-up (route outcomes, failure patterns, memory)');
114
121
  console.log(' feedback Record or list wrong-route feedback labels');
115
122
  console.log(' update Upgrade the global UKit CLI to the latest version');