portable-agent-layer 0.88.0 → 0.89.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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/inject-retrieval.ts +7 -5
- package/src/hooks/handlers/readme-sync.ts +6 -2
- package/src/hooks/handlers/session-name.ts +3 -12
- 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/hook-turn.ts +25 -2
- 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/prompt-text.ts +1 -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/session-names.ts +20 -4
- 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
|
@@ -25,12 +25,20 @@ import { tmpdir } from "node:os";
|
|
|
25
25
|
import { basename, join } from "node:path";
|
|
26
26
|
import {
|
|
27
27
|
getActiveAgent,
|
|
28
|
+
isAntigravity,
|
|
28
29
|
isClaude,
|
|
29
30
|
isCodex,
|
|
30
31
|
isCopilot,
|
|
31
32
|
isCursor,
|
|
32
33
|
isOpencode,
|
|
33
34
|
} from "./agent";
|
|
35
|
+
import {
|
|
36
|
+
extractAntigravityText,
|
|
37
|
+
INFERENCE_AGENT,
|
|
38
|
+
removeSpawnedConversation,
|
|
39
|
+
streamJsonPrompt,
|
|
40
|
+
writeInferenceAgent,
|
|
41
|
+
} from "./antigravity-inference";
|
|
34
42
|
import { logDebug, logError } from "./log";
|
|
35
43
|
import {
|
|
36
44
|
type FixedModelRoute,
|
|
@@ -108,6 +116,8 @@ function previewRoute(): RoutePreview {
|
|
|
108
116
|
return { agent, route: "copilot-spawn", reason: "copilot binary on PATH" };
|
|
109
117
|
if (isCursor() && hasCursorBinary())
|
|
110
118
|
return { agent, route: "cursor-spawn", reason: "cursor-agent binary on PATH" };
|
|
119
|
+
if (isAntigravity() && hasAntigravityBinary())
|
|
120
|
+
return { agent, route: "antigravity-spawn", reason: "agy binary on PATH" };
|
|
111
121
|
if (hasApiKey())
|
|
112
122
|
return {
|
|
113
123
|
agent,
|
|
@@ -130,6 +140,7 @@ export function canInfer(): boolean {
|
|
|
130
140
|
if (isOpencode() && hasOpencodeBinary()) return true;
|
|
131
141
|
if (isCopilot() && hasCopilotBinary()) return true;
|
|
132
142
|
if (isCursor() && hasCursorBinary()) return true;
|
|
143
|
+
if (isAntigravity() && hasAntigravityBinary()) return true;
|
|
133
144
|
return hasApiKey();
|
|
134
145
|
}
|
|
135
146
|
|
|
@@ -218,13 +229,23 @@ export async function inference(opts: InferenceOptions): Promise<InferenceResult
|
|
|
218
229
|
return inferenceViaCliSpawn(bin, buildCursorArgs(opts), buildCliPrompt(opts), opts);
|
|
219
230
|
}
|
|
220
231
|
}
|
|
232
|
+
if (isAntigravity()) {
|
|
233
|
+
const bin = getAntigravityBinary();
|
|
234
|
+
if (bin) {
|
|
235
|
+
logDebug(
|
|
236
|
+
"inference",
|
|
237
|
+
`${tag} route=antigravity-spawn agent=${agent} model=${modelFor("antigravity-spawn", opts)}`
|
|
238
|
+
);
|
|
239
|
+
return inferenceViaAntigravitySpawn(bin, opts);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
221
242
|
if (hasApiKey()) {
|
|
222
243
|
logDebug("inference", `${tag} route=anthropic-api agent=${agent}`);
|
|
223
244
|
return inferenceViaApi(opts);
|
|
224
245
|
}
|
|
225
246
|
logDebug(
|
|
226
247
|
"inference",
|
|
227
|
-
`${tag} route=none agent=${agent} hasApiKey=false hasOpenAiKey=${hasOpenAiKey()} hasClaude=${hasClaudeBinary()} hasCodex=${hasCodexBinary()} hasOpencode=${hasOpencodeBinary()} hasCopilot=${hasCopilotBinary()} hasCursor=${hasCursorBinary()}`
|
|
248
|
+
`${tag} route=none agent=${agent} hasApiKey=false hasOpenAiKey=${hasOpenAiKey()} hasClaude=${hasClaudeBinary()} hasCodex=${hasCodexBinary()} hasOpencode=${hasOpencodeBinary()} hasCopilot=${hasCopilotBinary()} hasCursor=${hasCursorBinary()} hasAgy=${hasAntigravityBinary()}`
|
|
228
249
|
);
|
|
229
250
|
return { success: false };
|
|
230
251
|
}
|
|
@@ -261,6 +282,10 @@ function getCursorBinary(): string | null {
|
|
|
261
282
|
return cachedBinary("cursor-agent");
|
|
262
283
|
}
|
|
263
284
|
|
|
285
|
+
function getAntigravityBinary(): string | null {
|
|
286
|
+
return cachedBinary("agy");
|
|
287
|
+
}
|
|
288
|
+
|
|
264
289
|
function hasClaudeBinary(): boolean {
|
|
265
290
|
return getClaudeBinary() !== null;
|
|
266
291
|
}
|
|
@@ -276,6 +301,9 @@ function hasCopilotBinary(): boolean {
|
|
|
276
301
|
function hasCursorBinary(): boolean {
|
|
277
302
|
return getCursorBinary() !== null;
|
|
278
303
|
}
|
|
304
|
+
function hasAntigravityBinary(): boolean {
|
|
305
|
+
return getAntigravityBinary() !== null;
|
|
306
|
+
}
|
|
279
307
|
|
|
280
308
|
/**
|
|
281
309
|
* Build the argv for `claude --print …`. Pure.
|
|
@@ -469,6 +497,51 @@ export function buildCursorArgs(_opts: InferenceOptions): string[] {
|
|
|
469
497
|
return ["-p", "--mode", "ask", "--output-format", "text", "--trust"];
|
|
470
498
|
}
|
|
471
499
|
|
|
500
|
+
/**
|
|
501
|
+
* Build the argv for a one-turn `agy` stream-json session. Pure.
|
|
502
|
+
*
|
|
503
|
+
* The prompt goes in as one NDJSON `user` event on stdin (streamJsonPrompt), not
|
|
504
|
+
* through `-p`: a multi-paragraph argv element cannot survive cmd.exe on Windows,
|
|
505
|
+
* and stream-json input drops a `-p` prompt anyway. Closing stdin ends the
|
|
506
|
+
* session once the turn's `result` event is out.
|
|
507
|
+
*
|
|
508
|
+
* `--json-schema` is deliberately absent: under the tool-less agent it looped
|
|
509
|
+
* through several turns, so the schema rides in the prompt as for every other CLI.
|
|
510
|
+
*/
|
|
511
|
+
export function buildAntigravityArgs(opts: InferenceOptions): string[] {
|
|
512
|
+
return [
|
|
513
|
+
"--input-format",
|
|
514
|
+
"stream-json",
|
|
515
|
+
"--output-format",
|
|
516
|
+
"stream-json",
|
|
517
|
+
"--model",
|
|
518
|
+
modelFor("antigravity-spawn", opts),
|
|
519
|
+
"--agent",
|
|
520
|
+
INFERENCE_AGENT,
|
|
521
|
+
];
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** Runs from an empty workspace that holds only the tool-less inference agent. */
|
|
525
|
+
async function inferenceViaAntigravitySpawn(
|
|
526
|
+
bin: string,
|
|
527
|
+
opts: InferenceOptions
|
|
528
|
+
): Promise<InferenceResult> {
|
|
529
|
+
const dir = await mkdtemp(join(tmpdir(), "pal-antigravity-"));
|
|
530
|
+
try {
|
|
531
|
+
writeInferenceAgent(dir);
|
|
532
|
+
return await inferenceViaCliSpawn(
|
|
533
|
+
bin,
|
|
534
|
+
buildAntigravityArgs(opts),
|
|
535
|
+
streamJsonPrompt(buildCliPrompt(opts)),
|
|
536
|
+
opts,
|
|
537
|
+
extractAntigravityText,
|
|
538
|
+
{ cwd: dir, afterRun: removeSpawnedConversation }
|
|
539
|
+
);
|
|
540
|
+
} finally {
|
|
541
|
+
await rm(dir, { recursive: true, force: true });
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
472
545
|
/**
|
|
473
546
|
* Build the argv for `copilot -p …` from inference options. Pure.
|
|
474
547
|
*
|
|
@@ -683,6 +756,8 @@ interface SpawnPlace {
|
|
|
683
756
|
cwd?: string;
|
|
684
757
|
env?: Record<string, string | undefined>;
|
|
685
758
|
logTag?: string;
|
|
759
|
+
/** Sees the final attempt's stdout whatever the outcome, timeouts included. */
|
|
760
|
+
afterRun?: (rawStdout: string) => void;
|
|
686
761
|
}
|
|
687
762
|
|
|
688
763
|
/**
|
|
@@ -731,6 +806,7 @@ async function inferenceViaCliSpawn(
|
|
|
731
806
|
attempt = await singleCliAttempt(binary, args, stdinInput, env, timeout, place.cwd);
|
|
732
807
|
}
|
|
733
808
|
|
|
809
|
+
place.afterRun?.(attempt.stdout);
|
|
734
810
|
const elapsedMs = Date.now() - started;
|
|
735
811
|
const finish = (result: InferenceResult): InferenceResult => {
|
|
736
812
|
logDebug(
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Antigravity's PreInvocation, which stands in for both SessionStart and
|
|
3
|
+
* UserPromptSubmit: agy has neither, and fires this before every model call.
|
|
4
|
+
*
|
|
5
|
+
* Only the first call of a turn (`invocationNum` 0) carries a new prompt. Its
|
|
6
|
+
* one reply injects a `userMessage` step, which agy keeps for the rest of the
|
|
7
|
+
* conversation — so the session context goes in once per conversation and the
|
|
8
|
+
* prompt context once per turn.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { resolve } from "node:path";
|
|
13
|
+
import { getPromptContext } from "../handlers/inject-retrieval";
|
|
14
|
+
import { latestUserRequest } from "./antigravity-transcript";
|
|
15
|
+
import { autoUpdateOnStart } from "./auto-update";
|
|
16
|
+
import { regenerateIfNeeded } from "./claude-md";
|
|
17
|
+
import { buildSystemReminder } from "./context";
|
|
18
|
+
import { type HookTurnPayload, hookSessionId } from "./hook-turn";
|
|
19
|
+
import { logContextSnapshot, logError, logPromptSnapshot } from "./log";
|
|
20
|
+
import { paths } from "./paths";
|
|
21
|
+
import { capturePrompt } from "./prompt-capture";
|
|
22
|
+
|
|
23
|
+
export interface InvocationPayload extends HookTurnPayload {
|
|
24
|
+
invocationNum?: number;
|
|
25
|
+
transcriptPath?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const REMEMBERED_CONVERSATIONS = 200;
|
|
29
|
+
|
|
30
|
+
function startedFile(): string {
|
|
31
|
+
return resolve(paths.state(), "antigravity-conversations.json");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function startedConversations(): string[] {
|
|
35
|
+
try {
|
|
36
|
+
const file = startedFile();
|
|
37
|
+
return existsSync(file) ? JSON.parse(readFileSync(file, "utf-8")) : [];
|
|
38
|
+
} catch {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** True exactly once per conversation: the call that gets to start it. */
|
|
44
|
+
function claimConversationStart(conversationId: string): boolean {
|
|
45
|
+
const started = startedConversations();
|
|
46
|
+
if (started.includes(conversationId)) return false;
|
|
47
|
+
const kept = [...started, conversationId].slice(-REMEMBERED_CONVERSATIONS);
|
|
48
|
+
writeFileSync(startedFile(), JSON.stringify(kept), "utf-8");
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function attempt(label: string, work: () => unknown): void {
|
|
53
|
+
try {
|
|
54
|
+
work();
|
|
55
|
+
} catch (err) {
|
|
56
|
+
logError(`InvocationContext:${label}`, err);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function sessionStartReminder(): string {
|
|
61
|
+
attempt("regenerate", regenerateIfNeeded);
|
|
62
|
+
attempt("auto-update", autoUpdateOnStart);
|
|
63
|
+
const reminder = buildSystemReminder({ agent: "claude" });
|
|
64
|
+
if (reminder) logContextSnapshot(reminder);
|
|
65
|
+
return reminder;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function promptReminder(prompt: string, conversationId: string) {
|
|
69
|
+
const context = await getPromptContext(prompt, conversationId);
|
|
70
|
+
logPromptSnapshot(prompt, context);
|
|
71
|
+
return context;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The agy reply that adds `text` to the conversation as one user step. */
|
|
75
|
+
function injectedStep(text: string): string {
|
|
76
|
+
return JSON.stringify({ injectSteps: [{ userMessage: text }] });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** What PreInvocation should print, or null to print nothing. */
|
|
80
|
+
export async function invocationContext(
|
|
81
|
+
payload: InvocationPayload,
|
|
82
|
+
sentAt: Date = new Date()
|
|
83
|
+
): Promise<string | null> {
|
|
84
|
+
const conversationId = hookSessionId(payload);
|
|
85
|
+
if (payload.invocationNum !== 0 || !conversationId || !payload.transcriptPath) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const prompt = latestUserRequest(payload.transcriptPath);
|
|
90
|
+
if (prompt === null) return null;
|
|
91
|
+
|
|
92
|
+
const parts = [
|
|
93
|
+
claimConversationStart(conversationId) ? sessionStartReminder() : null,
|
|
94
|
+
await promptReminder(prompt, conversationId),
|
|
95
|
+
].filter((part): part is string => Boolean(part));
|
|
96
|
+
await capturePrompt(prompt, conversationId, sentAt, "InvocationContext");
|
|
97
|
+
|
|
98
|
+
return parts.length > 0 ? injectedStep(parts.join("\n\n")) : null;
|
|
99
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { existsSync, readFileSync } from "node:fs";
|
|
10
10
|
import { isAbsolute, resolve } from "node:path";
|
|
11
|
-
import { normalizeToolUse } from "./agent";
|
|
11
|
+
import { ANTIGRAVITY_WRITE_TOOLS, FILE_TARGET_KEYS, normalizeToolUse } from "./agent";
|
|
12
12
|
import {
|
|
13
13
|
claimPending,
|
|
14
14
|
type LedgerEntry,
|
|
@@ -38,6 +38,7 @@ const LEDGERED_TOOLS = new Set([
|
|
|
38
38
|
"insert",
|
|
39
39
|
"str_replace",
|
|
40
40
|
"str_replace_editor",
|
|
41
|
+
...ANTIGRAVITY_WRITE_TOOLS,
|
|
41
42
|
]);
|
|
42
43
|
|
|
43
44
|
/**
|
|
@@ -53,9 +54,6 @@ const PATCHING_TOOLS = new Set(["apply_patch", "applypatch"]);
|
|
|
53
54
|
*/
|
|
54
55
|
const READING_COMMANDS = new Set(["view"]);
|
|
55
56
|
|
|
56
|
-
/** Agents disagree on the spelling; the value is the same file either way. */
|
|
57
|
-
const TARGET_KEYS = ["file_path", "filePath", "path"];
|
|
58
|
-
|
|
59
57
|
/** Every header in the V4A patch format that names a file the patch changes. */
|
|
60
58
|
const PATCHED_FILE_HEADER = /^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm;
|
|
61
59
|
|
|
@@ -153,7 +151,7 @@ function derivedPairingKey(anchor: string, tool: string, target: string): string
|
|
|
153
151
|
}
|
|
154
152
|
|
|
155
153
|
function sessionOf(payload: Record<string, unknown>): string | null {
|
|
156
|
-
const session = payload.sessionId ?? payload.session_id;
|
|
154
|
+
const session = payload.sessionId ?? payload.session_id ?? payload.conversationId;
|
|
157
155
|
return typeof session === "string" && session.length > 0 ? session : null;
|
|
158
156
|
}
|
|
159
157
|
|
|
@@ -229,6 +227,16 @@ export function snapshotCall(call: LedgeredCall): void {
|
|
|
229
227
|
savePending({ ...call, before: contentsOf(call.target), ts: new Date().toISOString() });
|
|
230
228
|
}
|
|
231
229
|
|
|
230
|
+
/** Antigravity fires PostToolUse for a failed call too, and says so only in `error`. */
|
|
231
|
+
function reportsFailure(payload: Record<string, unknown>): boolean {
|
|
232
|
+
return typeof payload.error === "string" && payload.error.length > 0;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** The calls in a post-tool payload whose change actually reached the disk. */
|
|
236
|
+
export function landedCalls(payload: Record<string, unknown>): LedgeredCall[] {
|
|
237
|
+
return reportsFailure(payload) ? [] : ledgeredCalls(payload);
|
|
238
|
+
}
|
|
239
|
+
|
|
232
240
|
/** Pair a parked before-state with the result, or nothing if none was parked. */
|
|
233
241
|
export function commitApplied(call: LedgeredCall): LedgerEntry | null {
|
|
234
242
|
const pending = claimPending(call.toolUseId);
|
|
@@ -303,7 +311,7 @@ export function ledgeredTarget(
|
|
|
303
311
|
): string | null {
|
|
304
312
|
if (!LEDGERED_TOOLS.has(toolName.toLowerCase())) return null;
|
|
305
313
|
if (readsRatherThanChanges(toolInput)) return null;
|
|
306
|
-
for (const key of
|
|
314
|
+
for (const key of FILE_TARGET_KEYS) {
|
|
307
315
|
const value = toolInput[key];
|
|
308
316
|
if (typeof value === "string" && value.length > 0) return value;
|
|
309
317
|
}
|
package/src/hooks/lib/models.ts
CHANGED
|
@@ -14,21 +14,40 @@ export type FixedModelRoute =
|
|
|
14
14
|
| "claude-spawn"
|
|
15
15
|
| "anthropic-api"
|
|
16
16
|
| "codex-spawn"
|
|
17
|
-
| "openai-api"
|
|
17
|
+
| "openai-api"
|
|
18
|
+
| "antigravity-spawn";
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
/** `large` is the flagship that authors skills and subagents; without one, the agent authors inline. */
|
|
21
|
+
type RouteModels = Record<InferenceTier, string> & { large?: string };
|
|
22
|
+
|
|
23
|
+
const ANTHROPIC_MODELS: RouteModels = {
|
|
24
|
+
small: HAIKU_MODEL,
|
|
25
|
+
medium: SONNET_MODEL,
|
|
26
|
+
large: FABLE_MODEL,
|
|
27
|
+
};
|
|
28
|
+
const OPENAI_MODELS: RouteModels = {
|
|
29
|
+
small: "gpt-6-luna",
|
|
30
|
+
medium: "gpt-6-sol",
|
|
31
|
+
large: "gpt-6-astra",
|
|
32
|
+
};
|
|
33
|
+
/** An Antigravity subagent's `model:` takes only a tier name, so `large` is `pro`. */
|
|
34
|
+
const GEMINI_MODELS: RouteModels = {
|
|
35
|
+
small: "gemini-3.8-flash-low",
|
|
36
|
+
medium: "gemini-3.8-flash-high",
|
|
37
|
+
large: "pro",
|
|
38
|
+
};
|
|
21
39
|
|
|
22
40
|
/**
|
|
23
41
|
* opencode's models come from the user's own config (opencodeTierModel). Copilot and
|
|
24
42
|
* Cursor are absent on purpose: which named models they accept depends on the user's
|
|
25
43
|
* plan, and a free plan refuses every one but Auto.
|
|
26
44
|
*/
|
|
27
|
-
const INFERENCE_MODELS: Record<FixedModelRoute,
|
|
45
|
+
const INFERENCE_MODELS: Record<FixedModelRoute, RouteModels> = {
|
|
28
46
|
"claude-spawn": ANTHROPIC_MODELS,
|
|
29
47
|
"anthropic-api": ANTHROPIC_MODELS,
|
|
30
48
|
"codex-spawn": OPENAI_MODELS,
|
|
31
49
|
"openai-api": OPENAI_MODELS,
|
|
50
|
+
"antigravity-spawn": GEMINI_MODELS,
|
|
32
51
|
};
|
|
33
52
|
|
|
34
53
|
export function isFixedModelRoute(route: string): route is FixedModelRoute {
|
|
@@ -42,22 +61,20 @@ export function inferenceModel(
|
|
|
42
61
|
return INFERENCE_MODELS[route][tier];
|
|
43
62
|
}
|
|
44
63
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
* present here → `create-skill` spins up that agent's `skill-author` subagent on
|
|
50
|
-
* this model; an agent absent → `create-skill` authors inline. Add a provider by
|
|
51
|
-
* adding one entry (and a matching platform block in assets/agents/skill-author.md).
|
|
52
|
-
*/
|
|
53
|
-
export const FLAGSHIP_AUTHOR_MODEL: Partial<Record<AgentType, string>> = {
|
|
54
|
-
claude: FABLE_MODEL,
|
|
55
|
-
// codex: "gpt-5.5", // enable once Codex ships a tool-capable flagship subagent
|
|
64
|
+
const AGENT_SPAWN_ROUTE: Partial<Record<AgentType, FixedModelRoute>> = {
|
|
65
|
+
claude: "claude-spawn",
|
|
66
|
+
codex: "codex-spawn",
|
|
67
|
+
antigravity: "antigravity-spawn",
|
|
56
68
|
};
|
|
57
69
|
|
|
58
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* The model an agent's `skill-author` / `subagent-author` runs on, or undefined
|
|
72
|
+
* when `create-skill` and `create-subagent` should author inline. A new provider
|
|
73
|
+
* needs a `large` model on its route and a platform block in assets/agents/*-author.md.
|
|
74
|
+
*/
|
|
59
75
|
export function flagshipAuthorModel(agent: AgentType): string | undefined {
|
|
60
|
-
|
|
76
|
+
const route = AGENT_SPAWN_ROUTE[agent];
|
|
77
|
+
return route ? INFERENCE_MODELS[route].large : undefined;
|
|
61
78
|
}
|
|
62
79
|
|
|
63
80
|
export interface ModelPricing {
|
package/src/hooks/lib/paths.ts
CHANGED
|
@@ -120,13 +120,43 @@ export const paths = {
|
|
|
120
120
|
|
|
121
121
|
// Platform directories (env override or cross-platform defaults)
|
|
122
122
|
const h = homedir();
|
|
123
|
+
|
|
124
|
+
const AGENT_DIR_DEFAULTS = {
|
|
125
|
+
PAL_CLAUDE_DIR: [".claude"],
|
|
126
|
+
PAL_OPENCODE_DIR: [".config", "opencode"],
|
|
127
|
+
PAL_CURSOR_DIR: [".cursor"],
|
|
128
|
+
PAL_COPILOT_DIR: [".copilot"],
|
|
129
|
+
PAL_CODEX_DIR: [".codex"],
|
|
130
|
+
PAL_GEMINI_DIR: [".gemini"],
|
|
131
|
+
PAL_AGENTS_DIR: [".agents"],
|
|
132
|
+
} as const;
|
|
133
|
+
|
|
134
|
+
type AgentDirOverride = keyof typeof AGENT_DIR_DEFAULTS;
|
|
135
|
+
|
|
136
|
+
const defaultAgentDir = (env: AgentDirOverride) => resolve(h, ...AGENT_DIR_DEFAULTS[env]);
|
|
137
|
+
|
|
138
|
+
const agentDir = (env: AgentDirOverride) => () => envPath(env, defaultAgentDir(env));
|
|
139
|
+
|
|
140
|
+
/** Every agent dir override with the real directory it replaces. */
|
|
141
|
+
export function agentDirOverrides(): { env: AgentDirOverride; realDir: string }[] {
|
|
142
|
+
return (Object.keys(AGENT_DIR_DEFAULTS) as AgentDirOverride[]).map((env) => ({
|
|
143
|
+
env,
|
|
144
|
+
realDir: defaultAgentDir(env),
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const geminiDir = agentDir("PAL_GEMINI_DIR");
|
|
149
|
+
|
|
123
150
|
export const platform = {
|
|
124
|
-
claudeDir: (
|
|
125
|
-
opencodeDir: (
|
|
126
|
-
cursorDir: (
|
|
127
|
-
copilotDir: (
|
|
128
|
-
codexDir: (
|
|
129
|
-
|
|
151
|
+
claudeDir: agentDir("PAL_CLAUDE_DIR"),
|
|
152
|
+
opencodeDir: agentDir("PAL_OPENCODE_DIR"),
|
|
153
|
+
cursorDir: agentDir("PAL_CURSOR_DIR"),
|
|
154
|
+
copilotDir: agentDir("PAL_COPILOT_DIR"),
|
|
155
|
+
codexDir: agentDir("PAL_CODEX_DIR"),
|
|
156
|
+
antigravityPluginDir: () => resolve(geminiDir(), "config", "plugins", "pal"),
|
|
157
|
+
antigravityCliDir: () => resolve(geminiDir(), "antigravity-cli"),
|
|
158
|
+
antigravitySettings: () => resolve(geminiDir(), "antigravity-cli", "settings.json"),
|
|
159
|
+
agentsDir: agentDir("PAL_AGENTS_DIR"),
|
|
130
160
|
} as const;
|
|
131
161
|
|
|
132
162
|
// Engine/asset paths (in PAL_PKG / repo root)
|
|
@@ -140,6 +170,9 @@ export const assets = {
|
|
|
140
170
|
cursorHooksTemplate: () => pkg("assets", "templates", "hooks.cursor.json"),
|
|
141
171
|
copilotHooksTemplate: () => pkg("assets", "templates", "hooks.copilot.json"),
|
|
142
172
|
codexHooksTemplate: () => pkg("assets", "templates", "hooks.codex.json"),
|
|
173
|
+
antigravityHooksTemplate: () => pkg("assets", "templates", "hooks.antigravity.json"),
|
|
174
|
+
antigravitySettingsTemplate: () =>
|
|
175
|
+
pkg("assets", "templates", "settings.antigravity.json"),
|
|
143
176
|
codexRulesTemplate: () => pkg("assets", "templates", "rules.codex.rules"),
|
|
144
177
|
statuslineScriptBash: () => pkg("assets", "statusline.sh"),
|
|
145
178
|
statuslineScriptPs1: () => pkg("assets", "statusline.ps1"),
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What every prompt hook records about the prompt itself, whichever event the
|
|
3
|
+
* agent delivers it on. Each handler is independent, so one failing is logged
|
|
4
|
+
* and never stops the other.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { captureRating } from "../handlers/rating";
|
|
8
|
+
import { captureSessionName } from "../handlers/session-name";
|
|
9
|
+
import { logError } from "./log";
|
|
10
|
+
|
|
11
|
+
const HANDLER_NAMES = ["rating", "session-name"];
|
|
12
|
+
|
|
13
|
+
export async function capturePrompt(
|
|
14
|
+
prompt: string,
|
|
15
|
+
sessionId: string | undefined,
|
|
16
|
+
sentAt: Date,
|
|
17
|
+
caller: string
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
const results = await Promise.allSettled([
|
|
20
|
+
captureRating(prompt, sessionId, sentAt),
|
|
21
|
+
captureSessionName(prompt, sessionId ?? ""),
|
|
22
|
+
]);
|
|
23
|
+
results.forEach((result, i) => {
|
|
24
|
+
if (result.status === "rejected") {
|
|
25
|
+
logError(`${caller}:${HANDLER_NAMES[i]}`, result.reason);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -16,7 +16,7 @@ interface SyncResult {
|
|
|
16
16
|
|
|
17
17
|
/** Files that, when changed, should trigger a README check. */
|
|
18
18
|
export const WATCHED_PATHS = [
|
|
19
|
-
"src/cli
|
|
19
|
+
"src/cli",
|
|
20
20
|
"src/hooks/lib/paths.ts",
|
|
21
21
|
"src/hooks/lib/inference.ts",
|
|
22
22
|
"src/tools/youtube-analyze.ts",
|
|
@@ -24,33 +24,6 @@ export const WATCHED_PATHS = [
|
|
|
24
24
|
"assets/agents",
|
|
25
25
|
];
|
|
26
26
|
|
|
27
|
-
/**
|
|
28
|
-
* A flag spelling of a command the README documents under its canonical name,
|
|
29
|
-
* or the `cli` prefix the dispatcher consumes before a command is ever named.
|
|
30
|
-
*/
|
|
31
|
-
function isAliasOrInternalRoute(cmd: string): boolean {
|
|
32
|
-
return ["--help", "-h", "help", "-v", "--version", "cli"].includes(cmd);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** Extract CLI command names from the switch statement in index.ts */
|
|
36
|
-
function extractCliCommands(): string[] {
|
|
37
|
-
const pkg = palPkg();
|
|
38
|
-
const cliPath = resolve(pkg, "src", "cli", "index.ts");
|
|
39
|
-
if (!existsSync(cliPath)) return [];
|
|
40
|
-
|
|
41
|
-
const content = readFileSync(cliPath, "utf-8");
|
|
42
|
-
const matches = content.matchAll(/case\s+"([^"]+)":/g);
|
|
43
|
-
const commands: string[] = [];
|
|
44
|
-
|
|
45
|
-
for (const match of matches) {
|
|
46
|
-
const cmd = match[1];
|
|
47
|
-
if (isAliasOrInternalRoute(cmd)) continue;
|
|
48
|
-
commands.push(cmd);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return [...new Set(commands)];
|
|
52
|
-
}
|
|
53
|
-
|
|
54
27
|
/** Extract PAL_* env var names from paths.ts + API keys from source */
|
|
55
28
|
function extractEnvVars(): string[] {
|
|
56
29
|
const pkg = palPkg();
|
|
@@ -113,7 +86,7 @@ function documentedSkillNames(readme: string): string[] {
|
|
|
113
86
|
}
|
|
114
87
|
|
|
115
88
|
/** Validate that README.md documents all code surfaces. */
|
|
116
|
-
export function validateReadmeSync(): SyncResult {
|
|
89
|
+
export function validateReadmeSync(cliCommands: string[]): SyncResult {
|
|
117
90
|
const pkg = palPkg();
|
|
118
91
|
const readmePath = resolve(pkg, "README.md");
|
|
119
92
|
|
|
@@ -124,8 +97,7 @@ export function validateReadmeSync(): SyncResult {
|
|
|
124
97
|
const readme = readFileSync(readmePath, "utf-8");
|
|
125
98
|
const issues: string[] = [];
|
|
126
99
|
|
|
127
|
-
|
|
128
|
-
for (const cmd of extractCliCommands()) {
|
|
100
|
+
for (const cmd of cliCommands) {
|
|
129
101
|
if (!readme.includes(`pal cli ${cmd}`)) {
|
|
130
102
|
issues.push(`CLI command "${cmd}" exists in code but not documented in README`);
|
|
131
103
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* lives here instead; the entrypoint is left with stdin, stdout and the ledger.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { normalizeToolUse } from "./agent";
|
|
10
|
+
import { ANTIGRAVITY_WRITE_TOOLS, FILE_TARGET_KEYS, normalizeToolUse } from "./agent";
|
|
11
11
|
import { logDebug } from "./log";
|
|
12
12
|
import { checkBashCommand, checkFilePath } from "./security";
|
|
13
13
|
|
|
@@ -43,6 +43,7 @@ const SHELL_TOOLS = [
|
|
|
43
43
|
"run_in_terminal",
|
|
44
44
|
"terminal",
|
|
45
45
|
"execute_command",
|
|
46
|
+
"run_command",
|
|
46
47
|
];
|
|
47
48
|
|
|
48
49
|
const FILE_WRITE_TOOLS = [
|
|
@@ -64,6 +65,7 @@ const FILE_WRITE_TOOLS = [
|
|
|
64
65
|
"replacestring",
|
|
65
66
|
"edit_notebook_file",
|
|
66
67
|
"notebookedit",
|
|
68
|
+
...ANTIGRAVITY_WRITE_TOOLS,
|
|
67
69
|
];
|
|
68
70
|
|
|
69
71
|
function isShellExec(input: SecurityInput): input is ShellExecInput {
|
|
@@ -121,7 +123,12 @@ export function decideRefusal(input: SecurityInput, cwd: string): GateRefusal |
|
|
|
121
123
|
`toolName=${toolUse.toolName} args=${Object.keys(toolUse.toolInput).join(",")}`
|
|
122
124
|
);
|
|
123
125
|
|
|
124
|
-
const command = firstStringArg(toolUse.toolInput, [
|
|
126
|
+
const command = firstStringArg(toolUse.toolInput, [
|
|
127
|
+
"command",
|
|
128
|
+
"commandLine",
|
|
129
|
+
"CommandLine",
|
|
130
|
+
"script",
|
|
131
|
+
]);
|
|
125
132
|
if (runsShellCommand(toolUse.toolName) && command) {
|
|
126
133
|
const reason = checkBashCommand(command);
|
|
127
134
|
// "No output" from a downstream tool is indistinguishable between "denied,
|
|
@@ -141,7 +148,7 @@ export function decideRefusal(input: SecurityInput, cwd: string): GateRefusal |
|
|
|
141
148
|
}
|
|
142
149
|
}
|
|
143
150
|
|
|
144
|
-
const filePath = firstStringArg(toolUse.toolInput,
|
|
151
|
+
const filePath = firstStringArg(toolUse.toolInput, FILE_TARGET_KEYS);
|
|
145
152
|
if (writesFile(toolUse.toolName) && filePath) {
|
|
146
153
|
const reason = checkFilePath(filePath);
|
|
147
154
|
if (reason) {
|
|
@@ -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
|
|
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
|