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.
- package/README.md +8 -5
- package/assets/agents/gemini-researcher.md +13 -0
- package/assets/agents/grok-researcher.md +13 -0
- package/assets/agents/multi-perspective-researcher.md +12 -0
- package/assets/agents/perplexity-researcher.md +13 -0
- package/assets/agents/skill-author.md +17 -0
- package/assets/agents/subagent-author.md +18 -1
- package/assets/skills/create-subagent/SKILL.md +3 -2
- package/assets/skills/create-subagent/authoring-guide.md +16 -0
- package/assets/templates/PAL/MEMORY_SYSTEM.md +1 -1
- package/assets/templates/PAL/README.md +6 -5
- package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +4 -1
- package/assets/templates/hooks.antigravity.json +42 -0
- package/assets/templates/settings.antigravity.json +44 -0
- package/package.json +1 -1
- package/src/cli/builtin-tools.ts +28 -23
- package/src/cli/doctor/agents.ts +64 -64
- package/src/cli/identity.ts +26 -20
- package/src/cli/index.ts +109 -312
- package/src/cli/knowledge.ts +160 -201
- package/src/cli/ledger-command.ts +69 -0
- package/src/cli/ledger.ts +20 -101
- package/src/cli/personal-context.ts +15 -12
- package/src/cli/rule.ts +29 -29
- package/src/cli/server.ts +33 -52
- package/src/cli/session-agent.ts +7 -9
- package/src/cli/skill.ts +73 -64
- package/src/cli/subagent.ts +51 -58
- package/src/cli/tree.ts +177 -0
- package/src/hooks/InvocationContext.ts +26 -0
- package/src/hooks/LedgerCommit.ts +4 -2
- package/src/hooks/LedgerSnapshot.ts +2 -0
- package/src/hooks/SecurityValidator.ts +2 -0
- package/src/hooks/StopOrchestrator.ts +12 -2
- package/src/hooks/UserPromptOrchestrator.ts +3 -15
- package/src/hooks/handlers/context-digests.ts +21 -21
- package/src/hooks/handlers/readme-sync.ts +6 -2
- package/src/hooks/lib/agent-definition.ts +6 -16
- package/src/hooks/lib/agent-registry.ts +88 -0
- package/src/hooks/lib/agent.ts +47 -23
- package/src/hooks/lib/antigravity-inference.ts +84 -0
- package/src/hooks/lib/antigravity-transcript.ts +129 -0
- package/src/hooks/lib/claim-check.ts +13 -2
- package/src/hooks/lib/claim-log.ts +5 -1
- package/src/hooks/lib/claude-md.ts +21 -1
- package/src/hooks/lib/context.ts +5 -1
- package/src/hooks/lib/export-manifest.ts +23 -0
- package/src/hooks/lib/export.ts +1 -24
- package/src/hooks/lib/hook-turn.ts +25 -2
- package/src/hooks/lib/import-merge.ts +1 -1
- package/src/hooks/lib/inference.ts +77 -1
- package/src/hooks/lib/invocation-context.ts +99 -0
- package/src/hooks/lib/ledger-hook.ts +14 -6
- package/src/hooks/lib/models.ts +34 -17
- package/src/hooks/lib/paths.ts +39 -6
- package/src/hooks/lib/prompt-capture.ts +28 -0
- package/src/hooks/lib/readme-sync.ts +3 -31
- package/src/hooks/lib/security-gate.ts +10 -3
- package/src/hooks/lib/semi-static.ts +13 -3
- package/src/hooks/lib/session-context.ts +3 -1
- package/src/hooks/lib/stop.ts +1 -0
- package/src/hooks/lib/transcript.ts +6 -3
- package/src/targets/agent-render.ts +3 -3
- package/src/targets/antigravity/install.ts +68 -0
- package/src/targets/antigravity/uninstall.ts +66 -0
- package/src/targets/lib.ts +81 -64
- package/src/targets/opencode/install.ts +2 -0
- package/src/targets/opencode/permission.ts +39 -0
- package/src/targets/opencode/uninstall.ts +9 -8
- package/src/tools/agent/algorithm-reflect.ts +70 -62
- package/src/tools/agent/algorithm-synthesize.ts +39 -14
- package/src/tools/agent/analyze.ts +31 -36
- package/src/tools/agent/handoff-note.ts +51 -54
- package/src/tools/agent/interaction.ts +32 -28
- package/src/tools/agent/project.ts +300 -287
- package/src/tools/agent/relationship-note.ts +40 -36
- package/src/tools/agent/relationship-reflect.ts +23 -32
- package/src/tools/agent/synthesize.ts +26 -27
- package/src/tools/agent/thread.ts +42 -51
- package/src/tools/agent/wisdom-frame.ts +45 -38
- package/src/tools/control-room/server.ts +0 -2
- package/src/tools/control-room/ui/dist/assets/{index-BZMDzWFa.js → index-mFpXpiBj.js} +1 -1
- package/src/tools/control-room/ui/dist/index.html +1 -1
- package/src/tools/control-room/ui/screens/log.tsx +10 -1
- package/src/tools/lib/command.ts +275 -0
- package/src/tools/subagent-doctor.ts +74 -9
- 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
|
+
}
|
package/src/hooks/lib/agent.ts
CHANGED
|
@@ -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 =
|
|
25
|
+
export type AgentType = AgentName | "vscode";
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
[
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
}
|
package/src/hooks/lib/context.ts
CHANGED
|
@@ -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 =
|
|
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
|
+
}
|
package/src/hooks/lib/export.ts
CHANGED
|
@@ -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
|
*
|