portable-agent-layer 0.35.0 → 0.37.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 (108) hide show
  1. package/README.md +2 -1
  2. package/assets/skills/analyze-pdf/tools/pdf-download.ts +1 -1
  3. package/assets/skills/analyze-youtube/tools/youtube-analyze.ts +1 -1
  4. package/assets/skills/consulting-report/tools/dev.ts +2 -2
  5. package/assets/skills/consulting-report/tools/generate-pdf.ts +9 -9
  6. package/assets/skills/consulting-report/tools/scaffold.ts +2 -2
  7. package/assets/skills/create-pdf/tools/md-to-html-pdf.ts +2 -2
  8. package/assets/skills/opinion/tools/opinion.ts +3 -2
  9. package/assets/skills/presentation/SKILL.md +1 -1
  10. package/assets/skills/presentation/tools/doctor.ts +2 -5
  11. package/assets/skills/presentation/tools/lib/inline.ts +6 -11
  12. package/assets/skills/presentation/tools/lib/lint-helpers.ts +2 -2
  13. package/assets/skills/presentation/tools/lib/lint-rules.ts +5 -2
  14. package/assets/skills/presentation/tools/setup-template.ts +10 -7
  15. package/assets/skills/projects/SKILL.md +44 -21
  16. package/assets/skills/research/tools/gemini-search.ts +2 -2
  17. package/assets/skills/research/tools/grok-search.ts +2 -2
  18. package/assets/skills/research/tools/perplexity-search.ts +2 -2
  19. package/assets/skills/telos/SKILL.md +7 -52
  20. package/assets/skills/telos/tools/update-telos.ts +0 -1
  21. package/assets/templates/PAL/ALGORITHM.md +54 -5
  22. package/assets/templates/PAL/PROJECT_LIFECYCLE.md +48 -0
  23. package/assets/templates/PAL/README.md +1 -1
  24. package/assets/templates/PAL/STEERING_RULES.md +4 -0
  25. package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +32 -17
  26. package/assets/templates/PAL/WORK_TRACKING.md +1 -1
  27. package/assets/templates/hooks.codex.json +44 -0
  28. package/assets/templates/hooks.cursor.json +11 -5
  29. package/assets/templates/pal-settings.json +1 -3
  30. package/assets/templates/settings.claude.json +2 -1
  31. package/package.json +2 -1
  32. package/src/cli/index.ts +112 -14
  33. package/src/cli/migrate.ts +299 -0
  34. package/src/cli/setup-identity.ts +3 -3
  35. package/src/cli/setup-telos.ts +12 -80
  36. package/src/hooks/CompactRecover.ts +11 -5
  37. package/src/hooks/LoadContext.ts +35 -11
  38. package/src/hooks/PreCompactPersist.ts +26 -34
  39. package/src/hooks/SecurityValidator.ts +43 -21
  40. package/src/hooks/StopOrchestrator.ts +4 -1
  41. package/src/hooks/UserPromptOrchestrator.ts +4 -2
  42. package/src/hooks/handlers/auto-graduate.ts +2 -2
  43. package/src/hooks/handlers/backup.ts +3 -3
  44. package/src/hooks/handlers/context-digests.ts +74 -0
  45. package/src/hooks/handlers/failure.ts +5 -3
  46. package/src/hooks/handlers/inject-retrieval.ts +29 -6
  47. package/src/hooks/handlers/persist-last-exchange.ts +76 -0
  48. package/src/hooks/handlers/rating.ts +2 -1
  49. package/src/hooks/handlers/readme-sync.ts +3 -2
  50. package/src/hooks/handlers/session-intelligence.ts +17 -93
  51. package/src/hooks/handlers/session-name.ts +2 -2
  52. package/src/hooks/handlers/synthesis.ts +5 -2
  53. package/src/hooks/handlers/update-counts.ts +3 -2
  54. package/src/hooks/lib/agent.ts +20 -18
  55. package/src/hooks/lib/claude-md.ts +69 -14
  56. package/src/hooks/lib/context.ts +92 -246
  57. package/src/hooks/lib/entities.ts +7 -7
  58. package/src/hooks/lib/frontmatter.ts +4 -4
  59. package/src/hooks/lib/graduation.ts +7 -6
  60. package/src/hooks/lib/inference.ts +6 -2
  61. package/src/hooks/lib/learning-category.ts +1 -1
  62. package/src/hooks/lib/learning-store.ts +6 -1
  63. package/src/hooks/lib/notify.ts +2 -2
  64. package/src/hooks/lib/opinions.ts +3 -3
  65. package/src/hooks/lib/paths.ts +2 -0
  66. package/src/hooks/lib/projects.ts +142 -74
  67. package/src/hooks/lib/readme-sync.ts +1 -1
  68. package/src/hooks/lib/relationship.ts +4 -16
  69. package/src/hooks/lib/retrieval-index.ts +5 -3
  70. package/src/hooks/lib/retrieval.ts +11 -12
  71. package/src/hooks/lib/security.ts +24 -18
  72. package/src/hooks/lib/semi-static.ts +188 -0
  73. package/src/hooks/lib/session-names.ts +1 -1
  74. package/src/hooks/lib/settings.ts +1 -1
  75. package/src/hooks/lib/setup.ts +2 -65
  76. package/src/hooks/lib/signals.ts +2 -2
  77. package/src/hooks/lib/stdin.ts +1 -1
  78. package/src/hooks/lib/stop.ts +16 -6
  79. package/src/hooks/lib/token-usage.ts +1 -2
  80. package/src/hooks/lib/transcript.ts +1 -1
  81. package/src/hooks/lib/wisdom.ts +5 -5
  82. package/src/hooks/lib/work-tracking.ts +8 -14
  83. package/src/targets/claude/uninstall.ts +1 -1
  84. package/src/targets/codex/install.ts +95 -0
  85. package/src/targets/codex/uninstall.ts +70 -0
  86. package/src/targets/copilot/install.ts +39 -8
  87. package/src/targets/copilot/uninstall.ts +58 -17
  88. package/src/targets/cursor/install.ts +8 -0
  89. package/src/targets/cursor/uninstall.ts +18 -1
  90. package/src/targets/lib.ts +166 -14
  91. package/src/targets/opencode/install.ts +29 -1
  92. package/src/targets/opencode/plugin.ts +23 -12
  93. package/src/targets/opencode/uninstall.ts +30 -3
  94. package/src/tools/agent/algorithm-reflect.ts +1 -1
  95. package/src/tools/agent/analyze.ts +18 -18
  96. package/src/tools/agent/handoff-note.ts +116 -0
  97. package/src/tools/agent/project.ts +375 -75
  98. package/src/tools/agent/relationship-note.ts +51 -0
  99. package/src/tools/agent/synthesize.ts +6 -42
  100. package/src/tools/agent/thread.ts +15 -14
  101. package/src/tools/agent/wisdom-frame.ts +9 -3
  102. package/src/tools/import.ts +1 -1
  103. package/src/tools/relationship-reflect.ts +15 -13
  104. package/src/tools/self-model.ts +23 -19
  105. package/src/tools/session-summary.ts +3 -3
  106. package/src/tools/token-cost.ts +15 -16
  107. package/assets/skills/telos/tools/update-projects.ts +0 -106
  108. package/assets/templates/telos/PROJECTS.md +0 -7
package/src/cli/index.ts CHANGED
@@ -8,8 +8,8 @@
8
8
  *
9
9
  * Admin commands (pal cli ...):
10
10
  * init Scaffold PAL home, install hooks for all targets
11
- * install [--claude] [--opencode] [--cursor] Register hooks/skills for targets
12
- * uninstall [--claude] [--opencode] [--cursor] Remove hooks/skills for targets
11
+ * install [--claude] [--opencode] [--cursor] [--codex] Register hooks/skills for targets
12
+ * uninstall [--claude] [--opencode] [--cursor] [--codex] Remove hooks/skills for targets
13
13
  * update Update PAL (git pull or npm update)
14
14
  * export [path] [--dry-run] Export user state to zip
15
15
  * import [path] [--dry-run] Import user state from zip
@@ -25,6 +25,7 @@ import { resolve } from "node:path";
25
25
  import { palHome, palPkg, platform } from "../hooks/lib/paths";
26
26
  import { hasRealContent, SETUP_STEPS, STEP_ORDER } from "../hooks/lib/setup";
27
27
  import { log } from "../targets/lib";
28
+ import { checkPendingMigrations } from "./migrate";
28
29
 
29
30
  const allArgs = process.argv.slice(2);
30
31
 
@@ -169,6 +170,11 @@ async function runCli(command: string | undefined, args: string[]) {
169
170
  case "doctor":
170
171
  doctor();
171
172
  break;
173
+ case "migrate": {
174
+ const { runMigrate } = await import("./migrate");
175
+ runMigrate(args);
176
+ break;
177
+ }
172
178
  case "usage": {
173
179
  const { usage } = await import("../tools/token-cost");
174
180
  usage();
@@ -204,14 +210,15 @@ function showHelp() {
204
210
  pal cli <command> [options] Admin commands
205
211
 
206
212
  Admin commands:
207
- pal cli init [--claude] [--opencode] [--cursor] Scaffold and install (default: all)
208
- pal cli install [--claude] [--opencode] [--cursor] Register hooks for targets
209
- pal cli uninstall [--claude] [--opencode] [--cursor] Remove hooks for targets
213
+ pal cli init [--claude] [--opencode] [--cursor] [--codex] Scaffold and install (default: all)
214
+ pal cli install [--claude] [--opencode] [--cursor] [--codex] Register hooks for targets
215
+ pal cli uninstall [--claude] [--opencode] [--cursor] [--codex] Remove hooks for targets
210
216
  pal cli update Update PAL (git pull or npm update)
211
217
  pal cli export [path] [--dry-run] Export state to zip
212
218
  pal cli import [path] [--dry-run] Import state from zip
213
219
  pal cli status Show PAL configuration
214
220
  pal cli doctor Check prerequisites and health
221
+ pal cli migrate [--list] [--dry-run] Run pending data migrations
215
222
  pal cli usage Summarize token usage and cost
216
223
 
217
224
  Environment:
@@ -221,32 +228,42 @@ function showHelp() {
221
228
  PAL_OPENCODE_DIR Override opencode config dir (default: ~/.config/opencode)
222
229
  PAL_CURSOR_DIR Override Cursor config dir (default: ~/.cursor)
223
230
  PAL_COPILOT_DIR Override Copilot config dir (default: ~/.copilot)
231
+ PAL_CODEX_DIR Override Codex config dir (default: ~/.codex)
224
232
  PAL_AGENTS_DIR Override agents dir (default: ~/.agents)
225
233
  `);
226
234
  }
227
235
 
228
- type Targets = { claude: boolean; opencode: boolean; cursor: boolean; copilot: boolean };
236
+ type Targets = {
237
+ claude: boolean;
238
+ opencode: boolean;
239
+ cursor: boolean;
240
+ copilot: boolean;
241
+ codex: boolean;
242
+ };
229
243
 
230
244
  function parseTargets(args: string[]): Targets {
231
245
  let claude = false;
232
246
  let opencode = false;
233
247
  let cursor = false;
234
248
  let copilot = false;
249
+ let codex = false;
235
250
  for (const arg of args) {
236
251
  if (arg === "--claude") claude = true;
237
252
  else if (arg === "--opencode") opencode = true;
238
253
  else if (arg === "--cursor") cursor = true;
239
254
  else if (arg === "--copilot") copilot = true;
255
+ else if (arg === "--codex") codex = true;
240
256
  else if (arg === "--all") {
241
257
  claude = true;
242
258
  opencode = true;
243
259
  cursor = true;
244
260
  copilot = true;
261
+ codex = true;
245
262
  }
246
263
  }
247
- if (!claude && !opencode && !cursor && !copilot)
248
- return { claude: true, opencode: true, cursor: true, copilot: true };
249
- return { claude, opencode, cursor, copilot };
264
+ if (!claude && !opencode && !cursor && !copilot && !codex)
265
+ return { claude: true, opencode: true, cursor: true, copilot: true, codex: true };
266
+ return { claude, opencode, cursor, copilot, codex };
250
267
  }
251
268
 
252
269
  /** Resolve targets against available agents. Errors if explicitly requested but missing. */
@@ -259,6 +276,7 @@ function resolveTargets(args: string[], health?: DoctorResult): Targets {
259
276
  a === "--opencode" ||
260
277
  a === "--cursor" ||
261
278
  a === "--copilot" ||
279
+ a === "--codex" ||
262
280
  a === "--all"
263
281
  );
264
282
 
@@ -279,6 +297,10 @@ function resolveTargets(args: string[], health?: DoctorResult): Targets {
279
297
  log.error("Copilot is not installed. Run 'pal cli doctor' for details.");
280
298
  process.exit(1);
281
299
  }
300
+ if (requested.codex && !h.codex.available) {
301
+ log.error("Codex is not installed. Run 'pal cli doctor' for details.");
302
+ process.exit(1);
303
+ }
282
304
  return requested;
283
305
  }
284
306
 
@@ -288,12 +310,14 @@ function resolveTargets(args: string[], health?: DoctorResult): Targets {
288
310
  opencode: h.opencode.available,
289
311
  cursor: h.cursor.available,
290
312
  copilot: h.copilot.available,
313
+ codex: h.codex.available,
291
314
  };
292
315
 
293
316
  if (!targets.claude) log.info("Skipping Claude Code (not installed)");
294
317
  if (!targets.opencode) log.info("Skipping opencode (not installed)");
295
318
  if (!targets.cursor) log.info("Skipping Cursor (not installed)");
296
319
  if (!targets.copilot) log.info("Skipping Copilot (not installed)");
320
+ if (!targets.codex) log.info("Skipping Codex (not installed)");
297
321
 
298
322
  return targets;
299
323
  }
@@ -341,6 +365,22 @@ function checkCopilotHooksRegistered(): boolean {
341
365
  return existsSync(resolve(platform.copilotDir(), "hooks", "pal-hooks.json"));
342
366
  }
343
367
 
368
+ function checkCodexHooksRegistered(): boolean {
369
+ const hooksPath = resolve(platform.codexDir(), "hooks.json");
370
+ if (!existsSync(hooksPath)) return false;
371
+ try {
372
+ const data = JSON.parse(readFileSync(hooksPath, "utf-8"));
373
+ const entries = data?.hooks?.SessionStart;
374
+ if (!Array.isArray(entries)) return false;
375
+ return entries.some((entry: { command?: string; hooks?: { command?: string }[] }) => {
376
+ if (entry?.command?.includes("LoadContext")) return true;
377
+ return entry?.hooks?.some((h) => h?.command?.includes("LoadContext")) ?? false;
378
+ });
379
+ } catch {
380
+ return false;
381
+ }
382
+ }
383
+
344
384
  function checkCopilotInstructionsPresent(): boolean {
345
385
  return existsSync(resolve(platform.copilotDir(), "copilot-instructions.md"));
346
386
  }
@@ -408,7 +448,7 @@ function checkHookHealth(home: string): HookHealth {
408
448
  // Filter to last 24h
409
449
  const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000);
410
450
  const recentErrors = lines.filter((line) => {
411
- const match = line.match(/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]/);
451
+ const match = new RegExp(/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]/).exec(line);
412
452
  if (!match) return false;
413
453
  return new Date(match[1]) > cutoff;
414
454
  });
@@ -434,6 +474,7 @@ interface DoctorResult {
434
474
  opencode: ToolCheck;
435
475
  cursor: ToolCheck;
436
476
  copilot: ToolCheck;
477
+ codex: ToolCheck;
437
478
  hasAgent: boolean;
438
479
  }
439
480
 
@@ -446,6 +487,7 @@ function doctor(silent = false): DoctorResult {
446
487
  opencode: { name: "opencode", available: true },
447
488
  cursor: { name: "cursor", available: true },
448
489
  copilot: { name: "copilot", available: true },
490
+ codex: { name: "codex", available: true },
449
491
  hasAgent: true,
450
492
  };
451
493
  }
@@ -455,8 +497,13 @@ function doctor(silent = false): DoctorResult {
455
497
  const opencode = checkTool("opencode");
456
498
  const cursor = checkTool("cursor");
457
499
  const copilot = checkTool("copilot", ["version"]);
500
+ const codex = checkTool("codex");
458
501
  const hasAgent =
459
- claude.available || opencode.available || cursor.available || copilot.available;
502
+ claude.available ||
503
+ opencode.available ||
504
+ cursor.available ||
505
+ copilot.available ||
506
+ codex.available;
460
507
 
461
508
  const home = palHome();
462
509
  const telosCount = (() => {
@@ -499,6 +546,9 @@ function doctor(silent = false): DoctorResult {
499
546
  copilot.available
500
547
  ? ok(`Copilot ${copilot.version || ""}`.trim())
501
548
  : fail("Copilot — not found");
549
+ codex.available
550
+ ? ok(`Codex ${codex.version || ""}`.trim())
551
+ : fail("Codex — not found");
502
552
  ok(`PAL home: ${home}`);
503
553
  telosCount > 0 ? ok(`TELOS: ${telosCount} files`) : fail("TELOS: not scaffolded");
504
554
 
@@ -572,6 +622,12 @@ function doctor(silent = false): DoctorResult {
572
622
  ? ok(`Copilot skills: ${n}`)
573
623
  : warn("Copilot skills — none found (run 'pal cli install --copilot')");
574
624
  }
625
+ if (codex.available) {
626
+ const n = countSkillsIn(resolve(platform.codexDir(), "skills"));
627
+ n > 0
628
+ ? ok(`Codex skills: ${n}`)
629
+ : warn("Codex skills — none found (run 'pal cli install --codex')");
630
+ }
575
631
 
576
632
  // Dependencies
577
633
  const nodeModulesPath = resolve(palPkg(), "node_modules");
@@ -610,6 +666,11 @@ function doctor(silent = false): DoctorResult {
610
666
  ? ok("copilot-instructions.md present")
611
667
  : warn("copilot-instructions.md missing (run 'pal cli install --copilot')");
612
668
  }
669
+ if (codex.available) {
670
+ checkCodexHooksRegistered()
671
+ ? ok("Codex hooks registered")
672
+ : fail("Codex hooks — not registered (run 'pal cli install --codex')");
673
+ }
613
674
 
614
675
  // API key checks
615
676
  process.env.PAL_ANTHROPIC_API_KEY
@@ -636,6 +697,17 @@ function doctor(silent = false): DoctorResult {
636
697
  }
637
698
  }
638
699
 
700
+ // Pending migrations
701
+ const pendingMigrations = checkPendingMigrations();
702
+ if (pendingMigrations.length > 0) {
703
+ for (const m of pendingMigrations) {
704
+ const detail = m.detail ? ` (${m.detail})` : "";
705
+ warn(
706
+ `Migration pending: ${m.id} — ${m.description}${detail} → run 'pal cli migrate'`
707
+ );
708
+ }
709
+ }
710
+
639
711
  if (!hasAgent) {
640
712
  console.log("");
641
713
  log.error("No supported agent found. Install Claude Code or opencode.");
@@ -643,7 +715,7 @@ function doctor(silent = false): DoctorResult {
643
715
  console.log("");
644
716
  }
645
717
 
646
- return { bun, claude, opencode, cursor, copilot, hasAgent };
718
+ return { bun, claude, opencode, cursor, copilot, codex, hasAgent };
647
719
  }
648
720
 
649
721
  // ── Commands ──
@@ -749,6 +821,12 @@ async function install(targets: Targets) {
749
821
  console.log("");
750
822
  }
751
823
 
824
+ if (targets.codex) {
825
+ console.log("━━━ Codex ━━━");
826
+ await import("../targets/codex/install");
827
+ console.log("");
828
+ }
829
+
752
830
  log.success("Done. Existing config was preserved — only new entries were added.");
753
831
  }
754
832
 
@@ -779,6 +857,12 @@ async function uninstall(args: string[]) {
779
857
  console.log("");
780
858
  }
781
859
 
860
+ if (targets.codex) {
861
+ console.log("━━━ Codex ━━━");
862
+ await import("../targets/codex/uninstall");
863
+ console.log("");
864
+ }
865
+
782
866
  log.success(
783
867
  `PAL uninstalled. Your TELOS, skills, and memory are still in ${palHome()}.`
784
868
  );
@@ -935,7 +1019,14 @@ async function update() {
935
1019
  }
936
1020
  }
937
1021
 
938
- const newPkg = JSON.parse(readFileSync(resolve(pkg, "package.json"), "utf-8"));
1022
+ let newPkg: { version: string };
1023
+ try {
1024
+ newPkg = JSON.parse(readFileSync(resolve(pkg, "package.json"), "utf-8")) as {
1025
+ version: string;
1026
+ };
1027
+ } catch (e) {
1028
+ throw new Error(`Failed to read updated package.json: ${e}`);
1029
+ }
939
1030
  log.success(`Updated: ${result.current} → ${newPkg.version}`);
940
1031
 
941
1032
  log.info("Reinstalling...");
@@ -946,7 +1037,14 @@ async function status() {
946
1037
  const home = palHome();
947
1038
  const pkg = palPkg();
948
1039
 
949
- const pkgJson = JSON.parse(readFileSync(resolve(pkg, "package.json"), "utf-8"));
1040
+ let pkgJson: { version: string };
1041
+ try {
1042
+ pkgJson = JSON.parse(readFileSync(resolve(pkg, "package.json"), "utf-8")) as {
1043
+ version: string;
1044
+ };
1045
+ } catch (e) {
1046
+ throw new Error(`Failed to read package.json: ${e}`);
1047
+ }
950
1048
 
951
1049
  console.log("");
952
1050
  log.info(`Version: ${pkgJson.version}`);
@@ -0,0 +1,299 @@
1
+ /**
2
+ * pal cli migrate — versioned, non-destructive data migrations.
3
+ *
4
+ * Each Migration has:
5
+ * check() — returns whether this migration is needed (safe to call repeatedly)
6
+ * run() — applies the migration; NEVER deletes source data
7
+ *
8
+ * Add new migrations by appending to MIGRATIONS. Registry is ordered; migrations
9
+ * run in declaration order. Doctor calls checkPendingMigrations() to surface
10
+ * pending work without running anything.
11
+ */
12
+
13
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
14
+ import { resolve } from "node:path";
15
+ import { paths } from "../hooks/lib/paths";
16
+ import {
17
+ type ProjectProgress,
18
+ type ProjectStatus,
19
+ readAllProjects,
20
+ readProject,
21
+ writeProject,
22
+ } from "../hooks/lib/projects";
23
+ import { readThreads, type Thread, writeThreads } from "../tools/agent/thread";
24
+
25
+ // ── Types ─────────────────────────────────────────────────────────
26
+
27
+ interface MigrationResult {
28
+ migrated: number;
29
+ skipped: number;
30
+ results: string[];
31
+ }
32
+
33
+ interface Migration {
34
+ id: string;
35
+ description: string;
36
+ check(): { pending: boolean; detail?: string };
37
+ run(dryRun?: boolean): MigrationResult;
38
+ }
39
+
40
+ // ── v1-projects: JSON progress files → ISA.md ─────────────────────
41
+
42
+ interface LegacyDecision {
43
+ ts: string;
44
+ decision: string;
45
+ rationale: string;
46
+ }
47
+
48
+ interface LegacyProject {
49
+ name: string;
50
+ path: string;
51
+ status: ProjectStatus;
52
+ created: string;
53
+ updated: string;
54
+ facts?: string[];
55
+ objectives?: string[];
56
+ next_steps?: string[];
57
+ blockers?: string[];
58
+ handoff?: string;
59
+ decisions?: LegacyDecision[];
60
+ }
61
+
62
+ function pendingJsonFiles(): string[] {
63
+ const progressDir = paths.progress();
64
+ if (!existsSync(progressDir)) return [];
65
+ return readdirSync(progressDir)
66
+ .filter((f) => f.endsWith(".json"))
67
+ .filter((f) => !readProject(f.slice(0, -5)));
68
+ }
69
+
70
+ const v1Projects: Migration = {
71
+ id: "v1-projects",
72
+ description: "Migrate legacy JSON progress files to ISA.md format",
73
+
74
+ check() {
75
+ const pending = pendingJsonFiles();
76
+ return {
77
+ pending: pending.length > 0,
78
+ detail: pending.length > 0 ? `${pending.length} file(s) in progress/` : undefined,
79
+ };
80
+ },
81
+
82
+ run(dryRun = false): MigrationResult {
83
+ const progressDir = paths.progress();
84
+ const files = pendingJsonFiles();
85
+
86
+ let migrated = 0;
87
+ let skipped = 0;
88
+ const results: string[] = [];
89
+
90
+ for (const file of files) {
91
+ const slug = file.slice(0, -5);
92
+ const filePath = resolve(progressDir, file);
93
+
94
+ try {
95
+ const raw = JSON.parse(readFileSync(filePath, "utf-8")) as LegacyProject;
96
+ if (!raw?.name || !raw?.path || !raw?.status) {
97
+ skipped++;
98
+ results.push(`${slug}: skipped (malformed JSON)`);
99
+ continue;
100
+ }
101
+
102
+ if (!dryRun) {
103
+ const p: ProjectProgress = {
104
+ name: raw.name,
105
+ path: raw.path,
106
+ status: raw.status,
107
+ created: raw.created ?? new Date().toISOString(),
108
+ updated: raw.updated ?? new Date().toISOString(),
109
+ ...(raw.handoff ? { handoff: raw.handoff } : {}),
110
+ ...(raw.next_steps?.length ? { next: raw.next_steps } : {}),
111
+ ...(raw.blockers?.length ? { blockers: raw.blockers } : {}),
112
+ };
113
+
114
+ if (raw.facts?.length) p.context = raw.facts.join("\n");
115
+ if (raw.objectives?.length)
116
+ p.goal = raw.objectives.map((o) => `- ${o}`).join("\n");
117
+ if (raw.decisions?.length) {
118
+ p.decisions = raw.decisions
119
+ .map((d) => `- ${d.ts.slice(0, 10)}: ${d.decision} (${d.rationale})`)
120
+ .join("\n");
121
+ }
122
+
123
+ writeProject(p);
124
+ }
125
+
126
+ migrated++;
127
+ results.push(`${slug}: ${dryRun ? "would migrate" : "migrated"} (source kept)`);
128
+ } catch {
129
+ skipped++;
130
+ results.push(`${slug}: skipped (read/write error)`);
131
+ }
132
+ }
133
+
134
+ return { migrated, skipped, results };
135
+ },
136
+ };
137
+
138
+ // ── v2-threads-to-isc: open threads → ISCs on matching project ────
139
+
140
+ function nextIscId(criteria: string): number {
141
+ const ids: number[] = [];
142
+ for (const line of criteria.split("\n")) {
143
+ const m = new RegExp(/^-\s+\[[ x]\]\s+ISC-(\d+):/i).exec(line);
144
+ if (m) ids.push(Number(m[1]));
145
+ }
146
+ return ids.length > 0 ? Math.max(...ids) + 1 : 1;
147
+ }
148
+
149
+ function pendingThreadsForProjects(): { thread: Thread; project: ProjectProgress }[] {
150
+ const threads = readThreads().filter((t) => t.status === "open");
151
+ if (threads.length === 0) return [];
152
+ const projects = readAllProjects();
153
+ const results: { thread: Thread; project: ProjectProgress }[] = [];
154
+ for (const thread of threads) {
155
+ const project = projects.find((p) => resolve(p.path) === resolve(thread.cwd));
156
+ if (project) results.push({ thread, project });
157
+ }
158
+ return results;
159
+ }
160
+
161
+ const v2ThreadsToIsc: Migration = {
162
+ id: "v2-threads-to-isc",
163
+ description: "Migrate open project-scoped threads to ISCs on their project ISA",
164
+
165
+ check() {
166
+ const pending = pendingThreadsForProjects();
167
+ return {
168
+ pending: pending.length > 0,
169
+ detail: pending.length > 0 ? `${pending.length} thread(s) to migrate` : undefined,
170
+ };
171
+ },
172
+
173
+ run(dryRun = false): MigrationResult {
174
+ const pending = pendingThreadsForProjects();
175
+ let migrated = 0;
176
+ let skipped = 0;
177
+ const results: string[] = [];
178
+
179
+ const threadUpdates: Map<string, Thread> = new Map();
180
+
181
+ for (const { thread, project } of pending) {
182
+ try {
183
+ if (!dryRun) {
184
+ const p = readProject(project.name);
185
+ if (!p) {
186
+ skipped++;
187
+ results.push(`${thread.id}: skipped (project "${project.name}" unreadable)`);
188
+ continue;
189
+ }
190
+ const current = p.criteria ?? "";
191
+ const id = nextIscId(current);
192
+ const newLine = `- [ ] ISC-${id}: ${thread.title}`;
193
+ p.criteria = current ? `${current.trimEnd()}\n${newLine}` : newLine;
194
+ p.updated = new Date().toISOString();
195
+ writeProject(p);
196
+ threadUpdates.set(thread.id, {
197
+ ...thread,
198
+ status: "resolved",
199
+ resolved: new Date().toISOString(),
200
+ });
201
+ }
202
+ migrated++;
203
+ results.push(
204
+ `${thread.id} → ${project.name} ISC: ${dryRun ? "would add" : "added"} "${thread.title}" (thread source kept)`
205
+ );
206
+ } catch {
207
+ skipped++;
208
+ results.push(`${thread.id}: skipped (error)`);
209
+ }
210
+ }
211
+
212
+ if (!dryRun && threadUpdates.size > 0) {
213
+ const all = readThreads().map((t) => threadUpdates.get(t.id) ?? t);
214
+ writeThreads(all);
215
+ }
216
+
217
+ return { migrated, skipped, results };
218
+ },
219
+ };
220
+
221
+ // ── Registry ──────────────────────────────────────────────────────
222
+
223
+ const MIGRATIONS: Migration[] = [v1Projects, v2ThreadsToIsc];
224
+
225
+ // ── Public API ────────────────────────────────────────────────────
226
+
227
+ interface PendingMigration {
228
+ id: string;
229
+ description: string;
230
+ detail?: string;
231
+ }
232
+
233
+ /** Returns migrations that have pending work. Used by doctor. */
234
+ export function checkPendingMigrations(): PendingMigration[] {
235
+ return MIGRATIONS.flatMap((m) => {
236
+ const { pending, detail } = m.check();
237
+ return pending ? [{ id: m.id, description: m.description, detail }] : [];
238
+ });
239
+ }
240
+
241
+ /** Entry point for `pal cli migrate`. */
242
+ export function runMigrate(args: string[]): void {
243
+ const dryRun = args.includes("--dry-run");
244
+ const list = args.includes("--list");
245
+
246
+ if (list) {
247
+ const pending = checkPendingMigrations();
248
+ const done = MIGRATIONS.filter((m) => !m.check().pending);
249
+
250
+ console.log("");
251
+ if (pending.length > 0) {
252
+ console.log(" Pending migrations:");
253
+ for (const m of pending) {
254
+ const detail = m.detail ? ` (${m.detail})` : "";
255
+ console.log(` ⚠ ${m.id} — ${m.description}${detail}`);
256
+ }
257
+ } else {
258
+ console.log(" No pending migrations.");
259
+ }
260
+ if (done.length > 0) {
261
+ console.log(" Done:");
262
+ for (const m of done) {
263
+ console.log(` ✓ ${m.id} — ${m.description}`);
264
+ }
265
+ }
266
+ console.log("");
267
+ return;
268
+ }
269
+
270
+ const pending = MIGRATIONS.filter((m) => m.check().pending);
271
+ if (pending.length === 0) {
272
+ console.log(" Nothing to migrate — all up to date.");
273
+ return;
274
+ }
275
+
276
+ if (dryRun) console.log(" Dry run — no files will be written.\n");
277
+
278
+ let totalMigrated = 0;
279
+ let totalSkipped = 0;
280
+
281
+ for (const m of pending) {
282
+ console.log(` Running: ${m.id} — ${m.description}`);
283
+ const result = m.run(dryRun);
284
+ totalMigrated += result.migrated;
285
+ totalSkipped += result.skipped;
286
+ for (const r of result.results) {
287
+ console.log(` ${r}`);
288
+ }
289
+ }
290
+
291
+ console.log("");
292
+ console.log(
293
+ ` ${dryRun ? "Would migrate" : "Migrated"}: ${totalMigrated} | Skipped: ${totalSkipped}`
294
+ );
295
+ if (!dryRun && totalMigrated > 0) {
296
+ console.log(" Source files preserved — delete manually once verified.");
297
+ }
298
+ console.log("");
299
+ }
@@ -16,9 +16,9 @@ export async function promptIdentity(): Promise<void> {
16
16
  if (!process.stdin.isTTY) return;
17
17
 
18
18
  const settings: PalSettingsData = { ...readSettings() };
19
- if (!settings.identity) settings.identity = {};
20
- if (!settings.identity.ai) settings.identity.ai = {};
21
- if (!settings.identity.principal) settings.identity.principal = {};
19
+ settings.identity ??= {};
20
+ settings.identity.ai ??= {};
21
+ settings.identity.principal ??= {};
22
22
 
23
23
  const ai = settings.identity.ai;
24
24
  const principal = settings.identity.principal;