portable-agent-layer 0.88.1 → 0.89.1

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 (87) hide show
  1. package/README.md +8 -5
  2. package/assets/agents/gemini-researcher.md +13 -0
  3. package/assets/agents/grok-researcher.md +13 -0
  4. package/assets/agents/multi-perspective-researcher.md +12 -0
  5. package/assets/agents/perplexity-researcher.md +13 -0
  6. package/assets/agents/skill-author.md +17 -0
  7. package/assets/agents/subagent-author.md +18 -1
  8. package/assets/skills/create-subagent/SKILL.md +3 -2
  9. package/assets/skills/create-subagent/authoring-guide.md +16 -0
  10. package/assets/templates/PAL/MEMORY_SYSTEM.md +1 -1
  11. package/assets/templates/PAL/README.md +6 -5
  12. package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +4 -1
  13. package/assets/templates/hooks.antigravity.json +42 -0
  14. package/assets/templates/settings.antigravity.json +44 -0
  15. package/package.json +1 -1
  16. package/src/cli/builtin-tools.ts +28 -23
  17. package/src/cli/doctor/agents.ts +64 -64
  18. package/src/cli/identity.ts +26 -20
  19. package/src/cli/index.ts +109 -312
  20. package/src/cli/knowledge.ts +160 -201
  21. package/src/cli/ledger-command.ts +69 -0
  22. package/src/cli/ledger.ts +20 -101
  23. package/src/cli/personal-context.ts +15 -12
  24. package/src/cli/rule.ts +29 -29
  25. package/src/cli/server.ts +33 -52
  26. package/src/cli/session-agent.ts +7 -9
  27. package/src/cli/skill.ts +73 -64
  28. package/src/cli/subagent.ts +51 -58
  29. package/src/cli/tree.ts +177 -0
  30. package/src/hooks/InvocationContext.ts +26 -0
  31. package/src/hooks/LedgerCommit.ts +4 -2
  32. package/src/hooks/LedgerSnapshot.ts +2 -0
  33. package/src/hooks/SecurityValidator.ts +2 -0
  34. package/src/hooks/StopOrchestrator.ts +12 -2
  35. package/src/hooks/UserPromptOrchestrator.ts +3 -15
  36. package/src/hooks/handlers/context-digests.ts +21 -21
  37. package/src/hooks/handlers/readme-sync.ts +6 -2
  38. package/src/hooks/lib/agent-definition.ts +6 -16
  39. package/src/hooks/lib/agent-registry.ts +88 -0
  40. package/src/hooks/lib/agent.ts +47 -23
  41. package/src/hooks/lib/antigravity-inference.ts +84 -0
  42. package/src/hooks/lib/antigravity-transcript.ts +129 -0
  43. package/src/hooks/lib/claim-check.ts +13 -2
  44. package/src/hooks/lib/claim-log.ts +5 -1
  45. package/src/hooks/lib/claude-md.ts +21 -1
  46. package/src/hooks/lib/context.ts +5 -1
  47. package/src/hooks/lib/export-manifest.ts +23 -0
  48. package/src/hooks/lib/export.ts +1 -24
  49. package/src/hooks/lib/hook-turn.ts +25 -2
  50. package/src/hooks/lib/import-merge.ts +1 -1
  51. package/src/hooks/lib/inference.ts +77 -1
  52. package/src/hooks/lib/invocation-context.ts +99 -0
  53. package/src/hooks/lib/ledger-hook.ts +14 -6
  54. package/src/hooks/lib/models.ts +34 -17
  55. package/src/hooks/lib/paths.ts +39 -6
  56. package/src/hooks/lib/prompt-capture.ts +28 -0
  57. package/src/hooks/lib/readme-sync.ts +3 -31
  58. package/src/hooks/lib/security-gate.ts +10 -3
  59. package/src/hooks/lib/semi-static.ts +13 -3
  60. package/src/hooks/lib/session-context.ts +3 -1
  61. package/src/hooks/lib/stop.ts +1 -0
  62. package/src/hooks/lib/transcript.ts +6 -3
  63. package/src/targets/agent-render.ts +3 -3
  64. package/src/targets/antigravity/install.ts +68 -0
  65. package/src/targets/antigravity/uninstall.ts +66 -0
  66. package/src/targets/lib.ts +81 -64
  67. package/src/targets/opencode/install.ts +2 -0
  68. package/src/targets/opencode/permission.ts +39 -0
  69. package/src/targets/opencode/uninstall.ts +9 -8
  70. package/src/tools/agent/algorithm-reflect.ts +70 -62
  71. package/src/tools/agent/algorithm-synthesize.ts +39 -14
  72. package/src/tools/agent/analyze.ts +31 -36
  73. package/src/tools/agent/handoff-note.ts +51 -54
  74. package/src/tools/agent/interaction.ts +32 -28
  75. package/src/tools/agent/project.ts +300 -287
  76. package/src/tools/agent/relationship-note.ts +40 -36
  77. package/src/tools/agent/relationship-reflect.ts +23 -32
  78. package/src/tools/agent/synthesize.ts +26 -27
  79. package/src/tools/agent/thread.ts +42 -51
  80. package/src/tools/agent/wisdom-frame.ts +45 -38
  81. package/src/tools/control-room/server.ts +0 -2
  82. package/src/tools/control-room/ui/dist/assets/{index-BZMDzWFa.js → index-mFpXpiBj.js} +1 -1
  83. package/src/tools/control-room/ui/dist/index.html +1 -1
  84. package/src/tools/control-room/ui/screens/log.tsx +10 -1
  85. package/src/tools/lib/command.ts +275 -0
  86. package/src/tools/subagent-doctor.ts +74 -9
  87. package/src/tools/token-cost.ts +18 -17
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * One entry here = the only change needed to add a new source across all consumers:
5
5
  * CLAUDE.md @imports, opencode instructions[], Cursor .mdc, Copilot .instructions.md,
6
- * and the session-stop digest writer.
6
+ * Antigravity plugin rules, and the session-stop digest writer.
7
7
  */
8
8
 
9
9
  import { existsSync, readFileSync } from "node:fs";
@@ -22,9 +22,9 @@ interface SemiStaticSource {
22
22
  readonly writesDigest: boolean;
23
23
  /** Returns current content — builds fresh when writesDigest is true, reads the file otherwise. */
24
24
  load(): string;
25
- /** Slug for ~/.cursor/rules/pal-${slug}.mdc and ~/.copilot/instructions/pal-${slug}.instructions.md */
25
+ /** Slug for the pal-${slug} rule/instruction files written for Cursor, Copilot and Antigravity. */
26
26
  readonly slug: string;
27
- /** Human-readable description for Cursor .mdc frontmatter. */
27
+ /** Human-readable description for Cursor .mdc and Antigravity rule frontmatter. */
28
28
  readonly description: string;
29
29
  }
30
30
 
@@ -38,6 +38,16 @@ export function copilotFilename(src: SemiStaticSource): string {
38
38
  return `pal-${src.slug}.instructions.md`;
39
39
  }
40
40
 
41
+ /** Returns the Antigravity plugin rule filename for a source. */
42
+ export function antigravityFilename(src: SemiStaticSource): string {
43
+ return `pal-${src.slug}.md`;
44
+ }
45
+
46
+ /** An Antigravity rule file; a rule without a valid `trigger` is silently discarded. */
47
+ export function antigravityRule(description: string, content: string): string {
48
+ return `---\ntrigger: always_on\ndescription: ${description}\n---\n\n${content}`;
49
+ }
50
+
41
51
  function readFileSafe(path: string): string {
42
52
  try {
43
53
  if (!existsSync(path)) return "";
@@ -7,6 +7,8 @@
7
7
  * context for weeks — and none of it was reachable from a test.
8
8
  */
9
9
 
10
+ import type { AgentName } from "./agent-registry";
11
+
10
12
  export interface ContextEnvelope {
11
13
  /** Whether the agent parses stdout as JSON or reads it as raw text. */
12
14
  kind: "json" | "text";
@@ -31,7 +33,7 @@ export type SessionStartContext = "full" | "without-handoff" | "none";
31
33
  * is resumed. Copilot does not document it, and has shipped a release that
32
34
  * dropped startup context, so it keeps getting the full context on resume.
33
35
  */
34
- const RESUME_KEEPS_CONTEXT: ReadonlySet<string> = new Set(["claude", "codex"]);
36
+ const RESUME_KEEPS_CONTEXT: ReadonlySet<string> = new Set<AgentName>(["claude", "codex"]);
35
37
 
36
38
  /**
37
39
  * After a compaction, CompactRecover restores the last exchange, so the
@@ -43,6 +43,7 @@ export interface StopTurnPayload extends HookTurnPayload {
43
43
  transcriptPath?: string | null;
44
44
  stop_hook_active?: boolean;
45
45
  loop_count?: number;
46
+ executionNum?: number;
46
47
  }
47
48
 
48
49
  type Transcript = ReturnType<typeof readTranscriptFile>;
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { readFileSync } from "node:fs";
7
+ import { antigravityMessage } from "./antigravity-transcript";
7
8
 
8
9
  interface Message {
9
10
  role: string;
@@ -61,7 +62,8 @@ function codexMessage(payload: CodexPayload | undefined): Message | null {
61
62
  // Claude Code tags transcript lines `type: "user"|"assistant"` with the text
62
63
  // under `message.content`. VS Code Copilot's own event log instead uses
63
64
  // `type: "user.message"|"assistant.message"` with a flat `data.content`
64
- // string. Codex nests each message as a `response_item` payload.
65
+ // string. Codex nests each message as a `response_item` payload. Antigravity
66
+ // writes one step per line, `type: "USER_INPUT"|"PLANNER_RESPONSE"|…`.
65
67
  function parseTranscriptEntry(entry: {
66
68
  type?: string;
67
69
  message?: { content?: unknown };
@@ -78,14 +80,15 @@ function parseTranscriptEntry(entry: {
78
80
  const role = entry.type === "user.message" ? "user" : "assistant";
79
81
  return typeof text === "string" && text ? { role, content: text } : null;
80
82
  }
81
- return null;
83
+ return antigravityMessage(entry);
82
84
  }
83
85
 
84
86
  /**
85
87
  * Read an agent transcript JSONL file and extract user/assistant messages.
86
88
  * Supports Claude Code's `{type:"user"|"assistant", message:{content}}` shape
87
89
  * VS Code Copilot's `{type:"user.message"|"assistant.message", data:{content}}` shape,
88
- * and Codex's `{type:"response_item", payload:{type:"message", role, content}}` shape.
90
+ * Codex's `{type:"response_item", payload:{type:"message", role, content}}` shape,
91
+ * and Antigravity's `{type:"USER_INPUT"|"PLANNER_RESPONSE", source, content}` steps.
89
92
  */
90
93
  export function readTranscriptFile(path: string): Message[] {
91
94
  try {
@@ -5,17 +5,17 @@
5
5
 
6
6
  import {
7
7
  type AgentDefinition,
8
- type AgentPlatform,
9
8
  parseAgentDefinition,
10
9
  } from "../hooks/lib/agent-definition";
10
+ import type { AgentName } from "../hooks/lib/agent-registry";
11
11
 
12
12
  const CODEX_OWNED_KEYS = new Set(["name", "description", "developer_instructions"]);
13
13
 
14
- export function agentFileName(stem: string, platform: AgentPlatform): string {
14
+ export function agentFileName(stem: string, platform: AgentName): string {
15
15
  return `${stem}.${platform === "codex" ? "toml" : "md"}`;
16
16
  }
17
17
 
18
- export function renderAgentForPlatform(content: string, platform: AgentPlatform): string {
18
+ export function renderAgentForPlatform(content: string, platform: AgentName): string {
19
19
  const definition = parseAgentDefinition(content);
20
20
  if (platform === "codex") return renderCodexAgent(definition);
21
21
  if (!definition.hasFrontmatter) return content;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * PAL — Antigravity CLI target installer
3
+ * Rules, hooks, skills and subagents live in one plugin, ~/.gemini/config/plugins/pal/.
4
+ * A plugin cannot carry permissions or a status line, so the command allowlist and
5
+ * the statusLine are merged into agy's settings.json; the user's own hooks.json is
6
+ * never touched.
7
+ */
8
+
9
+ import { copyFileSync, existsSync, mkdirSync } from "node:fs";
10
+ import { dirname, resolve } from "node:path";
11
+ import { skillsDirOf } from "../../hooks/lib/agent-registry";
12
+ import { ensureAntigravityRule } from "../../hooks/lib/claude-md";
13
+ import { assets, palPkg, platform } from "../../hooks/lib/paths";
14
+ import {
15
+ addStatuslineConfig,
16
+ copyAgentsForAntigravity,
17
+ copySkills,
18
+ copyStatusline,
19
+ countSkills,
20
+ loadHooksTemplate,
21
+ loadSettingsTemplate,
22
+ log,
23
+ mergeSettings,
24
+ nativeAgentsDir,
25
+ readJson,
26
+ writeJson,
27
+ } from "../lib";
28
+
29
+ const PLUGIN_DIR = platform.antigravityPluginDir();
30
+
31
+ mkdirSync(PLUGIN_DIR, { recursive: true });
32
+ writeJson(resolve(PLUGIN_DIR, "plugin.json"), {
33
+ name: "pal",
34
+ description: "Portable Agent Layer — personal context, skills and hooks",
35
+ });
36
+ log.success(`PAL plugin → ${PLUGIN_DIR}`);
37
+
38
+ ensureAntigravityRule();
39
+ log.success(
40
+ "PAL instructions → rules/pal.md (context rules follow from the shared digests)"
41
+ );
42
+
43
+ const PKG_ROOT = palPkg().replaceAll("\\", "/");
44
+ writeJson(
45
+ resolve(PLUGIN_DIR, "hooks.json"),
46
+ loadHooksTemplate(assets.antigravityHooksTemplate(), PKG_ROOT, "Antigravity CLI")
47
+ );
48
+ log.success("PAL hooks → hooks.json");
49
+
50
+ const SETTINGS = platform.antigravitySettings();
51
+ mkdirSync(dirname(SETTINGS), { recursive: true });
52
+ if (existsSync(SETTINGS)) copyFileSync(SETTINGS, `${SETTINGS}.bak.${Date.now()}`);
53
+ writeJson(
54
+ SETTINGS,
55
+ addStatuslineConfig(
56
+ mergeSettings(
57
+ readJson(SETTINGS, {}),
58
+ loadSettingsTemplate(assets.antigravitySettingsTemplate(), PKG_ROOT)
59
+ ),
60
+ "antigravity"
61
+ )
62
+ );
63
+ log.success("PAL command allowlist and statusLine → antigravity-cli/settings.json");
64
+ copyStatusline("antigravity");
65
+
66
+ copySkills(skillsDirOf("antigravity"));
67
+ const agentCount = copyAgentsForAntigravity(nativeAgentsDir("antigravity"));
68
+ log.success(`${countSkills()} skills · ${agentCount} agents → ${PLUGIN_DIR}`);
@@ -0,0 +1,66 @@
1
+ /**
2
+ * PAL — Antigravity CLI uninstaller
3
+ * Removes the PAL plugin, ~/.gemini/config/plugins/pal/, exactly the allowlist
4
+ * entries PAL merged into agy's settings.json, and PAL's statusLine and script. Skill links are unlinked first so
5
+ * removing the folder never walks into the skills they point at.
6
+ */
7
+
8
+ import {
9
+ copyFileSync,
10
+ existsSync,
11
+ lstatSync,
12
+ readdirSync,
13
+ rmSync,
14
+ unlinkSync,
15
+ } from "node:fs";
16
+ import { resolve } from "node:path";
17
+ import { skillsDirOf } from "../../hooks/lib/agent-registry";
18
+ import { assets, palPkg, platform } from "../../hooks/lib/paths";
19
+ import {
20
+ loadSettingsTemplate,
21
+ log,
22
+ readJson,
23
+ removeStatusline,
24
+ removeStatuslineConfig,
25
+ unmergeSettings,
26
+ writeJson,
27
+ } from "../lib";
28
+
29
+ function unlinkSkillLinks(dir: string): string[] {
30
+ if (!existsSync(dir)) return [];
31
+ const links = readdirSync(dir).filter((name) =>
32
+ lstatSync(resolve(dir, name)).isSymbolicLink()
33
+ );
34
+ for (const name of links) unlinkSync(resolve(dir, name));
35
+ return links;
36
+ }
37
+
38
+ const PLUGIN_DIR = platform.antigravityPluginDir();
39
+
40
+ if (existsSync(PLUGIN_DIR)) {
41
+ const removed = unlinkSkillLinks(skillsDirOf("antigravity"));
42
+ rmSync(PLUGIN_DIR, { recursive: true, force: true });
43
+ log.success(`Removed the PAL plugin and ${removed.length} skill link(s)`);
44
+ } else {
45
+ log.info("No PAL plugin found");
46
+ }
47
+
48
+ const SETTINGS = platform.antigravitySettings();
49
+ if (existsSync(SETTINGS)) {
50
+ copyFileSync(SETTINGS, `${SETTINGS}.bak.${Date.now()}`);
51
+ const template = loadSettingsTemplate(
52
+ assets.antigravitySettingsTemplate(),
53
+ palPkg().replaceAll("\\", "/")
54
+ );
55
+ writeJson(
56
+ SETTINGS,
57
+ removeStatuslineConfig(
58
+ unmergeSettings(readJson(SETTINGS, {}), template),
59
+ "antigravity"
60
+ )
61
+ );
62
+ log.success(
63
+ "Removed the PAL command allowlist and statusLine from antigravity-cli/settings.json"
64
+ );
65
+ }
66
+ removeStatusline("antigravity");
@@ -18,8 +18,13 @@ import {
18
18
  } from "node:fs";
19
19
  import { homedir } from "node:os";
20
20
  import { dirname, isAbsolute, resolve, sep } from "node:path";
21
- import { AGENT_PLATFORMS, type AgentPlatform } from "../hooks/lib/agent-definition";
22
- import { assets, palHome, platform } from "../hooks/lib/paths";
21
+ import {
22
+ AGENT_NAMES,
23
+ AGENT_REGISTRY,
24
+ type AgentName,
25
+ skillsDirOf,
26
+ } from "../hooks/lib/agent-registry";
27
+ import { agentDirOverrides, assets, palHome, platform } from "../hooks/lib/paths";
23
28
  import { declaredTriggers } from "../hooks/lib/skill-triggers";
24
29
  import { agentFileName, renderAgentForPlatform } from "./agent-render";
25
30
 
@@ -844,20 +849,10 @@ function pruneStaleSkillLinks(agentSkillsDir: string): string[] {
844
849
  return removed;
845
850
  }
846
851
 
847
- /**
848
- * Agent skills directories that need a per-skill discovery link.
849
- *
850
- * opencode is intentionally absent: it discovers the whole ~/.pal/skills/ tree
851
- * via the ~/.agents/skills → ~/.pal/skills symlink, so any personal skill is
852
- * picked up without a per-skill link.
853
- */
854
852
  function perSkillAgentDirs(): { agent: string; dir: string }[] {
855
- return [
856
- { agent: "claude", dir: resolve(platform.claudeDir(), "skills") },
857
- { agent: "cursor", dir: resolve(platform.cursorDir(), "skills") },
858
- { agent: "copilot", dir: resolve(platform.copilotDir(), "skills") },
859
- { agent: "codex", dir: resolve(platform.codexDir(), "skills") },
860
- ];
853
+ return AGENT_NAMES.filter(
854
+ (agent) => AGENT_REGISTRY[agent].skills === "own-skills-dir"
855
+ ).map((agent) => ({ agent, dir: skillsDirOf(agent) }));
861
856
  }
862
857
 
863
858
  /**
@@ -890,15 +885,9 @@ export function linkPersonalSkill(name: string): string[] {
890
885
  * PAL_*_DIR vars at a sandbox silently rewires their real setup.
891
886
  */
892
887
  function realAgentRoots(): string[] {
893
- const h = homedir();
894
888
  return [
895
- resolve(h, ".pal"),
896
- resolve(h, ".claude"),
897
- resolve(h, ".cursor"),
898
- resolve(h, ".copilot"),
899
- resolve(h, ".codex"),
900
- resolve(h, ".agents"),
901
- resolve(h, ".config", "opencode"),
889
+ resolve(homedir(), ".pal"),
890
+ ...agentDirOverrides().map(({ realDir }) => realDir),
902
891
  ];
903
892
  }
904
893
 
@@ -917,8 +906,9 @@ function assertInsideTestSandbox(link: string): void {
917
906
  if (!escaped) return;
918
907
  throw new Error(
919
908
  `Refusing to write ${link}: outside the test sandbox (${escaped} is a real agent directory). ` +
920
- "Point PAL_CLAUDE_DIR, PAL_CURSOR_DIR, PAL_COPILOT_DIR, PAL_CODEX_DIR, " +
921
- "PAL_OPENCODE_DIR and PAL_AGENTS_DIR at a temp directory in this test."
909
+ `Point ${agentDirOverrides()
910
+ .map(({ env }) => env)
911
+ .join(", ")} at a temp directory in this test.`
922
912
  );
923
913
  }
924
914
 
@@ -1044,7 +1034,7 @@ const shippedAgentSource = (stem: string) =>
1044
1034
  readFileSync(resolve(assets.agents(), `${stem}.md`), "utf-8");
1045
1035
 
1046
1036
  /** Install agents for a platform into a target directory. Always overwrites. */
1047
- function installAgents(targetDir: string, platform: AgentPlatform): number {
1037
+ function installAgents(targetDir: string, platform: AgentName): number {
1048
1038
  if (!existsSync(assets.agents())) return 0;
1049
1039
 
1050
1040
  mkdirSync(targetDir, { recursive: true });
@@ -1064,7 +1054,7 @@ function installAgents(targetDir: string, platform: AgentPlatform): number {
1064
1054
  }
1065
1055
 
1066
1056
  /** Shipped agents in targetDir that differ from what installAgents would write there. */
1067
- export function staleShippedAgents(targetDir: string, agent: AgentPlatform): string[] {
1057
+ export function staleShippedAgents(targetDir: string, agent: AgentName): string[] {
1068
1058
  if (!existsSync(assets.agents())) return [];
1069
1059
  const isStale = (stem: string) => {
1070
1060
  const installed = resolve(targetDir, agentFileName(stem, agent));
@@ -1078,7 +1068,7 @@ export function staleShippedAgents(targetDir: string, agent: AgentPlatform): str
1078
1068
  }
1079
1069
 
1080
1070
  /** Remove PAL agents from a directory. */
1081
- function uninstallAgents(targetDir: string, platform: AgentPlatform): string[] {
1071
+ function uninstallAgents(targetDir: string, platform: AgentName): string[] {
1082
1072
  if (!existsSync(assets.agents())) return [];
1083
1073
 
1084
1074
  const removed: string[] = [];
@@ -1113,6 +1103,10 @@ export function copyAgentsForCodex(codexAgentsDir: string): number {
1113
1103
  return installAgents(codexAgentsDir, "codex");
1114
1104
  }
1115
1105
 
1106
+ export function copyAgentsForAntigravity(pluginAgentsDir: string): number {
1107
+ return installAgents(pluginAgentsDir, "antigravity");
1108
+ }
1109
+
1116
1110
  export function removeAgentsFromCursor(cursorAgentsDir: string): string[] {
1117
1111
  return uninstallAgents(cursorAgentsDir, "cursor");
1118
1112
  }
@@ -1138,20 +1132,13 @@ const palAgentsStore = () => resolve(palHome(), "agents");
1138
1132
  * written into. An agent counts as installed when its agents directory already
1139
1133
  * exists (mirrors linkPersonalSkill's per-agent gate).
1140
1134
  */
1141
- function personalSubagentTargets(): { agent: AgentPlatform; dir: string }[] {
1142
- return AGENT_PLATFORMS.map((agent) => ({ agent, dir: nativeAgentsDir(agent) }));
1135
+ function personalSubagentTargets(): { agent: AgentName; dir: string }[] {
1136
+ return AGENT_NAMES.map((agent) => ({ agent, dir: nativeAgentsDir(agent) }));
1143
1137
  }
1144
1138
 
1145
1139
  /** The directory each agent reads its subagent definitions from. */
1146
- export function nativeAgentsDir(agent: AgentPlatform): string {
1147
- const home = {
1148
- claude: platform.claudeDir,
1149
- opencode: platform.opencodeDir,
1150
- cursor: platform.cursorDir,
1151
- copilot: platform.copilotDir,
1152
- codex: platform.codexDir,
1153
- }[agent];
1154
- return resolve(home(), "agents");
1140
+ export function nativeAgentsDir(agent: AgentName): string {
1141
+ return resolve(AGENT_REGISTRY[agent].home(), "agents");
1155
1142
  }
1156
1143
 
1157
1144
  /** Names of the subagents PAL ships (assets/agents/*.md). */
@@ -1203,7 +1190,7 @@ export function installPersonalSubagent(name: string): string[] {
1203
1190
  return renders.map(({ agent }) => agent);
1204
1191
  }
1205
1192
 
1206
- function renderForPersonalSubagent(content: string, agent: AgentPlatform): string {
1193
+ function renderForPersonalSubagent(content: string, agent: AgentName): string {
1207
1194
  try {
1208
1195
  return renderAgentForPlatform(content, agent);
1209
1196
  } catch (e) {
@@ -1211,8 +1198,12 @@ function renderForPersonalSubagent(content: string, agent: AgentPlatform): strin
1211
1198
  }
1212
1199
  }
1213
1200
 
1214
- /** Load and resolve the Copilot hooks template, substituting PKG_ROOT */
1215
- export function loadCopilotHooksTemplate(templatePath: string, pkgRoot: string): unknown {
1201
+ /** Any agent's hooks template with PKG_ROOT filled in, parsed. */
1202
+ export function loadHooksTemplate(
1203
+ templatePath: string,
1204
+ pkgRoot: string,
1205
+ agentLabel: string
1206
+ ): unknown {
1216
1207
  const resolved = readFileSync(templatePath, "utf-8").replaceAll(
1217
1208
  "{{PKG_ROOT}}",
1218
1209
  pkgRoot
@@ -1220,37 +1211,64 @@ export function loadCopilotHooksTemplate(templatePath: string, pkgRoot: string):
1220
1211
  try {
1221
1212
  return JSON.parse(resolved);
1222
1213
  } catch (e) {
1223
- throw new Error(`Failed to parse Copilot hooks template at ${templatePath}: ${e}`);
1214
+ throw new Error(
1215
+ `Failed to parse ${agentLabel} hooks template at ${templatePath}: ${e}`
1216
+ );
1224
1217
  }
1225
1218
  }
1226
1219
 
1220
+ /** Load and resolve the Copilot hooks template, substituting PKG_ROOT */
1221
+ export function loadCopilotHooksTemplate(templatePath: string, pkgRoot: string): unknown {
1222
+ return loadHooksTemplate(templatePath, pkgRoot, "Copilot");
1223
+ }
1224
+
1227
1225
  // --- Statusline ---
1228
1226
 
1229
- export type StatuslineTarget = "claude" | "cursor";
1227
+ export type StatuslineTarget = Extract<AgentName, "claude" | "cursor" | "antigravity">;
1228
+
1229
+ interface StatuslineSlot {
1230
+ dir: () => string;
1231
+ /** The script path as the agent's own config spells it, without the extension. */
1232
+ script: string;
1233
+ powershellFlags: string;
1234
+ extra?: Record<string, unknown>;
1235
+ }
1236
+
1237
+ const STATUSLINE: Record<StatuslineTarget, StatuslineSlot> = {
1238
+ claude: {
1239
+ dir: platform.claudeDir,
1240
+ script: "~/.claude/statusline",
1241
+ powershellFlags: "-NoProfile -ExecutionPolicy Bypass",
1242
+ },
1243
+ cursor: {
1244
+ dir: platform.cursorDir,
1245
+ script: "~/.cursor/statusline",
1246
+ powershellFlags: "-NoProfile",
1247
+ extra: { updateIntervalMs: 300, timeoutMs: 2000 },
1248
+ },
1249
+ antigravity: {
1250
+ dir: platform.antigravityCliDir,
1251
+ script: "~/.gemini/antigravity-cli/statusline",
1252
+ powershellFlags: "-NoProfile -ExecutionPolicy Bypass",
1253
+ },
1254
+ };
1230
1255
 
1231
1256
  function statuslineAgentDir(target: StatuslineTarget): string {
1232
- return target === "claude" ? platform.claudeDir() : platform.cursorDir();
1257
+ return STATUSLINE[target].dir();
1233
1258
  }
1234
1259
 
1235
1260
  function statuslineCommand(target: StatuslineTarget): string {
1236
- const isPlatformWin32 = process.platform === "win32";
1237
- if (target === "cursor") {
1238
- return isPlatformWin32
1239
- ? "powershell -NoProfile -File ~/.cursor/statusline.ps1"
1240
- : "~/.cursor/statusline.sh";
1241
- }
1242
- return isPlatformWin32
1243
- ? "powershell -NoProfile -ExecutionPolicy Bypass -File ~/.claude/statusline.ps1"
1244
- : "~/.claude/statusline.sh";
1261
+ const { script, powershellFlags } = STATUSLINE[target];
1262
+ return process.platform === "win32"
1263
+ ? `powershell ${powershellFlags} -File ${script}.ps1`
1264
+ : `${script}.sh`;
1245
1265
  }
1246
1266
 
1247
1267
  function isPalStatuslineCommand(cmd: string, target: StatuslineTarget): boolean {
1248
- return target === "claude"
1249
- ? cmd.includes(".claude/statusline")
1250
- : cmd.includes(".cursor/statusline");
1268
+ return cmd.includes(STATUSLINE[target].script.slice(2));
1251
1269
  }
1252
1270
 
1253
- /** Copy statusline script to ~/.claude/ or ~/.cursor/ for the current platform */
1271
+ /** Copy the statusline script into the agent's config dir for the current platform */
1254
1272
  export function copyStatusline(target: StatuslineTarget = "claude"): boolean {
1255
1273
  const agentDir = statuslineAgentDir(target);
1256
1274
  mkdirSync(agentDir, { recursive: true });
@@ -1282,7 +1300,7 @@ export function copyStatusline(target: StatuslineTarget = "claude"): boolean {
1282
1300
  }
1283
1301
  }
1284
1302
 
1285
- /** Remove statusline script from ~/.claude/ or ~/.cursor/ */
1303
+ /** Remove the statusline script from the agent's config dir */
1286
1304
  export function removeStatusline(target: StatuslineTarget = "claude"): boolean {
1287
1305
  const isPlatformWin32 = process.platform === "win32";
1288
1306
  const scriptName = isPlatformWin32 ? "statusline.ps1" : "statusline.sh";
@@ -1333,8 +1351,7 @@ export function addStatuslineConfig(
1333
1351
  if (!claudeStatuslineNeedsRefresh(cmd)) {
1334
1352
  return settings;
1335
1353
  }
1336
- } else if (!isPalStatuslineCommand(cmd, "cursor")) {
1337
- // Cursor: preserve user-defined statusLine commands
1354
+ } else if (!isPalStatuslineCommand(cmd, target)) {
1338
1355
  return settings;
1339
1356
  }
1340
1357
  }
@@ -1344,13 +1361,13 @@ export function addStatuslineConfig(
1344
1361
  type: "command",
1345
1362
  command,
1346
1363
  padding: 2,
1347
- ...(target === "cursor" ? { updateIntervalMs: 300, timeoutMs: 2000 } : {}),
1364
+ ...STATUSLINE[target].extra,
1348
1365
  };
1349
1366
 
1350
1367
  return settings;
1351
1368
  }
1352
1369
 
1353
- /** Remove statusLine config from settings (PAL-owned only for Cursor) */
1370
+ /** Remove statusLine config from settings (PAL-owned only, except for Claude) */
1354
1371
  export function removeStatuslineConfig(
1355
1372
  settings: Record<string, unknown>,
1356
1373
  target: StatuslineTarget = "claude"
@@ -1366,7 +1383,7 @@ export function removeStatuslineConfig(
1366
1383
  }
1367
1384
 
1368
1385
  const cmd = statusLine.command as string | undefined;
1369
- if (cmd && isPalStatuslineCommand(cmd, "cursor")) {
1386
+ if (cmd && isPalStatuslineCommand(cmd, target)) {
1370
1387
  delete settings.statusLine;
1371
1388
  }
1372
1389
  return settings;
@@ -8,6 +8,7 @@ import { resolve } from "node:path";
8
8
  import { palPkg, platform } from "../../hooks/lib/paths";
9
9
  import { getSemiStaticSources } from "../../hooks/lib/semi-static";
10
10
  import { copyAgentsForOpencode, copySkills, countSkills, log, writeJson } from "../lib";
11
+ import { allowPalHome, palHomePattern } from "./permission";
11
12
 
12
13
  const PKG_ROOT = palPkg();
13
14
  const OC_GLOBAL_DIR = platform.opencodeDir();
@@ -63,4 +64,5 @@ const existingInstructions = Array.isArray(ocConfig.instructions)
63
64
  ? (ocConfig.instructions as string[])
64
65
  : [];
65
66
  ocConfig.instructions = [...new Set([...existingInstructions, ...staticFiles])];
67
+ ocConfig = allowPalHome(ocConfig, palHomePattern());
66
68
  writeFileSync(configPath, `${JSON.stringify(ocConfig, null, 2)}\n`, "utf-8");
@@ -0,0 +1,39 @@
1
+ import { palHome } from "../../hooks/lib/paths";
2
+
3
+ type OpencodeConfig = Record<string, unknown>;
4
+ type Rules = Record<string, string>;
5
+
6
+ export function palHomePattern(): string {
7
+ return `${palHome().replaceAll("\\", "/")}/**`;
8
+ }
9
+
10
+ function externalDirectoryRules(config: OpencodeConfig): Rules {
11
+ const permission = (config.permission ?? {}) as Record<string, unknown>;
12
+ const rules = permission.external_directory;
13
+ if (typeof rules === "string") return { "*": rules };
14
+ return { ...((rules ?? {}) as Rules) };
15
+ }
16
+
17
+ function withExternalDirectoryRules(
18
+ config: OpencodeConfig,
19
+ rules: Rules
20
+ ): OpencodeConfig {
21
+ const permission = { ...((config.permission ?? {}) as Record<string, unknown>) };
22
+ if (Object.keys(rules).length > 0) permission.external_directory = rules;
23
+ else delete permission.external_directory;
24
+ const { permission: _previous, ...rest } = config;
25
+ return Object.keys(permission).length > 0 ? { ...rest, permission } : rest;
26
+ }
27
+
28
+ export function allowPalHome(config: OpencodeConfig, pattern: string): OpencodeConfig {
29
+ const { [pattern]: _previous, ...rules } = externalDirectoryRules(config);
30
+ return withExternalDirectoryRules(config, { ...rules, [pattern]: "allow" });
31
+ }
32
+
33
+ export function removePalHomeAllow(
34
+ config: OpencodeConfig,
35
+ pattern: string
36
+ ): OpencodeConfig {
37
+ const { [pattern]: _removed, ...rules } = externalDirectoryRules(config);
38
+ return withExternalDirectoryRules(config, rules);
39
+ }
@@ -8,6 +8,7 @@ import { resolve } from "node:path";
8
8
  import { platform } from "../../hooks/lib/paths";
9
9
  import { getSemiStaticSources } from "../../hooks/lib/semi-static";
10
10
  import { log, removeAgentsFromOpencode, removePalDocs, removeSkills } from "../lib";
11
+ import { palHomePattern, removePalHomeAllow } from "./permission";
11
12
 
12
13
  const OC_GLOBAL_DIR = platform.opencodeDir() || "";
13
14
 
@@ -58,14 +59,14 @@ try {
58
59
  /* gone */
59
60
  }
60
61
 
61
- // --- Remove PAL entries from config.json instructions[] ---
62
+ // --- Remove PAL entries from config.json instructions[] and permission ---
62
63
  const configPath = resolve(OC_GLOBAL_DIR, "config.json");
63
64
  if (existsSync(configPath) && statSync(configPath).size > 0) {
64
65
  try {
65
- const ocConfig = JSON.parse(readFileSync(configPath, "utf-8")) as Record<
66
- string,
67
- unknown
68
- >;
66
+ const ocConfig = removePalHomeAllow(
67
+ JSON.parse(readFileSync(configPath, "utf-8")) as Record<string, unknown>,
68
+ palHomePattern()
69
+ );
69
70
  if (Array.isArray(ocConfig.instructions)) {
70
71
  const palFiles = new Set(getSemiStaticSources().map((s) => s.path));
71
72
  const filtered = (ocConfig.instructions as string[]).filter(
@@ -76,11 +77,11 @@ if (existsSync(configPath) && statSync(configPath).size > 0) {
76
77
  } else {
77
78
  ocConfig.instructions = filtered;
78
79
  }
79
- writeFileSync(configPath, `${JSON.stringify(ocConfig, null, 2)}\n`, "utf-8");
80
- log.success("Removed PAL entries from config.json instructions[]");
81
80
  }
81
+ writeFileSync(configPath, `${JSON.stringify(ocConfig, null, 2)}\n`, "utf-8");
82
+ log.success("Removed PAL entries from config.json");
82
83
  } catch {
83
- log.warn("Could not clean config.json instructions[] — check manually");
84
+ log.warn("Could not clean config.json — check manually");
84
85
  }
85
86
  }
86
87