@estebanforge/pi-antigravity-bridge 1.7.4 → 1.7.6
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 +13 -0
- package/package.json +1 -1
- package/src/ask-tool.ts +185 -21
- package/src/web-tools.ts +10 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.7.6] - 2026-09-28
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **Plan-mode delegations enforce read-only instead of promising it.** The old design withheld the skip flag and relied on a prompt asking nicely; the field disagreed once — a plan delegation with the flag re-injected through the environment still edited files, because a prompt guard is a request, not a wall. Plan-mode runs now stage a temporary agy agent under `~/.gemini/config/agents/pi-bridge-ask-*` whose `tools:` list carries only `view_file` and `run_command`: with no file-editing tool in the toolset the model cannot emit an edit call at all (probed on agy 1.2.12 with and without `--dangerously-skip-permissions` — the restricted toolset reports "none" for edits both ways). The staged agent's `commandExecutionPolicy: auto` is what keeps reviews useful: analysis commands (git, rg, test runners) run headless for every user, with no `permissions.allow` edits — the per-user-config approach was rejected as unshippable for a product. With the agent staged, the skip flag returns under the same `skipPermissions` knob (default on): its only real danger was auto-approving edit calls, and none exist to approve. The prompt guard switches to a mutation-forbidding text (read-only commands allowed), and the tool description now states the true residual plainly: file-editing tools are hard-blocked, arbitrary shell execution under the flag stays prompt-guarded alone. Staging failure degrades fail-closed — no agent, no flag, the stricter all-commands-forbidden guard, with an env-injected skip flag stripped from `AGY_EXTRA_ARGS` on that path only. Agent dirs are nonce-named, pid-marked, removed in the run's finally, and orphans swept at registration (the web-delegate sweep gained a prefix parameter so both doctrines share one implementation).
|
|
10
|
+
- **Headless plan prompts say what they cannot do.** `agy -p` cannot answer permission prompts, and in plan mode a shell-command attempt is soft-denied: the run ends cleanly with nothing on stdout and the reason only on stderr. Plan prompts now append an explicit constraint — no shell commands, answer from the material in the prompt, state exactly what is missing — converting the silent death into a visible missing-content request (probed: the previously-fatal prompt shape now answers "Missing information: ..." in about ten seconds). With the staged reviewer agent above, this guard yields to the mutation-forbidding variant; it remains the rule on the staging-failure fallback path.
|
|
11
|
+
|
|
12
|
+
## [1.7.5] - 2026-09-25
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **Exit 0 with no output is a failure, not a silent success.** A headless `agy -p` run can end cleanly with nothing on stdout when a delegated tool call hits a permission gate the headless run cannot prompt for. The plan-mode fix (withholding the skip flag in plan) makes the command gate the common case: the call is auto-denied, the reason goes only to stderr, agy exits 0, and the execute path fell through to success, returning just the conversation footer. The delegating model read that as "returned only a conversationId, zero output, no error" (silent-failure handoff, 2026-09-25). An empty answer now returns a loud failure note carrying the redacted stderr plus the remedy (allow-list the command under `permissions.allow` in the CLI settings, or rerun outside plan mode), the ask-end log marks the run warn with an `empty` flag so error-only default logging records what the chat saw, and a new `details.empty` marker flips the TUI result row to its error glyph instead of a green checkmark. Three new tests pin both stderr shapes and the glyph flip through the fake-binary stub.
|
|
17
|
+
|
|
5
18
|
## [1.7.4] - 2026-09-25
|
|
6
19
|
|
|
7
20
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@estebanforge/pi-antigravity-bridge",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.6",
|
|
4
4
|
"description": "Gemini provider for Pi on the Antigravity ACP server (official Google ACP) or the stream-json agy CLI. antigravity/* models in Pi's /model picker, no-patch MCP bridge: agy runs Pi's tools. ToS safe to use.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
package/src/ask-tool.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// nothing (see its defer guard). This module is the single source of truth.
|
|
13
13
|
|
|
14
14
|
import { spawn } from "node:child_process";
|
|
15
|
+
import { randomBytes } from "node:crypto";
|
|
15
16
|
import * as fs from "node:fs";
|
|
16
17
|
import * as path from "node:path";
|
|
17
18
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
@@ -28,6 +29,7 @@ import { loadConfig, type AgyMode, type ThinkingTier } from "./config.js";
|
|
|
28
29
|
import { redactText } from "./redact.js";
|
|
29
30
|
import { acquireBridgeSuppression } from "./mcp-registration.js";
|
|
30
31
|
import { AGY_EFFORT_ORDER, spawnAgyModelsRaw, toAgyEffort } from "./models.js";
|
|
32
|
+
import { sweepStaleWebAgents, webAgentsRoot } from "./web-tools.js";
|
|
31
33
|
|
|
32
34
|
// --- Constants -------------------------------------------------------------
|
|
33
35
|
|
|
@@ -78,7 +80,7 @@ TWO MODES (you choose):
|
|
|
78
80
|
- **Continued conversation**: pass the conversationId returned in the PREVIOUS call's details (details.conversationId). agy resumes that conversation with full context intact.
|
|
79
81
|
|
|
80
82
|
EXECUTION MODES (param: mode):
|
|
81
|
-
- **plan**: agy reviews and plans without writing. Use for cross-review and read-only tasks. Enforced: plan runs
|
|
83
|
+
- **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.
|
|
82
84
|
- **accept-edits** (default): agy applies edits directly inside the workspace.
|
|
83
85
|
|
|
84
86
|
COMPACT OUTPUT (param: digest): when true, the prompt is prefixed to request compact digests instead of full file contents. Defaults on for plan, off for accept-edits.
|
|
@@ -270,6 +272,109 @@ function extraArgs(): string[] {
|
|
|
270
272
|
return raw ? raw.split(/\s+/).filter((s) => s.length > 0) : [];
|
|
271
273
|
}
|
|
272
274
|
|
|
275
|
+
// --- Prompt assembly -------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
/** Guard for plan runs backed by the restricted reviewer agent. Difference
|
|
278
|
+
* to PLAN_HEADLESS_GUARD: commands are the analysis capability here (the
|
|
279
|
+
* agent's commandExecutionPolicy auto-approves them headless, probed
|
|
280
|
+
* 2026-09-28), so the guard forbids FILE MUTATION - by tool or by shell -
|
|
281
|
+
* and keeps the answer-from-material discipline. */
|
|
282
|
+
export const AGENT_REVIEW_GUARD = [
|
|
283
|
+
"",
|
|
284
|
+
"--- Review constraints ---",
|
|
285
|
+
"- Read-only review. Do not create, modify, or delete any files, including through shell commands (no redirects, tee, rm, mv, git commit).",
|
|
286
|
+
"- Read-only commands (git log, git diff, git show, rg, ls, cat, test runners) are allowed for analysis.",
|
|
287
|
+
"- Work from the material provided in this prompt; if information you need is missing, state exactly what is missing in your answer.",
|
|
288
|
+
].join("\n");
|
|
289
|
+
|
|
290
|
+
/** Appended to every headless plan-mode prompt. `agy -p` cannot answer
|
|
291
|
+
* permission prompts: in plan mode a run_command attempt is soft-denied and
|
|
292
|
+
* the turn ends AT the denial (exit 0, empty stdout, notice only on stderr -
|
|
293
|
+
* the empty-output branch in execute). Probed 2026-09-28 on agy 1.2.12:
|
|
294
|
+
* allow rules ARE consulted (a verbatim allow-listed `git log` runs), but
|
|
295
|
+
* --sandbox does NOT relax the command gate, so the only input every user
|
|
296
|
+
* is guaranteed is the prompt itself. The guard steers the model to answer
|
|
297
|
+
* from that material. accept-edits runs keep their tools and never get it. */
|
|
298
|
+
export const PLAN_HEADLESS_GUARD = [
|
|
299
|
+
"",
|
|
300
|
+
"--- Headless session constraints ---",
|
|
301
|
+
"- Do not run shell commands. Command execution is denied in this session, and any attempt ends the session immediately with no answer.",
|
|
302
|
+
"- Work only from the material provided in this prompt.",
|
|
303
|
+
"- If information you need is missing, state exactly what is missing in your answer instead of trying to fetch it.",
|
|
304
|
+
].join("\n");
|
|
305
|
+
|
|
306
|
+
/** Assemble the prompt sent to agy: digest marker first (existing behavior),
|
|
307
|
+
* then the caller's prompt, then the plan-mode guard last - the position
|
|
308
|
+
* the model reads with the most recency. No user-config mutation: this is
|
|
309
|
+
* the only plan-mode lever the tool itself owns. */
|
|
310
|
+
export function buildFinalPrompt(
|
|
311
|
+
prompt: string,
|
|
312
|
+
mode: AgyMode,
|
|
313
|
+
digest: boolean,
|
|
314
|
+
/** True when the restricted reviewer agent is staged: its empty edit
|
|
315
|
+
* toolset is the hard block, so the prompt may ALLOW read-only commands. */
|
|
316
|
+
agentEnforced = false,
|
|
317
|
+
): string {
|
|
318
|
+
let out = digest ? `(Use compact digests, not full file contents.)\n${prompt}` : prompt;
|
|
319
|
+
if (mode === "plan") out += agentEnforced ? AGENT_REVIEW_GUARD : PLAN_HEADLESS_GUARD;
|
|
320
|
+
return out;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// --- Plan-mode reviewer agent ----------------------------------------------
|
|
324
|
+
|
|
325
|
+
/** Temp agent dirs share the ~/.gemini/config/agents discovery root with the
|
|
326
|
+
* web delegates; AGY_AGENTS_ROOT overrides for tests and sandboxes. */
|
|
327
|
+
export function askAgentsRoot(): string {
|
|
328
|
+
return process.env.AGY_AGENTS_ROOT ?? webAgentsRoot();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const ASK_AGENT_PREFIX = "pi-bridge-ask-";
|
|
332
|
+
const ASK_AGENT_TOOLS = ["view_file", "run_command"];
|
|
333
|
+
|
|
334
|
+
/** agent.md for the plan-mode reviewer. The tools list is the ENFORCEMENT:
|
|
335
|
+
* with no file-editing tool present the model cannot emit an edit call at
|
|
336
|
+
* all (probed 2026-09-28 on agy 1.2.12: the toolset reports "none" for
|
|
337
|
+
* edits, with and without the skip flag). commandExecutionPolicy auto is
|
|
338
|
+
* what lets read commands run headless with no user allow rules. Never add
|
|
339
|
+
* a write-capable tool here. */
|
|
340
|
+
export function reviewerAgentMd(name: string): string {
|
|
341
|
+
return [
|
|
342
|
+
"---",
|
|
343
|
+
`name: ${name}`,
|
|
344
|
+
"description: Temporary Pi plan-mode reviewer",
|
|
345
|
+
"mainAgent: true",
|
|
346
|
+
"subagent: false",
|
|
347
|
+
"model: inherit",
|
|
348
|
+
"excludeDefaultComponents: true",
|
|
349
|
+
"inheritCustomizations: false",
|
|
350
|
+
"inheritMcp: false",
|
|
351
|
+
"commandExecutionPolicy: auto",
|
|
352
|
+
"tools:",
|
|
353
|
+
...ASK_AGENT_TOOLS.map((t) => ` - ${t}`),
|
|
354
|
+
"skills: []",
|
|
355
|
+
"rules: []",
|
|
356
|
+
"agents: []",
|
|
357
|
+
"mcpServers: []",
|
|
358
|
+
"---",
|
|
359
|
+
"",
|
|
360
|
+
"You are a strict read-only code reviewer. Review, analyze, and plan; never modify anything.",
|
|
361
|
+
"",
|
|
362
|
+
].join("\n");
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Stage one unique reviewer agent (agent.md + pid marker) under root. Same
|
|
366
|
+
* hygiene doctrine as the web delegates: nonce-named, cleaned in finally,
|
|
367
|
+
* orphans swept by pid marker at registration. */
|
|
368
|
+
export function stageReviewerAgent(root: string): { name: string; dir: string } {
|
|
369
|
+
fs.mkdirSync(root, { recursive: true });
|
|
370
|
+
const name = `${ASK_AGENT_PREFIX}${process.pid}-${Date.now().toString(36)}-${randomBytes(4).toString("hex")}`;
|
|
371
|
+
const dir = path.join(root, name);
|
|
372
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
373
|
+
fs.writeFileSync(path.join(dir, "agent.md"), reviewerAgentMd(name), { mode: 0o600 });
|
|
374
|
+
fs.writeFileSync(path.join(dir, ".pid"), `${process.pid}\n`, { mode: 0o600 });
|
|
375
|
+
return { name, dir };
|
|
376
|
+
}
|
|
377
|
+
|
|
273
378
|
const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
|
|
274
379
|
|
|
275
380
|
// --- Registration ----------------------------------------------------------
|
|
@@ -284,6 +389,13 @@ export async function registerAskAntigravityTool(
|
|
|
284
389
|
* verbose tier; info/warn/error always land. */
|
|
285
390
|
log?: (event: string, data?: unknown, level?: "debug" | "info" | "warn" | "error") => void,
|
|
286
391
|
): Promise<void> {
|
|
392
|
+
// Orphan sweep for the plan-mode reviewer agents (SIGKILL can skip the
|
|
393
|
+
// run's finally): same pid-marker doctrine as the web delegates.
|
|
394
|
+
try {
|
|
395
|
+
sweepStaleWebAgents(askAgentsRoot(), Date.now(), ASK_AGENT_PREFIX);
|
|
396
|
+
} catch {
|
|
397
|
+
// a sweep failure must never block registration
|
|
398
|
+
}
|
|
287
399
|
pi.registerTool({
|
|
288
400
|
name: "AskAntigravity",
|
|
289
401
|
label: "Ask Antigravity",
|
|
@@ -390,7 +502,8 @@ export async function registerAskAntigravityTool(
|
|
|
390
502
|
}
|
|
391
503
|
|
|
392
504
|
const body = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
393
|
-
const errored =
|
|
505
|
+
const errored =
|
|
506
|
+
d?.exitCode !== 0 || !!d?.aborted || !!d?.timedOut || !!d?.empty;
|
|
394
507
|
|
|
395
508
|
let text = errored
|
|
396
509
|
? theme.fg("error", "✗ AskAntigravity error")
|
|
@@ -493,11 +606,27 @@ export async function registerAskAntigravityTool(
|
|
|
493
606
|
const snapshot = isContinuation ? null : snapshotConversations();
|
|
494
607
|
|
|
495
608
|
const mode: AgyMode = (params.mode as AgyMode | undefined) ?? "accept-edits";
|
|
609
|
+
// Plan runs stage a restricted reviewer agent: its tools list has NO
|
|
610
|
+
// file-editing tool, which is the hard edit block (the prompt guard
|
|
611
|
+
// alone was observed failing once - a sub-agent still edited files).
|
|
612
|
+
// Staging failure degrades to the legacy fallback: no agent, no skip
|
|
613
|
+
// flag, the stricter command-forbidding guard.
|
|
614
|
+
let reviewerAgent: { name: string; dir: string } | null = null;
|
|
615
|
+
if (mode === "plan") {
|
|
616
|
+
try {
|
|
617
|
+
reviewerAgent = stageReviewerAgent(askAgentsRoot());
|
|
618
|
+
} catch {
|
|
619
|
+
reviewerAgent = null;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
496
622
|
const useDigest: boolean =
|
|
497
623
|
typeof params.digest === "boolean" ? params.digest : mode === "plan";
|
|
498
|
-
const finalPrompt: string =
|
|
499
|
-
|
|
500
|
-
|
|
624
|
+
const finalPrompt: string = buildFinalPrompt(
|
|
625
|
+
params.prompt,
|
|
626
|
+
mode,
|
|
627
|
+
useDigest,
|
|
628
|
+
reviewerAgent !== null,
|
|
629
|
+
);
|
|
501
630
|
|
|
502
631
|
// Opt-in full-context export (isolated stays the default).
|
|
503
632
|
let contextFile: string | null = null;
|
|
@@ -528,22 +657,31 @@ export async function registerAskAntigravityTool(
|
|
|
528
657
|
{ model: resolved.model, thinking: resolved.effort ?? config.defaultThinking, mode, digest: useDigest, continue: isContinuation, timeoutMin },
|
|
529
658
|
"info",
|
|
530
659
|
);
|
|
531
|
-
const
|
|
660
|
+
const extraRaw = extraArgs();
|
|
661
|
+
// Fail-closed on the fallback path: AGY_EXTRA_ARGS lands before the
|
|
662
|
+
// mode flags, so an env-injected skip flag would re-arm what the
|
|
663
|
+
// plan-mode rule strips. Only the enforced agent makes the flag safe.
|
|
664
|
+
const extra =
|
|
665
|
+
mode === "plan" && !reviewerAgent
|
|
666
|
+
? extraRaw.filter((a) => a !== "--dangerously-skip-permissions")
|
|
667
|
+
: extraRaw;
|
|
532
668
|
if (extra.length) args.push(...extra);
|
|
533
669
|
if (resolved.model) args.push("--model", resolved.model);
|
|
534
670
|
if (resolved.effort) args.push("--effort", resolved.effort);
|
|
535
671
|
args.push("--mode", mode);
|
|
536
|
-
|
|
537
|
-
//
|
|
538
|
-
//
|
|
539
|
-
//
|
|
540
|
-
//
|
|
541
|
-
//
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
//
|
|
545
|
-
//
|
|
546
|
-
|
|
672
|
+
if (reviewerAgent) args.push("--agent", reviewerAgent.name);
|
|
673
|
+
// Honor the shared permissions setting (same knob as the provider).
|
|
674
|
+
// accept-edits: the flag keeps commands from hanging on an
|
|
675
|
+
// unanswerable prompt in -p mode. Plan runs WITH the reviewer agent:
|
|
676
|
+
// the flag is safe again because the agent's toolset has no
|
|
677
|
+
// file-editing tool - the 2026-09-25 write incident rode edit tools
|
|
678
|
+
// that existed then; probed 2026-09-28 the restricted toolset reports
|
|
679
|
+
// "none" for edits even with the flag, and plan discipline turned a
|
|
680
|
+
// create-a-file request into a proposed plan instead of a write. The
|
|
681
|
+
// flag is what lets analysis commands (builds, test runners) run for
|
|
682
|
+
// users without allow rules. Plan fallback (staging failed): strict
|
|
683
|
+
// legacy behavior, no flag - the prompt guard is all that is left.
|
|
684
|
+
if (config.skipPermissions !== false && (mode !== "plan" || reviewerAgent !== null)) {
|
|
547
685
|
args.push("--dangerously-skip-permissions");
|
|
548
686
|
}
|
|
549
687
|
if (isContinuation) args.push("--conversation", rawConvId as string);
|
|
@@ -710,10 +848,11 @@ export async function registerAskAntigravityTool(
|
|
|
710
848
|
details.aborted = outcome.aborted;
|
|
711
849
|
details.timedOut = outcome.timedOut;
|
|
712
850
|
details.durationMs = Date.now() - start;
|
|
851
|
+
const text = out.trim();
|
|
713
852
|
log?.(
|
|
714
853
|
"ask-end",
|
|
715
|
-
{ exitCode: outcome.exitCode, aborted: outcome.aborted, timedOut: outcome.timedOut, durationMs: details.durationMs, conversationId: details.conversationId },
|
|
716
|
-
outcome.exitCode !== 0 || outcome.aborted || outcome.timedOut ? "warn" : "info",
|
|
854
|
+
{ exitCode: outcome.exitCode, aborted: outcome.aborted, timedOut: outcome.timedOut, empty: !text, durationMs: details.durationMs, conversationId: details.conversationId },
|
|
855
|
+
outcome.exitCode !== 0 || outcome.aborted || outcome.timedOut || !text ? "warn" : "info",
|
|
717
856
|
);
|
|
718
857
|
|
|
719
858
|
if (!isContinuation && !details.conversationId && snapshot) {
|
|
@@ -727,8 +866,6 @@ export async function registerAskAntigravityTool(
|
|
|
727
866
|
}
|
|
728
867
|
}
|
|
729
868
|
|
|
730
|
-
const text = out.trim();
|
|
731
|
-
|
|
732
869
|
if (outcome.aborted) {
|
|
733
870
|
return {
|
|
734
871
|
content: [
|
|
@@ -763,6 +900,25 @@ export async function registerAskAntigravityTool(
|
|
|
763
900
|
};
|
|
764
901
|
}
|
|
765
902
|
|
|
903
|
+
// Exit 0 with nothing on stdout is still a failure for the
|
|
904
|
+
// delegator: headless agy auto-denies a permission-gated tool call
|
|
905
|
+
// (e.g. the command gate in plan mode), prints the reason only to
|
|
906
|
+
// stderr, and ends cleanly. Falling through to the success path here
|
|
907
|
+
// returned just the conversation footer, which read as an empty
|
|
908
|
+
// success (silent-failure bug found 2026-09-25).
|
|
909
|
+
if (!text) {
|
|
910
|
+
details.empty = true;
|
|
911
|
+
const stderr = redactText(details.stderr.trim());
|
|
912
|
+
const note = [
|
|
913
|
+
"agy exited cleanly but produced no output.",
|
|
914
|
+
stderr ? `stderr: ${stderr}` : null,
|
|
915
|
+
"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.",
|
|
916
|
+
]
|
|
917
|
+
.filter(Boolean)
|
|
918
|
+
.join(" ");
|
|
919
|
+
return { content: [{ type: "text", text: note }], details };
|
|
920
|
+
}
|
|
921
|
+
|
|
766
922
|
onUpdate?.({ content: [{ type: "text", text: "" }], details: { ...details } });
|
|
767
923
|
const footer = details.conversationId
|
|
768
924
|
? `\n\n[agy conversationId: ${details.conversationId} - pass as conversationId to continue this conversation]`
|
|
@@ -784,6 +940,11 @@ export async function registerAskAntigravityTool(
|
|
|
784
940
|
fs.unlinkSync(contextFile);
|
|
785
941
|
} catch {}
|
|
786
942
|
}
|
|
943
|
+
if (reviewerAgent) {
|
|
944
|
+
try {
|
|
945
|
+
fs.rmSync(reviewerAgent.dir, { recursive: true, force: true });
|
|
946
|
+
} catch {}
|
|
947
|
+
}
|
|
787
948
|
}
|
|
788
949
|
},
|
|
789
950
|
});
|
|
@@ -866,6 +1027,9 @@ interface AgyDetails {
|
|
|
866
1027
|
exitCode: number;
|
|
867
1028
|
aborted: boolean;
|
|
868
1029
|
timedOut: boolean;
|
|
1030
|
+
// Exit 0 with no answer on stdout (the empty-output failure branch).
|
|
1031
|
+
// renderResult flips to the error glyph on it.
|
|
1032
|
+
empty?: boolean;
|
|
869
1033
|
durationMs: number;
|
|
870
1034
|
stderr: string;
|
|
871
1035
|
}
|
package/src/web-tools.ts
CHANGED
|
@@ -37,10 +37,15 @@ export function webAgentsRoot(): string {
|
|
|
37
37
|
return path.join(os.homedir(), ".gemini", "config", "agents");
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
/** Remove leftover pi-bridge
|
|
41
|
-
* marker-less dirs only after a grace period (a live sibling
|
|
42
|
-
* between mkdir and its pid write). Never touches foreign
|
|
43
|
-
|
|
40
|
+
/** Remove leftover pi-bridge agent dirs matching `prefix`: dead-pid markers
|
|
41
|
+
* always, and marker-less dirs only after a grace period (a live sibling
|
|
42
|
+
* process may be between mkdir and its pid write). Never touches foreign
|
|
43
|
+
* agent dirs. The ask-tool reuses this for its reviewer agents. */
|
|
44
|
+
export function sweepStaleWebAgents(
|
|
45
|
+
root: string = webAgentsRoot(),
|
|
46
|
+
now = Date.now(),
|
|
47
|
+
prefix: string = WEB_AGENT_PREFIX,
|
|
48
|
+
): void {
|
|
44
49
|
let entries: string[];
|
|
45
50
|
try {
|
|
46
51
|
entries = readdirSync(root);
|
|
@@ -48,7 +53,7 @@ export function sweepStaleWebAgents(root: string = webAgentsRoot(), now = Date.n
|
|
|
48
53
|
return; // no agents dir yet: nothing to sweep
|
|
49
54
|
}
|
|
50
55
|
for (const entry of entries) {
|
|
51
|
-
if (!entry.startsWith(
|
|
56
|
+
if (!entry.startsWith(prefix)) continue;
|
|
52
57
|
try {
|
|
53
58
|
if (isStaleWebAgentDir(path.join(root, entry), now)) {
|
|
54
59
|
rmSync(path.join(root, entry), { recursive: true, force: true });
|