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
@@ -0,0 +1,88 @@
1
+ /**
2
+ * The one list of agents PAL installs into. Every per-agent union, roster and
3
+ * priority order elsewhere is derived from or checked against this table, so a
4
+ * new agent starts here.
5
+ */
6
+
7
+ import { resolve } from "node:path";
8
+ import { platform } from "./paths";
9
+
10
+ type SkillDiscovery = "own-skills-dir" | "shared-agents-skills";
11
+
12
+ interface AgentEntry {
13
+ label: string;
14
+ binary: string;
15
+ home: () => string;
16
+ skills: SkillDiscovery;
17
+ }
18
+
19
+ export const AGENT_REGISTRY = {
20
+ claude: {
21
+ label: "Claude Code",
22
+ binary: "claude",
23
+ home: platform.claudeDir,
24
+ skills: "own-skills-dir",
25
+ },
26
+ opencode: {
27
+ label: "opencode",
28
+ binary: "opencode",
29
+ home: platform.opencodeDir,
30
+ skills: "shared-agents-skills",
31
+ },
32
+ cursor: {
33
+ label: "Cursor",
34
+ binary: "cursor-agent",
35
+ home: platform.cursorDir,
36
+ skills: "own-skills-dir",
37
+ },
38
+ copilot: {
39
+ label: "Copilot",
40
+ binary: "copilot",
41
+ home: platform.copilotDir,
42
+ skills: "own-skills-dir",
43
+ },
44
+ codex: {
45
+ label: "Codex",
46
+ binary: "codex",
47
+ home: platform.codexDir,
48
+ skills: "own-skills-dir",
49
+ },
50
+ antigravity: {
51
+ label: "Antigravity CLI",
52
+ binary: "agy",
53
+ home: platform.antigravityPluginDir,
54
+ skills: "own-skills-dir",
55
+ },
56
+ } as const satisfies Record<string, AgentEntry>;
57
+
58
+ export type AgentName = keyof typeof AGENT_REGISTRY;
59
+
60
+ export const AGENT_NAMES = Object.keys(AGENT_REGISTRY) as AgentName[];
61
+
62
+ export const INFERENCE_PRIORITY = [
63
+ "claude",
64
+ "codex",
65
+ "opencode",
66
+ "copilot",
67
+ "cursor",
68
+ "antigravity",
69
+ ] as const satisfies readonly AgentName[];
70
+
71
+ export const LAUNCH_PRIORITY = [
72
+ "claude",
73
+ "codex",
74
+ "cursor",
75
+ "copilot",
76
+ "opencode",
77
+ "antigravity",
78
+ ] as const satisfies readonly AgentName[];
79
+
80
+ export function skillsDirOf(agent: AgentName): string {
81
+ const entry: AgentEntry = AGENT_REGISTRY[agent];
82
+ const root = entry.skills === "own-skills-dir" ? entry.home() : platform.agentsDir();
83
+ return resolve(root, "skills");
84
+ }
85
+
86
+ export function isAgentName(value: string): value is AgentName {
87
+ return Object.hasOwn(AGENT_REGISTRY, value);
88
+ }
@@ -14,24 +14,23 @@
14
14
  * on PATH rather than assuming claude.
15
15
  */
16
16
 
17
+ import {
18
+ AGENT_REGISTRY,
19
+ type AgentName,
20
+ INFERENCE_PRIORITY,
21
+ isAgentName,
22
+ } from "./agent-registry";
17
23
  import { findBinaryOnPath } from "./which";
18
24
 
19
- export type AgentType = "claude" | "cursor" | "codex" | "copilot" | "opencode" | "vscode";
25
+ export type AgentType = AgentName | "vscode";
20
26
 
21
- const KNOWN_AGENTS: ReadonlySet<AgentType> = new Set([
22
- "claude",
23
- "cursor",
24
- "codex",
25
- "copilot",
26
- "opencode",
27
- "vscode",
28
- ]);
27
+ function asAgentType(value: string | undefined): AgentType | undefined {
28
+ if (!value) return undefined;
29
+ return value === "vscode" || isAgentName(value) ? value : undefined;
30
+ }
29
31
 
30
32
  function agentFromEnv(): AgentType | undefined {
31
- const explicit = process.env.PAL_AGENT;
32
- return explicit && KNOWN_AGENTS.has(explicit as AgentType)
33
- ? (explicit as AgentType)
34
- : undefined;
33
+ return asAgentType(process.env.PAL_AGENT);
35
34
  }
36
35
 
37
36
  /**
@@ -43,8 +42,7 @@ function agentFromEnv(): AgentType | undefined {
43
42
  */
44
43
  export function agentFromArgv(): AgentType | undefined {
45
44
  const flag = process.argv.find((a) => a.startsWith("--agent="));
46
- const value = flag?.slice("--agent=".length);
47
- return value && KNOWN_AGENTS.has(value as AgentType) ? (value as AgentType) : undefined;
45
+ return asAgentType(flag?.slice("--agent=".length));
48
46
  }
49
47
 
50
48
  /**
@@ -73,17 +71,24 @@ function inClaudeCode(): boolean {
73
71
  return Boolean(process.env.CLAUDE_CODE_ENTRYPOINT);
74
72
  }
75
73
 
74
+ /** Exported into every hook and tool process agy spawns. */
75
+ function inAntigravity(): boolean {
76
+ return Boolean(process.env.ANTIGRAVITY_CONVERSATION_ID);
77
+ }
78
+
76
79
  /**
77
80
  * Which host is running this process, read from what the host itself exported.
78
81
  *
79
82
  * cursor-agent is tested first on purpose: it emulates Claude Code closely
80
83
  * enough to inject CLAUDE_PROJECT_DIR and CLAUDE_CODE_AUTO_COMPACT_WINDOW, so
81
84
  * a CLAUDE_* variable is evidence of Claude Code only once Cursor is ruled out.
85
+ * Antigravity comes last because its variable leaks into any agent it runs.
82
86
  */
83
87
  function agentFromRuntimeEnv(): AgentType | undefined {
84
88
  if (inCursorAgent()) return "cursor";
85
89
  if (inCodex()) return "codex";
86
90
  if (inClaudeCode()) return "claude";
91
+ if (inAntigravity()) return "antigravity";
87
92
  return undefined;
88
93
  }
89
94
 
@@ -117,13 +122,8 @@ export function declaredAgent(): AgentType | undefined {
117
122
  * The CLI each agent spawns for inference, in the order inference.ts routes
118
123
  * them. vscode is absent because it has no CLI of its own — it runs Claude's.
119
124
  */
120
- const AGENT_BINARIES: ReadonlyArray<readonly [AgentType, string]> = [
121
- ["claude", "claude"],
122
- ["codex", "codex"],
123
- ["opencode", "opencode"],
124
- ["copilot", "copilot"],
125
- ["cursor", "cursor-agent"],
126
- ];
125
+ const AGENT_BINARIES: ReadonlyArray<readonly [AgentType, string]> =
126
+ INFERENCE_PRIORITY.map((agent) => [agent, AGENT_REGISTRY[agent].binary] as const);
127
127
 
128
128
  /**
129
129
  * Which agent this machine actually has, for the case where nothing declared
@@ -150,6 +150,7 @@ export const isCursor = () => getActiveAgent() === "cursor";
150
150
  export const isCodex = () => getActiveAgent() === "codex";
151
151
  export const isCopilot = () => getActiveAgent() === "copilot";
152
152
  export const isOpencode = () => getActiveAgent() === "opencode";
153
+ export const isAntigravity = () => getActiveAgent() === "antigravity";
153
154
  const isVscode = () => getActiveAgent() === "vscode";
154
155
 
155
156
  /** Normalized preToolUse request — one shape for every agent's payload. */
@@ -159,6 +160,16 @@ export interface ToolUseRequest {
159
160
  hookEventName?: string;
160
161
  }
161
162
 
163
+ /** Every spelling agents use for the file a write tool changes. */
164
+ export const FILE_TARGET_KEYS = ["file_path", "filePath", "path", "TargetFile"];
165
+
166
+ /** Antigravity's file-writing tools, all of which name their file `TargetFile`. */
167
+ export const ANTIGRAVITY_WRITE_TOOLS = [
168
+ "write_to_file",
169
+ "replace_file_content",
170
+ "multi_replace_file_content",
171
+ ];
172
+
162
173
  function firstString(...values: unknown[]): string | undefined {
163
174
  return values.find((v): v is string => typeof v === "string" && v.length > 0);
164
175
  }
@@ -186,6 +197,14 @@ function toolInputOf(payload: Record<string, unknown>): Record<string, unknown>
186
197
  return firstObject(...candidates) ?? candidates.map(parsedObject).find(Boolean) ?? {};
187
198
  }
188
199
 
200
+ /** Antigravity nests the call as `toolCall: { name, args }`. */
201
+ function nestedToolCall(payload: Record<string, unknown>): ToolUseRequest | null {
202
+ const call = firstObject(payload.toolCall);
203
+ const toolName = firstString(call?.name);
204
+ if (!call || !toolName) return null;
205
+ return { toolName, toolInput: firstObject(call.args) ?? {} };
206
+ }
207
+
189
208
  /**
190
209
  * Normalize a preToolUse payload across agents.
191
210
  *
@@ -198,7 +217,7 @@ export function normalizeToolUse(raw: unknown): ToolUseRequest | null {
198
217
  const payload = firstObject(raw);
199
218
  if (!payload) return null;
200
219
  const toolName = firstString(payload.tool_name, payload.toolName);
201
- if (!toolName) return null;
220
+ if (!toolName) return nestedToolCall(payload);
202
221
  return {
203
222
  toolName,
204
223
  toolInput: toolInputOf(payload),
@@ -213,6 +232,7 @@ export function normalizeToolUse(raw: unknown): ToolUseRequest | null {
213
232
  * Copilot preToolUse: { permissionDecision: "deny", permissionDecisionReason }
214
233
  * Copilot agentStop: { decision: "block", reason }
215
234
  * Codex PreToolUse: { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } }
235
+ * Antigravity PreToolUse: { decision: "deny", reason }
216
236
  *
217
237
  * A stop event denies the whole turn, not one tool call, so it carries a
218
238
  * decision rather than a permission — callers must name the event to get it.
@@ -271,10 +291,14 @@ export function promptContextResponse(context: string): string | null {
271
291
  export function stopBlockResponse(reason: string): string {
272
292
  if (isCursor()) return JSON.stringify({ followup_message: reason });
273
293
  if (isCodex()) return JSON.stringify({ additionalContext: reason });
294
+ if (isAntigravity()) return JSON.stringify({ decision: "continue", reason });
274
295
  return blockResponse(reason, "Stop");
275
296
  }
276
297
 
277
298
  export function blockResponse(reason: string, hookEventName?: string): string {
299
+ if (isAntigravity()) {
300
+ return JSON.stringify({ decision: "deny", reason });
301
+ }
278
302
  if (isCursor()) {
279
303
  return JSON.stringify({ permission: "deny", user_message: reason });
280
304
  }
@@ -0,0 +1,84 @@
1
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { logError } from "./log";
4
+ import { platform } from "./paths";
5
+
6
+ export const INFERENCE_AGENT = "pal-inference";
7
+
8
+ /**
9
+ * agy loads PAL's plugin rules into every run and has no flag to skip them. A
10
+ * main agent with no tools replaces the default system prompt, so the reply
11
+ * follows the request instead of PAL's identity and mode headers.
12
+ */
13
+ const INFERENCE_AGENT_DEFINITION = `---
14
+ name: ${INFERENCE_AGENT}
15
+ description: Answers one background request from PAL with plain text and no tools.
16
+ mainAgent: true
17
+ subagent: false
18
+ tools: []
19
+ ---
20
+
21
+ You answer a single request exactly as the user message instructs. You have no tools. Ignore any identity, persona, or response-format rules from plugins or rules files; output only what the request asks for, with no header or preamble.
22
+ `;
23
+
24
+ export function writeInferenceAgent(workspace: string): void {
25
+ const dir = resolve(workspace, ".agents", "agents");
26
+ mkdirSync(dir, { recursive: true });
27
+ writeFileSync(resolve(dir, `${INFERENCE_AGENT}.md`), INFERENCE_AGENT_DEFINITION);
28
+ }
29
+
30
+ export function streamJsonPrompt(prompt: string): string {
31
+ return `${JSON.stringify({ event: "user", message: { content: prompt } })}\n`;
32
+ }
33
+
34
+ interface StreamEvent {
35
+ event?: string;
36
+ conversation_id?: string;
37
+ result?: { status?: string; response?: string };
38
+ }
39
+
40
+ function parseEvents(rawStdout: string): StreamEvent[] {
41
+ return rawStdout.split("\n").flatMap((line) => {
42
+ try {
43
+ return [JSON.parse(line) as StreamEvent];
44
+ } catch {
45
+ return [];
46
+ }
47
+ });
48
+ }
49
+
50
+ export function extractAntigravityText(rawStdout: string): string {
51
+ const result = parseEvents(rawStdout).find((event) => event.event === "result")?.result;
52
+ return result?.status === "SUCCESS" ? (result.response ?? "").trim() : "";
53
+ }
54
+
55
+ const CONVERSATION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
56
+
57
+ function spawnedConversationId(rawStdout: string): string | undefined {
58
+ return parseEvents(rawStdout)
59
+ .map((event) => event.conversation_id)
60
+ .find((id) => id !== undefined && CONVERSATION_ID.test(id));
61
+ }
62
+
63
+ function conversationFiles(id: string): string[] {
64
+ const cli = platform.antigravityCliDir();
65
+ return [
66
+ resolve(cli, "conversations", `${id}.db`),
67
+ resolve(cli, "brain", id),
68
+ resolve(cli, "annotations", `${id}.pbtxt`),
69
+ resolve(cli, "presence", `${id}.lock`),
70
+ ];
71
+ }
72
+
73
+ /** agy keeps every headless run as a conversation; a background call's is ~460 KB. */
74
+ export function removeSpawnedConversation(rawStdout: string): void {
75
+ const id = spawnedConversationId(rawStdout);
76
+ if (!id) return;
77
+ for (const path of conversationFiles(id)) {
78
+ try {
79
+ rmSync(path, { recursive: true, force: true });
80
+ } catch (err) {
81
+ logError("inference:antigravity-cleanup", err);
82
+ }
83
+ }
84
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Reading Antigravity's own transcript, `transcript_full.jsonl`: one step per
3
+ * line, `{ step_index, source, type, status, content, ... }`.
4
+ */
5
+
6
+ import { readFileSync } from "node:fs";
7
+ import { type HookTurnPayload, hookFinalReply } from "./hook-turn";
8
+
9
+ interface AntigravityToolCall {
10
+ name?: string;
11
+ args?: unknown;
12
+ }
13
+
14
+ interface AntigravityStep {
15
+ source?: string;
16
+ type?: string;
17
+ content?: unknown;
18
+ tool_calls?: AntigravityToolCall[];
19
+ }
20
+
21
+ type ModelText = AntigravityStep & { content: string };
22
+
23
+ export interface AntigravityMessage {
24
+ role: "user" | "assistant";
25
+ content: string;
26
+ }
27
+
28
+ const USER_REQUEST = /<USER_REQUEST>\n?([\s\S]*?)\n?<\/USER_REQUEST>/;
29
+
30
+ function parsedSteps(raw: string): AntigravityStep[] {
31
+ return raw.split("\n").flatMap((line) => {
32
+ try {
33
+ return line.trim() ? [JSON.parse(line) as AntigravityStep] : [];
34
+ } catch {
35
+ return [];
36
+ }
37
+ });
38
+ }
39
+
40
+ function stepsOf(transcriptPath: string): AntigravityStep[] {
41
+ try {
42
+ return parsedSteps(readFileSync(transcriptPath, "utf-8"));
43
+ } catch {
44
+ return [];
45
+ }
46
+ }
47
+
48
+ /** Steps PAL injects come back as USER_INPUT too, under source SYSTEM_SDK. */
49
+ function typedByUser(step: AntigravityStep): boolean {
50
+ return step.type === "USER_INPUT" && step.source === "USER_EXPLICIT";
51
+ }
52
+
53
+ /** The request inside its envelope, without the metadata agy appends to it. */
54
+ function requestText(content: string): string {
55
+ return (USER_REQUEST.exec(content)?.[1] ?? content).trim();
56
+ }
57
+
58
+ /** What the user typed in this step, or null for any step the user did not type. */
59
+ export function typedRequest(step: AntigravityStep): string | null {
60
+ return typedByUser(step) && typeof step.content === "string"
61
+ ? requestText(step.content)
62
+ : null;
63
+ }
64
+
65
+ /** What the model said to the user, as opposed to the thinking and tool calls beside it. */
66
+ function isModelText(step: AntigravityStep): step is ModelText {
67
+ return (
68
+ step.type === "PLANNER_RESPONSE" &&
69
+ typeof step.content === "string" &&
70
+ step.content.trim() !== ""
71
+ );
72
+ }
73
+
74
+ export function toolCallsOf(step: AntigravityStep): AntigravityToolCall[] {
75
+ return Array.isArray(step.tool_calls) ? step.tool_calls : [];
76
+ }
77
+
78
+ /** A step as a turn of the conversation, or null for everything around the turns. */
79
+ export function antigravityMessage(step: AntigravityStep): AntigravityMessage | null {
80
+ const request = typedRequest(step);
81
+ if (request) return { role: "user", content: request };
82
+ return isModelText(step) ? { role: "assistant", content: step.content } : null;
83
+ }
84
+
85
+ /**
86
+ * What the user last typed, or null for a conversation nobody typed into —
87
+ * which is what a subagent's is: it opens on its parent's message instead.
88
+ */
89
+ export function latestUserRequest(transcriptPath: string): string | null {
90
+ const step = stepsOf(transcriptPath).findLast(typedByUser);
91
+ return step ? typedRequest(step) : null;
92
+ }
93
+
94
+ function replyToLatestRequest(steps: AntigravityStep[]): string | undefined {
95
+ const request = steps.findLastIndex(typedByUser);
96
+ return steps.slice(request + 1).findLast(isModelText)?.content;
97
+ }
98
+
99
+ export interface AntigravityStopFields {
100
+ conversationId?: string;
101
+ fullyIdle?: boolean;
102
+ transcriptPath?: string | null;
103
+ }
104
+
105
+ /**
106
+ * A stop that ends no turn of the user's: agy still busy, or a subagent's
107
+ * conversation finishing inside its parent's turn. Only an agy payload carries
108
+ * `conversationId`, so no other agent's transcript is ever read here.
109
+ */
110
+ export function isSideStop(payload: AntigravityStopFields | null): boolean {
111
+ if (!payload?.conversationId) return false;
112
+ if (payload.fullyIdle === false) return true;
113
+ return (
114
+ typeof payload.transcriptPath === "string" &&
115
+ latestUserRequest(payload.transcriptPath) === null
116
+ );
117
+ }
118
+
119
+ /** agy hands its Stop hook no final reply, but by then the transcript holds it. */
120
+ export function withTranscriptReply<T extends AntigravityStopFields & HookTurnPayload>(
121
+ payload: T | null
122
+ ): T | null {
123
+ if (!payload?.conversationId || typeof payload.transcriptPath !== "string") {
124
+ return payload;
125
+ }
126
+ if (hookFinalReply(payload)) return payload;
127
+ const reply = replyToLatestRequest(stepsOf(payload.transcriptPath));
128
+ return reply ? { ...payload, lastAssistantMessage: reply } : payload;
129
+ }
@@ -5,6 +5,7 @@
5
5
  * unreadable turn is never unbacked.
6
6
  */
7
7
 
8
+ import { toolCallsOf, typedRequest } from "./antigravity-transcript";
8
9
  import { isSystemText } from "./prompt-text";
9
10
  import { isCodexInjectedContext } from "./transcript";
10
11
 
@@ -32,6 +33,7 @@ const COMMAND_TOOLS = new Set([
32
33
  "exec",
33
34
  "shell",
34
35
  "exec_command",
36
+ "run_command",
35
37
  ]);
36
38
 
37
39
  export type ClaimVerdict = "none" | "backed" | "unbacked" | "unknown";
@@ -86,6 +88,9 @@ export function isVersionControlClaim(claim: string): boolean {
86
88
 
87
89
  interface TranscriptLine {
88
90
  type?: string;
91
+ source?: string;
92
+ content?: unknown;
93
+ tool_calls?: { name?: string; args?: unknown }[];
89
94
  message?: { content?: unknown };
90
95
  payload?: {
91
96
  type?: string;
@@ -124,7 +129,7 @@ function promptText(entry: TranscriptLine): string | null {
124
129
  .map((b) => b.text ?? "")
125
130
  .join(" ");
126
131
  }
127
- return null;
132
+ return typedRequest(entry);
128
133
  }
129
134
 
130
135
  function isUserPrompt(entry: TranscriptLine): boolean {
@@ -135,6 +140,10 @@ function isUserPrompt(entry: TranscriptLine): boolean {
135
140
  const ran = (input: unknown) => JSON.stringify(input ?? {});
136
141
 
137
142
  function commandsIn(entry: TranscriptLine): string[] {
143
+ if (entry.type === "PLANNER_RESPONSE")
144
+ return toolCallsOf(entry)
145
+ .filter((call) => COMMAND_TOOLS.has(call.name ?? ""))
146
+ .map((call) => ran(call.args));
138
147
  if (entry.type === "assistant")
139
148
  return blocks(entry.message?.content)
140
149
  .filter((b) => b.type === "tool_use" && COMMAND_TOOLS.has(b.name ?? ""))
@@ -148,7 +157,9 @@ function commandsIn(entry: TranscriptLine): string[] {
148
157
  }
149
158
 
150
159
  function isTurnEntry(entry: TranscriptLine): boolean {
151
- return ["user", "assistant", "response_item"].includes(entry.type ?? "");
160
+ return ["user", "assistant", "response_item", "PLANNER_RESPONSE"].includes(
161
+ entry.type ?? ""
162
+ );
152
163
  }
153
164
 
154
165
  /** What each command this turn ran. Null when no line reads as a turn, so an
@@ -73,7 +73,11 @@ function record({ check, commands, session, blocked }: Watched, now: Date) {
73
73
  }
74
74
 
75
75
  export function alreadySentBack(payload: StopTurnPayload): boolean {
76
- return payload.stop_hook_active === true || (payload.loop_count ?? 0) > 0;
76
+ return (
77
+ payload.stop_hook_active === true ||
78
+ (payload.loop_count ?? 0) > 0 ||
79
+ (payload.executionNum ?? 0) > 0
80
+ );
77
81
  }
78
82
 
79
83
  function sendsBack(payload: StopTurnPayload, check: ClaimCheck): boolean {
@@ -21,7 +21,7 @@ import {
21
21
  } from "node:fs";
22
22
  import { dirname, relative, resolve } from "node:path";
23
23
  import { assets, ensureDir, paths, platform } from "./paths";
24
- import { getSemiStaticSources } from "./semi-static";
24
+ import { antigravityRule, getSemiStaticSources } from "./semi-static";
25
25
 
26
26
  const TEMPLATE_PATH = assets.agentsMdTemplate();
27
27
 
@@ -170,12 +170,31 @@ function ensureClaudeCodeMd(): void {
170
170
  }
171
171
  }
172
172
 
173
+ /** Keep the PAL plugin's always-on rule in step with AGENTS.md; only when the plugin is installed. */
174
+ export function ensureAntigravityRule(): void {
175
+ const pluginDir = platform.antigravityPluginDir();
176
+ if (!existsSync(pluginDir)) return;
177
+ const rulePath = resolve(pluginDir, "rules", "pal.md");
178
+ const expected = antigravityRule(
179
+ "PAL — identity, response modes and context routing",
180
+ buildClaudeMd()
181
+ );
182
+ try {
183
+ if (existsSync(rulePath) && readFileSync(rulePath, "utf-8") === expected) return;
184
+ ensureDir(dirname(rulePath));
185
+ writeFileSync(rulePath, expected, "utf-8");
186
+ } catch {
187
+ /* non-fatal */
188
+ }
189
+ }
190
+
173
191
  /** Regenerate AGENTS.md if any source file is newer, write real CLAUDE.md, ensure other symlinks. Returns true if rebuilt. */
174
192
  export function regenerateIfNeeded(): boolean {
175
193
  const { outputPath } = getOutputPaths();
176
194
  if (!needsRebuild()) {
177
195
  ensureSymlinks();
178
196
  ensureClaudeCodeMd();
197
+ ensureAntigravityRule();
179
198
  return false;
180
199
  }
181
200
  ensureDir(dirname(outputPath));
@@ -193,5 +212,6 @@ export function regenerateIfNeeded(): boolean {
193
212
  }
194
213
  }
195
214
  ensureSymlinks();
215
+ ensureAntigravityRule();
196
216
  return true;
197
217
  }
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { existsSync, readFileSync } from "node:fs";
7
7
  import { resolve } from "node:path";
8
+ import type { AgentName } from "./agent-registry";
8
9
  import { resolveAnchor } from "./anchor";
9
10
  import { dueNudgeSections } from "./daily-nudge";
10
11
  import { loadHandoffContext } from "./handoff-context";
@@ -173,7 +174,10 @@ function capSection(text: string, maxChars: number): string {
173
174
  }
174
175
 
175
176
  /** Agent targets — determines which context sections are skipped due to native loading. */
176
- export type AgentTarget = "claude" | "opencode" | "cursor" | "copilot";
177
+ export type AgentTarget = Extract<
178
+ AgentName,
179
+ "claude" | "opencode" | "cursor" | "copilot"
180
+ >;
177
181
 
178
182
  /**
179
183
  * Build the <system-reminder> content for the AI.
@@ -0,0 +1,23 @@
1
+ /** Archive metadata naming the machine that produced it. */
2
+ export const MANIFEST_NAME = "export-manifest.json";
3
+
4
+ export interface ExportManifest {
5
+ machineId: string;
6
+ label: string;
7
+ os: string;
8
+ exportedAt: string;
9
+ fileCount: number;
10
+ }
11
+
12
+ export function buildManifest(
13
+ identity: { id: string; label: string; os: string },
14
+ fileCount: number
15
+ ): ExportManifest {
16
+ return {
17
+ machineId: identity.id,
18
+ label: identity.label,
19
+ os: identity.os,
20
+ exportedAt: new Date().toISOString(),
21
+ fileCount,
22
+ };
23
+ }
@@ -7,6 +7,7 @@ import { existsSync, readdirSync } from "node:fs";
7
7
  import { relative, resolve } from "node:path";
8
8
  import AdmZip from "adm-zip";
9
9
  import { ensureActorRegistered } from "./actor";
10
+ import { buildManifest, MANIFEST_NAME } from "./export-manifest";
10
11
  import { ensureRegistered } from "./machine";
11
12
  import { palHome } from "./paths";
12
13
 
@@ -61,30 +62,6 @@ export function collectExportFiles(): string[] {
61
62
  return files;
62
63
  }
63
64
 
64
- /** Archive metadata naming the machine that produced it. */
65
- export const MANIFEST_NAME = "export-manifest.json";
66
-
67
- export interface ExportManifest {
68
- machineId: string;
69
- label: string;
70
- os: string;
71
- exportedAt: string;
72
- fileCount: number;
73
- }
74
-
75
- export function buildManifest(
76
- identity: { id: string; label: string; os: string },
77
- fileCount: number
78
- ): ExportManifest {
79
- return {
80
- machineId: identity.id,
81
- label: identity.label,
82
- os: identity.os,
83
- exportedAt: new Date().toISOString(),
84
- fileCount,
85
- };
86
- }
87
-
88
65
  /**
89
66
  * Zip the given files and write to outputPath. Returns file count.
90
67
  *