Compare commits
2 Commits
727a0757f6
...
78f794a924
| Author | SHA1 | Date | |
|---|---|---|---|
| 78f794a924 | |||
| 41c12a934f |
2
.bb-tok
2
.bb-tok
@@ -1 +1 @@
|
||||
eyJ0eXAiOiJKV1QiLCJub25jZSI6IlhjWmZZT3hzX0lySmZ6a3E5dnozYzhrSGZqM003c2N3UDZwLWFQUTBfaGciLCJhbGciOiJSUzI1NiIsIng1dCI6ImFGa21LVkZjLTRXVjZzWENCdk5aa1hJNTA1WSIsImtpZCI6ImFGa21LVkZjLTRXVjZzWENCdk5aa1hJNTA1WSJ9.eyJhdWQiOiJodHRwczovL2dyYXBoLm1pY3Jvc29mdC5jb20iLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC8xOWE1NjhlOC05ZTg4LTQxM2ItOTM0MS1jYmMyMjRiMzkxNDUvIiwiaWF0IjoxNzgzMDM5NDc0LCJuYmYiOjE3ODMwMzk0NzQsImV4cCI6MTc4MzA0MzM3NCwiYWNycyI6WyJwZmRyIl0sImFpbyI6ImsyRmdZTGg5Y083a1hPUElXdVk3ZGFHQmJSRzEvbjlrMlc1TnVGQ2E2ZmN0dE5qd3htWUEiLCJhcHBfZGlzcGxheW5hbWUiOiJDb21wdXRlckd1cnUgLSBUZW5hbnQgQWRtaW4iLCJhcHBpZCI6IjcwOWU2ZWVkLTA3MTEtNDg3NS05YzQ0LTJkMzUxOGM0NzA2MyIsImFwcGlkYWNyIjoiMiIsImlkcCI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0LzE5YTU2OGU4LTllODgtNDEzYi05MzQxLWNiYzIyNGIzOTE0NS8iLCJpZHR5cCI6ImFwcCIsIm9pZCI6IjdhMTk5YjExLTk3ZmItNGU2NS05MTdkLWY4ZDI5YTUzYmE0OSIsInJoIjoiMS5BVVlBNkdpbEdZaWVPMEdUUWN2Q0pMT1JSUU1BQUFBQUFBQUF3QUFBQUFBQUFBQUFBQUJHQUEuIiwicm9sZXMiOlsiUG9saWN5LlJlYWRXcml0ZS5Db25kaXRpb25hbEFjY2VzcyIsIlVzZXIuUmVhZFdyaXRlLkFsbCIsIlNlY3VyaXR5RXZlbnRzLlJlYWQuQWxsIiwiVXNlckF1dGhlbnRpY2F0aW9uTWV0aG9kLlJlYWRXcml0ZS5BbGwiLCJBcHBsaWNhdGlvbi5SZWFkV3JpdGUuQWxsIiwiRGlyZWN0b3J5LlJlYWRXcml0ZS5BbGwiLCJTaXRlcy5SZWFkV3JpdGUuQWxsIiwiQXBwUm9sZUFzc2lnbm1lbnQuUmVhZFdyaXRlLkFsbCIsIlJvbGVNYW5hZ2VtZW50LlJlYWRXcml0ZS5EaXJlY3RvcnkiLCJQb2xpY3kuUmVhZC5BbGwiLCJTaXRlcy5GdWxsQ29udHJvbC5BbGwiXSwic3ViIjoiN2ExOTliMTEtOTdmYi00ZTY1LTkxN2QtZjhkMjlhNTNiYTQ5IiwidGVuYW50X3JlZ2lvbl9zY29wZSI6Ik5BIiwidGlkIjoiMTlhNTY4ZTgtOWU4OC00MTNiLTkzNDEtY2JjMjI0YjM5MTQ1IiwidXRpIjoiMEl3N1NVWTRua0NibU9kVzZ1VW5BQSIsInZlciI6IjEuMCIsIndpZHMiOlsiYjFiZTFjM2UtYjY1ZC00ZjE5LTg0MjctZjZmYTBkOTdmZWI5IiwiMDk5N2ExZDAtMGQxZC00YWNiLWI0MDgtZDVjYTczMTIxZTkwIl0sInhtc19hY2QiOjE3NzY3MDg0MDEsInhtc19hY3RfZmN0IjoiMyA5IiwieG1zX2Z0ZCI6Ill6QjIwNkktSVJiOXBPTkN1WkZIZFpzR2VVaThJekpBZlVVMVQ2NUxxM1VCZFhOM1pYTjBNeTFrYzIxeiIsInhtc19pZHJlbCI6IjQgNyIsInhtc19wZnRleHAiOjE3ODMxMjk3NzQsInhtc19yZCI6IjAuNDJMbFlCSmlkQk1TNFdBVkVwQnpTenkzb0xIRlo3ZFh0YW5YTE5OY0lSRU9UaUdCdEk5ZVV3LWVldWs1UWV3NWMtTjJsczlDSWh3Y1FnTE1EQkJ3QUVvTGlYQndDd2xzZVNEN05kemY3dVFpU2FrV241YXRBZ0EiLCJ4bXNfc3ViX2ZjdCI6IjMgOSIsInhtc190Y2R0IjoxNTgxMDIyOTI3LCJ4bXNfdG50X2ZjdCI6IjMgNCJ9.nz5EdcWvEBK4R-D61jyT72d7Bah5gPGJxMfNfocee6DoMZK1rzOMEPwisIR98OD1aq7C3hn9FzB4bn4SCbHdXo5abETX2xn1Tr9hFCuJrXESC0fMbssfIjzV4bO-xVqQk-ylQAax0vaH05SvL2SMqAZYaM86e-K9IIgKc_IYjzVfvjuKbVyWw2s4NeBki4gwAoesPp1DS6K_RDEDpov_x0B6PULGDbQYzwe1YriA1kmnfYP_56YJqLCHMBFH7FeXkDeRpAVSMqWeZOALoA1EYaX1InF22t1alblT59m6rEN6C_Xo39xzwUKhisHPIcRZLNyS2d7NP3X2vtXd44n8FQ
|
||||
eyJ0eXAiOiJKV1QiLCJub25jZSI6IlNzRUhBcVJSVzVzOFYwTzBaVVBUdnZGZjRnVXl2YmRvNjFiVklhYjJNbDgiLCJhbGciOiJSUzI1NiIsIng1dCI6ImFGa21LVkZjLTRXVjZzWENCdk5aa1hJNTA1WSIsImtpZCI6ImFGa21LVkZjLTRXVjZzWENCdk5aa1hJNTA1WSJ9.eyJhdWQiOiJodHRwczovL2dyYXBoLm1pY3Jvc29mdC5jb20iLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC8xOWE1NjhlOC05ZTg4LTQxM2ItOTM0MS1jYmMyMjRiMzkxNDUvIiwiaWF0IjoxNzgzMDQ4NDQwLCJuYmYiOjE3ODMwNDg0NDAsImV4cCI6MTc4MzA1MjM0MCwiYWNycyI6WyJwZmRyIl0sImFpbyI6ImsyRmdZQWhtcnMxdW5yZlo2TmVTNUxmbDNzNkZNNXA0YnNoN1RuNGhmc1hJZkhWYjkzMEEiLCJhcHBfZGlzcGxheW5hbWUiOiJDb21wdXRlckd1cnUgLSBUZW5hbnQgQWRtaW4iLCJhcHBpZCI6IjcwOWU2ZWVkLTA3MTEtNDg3NS05YzQ0LTJkMzUxOGM0NzA2MyIsImFwcGlkYWNyIjoiMiIsImlkcCI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0LzE5YTU2OGU4LTllODgtNDEzYi05MzQxLWNiYzIyNGIzOTE0NS8iLCJpZHR5cCI6ImFwcCIsIm9pZCI6IjdhMTk5YjExLTk3ZmItNGU2NS05MTdkLWY4ZDI5YTUzYmE0OSIsInJoIjoiMS5BVVlBNkdpbEdZaWVPMEdUUWN2Q0pMT1JSUU1BQUFBQUFBQUF3QUFBQUFBQUFBQUFBQUJHQUEuIiwicm9sZXMiOlsiUG9saWN5LlJlYWRXcml0ZS5Db25kaXRpb25hbEFjY2VzcyIsIlVzZXIuUmVhZFdyaXRlLkFsbCIsIlNlY3VyaXR5RXZlbnRzLlJlYWQuQWxsIiwiVXNlckF1dGhlbnRpY2F0aW9uTWV0aG9kLlJlYWRXcml0ZS5BbGwiLCJBcHBsaWNhdGlvbi5SZWFkV3JpdGUuQWxsIiwiRGlyZWN0b3J5LlJlYWRXcml0ZS5BbGwiLCJTaXRlcy5SZWFkV3JpdGUuQWxsIiwiQXBwUm9sZUFzc2lnbm1lbnQuUmVhZFdyaXRlLkFsbCIsIlJvbGVNYW5hZ2VtZW50LlJlYWRXcml0ZS5EaXJlY3RvcnkiLCJQb2xpY3kuUmVhZC5BbGwiLCJTaXRlcy5GdWxsQ29udHJvbC5BbGwiXSwic3ViIjoiN2ExOTliMTEtOTdmYi00ZTY1LTkxN2QtZjhkMjlhNTNiYTQ5IiwidGVuYW50X3JlZ2lvbl9zY29wZSI6Ik5BIiwidGlkIjoiMTlhNTY4ZTgtOWU4OC00MTNiLTkzNDEtY2JjMjI0YjM5MTQ1IiwidXRpIjoiRFE2UE4wWlRqMEdSMDhWeEhhUWJBQSIsInZlciI6IjEuMCIsIndpZHMiOlsiYjFiZTFjM2UtYjY1ZC00ZjE5LTg0MjctZjZmYTBkOTdmZWI5IiwiMDk5N2ExZDAtMGQxZC00YWNiLWI0MDgtZDVjYTczMTIxZTkwIl0sInhtc19hY2QiOjE3NzY3MDg0MDEsInhtc19hY3RfZmN0IjoiOSAzIiwieG1zX2Z0ZCI6IjZuYTJJRXF0MWliaVd5emFrbzdzM0VHbEdnbm1JRlBUSjdFWUZTcEZPcjBCZFhOemIzVjBhQzFrYzIxeiIsInhtc19pZHJlbCI6IjEwIDciLCJ4bXNfcGZ0ZXhwIjoxNzgzMTM4NzQwLCJ4bXNfcmQiOiIwLjQyTGxZQkppZEJNUzRXQVZFcEJ6U3p5M29MSEZaN2RYdGFuWExOTmNJUkVPVGlHQnRJOWVVdy1lZXVrNVFldzVjLU4ybHM5Q0lod2NRZ0xNREJCd0FFb0xpWEJ3Q3dsc2VTRDdOZHpmN3VRaVNha1duNWF0QWdBIiwieG1zX3N1Yl9mY3QiOiIzIDkiLCJ4bXNfdGNkdCI6MTU4MTAyMjkyNywieG1zX3RudF9mY3QiOiIzIDE0In0.Rfz02nCkcm5iJ19EQVEFhZEcBf_xdd6kXTck-paz5UVgRhSWE8TDPfF9eliB38OZ59bRJdkY6TdtUNrlAAHhJxHs8Hxq6vyYJwKAhzfD-XYntD00Mas1yEe1pobc4vjLPrNYl5ie2KIZvSpmvUc97Lvf5juRueRbvUh9k9QgVwKfGBCHVXirBv9Bekhx4XUraskE-UvROmDvf5mi1DtkskMIC4-Xutz6gFTbJDj-uwTtI2vZa9dQMsVJfMpmq6pWD5lNrIm1tk6LMMIBVA00h3ue5hIt7v5IggvFFqk2JlacMnElQPNkgpwaMJTdcwrVEuonVZuKpv3iYZJU8MtCcg
|
||||
|
||||
1
.claude/MEMORY.md
Normal file
1
.claude/MEMORY.md
Normal file
@@ -0,0 +1 @@
|
||||
- [PS5.1 -Headers $H empty-auth quirk](.claude/memory/ps51_invoke_restmethod_headers_variable.md) - build Graph auth headers inline per call under RMM powershell -File; capture Graph error bodies
|
||||
25
.claude/memory/ps51_invoke_restmethod_headers_variable.md
Normal file
25
.claude/memory/ps51_invoke_restmethod_headers_variable.md
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: ps51-invoke-restmethod-headers-variable
|
||||
description: PS5.1 via RMM powershell -File - Invoke-RestMethod -Headers $H (script-scope hashtable) sent an EMPTY Authorization header; build headers inline per call instead
|
||||
metadata:
|
||||
type: reference
|
||||
---
|
||||
|
||||
On ACG-DWP-X-BB (WS2019, PowerShell 5.1, script executed via GuruRMM agent as
|
||||
`powershell -NoProfile -File script.ps1`), passing a script-scope hashtable variable to
|
||||
`Invoke-RestMethod -Headers $H` (where `$H=@{Authorization="Bearer $tok"}`) resulted in
|
||||
Graph rejecting every call with `InvalidAuthenticationToken / "Access token is empty"` —
|
||||
even though the token variable was verifiably populated (fingerprint printed correctly)
|
||||
and the same token + same URL worked with an inline-built header from the same machine.
|
||||
|
||||
**Fix that works:** build the header at each call site — e.g.
|
||||
`function Hdr { return @{Authorization=("Bearer " + $script:tok)} }` then
|
||||
`Invoke-RestMethod -Uri $u -Headers (Hdr)`. Inline `@{Authorization=('Bearer '+$t)}` also works.
|
||||
|
||||
Diagnosed 2026-07-02 during the BirthBio Datto-vs-SharePoint reconciliation after two
|
||||
full failed runs (masqueraded first as token expiry, then as clock skew — the real tell
|
||||
was the Graph error body "Access token is empty", captured only after adding response-body
|
||||
extraction to the retry helper). Always capture the Graph error BODY, not just the
|
||||
exception message: "(401) Unauthorized" alone cost three debugging cycles.
|
||||
|
||||
Related: [[gururmm-command-timeout-seconds]], [[sharepoint-graph-large-file-upload]]
|
||||
@@ -1,45 +1,68 @@
|
||||
---
|
||||
name: agy
|
||||
description: "Route a task to the Google Gemini CLI for an independent second model (sibling of grok): different-vendor second opinion, adversarial verification, Gemini code review of files/diffs, one-shot text answers. Triggers: ask gemini, gemini verify, gemini review, agy."
|
||||
description: "Route a task to the Google Antigravity CLI (agy) for an independent second model (sibling of grok): different-vendor second opinion, adversarial verification, code review of files/diffs, vision, live web search, one-shot text answers. Triggers: ask gemini, ask agy, agy verify, agy review, agy."
|
||||
---
|
||||
|
||||
# AGY — Gemini capability router
|
||||
# AGY — Antigravity CLI capability router
|
||||
|
||||
Claude shells out to the locally-installed **Google Gemini CLI** (`gemini`, npm
|
||||
global, v0.45.2) for a genuinely independent, different-vendor second model.
|
||||
AGY is the sibling of [`grok`](../grok/SKILL.md): both are second-opinion /
|
||||
review routers. Use whichever you want a second model from (or both, to triangulate).
|
||||
Verified working on this machine (2026-06-05; re-validated 2026-06-17 against the
|
||||
CLI's bundled help/README — JSON schema, all flags, pinned model, and live search
|
||||
all confirmed): text, verify, review (single file / file set / git diff),
|
||||
image-analyze (vision input), search (live Google web search). All KEYLESS — they
|
||||
work on Google OAuth, no API key.
|
||||
Claude shells out to the locally-installed **Google Antigravity CLI** (`agy`, a
|
||||
native Go binary at `~/AppData/Local/agy/bin/agy`, v1.0.16) for a genuinely
|
||||
independent, different-vendor second model. AGY is the sibling of
|
||||
[`grok`](../grok/SKILL.md): both are second-opinion / review routers. Use whichever
|
||||
you want a second model from (or both, to triangulate). Modes verified on this
|
||||
machine (2026-07-02): text, verify, review (single file / file set / git diff),
|
||||
image-analyze (vision), search (live web search + source URLs).
|
||||
|
||||
**Auth:** Gemini uses **Google login (OAuth)** — **no API key**. Creds live at
|
||||
`~/.gemini/oauth_creds.json`. If calls fail with an auth error, run `gemini`
|
||||
interactively once and choose **"Login with Google"**, then retry.
|
||||
> **REWIRED 2026-07-02 — `agy` replaces the old `gemini` npm CLI.** The former
|
||||
> Google `gemini` CLI (npm `@google/gemini-cli`) stopped working on this account
|
||||
> with a Cloud-project eligibility error (`throwIneligibleOrProjectIdError` — it
|
||||
> demanded a `GOOGLE_CLOUD_PROJECT` the personal account couldn't provide). The
|
||||
> Antigravity CLI (`agy`) uses Antigravity's OWN auth and works headless with no
|
||||
> project ID. The wrapper is `scripts/ask-agy.sh`; `scripts/ask-gemini.sh` is kept
|
||||
> as a thin **deprecated shim** that `exec`s ask-agy.sh so old references still work.
|
||||
|
||||
**Backend models (multi-vendor, `--model`):** `agy models` lists them — Gemini 3.5
|
||||
Flash (Low/Medium/High), Gemini 3.1 Pro (Low/High), Claude Sonnet 4.6 (Thinking),
|
||||
Claude Opus 4.6 (Thinking), GPT-OSS 120B. `text` uses the CLI default (Flash, fast);
|
||||
`verify`/`review*`/`image-analyze`/`search` pin the strong model
|
||||
`Gemini 3.1 Pro (High)`. Override any mode with `AGY_MODEL="<friendly name>"`.
|
||||
|
||||
**Auth:** Antigravity's own login (`~/.gemini/antigravity-cli/`) — **no API key, no
|
||||
`GOOGLE_CLOUD_PROJECT`**. If a call fails with an auth error, run `agy` interactively
|
||||
once to sign in, then retry.
|
||||
|
||||
## The wrapper
|
||||
|
||||
```
|
||||
bash "$CLAUDETOOLS_ROOT/.claude/skills/agy/scripts/ask-gemini.sh" <mode> ...
|
||||
bash "$CLAUDETOOLS_ROOT/.claude/skills/agy/scripts/ask-agy.sh" <mode> ...
|
||||
```
|
||||
|
||||
Authoritative headless flags (from `agy --help`, Go flag package — the web docs at
|
||||
antigravity.google/docs/cli are JS-rendered SPAs and unreadable by fetch tools, so
|
||||
`--help` is the source of truth): `-p/--print/--prompt` runs one prompt and prints
|
||||
the response (**the prompt is the flag's VALUE and MUST be last** — a bare `-p --model`
|
||||
makes "--model" the prompt); `--model "<name>"`; `--add-dir <dir>` (repeatable, gives
|
||||
the file tools access to a review/vision target); `--dangerously-skip-permissions`
|
||||
(auto-approve tool calls — without it print mode HANGS on an approval prompt);
|
||||
`--print-timeout <dur>` (default 5m). There is **no JSON output flag** — stdout is
|
||||
plain text/markdown, so the wrapper does no JSON parsing.
|
||||
|
||||
| Mode | Usage | What it does |
|
||||
|------|-------|--------------|
|
||||
| `text` | `ask-gemini.sh text "<prompt>"` or `text --prompt-file <path>` | One-shot text answer from an independent model. `--prompt-file` for long content (review/summarize a doc). Default model routing. |
|
||||
| `verify` | `ask-gemini.sh verify "<claim/finding>"` or `verify --prompt-file <path>` | Adversarial second opinion — Gemini tries to REFUTE / find gaps, returns a verdict + reasons. Pinned to the strong model. |
|
||||
| `review` | `ask-gemini.sh review <file-path> ["<instructions>"]` | Gemini reads the file itself (its `read_file` tool, read-only `plan` mode) and reviews it. Path resolution: absolute, CWD-relative, or relative to `$CLAUDETOOLS_ROOT` — **see the path gotcha below**. Spaces OK. Works even on gitignored files. |
|
||||
| `review-files` | `ask-gemini.sh review-files [-i "<instr>"] <f1> [f2 …]` | Review a **set** of files together (cross-file consistency, multi-file change). Same path resolution as `review` (**see gotcha below**); spaces OK. No code passed as a shell arg. |
|
||||
| `review-diff` | `ask-gemini.sh review-diff [-C <repo-dir>] [-i "<instr>"] <gitref> [-- <pathspec>]` | Review a **git diff** (`git diff <gitref>` from `<repo-dir>`; default repo root, use `-C` for a submodule e.g. `-C projects/msp-tools/guru-rmm`). Diff goes via the prompt file; Gemini can `read_file` changed files for full context. |
|
||||
| `image-analyze` | `ask-gemini.sh image-analyze <image-path> ["<question>"]` | **Vision** — Gemini `read_file`s the image and describes/answers about it. Pins the **pro vision model** (the default flash-lite router hallucinates image content). Path absolute or repo-relative; spaces OK. KEYLESS (works on OAuth). |
|
||||
| `search` | `ask-gemini.sh search "<query>"` (or `search --prompt-file <path>`) | **Live Google web search** (sibling of `grok xsearch`) — Gemini uses its `google_web_search` tool and returns the answer **with source URLs**. KEYLESS (works on OAuth). |
|
||||
| `raw` | `ask-gemini.sh raw <gemini args...>` | Escape hatch — passes args straight to `gemini`. |
|
||||
| `text` | `ask-agy.sh text "<prompt>"` or `text --prompt-file <path>` | One-shot text answer from an independent model. `--prompt-file` for long content (review/summarize a doc). Default model routing. |
|
||||
| `verify` | `ask-agy.sh verify "<claim/finding>"` or `verify --prompt-file <path>` | Adversarial second opinion — Gemini tries to REFUTE / find gaps, returns a verdict + reasons. Pinned to the strong model. |
|
||||
| `review` | `ask-agy.sh review <file-path> ["<instructions>"]` | agy reads the file itself (copied into an `--add-dir` workspace) and reviews it. Path resolution: absolute, CWD-relative, or relative to `$CLAUDETOOLS_ROOT` — **see the path gotcha below**. Spaces OK. Works even on gitignored files. |
|
||||
| `review-files` | `ask-agy.sh review-files [-i "<instr>"] <f1> [f2 …]` | Review a **set** of files together (cross-file consistency, multi-file change). Same path resolution as `review` (**see gotcha below**); spaces OK. No code passed as a shell arg. |
|
||||
| `review-diff` | `ask-agy.sh review-diff [-C <repo-dir>] [-i "<instr>"] <gitref> [-- <pathspec>]` | Review a **git diff** (`git diff <gitref>` from `<repo-dir>`; default repo root, use `-C` for a submodule e.g. `-C projects/msp-tools/guru-rmm`). Diff goes via the prompt file; Gemini can `read_file` changed files for full context. |
|
||||
| `image-analyze` | `ask-agy.sh image-analyze <image-path> ["<question>"]` | **Vision** — agy reads the image (via `--add-dir`) and describes/answers about it. Pinned to the strong model. Path absolute or repo-relative; spaces OK. |
|
||||
| `search` | `ask-agy.sh search "<query>"` (or `search --prompt-file <path>`) | **Live web search** (sibling of `grok xsearch`) — agy searches the web and returns the answer **with source URLs**. |
|
||||
| `raw` | `ask-agy.sh raw <agy args...>` | Escape hatch — passes args straight to `agy`. |
|
||||
|
||||
The script runs Gemini headless with `-o json`, extracts the answer from
|
||||
`.response` (parsing from the first `{` so the CLI's cosmetic warning lines are
|
||||
ignored), and keeps stderr separate from the JSON so 429-backoff / warning noise
|
||||
never corrupts the parse.
|
||||
The script runs `agy` headless (`--dangerously-skip-permissions ... -p "<prompt>"`),
|
||||
captures plain-text stdout (no JSON — agy has no structured-output mode), keeps stderr
|
||||
separate for diagnostics, and retries a couple times with backoff on an empty turn.
|
||||
For review/vision it copies the target into a temp dir and `--add-dir`s it so agy's
|
||||
file tools can read it regardless of location/spaces.
|
||||
|
||||
> [!WARNING]
|
||||
> **Path gotcha for `review` / `review-files` (this has bitten us repeatedly).**
|
||||
@@ -55,96 +78,84 @@ never corrupts the parse.
|
||||
|
||||
### Model
|
||||
|
||||
- `text` uses Gemini's **default routing** (currently a flash-tier model) — fast, cheap.
|
||||
- `verify` / `review*` pin a **strong** model — `gemini-3.1-pro-preview` (verified
|
||||
available 2026-06-05, still valid 2026-06-17; the GA-looking `gemini-3.1-pro` and
|
||||
`gemini-3-pro` both `ModelNotFoundError`, so keep the `-preview` suffix).
|
||||
- Override either with `GEMINI_MODEL=<id>` (e.g. `GEMINI_MODEL=gemini-2.5-pro`).
|
||||
- `image-analyze` and `search` also pin the strong model (`GEMINI_MODEL` still honored).
|
||||
- `text` uses the CLI **default** (Gemini 3.5 Flash) — fast, cheap.
|
||||
- `verify` / `review*` / `image-analyze` / `search` pin the strong model
|
||||
**`Gemini 3.1 Pro (High)`** via `--model`.
|
||||
- Override any mode with `AGY_MODEL="<friendly name>"` (exact strings from
|
||||
`agy models`, e.g. `AGY_MODEL="Claude Opus 4.6 (Thinking)"`).
|
||||
|
||||
### Multimodal: image INPUT works, image GENERATION does not
|
||||
### Vision
|
||||
|
||||
- **Image INPUT (vision) works on OAuth** — `image-analyze` reads an image with the
|
||||
pinned **pro vision model** and describes it correctly. The default flash-lite
|
||||
router HALLUCINATES image content, which is why the pro model is pinned.
|
||||
- **Image GENERATION (nano-banana) does NOT work on OAuth** — it needs a Google AI
|
||||
Studio `NANOBANANA_API_KEY` plus the `nanobanana` extension. **Deferred** for now.
|
||||
Image/video **generation** stays [GROK](../grok/SKILL.md)'s lane (`grok image` /
|
||||
`grok video`); AGY's multimodal support is read/analyze only.
|
||||
`image-analyze` copies the image into an `--add-dir` workspace and asks agy to read
|
||||
and describe it (pinned to the strong model). Report-only — describes an image you
|
||||
give it. Image/video **generation** stays [GROK](../grok/SKILL.md)'s lane
|
||||
(`grok image` / `grok video`).
|
||||
|
||||
## Machine availability (fleet)
|
||||
|
||||
AGY is **per-machine** — the skill syncs fleet-wide but the `gemini` binary does
|
||||
not. Availability is gated by `identity.json` (per-machine, gitignored):
|
||||
AGY is **per-machine** — the skill syncs fleet-wide but the `agy` binary does not.
|
||||
Availability is gated by `identity.json` (per-machine, gitignored):
|
||||
|
||||
```json
|
||||
"gemini": { "installed": true,
|
||||
"binary": "C:/Users/guru/AppData/Roaming/npm/gemini",
|
||||
"auth": "oauth", "is_fleet_host": true,
|
||||
"capabilities": ["text","verify","review","image-analyze","search"] }
|
||||
"agy": { "installed": true,
|
||||
"binary": "C:/Users/guru/AppData/Local/agy/bin/agy",
|
||||
"auth": "antigravity", "is_fleet_host": true,
|
||||
"capabilities": ["text","verify","review","review-files","review-diff","image-analyze","search"] }
|
||||
```
|
||||
|
||||
- If `gemini.installed` is `false` (or the block is absent), `ask-gemini.sh` exits
|
||||
**3** with routing guidance instead of failing obscurely. Claude on such a
|
||||
machine should NOT attempt local Gemini.
|
||||
- **Fleet Gemini hosts: `GURU-5070`, `GURU-BEAST-ROG`** — machines with the Gemini
|
||||
CLI installed and Google-OAuth'd. When others get it, install
|
||||
`@google/gemini-cli`, run `gemini` once to log in with Google, then set their
|
||||
`identity.json` `gemini` block (and update this line).
|
||||
- If `agy.installed` is `false` (or the block is absent), `ask-agy.sh` exits **3**
|
||||
with routing guidance instead of failing obscurely. Claude on such a machine
|
||||
should NOT attempt local agy. (The wrapper falls back to the legacy `.gemini`
|
||||
block if `.agy` is absent, for machines mid-migration.)
|
||||
- **Fleet host: `GURU-5070`** (agy installed + Antigravity-authed). When others get
|
||||
it, install the Antigravity CLI, run `agy` once to sign in, then set their
|
||||
`identity.json` `agy` block (and update this line).
|
||||
|
||||
**Remote routing (NOT yet wired):** a non-host machine cannot run Gemini locally.
|
||||
To fulfill an AGY request from elsewhere, route it to the host (`GURU-5070`) —
|
||||
same pending channels as Grok (GuruRMM agent exec, a relay, or a coord-API job
|
||||
queue). Until that's built, AGY requests originate on the host machine.
|
||||
**Remote routing (NOT yet wired):** a non-host machine cannot run agy locally. Route
|
||||
the request to the host (`GURU-5070`) — same pending channels as Grok (GuruRMM agent
|
||||
exec, a relay, or a coord-API job queue). Until built, AGY requests originate on the host.
|
||||
|
||||
## When to route to Gemini (AGY)
|
||||
## When to route to AGY
|
||||
|
||||
- **Independent verification** — a genuinely different vendor/model to red-team a
|
||||
Claude finding or design before acting on it. (`verify`)
|
||||
- **Second-model code review** — have Gemini read and critique a file, a set of
|
||||
files, or a diff independently of Claude. (`review`, `review-files`, `review-diff`)
|
||||
- **Diverse drafts / second opinion** — alternative phrasing or approach to
|
||||
compare. (`text`)
|
||||
- **Google-ecosystem reach** — when a Google-side model/behavior is specifically
|
||||
wanted as the comparison point.
|
||||
- **Independent verification** — a different vendor/model to red-team a Claude
|
||||
finding or design before acting on it. (`verify`)
|
||||
- **Second-model code review** — agy reads and critiques a file, a set, or a diff
|
||||
independently of Claude. (`review`, `review-files`, `review-diff`)
|
||||
- **Diverse drafts / second opinion** — alternative phrasing or approach. (`text`)
|
||||
- **Live web facts** — current info past Claude's cutoff, with source URLs. (`search`)
|
||||
|
||||
AGY and [GROK](../grok/SKILL.md) are sibling second-opinion routers. Pick one, or
|
||||
run both and compare — disagreement between them is a strong signal to slow down.
|
||||
AGY and [GROK](../grok/SKILL.md) are sibling second-opinion routers. Pick one, or run
|
||||
both and compare — disagreement between them is a strong signal to slow down.
|
||||
|
||||
## When NOT to
|
||||
|
||||
- Pure classify / extract / summarize → cheaper via Tier-0 Ollama (`.claude/OLLAMA.md`).
|
||||
- Editing this repo's code → Claude's own agents own the codebase work. Gemini's
|
||||
`review*` modes are read-only (`--approval-mode plan`) by design; do not give
|
||||
Gemini write access to this repo.
|
||||
- Image / video **generation** → that's GROK's lane (`grok image` / `grok video`),
|
||||
not Gemini here (nano-banana needs an API key — deferred). Gemini CAN analyze an
|
||||
image you give it (`image-analyze`, vision input on OAuth).
|
||||
- **Never** delegate unsupervised destructive / production actions to Gemini.
|
||||
Always review Gemini output before acting on it — like Grok, it can over-claim.
|
||||
- Editing this repo's code → Claude's own agents own the codebase work. The wrapper
|
||||
runs agy read-only in intent (review prompts say "do not modify anything"); do not
|
||||
point it at write tasks in this repo.
|
||||
- Image / video **generation** → GROK's lane. AGY vision is read/analyze only.
|
||||
- **Never** delegate unsupervised destructive / production actions to agy. Always
|
||||
review its output before acting — like Grok, it can over-claim.
|
||||
|
||||
## Safety / operational notes
|
||||
|
||||
- `--skip-trust` is REQUIRED for headless runs (the CWD isn't a Gemini "trusted
|
||||
folder"). Equivalent env: `GEMINI_CLI_TRUST_WORKSPACE=true`. The wrapper passes it.
|
||||
- `review*` runs under `--approval-mode plan` (read-only): Gemini can read files
|
||||
but cannot modify anything. Do not change this to `auto_edit`/`yolo`.
|
||||
- Gemini's `read_file` honors `.gitignore` **and** a workspace sandbox (only files
|
||||
inside the workspace are readable). The wrapper sidesteps both by copying each
|
||||
review target into a temp dir added via `--include-directories` — so review
|
||||
works for tracked, gitignored, and spaced-path files alike.
|
||||
- Prompts are passed via `-p "$(cat <prompt-file>)"` built from a temp file, not
|
||||
inline shell args (avoids quote hell with long/structured content).
|
||||
- stdin is always closed (`</dev/null`) so `-p` never hangs waiting on stdin.
|
||||
- stdout carries two cosmetic warning lines ("True color (24-bit) support not
|
||||
detected", "Ripgrep is not available...") before output; JSON extraction from
|
||||
the first `{` ignores them. A transient `429 No capacity` backoff may appear on
|
||||
**stderr** and self-recovers — it does not affect the parsed answer.
|
||||
- `--dangerously-skip-permissions` is passed so print mode never HANGS on a tool
|
||||
approval prompt (headless). It only lets agy run its own read/search tools inside
|
||||
its sandbox to answer — the wrapper never asks it to write to this repo.
|
||||
- The prompt is passed as the VALUE of `-p` and MUST be last on the command line;
|
||||
all other flags (`--model`, `--add-dir`, `--print-timeout`) come before it.
|
||||
- Prompts are built in a temp file and read with `-p "$(cat <file>)"` (avoids
|
||||
quote hell); stdin is closed (`</dev/null`) so `-p` never waits on stdin.
|
||||
- Output is plain text/markdown. Tool-using turns (review/search/vision) may emit a
|
||||
line or two of tool narration before the answer; the prompts ask agy to suppress
|
||||
it, and residual narration is harmless.
|
||||
|
||||
## Reference
|
||||
- Binary: npm global `gemini` (`C:/Users/guru/AppData/Roaming/npm/gemini` on the
|
||||
host; the npm global dir is on PATH). The wrapper auto-locates it or honors `GEMINI=`.
|
||||
- Version 0.45.2. Auth: Google OAuth (`~/.gemini/oauth_creds.json`), no API key.
|
||||
- Headless contract: `gemini -p "<prompt>" -o json --skip-trust </dev/null` →
|
||||
`{session_id, response, stats}`; answer is `.response`.
|
||||
- Binary: native Go `agy` (`C:/Users/guru/AppData/Local/agy/bin/agy`; on PATH). The
|
||||
wrapper auto-locates it, honors `AGY=`, or reads `identity.json` `agy.binary`.
|
||||
- Version 1.0.16. Auth: Antigravity login (`~/.gemini/antigravity-cli/`), no API key,
|
||||
no `GOOGLE_CLOUD_PROJECT`.
|
||||
- Headless contract: `agy [--model "<name>"] [--add-dir <dir>] --dangerously-skip-permissions -p "<prompt>"` → plain-text answer on stdout (no JSON).
|
||||
- Migration: replaced the `gemini` npm CLI 2026-07-02 (that CLI hit
|
||||
`throwIneligibleOrProjectIdError`). `ask-gemini.sh` is a deprecated shim → `ask-agy.sh`.
|
||||
- Sibling router: [`grok`](../grok/SKILL.md) (image/video/live-data + second opinion).
|
||||
|
||||
280
.claude/skills/agy/scripts/ask-agy.sh
Normal file
280
.claude/skills/agy/scripts/ask-agy.sh
Normal file
@@ -0,0 +1,280 @@
|
||||
#!/usr/bin/env bash
|
||||
# ask-agy.sh — Claude -> Google Antigravity CLI (`agy`) router (independent second model).
|
||||
#
|
||||
# Sibling of ask-grok.sh. Routes a task to the Antigravity CLI (`agy`, a native Go
|
||||
# binary at ~/AppData/Local/agy/bin/agy) for an independent, different-vendor second
|
||||
# opinion, verification, code review, vision, or live web search. Multi-model backend
|
||||
# (Gemini 3.5 Flash / Gemini 3.1 Pro / Claude / GPT-OSS) selectable via --model.
|
||||
#
|
||||
# REPLACES the old `gemini` npm CLI (ask-gemini.sh), which broke on this account with
|
||||
# a Cloud-project eligibility error (throwIneligibleOrProjectIdError). `agy` uses
|
||||
# Antigravity's own auth (~/.gemini/antigravity-cli/), no GOOGLE_CLOUD_PROJECT needed.
|
||||
#
|
||||
# Authoritative flag reference (from `agy --help`, Go flag package — 2026-07-02, v1.0.16):
|
||||
# -p | --print | --prompt Run ONE prompt non-interactively and print the response.
|
||||
# STRING flag: the prompt is its VALUE. MUST be LAST on the
|
||||
# line (a bare `-p --model` makes "--model" the prompt).
|
||||
# --model "<name>" Model for the session. Friendly names from `agy models`,
|
||||
# e.g. "Gemini 3.1 Pro (High)", "Gemini 3.5 Flash (Medium)",
|
||||
# "Claude Opus 4.6 (Thinking)". Omit -> CLI default (Flash).
|
||||
# --add-dir <dir> Add a directory to the workspace (repeatable). Needed so
|
||||
# the agent's file tools can read a review/vision target.
|
||||
# --dangerously-skip-permissions Auto-approve tool calls (else print mode can HANG
|
||||
# on an approval prompt until --print-timeout). Headless-safe.
|
||||
# --print-timeout <dur> Print-mode wait timeout (default 5m0s). We also wrap in the
|
||||
# shell `timeout` as a hard belt-and-suspenders bound.
|
||||
# (There is NO JSON/structured-output flag — stdout is plain text/markdown.)
|
||||
#
|
||||
# Output contract: plain text on stdout. Tool-using turns (review/search/vision) may
|
||||
# emit a line or two of tool narration ("I will view the content of ...") before the
|
||||
# answer; we ask the model to suppress it but tolerate residual. No JSON parsing.
|
||||
#
|
||||
# Usage (unchanged from the old skill so callers/SKILL.md stay valid):
|
||||
# ask-agy.sh text "<prompt>" # one-shot answer
|
||||
# ask-agy.sh text --prompt-file <path> # long content
|
||||
# ask-agy.sh verify "<claim or finding to refute>" # adversarial check
|
||||
# ask-agy.sh verify --prompt-file <path>
|
||||
# ask-agy.sh review <file> [instructions] # agy reads + reviews one file
|
||||
# ask-agy.sh review-files [-i "instr"] <f1> [f2 ...] # review a SET together
|
||||
# ask-agy.sh review-diff [-C <repo-dir>] [-i "instr"] <gitref> [-- <pathspec>]
|
||||
# ask-agy.sh image-analyze <image-path> ["question"] # vision: read image + describe
|
||||
# ask-agy.sh search "<query>" # live web search + sources
|
||||
# ask-agy.sh raw <agy args...> # escape hatch
|
||||
#
|
||||
# Exit: 0 ok, 1 no result, 2 usage, 3 not installed here, 127 agy not found.
|
||||
set -uo pipefail
|
||||
SELF="ask-agy"
|
||||
|
||||
# --- path conversion: native-Windows path for agy args (no-op off Windows) ---
|
||||
# agy is a native Windows binary; Git Bash hands it POSIX paths it cannot resolve.
|
||||
if command -v cygpath >/dev/null 2>&1; then
|
||||
winpath() { cygpath -w -- "$1" 2>/dev/null || printf '%s' "$1"; }
|
||||
else
|
||||
winpath() { printf '%s' "$1"; }
|
||||
fi
|
||||
|
||||
# --- identity.json (per-machine, gitignored): is agy installed here? ---
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 2>/dev/null && pwd)"
|
||||
PYBIN="$(command -v py 2>/dev/null || command -v python 2>/dev/null || command -v python3 2>/dev/null || true)"
|
||||
IDFILE=""
|
||||
[ -n "${CLAUDETOOLS_ROOT:-}" ] && [ -f "$CLAUDETOOLS_ROOT/.claude/identity.json" ] && IDFILE="$CLAUDETOOLS_ROOT/.claude/identity.json"
|
||||
[ -z "$IDFILE" ] && IDFILE="$(cd "$SCRIPT_DIR/../../.." 2>/dev/null && pwd)/identity.json"
|
||||
idagy() { # read field $1 from identity.json .agy (fallback .gemini), empty if absent
|
||||
[ -f "$IDFILE" ] || { echo ""; return; }
|
||||
[ -n "$PYBIN" ] || { echo ""; return; }
|
||||
"$PYBIN" -c "import json,sys
|
||||
try:
|
||||
d=json.load(sys.stdin); g=(d.get('agy') or d.get('gemini') or {}); v=g.get('$1','')
|
||||
print('' if v is None else (str(v).lower() if isinstance(v,bool) else v))
|
||||
except Exception: print('')" < "$IDFILE"
|
||||
}
|
||||
|
||||
if [ "$(idagy installed)" = "false" ]; then
|
||||
echo "[$SELF] agy is not installed on this machine (identity.json agy.installed=false)." >&2
|
||||
echo "[$SELF] Antigravity CLI runs only on the fleet host. Route this request there, or install agy + set identity.json agy.installed=true." >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
# --- locate the agy binary: AGY env > identity.json agy.binary > PATH > known paths ---
|
||||
AGY="${AGY:-}"
|
||||
if [ -n "$AGY" ] && [ ! -x "$AGY" ] && ! command -v "$AGY" >/dev/null 2>&1; then
|
||||
echo "[$SELF] AGY='$AGY' is not an executable agy binary." >&2; exit 127
|
||||
fi
|
||||
cand="$(idagy binary)"
|
||||
[ -z "$AGY" ] && [ -n "$cand" ] && [ -x "$cand" ] && AGY="$cand"
|
||||
if [ -z "$AGY" ]; then
|
||||
if command -v agy >/dev/null 2>&1; then AGY="$(command -v agy)"; else
|
||||
for c in "/c/Users/${USERNAME:-${USER:-x}}/AppData/Local/agy/bin/agy" \
|
||||
"$HOME/AppData/Local/agy/bin/agy" "$LOCALAPPDATA/agy/bin/agy" \
|
||||
"/usr/local/bin/agy" "$HOME/.local/bin/agy"; do
|
||||
[ -n "$c" ] && [ -x "$c" ] && { AGY="$c"; break; }
|
||||
done
|
||||
fi
|
||||
fi
|
||||
[ -z "$AGY" ] && { echo "[$SELF] agy CLI not found (set identity.json agy.binary, AGY=, or install the Antigravity CLI)" >&2; exit 127; }
|
||||
|
||||
# Strong model for verify/review*/vision/search; text uses the CLI default (Flash).
|
||||
STRONG_MODEL="${AGY_MODEL:-Gemini 3.1 Pro (High)}"
|
||||
|
||||
MODE="${1:-}"; shift 2>/dev/null || true
|
||||
[ -z "$MODE" ] && { echo "usage: $SELF {text|verify|review|review-files|review-diff|image-analyze|search|raw} ..." >&2; exit 2; }
|
||||
|
||||
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
||||
PF="$TMP/prompt.txt"; OUT="$TMP/out.txt"; ERR="$TMP/err.txt"
|
||||
REPO_ROOT="${CLAUDETOOLS_ROOT:-$(cd "$SCRIPT_DIR/../../../.." 2>/dev/null && pwd)}"
|
||||
_logerr() { bash "$REPO_ROOT/.claude/scripts/log-skill-error.sh" "agy" "$@" >/dev/null 2>&1 || true; }
|
||||
|
||||
TIMEOUT_CMD="timeout"
|
||||
if [[ "${OSTYPE:-}" == "darwin"* ]]; then
|
||||
TIMEOUT_CMD="$(command -v gtimeout 2>/dev/null || echo timeout)"
|
||||
fi
|
||||
|
||||
# run agy headless. $1 = hard timeout secs; remaining args = flags placed BEFORE -p.
|
||||
# The prompt (from $PF) is always the LAST token, as the value of -p. agy has no JSON
|
||||
# mode, so stdout is the answer (plain text); stderr kept separate for diagnostics.
|
||||
LAST_FLAGS=()
|
||||
run_agy() {
|
||||
local to="$1"; shift
|
||||
LAST_FLAGS=("$@")
|
||||
"$TIMEOUT_CMD" "$to" "$AGY" "$@" --print-timeout "${to}s" -p "$(cat "$PF")" \
|
||||
>"$OUT" 2>"$ERR" </dev/null || true
|
||||
}
|
||||
|
||||
# agy occasionally returns an empty turn; retry a couple times with backoff, then fail.
|
||||
emit_or_fail() {
|
||||
local txt tries=0 max="${AGY_MAX_TRIES:-3}"
|
||||
txt="$(sed -e 's/[[:space:]]*$//' "$OUT" | sed -e :a -e '/^\s*$/{$d;N;ba}')"
|
||||
while [ -z "${txt//[[:space:]]/}" ] && [ "$tries" -lt $((max-1)) ] && [ ${#LAST_FLAGS[@]} -ge 0 ]; do
|
||||
tries=$((tries+1))
|
||||
echo "[$SELF] empty response - retry $tries/$((max-1)) (backoff ${tries}x3s)..." >&2
|
||||
sleep $((tries*3))
|
||||
run_agy 240 "${LAST_FLAGS[@]}"
|
||||
txt="$(cat "$OUT")"
|
||||
done
|
||||
if [ -n "${txt//[[:space:]]/}" ]; then cat "$OUT"; return 0; fi
|
||||
if grep -qiE 'not authenticated|please (log|sign).?in|auth(entication)? (failed|error|required)|token (has )?expired|unauthorized' "$ERR" 2>/dev/null; then
|
||||
echo "[$SELF] agy auth error - run 'agy' interactively once to sign in, then retry." >&2
|
||||
_logerr "agy auth/login failure" --context "mode=$MODE"
|
||||
else
|
||||
echo "[$SELF] no response from agy after $max attempts. stderr tail:" >&2
|
||||
tail -3 "$ERR" >&2 2>/dev/null || true
|
||||
_logerr "agy returned no response (empty after $max attempts)" --context "mode=$MODE err=$(tail -1 "$ERR" 2>/dev/null | tr -d '\n' | cut -c1-80)"
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Copy review/vision targets into an included workspace dir so agy's file tools can
|
||||
# reach them regardless of location/spaces; --add-dir this dir.
|
||||
INCLUDE_DIR="$TMP/inbox"
|
||||
prep_includes() { mkdir -p "$INCLUDE_DIR"; }
|
||||
|
||||
NO_TOOLS='Do not use any tools; answer directly in text.'
|
||||
NO_NARRATE='Do not narrate your tool use or steps; output only the final answer.'
|
||||
|
||||
case "$MODE" in
|
||||
text|verify)
|
||||
SRC=""
|
||||
if [ "${1:-}" = "--prompt-file" ]; then
|
||||
[ -f "${2:-}" ] || { echo "[$SELF] prompt file not found: ${2:-}" >&2; exit 2; }
|
||||
SRC="$(cat "$2")"
|
||||
else
|
||||
SRC="${1:-}"
|
||||
fi
|
||||
[ -z "$SRC" ] && { echo "usage: $SELF $MODE \"<prompt>\" | $SELF $MODE --prompt-file <path>" >&2; exit 2; }
|
||||
if [ "$MODE" = "verify" ]; then
|
||||
printf 'You are an adversarial reviewer giving an independent second opinion. Evaluate the following claim/finding/document: try hard to find any way it is WRONG, incomplete, unsupported, or overstated. Then give a clear VERDICT (correct / partly correct / incorrect) plus specific justification. %s\n\nContent:\n%s' "$NO_TOOLS" "$SRC" > "$PF"
|
||||
run_agy 240 --dangerously-skip-permissions --model "$STRONG_MODEL"
|
||||
else
|
||||
printf '%s\n\n%s' "$NO_TOOLS" "$SRC" > "$PF"
|
||||
run_agy 240 --dangerously-skip-permissions
|
||||
fi
|
||||
emit_or_fail
|
||||
;;
|
||||
|
||||
review|file)
|
||||
[ -z "${1:-}" ] && { echo "usage: $SELF review <file-path> [instructions]" >&2; exit 2; }
|
||||
target="$1"
|
||||
instr="${2:-Give an independent, critical review of this file: accuracy, gaps/omissions, bugs, and concrete improvements. Be specific.}"
|
||||
if [ -f "$target" ]; then resolved="$target"
|
||||
elif [ -f "$REPO_ROOT/$target" ]; then resolved="$REPO_ROOT/$target"
|
||||
else echo "[$SELF] file not found: $target" >&2; exit 2; fi
|
||||
prep_includes
|
||||
base="$(basename "$resolved")"; cp -f "$resolved" "$INCLUDE_DIR/$base"
|
||||
tgt_win="$(winpath "$INCLUDE_DIR/$base")"; inc_win="$(winpath "$INCLUDE_DIR")"
|
||||
printf 'Read the file at this absolute path, then perform the task and stop. Do not modify anything. %s\nPath: %s\n\nTask: %s' "$NO_NARRATE" "$tgt_win" "$instr" > "$PF"
|
||||
run_agy 300 --dangerously-skip-permissions --model "$STRONG_MODEL" --add-dir "$inc_win"
|
||||
emit_or_fail
|
||||
;;
|
||||
|
||||
review-files)
|
||||
instr='Independently review these files together as a unit: correctness/bugs, gaps, cross-file consistency, and concrete improvements. Be specific and cite file:line.'
|
||||
files=()
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-i|--instr) instr="${2:-}"; shift 2 2>/dev/null || shift ;;
|
||||
*) files+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
[ ${#files[@]} -eq 0 ] && { echo "usage: $SELF review-files [-i \"instructions\"] <file> [file ...]" >&2; exit 2; }
|
||||
prep_includes
|
||||
list=""; declare -A seen=()
|
||||
for f in "${files[@]}"; do
|
||||
if [ -f "$f" ]; then r="$f"
|
||||
elif [ -f "$REPO_ROOT/$f" ]; then r="$REPO_ROOT/$f"
|
||||
else echo "[$SELF] file not found: $f" >&2; exit 2; fi
|
||||
base="$(basename "$r")"
|
||||
if [ -n "${seen[$base]:-}" ]; then
|
||||
n=1; while [ -e "$INCLUDE_DIR/${n}_${base}" ]; do n=$((n+1)); done; base="${n}_${base}"
|
||||
fi
|
||||
seen[$base]=1; cp -f "$r" "$INCLUDE_DIR/$base"
|
||||
list+="- $(winpath "$INCLUDE_DIR/$base")
|
||||
"
|
||||
done
|
||||
inc_win="$(winpath "$INCLUDE_DIR")"
|
||||
printf 'Read EACH of these files (absolute paths), then perform the task across ALL of them and stop. Do not modify anything. %s\n\nFiles:\n%s\nTask: %s' "$NO_NARRATE" "$list" "$instr" > "$PF"
|
||||
run_agy 360 --dangerously-skip-permissions --model "$STRONG_MODEL" --add-dir "$inc_win"
|
||||
emit_or_fail
|
||||
;;
|
||||
|
||||
review-diff)
|
||||
gdir="$REPO_ROOT"
|
||||
instr='Review this git diff: correctness/bugs introduced, regressions, missing edge cases, and concrete fixes. Focus on the CHANGES. Be specific and cite file:line.'
|
||||
ref=""; pathspec=()
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-C|--dir) gdir="${2:-}"; shift 2 2>/dev/null || shift ;;
|
||||
-i|--instr) instr="${2:-}"; shift 2 2>/dev/null || shift ;;
|
||||
--) shift; while [ $# -gt 0 ]; do pathspec+=("$1"); shift; done ;;
|
||||
*) if [ -z "$ref" ]; then ref="$1"; else pathspec+=("$1"); fi; shift ;;
|
||||
esac
|
||||
done
|
||||
[ -z "$ref" ] && { echo "usage: $SELF review-diff [-C <repo-dir>] [-i \"instr\"] <gitref> [-- <pathspec>]" >&2; exit 2; }
|
||||
[ -d "$gdir" ] || { [ -d "$REPO_ROOT/$gdir" ] && gdir="$REPO_ROOT/$gdir"; }
|
||||
git -C "$gdir" rev-parse --git-dir >/dev/null 2>&1 || { echo "[$SELF] not a git repo: $gdir" >&2; exit 2; }
|
||||
if [ ${#pathspec[@]} -gt 0 ]; then
|
||||
git -C "$gdir" diff "$ref" -- "${pathspec[@]}" > "$TMP/diff.txt" 2>"$TMP/differr.txt"
|
||||
else
|
||||
git -C "$gdir" diff "$ref" > "$TMP/diff.txt" 2>"$TMP/differr.txt"
|
||||
fi
|
||||
[ -s "$TMP/diff.txt" ] || { echo "[$SELF] empty/failed diff for '$ref' in $gdir: $(head -1 "$TMP/differr.txt" 2>/dev/null)" >&2; exit 1; }
|
||||
gdir_win="$(winpath "$gdir")"
|
||||
{ printf 'Review the following unified git diff. %s\n%s\nYou may read any changed file for full context (paths are relative to %s; strip a/ b/ prefixes). Do not modify anything.\n\n=== BEGIN DIFF ===\n' "$instr" "$NO_NARRATE" "$gdir_win"; cat "$TMP/diff.txt"; printf '\n=== END DIFF ===\n'; } > "$PF"
|
||||
run_agy 360 --dangerously-skip-permissions --model "$STRONG_MODEL" --add-dir "$gdir_win"
|
||||
emit_or_fail
|
||||
;;
|
||||
|
||||
image-analyze|image|vision)
|
||||
[ -z "${1:-}" ] && { echo "usage: $SELF image-analyze <image-path> [\"question\"]" >&2; exit 2; }
|
||||
target="$1"; question="${2:-Describe exactly what is in this image.}"
|
||||
if [ -f "$target" ]; then resolved="$target"
|
||||
elif [ -f "$REPO_ROOT/$target" ]; then resolved="$REPO_ROOT/$target"
|
||||
else echo "[$SELF] image not found: $target" >&2; exit 2; fi
|
||||
prep_includes
|
||||
base="$(basename "$resolved")"; cp -f "$resolved" "$INCLUDE_DIR/$base"
|
||||
img_win="$(winpath "$INCLUDE_DIR/$base")"; inc_win="$(winpath "$INCLUDE_DIR")"
|
||||
printf 'Read the image at this absolute path and describe exactly what you see. Report only what is actually present; do not guess or invent content. Then stop. %s\nImage path: %s\n\nQuestion: %s' "$NO_NARRATE" "$img_win" "$question" > "$PF"
|
||||
run_agy 300 --dangerously-skip-permissions --model "$STRONG_MODEL" --add-dir "$inc_win"
|
||||
emit_or_fail
|
||||
;;
|
||||
|
||||
search|websearch)
|
||||
SRC=""
|
||||
if [ "${1:-}" = "--prompt-file" ]; then
|
||||
[ -f "${2:-}" ] || { echo "[$SELF] prompt file not found: ${2:-}" >&2; exit 2; }
|
||||
SRC="$(cat "$2")"
|
||||
else
|
||||
SRC="${1:-}"
|
||||
fi
|
||||
[ -z "$SRC" ] && { echo "usage: $SELF search \"<query>\" | $SELF search --prompt-file <path>" >&2; exit 2; }
|
||||
printf 'Search the web for current, live information answering the following, then stop. Answer concisely and ALWAYS include the source URLs you used (a Sources list of full URLs). Do not fabricate URLs. %s\n\nQuery: %s' "$NO_NARRATE" "$SRC" > "$PF"
|
||||
run_agy 240 --dangerously-skip-permissions --model "$STRONG_MODEL"
|
||||
emit_or_fail
|
||||
;;
|
||||
|
||||
raw)
|
||||
"$AGY" "$@"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "[$SELF] unknown mode '$MODE' (use text|verify|review|review-files|review-diff|image-analyze|search|raw)" >&2; exit 2 ;;
|
||||
esac
|
||||
@@ -1,388 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# ask-gemini.sh — Claude -> Google Gemini CLI router (independent second model).
|
||||
#
|
||||
# Sibling of ask-grok.sh. Routes a task to the official Google Gemini CLI
|
||||
# (`gemini`, npm global) for an independent, different-vendor second opinion,
|
||||
# verification, or a Gemini code review. Headless, safe-by-default, JSON-parsed.
|
||||
#
|
||||
# Auth is Google login (OAuth) — NO API key. Creds: ~/.gemini/oauth_creds.json.
|
||||
# If a call fails with an auth error, run `gemini` interactively once and pick
|
||||
# "Login with Google".
|
||||
#
|
||||
# Output contract (VERIFIED on GURU-5070, gemini 0.45.2):
|
||||
# - Prefer JSON: `gemini -p ... -o json` -> {session_id, response, stats}.
|
||||
# The answer text is `.response`. stdout may carry two cosmetic warning lines
|
||||
# ("True color..." / "Ripgrep is not available...") before the JSON; we extract
|
||||
# the object starting at the FIRST '{' to ignore them. stderr (429 backoff,
|
||||
# warnings) is captured SEPARATELY and never fed to the JSON parser.
|
||||
# - `--skip-trust` is REQUIRED headless (the CWD isn't a trusted folder).
|
||||
# - stdin is always closed (</dev/null) so `-p` never hangs waiting on stdin.
|
||||
#
|
||||
# File reads (review*): Gemini's read_file honors .gitignore AND a workspace
|
||||
# sandbox (only files under the workspace/included dirs are readable). To make
|
||||
# review robust for ANY file (tracked, gitignored, with spaces), we copy each
|
||||
# target into a temp dir and add it to the workspace via --include-directories.
|
||||
# review-diff runs with the repo dir included so changed files read in place.
|
||||
#
|
||||
# Usage:
|
||||
# ask-gemini.sh text "<prompt>" # one-shot answer
|
||||
# ask-gemini.sh text --prompt-file <path> # long content
|
||||
# ask-gemini.sh verify "<claim or finding to refute>" # adversarial check
|
||||
# ask-gemini.sh verify --prompt-file <path>
|
||||
# ask-gemini.sh review <file> [instructions] # gemini reads + reviews one file
|
||||
# ask-gemini.sh review-files [-i "instr"] <f1> [f2 ...] # review a SET of files together
|
||||
# ask-gemini.sh review-diff [-C <repo-dir>] [-i "instr"] <gitref> [-- <pathspec>]
|
||||
# ask-gemini.sh image-analyze <image-path> ["question"] # vision: read_file image + describe (PRO model)
|
||||
# ask-gemini.sh search "<query>" # Google-grounded live web search + sources
|
||||
# ask-gemini.sh raw <gemini args...> # escape hatch
|
||||
#
|
||||
# Exit: 0 ok, 1 no result, 2 usage, 3 not installed here, 127 gemini/python not found.
|
||||
set -uo pipefail
|
||||
SELF="ask-gemini"
|
||||
|
||||
PY="$(command -v py 2>/dev/null || command -v python 2>/dev/null || command -v python3 2>/dev/null || true)"
|
||||
[ -z "$PY" ] && { echo "[$SELF] python (py/python/python3) required for JSON parsing" >&2; exit 127; }
|
||||
|
||||
# --- path conversion: native-Windows path for the gemini args (no-op off Windows) ---
|
||||
# gemini is a native Windows binary (npm shim -> node.exe); Git Bash hands it POSIX
|
||||
# paths (/tmp, /c/.., /d/..) it cannot resolve. cygpath -w converts to C:\... on
|
||||
# MSYS/Cygwin; on Linux/macOS it passes through unchanged. Explicit conversion
|
||||
# removes reliance on MSYS auto-conversion (which breaks on spaces/edge cases).
|
||||
if command -v cygpath >/dev/null 2>&1; then
|
||||
winpath() { cygpath -w -- "$1" 2>/dev/null || printf '%s' "$1"; }
|
||||
else
|
||||
winpath() { printf '%s' "$1"; }
|
||||
fi
|
||||
|
||||
# --- identity.json (per-machine, gitignored) declares whether gemini is installed here ---
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 2>/dev/null && pwd)"
|
||||
IDFILE=""
|
||||
[ -n "${CLAUDETOOLS_ROOT:-}" ] && [ -f "$CLAUDETOOLS_ROOT/.claude/identity.json" ] && IDFILE="$CLAUDETOOLS_ROOT/.claude/identity.json"
|
||||
[ -z "$IDFILE" ] && IDFILE="$(cd "$SCRIPT_DIR/../../.." 2>/dev/null && pwd)/identity.json"
|
||||
idgem() { # read field $1 from identity.json .gemini (empty if absent)
|
||||
[ -f "$IDFILE" ] || { echo ""; return; }
|
||||
"$PY" -c "import json,sys
|
||||
try:
|
||||
g=(json.load(sys.stdin).get('gemini') or {}); v=g.get('$1','')
|
||||
print('' if v is None else (str(v).lower() if isinstance(v,bool) else v))
|
||||
except Exception: print('')" < "$IDFILE"
|
||||
}
|
||||
|
||||
# If identity explicitly says gemini is NOT installed here, fail fast with guidance.
|
||||
if [ "$(idgem installed)" = "false" ]; then
|
||||
echo "[$SELF] gemini is not installed on this machine (identity.json gemini.installed=false)." >&2
|
||||
echo "[$SELF] Gemini runs only on the fleet host. Route this request there, or install the gemini CLI (npm i -g @google/gemini-cli) + set identity.json gemini.installed=true." >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
# --- locate the gemini binary: GEMINI env > identity.json gemini.binary > auto-locate ---
|
||||
# An explicit GEMINI= override that isn't runnable is a user error -> fail clearly up front
|
||||
# (covers absolute paths AND a bare name resolvable on PATH, e.g. GEMINI=gemini).
|
||||
GEMINI="${GEMINI:-}"
|
||||
if [ -n "$GEMINI" ] && [ ! -x "$GEMINI" ] && ! command -v "$GEMINI" >/dev/null 2>&1; then
|
||||
echo "[$SELF] GEMINI='$GEMINI' is not an executable gemini binary." >&2; exit 127
|
||||
fi
|
||||
cand="$(idgem binary)"
|
||||
[ -z "$GEMINI" ] && [ -n "$cand" ] && [ -x "$cand" ] && GEMINI="$cand"
|
||||
if [ -z "$GEMINI" ]; then
|
||||
if command -v gemini >/dev/null 2>&1; then GEMINI="$(command -v gemini)"; else
|
||||
for c in "${APPDATA:-}/npm/gemini" "/c/Users/${USERNAME:-${USER:-x}}/AppData/Roaming/npm/gemini" \
|
||||
"$HOME/AppData/Roaming/npm/gemini" "/usr/local/bin/gemini" "$HOME/.npm-global/bin/gemini"; do
|
||||
[ -n "$c" ] && [ -x "$c" ] && { GEMINI="$c"; break; }
|
||||
done
|
||||
fi
|
||||
fi
|
||||
[ -z "$GEMINI" ] && { echo "[$SELF] gemini CLI not found (set identity.json gemini.binary, GEMINI=, or install: npm i -g @google/gemini-cli)" >&2; exit 127; }
|
||||
|
||||
# Model: default routing for text; a strong pinned model for verify/review.
|
||||
# gemini-3.1-pro-preview verified available on this account (2026-06-05); overridable.
|
||||
STRONG_MODEL="${GEMINI_MODEL:-gemini-3.1-pro-preview}"
|
||||
|
||||
MODE="${1:-}"; shift 2>/dev/null || true
|
||||
[ -z "$MODE" ] && { echo "usage: $SELF {text|verify|review|review-files|review-diff|image-analyze|search|raw} ..." >&2; exit 2; }
|
||||
|
||||
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
||||
PF="$TMP/prompt.txt"; OUT="$TMP/out.txt"; ERR="$TMP/err.txt"
|
||||
REPO_ROOT="${CLAUDETOOLS_ROOT:-$(cd "$SCRIPT_DIR/../../../.." 2>/dev/null && pwd)}"
|
||||
# Functional-error logger (skill name "agy"); soft-fails, never breaks the caller.
|
||||
_logerr() { bash "$REPO_ROOT/.claude/scripts/log-skill-error.sh" "agy" "$@" >/dev/null 2>&1 || true; }
|
||||
|
||||
# gtimeout on macOS (brew coreutils), timeout elsewhere.
|
||||
TIMEOUT_CMD="timeout"
|
||||
if [[ "${OSTYPE:-}" == "darwin"* ]]; then
|
||||
TIMEOUT_CMD="$(command -v gtimeout 2>/dev/null || echo timeout)"
|
||||
fi
|
||||
|
||||
# run gemini headless reading the prompt file. $1=timeout secs; rest=extra flags.
|
||||
# stdout -> $OUT, stderr -> $ERR (kept separate so warning/429 noise never reaches
|
||||
# the JSON parser). Never fail the script on gemini's exit code; we judge by output.
|
||||
# Records the invocation so emit_or_fail can replay it once on a transient empty turn.
|
||||
LAST_RUN=()
|
||||
run_gemini() {
|
||||
local to="$1"; shift
|
||||
LAST_RUN=("$to" "$@")
|
||||
"$TIMEOUT_CMD" "$to" "$GEMINI" -p "$(cat "$PF")" -o json --skip-trust "$@" \
|
||||
>"$OUT" 2>"$ERR" </dev/null || true
|
||||
}
|
||||
|
||||
# extract .response from the JSON object starting at the first '{' in $OUT.
|
||||
# Parsed via stdin so Windows python never resolves a git-bash (/c/...) path.
|
||||
#
|
||||
# Some pinned-pro tool-using turns (notably image-analyze) leak the model's
|
||||
# internal reasoning stream into .response: a stray token + a 'thought' marker
|
||||
# followed by 'CRITICAL INSTRUCTION N:' lines, then the real answer. We strip
|
||||
# that preamble ONLY when the signature is clearly present, so clean responses
|
||||
# (text/verify/review/search) pass through byte-for-byte unchanged.
|
||||
gresponse() { "$PY" -c "import json,sys,re,os
|
||||
raw=sys.stdin.read()
|
||||
i=raw.find('{')
|
||||
if i < 0:
|
||||
print(''); sys.exit(0)
|
||||
try:
|
||||
r=json.loads(raw[i:]).get('response','') or ''
|
||||
except Exception:
|
||||
print(''); sys.exit(0)
|
||||
head=r[:40].lower()
|
||||
leak=('thought' in head) or ('critical instruction' in r.lower()[:600])
|
||||
if leak:
|
||||
lines=r.split('\n')
|
||||
keep=[]; dropping=True
|
||||
for ln in lines:
|
||||
s=ln.strip()
|
||||
low=s.lower()
|
||||
if dropping and (
|
||||
low.endswith('thought') or low.startswith('critical instruction')
|
||||
or low.startswith('thought:') or low=='' ):
|
||||
continue
|
||||
dropping=False
|
||||
keep.append(ln)
|
||||
cleaned='\n'.join(keep).strip()
|
||||
r=cleaned if cleaned else r.strip()
|
||||
# AGY_CLEAN: aggressive prefix scrub for tool-using turns (image-analyze), which
|
||||
# can fuse a stray stream/tool token onto the front of the answer (e.g. '.',
|
||||
# '.94>', 'uem_image_0_0_png}'). Off by default so text/verify/review/search are
|
||||
# byte-exact. We only remove a junk run that ends in a stream delimiter (} > :)
|
||||
# or a lone leading punctuation char, immediately before the first real sentence.
|
||||
if os.environ.get('AGY_CLEAN') == '1' and r:
|
||||
# The pro-preview tool loop sometimes prepends a numbered/markdown reasoning
|
||||
# block before the actual answer. If a clear answer pivot follows such a
|
||||
# preamble, keep from the pivot onward (the user-facing answer).
|
||||
if re.search(r'(?im)^\s*\d+[.)]\s', r) or 'thought' in r[:60].lower():
|
||||
pivs=list(re.finditer(r'(?i)(Based on the image\b|\*\*Answer:?\*\*|The image (?:contains|shows|displays)\b)', r))
|
||||
if pivs:
|
||||
r=r[pivs[-1].start():]
|
||||
m=re.match(r'^[^\n]{0,40}?(?:\.png\)|\.jpe?g\)|[}>:)])\s*([\"A-Z].*)$', r, re.S)
|
||||
if m and m.group(1):
|
||||
r=m.group(1)
|
||||
else:
|
||||
# a short leading junk run (ASCII punctuation/digits or non-Latin stream
|
||||
# tokens) before a capitalized/quoted sentence start. Bounded length so we
|
||||
# never eat a real lowercase sentence or real prose.
|
||||
m=re.match(r'^(?:[^A-Za-z\"]|[^\x00-\x7f]){1,8}([A-Z\"].*)$', r, re.S)
|
||||
if m and m.group(1):
|
||||
r=m.group(1)
|
||||
r=r.strip()
|
||||
print(r)" < "$OUT"; }
|
||||
|
||||
# detect a GENUINE auth failure in stderr (precise remediation hint). Tightened 2026-06-17 -
|
||||
# the old broad regex (bare login|credential|authenticat|oauth|401) matched benign mid-run
|
||||
# token-refresh lines and false-flagged working sessions as auth failures.
|
||||
auth_failed() { grep -qiE 'invalid_grant|unauthorized|not authenticated|authentication failed|re-?authenticat|please (log|sign).?in|login with google|token (has )?expired|no (valid )?credentials' "$ERR" 2>/dev/null; }
|
||||
# detect a quota / rate-capacity exhaustion (the pinned strong model can be capped mid-session)
|
||||
quota_exhausted() { grep -qiE 'exhausted your capacity|quota|resource[_ ]?exhausted|rate limit|too many requests|429' "$ERR" 2>/dev/null; }
|
||||
|
||||
emit_or_fail() { # print .response; gemini intermittently returns an empty turn, so retry a few
|
||||
# times with backoff before giving up (single retry was insufficient - 2 empties
|
||||
# in a row caused spurious failures during live research, 2026-06-17).
|
||||
# Do ALL retries first; only classify the failure (auth vs generic) AFTER exhausting them.
|
||||
# (Checking auth_failed INSIDE the loop caused false aborts: a benign mid-run credential-refresh
|
||||
# line in stderr matched the auth regex and killed the retries even though auth was fine. 2026-06-17.)
|
||||
local txt tries=0 max="${AGY_MAX_TRIES:-3}"
|
||||
txt="$(gresponse)"
|
||||
while [ -z "$txt" ] && [ "$tries" -lt $((max-1)) ] && [ ${#LAST_RUN[@]} -gt 0 ]; do
|
||||
tries=$((tries+1))
|
||||
echo "[$SELF] empty response - retry $tries/$((max-1)) (backoff ${tries}x3s)..." >&2
|
||||
sleep $((tries*3)) # 3s, 6s backoff (covers transient empties / 429s / token refresh)
|
||||
run_gemini "${LAST_RUN[@]}"
|
||||
txt="$(gresponse)"
|
||||
done
|
||||
if [ -n "$txt" ]; then printf '%s\n' "$txt"; return 0; fi
|
||||
# Quota fallback: if the pinned strong model is capacity/quota-capped, retry ONCE on the default
|
||||
# (lighter) model by stripping -m from the last invocation - the default model has a separate quota.
|
||||
if quota_exhausted && [ ${#LAST_RUN[@]} -gt 0 ]; then
|
||||
echo "[$SELF] '$STRONG_MODEL' quota exhausted - retrying once on the default (lighter) model..." >&2
|
||||
local nr=() a skip=0
|
||||
for a in "${LAST_RUN[@]}"; do
|
||||
if [ "$skip" = 1 ]; then skip=0; continue; fi
|
||||
if [ "$a" = "-m" ]; then skip=1; continue; fi
|
||||
nr+=("$a")
|
||||
done
|
||||
run_gemini "${nr[@]}"
|
||||
txt="$(gresponse)"
|
||||
if [ -n "$txt" ]; then printf '%s\n' "$txt"; return 0; fi
|
||||
fi
|
||||
if auth_failed; then
|
||||
echo "[$SELF] Gemini auth error - run 'gemini' interactively and choose 'Login with Google', then retry." >&2
|
||||
_logerr "gemini auth/login failure" --context "mode=$MODE"
|
||||
else
|
||||
echo "[$SELF] no response from gemini after $max attempts. stderr tail:" >&2
|
||||
tail -3 "$ERR" >&2 2>/dev/null || true
|
||||
_logerr "gemini returned no response (empty after $max attempts)" --context "mode=$MODE err=$(tail -1 "$ERR" 2>/dev/null | tr -d '\n' | cut -c1-80)"
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Copy target files into an included temp workspace dir so gemini's read_file can
|
||||
# reach them regardless of .gitignore / workspace sandbox. Echoes the included dir.
|
||||
INCLUDE_DIR="$TMP/inbox"
|
||||
prep_includes() { mkdir -p "$INCLUDE_DIR"; }
|
||||
|
||||
case "$MODE" in
|
||||
text|verify)
|
||||
SRC=""
|
||||
if [ "${1:-}" = "--prompt-file" ]; then
|
||||
[ -f "${2:-}" ] || { echo "[$SELF] prompt file not found: ${2:-}" >&2; exit 2; }
|
||||
SRC="$(cat "$2")"
|
||||
else
|
||||
SRC="${1:-}"
|
||||
fi
|
||||
[ -z "$SRC" ] && { echo "usage: $SELF $MODE \"<prompt>\" | $SELF $MODE --prompt-file <path>" >&2; exit 2; }
|
||||
if [ "$MODE" = "verify" ]; then
|
||||
printf 'You are an adversarial reviewer giving an independent second opinion. Evaluate the following claim/finding/document: try hard to find any way it is WRONG, incomplete, unsupported, or overstated. Then give a clear VERDICT (e.g. correct / partly correct / incorrect) plus specific justification. Answer in text only; do not use any tools.\n\nContent:\n%s' "$SRC" > "$PF"
|
||||
run_gemini 180 -m "$STRONG_MODEL"
|
||||
else
|
||||
printf 'Answer the following directly in text. Do not use any tools.\n\n%s' "$SRC" > "$PF"
|
||||
run_gemini 180
|
||||
fi
|
||||
emit_or_fail
|
||||
;;
|
||||
|
||||
review|file)
|
||||
[ -z "${1:-}" ] && { echo "usage: $SELF review <file-path> [instructions]" >&2; exit 2; }
|
||||
target="$1"
|
||||
instr="${2:-Give an independent, critical review of this file: accuracy, gaps/omissions, bugs, and concrete improvements. Be specific.}"
|
||||
# GOTCHA: a relative path resolves against ONLY CWD or $REPO_ROOT ($CLAUDETOOLS_ROOT) --
|
||||
# NOT a submodule/subdir. "server/src/x.rs" relative to a submodule fails ("file not found")
|
||||
# unless CWD is that submodule. Pass ABSOLUTE paths for submodule/subtree files.
|
||||
if [ -f "$target" ]; then resolved="$target"
|
||||
elif [ -f "$REPO_ROOT/$target" ]; then resolved="$REPO_ROOT/$target"
|
||||
else echo "[$SELF] file not found: $target" >&2; exit 2; fi
|
||||
prep_includes
|
||||
base="$(basename "$resolved")"
|
||||
cp -f "$resolved" "$INCLUDE_DIR/$base"
|
||||
tgt_win="$(winpath "$INCLUDE_DIR/$base")"
|
||||
inc_win="$(winpath "$INCLUDE_DIR")"
|
||||
printf 'Use your read_file tool to read the file at this absolute path, then perform the task and stop. Do not modify anything.\nPath: %s\n\nTask: %s' "$tgt_win" "$instr" > "$PF"
|
||||
run_gemini 240 -m "$STRONG_MODEL" --approval-mode plan --include-directories "$inc_win"
|
||||
emit_or_fail
|
||||
;;
|
||||
|
||||
review-files)
|
||||
instr='Independently review these files together as a unit: correctness/bugs, gaps, cross-file consistency, and concrete improvements. Be specific and cite file:line.'
|
||||
files=()
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-i|--instr) instr="${2:-}"; shift 2 2>/dev/null || shift ;;
|
||||
*) files+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
[ ${#files[@]} -eq 0 ] && { echo "usage: $SELF review-files [-i \"instructions\"] <file> [file ...]" >&2; exit 2; }
|
||||
prep_includes
|
||||
list=""
|
||||
declare -A seen=()
|
||||
# GOTCHA: each relative path resolves against ONLY CWD or $REPO_ROOT ($CLAUDETOOLS_ROOT) --
|
||||
# NOT a submodule/subdir. Paths relative to a submodule fail unless CWD is that submodule.
|
||||
# Pass ABSOLUTE paths for submodule/subtree files (e.g. build the list with `find "$(pwd)/..."`).
|
||||
for f in "${files[@]}"; do
|
||||
if [ -f "$f" ]; then r="$f"
|
||||
elif [ -f "$REPO_ROOT/$f" ]; then r="$REPO_ROOT/$f"
|
||||
else echo "[$SELF] file not found: $f" >&2; exit 2; fi
|
||||
base="$(basename "$r")"
|
||||
# de-collide identical basenames from different dirs
|
||||
if [ -n "${seen[$base]:-}" ]; then
|
||||
n=1; while [ -e "$INCLUDE_DIR/${n}_${base}" ]; do n=$((n+1)); done; base="${n}_${base}"
|
||||
fi
|
||||
seen[$base]=1
|
||||
cp -f "$r" "$INCLUDE_DIR/$base"
|
||||
list+="- $(winpath "$INCLUDE_DIR/$base")
|
||||
"
|
||||
done
|
||||
inc_win="$(winpath "$INCLUDE_DIR")"
|
||||
printf 'Use your read_file tool to read EACH of these files (absolute paths), then perform the task across ALL of them and stop. Do not modify anything.\n\nFiles:\n%s\nTask: %s' "$list" "$instr" > "$PF"
|
||||
run_gemini 300 -m "$STRONG_MODEL" --approval-mode plan --include-directories "$inc_win"
|
||||
emit_or_fail
|
||||
;;
|
||||
|
||||
review-diff)
|
||||
gdir="$REPO_ROOT"
|
||||
instr='Review this git diff: correctness/bugs introduced, regressions, missing edge cases, and concrete fixes. Focus on the CHANGES. Be specific and cite file:line.'
|
||||
ref=""; pathspec=()
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-C|--dir) gdir="${2:-}"; shift 2 2>/dev/null || shift ;;
|
||||
-i|--instr) instr="${2:-}"; shift 2 2>/dev/null || shift ;;
|
||||
--) shift; while [ $# -gt 0 ]; do pathspec+=("$1"); shift; done ;;
|
||||
*) if [ -z "$ref" ]; then ref="$1"; else pathspec+=("$1"); fi; shift ;;
|
||||
esac
|
||||
done
|
||||
[ -z "$ref" ] && { echo "usage: $SELF review-diff [-C <repo-dir>] [-i \"instr\"] <gitref> [-- <pathspec>]" >&2; exit 2; }
|
||||
[ -d "$gdir" ] || { [ -d "$REPO_ROOT/$gdir" ] && gdir="$REPO_ROOT/$gdir"; }
|
||||
git -C "$gdir" rev-parse --git-dir >/dev/null 2>&1 || { echo "[$SELF] not a git repo: $gdir" >&2; exit 2; }
|
||||
if [ ${#pathspec[@]} -gt 0 ]; then
|
||||
git -C "$gdir" diff "$ref" -- "${pathspec[@]}" > "$TMP/diff.txt" 2>"$TMP/differr.txt"
|
||||
else
|
||||
git -C "$gdir" diff "$ref" > "$TMP/diff.txt" 2>"$TMP/differr.txt"
|
||||
fi
|
||||
[ -s "$TMP/diff.txt" ] || { echo "[$SELF] empty/failed diff for '$ref' in $gdir: $(head -1 "$TMP/differr.txt" 2>/dev/null)" >&2; exit 1; }
|
||||
gdir_win="$(winpath "$gdir")"
|
||||
{ printf 'Review the following unified git diff. %s\nYou may use your read_file tool on any changed file for full context (paths in the diff are relative to %s; strip the a/ b/ prefixes). Do not modify anything.\n\n=== BEGIN DIFF ===\n' "$instr" "$gdir_win"; cat "$TMP/diff.txt"; printf '\n=== END DIFF ===\n'; } > "$PF"
|
||||
run_gemini 300 -m "$STRONG_MODEL" --approval-mode plan --include-directories "$gdir_win"
|
||||
emit_or_fail
|
||||
;;
|
||||
|
||||
image-analyze|image|vision)
|
||||
# Independent second-model VISION. The default flash-lite router hallucinates
|
||||
# image content, so we PIN the pro vision model (STRONG_MODEL) and run with
|
||||
# yolo approval so read_file can execute. The image is copied into an included
|
||||
# temp dir (like the review modes) and handed to Gemini by absolute winpath.
|
||||
[ -z "${1:-}" ] && { echo "usage: $SELF image-analyze <image-path> [\"question\"]" >&2; exit 2; }
|
||||
target="$1"
|
||||
question="${2:-Describe exactly what is in this image.}"
|
||||
if [ -f "$target" ]; then resolved="$target"
|
||||
elif [ -f "$REPO_ROOT/$target" ]; then resolved="$REPO_ROOT/$target"
|
||||
else echo "[$SELF] image not found: $target" >&2; exit 2; fi
|
||||
prep_includes
|
||||
base="$(basename "$resolved")"
|
||||
cp -f "$resolved" "$INCLUDE_DIR/$base"
|
||||
img_win="$(winpath "$INCLUDE_DIR/$base")"
|
||||
inc_win="$(winpath "$INCLUDE_DIR")"
|
||||
# Image path goes in via %s (never as a printf format string).
|
||||
printf 'Use your read_file tool to read the image at this absolute path, then describe exactly what you see. Report only what is actually present in the image; do not guess or invent content. Then stop. Do not modify anything.\nImage path: %s\n\nQuestion: %s' "$img_win" "$question" > "$PF"
|
||||
run_gemini 240 -m "$STRONG_MODEL" --approval-mode yolo --include-directories "$inc_win"
|
||||
AGY_CLEAN=1 emit_or_fail
|
||||
;;
|
||||
|
||||
search|websearch)
|
||||
# Google-grounded LIVE web search (mirrors grok xsearch). Gemini's
|
||||
# google_web_search tool works on OAuth; run with yolo so the tool can fire.
|
||||
# Query goes via the prompt file so long queries don't hit shell-quote limits.
|
||||
SRC=""
|
||||
if [ "${1:-}" = "--prompt-file" ]; then
|
||||
[ -f "${2:-}" ] || { echo "[$SELF] prompt file not found: ${2:-}" >&2; exit 2; }
|
||||
SRC="$(cat "$2")"
|
||||
else
|
||||
SRC="${1:-}"
|
||||
fi
|
||||
[ -z "$SRC" ] && { echo "usage: $SELF search \"<query>\" | $SELF search --prompt-file <path>" >&2; exit 2; }
|
||||
printf 'Use your google_web_search tool to find current, live information answering the following, then stop. Answer concisely and ALWAYS include the source URLs you used (a Sources list of full URLs). Do not fabricate URLs.\n\nQuery: %s' "$SRC" > "$PF"
|
||||
run_gemini 180 -m "$STRONG_MODEL" --approval-mode yolo
|
||||
emit_or_fail
|
||||
;;
|
||||
|
||||
raw)
|
||||
"$GEMINI" "$@"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "[$SELF] unknown mode '$MODE' (use text|verify|review|review-files|review-diff|image-analyze|search|raw)" >&2; exit 2 ;;
|
||||
esac
|
||||
# ask-gemini.sh — DEPRECATED shim. The AGY skill now routes to the Antigravity CLI
|
||||
# (`agy`), not the old Google `gemini` npm CLI (which broke on this account with a
|
||||
# Cloud-project eligibility error). This shim forwards to ask-agy.sh so any existing
|
||||
# references keep working. New callers should use ask-agy.sh directly.
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 2>/dev/null && pwd)"
|
||||
exec bash "$DIR/ask-agy.sh" "$@"
|
||||
|
||||
@@ -238,12 +238,12 @@ for ln in sys.stdin:
|
||||
if e.get("type")=="text": t.append(e.get("data",""))
|
||||
print("".join(t).strip())' < "$OUT")"
|
||||
if [ "$GRC" -eq 0 ] && [ -n "$ans" ]; then printf '%s\n' "$ans"; exit 0; fi
|
||||
echo "[$SELF] grok xsearch did not finish (rc=$GRC) -> falling back to gemini search" >&2
|
||||
_logerr "grok xsearch incomplete (rc=$GRC); auto-fell back to gemini" --context "mode=xsearch"
|
||||
GEM="$REPO_ROOT/.claude/skills/agy/scripts/ask-gemini.sh"
|
||||
if [ -f "$GEM" ]; then echo "[grok xsearch timed out -> answered via gemini search]"; exec bash "$GEM" search "$Q"; fi
|
||||
echo "[$SELF] grok xsearch did not finish (rc=$GRC) -> falling back to agy search" >&2
|
||||
_logerr "grok xsearch incomplete (rc=$GRC); auto-fell back to agy" --context "mode=xsearch"
|
||||
GEM="$REPO_ROOT/.claude/skills/agy/scripts/ask-agy.sh"
|
||||
if [ -f "$GEM" ]; then echo "[grok xsearch timed out -> answered via agy search]"; exec bash "$GEM" search "$Q"; fi
|
||||
[ -n "$ans" ] && { printf '%s\n' "$ans"; exit 0; } # last resort: whatever partial streamed
|
||||
echo "[$SELF] no result (grok timed out; gemini fallback unavailable)" >&2; exit 1
|
||||
echo "[$SELF] no result (grok timed out; agy fallback unavailable)" >&2; exit 1
|
||||
;;
|
||||
review|file)
|
||||
[ -z "${1:-}" ] && { echo "usage: $SELF review <file-path> [instructions]" >&2; exit 2; }
|
||||
|
||||
1
.runner-tok
Normal file
1
.runner-tok
Normal file
@@ -0,0 +1 @@
|
||||
eyJ0eXAiOiJKV1QiLCJub25jZSI6ImhsZElMaDBmNm5TMkV1bnhIam1xZjNLUEFUVzlEZUpwVEhRR2FCVFptYm8iLCJhbGciOiJSUzI1NiIsIng1dCI6ImFGa21LVkZjLTRXVjZzWENCdk5aa1hJNTA1WSIsImtpZCI6ImFGa21LVkZjLTRXVjZzWENCdk5aa1hJNTA1WSJ9.eyJhdWQiOiJodHRwczovL2dyYXBoLm1pY3Jvc29mdC5jb20iLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC8xOWE1NjhlOC05ZTg4LTQxM2ItOTM0MS1jYmMyMjRiMzkxNDUvIiwiaWF0IjoxNzgzMDQ4Mjc0LCJuYmYiOjE3ODMwNDgyNzQsImV4cCI6MTc4MzA1MjE3NCwiYWNycyI6WyJwZmRyIl0sImFpbyI6IkFTUUEyLzhjQUFBQUtzcXJxZTRkYVZNbWMvSEk0R2pGK3hxdkVzdkRpWTJnTzVWSlE4aytUSTA9IiwiYXBwX2Rpc3BsYXluYW1lIjoiQ29tcHV0ZXJHdXJ1IC0gVGVuYW50IEFkbWluIiwiYXBwaWQiOiI3MDllNmVlZC0wNzExLTQ4NzUtOWM0NC0yZDM1MThjNDcwNjMiLCJhcHBpZGFjciI6IjIiLCJpZHAiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC8xOWE1NjhlOC05ZTg4LTQxM2ItOTM0MS1jYmMyMjRiMzkxNDUvIiwiaWR0eXAiOiJhcHAiLCJvaWQiOiI3YTE5OWIxMS05N2ZiLTRlNjUtOTE3ZC1mOGQyOWE1M2JhNDkiLCJyaCI6IjEuQVVZQTZHaWxHWWllTzBHVFFjdkNKTE9SUlFNQUFBQUFBQUFBd0FBQUFBQUFBQUFBQUFCR0FBLiIsInJvbGVzIjpbIlBvbGljeS5SZWFkV3JpdGUuQ29uZGl0aW9uYWxBY2Nlc3MiLCJVc2VyLlJlYWRXcml0ZS5BbGwiLCJTZWN1cml0eUV2ZW50cy5SZWFkLkFsbCIsIlVzZXJBdXRoZW50aWNhdGlvbk1ldGhvZC5SZWFkV3JpdGUuQWxsIiwiQXBwbGljYXRpb24uUmVhZFdyaXRlLkFsbCIsIkRpcmVjdG9yeS5SZWFkV3JpdGUuQWxsIiwiU2l0ZXMuUmVhZFdyaXRlLkFsbCIsIkFwcFJvbGVBc3NpZ25tZW50LlJlYWRXcml0ZS5BbGwiLCJSb2xlTWFuYWdlbWVudC5SZWFkV3JpdGUuRGlyZWN0b3J5IiwiUG9saWN5LlJlYWQuQWxsIiwiU2l0ZXMuRnVsbENvbnRyb2wuQWxsIl0sInN1YiI6IjdhMTk5YjExLTk3ZmItNGU2NS05MTdkLWY4ZDI5YTUzYmE0OSIsInRlbmFudF9yZWdpb25fc2NvcGUiOiJOQSIsInRpZCI6IjE5YTU2OGU4LTllODgtNDEzYi05MzQxLWNiYzIyNGIzOTE0NSIsInV0aSI6Ik9TMlRQV1I3Y1VtYmpIUWFXRkVkQUEiLCJ2ZXIiOiIxLjAiLCJ3aWRzIjpbImIxYmUxYzNlLWI2NWQtNGYxOS04NDI3LWY2ZmEwZDk3ZmViOSIsIjA5OTdhMWQwLTBkMWQtNGFjYi1iNDA4LWQ1Y2E3MzEyMWU5MCJdLCJ4bXNfYWNkIjoxNzc2NzA4NDAxLCJ4bXNfYWN0X2ZjdCI6IjkgMyIsInhtc19mdGQiOiJkRm1iM1Fqa3oxMjBKZFJHbjNfNnVzajZDZHdwNi1hcW5BcDFXT21iZENFQmRYTmxZWE4wTFdSemJYTSIsInhtc19pZHJlbCI6IjcgMTYiLCJ4bXNfcGZ0ZXhwIjoxNzgzMTM4NTc0LCJ4bXNfcmQiOiIwLjQyTGxZQkppZEJNUzRXQVZFcEJ6U3p5M29MSEZaN2RYdGFuWExOTmNJUkVPVGlHQnRJOWVVdy1lZXVrNVFldzVjLU4ybHM5Q0lod2NRZ0xNREJCd0FFb0xpWEJ3Q3dsc2VTRDdOZHpmN3VRaVNha1duNWF0QWdBIiwieG1zX3N1Yl9mY3QiOiIzIDkiLCJ4bXNfdGNkdCI6MTU4MTAyMjkyNywieG1zX3RudF9mY3QiOiIzIDgifQ.B5m5DUvFOW5_ob86hgh6nF7al3Y56hClOdscolCMtvGPoN1OKxilEeU1yy7hHUG67NGH7mUXa4MYkXul5Xv42jvHYMvcI_t8zQY68Wm4td30nIgXFhvVfyT8eV8Dhs6EF2x6HOkyF58XZe9tI837eQmo2mAdSoJZbJF-bDcKmHjTQWJZAcycXlRr5JKUWq4QVXYUmhihpOHt9n4eVJAbuHF1sokDc8XfysRp93sa72Fz4FEhaM8ejv3oNj0l7AE3UmIfL4Z2FT5qglaJmKzELXV-dOAwyt_LpYjoxcCoVdzrm7OGKW38CbjaJKNIXs1bHbmxuNMkup3tpdt8c6CV9g
|
||||
@@ -0,0 +1,55 @@
|
||||
# Datto -> SharePoint FULL reconciliation - COMPLETE - 2026-07-02/03
|
||||
|
||||
## Outcome
|
||||
|
||||
**557 missing files recovered; all 5 site mappings independently verified at zero genuinely-missing.**
|
||||
|
||||
| Site | Datto files | Missing found | Recovered | Verified residual |
|
||||
|---|---|---|---|---|
|
||||
| Donor Services | 57,329 | 74 | 73 | 1 = intentional ghost-skip ("26-02328 - Copy.pdf", same-size renamed sibling in SP) |
|
||||
| Admin | 6,301 | 472 | 468 + 4 (long-path retry) | 0 |
|
||||
| Birth Biologic Activity Reports | 1 | 1 | 1 | 0 |
|
||||
| Quality (QSD) | 3,768 | 11 | 11 | 0 |
|
||||
| Supply Management | 160 | 0 | - | 0 |
|
||||
|
||||
sp-only counts (files in SP not in Datto: staff's new post-migration work) and size-diffs
|
||||
(docx uniformly larger in SP = SharePoint Office property promotion; content-harmless) are
|
||||
expected and documented per-site in C:\GuruCompare\ on ACG-DWP-X-BB.
|
||||
|
||||
## Root cause (answers "how was it missing after the sync to 0")
|
||||
|
||||
The 6/27 delta upload used simple Graph PUTs which **silently skip files >=4MB**; the
|
||||
"reconciled to 0" claim trusted the uploader's own success reporting. Quality got the
|
||||
chunked-upload fix 6/30; Donor Services and Admin were never re-verified. Admin was worst
|
||||
(472 files, incl. >260-char long-path victims in consultant folder trees). Staff had begun
|
||||
manually recreating missing donor PDFs (e.g. 26-04085.pdf, Mary Ster, 7/2).
|
||||
|
||||
## Method (repeatable)
|
||||
|
||||
`C:\GuruCompare\bb-fix-all.ps1` on ACG-DWP-X-BB (source in ClaudeTools scratch/session log):
|
||||
per-site full inventory diff (path+size, case-insensitive) + rail-guarded fix:
|
||||
- chunked upload sessions for >=4MB, simple PUT below
|
||||
- conflictBehavior=fail (never overwrites SP)
|
||||
- same-size-in-folder ghost detection (never resurrects renamed files)
|
||||
- \\?\ long-path file opens; FileShare::ReadWrite (Datto service can stay running)
|
||||
- fail-hard SP enumeration w/ 429 retry; sanity-abort if SP looks empty; per-site fresh token
|
||||
|
||||
Run via GuruRMM per site: `powershell -File C:\GuruCompare\bb-fix-all.ps1 -Site <n>`
|
||||
(fresh Graph token to C:\GuruCompare\tok.txt first). Logs: C:\GuruCompare\<site>-fix-log.txt.
|
||||
|
||||
## Gotchas hit (memorialized)
|
||||
|
||||
- PS5.1 under RMM powershell -File: `-Headers $H` script-scope hashtable sent EMPTY
|
||||
Authorization ("Access token is empty") - build headers inline. Memory: ps51-invoke-restmethod-headers-variable.
|
||||
- Two failed runs masqueraded as token expiry / clock skew; capturing the Graph error BODY
|
||||
was what cracked it.
|
||||
- ACG-DWP-X-BB clock was NTP-corrected 8:03 PM 7/2 (had drifted while parked).
|
||||
|
||||
## Open items
|
||||
|
||||
- OneDrive placeholder mirror on ACG-DWP-X-BB for standing local comparisons: policies ready
|
||||
to push; needs interactive OneDrive sign-in as sysadmin@birthbiologic.com via ScreenConnect.
|
||||
- Datto Workplace service on the VM left RUNNING (catch-net for the still-active
|
||||
donor-scan-to-Datto workflow; 2 scans from 6/29 + HR file were among the recovered).
|
||||
Decide: keep as ongoing catch-net vs re-freeze.
|
||||
- Client comms: staff can stop manually recreating missing April/May donor records.
|
||||
10
errorlog.md
10
errorlog.md
@@ -19,6 +19,16 @@ Categories (the `[type]` tag): _(none)_ = skill/command execution failure ·
|
||||
|
||||
<!-- Append entries below this line -->
|
||||
|
||||
2026-07-03 | GURU-5070 | agy/gemini-cli | old gemini npm CLI dead on this account: throwIneligibleOrProjectIdError (needs GOOGLE_CLOUD_PROJECT); replaced by Antigravity 'agy' binary [ctx: fix=rewired-to-agy]
|
||||
|
||||
2026-07-03 | GURU-5070 | grok | grok returned no text [ctx: mode=text stopReason=Cancelled]
|
||||
|
||||
2026-07-03 | GURU-5070 | agy | gemini returned no response (empty after 3 attempts) [ctx: mode=search err=}]
|
||||
|
||||
2026-07-03 | GURU-5070 | grok | grok xsearch incomplete (rc=124); auto-fell back to gemini [ctx: mode=xsearch]
|
||||
|
||||
2026-07-03 | GURU-5070 | agy | gemini returned no response (empty after 3 attempts) [ctx: mode=review err= at process.processTicksAndRejections (node:internal/process/task_queues:104:] (x2)
|
||||
|
||||
2026-07-03 | Howard-Home | remediation-tool/graph | [friction] auditLogs/signIns $filter (userId/UPN) hangs HTTP000 on Cascades tenant; large $top also hangs. Fix: small unfiltered $top=50, filter client-side
|
||||
|
||||
2026-07-03 | Howard-Home | rmm/ps-encoded | [friction] iconv missing in Git Bash so ps-encoded.sh cannot build UTF-16LE -EncodedCommand. Fix: py -c base64.b64encode(text.encode('utf-16-le'))
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
## User
|
||||
- **User:** Mike Swanson (mike)
|
||||
- **Machine:** GURU-5070
|
||||
- **Role:** admin
|
||||
|
||||
## Session Summary
|
||||
|
||||
Long multi-thread session. Three bodies of work: (1) Peaceful Spirit infrastructure — bringing a new
|
||||
NW-site domain controller online and cleaning up the dead PST-SERVER2; (2) Peaceful Spirit data
|
||||
recovery — the 2025 server-crash corrupted-file investigation, recovering four payroll spreadsheets
|
||||
and emailing them to the client; (3) the dominant thread — a deep GuruRMM VSS/CrowdStrike redesign
|
||||
that evolved from "internalize the agent's VSS ops to native COM" into a full architectural pivot
|
||||
("agent = policy configurator, not shadow operator") with two new spec folders and a validated
|
||||
native-COM code path.
|
||||
|
||||
Peaceful Spirit: a new physical server (shipped as PST-SERVER01) was enrolled in GuruRMM, renamed to
|
||||
PST-DC-NW pre-domain-join (via RMM), then joined + promoted DC/GC/DNS into the NW site — all via RMM.
|
||||
Before promotion, the dead PST-SERVER2 (hardware died ~2026-06-14) was metadata-cleaned from AD
|
||||
(DFSR member/connection objects, NTDS/config server object, DC computer account, 18 stale DNS
|
||||
records); PST-DFS was re-added with PST-DC-NW as the C:\Shares receiver (initial ~265 GB sync
|
||||
started). Data recovery: read Mara's Claude-analysis docx, inventoried PST-SERVER D: (the old crashed
|
||||
server's drive), scanned the live G:\Shares for Mike's `[C]`-prefixed corrupted files (5,044 files),
|
||||
then content-fingerprinted the carved `D:\Unknown folder` output to positively identify four
|
||||
MPEG-corrupted 2024/2025 payroll xlsx (incl. the `IC Payments 2-12 to 2-26-25` file Mara chased for a
|
||||
year), which were emailed to Mara from mike@azcomputerguru.com.
|
||||
|
||||
GuruRMM VSS: the session opened investigating a CrowdStrike Falcon T1490 "Inhibit System Recovery"
|
||||
detection on NEPTUNE — the GuruRMM agent's scheduled VSS pass, running `powershell -ExecutionPolicy
|
||||
Bypass` shadow-copy ops, matched the ransomware TTP and Falcon blocked it. This drove: an audit of all
|
||||
agent shell-outs (Grok + agy reviewed), an adversarial pass (agy) that corrected the plan, a
|
||||
`vss-native-com` spec, and actual implementation — native VSS provision (validated on NEPTUNE) and a
|
||||
hand-rolled `IVssBackupComponents` create/delete (built from the SDK vsbackup.h, compiled + create
|
||||
runtime-validated). Runtime testing then revealed the actual T1490 trigger was the PRUNE/DELETE (all
|
||||
explicit deletes are Falcon-blocked; cap-driven eviction rotates shadows fine), prompting the pivot:
|
||||
drop the scheduled prune, configure the OS-native shadow-copy system instead. A second spec,
|
||||
`vss-policy-config`, captures that; multi-AI (Grok + Gemini) confirmed a uniform native-COM-create
|
||||
approach works on all SKUs including workstations, and added the `MaxShadowCopies` count governor.
|
||||
Task 1 of that spec (the blocking spike) was completed on GURU-5070 (Win11 Pro client): MaxShadowCopies
|
||||
FIFO-evicts (does not block), and native COM create works on the client SKU.
|
||||
|
||||
Also rewired the `agy` skill from the dead Google `gemini` npm CLI (which failed with
|
||||
`throwIneligibleOrProjectIdError`) to the new Antigravity `agy` binary, and repointed grok's xsearch
|
||||
fallback to it.
|
||||
|
||||
## Key Decisions
|
||||
|
||||
- **PST-DC-NW rename before domain join.** Renaming a promoted DC is messy (DNS/SPN/DFSR cleanup), so
|
||||
the rename ran first, on the workgroup box, via RMM.
|
||||
- **PST-SERVER2 got a full AD metadata cleanup, not just power-off.** It died past cases where you can
|
||||
resume replication; leaving stale DC metadata is what caused the June tombstone mess. Removed via
|
||||
direct AD object deletion (Remove-DfsrMember fails on a dead member — "network path not found").
|
||||
- **Data recovery: identify carved files by content, not by name.** The crash detached filenames from
|
||||
data blocks; carved output has bracket names. Fingerprinted sharedStrings + exact byte-size +
|
||||
in-workbook date serials to positively match. Did NOT place recovered files into the live HIPAA
|
||||
share — emailed to Mara for her to verify.
|
||||
- **VSS: use native VSS COM API `IVssBackupComponents`, NOT WMI** (adversarial-pass finding) — WMI
|
||||
`Win32_ShadowCopy` still runs in wmiprvse.exe and trips T1490; only the native requestor reads as
|
||||
legitimate backup software.
|
||||
- **Concurrency: dedicated COM-owning OS thread + MPSC + timeout, NOT tokio::spawn_blocking** — a hung
|
||||
COM/WMI call on the shared blocking pool would deadlock the agent.
|
||||
- **THE PIVOT: agent = configurator, not operator.** Runtime testing proved the T1490 trigger is the
|
||||
DELETE/prune (Falcon blocks ALL explicit shadow deletes), while cap-driven kernel eviction rotates
|
||||
shadows fine. So drop the scheduled prune (retention = cap), configure the OS-native create schedule,
|
||||
and the agent just sets policy + reports status. This removed most of the create/delete-internalization
|
||||
work from the hot path.
|
||||
- **Workstations: uniform native-COM create on all SKUs** (multi-AI validated). `vssadmin create shadow`
|
||||
is Server-only, but `IVssBackupComponents::DoSnapshotSet` and volsnap cap-eviction are NOT SKU-gated —
|
||||
only the CLI is. The scheduled-task action is the signed agent's native create on every SKU.
|
||||
- **Two retention governors, both FIFO, zero deletes:** shadow-storage MaxSize (native COM) +
|
||||
MaxShadowCopies count (registry). Grok caught the second one; Task 1 confirmed it FIFO-evicts.
|
||||
- **Do NOT ship the Falcon IOA exclusion** (`powershell.exe`+`gururmm_*.ps1`) — adversarial review
|
||||
flagged it as an RCE backdoor (SystemTemp is world-writable-ish). Stopgap = pause the VSS schedule.
|
||||
- **Remote build/verify loop:** this box (GURU-5070) can't `cargo check` the agent (no MSVC build
|
||||
tools); push branch to internal Gitea over HTTP, `cargo check`/`build` on the Pluto build host.
|
||||
- **Binary transfer to RMM-managed boxes: use the RMM server's `/downloads` path** (NEPTUNE/GURU-5070
|
||||
reach `rmm.azcomputerguru.com/downloads` over HTTPS) — peer-to-peer (Pluto:8099) is network-segmented.
|
||||
|
||||
## Problems Encountered
|
||||
|
||||
- **Falcon T1490 blocked the agent's VSS pass on NEPTUNE.** Root cause = the temp-ps1 + ExecutionPolicy
|
||||
Bypass shadow-op pattern (ransomware TTP). Immediate: disabled the `GuruRMM-VSS-Snapshot` scheduled
|
||||
task (stopgap). Real fix = the redesign.
|
||||
- **`IVssBackupComponents` is absent from windows-rs 0.58** (its header vsbackup.h is outside
|
||||
win32metadata). Resolved by hand-declaring the interface: pulled the exact 40-slot vtable order + IID
|
||||
+ method signatures from the SDK `vsbackup.h` on Pluto, built it with `windows_core::imp::define_interface!`
|
||||
+ a manual vtable (usize placeholders for the 33 unused slots) + `vssapi.dll` FFI.
|
||||
- **Link error: `VssFreeSnapshotProperties` not in vssapi.lib** (LNK2019). Resolved by freeing the
|
||||
VSS_SNAPSHOT_PROP string members directly via `CoTaskMemFree` (what the function does internally).
|
||||
- **`AddDiffArea`/`ChangeDiffAreaMaximumSize` take `*const u16`, not `PCWSTR`** — caught by the Pluto
|
||||
cargo check (a check I can't run locally). Fixed.
|
||||
- **RMM inline-JSON dispatch mangles backslash payloads** (recurring) — used the ps-encoded script-file
|
||||
path and forward slashes throughout.
|
||||
- **Native delete "succeeded" but didn't remove the shadow.** Investigation: the shadow was a normal
|
||||
ClientAccessible shadow, and vssadmin + WMI `.Delete()` ALSO failed — because Falcon blocks all
|
||||
explicit shadow deletes (T1490). This was the pivotal finding driving the configurator redesign.
|
||||
- **agy/Gemini CLI dead** (`throwIneligibleOrProjectIdError`) — the old npm gemini CLI needs a
|
||||
GOOGLE_CLOUD_PROJECT the account can't supply. Resolved by rewiring the skill to the Antigravity
|
||||
`agy` binary (own auth, no project ID).
|
||||
- **Gitea push over the Cloudflare https remote fails auth** — pushed to the internal HTTP Gitea
|
||||
(`http://172.16.3.20:3000`) with URL-encoded vault creds (password has URL-breaking chars).
|
||||
- **One transient native-create slowness on GURU-5070** (>150s once, then <75s) — not a systematic
|
||||
hang; the scheduled create action needs a sane timeout/retry.
|
||||
|
||||
## Configuration Changes
|
||||
|
||||
Peaceful Spirit / infra (via RMM, on PST-SERVER `87293069-...` and PST-DC-NW `f60e9820-...`):
|
||||
- PST-SERVER01 renamed to PST-DC-NW; domain-joined PEACEFULSPIRIT.local; promoted DC/GC/DNS (site NW);
|
||||
static 192.168.1.5; timezone AZ. PST-SERVER2 AD/DNS/DFSR metadata removed. PST-DFS re-added with
|
||||
PST-DC-NW C:\Shares receiver. DSRM password vaulted.
|
||||
- `GuruRMM-VSS-Snapshot` scheduled task DISABLED on NEPTUNE (`b3a9b454-...`) — T1490 stopgap.
|
||||
|
||||
Repo (guru-rmm submodule, branch `feat/vss-native-com`):
|
||||
- `agent/src/vss_com.rs` (new) — native COM VSS worker: dedicated COM thread + MPSC; `provision` (cap),
|
||||
`create`/`delete` (hand-rolled IVssBackupComponents).
|
||||
- `agent/src/vss.rs` — provision/create/delete branched native(modern)/PS(legacy); added top-level
|
||||
`provision_storage` wrapper.
|
||||
- `agent/src/main.rs` — added `mod vss_com`; hidden diagnostic verbs `vss-provision-test`,
|
||||
`vss-roundtrip-test`.
|
||||
- `agent/Cargo.toml` — +windows features `Win32_Storage_Vss`, `Win32_System_Com`; +`windows-core` dep.
|
||||
- `specs/vss-native-com/` (4 files) — the internalization spec (superseded-in-part by the pivot).
|
||||
- `specs/vss-policy-config/` (4 files) — the configurator redesign spec (Task 1 DONE).
|
||||
- `docs/RMM_THOUGHTS.md` — internalize-VSS entry, shell-out audit, adversarial-pass corrections, the
|
||||
configurator PIVOT.
|
||||
- `specs/crowdstrike-falcon/{plan,references}.md` — T1490 rollout dependency (pause schedule, not exclusion).
|
||||
|
||||
Repo (ClaudeTools main):
|
||||
- `.claude/skills/agy/scripts/ask-agy.sh` (new) — Antigravity CLI wrapper; `ask-gemini.sh` now a shim.
|
||||
- `.claude/skills/agy/SKILL.md` — rewired to agy.
|
||||
- `.claude/skills/grok/scripts/ask-grok.sh` — xsearch fallback repointed gemini->agy.
|
||||
- `.claude/identity.json` (local, gitignored) — added `agy` block, retired `gemini`.
|
||||
- `wiki/clients/peaceful-spirit.md` — extensive updates (PST-DC-NW, metadata cleanup, 2025 crash &
|
||||
corruption section, recovery results).
|
||||
|
||||
## Credentials & Secrets
|
||||
|
||||
- **PST-DC-NW DSRM password** — created + vaulted at `clients/peaceful-spirit/dc-nw` (SOPS vault).
|
||||
- **PEACEFULSPIRIT\sysadmin (DA)** — read from vault `clients/peaceful-spirit/server` for the
|
||||
join/promotion; used via RMM Invoke-Command (FQDN). Not rotated (RMM internal).
|
||||
- **GURU-5070 finding (NOT vaulted — flag for follow-up):** `~/.gemini/antigravity-cli/settings.json`
|
||||
contains a live 1Password service-account token (`OP_SERVICE_ACCOUNT_TOKEN=ops_eyJ...`) in its
|
||||
`permissions.allow` list, in plaintext. Should be vaulted + scrubbed. (The adjacent
|
||||
`GEMINI_API_KEY=AIzaSyDummyKey...` is a dummy.)
|
||||
- Gitea push creds: vault `services/gitea.sops.yaml` (credentials.username/password); push via internal
|
||||
HTTP `172.16.3.20:3000`.
|
||||
|
||||
## Infrastructure & Servers
|
||||
|
||||
- **PST-DC-NW** (Peaceful Spirit NW DC) — 192.168.1.5, Win Server 2019, RMM agent `f60e9820-4a00-4598-83f7-c14085db5768`, site "North West".
|
||||
- **PST-SERVER** (CC DC) — 192.168.0.2, RMM agent `87293069-33b6-45e8-a68f-6811216cdb96`. D: = old crashed server's drive (recovery corpus; FROZEN, no cleanup). G:\Shares = live data.
|
||||
- **PST-SERVER2** — DEAD (~2026-06-14), metadata cleaned 2026-07-03.
|
||||
- **NEPTUNE** (ACG internal) — 172.16.3.11 / WAN 67.206.163.122, Win Server 2022, RMM agent `b3a9b454-86eb-491c-ac67-c1f98987d8dc`, CrowdStrike Falcon present. Runs cbb.exe (MSP360, native VSS backup). Leftover test shadow `{8FEFDAE3-A002-439F-B362-92FC4B9CCDAD}` (Falcon-blocked delete; will rotate).
|
||||
- **GURU-5070** (this box) — Win11 Pro (ProductType=1), RMM agent `819df0c8-4824-4424-b55a-2c5cb4d6ca39`, site "Mike's Car", Falcon present. Leftover test shadow `{87AFEA99-3336-4F85-A9D9-0E3F2F00D51B}`.
|
||||
- **Pluto** build host — `Administrator@172.16.3.36` (SSH key), C:\gururmm checkout, has MSVC + Rust (stable + 1.77 legacy) + Windows SDK. Used for `cargo check`/`build`.
|
||||
- **RMM server** — `guru@172.16.3.30` (Ubuntu 22.04, SSH key), serves `rmm.azcomputerguru.com/downloads` from `/var/www/gururmm/downloads`. Used to stage test binaries for RMM-managed boxes.
|
||||
- **Internal Gitea** — `http://172.16.3.20:3000` (HTTP, not the Cloudflare https remote).
|
||||
- **B2 (Backblaze)** — 15 buckets; generic `MSPBackups20200311` holds other clients only (no pre-crash Peaceful Spirit data); ACG-PST plan created on crash day (no pre-incident copy).
|
||||
|
||||
## Commands & Outputs
|
||||
|
||||
- Falcon efficacy (Datto EDR): 500 detections / 90 days fleet-wide = ZERO T1490/shadow detections from native backup binaries.
|
||||
- MaxShadowCopies FIFO test (GURU-5070): `MaxShadowCopies=3`, WMI Create -> oldest evicted, count stayed 3. `[RESULT] FIFO-EVICT`.
|
||||
- Native COM create on client (GURU-5070): `[OK] created shadow_id={87AFEA99...} device=\\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy7`.
|
||||
- Native provision runtime (NEPTUNE): `ChangeDiffAreaMaximumSize` moved C: cap 279GB(15%)->298GB(16%), restored.
|
||||
- Pluto build: STABLE_RC=0, LEGACY_RC=0 (both variants); Task 4 fresh binary sha256 `1368eba8...`.
|
||||
- Push pattern: `git push http://<user>:<urlenc-pw>@172.16.3.20:3000/azcomputerguru/gururmm.git feat/vss-native-com`.
|
||||
|
||||
## Pending / Incomplete Tasks
|
||||
|
||||
- **vss-policy-config Task 2+** (unblocked): build the `vss-create` verb + scheduled-task registration
|
||||
(uniform native create, all SKUs) + set both governors (cap via COM, MaxShadowCopies via registry) +
|
||||
every-N-hours schedule support; retire the legacy `GuruRMM-VSS-Snapshot` task on migration; drop the
|
||||
scheduled prune; update status/compliance. Then build both variants on Pluto + runtime-test on a
|
||||
Server AND a Win10/11 Pro box; merge (fleet migration).
|
||||
- **Vault the leaked 1Password token** in GURU-5070's antigravity settings.json + scrub it.
|
||||
- **Leftover test shadows:** `{8FEFDAE3}` (NEPTUNE), `{87AFEA99}` (GURU-5070) — Falcon-blocked delete;
|
||||
will rotate via governors. Optional cleanup.
|
||||
- **Peaceful Spirit open items** (in the wiki): PST-DC-NW DFS-R initial ~265 GB sync -> then Gate 4
|
||||
(share C:\Shares, add folder target + 2nd namespace root); deletion recovery ~3,342 genuine files
|
||||
(awaiting Mike/Mara go); 5th corrupted payroll file `IC Payments 5-13 to 5-27-25` unmatched.
|
||||
- **EV code-signing + AV-vendor whitelisting** — prerequisite the adversarial review flagged for any
|
||||
on-demand delete on Falcon hosts.
|
||||
- Parked agent shell-out internalizations: `users.rs` (T1098, native NetUser*), `inventory.rs`.
|
||||
|
||||
## Reference Information
|
||||
|
||||
- Branch: `feat/vss-native-com` (guru-rmm submodule). Key commits: `35491a4` (vss-policy-config spec),
|
||||
`d04a2d8` (Task 1 done), `2314ad55` (agy rewire), `07c8dc0` (every-N-hours).
|
||||
- IVssBackupComponents IID: `665c1d5f-c218-414d-a05d-7fef5f9d5c86`. VSS SW provider: `b5946137-7b9f-4925-af80-51abd60b20d5`. CLSID_VssSnapshotMgmt: `0b5a2c52-3eb9-470a-96e2-6c6d4570e40f`.
|
||||
- MaxShadowCopies: `HKLM\SYSTEM\CurrentControlSet\Services\VSS\Settings\MaxShadowCopies` (DWORD, default 64, max 512).
|
||||
- SDK header (Pluto): `C:\Program Files (x86)\Windows Kits\10\Include\10.0.26100.0\um\vsbackup.h`.
|
||||
- Recovered payroll files emailed to Mara (info@bestmassageintucson.com) from mike@azcomputerguru.com.
|
||||
- Specs: `projects/msp-tools/guru-rmm/specs/{vss-native-com,vss-policy-config}/`.
|
||||
Reference in New Issue
Block a user