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.
- package/README.md +2 -1
- package/assets/skills/analyze-pdf/tools/pdf-download.ts +1 -1
- package/assets/skills/analyze-youtube/tools/youtube-analyze.ts +1 -1
- package/assets/skills/consulting-report/tools/dev.ts +2 -2
- package/assets/skills/consulting-report/tools/generate-pdf.ts +9 -9
- package/assets/skills/consulting-report/tools/scaffold.ts +2 -2
- package/assets/skills/create-pdf/tools/md-to-html-pdf.ts +2 -2
- package/assets/skills/opinion/tools/opinion.ts +3 -2
- package/assets/skills/presentation/SKILL.md +1 -1
- package/assets/skills/presentation/tools/doctor.ts +2 -5
- package/assets/skills/presentation/tools/lib/inline.ts +6 -11
- package/assets/skills/presentation/tools/lib/lint-helpers.ts +2 -2
- package/assets/skills/presentation/tools/lib/lint-rules.ts +5 -2
- package/assets/skills/presentation/tools/setup-template.ts +10 -7
- package/assets/skills/projects/SKILL.md +44 -21
- package/assets/skills/research/tools/gemini-search.ts +2 -2
- package/assets/skills/research/tools/grok-search.ts +2 -2
- package/assets/skills/research/tools/perplexity-search.ts +2 -2
- package/assets/skills/telos/SKILL.md +7 -52
- package/assets/skills/telos/tools/update-telos.ts +0 -1
- package/assets/templates/PAL/ALGORITHM.md +54 -5
- package/assets/templates/PAL/PROJECT_LIFECYCLE.md +48 -0
- package/assets/templates/PAL/README.md +1 -1
- package/assets/templates/PAL/STEERING_RULES.md +4 -0
- package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +32 -17
- package/assets/templates/PAL/WORK_TRACKING.md +1 -1
- package/assets/templates/hooks.codex.json +44 -0
- package/assets/templates/hooks.cursor.json +11 -5
- package/assets/templates/pal-settings.json +1 -3
- package/assets/templates/settings.claude.json +2 -1
- package/package.json +2 -1
- package/src/cli/index.ts +112 -14
- package/src/cli/migrate.ts +299 -0
- package/src/cli/setup-identity.ts +3 -3
- package/src/cli/setup-telos.ts +12 -80
- package/src/hooks/CompactRecover.ts +11 -5
- package/src/hooks/LoadContext.ts +35 -11
- package/src/hooks/PreCompactPersist.ts +26 -34
- package/src/hooks/SecurityValidator.ts +43 -21
- package/src/hooks/StopOrchestrator.ts +4 -1
- package/src/hooks/UserPromptOrchestrator.ts +4 -2
- package/src/hooks/handlers/auto-graduate.ts +2 -2
- package/src/hooks/handlers/backup.ts +3 -3
- package/src/hooks/handlers/context-digests.ts +74 -0
- package/src/hooks/handlers/failure.ts +5 -3
- package/src/hooks/handlers/inject-retrieval.ts +29 -6
- package/src/hooks/handlers/persist-last-exchange.ts +76 -0
- package/src/hooks/handlers/rating.ts +2 -1
- package/src/hooks/handlers/readme-sync.ts +3 -2
- package/src/hooks/handlers/session-intelligence.ts +17 -93
- package/src/hooks/handlers/session-name.ts +2 -2
- package/src/hooks/handlers/synthesis.ts +5 -2
- package/src/hooks/handlers/update-counts.ts +3 -2
- package/src/hooks/lib/agent.ts +20 -18
- package/src/hooks/lib/claude-md.ts +69 -14
- package/src/hooks/lib/context.ts +92 -246
- package/src/hooks/lib/entities.ts +7 -7
- package/src/hooks/lib/frontmatter.ts +4 -4
- package/src/hooks/lib/graduation.ts +7 -6
- package/src/hooks/lib/inference.ts +6 -2
- package/src/hooks/lib/learning-category.ts +1 -1
- package/src/hooks/lib/learning-store.ts +6 -1
- package/src/hooks/lib/notify.ts +2 -2
- package/src/hooks/lib/opinions.ts +3 -3
- package/src/hooks/lib/paths.ts +2 -0
- package/src/hooks/lib/projects.ts +142 -74
- package/src/hooks/lib/readme-sync.ts +1 -1
- package/src/hooks/lib/relationship.ts +4 -16
- package/src/hooks/lib/retrieval-index.ts +5 -3
- package/src/hooks/lib/retrieval.ts +11 -12
- package/src/hooks/lib/security.ts +24 -18
- package/src/hooks/lib/semi-static.ts +188 -0
- package/src/hooks/lib/session-names.ts +1 -1
- package/src/hooks/lib/settings.ts +1 -1
- package/src/hooks/lib/setup.ts +2 -65
- package/src/hooks/lib/signals.ts +2 -2
- package/src/hooks/lib/stdin.ts +1 -1
- package/src/hooks/lib/stop.ts +16 -6
- package/src/hooks/lib/token-usage.ts +1 -2
- package/src/hooks/lib/transcript.ts +1 -1
- package/src/hooks/lib/wisdom.ts +5 -5
- package/src/hooks/lib/work-tracking.ts +8 -14
- package/src/targets/claude/uninstall.ts +1 -1
- package/src/targets/codex/install.ts +95 -0
- package/src/targets/codex/uninstall.ts +70 -0
- package/src/targets/copilot/install.ts +39 -8
- package/src/targets/copilot/uninstall.ts +58 -17
- package/src/targets/cursor/install.ts +8 -0
- package/src/targets/cursor/uninstall.ts +18 -1
- package/src/targets/lib.ts +166 -14
- package/src/targets/opencode/install.ts +29 -1
- package/src/targets/opencode/plugin.ts +23 -12
- package/src/targets/opencode/uninstall.ts +30 -3
- package/src/tools/agent/algorithm-reflect.ts +1 -1
- package/src/tools/agent/analyze.ts +18 -18
- package/src/tools/agent/handoff-note.ts +116 -0
- package/src/tools/agent/project.ts +375 -75
- package/src/tools/agent/relationship-note.ts +51 -0
- package/src/tools/agent/synthesize.ts +6 -42
- package/src/tools/agent/thread.ts +15 -14
- package/src/tools/agent/wisdom-frame.ts +9 -3
- package/src/tools/import.ts +1 -1
- package/src/tools/relationship-reflect.ts +15 -13
- package/src/tools/self-model.ts +23 -19
- package/src/tools/session-summary.ts +3 -3
- package/src/tools/token-cost.ts +15 -16
- package/assets/skills/telos/tools/update-projects.ts +0 -106
- 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 = {
|
|
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 =
|
|
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 ||
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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;
|