Zurück zu Skills
amelnagdy/delegate-skillsVor der Ausführung prüfen

SKILL DETAIL

agy-delegate

amelnagdy/delegate-skills/agy-delegate

>-

Installationen · 92Quelle ansehen

Installation

npx skills add https://github.com/amelnagdy/delegate-skills --skill agy-delegate

Skill-Dateien

SKILL.md

Zuletzt synchronisiert · 30.08.2026

references/dispatch-and-poll.md
# Dispatch and poll

`scripts/relay.mjs` is the dispatch layer. It wraps `agy --print`, runs the brief in Antigravity,
captures the final response, and writes a structured `result.json`. Your job collapses to: run one
command, then read one file.

## Before the first run: check the binary

```bash
command -v agy
agy help
agy models
```

`agy models` proves the CLI can authenticate and list available model labels. The relay records the
version it can infer from `agy changelog` into `result.json`. Neither command proves that a headless
write will be approved.

## Dispatching

```bash
node "<skill-dir>/scripts/relay.mjs" --brief brief.txt --cd /path/to/repo
```

(`<skill-dir>` is wherever this skill is installed - the folder containing its `SKILL.md`.)

Options:

| Flag | Effect |
| --- | --- |
| `--brief <file>` | The brief. Omit it to read the brief from stdin before passing it to `agy --print`. |
| `--cd <dir>` | Working root for Antigravity (default: current directory). |
| `--lane <name>` | Fleet lane from `delegate-setup` config. Applies that lane's dials; fails if the lane's `implementer` is not this relay. Explicit dial flags win. |
| `--model <name>` | Antigravity model label. Optional; a fresh run can use Antigravity's configured default. |
| `--effort <level>` | Reasoning effort: `low`, `medium`, or `high` (passed as agy's own `--effort`). |
| `--project <id>` | Use an existing Antigravity project. |
| `--new-project` | Force a fresh Antigravity project. This is the default for fresh dispatches. |
| `--resume-last` | Continue the most recent Antigravity conversation; send only the delta brief. |
| `--conversation <id>` | Continue a specific Antigravity conversation; send only the delta brief. |
| `--sandbox` | Enable Antigravity's terminal sandbox for the run. |
| `--read-only` | Run in plan mode (`--mode plan`), removing write and edit paths; mutually exclusive with `--dangerously-skip-permissions`. |
| `--dangerously-skip-permissions` | Pass Antigravity's permission-bypass flag; mutually exclusive with `--read-only`. Never use this unless the human explicitly accepts it. |
| `--print-timeout <duration>` | Timeout agy itself applies to print mode (default: `30m`). |
| `--timeout <dur>` | Relay-side watchdog (e.g. `30m`); overrides the default of `--print-timeout` plus a 60s grace. On expiry the agy process tree is killed and `result.json` gets `status: "timeout"`. Set it explicitly when agy may hang past its own print timeout. Malformed, zero, and out-of-range durations are rejected; the maximum is `596h31m23s`. |
| `--add-dir <dir>` | Add an extra workspace directory. Repeatable; relative paths resolve against `--cd`. Fresh runs always add the `--cd` repo (absolute path) as a workspace dir. Edits inside extra workspaces are not reported in `touchedFiles`. |
| `--out-dir <dir>` | Where artifacts go (default: a fresh dir under the system temp dir). |

Artifacts default to the system temp dir on purpose: the repo under review stays clean, so the
touched-files report shows only Antigravity's edits and nothing of the helper's own.

## The result

`<out-dir>/result.json` is the contract. Fields:

- `schema` - the result-format version (currently `delegate-relay.result.v1`)
- `tool` - `agy`
- `status` - `completed` | `failed` | `timeout` | `aborted` | `agy_unavailable`
- `exitCode` - mirrors Antigravity's exit code; `128` plus the signal number if the child was killed; `127` if `agy` is not on PATH; on a `timeout` the relay forces a non-zero code even when the child exited `0` after the watchdog's SIGTERM
- `signal` - the signal that killed the child, otherwise `null`
- `agyVersion` - inferred from `agy changelog` when available
- `projectId` / `conversationId` - parsed from the Antigravity log when present
- `finalMessage` - Antigravity's stdout response
- `touchedFiles` - `git status --porcelain` lines in the working root: your review starting point.
  `null` (not `[]`) when git cannot report; `[]` means git ran and the tree is clean
- `readOnlyViolation` - `true` when fingerprints prove a working-tree change, `false` when coverage is complete and proves none, and `null` when fingerprinting was incomplete or the run was not `--read-only`
- `briefPath` / `finalPath` / `logPath` / `stderrPath` - the exact brief, final message, Antigravity
  log, and stderr capture
- `workdir`, `model`, `effort`, `project` (the `--project` you passed, vs `projectId` parsed from the log),
  `sandbox`, `readOnly`, `dangerouslySkipPermissions`, `resumed` (true for a `--resume-last` or `--conversation`
  run), `startedAt`, `finishedAt`
- `stderrTail` - last ~20 stderr lines; present on every run that did not complete (`failed`, `timeout`, `aborted`), except a launch failure, which reports `failed` with no `stderrTail`; also present when `finalMessage` is empty so diagnostics are not discarded
- `error` - present on a launch failure, `timeout`, `aborted`, headless permission denial, or silent no-op

The helper also prints a summary to stdout and normally exits with Antigravity's exit code. It forces
exit 1 when Antigravity exits 0 after a detected headless permission denial or with neither a final
message nor observable working-tree changes, so a wrapping script can branch on success/failure directly.

## Waiting for completion

The helper blocks until Antigravity finishes. Back it with whatever your orchestrator offers:

- **Claude Code:** run the Bash call with `run_in_background: true`; you're notified on completion,
  then read `result.json`.
- **Plain shell / other agents:** foreground for short tasks, or background and poll. A run is done
  when `result.json` exists with a `status`. A pre-run usage error exits with code 2 before writing any
  file, so check the exit code too. A missing `agy` binary exits 127 and writes `result.json` with
  `status: agy_unavailable`.

Trust the working tree and the process state over any progress display. A run is finished when the
process has exited and `result.json` is written.

## When a run misbehaves

- **`status: agy_unavailable` (exit 127):** `agy` is not on PATH. Install the Antigravity CLI and run
  its first-launch setup, then re-dispatch.
- **`status: timeout`:** the relay watchdog killed the run. Inspect `error` to see whether the selected
  limit was explicit `--timeout` or the derived `--print-timeout` plus 60s grace. The working tree may
  hold a half-applied change — inspect it before changing that limit, reducing the brief, or resuming.
- **`status: aborted`:** the relay itself was killed (its parent's timeout, a stopped task, a
  closed terminal) and forwarded the kill to agy. The result is written before the relay exits;
  inspect the working tree before re-dispatching. On native Windows a hard kill of the relay is
  uncatchable (Node supports no `SIGTERM` handler there), so this status may never get written -
  a relay process that is gone without a `result.json` is an aborted run; inspect the working
  tree and `events.jsonl` directly.
- **`status: failed` with `signal: "SIGKILL"`:** the host ended the child - commonly the OOM killer
  or a supervisor timeout, not an implementer error. Free up host memory or split the task into
  smaller briefs, then re-dispatch.
- **`status: failed`:** read `result.json`'s `stderrTail`, `stderrPath`, and `logPath` for the cause.
  Common causes: auth lapse, an unknown model label, timeout, or a permission the run needed.
- **Headless write permission denied:** the relay detects Antigravity's `no output produced ...
  auto-denied` stderr sentinel, reports `status: failed`, preserves `stderrTail`, and exits 1. Settings
  allow-rules are not recommended here because they have not been demonstrated to apply in
  `--print` mode. Ask the human before re-dispatching with `--dangerously-skip-permissions`; that flag
  auto-approves every tool permission request and the run must be treated as full access.
- **Empty `finalMessage`:** a run with edits may still be correct - check `touchedFiles`, the diff, and
  the preserved `stderrTail`. With no observable edits, the relay reports `status: failed` rather than
  claiming completion. To get a report next time, add a `<structured_output_contract>` block (see
  [writing-the-brief.md](writing-the-brief.md)).

## What the helper is doing

Under the hood the helper runs roughly:

```bash
agy --new-project --add-dir <repo> --print-timeout 30m --print=<brief>
agy --continue --print-timeout 30m --print=<delta brief>
agy --conversation <id> --print-timeout 30m --print=<delta brief>
```

`agy --print` requires the prompt as a flag argument, so keep briefs focused. The relay still accepts
stdin or `--brief <file>` for your convenience; it reads the text first, then passes it to `agy` as
`--print=<brief>` (the `=` form so a brief that begins with a bare flag like `--help` still runs).
Two consequences of the brief riding the command line: it is visible in the host process list (`ps`),
so on a shared machine keep secrets out of it; and a brief over ~120 KB is rejected up front (the OS
caps a single argument), so have `agy` read large context from the workspace instead of inlining it.

## The commit boundary

The helper never commits - by design, not omission. The robust contract is: Antigravity edits the
working tree, the orchestrator reviews and commits. See [review-and-land.md](review-and-land.md).
references/multi-task-queues.md
# Multi-task queues

The single-task loop scales to a queue, and that is where delegation pays off most - a removal split
across layers, a migration touching many files, a refactor sweep. The discipline that makes a queue
trustworthy is sequencing and bookkeeping, not parallelism.

## Run sequentially, one commit per task

Run tasks **one at a time, in dependency order**, landing each (review + gates + commit) before
dispatching the next. Three reasons:

- **Later tasks assume earlier ones landed.** Task 3's brief can say "the X added in the previous step
  exists" only if the previous step actually committed.
- **One commit per task** keeps the history reviewable and any single step revertible.
- **Each review is honest.** A clean working tree before each dispatch means the next task's
  `touchedFiles` shows only *its* changes.

Parallelism is occasionally worth it for genuinely independent tasks on separate files, but it
sacrifices the clean-tree-per-task property and makes review harder. Default to sequential.

## Carry decided constraints forward

Implementation surfaces facts the original plan did not have: a helper got named, a fixture lives in a
specific place, an interface was chosen. When a later task depends on one of those, fold it into that
task's brief as an explicit line. A fresh Antigravity conversation has no memory of the earlier run, so
a constraint that emerged in task 2 must be restated in task 5's brief or it will not hold.

## Keep a progress file

For anything longer than two or three tasks - especially a run the human steps away from - maintain a
single progress file alongside the work:

- **Status table** - each task: queued / at-implementer / reviewed+committed (with the commit hash).
- **Per-task review notes** - what landed, what you verified, the gate outcome.
- **Needs your eyes** - design decisions Antigravity made, non-blocking nitpicks, anything you want the
  human to overrule or confirm.
- **End-of-run checklist** - what happens after the last task.

Update it as each task lands, not in a batch at the end.

## Close with a coherence check

Per-task review proves each step in isolation; it does not prove the steps cohere. After the last task,
verify the whole:

- Run the full test/build once more on the final tree.
- Do a repo-wide check for the thing the queue was about.
- For schema work, replay all the new migrations from a clean state and check for drift.
- Then push and open or update the PR, with a description that reflects what actually shipped.

## When to stop and ask

Proceed without asking on anything that follows from the agreed plan. Stop and surface when:

- A task cannot be completed correctly within its brief's scope.
- A review finds something that calls the *plan* into question, not just the implementation.
- The gates reveal a problem that affects tasks already done.

Then report where you are, what is committed, and what the open question is - and wait.
references/review-and-land.md
# Review and land

Antigravity did the typing; you own the judgment. Verify against reality, never against the self-report
- and read the diff as generated code, which fails in ways a green gate cannot see.

## Check the tests before trusting the gates

If the diff touches existing tests, review those edits *first* - before the gate re-run means anything.
A weakened assertion, an added skip, or a deleted test makes the gate measure less than it did before
the run.

- **Unbriefed edits to existing tests are a contract change, not part of the fix.** Flag them, don't
  absorb them.
- **Skipped, disabled, or commented-out tests added in this diff:** treat the underlying test as
  failing until proven otherwise.
- **Loosened assertions** (exact match relaxed to contains/truthy, error-type checks broadened,
  tolerance widened): same treatment.

## Re-run the gates yourself

`result.json` carries Antigravity's own claims. Treat them as claims, not evidence - re-run the
project's actual test/lint/build commands in the working tree and read the output. Passing is necessary,
not sufficient.

For changes with their own verification shape, go further:

- **Migrations / schema:** round-trip them and check for drift.
- **Removals / renames:** grep the codebase for dangling references.
- **Anything stateful:** exercise the actual behavior, don't just confirm it compiles.

## Read the diff against the brief

Open the diff (`touchedFiles` in the result is your starting list) and hold it against what you asked
for:

- **Scope creep** - did Antigravity change things the brief said to leave untouched?
- **Scope shortfall** - did it do the whole task, including edge cases and cleanup?
- **Quiet judgment calls** - did it make a defensible but unasked decision you need to understand?

## The implementer sweep

Generated code fails in systematic ways that gates are structurally blind to. Walk these against every
diff before you commit:

- **Hardcoded success or fixture data** on a path the brief says does real work.
- **Catch-all error handling that returns a default** instead of propagating or recovering explicitly.
- **Unverified imports and API calls** - confirm new dependencies, methods, and signatures exist in the
  installed version.
- **Dead weight** - unused imports, helpers nothing calls, unreachable branches, scaffolding comments.
- **A second way to do what the file already does** - new client, error idiom, or logging style beside
  an existing one.
- **New tests that assert internals** instead of behavior.
- **Near-duplicate test bodies** differing by one value.
- **Speculative surface** - optional parameters, config flags, or abstractions with no caller.
- **Guards for impossible cases** that bury validation that matters at real trust boundaries.

Anything the sweep catches goes back to Antigravity as a delta brief or gets fixed in the tree before
commit - and either way is reported to the user.

If the `guard-skills` package is installed, run the relevant guard on the diff for the full treatment.

## The commit boundary

When the gates pass and the diff holds, **you commit** - the orchestrator, never the implementer. Write
a clear message describing what landed. If your project attributes co-authorship, that is the place for
it.

From dispatch until that commit, the uncommitted working tree is the authoritative copy of the
implementer's work — the only one you can commit from, and often the only copy at all. Never run `git checkout`, `reset`, `clean`, or a branch switch in the
workspace between those two points — however messy an interrupted run looks, inspect it first:
`git status`, `git diff`, `git diff --cached` for anything the implementer staged (plain
`git diff` is blind to the index), and open any untracked files (`??` in `git status`) directly —
they are the implementer's new files, and no diff shows their contents. The tree is evidence,
not clutter. After that inspection the
verdict can legitimately be to discard — work built on a premise you have since corrected, for
example — and then `git checkout`/`clean` is the right tool. The ban is on reflexive cleanup
before anyone has looked.

## Reworking: send the delta, not the whole task

If the review turns up problems, don't restate the entire brief. Continue the same Antigravity
conversation with just the correction:

```bash
echo "The fix is right, but the test mocks the DB session - use the real migrated fixture instead, and
drop the now-unused import." | node "<skill-dir>/scripts/relay.mjs" --resume-last --cd /path/to/repo
```

`--resume-last` keeps Antigravity's conversation context from the first run, so a short delta is enough.
Then review again - rework gets the same gate-rerun, test check, diff-read, and sweep as the original,
no shortcuts.

## Surface, don't absorb

The human opted into delegation, so committing verified, gate-passing work is the agreed contract. But
keep them in the loop on anything that changes the shape of the work:

- **Report design decisions** Antigravity made, and any defensible-but-unrequested turns it took.
- **Note non-blocking nitpicks** you chose not to block on, so the human can overrule you.
- **Stop and ask** if correct completion requires going beyond the brief.

For a multi-task run, capture these in the progress file rather than letting them scroll past - see
[multi-task-queues.md](multi-task-queues.md).
references/writing-the-brief.md
# Writing the brief

A brief is the entire task as Antigravity will see it. It runs in a separate conversation with **no
memory of your conversation, no access to your prior notes, and no shared context** - only the text you
send and whatever it can inspect in the workspace. If a constraint is not in the brief or discoverable
in the repo, it does not exist for Antigravity.

## Model choice

`agy` has a configured default model, so a fresh dispatch does not require `--model`. Pass `--model`
only when the human has named a preferred Antigravity model label for this task. `agy models` shows the
available labels.

A resumed run keeps the conversation context. Send only the delta brief.

## The shape that works

Antigravity responds well to compact, block-structured prompts with XML tags rather than long prose.
State the task, what "done" looks like, how to behave by default, and the few constraints that actually
matter. Add a block only when the task needs it.

```xml
<task>
One or two sentences: the concrete job and where it lives. Then the specifics - current state, what to
change, and explicitly what to leave untouched. The "leave untouched" list is what keeps Antigravity
from wandering into unrelated refactors.
</task>

<verification_loop>
Run these before finishing and fix anything they surface, don't just report it:
  <the project's real test command>
  <the project's real lint/format command>
  <the project's real build/typecheck command>
Confirm the working tree shows only the intended changes afterward.
</verification_loop>

<action_safety>
Keep changes scoped to the task. No unrelated refactors, renames, or cleanup unless required for
correctness. Do NOT run git add or git commit - the orchestrator commits after reviewing. Leave the
work uncommitted in the working tree.
</action_safety>

<structured_output_contract>
End with a report in this exact shape:
  1. What changed and why
  2. Files touched
  3. Gate outcomes (paste the test/lint counts)
  4. Anything you deviated on, left open, or want a decision on
</structured_output_contract>
```

That four-block skeleton covers most implementation tasks. Reach for extra blocks when the task profile
calls for them:

- **Debugging / open-ended fixes** - add `<completeness_contract>` (resolve fully, don't stop at the
  first plausible fix) and `<missing_context_gating>` (don't guess missing repo facts; find them or
  state what's unknown).
- **Research / recommendations** - add `<research_mode>` (separate observed facts, inferences, open
  questions).

## Always ask for the report explicitly

The relay captures `agy --print` stdout as `finalMessage`. If Antigravity finishes without a closing
summary, the result is not useful to review. The `<structured_output_contract>` block is what guarantees
a report you can read.

## Discover the real gates

`<verification_loop>` is only useful if it names the project's *actual* commands. Read the repo's
`AGENTS.md` / `CLAUDE.md` / `Makefile` / `package.json` first and copy the real ones in (`make test`,
`npm run lint`, `cargo test`, `pytest -q`, whatever it is). A brief that says "run the tests" without
naming them gets you an implementer that guesses - or skips.

## Honor the repo's conventions

If the project has house rules in `AGENTS.md`, `CLAUDE.md`, or a similar file, restate the load-bearing
ones in the brief. Antigravity can inspect the workspace, but compliance is more reliable when the
important rules are directly in front of it.

## One task per brief

Keep each brief to a single, bounded job. "Review this, fix what you find, update the docs, and suggest
a roadmap" produces a muddled run; split it into separate dispatches. One brief -> one Antigravity run
-> one commit keeps review and rollback clean.

## Premises freeze at dispatch

The implementer starts from the brief's facts and there is no steering channel mid-run. Audit the
fact block before sending — ownership, target branch, constraints, anything a judgment call rests
on. If a premise turns out wrong while the run is live, stop the run and re-dispatch a corrected
brief rather than discounting the output afterward; for a write-capable run, inspect the working
tree and reconcile any partial or premise-contaminated edits — keep or revert them — before the
re-dispatch.

## A worked example

```xml
<task>
In the payments service at services/billing/, the refund path double-charges when a refund is retried
after a network timeout. Make refund submission idempotent: check for an existing refund by idempotency
key before creating a new one. Touch only services/billing/refund.py and its tests. Leave the charge
path, API routes, and data models untouched.
</task>

<verification_loop>
Run and make green before finishing:
  pytest tests/billing/ -q
  ruff check services/billing/
Confirm git status shows only refund.py and its test file changed.
</verification_loop>

<action_safety>
Scope strictly to the refund idempotency fix. No unrelated refactors. Do NOT git add or commit; leave
changes in the working tree for review.
</action_safety>

<structured_output_contract>
Report: (1) the root cause and your fix, (2) files touched, (3) pytest + ruff outcomes with counts,
(4) anything you left open or want decided.
</structured_output_contract>
```

Send this with `relay.mjs` (see [dispatch-and-poll.md](dispatch-and-poll.md)); review the result and
commit it yourself (see [review-and-land.md](review-and-land.md)).
scripts/relay.mjs
#!/usr/bin/env node
/**
 * delegate-skills · agy-delegate · relay.mjs
 *
 * Dispatch a self-contained brief to the Google Antigravity CLI (`agy --print`),
 * capture the run, and write a structured result the orchestrating agent can
 * review. The orchestrator runs this one command and reads the result JSON -
 * every Antigravity-specific mechanic lives in here, which keeps the skill
 * orchestrator-agnostic.
 *
 * Trust posture: relay.mjs itself makes no network calls, reads or writes no
 * credentials, and sends no telemetry; it has no dependencies (Node built-ins
 * only). It shells out only to `agy` and `git`. The `agy` process it launches
 * does authenticate - exactly as you do at the terminal. Read this file before
 * you run it.
 *
 * Note: `agy --print` takes the prompt as a command-line argument, so the brief is
 * visible in the host process list (`ps`, /proc). On a shared machine keep secrets
 * out of the brief - reference them by a path or env var the workspace can read.
 *
 * It deliberately does NOT commit. Committing is always the orchestrator's job -
 * after it reviews the diff and re-runs the project gates.
 *
 * Antigravity owns its own permission policy. This helper does not pass
 * --dangerously-skip-permissions by default; opt into that flag only when the
 * human explicitly accepts it. Pass --sandbox to enable Antigravity's terminal
 * sandbox for the run. Combining both flags must be treated as full access because
 * permission requests to act outside the sandbox may be auto-approved.
 *
 * Usage:
 *   node relay.mjs --brief <file> [options]
 *   cat brief.txt | node relay.mjs [options]
 *
 * Options:
 *   --brief <file>          Path to the brief. If omitted, the brief is read from stdin.
 *   --cd <dir>              Working root for Antigravity (default: current directory).
 *   --lane <name>           Fleet lane from delegate-setup config (dials apply; explicit flags win).
 *   --model <name>          Antigravity model label (default: agy's configured default).
 *   --effort <level>        Reasoning effort: low, medium, or high (passed as agy's own --effort).
 *   --project <id>          Use an existing Antigravity project.
 *   --new-project           Force a fresh Antigravity project (default for fresh runs).
 *   --resume-last           Continue the most recent Antigravity conversation; send only the delta brief.
 *   --conversation <id>     Continue a specific Antigravity conversation; send only the delta brief.
 *   --sandbox               Enable Antigravity's terminal sandbox for this run.
 *   --read-only             Run in plan mode (`--mode plan`), removing write and edit paths.
 *                           Mutually exclusive with --dangerously-skip-permissions.
 *   --dangerously-skip-permissions
 *                           Auto-approve Antigravity tool permission requests. Use only with human approval.
 *                           Mutually exclusive with --read-only.
 *   --print-timeout <dur>   Timeout agy itself applies to print mode (default: 30m).
 *   --timeout <dur>         Relay-side watchdog, h/m/s like 30m (default: --print-timeout
 *                           plus a 60s grace). On expiry the agy process tree is killed and
 *                           result.json gets status "timeout". Set it explicitly when agy
 *                           may hang past its own print timeout.
 *   --add-dir <dir>         Add an extra workspace directory. Repeatable.
 *   --out-dir <dir>         Where to write run artifacts (default: a fresh dir under
 *                           the system temp dir, so the repo under review stays clean).
 *   -h, --help              Show this help.
 *
 * Result: written to <out-dir>/result.json and summarized on stdout -
 *   status, exitCode, agyVersion, projectId, conversationId, finalMessage
 *   (Antigravity's own report), touchedFiles (git porcelain, null if git can't report),
 *   readOnlyViolation (on --read-only), and the paths to brief.txt, final.txt, agy.log, and stderr.txt.
 *
 * Exit codes: a pre-run usage error (bad/missing args, empty brief) exits 2
 * before any run and writes no result file; a missing `agy` binary exits 127;
 * otherwise the exit code mirrors Antigravity's own, except that an exit-zero
 * permission denial or silent write-dispatch no-op is forced to exit 1.
 * If the child dies on a signal, the exit code is 128 plus the signal number and
 * `result.json` records the signal.
 * Once the brief validates, `result.json` is written on every outcome -
 * completed, failed, timeout (the relay watchdog fired after explicit --timeout,
 * or after --print-timeout plus 60s grace), aborted (the relay itself was killed
 * and forwarded the kill to agy), or agy_unavailable. An orchestrator that polls for the
 * file must therefore also treat a non-zero exit with no file as a usage error.
 */

import {spawn, execFileSync, spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdirSync, writeFileSync, renameSync, readFileSync, readlinkSync, lstatSync, existsSync, appendFileSync, realpathSync } from "node:fs";
import {join, resolve, basename, dirname, relative, isAbsolute } from "node:path";
import { fileURLToPath } from "node:url";
import { constants, tmpdir } from "node:os";
import { StringDecoder } from "node:string_decoder";

const DEFAULT_PRINT_TIMEOUT = "30m";
const MAX_TIMER_MS = 2_147_483_647;
const MAX_TIMER_DURATION = "596h31m23s";
const VERSION_PROBE_TIMEOUT_MS = 10_000;

const IMPLEMENTER_KEY = "agy";

function applyFleetLane(opts, flagged) {
  if (!opts.lane) return;
  const script = join(dirname(fileURLToPath(import.meta.url)), "../../delegate-setup/scripts/lane.mjs");
  if (!existsSync(script)) {
    fail("--lane requires the delegate-setup skill installed beside this relay");
  }
  const r = spawnSync(
    process.execPath,
    [script, "resolve", "--cwd", opts.cd, "--lane", opts.lane, "--implementer", IMPLEMENTER_KEY],
    { encoding: "utf8", env: process.env },
  );
  if (r.error) fail(`lane resolve failed: ${r.error.message}`);
  if (r.status !== 0) {
    fail((r.stderr || "lane resolve failed").trim().replace(/^lane\.mjs:\s*/, ""));
  }
  let resolved;
  try {
    const lines = (r.stdout || "").trim().split("\n").filter(Boolean);
    resolved = JSON.parse(lines[lines.length - 1]);
  } catch {
    fail("lane resolve returned invalid JSON");
  }
  opts.laneSource = resolved.source;
  for (const [field, value] of Object.entries(resolved.dials || {})) {
    if (flagged.has(field)) continue;
    if (field === "autonomy" && (flagged.has("autonomy") || flagged.has("sandbox") || flagged.has("readOnly"))) continue;
    if (field === "agent" && (flagged.has("agent") || flagged.has("readOnly"))) continue;
    if (field === "sandbox" && (flagged.has("sandbox") || flagged.has("readOnly"))) continue;
    if (field === "permissionMode" && (flagged.has("permissionMode") || flagged.has("readOnly"))) continue;
    if (field === "planOnly" && (flagged.has("planOnly") || flagged.has("readOnly"))) continue;
    if (field === "readOnly" && (flagged.has("readOnly") || flagged.has("dangerouslySkipPermissions"))) continue;
    if (field === "force" && flagged.has("force")) continue;
    opts[field] = value;
  }
}

function fail(message, code = 2) {
  process.stderr.write(`relay: ${message}\n`);
  process.exit(code);
}

function parseArgs(argv) {
  const flagged = new Set();
  const opts = {
    lane: null,
    laneSource: null,
    brief: null,
    cd: process.cwd(),
    model: null,
    effort: null,
    project: null,
    newProject: false,
    resumeLast: false,
    conversation: null,
    sandbox: false,
    readOnly: false,
    dangerouslySkipPermissions: false,
    printTimeout: DEFAULT_PRINT_TIMEOUT,
    timeout: null,
    addDirs: [],
    outDir: null,
  };
  for (let i = 0; i < argv.length; i += 1) {
    const arg = argv[i];
    const next = () => {
      const value = argv[i + 1];
      if (value === undefined) fail(`${arg} requires a value`);
      i += 1;
      return value;
    };
    switch (arg) {
      case "-h":
      case "--help":
        process.stdout.write(headerComment());
        process.exit(0);
        break;
      case "--brief": opts.brief = next(); break;
      case "--cd": opts.cd = resolve(next()); break;
      case "--lane": opts.lane = next(); break;
      case "--model": opts.model = next(); flagged.add("model"); break;
      case "--effort": opts.effort = next(); flagged.add("effort"); break;
      case "--project": opts.project = next(); break;
      case "--new-project": opts.newProject = true; break;
      case "--resume-last": opts.resumeLast = true; break;
      case "--conversation": opts.conversation = next(); break;
      case "--sandbox": opts.sandbox = true; flagged.add("sandbox"); break;
      case "--read-only": opts.readOnly = true; flagged.add("readOnly"); break;
      case "--dangerously-skip-permissions":
        opts.dangerouslySkipPermissions = true;
        flagged.add("dangerouslySkipPermissions");
        break;
      case "--print-timeout": opts.printTimeout = next(); break;
      case "--timeout": opts.timeout = next(); flagged.add("timeout"); break;
      case "--add-dir": opts.addDirs.push(next()); break;
      case "--out-dir": opts.outDir = resolve(next()); break;
      default:
        fail(`unknown option: ${arg}`);
    }
  }
  applyFleetLane(opts, flagged);
  if (opts.effort !== null && !["low", "medium", "high"].includes(opts.effort)) {
    fail(`invalid --effort "${opts.effort}" (expected: low, medium, high)`);
  }
  if (opts.readOnly && opts.dangerouslySkipPermissions) {
    fail("--read-only and --dangerously-skip-permissions are mutually exclusive; pass only one");
  }
  if (opts.resumeLast && opts.conversation) {
    fail("--resume-last and --conversation are mutually exclusive; pass only one");
  }
  // A malformed --timeout must fail loudly: parseDuration returns null for it, and a null
  // delay makes setTimeout fire on the next tick - a silent instant "timeout", the worst
  // failure mode a watchdog has. Zero is rejected for the same reason.
  if (opts.timeout !== null) {
    const milliseconds = parseDuration(opts.timeout);
    if (milliseconds === null || milliseconds <= 0 || milliseconds > MAX_TIMER_MS) {
      fail(`--timeout "${opts.timeout}" must be an h/m/s duration from 1s through ${MAX_TIMER_DURATION}`);
    }
  }
  const printTimeoutMs = parseDuration(opts.printTimeout);
  if (printTimeoutMs === null || printTimeoutMs <= 0 || printTimeoutMs + 60_000 > MAX_TIMER_MS) {
    fail(`--print-timeout "${opts.printTimeout}" must be an h/m/s duration from 1s through 596h30m23s so its 60s grace fits the relay watchdog limit`);
  }
  if (opts.project && (opts.resumeLast || opts.conversation)) {
    fail("--project cannot be combined with --resume-last or --conversation");
  }
  if (opts.project && opts.newProject) {
    fail("--project and --new-project are mutually exclusive");
  }
  if (opts.newProject && (opts.resumeLast || opts.conversation)) {
    fail("--new-project cannot be combined with --resume-last or --conversation");
  }
  // agy requires absolute --add-dir paths; resolve a relative one against --cd
  // (not the relay's own cwd) - and only after the loop, since --add-dir may
  // appear before --cd on the command line. resolve() passes absolutes through.
  opts.addDirs = opts.addDirs.map((dir) => resolve(opts.cd, dir));
  return opts;
}

function headerComment() {
  // The leading block comment doubles as --help text.
  const src = readFileSync(new URL(import.meta.url), "utf8");
  const match = src.match(/\/\*\*([\s\S]*?)\*\//);
  if (!match) return "relay.mjs - dispatch a brief to agy --print\n";
  return `${match[1].replace(/^\s*\* ?/gm, "").trim()}\n`;
}

function readBrief(opts) {
  if (opts.brief) {
    if (!existsSync(opts.brief)) fail(`brief file not found: ${opts.brief}`);
    return readFileSync(opts.brief, "utf8");
  }
  if (process.stdin.isTTY) {
    fail("no --brief given and stdin is a TTY; pass --brief <file> or pipe the brief on stdin");
  }
  let stdin = "";
  try {
    stdin = readFileSync(0, "utf8");
  } catch {
    stdin = "";
  }
  return stdin;
}

function killChild(child, signal = "SIGTERM") {
  if (!child || !child.pid) return;
  if (process.platform === "win32") {
    if (signal !== "SIGTERM") return;
    try {
      execFileSync("taskkill", ["/pid", String(child.pid), "/t", "/f"], {
        stdio: ["ignore", "ignore", "inherit"],
      });
    } catch {
      // The process tree already exited.
    }
    return;
  }
  try {
    process.kill(-child.pid, signal);
  } catch {
    try {
      child.kill(signal);
    } catch {
      // The process group already exited.
    }
  }
}

function agyVersion(timeoutMs) {
  try {
    const out = execFileSync("agy", ["changelog"], {
      encoding: "utf8",
      timeout: Math.min(timeoutMs, VERSION_PROBE_TIMEOUT_MS),
      killSignal: "SIGKILL",
    }).trim();
    const firstLine = out.split("\n").find(Boolean) || "";
    const match = firstLine.match(/^([^:\s]+):/);
    return match ? match[1] : firstLine || null;
  } catch (err) {
    // Only a missing binary means "unavailable"; any other changelog failure
    // (permissions, a broken subcommand) must not masquerade as exit 127.
    if (err && err.code === "ENOENT") return null;
    if (err && err.code === "ETIMEDOUT") throw err;
    return "unknown";
  }
}

function parseDuration(duration) {
  const match = /^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/.exec(duration);
  if (!match || (!match[1] && !match[2] && !match[3])) return null;
  try {
    const seconds =
      BigInt(match[1] || 0) * 3600n +
      BigInt(match[2] || 0) * 60n +
      BigInt(match[3] || 0);
    const milliseconds = seconds * 1000n;
    if (milliseconds <= 0n || milliseconds > BigInt(MAX_TIMER_MS)) return null;
    return Number(milliseconds);
  } catch {
    return null;
  }
}

function gitTouchedFiles(cwd) {
  try {
    const output = execFileSync("git", ["status", "--porcelain"], {
      cwd,
      encoding: "utf8",
      timeout: 10_000,
      killSignal: "SIGKILL",
      stdio: ["ignore", "pipe", "ignore"],
      maxBuffer: 64 * 1024 * 1024,
    });
    return output.split("\n").map((line) => line.trimEnd()).filter(Boolean);
  } catch {
    return null;
  }
}

function gitWorktreeFingerprint(cwd, excludedPaths = []) {
  try {
    const git = (args) => execFileSync("git", args, {
      cwd,
      timeout: 10_000,
      killSignal: "SIGKILL",
      stdio: ["ignore", "pipe", "ignore"],
      maxBuffer: 64 * 1024 * 1024,
    });
    const root = realpathSync.native(git(["rev-parse", "--show-toplevel"]).toString("utf8").replace(/\r?\n$/, ""));
    const exclusions = excludedPaths
      .map((path) => {
        const absolute = resolve(path);
        try { return relative(root, realpathSync.native(absolute)); }
        catch { return relative(root, join(realpathSync.native(dirname(absolute)), basename(absolute))); }
      })
      .filter((path) => path && path !== ".." && !path.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) && !isAbsolute(path))
      .map((path) => `:(exclude,top,literal)${path.replaceAll("\\", "/")}`);
    const pathspec = [":(top)", ...exclusions];
    const status = git(["status", "--porcelain=v1", "-z", "--untracked-files=all", "--no-renames", "--", ...pathspec]);
    const fingerprint = createHash("sha256").update("status\0").update(status);
    fingerprint.update("\0index\0").update(git(["diff", "--cached", "--raw", "--full-index", "--no-renames", "-z", "--", ...pathspec]));
    fingerprint.update("\0worktree\0").update(git(["diff", "--raw", "--full-index", "--no-renames", "-z", "--", ...pathspec]));

    const paths = [...new Set(status.toString("utf8").split("\0").filter(Boolean).map((entry) => entry.slice(3)))].sort();
    for (const path of paths) {
      const fullPath = join(cwd, path);
      fingerprint.update("\0path\0").update(path).update("\0");
      let stat;
      try {
        stat = lstatSync(fullPath);
      } catch (error) {
        if (error?.code !== "ENOENT") throw error;
        fingerprint.update("missing");
        continue;
      }
      fingerprint.update(String(stat.mode)).update("\0");
      if (stat.isSymbolicLink()) fingerprint.update(readlinkSync(fullPath));
      else if (stat.isFile()) fingerprint.update(git(["hash-object", "--no-filters", "--", path]));
      else if (stat.isDirectory()) {
        const nestedState = gitWorktreeFingerprint(fullPath, excludedPaths);
        if (nestedState === null) return null;
        let headState;
        try {
          headState = git(["-C", fullPath, "rev-parse", "--verify", "HEAD"]);
        } catch {
          const symbolicHead = git(["-C", fullPath, "symbolic-ref", "--quiet", "HEAD"]).toString("utf8").trim();
          const target = spawnSync("git", ["-C", fullPath, "show-ref", "--verify", "--quiet", symbolicHead], { cwd, timeout: 10_000, killSignal: "SIGKILL", stdio: "ignore" });
          if (target.status !== 1) return null;
          headState = Buffer.from(`unborn\0${symbolicHead}`);
        }
        fingerprint.update("submodule\0").update(headState).update(nestedState);
      } else return null;
    }
    return fingerprint.digest("hex");
  } catch {
    return null;
  }
}

function readOnlyVerdict(opts, beforeState, afterState) {
  // Three-valued on purpose: true when fingerprints prove a change, false when coverage
  // is complete and proves none, null when the fingerprint could not be taken or the run
  // was not read-only.
  if (!opts.readOnly) return null;
  if (beforeState === null || afterState === null) return null;
  return beforeState !== afterState;
}

function timestamp() {
  return new Date().toISOString().replace(/[:.]/g, "-");
}

function prepareRunDir(opts, brief) {
  const startedAt = new Date().toISOString();
  const outDir = opts.outDir || join(tmpdir(), "delegate-relay", `${basename(opts.cd) || "repo"}-${timestamp()}`);
  mkdirSync(outDir, { recursive: true });
  const run = {
    startedAt,
    briefPath: join(outDir, "brief.txt"),
    finalPath: join(outDir, "final.txt"),
    logPath: join(outDir, "agy.log"),
    stderrPath: join(outDir, "stderr.txt"),
    resultPath: join(outDir, "result.json"),
  };
  writeFileSync(run.briefPath, brief, "utf8");
  writeFileSync(run.stderrPath, "", "utf8");
  return run;
}

function buildArgv(opts, brief, run) {
  const argv = [];
  if (opts.project) {
    argv.push("--project", opts.project);
  } else if (opts.conversation) {
    argv.push("--conversation", opts.conversation);
  } else if (opts.resumeLast) {
    argv.push("--continue");
  } else {
    argv.push("--new-project");
  }

  if (!opts.resumeLast && !opts.conversation) {
    // The disposable smoke showed that relying on cwd alone can produce a false
    // "I created the file" response, so pin the workspace explicitly. agy requires
    // an absolute path here (it rejects "." as non-absolute); opts.cd is already
    // resolve()d, and an argv-array element carries spaces fine without a shell.
    argv.push("--add-dir", opts.cd);
    for (const dir of opts.addDirs) argv.push("--add-dir", dir);
  }
  if (opts.model) argv.push("--model", opts.model);
  if (opts.effort) argv.push("--effort", opts.effort);
  if (opts.readOnly) argv.push("--mode", "plan");
  if (opts.sandbox) argv.push("--sandbox");
  if (opts.dangerouslySkipPermissions) argv.push("--dangerously-skip-permissions");
  if (opts.printTimeout) argv.push("--print-timeout", opts.printTimeout);
  argv.push("--log-file", run.logPath);
  // Use the --print=<brief> form, not a separate ["--print", brief] pair: agy's flag
  // parser intercepts a value that is exactly a bare flag (a brief consisting only of
  // "--help" or "-h" prints usage instead of running). The = form always binds the value.
  argv.push(`--print=${brief}`);
  return argv;
}

function parseIdsFromLog(logPath) {
  if (!existsSync(logPath)) return { projectId: null, conversationId: null };
  const text = readFileSync(logPath, "utf8");
  const projectMatches = [
    /project: created project "[^"]*" \(id=([0-9a-f-]+)\)/i,
    /Conversation using project ID: ([0-9a-f-]+)/i,
    /Backend project ID updated dynamically to: ([0-9a-f-]+)/i,
  ];
  const conversationMatches = [
    /Print mode: conversation=([0-9a-f-]+)/i,
    /Created conversation ([0-9a-f-]+)/i,
  ];
  const firstMatch = (patterns) => {
    for (const pattern of patterns) {
      const match = text.match(pattern);
      if (match) return match[1];
    }
    return null;
  };
  return {
    projectId: firstMatch(projectMatches),
    conversationId: firstMatch(conversationMatches),
  };
}

function makeResultWriter(opts, version, run) {
  return (extra) => {
    const ids = parseIdsFromLog(run.logPath);
    const result = {
      schema: "delegate-relay.result.v1",
      lane: opts.lane,
      laneSource: opts.laneSource,
      tool: "agy",
      workdir: opts.cd,
      model: opts.model,
      effort: opts.effort,
      project: opts.project,
      sandbox: opts.sandbox,
      readOnly: opts.readOnly,
      readOnlyViolation: null,
      dangerouslySkipPermissions: opts.dangerouslySkipPermissions,
      resumed: Boolean(opts.resumeLast || opts.conversation),
      agyVersion: version,
      projectId: ids.projectId,
      conversationId: ids.conversationId,
      startedAt: run.startedAt,
      finishedAt: new Date().toISOString(),
      briefPath: run.briefPath,
      finalPath: existsSync(run.finalPath) ? run.finalPath : null,
      logPath: existsSync(run.logPath) ? run.logPath : null,
      stderrPath: run.stderrPath,
      ...extra,
    };
    // Publish atomically so a polling orchestrator never reads a half-written file
    // (same idiom as claude-delegate's writeJsonAtomic and qoder-delegate).
    const temporary = `${run.resultPath}.${process.pid}.tmp`;
    writeFileSync(temporary, `${JSON.stringify(result, null, 2)}\n`, "utf8");
    renameSync(temporary, run.resultPath);
    return result;
  };
}

function reportUnavailable(writeResult, resultPath) {
  const result = writeResult({ status: "agy_unavailable", exitCode: 127, signal: null, finalMessage: "", touchedFiles: null });
  printSummary(result, resultPath);
  process.stderr.write("relay: `agy` not found on PATH. Install the Antigravity CLI and complete first-launch setup.\n");
  process.exit(127);
}

function reportVersionTimeout(writeResult, run, timeoutMs, error) {
  const stderr = String(error?.stderr || "").trim();
  if (stderr) writeFileSync(run.stderrPath, `${stderr}\n`, "utf8");
  const message = `agy changelog version preflight timed out after ${Math.min(timeoutMs, VERSION_PROBE_TIMEOUT_MS)}ms; agy was not dispatched`;
  const result = writeResult({
    status: "timeout",
    exitCode: 124,
    signal: null,
    finalMessage: "",
    touchedFiles: null,
    ...(stderr ? { stderrTail: stderr.split("\n").slice(-20) } : {}),
    error: message,
  });
  printSummary(result, run.resultPath);
  process.stderr.write(`relay: ${message}\n`);
  process.exit(result.exitCode);
}

function dispatchToAgy(opts, brief, run, writeResult, watchdogMs) {
  const relayArtifacts = [run.briefPath, run.finalPath, run.logPath, run.stderrPath, run.resultPath];
  const beforeState = gitWorktreeFingerprint(opts.cd, relayArtifacts);
  const argv = buildArgv(opts, brief, run);
  // Antigravity's installer provides a native `agy` binary. Launch directly so
  // multi-line briefs and paths with spaces are passed as argv, not shell text.
  const child = spawn("agy", argv, {
    cwd: opts.cd,
    env: { ...process.env, PWD: opts.cd },
    stdio: ["ignore", "pipe", "pipe"],
    detached: process.platform !== "win32", // POSIX: lead a new process group so killChild can fell the whole tree
  });

  let stdout = "";
  const stderrTail = [];
  let settled = false;
  let watchdogFired = false;
  let sigkillTimer = null;
  const watchdogTimer = setTimeout(() => {
    watchdogFired = true;
    child.once("exit", () => {
      child.stdout.destroy();
      child.stderr.destroy();
    });
    killChild(child);
    sigkillTimer = setTimeout(() => {
      if (!settled) killChild(child, "SIGKILL");
    }, 10_000);
  }, watchdogMs);

  // The relay's own death must still produce a result: without this, a kill from the
  // orchestrator's side (its command timeout, a stopped task, a closed terminal) writes
  // no result.json and leaves the agy child running or dying mid-edit with nothing
  // recording why. SIGTERM/SIGHUP registration is a no-op on Windows; SIGINT works there.
  for (const sig of ["SIGTERM", "SIGINT", "SIGHUP"]) {
    process.on(sig, () => {
      if (settled) return;
      settled = true;
      clearTimeout(watchdogTimer);
      if (sigkillTimer) clearTimeout(sigkillTimer);
      const finalMessage = stdout.trim();
      if (finalMessage) writeFileSync(run.finalPath, finalMessage, "utf8");
      const abortedFields = {
        status: "aborted",
        exitCode: 128 + (constants.signals[sig] || 15),
        signal: sig,
        finalMessage,
        stderrTail: stderrTail.slice(-20),
        error: `the relay was killed by ${sig}; agy was terminated with it — inspect the working tree before re-dispatching`,
      };
      let finalized = false;
      const finalizeAbort = () => {
        if (finalized) return;
        finalized = true;
        if (sigkillTimer) clearTimeout(sigkillTimer);
        const afterState = gitWorktreeFingerprint(opts.cd, relayArtifacts);
        const result = writeResult({
          ...abortedFields,
          touchedFiles: gitTouchedFiles(opts.cd),
          readOnlyViolation: readOnlyVerdict(opts, beforeState, afterState),
        });
        printSummary(result, run.resultPath);
        process.exit(result.exitCode);
      };
      child.once("close", finalizeAbort);
      killChild(child);
      sigkillTimer = setTimeout(() => {
        killChild(child, "SIGKILL");
      }, 2000);
    });
  }

  // Decode across chunk boundaries: a multibyte UTF-8 character split between
  // two data events would otherwise decode as U+FFFD and corrupt the report.
  const stdoutDecoder = new StringDecoder("utf8");
  const stderrDecoder = new StringDecoder("utf8");

  child.stdout.on("data", (chunk) => {
    stdout += stdoutDecoder.write(chunk);
  });

  child.stderr.on("data", (chunk) => {
    process.stderr.write(chunk);
    appendFileSync(run.stderrPath, chunk);
    const text = stderrDecoder.write(chunk);
    for (const line of text.split("\n")) {
      if (line.trim()) stderrTail.push(line.trimEnd());
    }
    while (stderrTail.length > 20) stderrTail.shift();
  });

  child.on("error", (err) => {
    if (settled) return;
    settled = true;
    clearTimeout(watchdogTimer);
    if (sigkillTimer) clearTimeout(sigkillTimer);
    const finalMessage = stdout.trim();
    if (finalMessage) writeFileSync(run.finalPath, finalMessage, "utf8");
    const afterState = gitWorktreeFingerprint(opts.cd, relayArtifacts);
    const readOnlyViolation = readOnlyVerdict(opts, beforeState, afterState);
    const result = writeResult({
      status: "failed",
      exitCode: 1,
      signal: null,
      finalMessage,
      touchedFiles: gitTouchedFiles(opts.cd),
      readOnlyViolation,
      error: String(err && err.message ? err.message : err),
    });
    printSummary(result, run.resultPath);
    process.exit(1);
  });

  child.on("close", (code, signal) => {
    if (settled) return;
    settled = true;
    clearTimeout(watchdogTimer);
    if (sigkillTimer) clearTimeout(sigkillTimer);
    // a descendant that ignored SIGTERM must not outlive the timeout report: once the
    // parent is down, sweep the group (no-op where taskkill already felled the tree)
    if (watchdogFired) killChild(child, "SIGKILL");
    const finalMessage = stdout.trim();
    if (finalMessage) writeFileSync(run.finalPath, finalMessage, "utf8");
    const touchedFiles = gitTouchedFiles(opts.cd);
    const stderr = readFileSync(run.stderrPath, "utf8");
    const diagnostics = stderr.split("\n").map((line) => line.trimEnd()).filter(Boolean).slice(-20);
    const permissionDenied = /no output produced\s+[—-]\s+a tool required the "([^"]+)" permission that headless\s+mode cannot prompt for, so it was auto-denied/i.exec(stderr);
    // A clean read-only run still owes the caller a plan. With neither a final message
    // nor observable worktree changes, exit 0 cannot confirm any dispatch completed.
    const afterState = gitWorktreeFingerprint(opts.cd, relayArtifacts);
    const worktreeChanged = beforeState !== null && afterState !== null && beforeState !== afterState;
    const readOnlyViolation = readOnlyVerdict(opts, beforeState, afterState);
    const silentNoop = code === 0 && !finalMessage && !worktreeChanged;
    // A timed-out run is failed even if agy handles SIGTERM by exiting 0 -
    // orchestrators key off status and the relay exit code.
    const succeeded = code === 0 && !watchdogFired && !permissionDenied && !silentNoop;
    const mapped = code ?? (constants.signals[signal] ? 128 + constants.signals[signal] : 1);
    const result = writeResult({
      status: succeeded ? "completed" : watchdogFired ? "timeout" : "failed",
      exitCode: succeeded ? 0 : mapped === 0 ? 1 : mapped,
      signal: signal ?? null,
      finalMessage,
      touchedFiles,
      readOnlyViolation,
      ...(!succeeded || !finalMessage ? { stderrTail: diagnostics } : {}),
      ...(watchdogFired
        ? {
            error: opts.timeout !== null
              ? `agy did not finish within --timeout ${opts.timeout}; killed by the relay watchdog`
              : `agy did not exit within --print-timeout ${opts.printTimeout} plus 60s grace; killed by the relay watchdog`,
          }
        : permissionDenied
          ? { error: `Antigravity auto-denied the ${permissionDenied[1]} permission because headless --print cannot prompt; ask the human whether to re-dispatch with --dangerously-skip-permissions and treat that run as full access` }
          : silentNoop
            ? { error: "agy exited 0 without a final message or observable working-tree changes; the relay cannot confirm this dispatch completed" }
            : {}),
    });
    printSummary(result, run.resultPath);
    process.exit(result.exitCode);
  });
}

function main() {
  const opts = parseArgs(process.argv.slice(2));
  const brief = readBrief(opts);
  if (!brief.trim()) fail("empty brief (pass --brief <file> or pipe the brief on stdin)");

  // agy --print takes the prompt as a CLI argument, so the brief rides argv. The OS caps a
  // single argument (~128KB on Linux via MAX_ARG_STRLEN), so a huge brief would fail to spawn
  // with an opaque E2BIG. Reject it early with a clear message instead of a generic failure.
  const briefBytes = Buffer.byteLength(brief, "utf8");
  const MAX_BRIEF_BYTES = 120 * 1024;
  if (briefBytes > MAX_BRIEF_BYTES) {
    fail(`brief is ${Math.round(briefBytes / 1024)}KB; agy passes the prompt as a CLI argument, which the OS caps (~128KB on Linux). Trim it, or have agy read large context from the workspace instead of inlining it.`);
  }

  const printTimeoutMs = parseDuration(opts.printTimeout);
  // An explicit --timeout wins over the default print-timeout-plus-grace: agy can hang well
  // past its own print timeout, which is exactly the case the grace window cannot cover.
  const watchdogMs = opts.timeout !== null ? parseDuration(opts.timeout) : printTimeoutMs + 60_000;
  const run = prepareRunDir(opts, brief);
  let version;
  try {
    version = agyVersion(watchdogMs);
  } catch (error) {
    const writeResult = makeResultWriter(opts, "unknown", run);
    reportVersionTimeout(writeResult, run, watchdogMs, error);
    return;
  }
  const writeResult = makeResultWriter(opts, version, run);

  if (!version) {
    reportUnavailable(writeResult, run.resultPath);
    return;
  }

  dispatchToAgy(opts, brief, run, writeResult, watchdogMs);
}

function printSummary(result, resultPath) {
  const lines = [];
  lines.push("");
  lines.push(`relay: ${result.status} (exit ${result.exitCode}${result.signal ? `, killed by ${result.signal}` : ""})  ·  agy ${result.agyVersion ?? "?"}`);
  if (result.signal === "SIGKILL" && result.status === "failed") lines.push("hint: the host killed the process (commonly the OOM killer or a supervisor timeout) — this is not an agy error; check host memory and re-dispatch, or split the task into smaller briefs.");
  if (result.signal === "SIGTERM" && result.status === "failed") lines.push("hint: something outside the relay terminated agy (a supervisor, the session ending, or a manual kill) — when the relay itself does the killing it reports status \"timeout\" or \"aborted\" instead; inspect the working tree before re-dispatching.");
  if (result.resumed) lines.push("mode: resumed an existing conversation");
  if (result.projectId) lines.push(`project id: ${result.projectId}`);
  if (result.conversationId) lines.push(`conversation id (resume with: --conversation ${result.conversationId}): ${result.conversationId}`);
  const touched = result.touchedFiles;
  if (touched === null) {
    lines.push("touched files: git unavailable - inspect the working tree directly");
  } else {
    lines.push(`touched files: ${touched.length}`);
    for (const file of touched.slice(0, 40)) lines.push(`  ${file}`);
    if (touched.length > 40) lines.push(`  ... and ${touched.length - 40} more`);
  }
  if (result.stderrTail && result.stderrTail.length) {
    lines.push("last stderr:");
    for (const line of result.stderrTail.slice(-8)) lines.push(`  ${line}`);
  }
  lines.push("");
  lines.push("--- agy final report ---");
  lines.push(result.finalMessage || "(no final message captured)");
  lines.push("--- end report ---");
  lines.push("");
  lines.push(`result: ${resultPath}`);
  lines.push("relay does not commit. Review the diff, re-run the project gates yourself, then commit from the orchestrator.");
  process.stdout.write(`${lines.join("\n")}\n`);
}

main();
SKILL.md
---
name: agy-delegate
description: >-
  Delegate a coding task to the Google Antigravity CLI (`agy`) as a background implementer, then review
  its diff and land it yourself. Use this whenever the user wants to hand implementation work to
  Antigravity or agy - phrasings like "have Antigravity do X", "delegate this to agy", "run it through
  agy", or "use Antigravity to implement/fix/refactor" - or wants to run a queue of coding tasks
  through agy while staying the reviewer. DO NOT USE for tasks small enough to do inline, or when the
  user wants the code written directly without delegating.
license: MIT
compatibility: Requires the `agy` CLI installed and authenticated, Node.js, and git. The orchestrator must be able to run shell commands and read files. Shell examples assume bash/zsh (macOS/Linux, or Git Bash/WSL on Windows).
metadata:
  version: 0.5.0
---

# Antigravity Delegate

You are the **orchestrator**. This skill lets you hand a bounded coding task to a separate
**implementer** - the Google Antigravity CLI (`agy`) - then review what it produced and land it
yourself. You write the brief and own the judgment; Antigravity does the typing in its own
conversation; you verify and commit.

Nothing here is specific to one orchestrating agent. The loop needs only the ability to run a shell
command and read a file, so any comparable agent can drive it. It is designed for and run on Claude
Code; treat other orchestrators as designed-for, not yet proven.

## When NOT to use this

- The task is small enough to just do inline - delegation overhead is not worth it.
- The `agy` CLI is not installed or not authenticated. Install it from Antigravity's CLI docs and run
  the first-launch setup.
- You want to write the code yourself, or you only need Antigravity's opinion on code you wrote (a
  `--read-only` dispatch covers review without edits, but a plain review may not need delegation at all).

## Prerequisites (check once)

1. `agy help` succeeds. If not, install the Antigravity CLI and complete first-launch setup.
2. `agy models` succeeds. That proves the CLI can authenticate and list the available model labels.
3. You are in (or will point `--cd` at) the target git repository.

These checks do not prove that a headless write will be approved. In `--print` mode, Antigravity
cannot prompt for a write permission and may auto-deny it. The relay detects that denial instead of
reporting completion.

## Choose the implementer model

`agy` has a configured default model, so `--model` is optional. Use it when the human has a preferred
Antigravity model label for the task. Otherwise let Antigravity use its own current default rather than
guessing.

## The loop

Run these five steps per task. Steps 1, 4, and 5 are your judgment; 2 and 3 are mechanical.

### 1. Write the brief

Antigravity sees only the text you send plus what it can inspect in the workspace - no chat history, no
shared context. Everything the task needs goes in the brief: the goal, the current state, what to
change, what to leave untouched, the project's **actual** gate commands, and a report contract. Tell
Antigravity it will **not** commit (you will). Keep one task per brief. Full guidance and a template:
[references/writing-the-brief.md](references/writing-the-brief.md).

### 2. Dispatch

Send the brief to Antigravity with the bundled helper. It wraps `agy --print`, captures the run, and
writes a structured `result.json` - so your only job is "run a command, read a file." (`<skill-dir>`
below is this skill's installed directory - the folder containing this `SKILL.md`.)

```bash
node "<skill-dir>/scripts/relay.mjs" --brief brief.txt --cd /path/to/repo
# choose a model label:                 add --model "<label from agy models>"
# reasoning effort (low, medium, high): add --effort high
# read-only (plan mode — no edits):     add --read-only
# enable Antigravity terminal sandbox:  add --sandbox
# resume the most recent conversation:  add --resume-last  (delta brief only)
# see all options:                      node .../relay.mjs --help
```

The helper starts a fresh Antigravity project by default and passes `--add-dir <repo>` (the `--cd`
path, absolute) so `agy` has an explicit workspace. It does **not** pass `--dangerously-skip-permissions` by default.
Mechanics, flags, and the `result.json` shape: [references/dispatch-and-poll.md](references/dispatch-and-poll.md).

### 3. Wait for completion

The helper blocks until Antigravity finishes, so back it with whatever your orchestrator offers and
resume when it returns:

- **Claude Code:** run the Bash call with `run_in_background: true`; you are notified on completion.
- **Plain shell / other agents:** run it in the foreground for short tasks, or background it and poll
  the result file.

Do not trust progress trackers over reality: a run is finished when `result.json` is written and the
process has exited. Read the working tree, not a status line. The implementer's full report is
the `finalMessage` field in `result.json` (also printed in full on stdout between the report markers).

### 4. Review - do not trust the self-report

Antigravity's `result.json` includes its own final message and any gate claims. **Re-verify, don't
accept:**

- **Re-run the project's gates yourself** (the test/lint/build commands from step 1).
- **Read the diff** against the brief: did Antigravity do what was asked, nothing more and nothing less?
  `touchedFiles` in the result is your starting point.
- **Run the relevant guard skills** on the diff if you have them installed.
- For schema/migration changes, round-trip them; for removals, grep for dangling references.

Full checklist: [references/review-and-land.md](references/review-and-land.md).

### 5. Land it

The implementer edits the working tree; **the orchestrator commits.** Only after the gates pass and the
diff holds:

- Commit the verified work yourself, with a clear message.
- If it needs changes, send a delta brief with `--resume-last` and review again.

## Permission model

Antigravity owns its own permission policy. The relay does not bypass it by default. Use
`--dangerously-skip-permissions` only when the human explicitly accepts that Antigravity may
auto-approve tool permission requests. `--read-only` runs `agy` in plan mode (`--mode plan`),
removing write and edit paths, and is mutually exclusive with `--dangerously-skip-permissions`.
Use `--sandbox` when you want Antigravity's terminal sandbox enabled for the run.
Antigravity's own help says `--dangerously-skip-permissions` auto-approves all tool permission
requests without prompting, including a request to act outside the sandbox. Do not treat
`--sandbox` as an enforced boundary when the flags are combined; treat the run as full access.
If headless `--print` auto-denies a write, the relay reports `status: "failed"` and exits non-zero.
The relay fingerprints the working tree before and after a `--read-only` run to report
`readOnlyViolation` in `result.json`. Settings allow-rules are not documented here as a fix
because they have not been demonstrated to apply to this headless path. Do not add the bypass
flag without explicit human approval.

## Authorization model

Delegation is something the human opts into. Once they have ("run this queue", "proceed"), committing
verified, gate-passing work is the agreed contract. Two limits on that mandate: **surface, don't
absorb** (report Antigravity's design decisions, defensible-but-unasked turns, and non-blocking
nitpicks rather than silently keeping them) and **stop for scope changes** (if correct completion needs
going beyond the brief, ask - don't expand the mandate yourself). The full treatment is in
[references/review-and-land.md](references/review-and-land.md).

## References

- [references/writing-the-brief.md](references/writing-the-brief.md) - how to write a brief Antigravity
  can execute blind: structure, XML blocks, the report contract, and real gate commands.
- [references/dispatch-and-poll.md](references/dispatch-and-poll.md) - `relay.mjs` flags, the
  `result.json` contract, backgrounding per orchestrator, and recovery when a run misbehaves.
- [references/review-and-land.md](references/review-and-land.md) - the review checklist, the commit
  boundary, and the rework cycle via `--resume-last`.
- [references/multi-task-queues.md](references/multi-task-queues.md) - running a sequential queue:
  carrying constraints forward, progress tracking, and the end-of-run coherence check.