@estebanforge/pi-antigravity-bridge 1.7.3 → 1.7.5

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/src/driver.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  //
8
8
  // Recycle semantics: the child is killed and respawned when the next turn's
9
9
  // process profile (model / effort / mode / cwd / conversation) drifts from the
10
- // running one, mirroring tianzuo/pi-antigravity's driver. Stats and a bounded
10
+ // running one. Stats and a bounded
11
11
  // lifecycle log feed /agy doctor.
12
12
  //
13
13
  // The driver never talks to the MCP bridge directly: the provider owns the
@@ -20,6 +20,7 @@ import { parseAgyLine } from "./stream-events.js";
20
20
  import { isKnownNoiseLine, MAX_FRAME_BYTES, stripGluedNoise } from "./frame-guard.js";
21
21
  import { bridgeMcpConfigDir, bridgeMcpConfigExists } from "./mcp-server.js";
22
22
  import { gateHooksStaged } from "./approval-hook.js";
23
+ import { parkedTurnAnswer } from "./parked-turn.js";
23
24
  import { redactText } from "./redact.js";
24
25
  import type {
25
26
  AgyUsage,
@@ -62,6 +63,9 @@ interface ActiveTurn {
62
63
  response: string;
63
64
  usage?: AgyUsage;
64
65
  conversationId?: string;
66
+ /** Wall clock at turn creation; the parked-turn probe filters
67
+ * transcript steps to those written after this. */
68
+ startedAt: number;
65
69
  sawResult: boolean;
66
70
  /** Text-dedupe guard state (delta vs cumulative response_text). */
67
71
  cumulativeText: boolean | undefined;
@@ -71,6 +75,15 @@ interface ActiveTurn {
71
75
  parks: number;
72
76
  }
73
77
 
78
+ /** The skip-permissions flag actually passed to agy. Plan mode never
79
+ * carries it: the flag auto-approves every permission request including
80
+ * plan mode's own approval gate, silently turning review-only into full
81
+ * write access. #start (argv + profile) and #recycleCause (comparison)
82
+ * must both go through this, or plan turns recycle on every turn. */
83
+ function effectiveSkipPermissions(mode: DriverTurnRequest["mode"], skipPermissions: boolean): boolean {
84
+ return mode !== "plan" && skipPermissions;
85
+ }
86
+
74
87
  function emit(turn: ActiveTurn, activity: DriverActivity): void {
75
88
  if (turn.closed) return;
76
89
  if (turn.wake.length > 0) turn.wake.shift()!();
@@ -171,10 +184,7 @@ export class StreamDriver implements TurnDriver {
171
184
  // 0 disables the stall guard (config inactivityTimeoutMin: 0).
172
185
  if (idleMin > 0) {
173
186
  turn.idleTimer = setTimeout(() => {
174
- if (turn.closed) return;
175
- this.#log(`stall:${turn.id}`);
176
- this.#killChild();
177
- this.#failTurn(turn, `agy stalled for ${idleMin}m with no output`);
187
+ void this.#turnDeadlineGuard(turn, "stall", `agy stalled for ${idleMin}m with no output`);
178
188
  }, idleMin * 60_000);
179
189
  }
180
190
  }
@@ -279,6 +289,7 @@ export class StreamDriver implements TurnDriver {
279
289
  sawResult: false,
280
290
  cumulativeText: undefined,
281
291
  parks: 0,
292
+ startedAt: Date.now(),
282
293
  };
283
294
  if (request.signal) {
284
295
  turn.onAbort = () => {
@@ -306,19 +317,13 @@ export class StreamDriver implements TurnDriver {
306
317
  const totalMin = turn.request.timeoutMin ?? 10;
307
318
  if (totalMin > 0) {
308
319
  turn.overallTimer = setTimeout(() => {
309
- if (turn.closed) return;
310
- this.#log(`timeout:${turn.id}`);
311
- this.#killChild();
312
- this.#failTurn(turn, `agy exceeded the ${totalMin}m turn timeout`);
320
+ void this.#turnDeadlineGuard(turn, "timeout", `agy exceeded the ${totalMin}m turn timeout`);
313
321
  }, totalMin * 60_000);
314
322
  }
315
323
  const idleMin = turn.request.inactivityMin ?? 5;
316
324
  if (idleMin > 0) {
317
325
  turn.idleTimer = setTimeout(() => {
318
- if (turn.closed) return;
319
- this.#log(`stall:${turn.id}`);
320
- this.#killChild();
321
- this.#failTurn(turn, `agy stalled for ${idleMin}m with no output`);
326
+ void this.#turnDeadlineGuard(turn, "stall", `agy stalled for ${idleMin}m with no output`);
322
327
  }, idleMin * 60_000);
323
328
  }
324
329
  }
@@ -327,12 +332,21 @@ export class StreamDriver implements TurnDriver {
327
332
  this.#state = "starting";
328
333
  this.#generation += 1;
329
334
  const generation = this.#generation;
335
+ // Plan mode never carries the skip flag: the flag auto-approves ALL
336
+ // permission requests including plan mode's own approval gate, which
337
+ // would silently turn "review-only" into full write access (probed
338
+ // 2026-09-25: with the flag a plan session wrote files; without it,
339
+ // file and command attempts end exit 0 in ~20-30s with a "confirm
340
+ // plan" message). The profile stores the EFFECTIVE value so the
341
+ // recycle comparison stays consistent with the argv.
342
+ const skipPermissions = effectiveSkipPermissions(request.mode, request.skipPermissions);
330
343
  this.#profile = {
331
344
  cwd: request.cwd,
332
345
  model: request.model,
333
346
  effort: request.effort,
334
347
  mode: request.mode,
335
- skipPermissions: request.skipPermissions,
348
+ skipPermissions,
349
+ agent: request.agent,
336
350
  };
337
351
  this.#boundConversation = request.conversationId ?? undefined;
338
352
  this.#stderrTail = "";
@@ -346,7 +360,8 @@ export class StreamDriver implements TurnDriver {
346
360
  args.push("--model", request.model);
347
361
  if (request.effort) args.push("--effort", request.effort);
348
362
  args.push("--mode", request.mode);
349
- if (request.skipPermissions) args.push("--dangerously-skip-permissions");
363
+ if (request.agent) args.push("--agent", request.agent);
364
+ if (skipPermissions) args.push("--dangerously-skip-permissions");
350
365
  if (request.conversationId) args.push("--conversation", request.conversationId);
351
366
  args.push(
352
367
  "--input-format",
@@ -607,6 +622,40 @@ export class StreamDriver implements TurnDriver {
607
622
  }
608
623
  }
609
624
 
625
+ /** Stall / total-timeout guard shared by both timer sites. Kills the
626
+ * child FIRST (bumping the generation so a racing exit handler cannot
627
+ * settle the turn ERROR mid-probe), then probes the conversation's
628
+ * brain transcript for a clean final-response step written during the
629
+ * turn (parked turn: agy finished its answer but never streamed it).
630
+ * One found, the turn settles OK with the withheld answer instead of
631
+ * discarding finished work; either way the next turn respawns into a
632
+ * known state. */
633
+ async #turnDeadlineGuard(turn: ActiveTurn, label: string, message: string): Promise<void> {
634
+ if (turn.closed) return;
635
+ this.#log(`${label}:${turn.id}`);
636
+ this.#killChild();
637
+ let parked: string | undefined;
638
+ try {
639
+ parked = turn.conversationId
640
+ ? await parkedTurnAnswer(turn.conversationId, turn.startedAt)
641
+ : undefined;
642
+ } catch {
643
+ parked = undefined; // the guard must never throw
644
+ }
645
+ if (parked !== undefined) {
646
+ this.#settle(turn, {
647
+ conversationId: turn.conversationId,
648
+ status: "OK",
649
+ response: parked,
650
+ usage: turn.usage,
651
+ finished: true,
652
+ aborted: false,
653
+ });
654
+ return;
655
+ }
656
+ this.#failTurn(turn, message);
657
+ }
658
+
610
659
  #failTurn(turn: ActiveTurn, message: string): void {
611
660
  this.#settle(turn, {
612
661
  conversationId: turn.conversationId,
@@ -626,7 +675,10 @@ export class StreamDriver implements TurnDriver {
626
675
  if (cur.model !== next.model) return "model";
627
676
  if (cur.effort !== next.effort) return "effort";
628
677
  if (cur.mode !== next.mode) return "mode";
629
- if (cur.skipPermissions !== next.skipPermissions) return "permissions";
678
+ if (cur.skipPermissions !== effectiveSkipPermissions(next.mode, next.skipPermissions)) {
679
+ return "permissions";
680
+ }
681
+ if (cur.agent !== next.agent) return "agent";
630
682
  if (!next.conversationId) return this.#boundConversation ? "conversation-reset" : undefined;
631
683
  return next.conversationId === this.#boundConversation ? undefined : "conversation";
632
684
  }
package/src/models.ts CHANGED
@@ -25,6 +25,14 @@ import type { Api, Model, ThinkingLevel, ThinkingLevelMap } from "@earendil-work
25
25
 
26
26
  const DISCOVERY_TIMEOUT_MS = 8_000;
27
27
 
28
+ /** Stdout ceiling for `agy models`. Real output is a few KB of ASCII slugs;
29
+ * anything past 1 MiB is a runaway process. Fail closed to "" so the
30
+ * fallback catalog engages instead of letting the buffer grow without
31
+ * limit. The check counts decoded string length (UTF-16 code units), not
32
+ * raw bytes — a deliberate proxy, since the margin between a few KB of
33
+ * real output and 1 MiB dwarfs any encoding difference. */
34
+ export const MODELS_OUTPUT_CAP_BYTES = 1024 * 1024;
35
+
28
36
  /** Reasoning-effort tiers agy accepts via --effort. */
29
37
  export type AgyEffort = "low" | "medium" | "high";
30
38
 
@@ -104,28 +112,50 @@ export interface AgyModelEntry {
104
112
  efforts?: AgyEffort[];
105
113
  }
106
114
 
107
- /** Spawn `agy models` and return its raw stdout text. Returns "" on any
108
- * failure (non-zero exit, spawn error, or watchdog timeout). Bounded by
109
- * DISCOVERY_TIMEOUT_MS so a hung agy (auth prompt, network stall) can't
110
- * block extension load. Shared by the provider and the tool catalog so the
111
- * extension spawns `agy models` ONCE per load. */
112
- export async function spawnAgyModelsRaw(binary: string): Promise<string> {
115
+ /** Spawn a read-only agy listing subcommand and return its raw stdout text.
116
+ * Returns "" on any failure (non-zero exit, spawn error, watchdog timeout,
117
+ * or output cap). Bounded by timeoutMs (default DISCOVERY_TIMEOUT_MS) so a
118
+ * hung agy (auth prompt, network stall) can't block the caller, and by
119
+ * capBytes (default MODELS_OUTPUT_CAP_BYTES) so a runaway agy can't grow
120
+ * the buffer without limit. Shared by the model catalog (spawned ONCE per
121
+ * load, provider + tool catalog), the agent roster, and the quota fetch
122
+ * (which passes its own 30s budget: /usage answers in ~10s live). */
123
+ export async function spawnAgyRaw(
124
+ binary: string,
125
+ args: string[],
126
+ timeoutMs: number = DISCOVERY_TIMEOUT_MS,
127
+ capBytes: number = MODELS_OUTPUT_CAP_BYTES,
128
+ ): Promise<string> {
113
129
  try {
114
130
  return await new Promise<string>((resolve, reject) => {
115
- const proc = spawn(binary, ["models"], {
131
+ const proc = spawn(binary, args, {
116
132
  stdio: ["ignore", "pipe", "ignore"],
117
133
  shell: false,
118
134
  });
119
135
  proc.stdout?.setEncoding("utf8");
120
136
  let out = "";
121
137
  let done = false;
138
+ let capped = false;
122
139
  const finish = (v: string) => {
123
140
  if (done) return;
124
141
  done = true;
125
142
  clearTimeout(watchdog);
126
143
  resolve(v);
127
144
  };
128
- proc.stdout?.on("data", (d: string) => (out += d));
145
+ proc.stdout?.on("data", (d: string) => {
146
+ if (capped) return;
147
+ if (out.length + d.length > capBytes) {
148
+ capped = true;
149
+ try {
150
+ proc.kill("SIGKILL");
151
+ } catch {
152
+ /* already gone */
153
+ }
154
+ finish("");
155
+ return;
156
+ }
157
+ out += d;
158
+ });
129
159
  proc.on("error", (err) => {
130
160
  clearTimeout(watchdog);
131
161
  reject(err);
@@ -138,13 +168,18 @@ export async function spawnAgyModelsRaw(binary: string): Promise<string> {
138
168
  /* already gone */
139
169
  }
140
170
  finish("");
141
- }, DISCOVERY_TIMEOUT_MS);
171
+ }, timeoutMs);
142
172
  });
143
173
  } catch {
144
174
  return "";
145
175
  }
146
176
  }
147
177
 
178
+ /** Spawn `agy models`: the model catalog listing. */
179
+ export async function spawnAgyModelsRaw(binary: string): Promise<string> {
180
+ return spawnAgyRaw(binary, ["models"]);
181
+ }
182
+
148
183
  // --- catalog cache ----------------------------------------------------------
149
184
  //
150
185
  // `agy models` can take seconds (OAuth refresh, cold start) and its output
@@ -5,7 +5,7 @@
5
5
  //
6
6
  // Mutating tools (commands, edits, writes) and agy-specialty tools are never
7
7
  // re-executed - they replay through the display-only `antigravity` wrapper
8
- // tool. Ported from tianzuo/pi-antigravity lib/native-tools.ts (MIT).
8
+ // tool.
9
9
 
10
10
  export interface NativeToolCall {
11
11
  /** pi builtin tool name (`read`, `ls`, `grep`, `find`). */
@@ -0,0 +1,125 @@
1
+ // Parked-turn stall probe (stream-json engine only).
2
+ //
3
+ // The stall guard fails a turn after N minutes of stdout silence. But agy
4
+ // sometimes finishes its answer without streaming it (parked turn): the
5
+ // response only ever lands in the conversation's brain transcript. Before
6
+ // failing, this module reads the transcript tail and looks for a DONE,
7
+ // MODEL-sourced final-response step written DURING the turn. One found, the
8
+ // driver can settle the turn OK with the withheld answer instead of
9
+ // throwing away finished work.
10
+ //
11
+ // Live-verified format (agy 1.2.10, transcript_full.jsonl last line):
12
+ // {"step_index":1,"source":"MODEL","type":"PLANNER_RESPONSE",
13
+ // "status":"DONE","created_at":"2026-09-08T16:11:43Z","content":"..."}
14
+ // A corpus sweep (2026-09-24, 9205 response steps across all local
15
+ // transcripts) shows PLANNER_RESPONSE is the only *RESPONSE type agy
16
+ // writes, so the gate matches it exactly. A deeper sweep during the agy
17
+ // peer review (2026-09-25, 9547 steps) found 96.6% of PLANNER_RESPONSE
18
+ // steps carry tool_calls — they are intermediate plan-and-act steps whose
19
+ // content is status text ("Running test..."), NOT answers. The probe
20
+ // therefore requires the NEWEST step written during the turn to be a
21
+ // clean, tool-free response; any other newer step means the turn was
22
+ // still mid-flight. If a future agy renames the type, the probe degrades
23
+ // to the old stall-fail — the safe direction.
24
+ // The ACP engine writes no transcript under its brain tree (probed
25
+ // 2026-09-24) and reports turn completion itself, so this probe is
26
+ // deliberately stream-json-only.
27
+
28
+ import { open } from "node:fs/promises";
29
+ import { agyConversationDir } from "./agy-paths.js";
30
+
31
+ /** How much of the transcript tail to read. One final step is a few KiB;
32
+ * 512KiB covers it plus slack for big tool-step lines above it. */
33
+ export const PARKED_PROBE_MAX_BYTES = 512 * 1024;
34
+
35
+ /** Clock tolerance for "written during the turn": transcript timestamps are
36
+ * second-precision and may come from a slightly skewed source. */
37
+ const CREATED_AT_TOLERANCE_MS = 2_000;
38
+
39
+ /** Pull the parked answer out of a transcript tail. Scans from the END;
40
+ * the FIRST step written during the turn decides: a clean, tool-free DONE
41
+ * MODEL response yields its content; any other step from the turn (tool
42
+ * output, error, plan step with tool_calls) means no answer landed and
43
+ * the probe returns undefined. Steps older than the turn (clock tolerance
44
+ * 2s) end the scan. Malformed or foreign lines are skipped without
45
+ * deciding. Pure; exported for tests. */
46
+ export function extractParkedAnswer(
47
+ tail: string,
48
+ turnStartedMs: number,
49
+ ): string | undefined {
50
+ const lines = tail.split("\n");
51
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
52
+ const line = lines[i]?.trim();
53
+ if (!line) continue;
54
+ let step: {
55
+ source?: unknown;
56
+ type?: unknown;
57
+ status?: unknown;
58
+ created_at?: unknown;
59
+ content?: unknown;
60
+ tool_calls?: unknown;
61
+ };
62
+ try {
63
+ step = JSON.parse(line) as typeof step;
64
+ } catch {
65
+ continue;
66
+ }
67
+ if (typeof step.created_at !== "string") continue;
68
+ const createdAtMs = Date.parse(step.created_at);
69
+ if (Number.isNaN(createdAtMs)) continue;
70
+ if (createdAtMs < turnStartedMs - CREATED_AT_TOLERANCE_MS) break;
71
+
72
+ // The newest step written during this turn must be a completed MODEL
73
+ // response without tool calls. Intermediate tool steps, errors, or
74
+ // non-terminal responses mean the turn did not finish with a parked answer.
75
+ if (step.source !== "MODEL" || step.status !== "DONE") return undefined;
76
+ if (step.type !== "PLANNER_RESPONSE") return undefined;
77
+ if (Array.isArray(step.tool_calls) && step.tool_calls.length > 0) return undefined;
78
+ if (typeof step.content !== "string" || step.content.length === 0) return undefined;
79
+ return step.content;
80
+ }
81
+ return undefined;
82
+ }
83
+
84
+ /** Read at most `maxBytes` from the end of a file. Empty string on any
85
+ * error (missing file, permission, ...) — the caller degrades to the old
86
+ * stall-fail behavior. */
87
+ export async function readTranscriptTail(filePath: string, maxBytes: number): Promise<string> {
88
+ let handle;
89
+ try {
90
+ handle = await open(filePath, "r");
91
+ } catch {
92
+ return "";
93
+ }
94
+ try {
95
+ const { size } = await handle.stat();
96
+ const start = Math.max(0, size - maxBytes);
97
+ const length = size - start;
98
+ const buffer = Buffer.alloc(length);
99
+ let read = 0;
100
+ while (read < length) {
101
+ const { bytesRead } = await handle.read(buffer, read, length - read, start + read);
102
+ if (bytesRead === 0) break;
103
+ read += bytesRead;
104
+ }
105
+ return buffer.subarray(0, read).toString("utf8");
106
+ } catch {
107
+ return "";
108
+ } finally {
109
+ await handle.close().catch(() => {});
110
+ }
111
+ }
112
+
113
+ /** Probe one stream-json conversation's transcript for a parked answer.
114
+ * Undefined whenever nothing usable is found (missing transcript, no DONE
115
+ * step from this turn) — never throws; the stall guard must not fail. */
116
+ export async function parkedTurnAnswer(
117
+ conversationId: string,
118
+ turnStartedMs: number,
119
+ home: string | undefined = undefined,
120
+ ): Promise<string | undefined> {
121
+ const transcript = `${agyConversationDir("stream-json", conversationId, home)}/.system_generated/logs/transcript_full.jsonl`;
122
+ const tail = await readTranscriptTail(transcript, PARKED_PROBE_MAX_BYTES);
123
+ if (tail === "") return undefined;
124
+ return extractParkedAnswer(tail, turnStartedMs);
125
+ }
package/src/provider.ts CHANGED
@@ -33,6 +33,7 @@ import {
33
33
  import fs from "node:fs";
34
34
  import type { Api } from "@earendil-works/pi-ai";
35
35
  import type { DriverActivity, TurnDriver, TurnHandle } from "./driver-types.js";
36
+ import { SubagentRoster } from "./subagent-roster.js";
36
37
  import { toPiUsage } from "./stream-events.js";
37
38
  import { mapAgyToolToNative } from "./native-tools.js";
38
39
  import { toAgyEffort, type AgyModelEntry } from "./models.js";
@@ -349,6 +350,8 @@ export interface StreamSimpleDeps {
349
350
  /** Output-only ACP tool events; persisted as TUI entries, never sent to
350
351
  * Antigravity or dispatched through Pi's executable tools. */
351
352
  onNativeEvent?: (event: NativeDisplayEvent) => void;
353
+ /** Subagent roster (both engines); folded from every activity. */
354
+ roster?: SubagentRoster;
352
355
  /** Daily file log sink (src/daily-log.ts). Records pre-dispatch turn
353
356
  * errors that never create a driver turn (and so never reach onTurnEnd). */
354
357
  log?: (event: string, data?: unknown, level?: "debug" | "info" | "warn" | "error") => void;
@@ -916,6 +919,8 @@ export interface DriverDeps {
916
919
  replay?: WrapperReplay;
917
920
  nativeActive?: (name: string) => boolean;
918
921
  onNativeEvent?: (event: NativeDisplayEvent) => void;
922
+ /** Subagent roster fold target (both engines; best-effort telemetry). */
923
+ roster?: SubagentRoster;
919
924
  /** Active engine (config), for engine-scoped session keys. */
920
925
  engine: "stream-json" | "acp";
921
926
  /** Daily file log sink for pre-dispatch errors (see StreamSimpleDeps). */
@@ -930,6 +935,7 @@ export interface ActivityFeatures {
930
935
  roundTrips?: ToolRoundTrips;
931
936
  engine?: "stream-json" | "acp";
932
937
  onNativeEvent?: (event: NativeDisplayEvent) => void;
938
+ roster?: SubagentRoster;
933
939
  }
934
940
 
935
941
  /** Process-wide counter: round-trip ids must never repeat across turns in
@@ -976,6 +982,10 @@ export function consumeActivity(
976
982
  feats: ActivityFeatures,
977
983
  ): "parked" | "continue" {
978
984
  const partial = blocks.partial;
985
+ // Subagent roster folds BEFORE any rendering branch: pure in-memory
986
+ // telemetry over the same activities both engines emit, and it must see
987
+ // spawn/message/manage steps regardless of how they render.
988
+ feats.roster?.fold(activity);
979
989
  switch (activity.type) {
980
990
  case "text":
981
991
  appendText(stream, blocks, activity.delta);
@@ -1232,6 +1242,7 @@ async function runTurnDriver(
1232
1242
  effort,
1233
1243
  mode: config.mode,
1234
1244
  skipPermissions: config.skipPermissions,
1245
+ agent: config.agent,
1235
1246
  timeoutMin: config.turnTimeoutMin,
1236
1247
  inactivityMin: config.inactivityTimeoutMin,
1237
1248
  conversationId: existing?.conversationId ?? null,
@@ -1261,6 +1272,7 @@ async function runTurnDriver(
1261
1272
  roundTrips: deps.roundTrips,
1262
1273
  engine: deps.engine,
1263
1274
  onNativeEvent: deps.onNativeEvent,
1275
+ roster: deps.roster,
1264
1276
  };
1265
1277
 
1266
1278
  for (;;) {
@@ -1285,7 +1297,7 @@ async function runTurnDriver(
1285
1297
  finalize(stream, blocks, "error", outcome.error ?? "agy turn failed");
1286
1298
  return;
1287
1299
  }
1288
- if (blocks.textIdx === null && blocks.thinkingIdx === null && outcome.response) {
1300
+ if (blocks.textIdx === null && outcome.response) {
1289
1301
  appendText(stream, blocks, outcome.response);
1290
1302
  }
1291
1303
  if (blocks.textIdx === null && blocks.thinkingIdx === null) {
@@ -1328,6 +1340,7 @@ export function createStreamSimple(
1328
1340
  replay: deps.replay,
1329
1341
  nativeActive: deps.nativeActive,
1330
1342
  onNativeEvent: deps.onNativeEvent,
1343
+ roster: deps.roster,
1331
1344
  // Record the engine of the driver that will ACTUALLY run: if the
1332
1345
  // ACP driver is absent, the config switch falls back to stream,
1333
1346
  // and keying the session as @acp would store a stream
package/src/skills.ts CHANGED
@@ -5,7 +5,6 @@
5
5
  // can tell when a skill applies. Calling it returns the full SKILL.md plus the
6
6
  // bundled resource dir. Nothing is appended to the prompt: agy sees the
7
7
  // catalog in tools/list on every spawn, including after pi compaction.
8
- // Shape borrowed from tianzuo/pi-antigravity lib/skills.ts (MIT).
9
8
 
10
9
  import fs from "node:fs";
11
10
  import os from "node:os";
@@ -3,8 +3,7 @@
3
3
  // `agy --input-format stream-json --output-format stream-json` emits one JSON
4
4
  // object per line on stdout: `init` (conversation binding), `step_update`
5
5
  // (user_input / agent_response / checkpoint / tool steps), `result` (terminal).
6
- // Shapes captured from live output and cross-checked against
7
- // tianzuo/pi-antigravity lib/events.ts (MIT). Unknown event kinds parse as
6
+ // Shapes captured from live output. Unknown event kinds parse as
8
7
  // {kind:"unknown"} so a future agy release degrades instead of crashing the
9
8
  // reader loop.
10
9