@estebanforge/pi-ask-antigravity 1.2.8 → 1.2.10
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/CHANGELOG.md +14 -0
- package/README.md +3 -4
- package/extensions/index.ts +277 -41
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.2.10] - 2026-10-06
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- **Breaking: model aliases track agy's Claude 5.5 tiered catalog.** `sonnet`/`opus` now resolve to `claude-sonnet-5-5`/`claude-opus-5-5` with the tier riding `--effort` (nearest-tier pick against the requested thinking level), replacing the retired fixed `claude-sonnet-4-6`/`claude-opus-4-6-thinking` slugs Google removed from Antigravity. The static overlay carries the full low/medium/high spread so the aliases still resolve when the live catalog omits Claude, and the bare base id (`claude-sonnet-5-5`) resolves to the nearest tier variant because a bare base is rejected upstream. Exact tiered ids (`claude-sonnet-5-5-low`) split to base + effort exactly like Gemini's.
|
|
10
|
+
- **The `gpt-oss` alias and its overlay entry are removed** ahead of upstream's announced removal of that family: nobody used the fixed-thinking slug and it loses to the cheaper tiered options. The alias now resolves to nothing (the raw string passes to agy and fails there) instead of the tool offering a dying model.
|
|
11
|
+
|
|
12
|
+
## [1.2.9] - 2026-09-28
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **Plan-mode delegations enforce read-only instead of promising it.** Port of the bridge fix one commit earlier in the stack: the prompt-only guard failed in the field (a plan delegation behind an env-injected skip flag still edited files), so plan-mode runs now stage a temporary agy agent — `tools:` limited to `view_file` and `run_command`, `commandExecutionPolicy: auto`, `inheritMcp: false` — under the shared `~/.gemini/config/agents` discovery root and pass `--agent`. No file-editing tool in the toolset means no edit call is possible, with or without `--dangerously-skip-permissions` (probed on agy 1.2.12); the policy is what lets analysis commands run headless for users with no allow rules. With the agent staged, the skip flag returns under the shared `skipPermissions` knob; staging failure falls back fail-closed to no agent, no flag, and the strict all-commands-forbidden guard, with an env-injected skip flag stripped from `AGY_EXTRA_ARGS` on that path. Includes the standalone sweep for orphaned agent dirs (pid-marker doctrine), duplicated rather than extracted: the standalone stays self-contained per repo convention. Tool descriptions state the residual honestly: file-editing tools are hard-blocked, arbitrary shell execution under the flag is prompt-guarded alone.
|
|
17
|
+
- **Headless plan prompts say what they cannot do.** Same hardening as the bridge: plan prompts append an explicit no-commands constraint and a state-what-is-missing instruction, converting the soft-denied empty-output death of a shell-command attempt into a visible missing-content request. On the enforced path the guard yields to the mutation-forbidding variant; the command-forbidding rule remains on the fallback path.
|
|
18
|
+
|
|
5
19
|
## [1.2.8] - 2026-09-25
|
|
6
20
|
|
|
7
21
|
### Fixed
|
package/README.md
CHANGED
|
@@ -49,15 +49,14 @@ Verbose `agy models` strings (`Gemini 3.5 Flash (Medium)`) are hostile to natura
|
|
|
49
49
|
| `pro low` | latest Pro, Low |
|
|
50
50
|
| `3.5 flash` | pinned version, default tier |
|
|
51
51
|
| `3.1 pro` | pinned Pro version (→ High) |
|
|
52
|
-
| `sonnet` | Claude Sonnet
|
|
53
|
-
| `opus` | Claude Opus
|
|
54
|
-
| `gpt-oss` | GPT-OSS 120B (Medium) |
|
|
52
|
+
| `sonnet` | Claude Sonnet 5.5, default tier (Medium) |
|
|
53
|
+
| `opus` | Claude Opus 5.5, default tier (Medium) |
|
|
55
54
|
| `Gemini 3.5 Flash (Medium)` | exact passthrough |
|
|
56
55
|
|
|
57
56
|
- **Latest** = highest version number available for the family.
|
|
58
57
|
- **Default tier**: Flash → Medium, Pro → High. Overridable per-config (below).
|
|
59
58
|
- When a tier is unavailable for a family, the nearest is chosen with ties broken toward the higher tier (so "latest and greatest" wins).
|
|
60
|
-
- The non-Gemini aliases (`sonnet`, `opus
|
|
59
|
+
- The non-Gemini aliases (`sonnet`, `opus`) are a static overlay (the full low/medium/high tier spread) merged with the live `agy models` catalog. Live entries always win on case-insensitive full-string equality, so an updated `agy` listing takes precedence over the hardcoded fallback. A bare base id (`claude-sonnet-5-5`) resolves to the nearest tier variant; the retired `gpt-oss` family resolves to nothing.
|
|
61
60
|
|
|
62
61
|
## Execution modes (`mode`)
|
|
63
62
|
|
package/extensions/index.ts
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
import { spawn } from "node:child_process";
|
|
32
|
+
import { randomBytes } from "node:crypto";
|
|
32
33
|
import * as fs from "node:fs";
|
|
33
34
|
import * as os from "node:os";
|
|
34
35
|
import * as path from "node:path";
|
|
@@ -111,13 +112,15 @@ type Mode = "plan" | "accept-edits";
|
|
|
111
112
|
// whose full string equals the overlay target, the live entry wins. When it
|
|
112
113
|
// does not (older agy, missing model, plan gate), the overlay entry resolves
|
|
113
114
|
// the alias so the user can still type "sonnet" and get a working answer.
|
|
114
|
-
// "sonnet" -> claude-sonnet-
|
|
115
|
-
// "opus" -> claude-opus-
|
|
116
|
-
// "gpt-oss" -> gpt-oss-120b-medium
|
|
115
|
+
// "sonnet" -> claude-sonnet-5-5 (tiered low/medium/high like Gemini)
|
|
116
|
+
// "opus" -> claude-opus-5-5 (tiered low/medium/high like Gemini)
|
|
117
117
|
const STATIC_ALIAS_OVERLAY: ReadonlyArray<ModelEntry> = [
|
|
118
|
-
{ full: "claude-sonnet-
|
|
119
|
-
{ full: "claude-
|
|
120
|
-
{ full: "
|
|
118
|
+
{ full: "claude-sonnet-5-5-low", family: "other", version: null, tier: "low" },
|
|
119
|
+
{ full: "claude-sonnet-5-5-medium", family: "other", version: null, tier: "medium" },
|
|
120
|
+
{ full: "claude-sonnet-5-5-high", family: "other", version: null, tier: "high" },
|
|
121
|
+
{ full: "claude-opus-5-5-low", family: "other", version: null, tier: "low" },
|
|
122
|
+
{ full: "claude-opus-5-5-medium", family: "other", version: null, tier: "medium" },
|
|
123
|
+
{ full: "claude-opus-5-5-high", family: "other", version: null, tier: "high" },
|
|
121
124
|
];
|
|
122
125
|
|
|
123
126
|
// Short alias → overlay full string. Used by resolveModel to recognize
|
|
@@ -125,11 +128,21 @@ const STATIC_ALIAS_OVERLAY: ReadonlyArray<ModelEntry> = [
|
|
|
125
128
|
// not match. The overlay entries are also merged into the live catalog for
|
|
126
129
|
// exact-string passthrough, so this map only needs to cover the short names.
|
|
127
130
|
const STATIC_SHORT_ALIAS: ReadonlyMap<string, string> = new Map([
|
|
128
|
-
["sonnet", "claude-sonnet-
|
|
129
|
-
["opus", "claude-opus-
|
|
130
|
-
["gpt-oss", "gpt-oss-120b-medium"],
|
|
131
|
+
["sonnet", "claude-sonnet-5-5"],
|
|
132
|
+
["opus", "claude-opus-5-5"],
|
|
131
133
|
]);
|
|
132
134
|
|
|
135
|
+
/** Families dropped outright even while `agy models` still lists them: the
|
|
136
|
+
* fixed-thinking single slug loses to the cheaper effort-tiered options and
|
|
137
|
+
* its removal upstream is already announced, so we stop offering it now. */
|
|
138
|
+
const HIDDEN_FAMILY_RE = /^gpt-oss-/;
|
|
139
|
+
|
|
140
|
+
/** Drop families we refuse to offer. Exported pure so tests exercise the
|
|
141
|
+
* same filter the live parse runs; discoverModels applies it per load. */
|
|
142
|
+
export function filterHiddenModels(entries: ModelEntry[]): ModelEntry[] {
|
|
143
|
+
return entries.filter((e) => !HIDDEN_FAMILY_RE.test(e.full));
|
|
144
|
+
}
|
|
145
|
+
|
|
133
146
|
/** Merge the live catalog with the static alias overlay. Live entries win on
|
|
134
147
|
* case-insensitive full-string equality so an updated `agy models` listing
|
|
135
148
|
* always takes precedence over the hardcoded fallback. */
|
|
@@ -156,7 +169,7 @@ TWO MODES (you choose):
|
|
|
156
169
|
- **Continued conversation**: pass the conversationId returned in the PREVIOUS call's details (details.conversationId). agy resumes that conversation with full context intact — use for follow-ups, multi-turn refinement, or when the user says "ask agy to follow up / continue / now do X based on what you just did". Thread the id from each result into the next call.
|
|
157
170
|
|
|
158
171
|
EXECUTION MODES (param: mode):
|
|
159
|
-
- **plan**: agy reviews and plans without writing. Use for cross-review and read-only tasks. Enforced: plan runs
|
|
172
|
+
- **plan**: agy reviews and plans without writing. Use for cross-review and read-only tasks. Enforced: plan runs execute in a temporary restricted agent whose toolset has NO file-editing tools (hard block); the reviewer may still execute arbitrary shell commands, kept read-only by the prompt guard alone. Inline the material to review - a plan run cannot fetch it.
|
|
160
173
|
- **accept-edits** (default): agy applies edits directly inside the workspace.
|
|
161
174
|
- For agy's orthogonal \`--sandbox\` shell-containment flag, set the \`AGY_EXTRA_ARGS=--sandbox\` env var.
|
|
162
175
|
|
|
@@ -282,6 +295,150 @@ function saveConfig(patch: Partial<Config>): SaveResult {
|
|
|
282
295
|
return { path: targetPath, routedToProject: projectShadows };
|
|
283
296
|
}
|
|
284
297
|
|
|
298
|
+
// --- Prompt assembly -------------------------------------------------------
|
|
299
|
+
|
|
300
|
+
/** Appended to every headless plan-mode prompt. `agy -p` cannot answer
|
|
301
|
+
* permission prompts: in plan mode a run_command attempt is soft-denied and
|
|
302
|
+
* the turn ends AT the denial (exit 0, empty stdout, notice only on stderr -
|
|
303
|
+
* the empty-output branch in execute). Probed 2026-09-28 on agy 1.2.12:
|
|
304
|
+
* allow rules ARE consulted (a verbatim allow-listed `git log` runs), but
|
|
305
|
+
* --sandbox does NOT relax the command gate, so the only input every user
|
|
306
|
+
* is guaranteed is the prompt itself. The guard steers the model to answer
|
|
307
|
+
* from that material. accept-edits runs keep their tools and never get it. */
|
|
308
|
+
export const PLAN_HEADLESS_GUARD = [
|
|
309
|
+
"",
|
|
310
|
+
"--- Headless session constraints ---",
|
|
311
|
+
"- Do not run shell commands. Command execution is denied in this session, and any attempt ends the session immediately with no answer.",
|
|
312
|
+
"- Work only from the material provided in this prompt.",
|
|
313
|
+
"- If information you need is missing, state exactly what is missing in your answer instead of trying to fetch it.",
|
|
314
|
+
].join("\n");
|
|
315
|
+
|
|
316
|
+
/** Guard for plan runs backed by the restricted reviewer agent. Difference
|
|
317
|
+
* to PLAN_HEADLESS_GUARD: commands are the analysis capability here (the
|
|
318
|
+
* agent's commandExecutionPolicy auto-approves them headless, probed
|
|
319
|
+
* 2026-09-28), so the guard forbids FILE MUTATION - by tool or by shell -
|
|
320
|
+
* and keeps the answer-from-material discipline. */
|
|
321
|
+
export const AGENT_REVIEW_GUARD = [
|
|
322
|
+
"",
|
|
323
|
+
"--- Review constraints ---",
|
|
324
|
+
"- Read-only review. Do not create, modify, or delete any files, including through shell commands (no redirects, tee, rm, mv, git commit).",
|
|
325
|
+
"- Read-only commands (git log, git diff, git show, rg, ls, cat, test runners) are allowed for analysis.",
|
|
326
|
+
"- Work from the material provided in this prompt; if information you need is missing, state exactly what is missing in your answer.",
|
|
327
|
+
].join("\n");
|
|
328
|
+
|
|
329
|
+
/** Assemble the prompt sent to agy: digest marker first (existing behavior),
|
|
330
|
+
* then the caller's prompt, then the plan-mode guard last - the position
|
|
331
|
+
* the model reads with the most recency. No user-config mutation: this is
|
|
332
|
+
* the only plan-mode lever the tool itself owns. */
|
|
333
|
+
export function buildFinalPrompt(
|
|
334
|
+
prompt: string,
|
|
335
|
+
mode: Mode,
|
|
336
|
+
digest: boolean,
|
|
337
|
+
/** True when the restricted reviewer agent is staged: its empty edit
|
|
338
|
+
* toolset is the hard block, so the prompt may ALLOW read-only commands. */
|
|
339
|
+
agentEnforced = false,
|
|
340
|
+
): string {
|
|
341
|
+
let out = digest ? `(Use compact digests, not full file contents.)\n${prompt}` : prompt;
|
|
342
|
+
if (mode === "plan") out += agentEnforced ? AGENT_REVIEW_GUARD : PLAN_HEADLESS_GUARD;
|
|
343
|
+
return out;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// --- Plan-mode reviewer agent ----------------------------------------------
|
|
347
|
+
|
|
348
|
+
const ASK_AGENT_PREFIX = "pi-bridge-ask-";
|
|
349
|
+
const ASK_AGENT_TOOLS = ["view_file", "run_command"];
|
|
350
|
+
|
|
351
|
+
/** Temp agent dirs share the ~/.gemini/config/agents discovery root; the
|
|
352
|
+
* AGY_AGENTS_ROOT override exists for tests and sandboxes. */
|
|
353
|
+
export function askAgentsRoot(): string {
|
|
354
|
+
return process.env.AGY_AGENTS_ROOT ?? path.join(os.homedir(), ".gemini", "config", "agents");
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** agent.md for the plan-mode reviewer. The tools list is the ENFORCEMENT:
|
|
358
|
+
* with no file-editing tool present the model cannot emit an edit call at
|
|
359
|
+
* all (probed 2026-09-28 on agy 1.2.12: the toolset reports "none" for
|
|
360
|
+
* edits, with and without the skip flag). commandExecutionPolicy auto is
|
|
361
|
+
* what lets read commands run headless with no user allow rules. Never add
|
|
362
|
+
* a write-capable tool here. */
|
|
363
|
+
export function reviewerAgentMd(name: string): string {
|
|
364
|
+
return [
|
|
365
|
+
"---",
|
|
366
|
+
`name: ${name}`,
|
|
367
|
+
"description: Temporary Pi plan-mode reviewer",
|
|
368
|
+
"mainAgent: true",
|
|
369
|
+
"subagent: false",
|
|
370
|
+
"model: inherit",
|
|
371
|
+
"excludeDefaultComponents: true",
|
|
372
|
+
"inheritCustomizations: false",
|
|
373
|
+
"inheritMcp: false",
|
|
374
|
+
"commandExecutionPolicy: auto",
|
|
375
|
+
"tools:",
|
|
376
|
+
...ASK_AGENT_TOOLS.map((t) => ` - ${t}`),
|
|
377
|
+
"skills: []",
|
|
378
|
+
"rules: []",
|
|
379
|
+
"agents: []",
|
|
380
|
+
"mcpServers: []",
|
|
381
|
+
"---",
|
|
382
|
+
"",
|
|
383
|
+
"You are a strict read-only code reviewer. Review, analyze, and plan; never modify anything.",
|
|
384
|
+
"",
|
|
385
|
+
].join("\n");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** Stage one unique reviewer agent (agent.md + pid marker) under root. Same
|
|
389
|
+
* hygiene doctrine as the web delegates: nonce-named, cleaned in finally,
|
|
390
|
+
* orphans swept by pid marker at registration. */
|
|
391
|
+
export function stageReviewerAgent(root: string): { name: string; dir: string } {
|
|
392
|
+
fs.mkdirSync(root, { recursive: true });
|
|
393
|
+
const name = `${ASK_AGENT_PREFIX}${process.pid}-${Date.now().toString(36)}-${randomBytes(4).toString("hex")}`;
|
|
394
|
+
const dir = path.join(root, name);
|
|
395
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
396
|
+
fs.writeFileSync(path.join(dir, "agent.md"), reviewerAgentMd(name), { mode: 0o600 });
|
|
397
|
+
fs.writeFileSync(path.join(dir, ".pid"), `${process.pid}\n`, { mode: 0o600 });
|
|
398
|
+
return { name, dir };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Remove leftover reviewer agent dirs: dead-pid markers always, marker-less
|
|
402
|
+
* dirs only after the grace period (a live sibling may sit between mkdir and
|
|
403
|
+
* its pid write). Never touches foreign agent dirs. */
|
|
404
|
+
export function sweepStaleAskAgents(
|
|
405
|
+
root: string = askAgentsRoot(),
|
|
406
|
+
now = Date.now(),
|
|
407
|
+
prefix: string = ASK_AGENT_PREFIX,
|
|
408
|
+
): void {
|
|
409
|
+
let entries: string[];
|
|
410
|
+
try {
|
|
411
|
+
entries = fs.readdirSync(root);
|
|
412
|
+
} catch {
|
|
413
|
+
return; // no agents dir yet: nothing to sweep
|
|
414
|
+
}
|
|
415
|
+
for (const entry of entries) {
|
|
416
|
+
if (!entry.startsWith(prefix)) continue;
|
|
417
|
+
const dir = path.join(root, entry);
|
|
418
|
+
try {
|
|
419
|
+
let stale: boolean;
|
|
420
|
+
try {
|
|
421
|
+
const pid = Number.parseInt(fs.readFileSync(path.join(dir, ".pid"), "utf8").trim(), 10);
|
|
422
|
+
stale = Number.isInteger(pid) && pid > 0 ? !pidAlive(pid) : now - fs.statSync(dir).mtimeMs > 24 * 60 * 60 * 1000;
|
|
423
|
+
} catch {
|
|
424
|
+
stale = now - fs.statSync(dir).mtimeMs > 24 * 60 * 60 * 1000;
|
|
425
|
+
}
|
|
426
|
+
if (stale) fs.rmSync(dir, { recursive: true, force: true });
|
|
427
|
+
} catch {
|
|
428
|
+
// vanished mid-sweep: nothing to remove
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function pidAlive(pid: number): boolean {
|
|
434
|
+
try {
|
|
435
|
+
process.kill(pid, 0);
|
|
436
|
+
return true;
|
|
437
|
+
} catch {
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
285
442
|
// --- Model parsing + alias resolution --------------------------------------
|
|
286
443
|
|
|
287
444
|
/** Parse one `agy models` line into a structured entry. */
|
|
@@ -321,19 +478,47 @@ function nearestTier(available: ThinkingTier[], preferred: ThinkingTier): Thinki
|
|
|
321
478
|
return sorted[0] ?? preferred;
|
|
322
479
|
}
|
|
323
480
|
|
|
324
|
-
/** Build the argv-facing resolution from a picked catalog entry. Gemini
|
|
325
|
-
* (slugs starting "gemini-") accept a separate
|
|
326
|
-
* suffix out of the slug: the base alone
|
|
327
|
-
* wants, and the tier goes to --effort
|
|
328
|
-
*
|
|
329
|
-
* agy
|
|
481
|
+
/** Build the argv-facing resolution from a picked catalog entry. Gemini and
|
|
482
|
+
* Claude bases (slugs starting "gemini-"/"claude-") accept a separate
|
|
483
|
+
* --effort, so split the tier suffix out of the slug: the base alone
|
|
484
|
+
* (claude-sonnet-5-5) is what --model wants, and the tier goes to --effort
|
|
485
|
+
* (agy rejects a bare base: "requires --effort"). Only unverified families
|
|
486
|
+
* keep agy's exact slug whole, so an unknown suffix can never trigger an
|
|
487
|
+
* unsupported --effort. */
|
|
330
488
|
function toResolved(full: string, tier: ThinkingTier | null): ResolvedModel {
|
|
331
|
-
if (tier && full.toLowerCase()
|
|
489
|
+
if (tier && /^(gemini|claude)-/.test(full.toLowerCase())) {
|
|
332
490
|
return { model: full.replace(/-(low|medium|high)$/, ""), effort: tier };
|
|
333
491
|
}
|
|
334
492
|
return { model: full };
|
|
335
493
|
}
|
|
336
494
|
|
|
495
|
+
/** Resolve a tiered base id - a short-alias target ("claude-sonnet-5-5") or a
|
|
496
|
+
* bare base slug the provider itself advertises - to the catalog variant
|
|
497
|
+
* nearest the requested tier. A bare base alone is invalid upstream
|
|
498
|
+
* ("requires --effort"), so the pick always rides the split in toResolved;
|
|
499
|
+
* with no variants in the catalog, the overlay/exact entry or the raw base
|
|
500
|
+
* passes through unchanged. */
|
|
501
|
+
function resolveTieredBase(
|
|
502
|
+
base: string,
|
|
503
|
+
entries: ModelEntry[],
|
|
504
|
+
defaultThinking: ThinkingTier,
|
|
505
|
+
preferredTier: ThinkingTier | undefined,
|
|
506
|
+
): ResolvedModel {
|
|
507
|
+
const needle = base.toLowerCase();
|
|
508
|
+
const variants = entries.filter((e) => e.full.toLowerCase().startsWith(`${needle}-`));
|
|
509
|
+
if (variants.length > 0) {
|
|
510
|
+
const tiers = variants.map((e) => e.tier).filter((t): t is ThinkingTier => t !== null);
|
|
511
|
+
const preferred =
|
|
512
|
+
preferredTier ??
|
|
513
|
+
(tiers.includes(defaultThinking) ? defaultThinking : FAMILY_DEFAULT_TIER.other);
|
|
514
|
+
const chosen = nearestTier(tiers, preferred);
|
|
515
|
+
const picked = variants.find((e) => e.tier === chosen) ?? variants[0];
|
|
516
|
+
return toResolved(picked.full, picked.tier);
|
|
517
|
+
}
|
|
518
|
+
const fromCatalog = entries.find((e) => e.full.toLowerCase() === needle);
|
|
519
|
+
return toResolved(fromCatalog?.full ?? base, fromCatalog?.tier ?? null);
|
|
520
|
+
}
|
|
521
|
+
|
|
337
522
|
/**
|
|
338
523
|
* Resolve a friendly alias / partial name to an argv-facing {model, effort?}.
|
|
339
524
|
* Returns null only when the family is unrecognized; the caller then passes
|
|
@@ -354,7 +539,7 @@ export function resolveModel(
|
|
|
354
539
|
const exact = entries.find((e) => e.full.toLowerCase() === lower);
|
|
355
540
|
if (exact) return toResolved(exact.full, exact.tier);
|
|
356
541
|
|
|
357
|
-
// 1b. Static short alias ("sonnet" / "opus"
|
|
542
|
+
// 1b. Static short alias ("sonnet" / "opus"). Checked
|
|
358
543
|
// before the family parser because none of these names contain
|
|
359
544
|
// "flash" or "pro" and would otherwise return null below. The
|
|
360
545
|
// resolved full string is then re-validated against the catalog
|
|
@@ -364,9 +549,12 @@ export function resolveModel(
|
|
|
364
549
|
// so the "live entries win" guarantee holds even when agy lists
|
|
365
550
|
// the model under different casing than the overlay.
|
|
366
551
|
if (STATIC_SHORT_ALIAS.has(lower)) {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
552
|
+
return resolveTieredBase(
|
|
553
|
+
STATIC_SHORT_ALIAS.get(lower) as string,
|
|
554
|
+
entries,
|
|
555
|
+
defaultThinking,
|
|
556
|
+
preferredTier,
|
|
557
|
+
);
|
|
370
558
|
}
|
|
371
559
|
|
|
372
560
|
// 2. Parse the alias.
|
|
@@ -384,7 +572,19 @@ export function resolveModel(
|
|
|
384
572
|
if (!family && (/gemini/.test(lower) || lower === "" || lower === "default")) {
|
|
385
573
|
family = "flash";
|
|
386
574
|
}
|
|
387
|
-
if (!family)
|
|
575
|
+
if (!family) {
|
|
576
|
+
// A bare base slug the provider itself advertises ("claude-sonnet-5-5",
|
|
577
|
+
// the id behind the antigravity/ entry in pi's picker) is invalid
|
|
578
|
+
// upstream without an effort: resolve it to the nearest tier variant
|
|
579
|
+
// exactly like the short aliases.
|
|
580
|
+
if (
|
|
581
|
+
entries.some((e) => e.full.toLowerCase().startsWith(`${lower}-`)) ||
|
|
582
|
+
entries.some((e) => e.full.toLowerCase() === lower)
|
|
583
|
+
) {
|
|
584
|
+
return resolveTieredBase(lower, entries, defaultThinking, preferredTier);
|
|
585
|
+
}
|
|
586
|
+
return null; // unknown family -> let agy handle it
|
|
587
|
+
}
|
|
388
588
|
|
|
389
589
|
// 3. Filter by family.
|
|
390
590
|
let candidates = entries.filter((e) => e.family === family);
|
|
@@ -525,10 +725,9 @@ async function discoverModels(binary: string): Promise<ModelEntry[]> {
|
|
|
525
725
|
finish("");
|
|
526
726
|
}, DISCOVERY_TIMEOUT_MS);
|
|
527
727
|
});
|
|
528
|
-
return
|
|
529
|
-
.split("\n")
|
|
530
|
-
|
|
531
|
-
.filter((e): e is ModelEntry => e !== null);
|
|
728
|
+
return filterHiddenModels(
|
|
729
|
+
text.split("\n").map(parseModelLine).filter((e): e is ModelEntry => e !== null),
|
|
730
|
+
);
|
|
532
731
|
} catch {
|
|
533
732
|
return [];
|
|
534
733
|
}
|
|
@@ -776,11 +975,19 @@ export default async function (pi: ExtensionAPI) {
|
|
|
776
975
|
// before (standalone tool).
|
|
777
976
|
if (isBridgeInstalled()) return;
|
|
778
977
|
|
|
978
|
+
// Orphan sweep for the plan-mode reviewer agents (SIGKILL can skip the
|
|
979
|
+
// run's finally): same pid-marker doctrine as the bridge's web delegates.
|
|
980
|
+
try {
|
|
981
|
+
sweepStaleAskAgents();
|
|
982
|
+
} catch {
|
|
983
|
+
// a sweep failure must never block registration
|
|
984
|
+
}
|
|
985
|
+
|
|
779
986
|
const binary = resolveAgy();
|
|
780
987
|
// Discovered once at load; frozen for the session. Run /reload after an
|
|
781
988
|
// `agy update` to refresh. Failure is non-fatal: resolveModel falls back
|
|
782
989
|
// to passthrough so exact slugs typed by the user still work. The static
|
|
783
|
-
// alias overlay (sonnet / opus
|
|
990
|
+
// alias overlay (sonnet / opus) is merged on top so those
|
|
784
991
|
// aliases resolve even when agy doesn't surface them in the live catalog.
|
|
785
992
|
const discovered = mergeCatalog(await discoverModels(binary).catch(() => []));
|
|
786
993
|
|
|
@@ -954,7 +1161,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
954
1161
|
],
|
|
955
1162
|
{
|
|
956
1163
|
description:
|
|
957
|
-
"agy execution mode. 'plan' = review-only
|
|
1164
|
+
"agy execution mode. 'plan' = review-only (--mode plan in a temporary restricted agent: file-editing tools are hard-blocked; arbitrary shell commands remain prompt-guarded only; with skipPermissions on, commands still run for analysis). 'accept-edits' = agy applies edits directly (--mode accept-edits, default). For agy's orthogonal --sandbox shell-containment flag, set the AGY_EXTRA_ARGS env var.",
|
|
958
1165
|
default: "accept-edits",
|
|
959
1166
|
},
|
|
960
1167
|
),
|
|
@@ -1162,6 +1369,19 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1162
1369
|
const snapshot = isContinuation ? null : snapshotConversations(CONVERSATIONS_DIR);
|
|
1163
1370
|
|
|
1164
1371
|
const mode: Mode = (params.mode as Mode | undefined) ?? "accept-edits";
|
|
1372
|
+
// Plan runs stage a restricted reviewer agent: its tools list has NO
|
|
1373
|
+
// file-editing tool, which is the hard edit block (the prompt guard
|
|
1374
|
+
// alone was observed failing once - a sub-agent still edited files).
|
|
1375
|
+
// Staging failure degrades to the legacy fallback: no agent, no skip
|
|
1376
|
+
// flag, the stricter command-forbidding guard.
|
|
1377
|
+
let reviewerAgent: { name: string; dir: string } | null = null;
|
|
1378
|
+
if (mode === "plan") {
|
|
1379
|
+
try {
|
|
1380
|
+
reviewerAgent = stageReviewerAgent(askAgentsRoot());
|
|
1381
|
+
} catch {
|
|
1382
|
+
reviewerAgent = null;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1165
1385
|
// digest default: on for plan (review-only contexts where full file
|
|
1166
1386
|
// contents are noise), off for accept-edits (agy applies edits and
|
|
1167
1387
|
// may need richer context for diffs).
|
|
@@ -1169,9 +1389,12 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1169
1389
|
typeof params.digest === "boolean"
|
|
1170
1390
|
? params.digest
|
|
1171
1391
|
: mode === "plan";
|
|
1172
|
-
const finalPrompt: string =
|
|
1173
|
-
|
|
1174
|
-
|
|
1392
|
+
const finalPrompt: string = buildFinalPrompt(
|
|
1393
|
+
params.prompt,
|
|
1394
|
+
mode,
|
|
1395
|
+
useDigest,
|
|
1396
|
+
reviewerAgent !== null,
|
|
1397
|
+
);
|
|
1175
1398
|
|
|
1176
1399
|
// Opt-in full-context export (isolated stays the default).
|
|
1177
1400
|
let contextFile: string | null = null;
|
|
@@ -1197,22 +1420,30 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1197
1420
|
: finalPrompt;
|
|
1198
1421
|
|
|
1199
1422
|
const args: string[] = ["--add-dir", cwd];
|
|
1200
|
-
const
|
|
1423
|
+
const extraRaw = extraArgs();
|
|
1424
|
+
// Fail-closed on the fallback path: AGY_EXTRA_ARGS lands before the
|
|
1425
|
+
// mode flags, so an env-injected skip flag would re-arm what the
|
|
1426
|
+
// plan-mode rule strips. Only the enforced agent makes the flag safe.
|
|
1427
|
+
const extra =
|
|
1428
|
+
mode === "plan" && !reviewerAgent
|
|
1429
|
+
? extraRaw.filter((a) => a !== "--dangerously-skip-permissions")
|
|
1430
|
+
: extraRaw;
|
|
1201
1431
|
if (extra.length) args.push(...extra);
|
|
1202
1432
|
if (resolved.model) args.push("--model", resolved.model);
|
|
1203
1433
|
if (resolved.effort) args.push("--effort", resolved.effort);
|
|
1204
1434
|
args.push("--mode", mode);
|
|
1435
|
+
if (reviewerAgent) args.push("--agent", reviewerAgent.name);
|
|
1205
1436
|
// accept-edits auto-approves file edits but NOT shell commands, so a
|
|
1206
1437
|
// run_command would hang on an unanswerable y/n prompt in non-interactive
|
|
1207
1438
|
// -p mode. Honor the shared permissions setting (same knob as the bridge).
|
|
1208
|
-
// Plan
|
|
1209
|
-
//
|
|
1210
|
-
//
|
|
1211
|
-
//
|
|
1212
|
-
//
|
|
1213
|
-
//
|
|
1214
|
-
//
|
|
1215
|
-
if (mode !== "plan"
|
|
1439
|
+
// Plan runs WITH the reviewer agent: the flag is safe again because the
|
|
1440
|
+
// agent's toolset has no file-editing tool - the 2026-09-25 write
|
|
1441
|
+
// incident rode edit tools that existed then; probed 2026-09-28 the
|
|
1442
|
+
// restricted toolset reports "none" for edits even with the flag. The
|
|
1443
|
+
// flag is what lets analysis commands (builds, test runners) run for
|
|
1444
|
+
// users without allow rules. Plan fallback (staging failed): strict
|
|
1445
|
+
// legacy behavior, no flag - the prompt guard is all that is left.
|
|
1446
|
+
if (config.skipPermissions && (mode !== "plan" || reviewerAgent !== null)) {
|
|
1216
1447
|
args.push("--dangerously-skip-permissions");
|
|
1217
1448
|
}
|
|
1218
1449
|
if (isContinuation) args.push("--conversation", rawConvId as string);
|
|
@@ -1454,7 +1685,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1454
1685
|
const note = [
|
|
1455
1686
|
"agy exited cleanly but produced no output.",
|
|
1456
1687
|
details.stderr.trim() ? `stderr: ${details.stderr.trim()}` : null,
|
|
1457
|
-
"Common cause: a tool call needed a permission that headless mode cannot prompt for (typically the command gate in plan mode), so it was auto-denied
|
|
1688
|
+
"Common cause: a tool call needed a permission that headless mode cannot prompt for (typically the command gate in plan mode), so it was auto-denied and the turn ended with no answer. Recovery: retry in plan mode with all needed content inlined in the prompt - plan runs cannot fetch it, commands are denied. Or rerun outside plan mode with skipPermissions, or add your own permissions.allow rules in ~/.gemini/antigravity-cli/settings.json.",
|
|
1458
1689
|
]
|
|
1459
1690
|
.filter(Boolean)
|
|
1460
1691
|
.join(" ");
|
|
@@ -1495,6 +1726,11 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1495
1726
|
fs.unlinkSync(contextFile);
|
|
1496
1727
|
} catch {}
|
|
1497
1728
|
}
|
|
1729
|
+
if (reviewerAgent) {
|
|
1730
|
+
try {
|
|
1731
|
+
fs.rmSync(reviewerAgent.dir, { recursive: true, force: true });
|
|
1732
|
+
} catch {}
|
|
1733
|
+
}
|
|
1498
1734
|
}
|
|
1499
1735
|
},
|
|
1500
1736
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@estebanforge/pi-ask-antigravity",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.10",
|
|
4
4
|
"description": "Pi tool that delegates a self-contained sub-task to Google Antigravity's agy CLI (the CLI for Gemini). Friendly model aliases (flash/pro/gemini), configurable default model + thinking, and the AskAntigravity delegation tool.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
]
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@earendil-works/pi-coding-agent": "
|
|
42
|
+
"@earendil-works/pi-coding-agent": "^1.0.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependenciesMeta": {
|
|
45
45
|
"@earendil-works/pi-coding-agent": {
|