portable-agent-layer 0.36.0 → 0.38.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 +1 -0
- 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 -20
- 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/tools/update-telos.ts +0 -1
- package/assets/templates/PAL/ALGORITHM.md +27 -3
- package/assets/templates/hooks.codex.json +44 -0
- package/assets/templates/hooks.cursor.json +11 -5
- package/package.json +5 -2
- package/src/cli/index.ts +113 -17
- package/src/cli/migrate.ts +299 -0
- package/src/cli/setup-identity.ts +3 -3
- package/src/cli/setup-telos.ts +0 -1
- package/src/hooks/CompactRecover.ts +11 -5
- package/src/hooks/LoadContext.ts +14 -2
- 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/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 +9 -8
- 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/context.ts +45 -117
- package/src/hooks/lib/entities.ts +7 -7
- package/src/hooks/lib/frontmatter.ts +4 -4
- package/src/hooks/lib/graduation.ts +8 -7
- 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 +3 -15
- package/src/hooks/lib/retrieval-index.ts +5 -3
- package/src/hooks/lib/retrieval.ts +11 -12
- package/src/hooks/lib/security.ts +22 -18
- package/src/hooks/lib/semi-static.ts +4 -2
- package/src/hooks/lib/session-names.ts +1 -1
- package/src/hooks/lib/settings.ts +1 -1
- package/src/hooks/lib/setup.ts +2 -60
- package/src/hooks/lib/signals.ts +2 -2
- package/src/hooks/lib/stdin.ts +1 -1
- package/src/hooks/lib/stop.ts +13 -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 +13 -18
- package/src/targets/codex/install.ts +95 -0
- package/src/targets/codex/uninstall.ts +70 -0
- package/src/targets/lib.ts +140 -14
- package/src/targets/opencode/plugin.ts +22 -11
- package/src/tools/agent/algorithm-reflect.ts +1 -1
- package/src/tools/agent/analyze.ts +18 -18
- package/src/tools/agent/handoff-note.ts +1 -1
- package/src/tools/agent/project.ts +375 -75
- 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 +13 -11
- package/src/tools/self-model.ts +20 -16
- 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/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,16 +448,14 @@ 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
|
});
|
|
415
455
|
|
|
416
456
|
const lastError =
|
|
417
457
|
recentErrors.length > 0
|
|
418
|
-
? recentErrors
|
|
419
|
-
.replace(/^\[.*?\] ERROR /, "")
|
|
420
|
-
.slice(0, 120)
|
|
458
|
+
? (recentErrors.at(-1) ?? "").replace(/^\[.*?\] ERROR /, "").slice(0, 120)
|
|
421
459
|
: null;
|
|
422
460
|
|
|
423
461
|
return { totalErrors: recentErrors.length, lastError };
|
|
@@ -434,6 +472,7 @@ interface DoctorResult {
|
|
|
434
472
|
opencode: ToolCheck;
|
|
435
473
|
cursor: ToolCheck;
|
|
436
474
|
copilot: ToolCheck;
|
|
475
|
+
codex: ToolCheck;
|
|
437
476
|
hasAgent: boolean;
|
|
438
477
|
}
|
|
439
478
|
|
|
@@ -446,6 +485,7 @@ function doctor(silent = false): DoctorResult {
|
|
|
446
485
|
opencode: { name: "opencode", available: true },
|
|
447
486
|
cursor: { name: "cursor", available: true },
|
|
448
487
|
copilot: { name: "copilot", available: true },
|
|
488
|
+
codex: { name: "codex", available: true },
|
|
449
489
|
hasAgent: true,
|
|
450
490
|
};
|
|
451
491
|
}
|
|
@@ -455,8 +495,13 @@ function doctor(silent = false): DoctorResult {
|
|
|
455
495
|
const opencode = checkTool("opencode");
|
|
456
496
|
const cursor = checkTool("cursor");
|
|
457
497
|
const copilot = checkTool("copilot", ["version"]);
|
|
498
|
+
const codex = checkTool("codex");
|
|
458
499
|
const hasAgent =
|
|
459
|
-
claude.available ||
|
|
500
|
+
claude.available ||
|
|
501
|
+
opencode.available ||
|
|
502
|
+
cursor.available ||
|
|
503
|
+
copilot.available ||
|
|
504
|
+
codex.available;
|
|
460
505
|
|
|
461
506
|
const home = palHome();
|
|
462
507
|
const telosCount = (() => {
|
|
@@ -499,6 +544,9 @@ function doctor(silent = false): DoctorResult {
|
|
|
499
544
|
copilot.available
|
|
500
545
|
? ok(`Copilot ${copilot.version || ""}`.trim())
|
|
501
546
|
: fail("Copilot — not found");
|
|
547
|
+
codex.available
|
|
548
|
+
? ok(`Codex ${codex.version || ""}`.trim())
|
|
549
|
+
: fail("Codex — not found");
|
|
502
550
|
ok(`PAL home: ${home}`);
|
|
503
551
|
telosCount > 0 ? ok(`TELOS: ${telosCount} files`) : fail("TELOS: not scaffolded");
|
|
504
552
|
|
|
@@ -572,6 +620,12 @@ function doctor(silent = false): DoctorResult {
|
|
|
572
620
|
? ok(`Copilot skills: ${n}`)
|
|
573
621
|
: warn("Copilot skills — none found (run 'pal cli install --copilot')");
|
|
574
622
|
}
|
|
623
|
+
if (codex.available) {
|
|
624
|
+
const n = countSkillsIn(resolve(platform.codexDir(), "skills"));
|
|
625
|
+
n > 0
|
|
626
|
+
? ok(`Codex skills: ${n}`)
|
|
627
|
+
: warn("Codex skills — none found (run 'pal cli install --codex')");
|
|
628
|
+
}
|
|
575
629
|
|
|
576
630
|
// Dependencies
|
|
577
631
|
const nodeModulesPath = resolve(palPkg(), "node_modules");
|
|
@@ -610,6 +664,11 @@ function doctor(silent = false): DoctorResult {
|
|
|
610
664
|
? ok("copilot-instructions.md present")
|
|
611
665
|
: warn("copilot-instructions.md missing (run 'pal cli install --copilot')");
|
|
612
666
|
}
|
|
667
|
+
if (codex.available) {
|
|
668
|
+
checkCodexHooksRegistered()
|
|
669
|
+
? ok("Codex hooks registered")
|
|
670
|
+
: fail("Codex hooks — not registered (run 'pal cli install --codex')");
|
|
671
|
+
}
|
|
613
672
|
|
|
614
673
|
// API key checks
|
|
615
674
|
process.env.PAL_ANTHROPIC_API_KEY
|
|
@@ -636,6 +695,17 @@ function doctor(silent = false): DoctorResult {
|
|
|
636
695
|
}
|
|
637
696
|
}
|
|
638
697
|
|
|
698
|
+
// Pending migrations
|
|
699
|
+
const pendingMigrations = checkPendingMigrations();
|
|
700
|
+
if (pendingMigrations.length > 0) {
|
|
701
|
+
for (const m of pendingMigrations) {
|
|
702
|
+
const detail = m.detail ? ` (${m.detail})` : "";
|
|
703
|
+
warn(
|
|
704
|
+
`Migration pending: ${m.id} — ${m.description}${detail} → run 'pal cli migrate'`
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
639
709
|
if (!hasAgent) {
|
|
640
710
|
console.log("");
|
|
641
711
|
log.error("No supported agent found. Install Claude Code or opencode.");
|
|
@@ -643,7 +713,7 @@ function doctor(silent = false): DoctorResult {
|
|
|
643
713
|
console.log("");
|
|
644
714
|
}
|
|
645
715
|
|
|
646
|
-
return { bun, claude, opencode, cursor, copilot, hasAgent };
|
|
716
|
+
return { bun, claude, opencode, cursor, copilot, codex, hasAgent };
|
|
647
717
|
}
|
|
648
718
|
|
|
649
719
|
// ── Commands ──
|
|
@@ -749,6 +819,12 @@ async function install(targets: Targets) {
|
|
|
749
819
|
console.log("");
|
|
750
820
|
}
|
|
751
821
|
|
|
822
|
+
if (targets.codex) {
|
|
823
|
+
console.log("━━━ Codex ━━━");
|
|
824
|
+
await import("../targets/codex/install");
|
|
825
|
+
console.log("");
|
|
826
|
+
}
|
|
827
|
+
|
|
752
828
|
log.success("Done. Existing config was preserved — only new entries were added.");
|
|
753
829
|
}
|
|
754
830
|
|
|
@@ -779,6 +855,12 @@ async function uninstall(args: string[]) {
|
|
|
779
855
|
console.log("");
|
|
780
856
|
}
|
|
781
857
|
|
|
858
|
+
if (targets.codex) {
|
|
859
|
+
console.log("━━━ Codex ━━━");
|
|
860
|
+
await import("../targets/codex/uninstall");
|
|
861
|
+
console.log("");
|
|
862
|
+
}
|
|
863
|
+
|
|
782
864
|
log.success(
|
|
783
865
|
`PAL uninstalled. Your TELOS, skills, and memory are still in ${palHome()}.`
|
|
784
866
|
);
|
|
@@ -935,7 +1017,14 @@ async function update() {
|
|
|
935
1017
|
}
|
|
936
1018
|
}
|
|
937
1019
|
|
|
938
|
-
|
|
1020
|
+
let newPkg: { version: string };
|
|
1021
|
+
try {
|
|
1022
|
+
newPkg = JSON.parse(readFileSync(resolve(pkg, "package.json"), "utf-8")) as {
|
|
1023
|
+
version: string;
|
|
1024
|
+
};
|
|
1025
|
+
} catch (e) {
|
|
1026
|
+
throw new Error(`Failed to read updated package.json: ${e}`);
|
|
1027
|
+
}
|
|
939
1028
|
log.success(`Updated: ${result.current} → ${newPkg.version}`);
|
|
940
1029
|
|
|
941
1030
|
log.info("Reinstalling...");
|
|
@@ -946,7 +1035,14 @@ async function status() {
|
|
|
946
1035
|
const home = palHome();
|
|
947
1036
|
const pkg = palPkg();
|
|
948
1037
|
|
|
949
|
-
|
|
1038
|
+
let pkgJson: { version: string };
|
|
1039
|
+
try {
|
|
1040
|
+
pkgJson = JSON.parse(readFileSync(resolve(pkg, "package.json"), "utf-8")) as {
|
|
1041
|
+
version: string;
|
|
1042
|
+
};
|
|
1043
|
+
} catch (e) {
|
|
1044
|
+
throw new Error(`Failed to read package.json: ${e}`);
|
|
1045
|
+
}
|
|
950
1046
|
|
|
951
1047
|
console.log("");
|
|
952
1048
|
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;
|
package/src/cli/setup-telos.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Interactive TELOS setup — prompts for personal context during `pal install`.
|
|
3
3
|
* Skips any step whose TELOS file already has real content.
|
|
4
|
-
* Projects use the upsertProject tool directly with a structured add-another loop.
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
6
|
import { writeFileSync } from "node:fs";
|
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
* Storage: ~/.pal/memory/state/last-exchange/{session_id}.json (with latest.json fallback).
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { existsSync
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import { readFile, unlink } from "node:fs/promises";
|
|
12
13
|
import { resolve } from "node:path";
|
|
14
|
+
import { isCursor } from "./lib/agent";
|
|
13
15
|
import { logDebug, logError } from "./lib/log";
|
|
14
16
|
import { paths } from "./lib/paths";
|
|
15
17
|
import { readStdinJSON } from "./lib/stdin";
|
|
@@ -61,7 +63,7 @@ const main = async () => {
|
|
|
61
63
|
process.exit(0);
|
|
62
64
|
}
|
|
63
65
|
|
|
64
|
-
const saved = JSON.parse(
|
|
66
|
+
const saved = JSON.parse(await readFile(file, "utf-8")) as SavedExchange;
|
|
65
67
|
const userBudget = Math.floor(MAX_OUTPUT * 0.4);
|
|
66
68
|
const assistantBudget = MAX_OUTPUT - userBudget - 300; // reserve for framing
|
|
67
69
|
|
|
@@ -81,7 +83,11 @@ const main = async () => {
|
|
|
81
83
|
"</system-reminder>",
|
|
82
84
|
].join("\n");
|
|
83
85
|
|
|
84
|
-
|
|
86
|
+
if (isCursor()) {
|
|
87
|
+
process.stdout.write(JSON.stringify({ additional_context: out }));
|
|
88
|
+
} else {
|
|
89
|
+
process.stdout.write(out);
|
|
90
|
+
}
|
|
85
91
|
logDebug("CompactRecover", `Re-injected ${out.length} chars from ${file}`);
|
|
86
92
|
|
|
87
93
|
// Consume-on-read: drop the session-keyed file after a successful injection so it
|
|
@@ -90,7 +96,7 @@ const main = async () => {
|
|
|
90
96
|
const sessionFile = sessionId ? resolve(stateDir, `${sessionId}.json`) : null;
|
|
91
97
|
if (sessionFile && file === sessionFile) {
|
|
92
98
|
try {
|
|
93
|
-
|
|
99
|
+
await unlink(sessionFile);
|
|
94
100
|
} catch (err) {
|
|
95
101
|
logError("CompactRecover:cleanup", err);
|
|
96
102
|
}
|
|
@@ -102,4 +108,4 @@ const main = async () => {
|
|
|
102
108
|
process.exit(0);
|
|
103
109
|
};
|
|
104
110
|
|
|
105
|
-
main();
|
|
111
|
+
await main();
|