portable-agent-layer 0.85.14 → 0.85.16

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.85.14",
3
+ "version": "0.85.16",
4
4
  "description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,3 +1,4 @@
1
+ import { spawnSync } from "node:child_process";
1
2
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
3
  import { homedir } from "node:os";
3
4
  import { dirname, resolve } from "node:path";
@@ -67,22 +68,47 @@ function playwrightPlatformOverride(host: BrowserHost): string {
67
68
  return `PLAYWRIGHT_HOST_PLATFORM_OVERRIDE=ubuntu24.04-${host.arch === "arm64" ? "arm64" : "x64"} `;
68
69
  }
69
70
 
71
+ function chromiumInstallCommand(host: BrowserHost): string {
72
+ const pinned = host.playwright ? `playwright@${host.playwright.version}` : "playwright";
73
+ return `${playwrightPlatformOverride(host)}bun x ${pinned} install chromium`;
74
+ }
75
+
70
76
  export function playwrightFinding(host: BrowserHost): Finding {
71
77
  if (hasChromium(host)) return passed("playwright", "Playwright Chromium installed");
72
- const pinned = host.playwright ? `playwright@${host.playwright.version}` : "playwright";
73
- const override = playwrightPlatformOverride(host);
74
78
  return warning(
75
79
  "playwright.missing",
76
80
  "Playwright's Chromium is not installed — create-pdf, consulting-report and screenshots fail",
77
81
  {
78
- say: override
82
+ say: playwrightPlatformOverride(host)
79
83
  ? "Install it as Ubuntu 24.04, which Playwright supports"
80
84
  : "Install it",
81
- command: `${override}bun x ${pinned} install chromium`,
85
+ command: chromiumInstallCommand(host),
82
86
  }
83
87
  );
84
88
  }
85
89
 
90
+ function thisBrowserHost(): BrowserHost {
91
+ return {
92
+ browsersPath: playwrightBrowsersPath(),
93
+ playwright: installedPlaywright(),
94
+ platform: process.platform,
95
+ arch: process.arch,
96
+ osRelease: readOsRelease(),
97
+ };
98
+ }
99
+
100
+ function runInShell(command: string): boolean {
101
+ return spawnSync(command, { cwd: palPkg(), shell: true, stdio: "ignore" }).status === 0;
102
+ }
103
+
104
+ /** A failure is left to the doctor's report that follows, which names the fix. */
105
+ export function installChromium(
106
+ host: BrowserHost = thisBrowserHost(),
107
+ run: (command: string) => boolean = runInShell
108
+ ): void {
109
+ if (!hasChromium(host)) run(chromiumInstallCommand(host));
110
+ }
111
+
86
112
  interface PalInstall {
87
113
  pal: string | null;
88
114
  pkg: string;
@@ -136,13 +162,7 @@ export function environmentFindings(rtk: ToolCheck): Finding[] {
136
162
  pkg: palPkg(),
137
163
  repoMode: isRepoMode(),
138
164
  }),
139
- playwrightFinding({
140
- browsersPath: playwrightBrowsersPath(),
141
- playwright: installedPlaywright(),
142
- platform: process.platform,
143
- arch: process.arch,
144
- osRelease: readOsRelease(),
145
- }),
165
+ playwrightFinding(thisBrowserHost()),
146
166
  rtkFinding(rtk),
147
167
  ...versionControlFindings(),
148
168
  ];
package/src/cli/index.ts CHANGED
@@ -50,10 +50,10 @@ import { inference, previewInferenceRoute } from "../hooks/lib/inference";
50
50
  import { logDebug } from "../hooks/lib/log";
51
51
  import { ensureRegistered, writeRegistryEntry } from "../hooks/lib/machine";
52
52
  import { palHome, palPkg, paths, platform, toPath } from "../hooks/lib/paths";
53
- import { telosStatus } from "../hooks/lib/telos-topics";
54
- import { log } from "../targets/lib";
53
+ import { log, narrateSteps } from "../targets/lib";
55
54
  import { builtinToolVerbs, runBuiltinTool } from "./builtin-tools";
56
55
  import { type DoctorResult, detectAgents } from "./doctor/agents";
56
+ import { installChromium } from "./doctor/environment";
57
57
  import { runDoctor } from "./doctor/run";
58
58
  import { findSessionAgent, NO_SESSION_AGENT_MESSAGE } from "./session-agent";
59
59
 
@@ -155,7 +155,7 @@ async function runCli(command: string | undefined, args: string[]) {
155
155
  break;
156
156
  case "install":
157
157
  banner();
158
- await install(resolveTargets(args));
158
+ process.exit(await install(resolveTargets(args), args));
159
159
  break;
160
160
  case "uninstall":
161
161
  await uninstall(args);
@@ -265,24 +265,10 @@ function banner() {
265
265
  console.log("");
266
266
  console.log(" ╔═══════════════════════════════════╗");
267
267
  console.log(" ║ PAL — Portable Agent Layer ║");
268
- console.log(" ║ Non-destructive · Modular ║");
269
268
  console.log(" ╚═══════════════════════════════════╝");
270
269
  console.log("");
271
270
  }
272
271
 
273
- /**
274
- * Install asks for identity and stops. TELOS is a conversation, not a form: a
275
- * user who has just installed a tool cannot yet say what they want from it, and
276
- * answers given under that pressure are worse than none.
277
- */
278
- function pointAtOnboarding(): void {
279
- const unanswered = telosStatus().filter((topic) => topic.priority && !topic.answered);
280
- if (unanswered.length === 0) return;
281
- log.info(
282
- `PAL knows nothing about you yet (${unanswered.length} topics open). When you have half an hour, ask your agent to onboard you.`
283
- );
284
- }
285
-
286
272
  function showHelp() {
287
273
  console.log(`
288
274
  Usage:
@@ -290,8 +276,9 @@ function showHelp() {
290
276
  pal cli <command> [options] Admin commands
291
277
 
292
278
  Admin commands:
293
- pal cli init [--claude] [--opencode] [--cursor] [--codex] Scaffold and install (default: all)
294
- pal cli install [--claude] [--opencode] [--cursor] [--codex] Register hooks for targets
279
+ pal cli init [--claude] [--opencode] [--cursor] [--codex] Scaffold and install (default: all), then run the doctor
280
+ pal cli install [--claude] [--opencode] [--cursor] [--codex] Register hooks for targets, then run the doctor
281
+ Add --verbose to either for the step-by-step log
295
282
  pal cli uninstall [--claude] [--opencode] [--cursor] [--codex] Remove hooks for targets
296
283
  pal cli update Update PAL (git pull or npm update)
297
284
  pal cli export [path] [--dry-run] Export state to zip
@@ -486,12 +473,10 @@ async function init(args: string[]) {
486
473
  const { scaffoldTelos } = await import("../targets/lib");
487
474
 
488
475
  banner();
476
+ narrateSteps(args.includes("--verbose"));
489
477
 
490
478
  const health = detectAgents();
491
- runDoctor([], health);
492
- if (!health.hasAgent) {
493
- process.exit(1);
494
- }
479
+ if (!health.hasAgent) process.exit(runDoctor([], health));
495
480
 
496
481
  const home = palHome();
497
482
  log.info(`Creating PAL home at ${home}`);
@@ -506,7 +491,7 @@ async function init(args: string[]) {
506
491
 
507
492
  // Auto-detect available targets
508
493
  const targets = resolveTargets(args, health);
509
- await install(targets);
494
+ process.exit(await install(targets, args));
510
495
  }
511
496
 
512
497
  /**
@@ -522,25 +507,28 @@ function runQuietly(cmd: string, args: string[], cwd: string): number | null {
522
507
  return r.status;
523
508
  }
524
509
 
525
- async function install(targets: Targets) {
526
- // Ensure dependencies are installed
510
+ function targetInstallers(): [keyof Targets, string, () => Promise<unknown>][] {
511
+ return [
512
+ ["claude", "Claude Code", () => import("../targets/claude/install")],
513
+ ["opencode", "opencode", () => import("../targets/opencode/install")],
514
+ ["cursor", "Cursor", () => import("../targets/cursor/install")],
515
+ ["copilot", "Copilot", () => import("../targets/copilot/install")],
516
+ ["codex", "Codex", () => import("../targets/codex/install")],
517
+ ];
518
+ }
519
+
520
+ async function install(targets: Targets, args: string[]): Promise<number> {
521
+ narrateSteps(args.includes("--verbose"));
527
522
  const pkg = palPkg();
528
523
  if (runQuietly("bun", ["install", "--frozen-lockfile"], pkg) !== 0) {
529
524
  log.warn("bun install failed — continuing anyway, but hooks may not work");
530
525
  }
531
526
 
532
- // Fetch the Chromium build Playwright uses for PDF rendering (create-pdf skill).
533
- // Idempotent — skipped if already cached. Skipped entirely under PAL_SKIP_BROWSER_INSTALL=1
534
- // (used by tests to avoid a ~150MB download on every run).
527
+ const { applyPendingMigrations } = await import("./migrate");
528
+ for (const line of applyPendingMigrations()) log.info(line);
529
+
535
530
  // Uses `bun x` (not `bunx`) for Windows compatibility — bunx resolves unreliably under cmd.exe.
536
- if (process.env.PAL_SKIP_BROWSER_INSTALL !== "1") {
537
- const pw = runQuietly("bun", ["x", "playwright", "install", "chromium"], pkg);
538
- if (pw !== 0) {
539
- log.warn(
540
- `playwright install chromium failed (exit ${pw}) — create-pdf and consulting-report skills won't work. Retry manually: bun x playwright install chromium`
541
- );
542
- }
543
- }
531
+ if (process.env.PAL_SKIP_BROWSER_INSTALL !== "1") installChromium();
544
532
 
545
533
  // Scaffold TELOS + PAL settings, then prompt for missing identity
546
534
  const { scaffoldTelos, scaffoldPalSettings, copyPalDocs, generateSkillIndex } =
@@ -553,7 +541,6 @@ async function install(targets: Targets) {
553
541
  await promptIdentity();
554
542
  await promptAttribution();
555
543
  await promptAutoUpdate();
556
- pointAtOnboarding();
557
544
 
558
545
  // Registers the label loadActor derives, so it travels on the next export.
559
546
  const { ensureActorRegistered } = await import("../hooks/lib/actor");
@@ -566,34 +553,10 @@ async function install(targets: Targets) {
566
553
  const palDocsCount = copyPalDocs();
567
554
  regenerateIfNeeded();
568
555
 
569
- if (targets.claude) {
570
- console.log("━━━ Claude Code ━━━");
571
- await import("../targets/claude/install");
572
- console.log("");
573
- }
574
-
575
- if (targets.opencode) {
576
- console.log("━━━ opencode ━━━");
577
- await import("../targets/opencode/install");
578
- console.log("");
579
- }
580
-
581
- if (targets.cursor) {
582
- console.log("━━━ Cursor ━━━");
583
- await import("../targets/cursor/install");
584
- console.log("");
585
- }
586
-
587
- if (targets.copilot) {
588
- console.log("━━━ Copilot ━━━");
589
- await import("../targets/copilot/install");
590
- console.log("");
591
- }
592
-
593
- if (targets.codex) {
594
- console.log("━━━ Codex ━━━");
595
- await import("../targets/codex/install");
596
- console.log("");
556
+ for (const [target, label, installTarget] of targetInstallers()) {
557
+ if (!targets[target]) continue;
558
+ log.heading(label);
559
+ await installTarget();
597
560
  }
598
561
 
599
562
  // The rest of the shared work reads what the installers just wrote: the index
@@ -608,7 +571,8 @@ async function install(targets: Targets) {
608
571
 
609
572
  await refreshControlRoom();
610
573
 
611
- log.success("Done. Existing config was preserved — only new entries were added.");
574
+ if (args.includes("--verbose")) console.log("");
575
+ return runDoctor(args);
612
576
  }
613
577
 
614
578
  /**
@@ -912,7 +876,7 @@ async function update() {
912
876
  clearUpdateCache();
913
877
 
914
878
  log.info("Reinstalling...");
915
- await install(resolveTargets([]));
879
+ process.exit(await install(resolveTargets([]), []));
916
880
  }
917
881
 
918
882
  function cliDebug(args: string[]) {
@@ -0,0 +1,118 @@
1
+ import { existsSync, lstatSync, readlinkSync, renameSync, unlinkSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { paths, platform } from "../hooks/lib/paths";
4
+ import { readJson, writeJson } from "../targets/lib";
5
+
6
+ /** Something an older PAL wrote that the current one neither writes nor reads. */
7
+ export interface Leftover {
8
+ id: string;
9
+ description: string;
10
+ find(): string[];
11
+ remove(): void;
12
+ }
13
+
14
+ type Json = Record<string, unknown>;
15
+
16
+ const claudeSettings = () => resolve(platform.claudeDir(), "settings.json");
17
+ const palSettings = () => resolve(paths.memory(), "pal-settings.json");
18
+
19
+ function isPathScopedFileToolRule(rule: string): boolean {
20
+ return /^(?:Grep|Glob)\(/.test(rule);
21
+ }
22
+
23
+ function claudeAllowRules(): string[] {
24
+ const settings = readJson<Json>(claudeSettings(), {});
25
+ const allow = (settings.permissions as { allow?: unknown } | undefined)?.allow;
26
+ return Array.isArray(allow)
27
+ ? allow.filter((r): r is string => typeof r === "string")
28
+ : [];
29
+ }
30
+
31
+ const claudeFileToolRules: Leftover = {
32
+ id: "claude-file-tool-rules",
33
+ description:
34
+ "Remove Grep()/Glob() allow rules older templates wrote — Claude Code ignores them and warns on every prompt",
35
+ find: () => claudeAllowRules().filter(isPathScopedFileToolRule),
36
+ remove() {
37
+ const settings = readJson<Json>(claudeSettings(), {});
38
+ const permissions = settings.permissions as { allow: string[] };
39
+ permissions.allow = permissions.allow.filter(
40
+ (rule) => !isPathScopedFileToolRule(rule)
41
+ );
42
+ writeJson(claudeSettings(), settings);
43
+ },
44
+ };
45
+
46
+ function startupFiles(): string[] {
47
+ const settings = readJson<Json>(palSettings(), {});
48
+ const files = (settings.loadAtStartup as { files?: unknown } | undefined)?.files;
49
+ return Array.isArray(files)
50
+ ? files.filter((f): f is string => typeof f === "string")
51
+ : [];
52
+ }
53
+
54
+ const isProjectsFile = (file: string) => file.endsWith("PROJECTS.md");
55
+
56
+ const startupProjectsFile: Leftover = {
57
+ id: "startup-projects-md",
58
+ description:
59
+ "Stop loading PROJECTS.md at startup — projects moved to the project store",
60
+ find: () => startupFiles().filter(isProjectsFile),
61
+ remove() {
62
+ const settings = readJson<Json>(palSettings(), {});
63
+ const startup = settings.loadAtStartup as { files: string[] };
64
+ startup.files = startup.files.filter((file) => !isProjectsFile(file));
65
+ writeJson(palSettings(), settings);
66
+ },
67
+ };
68
+
69
+ const oldOpencodePlugin = () =>
70
+ resolve(platform.opencodeDir(), "plugins", "pai-plugin.ts");
71
+
72
+ const opencodePluginOldName: Leftover = {
73
+ id: "opencode-pai-plugin",
74
+ description: "Remove the opencode plugin under its old name, pai-plugin.ts",
75
+ find: () => (existsSync(oldOpencodePlugin()) ? [oldOpencodePlugin()] : []),
76
+ remove: () => unlinkSync(oldOpencodePlugin()),
77
+ };
78
+
79
+ const oldCopilotInstructions = () =>
80
+ resolve(platform.copilotDir(), "copilot-instructions.md");
81
+
82
+ function linksToAgentsMd(path: string): boolean {
83
+ try {
84
+ return lstatSync(path).isSymbolicLink() && readlinkSync(path).includes("AGENTS.md");
85
+ } catch {
86
+ return false;
87
+ }
88
+ }
89
+
90
+ const copilotInstructionsLink: Leftover = {
91
+ id: "copilot-instructions-link",
92
+ description:
93
+ "Remove the copilot-instructions.md link to AGENTS.md — Copilot now reads PAL's own instruction files",
94
+ find: () =>
95
+ linksToAgentsMd(oldCopilotInstructions()) ? [oldCopilotInstructions()] : [],
96
+ remove: () => unlinkSync(oldCopilotInstructions()),
97
+ };
98
+
99
+ const debugLog = () => resolve(paths.debug(), "debug.log");
100
+
101
+ const debugLogPrev: Leftover = {
102
+ id: "debug-log-prev",
103
+ description: "Rename the old single rotated debug log, debug.log.prev, to debug.log.1",
104
+ find: () => (existsSync(`${debugLog()}.prev`) ? [`${debugLog()}.prev`] : []),
105
+ remove() {
106
+ const prev = `${debugLog()}.prev`;
107
+ if (existsSync(`${debugLog()}.1`)) unlinkSync(prev);
108
+ else renameSync(prev, `${debugLog()}.1`);
109
+ },
110
+ };
111
+
112
+ export const LEFTOVERS: Leftover[] = [
113
+ claudeFileToolRules,
114
+ startupProjectsFile,
115
+ opencodePluginOldName,
116
+ copilotInstructionsLink,
117
+ debugLogPrev,
118
+ ];
@@ -1,13 +1,13 @@
1
1
  /**
2
- * pal cli migrate — versioned, non-destructive data migrations.
2
+ * pal cli migrate — the only place PAL knows about its own past.
3
3
  *
4
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
5
+ * check() — reads what is on disk, so it is safe to call on every install
6
+ * run() — applies it; user data is moved, never deleted, while leftovers
7
+ * PAL itself wrote and no longer reads are removed
7
8
  *
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.
9
+ * Retiring a setting, hook, file or name means adding a migration that removes
10
+ * its leftovers. Install runs pending migrations first; the doctor lists them.
11
11
  */
12
12
 
13
13
  import {
@@ -40,6 +40,7 @@ import {
40
40
  slugify,
41
41
  } from "../tools/knowledge/lib";
42
42
  import { readThreads, type Thread, writeThreads } from "../tools/lib/thread";
43
+ import { LEFTOVERS, type Leftover } from "./leftovers";
43
44
 
44
45
  // ── Types ─────────────────────────────────────────────────────────
45
46
 
@@ -715,6 +716,29 @@ const v7RetiredSettingsKeys: Migration = {
715
716
  },
716
717
  };
717
718
 
719
+ function leftoverMigration(leftover: Leftover): Migration {
720
+ return {
721
+ id: leftover.id,
722
+ description: leftover.description,
723
+ movesSources: true,
724
+ check() {
725
+ const found = leftover.find();
726
+ return { pending: found.length > 0, detail: found.join(", ") || undefined };
727
+ },
728
+ run(dryRun = false): MigrationResult {
729
+ const found = leftover.find();
730
+ if (found.length === 0) return { migrated: 0, skipped: 0, results: [] };
731
+ if (!dryRun) leftover.remove();
732
+ const verb = dryRun ? "would remove" : "removed";
733
+ return {
734
+ migrated: found.length,
735
+ skipped: 0,
736
+ results: [`${verb} ${found.join(", ")}`],
737
+ };
738
+ },
739
+ };
740
+ }
741
+
718
742
  const MIGRATIONS: Migration[] = [
719
743
  v1Projects,
720
744
  v2ThreadsToIsc,
@@ -723,6 +747,7 @@ const MIGRATIONS: Migration[] = [
723
747
  v5AttributionKeys,
724
748
  v6HistorySlugs,
725
749
  v7RetiredSettingsKeys,
750
+ ...LEFTOVERS.map(leftoverMigration),
726
751
  ];
727
752
 
728
753
  // ── Public API ────────────────────────────────────────────────────
@@ -733,6 +758,13 @@ interface PendingMigration {
733
758
  detail?: string;
734
759
  }
735
760
 
761
+ /** Install runs these first, so one install leaves nothing an older PAL wrote. */
762
+ export function applyPendingMigrations(): string[] {
763
+ return MIGRATIONS.filter((m) => m.check().pending).flatMap((m) =>
764
+ m.run(false).results.map((line) => `${m.id}: ${line}`)
765
+ );
766
+ }
767
+
736
768
  /** Returns migrations that have pending work. Used by doctor. */
737
769
  export function checkPendingMigrations(): PendingMigration[] {
738
770
  return MIGRATIONS.flatMap((m) => {
@@ -36,22 +36,10 @@ function timestamp(): string {
36
36
  * .MAX_ROTATED (oldest). Each rotation shifts .N-1 → .N, drops the oldest,
37
37
  * and renames the current log to .1. Total disk footprint bounded at
38
38
  * (MAX_ROTATED + 1) * MAX_LOG_SIZE ≈ 300KB.
39
- *
40
- * Migrates legacy .prev → .1 on first new rotation so existing history
41
- * survives the format change.
42
39
  */
43
40
  function rotateIfNeeded(path: string): void {
44
41
  try {
45
42
  if (!existsSync(path) || statSync(path).size <= MAX_LOG_SIZE) return;
46
- // Backward-compat migration: legacy .prev was the single old rotation file.
47
- const legacyPrev = `${path}.prev`;
48
- if (existsSync(legacyPrev) && !existsSync(`${path}.1`)) {
49
- try {
50
- renameSync(legacyPrev, `${path}.1`);
51
- } catch {
52
- /* ignore */
53
- }
54
- }
55
43
  // Drop the oldest, then shift .N-1 → .N for N from MAX_ROTATED down to 2.
56
44
  const oldest = `${path}.${MAX_ROTATED}`;
57
45
  if (existsSync(oldest)) {
@@ -99,7 +87,7 @@ function logFilesOldestFirst(): string[] {
99
87
  { length: MAX_ROTATED },
100
88
  (_, i) => `${path}.${MAX_ROTATED - i}`
101
89
  );
102
- return [...rotated, `${path}.prev`, path];
90
+ return [...rotated, path];
103
91
  }
104
92
 
105
93
  function readAllLogs(): string {
@@ -4,7 +4,7 @@
4
4
  * and the VS Code chat.instructionsFilesLocations entry.
5
5
  */
6
6
 
7
- import { copyFileSync, existsSync, lstatSync, readlinkSync, unlinkSync } from "node:fs";
7
+ import { copyFileSync, existsSync, unlinkSync } from "node:fs";
8
8
  import { resolve } from "node:path";
9
9
  import { platform } from "../../hooks/lib/paths";
10
10
  import {
@@ -63,22 +63,6 @@ log.success(
63
63
  `Removed ${removedInstructions.length} ~/.copilot/instructions/pal-*.instructions.md`
64
64
  );
65
65
 
66
- // --- Backward compat: remove old copilot-instructions.md symlink if present ---
67
- const legacyPath = resolve(COPILOT_DIR, "copilot-instructions.md");
68
- if (existsSync(legacyPath)) {
69
- try {
70
- if (
71
- lstatSync(legacyPath).isSymbolicLink() &&
72
- readlinkSync(legacyPath).includes("AGENTS.md")
73
- ) {
74
- unlinkSync(legacyPath);
75
- log.success("Removed legacy copilot-instructions.md symlink");
76
- }
77
- } catch {
78
- /* ignore */
79
- }
80
- }
81
-
82
66
  // --- Remove ~/.copilot/instructions entry from VS Code settings ---
83
67
  const vsSettingsPath = vscodeSettingsFile();
84
68
  if (vsSettingsPath && existsSync(vsSettingsPath)) {
@@ -27,9 +27,21 @@ function runningUnderTest(): boolean {
27
27
  return process.env.PAL_TEST_SANDBOX === "1";
28
28
  }
29
29
 
30
+ let narrating = true;
31
+
32
+ /** Install's step log is for --verbose; the doctor report that follows carries the result. */
33
+ export function narrateSteps(on: boolean): void {
34
+ narrating = on;
35
+ }
36
+
37
+ function narrate(line: string): void {
38
+ if (narrating) console.log(line);
39
+ }
40
+
30
41
  export const log = {
31
- info: (msg: string) => console.log(`\x1b[34m[pal]\x1b[0m ${msg}`),
32
- success: (msg: string) => console.log(`\x1b[32m[pal]\x1b[0m ${msg}`),
42
+ info: (msg: string) => narrate(`\x1b[34m[pal]\x1b[0m ${msg}`),
43
+ success: (msg: string) => narrate(`\x1b[32m[pal]\x1b[0m ${msg}`),
44
+ heading: (label: string) => narrate(`\n━━━ ${label} ━━━`),
33
45
  warn: (msg: string) => console.log(`\x1b[33m[pal]\x1b[0m ${msg}`),
34
46
  error: (msg: string) => console.error(`\x1b[31m[pal]\x1b[0m ${msg}`),
35
47
 
@@ -42,7 +54,7 @@ export const log = {
42
54
  */
43
55
  detail: (msg: string) => {
44
56
  if (runningUnderTest()) return;
45
- console.log(`\x1b[34m[pal]\x1b[0m ${msg}`);
57
+ narrate(`\x1b[34m[pal]\x1b[0m ${msg}`);
46
58
  },
47
59
  };
48
60
 
@@ -199,7 +211,7 @@ export function mergeSettings(existing: Settings, template: Settings): Settings
199
211
  }
200
212
  }
201
213
 
202
- // Merge permissions.allow (deduplicate), then drop deprecated entries.
214
+ // Merge permissions.allow (deduplicate)
203
215
  if (template.permissions?.allow) {
204
216
  result.permissions ??= {};
205
217
  result.permissions.allow ??= [];
@@ -209,15 +221,6 @@ export function mergeSettings(existing: Settings, template: Settings): Settings
209
221
  }
210
222
  }
211
223
  }
212
- // Strip ineffective Grep(...)/Glob(...) rules left by older templates. Claude Code
213
- // governs Grep/Glob via Read(...) rules, so a path-scoped Grep()/Glob() entry never
214
- // matches and Claude Code warns about it on every prompt. Read(//*) already covers them.
215
- if (result.permissions?.allow) {
216
- result.permissions.allow = result.permissions.allow.filter(
217
- (perm) => !isIneffectiveFileToolRule(perm)
218
- );
219
- }
220
-
221
224
  // Merge skillOverrides (object with skill name keys, add if not present)
222
225
  if (template.skillOverrides && typeof template.skillOverrides === "object") {
223
226
  result.skillOverrides ??= {};
@@ -476,15 +479,6 @@ function isOutdatedPalHook(cmd: string, palCanonical: Set<string>): boolean {
476
479
  return palCanonical.has(canonicalPalCmd(cmd)) || runsRemovedPalHook(cmd);
477
480
  }
478
481
 
479
- /**
480
- * True for path-scoped Grep()/Glob() allow rules, which Claude Code cannot honor —
481
- * it resolves Grep/Glob permission through Read(...) rules, so these never match and
482
- * trigger a warning on every prompt. Bare "Grep"/"Glob" tool allows are left intact.
483
- */
484
- function isIneffectiveFileToolRule(perm: string): boolean {
485
- return /^(?:Grep|Glob)\(/.test(perm);
486
- }
487
-
488
482
  export function loadCodexHooksTemplate(
489
483
  templatePath: string,
490
484
  pkgRoot: string
@@ -685,21 +679,6 @@ export function scaffoldPalSettings(): void {
685
679
  copyFileSync(src, dst);
686
680
  log.info("Created pal-settings.json from template");
687
681
  }
688
-
689
- // Strip deprecated loadAtStartup.files entries from existing installs.
690
- // mergeSettings only adds, never removes — deprecated entries persist indefinitely otherwise.
691
- try {
692
- const raw = JSON.parse(readFileSync(dst, "utf-8"));
693
- const files: string[] = raw?.loadAtStartup?.files ?? [];
694
- const cleaned = files.filter((f: string) => !f.endsWith("PROJECTS.md"));
695
- if (cleaned.length !== files.length) {
696
- raw.loadAtStartup.files = cleaned;
697
- writeFileSync(dst, `${JSON.stringify(raw, null, 2)}\n`, "utf-8");
698
- log.info("Removed deprecated PROJECTS.md from loadAtStartup.files");
699
- }
700
- } catch {
701
- /* non-fatal — malformed settings left as-is */
702
- }
703
682
  }
704
683
 
705
684
  // --- PAL docs (modular context routing files) ---
@@ -3,14 +3,7 @@
3
3
  * Deploys plugin, installs skills, generates AGENTS.md.
4
4
  */
5
5
 
6
- import {
7
- existsSync,
8
- mkdirSync,
9
- readFileSync,
10
- statSync,
11
- unlinkSync,
12
- writeFileSync,
13
- } from "node:fs";
6
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
14
7
  import { resolve } from "node:path";
15
8
  import { palPkg, platform } from "../../hooks/lib/paths";
16
9
  import { getSemiStaticSources } from "../../hooks/lib/semi-static";
@@ -22,11 +15,7 @@ const OC_PLUGINS_DIR = resolve(OC_GLOBAL_DIR, "plugins");
22
15
 
23
16
  mkdirSync(OC_PLUGINS_DIR, { recursive: true });
24
17
 
25
- // --- 1. Deploy plugin (clean up legacy filename) ---
26
- const legacyPlugin = resolve(OC_PLUGINS_DIR, "pai-plugin.ts");
27
- if (existsSync(legacyPlugin)) {
28
- unlinkSync(legacyPlugin);
29
- }
18
+ // --- 1. Deploy plugin ---
30
19
  const pluginSrc = resolve(PKG_ROOT, "src", "targets", "opencode", "plugin.ts");
31
20
  const pluginDst = resolve(OC_PLUGINS_DIR, "pal-plugin.ts");
32
21
  // Embed PKG_ROOT as a hardcoded constant so no env config is needed