@caddis/cli 0.4.50 → 0.4.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "caddis",
3
- "version": "1.3.136",
3
+ "version": "1.3.138",
4
4
  "description": "caddis — agent-agnostic dev harness as an Antigravity (agy) plugin: TDD skills + commands, single-sourced with the Claude Code plugin."
5
5
  }
@@ -14,6 +14,10 @@ from typing import Callable, Mapping
14
14
  import oss_model
15
15
 
16
16
 
17
+ # agy is Flash-only on some machines and is good only at small, exact, mechanical cards (112 of 114 passed
18
+ # in the 2026-10-08 UI-upgrade run) and CI watching. Give design-fidelity, cross-file refactor and judgment
19
+ # work to codex or claude. See .caddis/kb/agy-headless-implementer-limits.md. A separate `design` role is an
20
+ # owner decision: it changes the routing line every session prints.
17
21
  DEFAULT_ROUTING: dict[str, list[str]] = {
18
22
  "critical": ["opus"],
19
23
  "light": ["sonnet"],
@@ -689,7 +689,49 @@ if [ -f "pyproject.toml" ] || [ -f "requirements.txt" ]; then
689
689
  echo "[gate] interpreter: $PY"
690
690
  py_gate ruff check .
691
691
  if py_sources_exist; then py_gate mypy .; else echo "[skip] mypy: no Python sources"; fi
692
- py_gate pytest -q
692
+ # pytest is OPT-IN here (CADDIS_GATE_PYTEST=1). Found 2026-10-10: the full suite took 25-31 min in this
693
+ # hook on a box whose system disk was slow, while CI ran the same suite in 5 min - and a merge needs green
694
+ # CI anyway. A local gate five times slower than the check it stands in for trains people to --no-verify it.
695
+ # When it does run: CADDIS_GATE_TMP moves TMPDIR/TEMP/TMP (tests write to tmp_path; Python reads TMPDIR
696
+ # first, so set all three), CADDIS_GATE_PYTEST_ARGS adds arguments (for example "-n 4" with pytest-xdist;
697
+ # use a number, not "auto"), and a machine-wide lock (CADDIS_GATE_LOCK, mkdir-based, waits up to
698
+ # CADDIS_GATE_LOCK_WAIT seconds, a stale one is taken over after 60 min) lets only ONE pytest gate run at a time, so several repos pushing together cannot
699
+ # pile their suites on the same disk and CI runners.
700
+ if [ "${CADDIS_GATE_PYTEST:-0}" = "1" ]; then
701
+ if [ -n "${CADDIS_GATE_TMP:-}" ]; then
702
+ mkdir -p "$CADDIS_GATE_TMP" 2>/dev/null || true
703
+ export TMPDIR="$CADDIS_GATE_TMP" TEMP="$CADDIS_GATE_TMP" TMP="$CADDIS_GATE_TMP"
704
+ fi
705
+ LOCK="${CADDIS_GATE_LOCK:-${TMPDIR:-${TEMP:-/tmp}}/caddis-pytest-gate.lock}"
706
+ mkdir -p "$(dirname "$LOCK")" 2>/dev/null || true
707
+ lock_wait="${CADDIS_GATE_LOCK_WAIT:-1800}"
708
+ waited=0
709
+ got_lock=0
710
+ while :; do
711
+ if mkdir "$LOCK" 2>/dev/null; then got_lock=1; break; fi
712
+ # A stale lock is removed, then this round still sleeps and counts: if the removal fails (a lock owned
713
+ # by another user), the loop ends at lock_wait instead of spinning forever.
714
+ if [ -n "$(find "$LOCK" -maxdepth 0 -mmin +60 2>/dev/null)" ]; then
715
+ rmdir "$LOCK" 2>/dev/null || true
716
+ fi
717
+ if [ "$waited" -ge "$lock_wait" ]; then break; fi
718
+ if [ "$waited" -eq 0 ]; then echo "[gate] pytest: waiting for another repo's pytest gate ($LOCK)"; fi
719
+ sleep 1
720
+ waited=$((waited + 1))
721
+ done
722
+ if [ "$got_lock" -eq 1 ]; then
723
+ trap 'rmdir "$LOCK" 2>/dev/null || true' EXIT INT TERM
724
+ # shellcheck disable=SC2086 # CADDIS_GATE_PYTEST_ARGS is meant to word-split
725
+ py_gate pytest -q ${CADDIS_GATE_PYTEST_ARGS:-}
726
+ rmdir "$LOCK" 2>/dev/null || true
727
+ trap - EXIT INT TERM
728
+ else
729
+ echo "[gate] pytest: another pytest gate holds $LOCK for over ${lock_wait}s - cannot run, so the gate fails" >&2
730
+ fail=1
731
+ fi
732
+ else
733
+ echo "[skip] pytest: runs in CI (set CADDIS_GATE_PYTEST=1 to run it here)"
734
+ fi
693
735
  fi
694
736
  fi
695
737
  if [ -f "package.json" ] && command -v npm >/dev/null 2>&1; then
@@ -19,6 +19,7 @@ agy Stop stdin (camelCase protojson):
19
19
  """
20
20
  import json
21
21
  import os
22
+ import subprocess
22
23
  import sys
23
24
  from datetime import datetime, timezone
24
25
 
@@ -27,6 +28,30 @@ def _artifact_root(root: str) -> str:
27
28
  return os.path.join(str(root), ".caddis")
28
29
 
29
30
 
31
+ def _repo_root(path: str):
32
+ """The git top-level folder that holds `path`, or None when `path` is not inside a repository.
33
+
34
+ agy can run with its working folder in a SUB-folder of the repo (a monorepo app). The artifact
35
+ folder belongs at the repo root, where the project's ignore rule (`/.caddis/`) covers it. Creating
36
+ it in the sub-folder left untracked files that every lane had to delete before staging
37
+ (UI-upgrade run, 2026-10-08).
38
+ """
39
+ try:
40
+ done = subprocess.run(
41
+ ["git", "-C", str(path), "rev-parse", "--show-toplevel"],
42
+ capture_output=True, text=True, timeout=5,
43
+ )
44
+ top = done.stdout.strip()
45
+ return os.path.normpath(top) if done.returncode == 0 and top else None
46
+ except Exception:
47
+ return None
48
+
49
+
50
+ def _user_artifact_root() -> str:
51
+ """Where usage goes when the workspace is in no repository: the user's own folder, never the cwd."""
52
+ return _artifact_root(os.path.expanduser("~"))
53
+
54
+
30
55
 
31
56
  def _hook_note(feature, exc, root=None):
32
57
  """Record a swallowed failure in the caddis hook ledger. Never raises, never prints.
@@ -82,13 +107,22 @@ def main() -> None:
82
107
  return
83
108
  try:
84
109
  roots = _workspace_roots(data) or [os.getcwd()]
110
+ # A workspace folder may sit inside a repo: the artifact dir is the REPO ROOT's, not the sub-folder's.
111
+ resolved = [(_repo_root(r) or str(r)) for r in roots]
85
112
  # Write the record to every root that ALREADY has a .caddis dir, rather than to
86
113
  # whichever root happened to be first. A multi-root session's usage belongs to each
87
114
  # repo that participates in it, and creating .caddis in a repo that never opted in
88
115
  # would be caddis littering someone else's tree.
89
- targets = [_artifact_root(r) for r in roots if os.path.isdir(_artifact_root(r))]
116
+ targets = []
117
+ for root in resolved:
118
+ art = _artifact_root(root)
119
+ if os.path.isdir(art) and art not in targets:
120
+ targets.append(art)
90
121
  if not targets:
91
- targets = [_artifact_root(roots[0])] # single-root default: unchanged behaviour
122
+ # Nothing has opted in. In a repo, the first root's repo root is the default. Outside any
123
+ # repo there is no project to attach usage to, so it goes to the user's folder, not the cwd.
124
+ first_repo = _repo_root(roots[0])
125
+ targets = [_artifact_root(first_repo) if first_repo else _user_artifact_root()]
92
126
  record = {
93
127
  "ts": datetime.now(timezone.utc).isoformat(),
94
128
  "event": "session_end",
@@ -70,7 +70,9 @@ In either route, derive node and link IDs from immutable business keys. Preserve
70
70
  8. **Short titles**: titles fit one line at column width. Long descriptions belong in subtitles or member lists.
71
71
  9. **Fixed node dimensions**: node kinds use fixed box sizes so font rendering does not shift layout.
72
72
  10. **Reduced motion**: removes all animations while preserving static borders and status glows.
73
- 11. **Thin links, dotted canvas**: links are hairlines (about 0.8 px, with a 6 px arrowhead) and the canvas background is faint dots, not a grid. Both come from tokens (`--edge-w`, `--dot`, ...), so an app can change them with one CSS line; see `references/design-language.md`.
73
+ 11. **The lane is the owner**: a lane is the app, team or system that owns its row. Never add a column that repeats the owner (an "Apps" or "Owner" column); it adds a block between the real stages and the pages and says nothing new.
74
+ 12. **Draw a shared node once**: when several rows use the same database, job or table, draw one node, centre it on the rows it serves, and link it to each node downstream. A chip (`kind: "chip"` with `ref`) is only for a node that lives in another lane, never a copy inside the same lane.
75
+ 13. **Thin links, dotted canvas**: links are hairlines (about 0.8 px, with a 6 px arrowhead) and the canvas background is faint dots, not a grid. Both come from tokens (`--edge-w`, `--dot`, ...), so an app can change them with one CSS line; see `references/design-language.md`.
74
76
 
75
77
  ## Model summary
76
78
 
@@ -21,7 +21,7 @@ You can start from these base states and adjust words to match your domain:
21
21
 
22
22
  - **Example file:** `examples/lineage.json`
23
23
  - **Scenario:** Trace data assets from ingestion sources through storage, pipelines, transformations, to consuming dashboards and applications.
24
- - **Typical columns:** Source, Host, Database, Pipeline, Tables, Refresher, Views, App, Dashboard.
24
+ - **Typical columns:** Source, Host, Database, Pipeline, Tables, Refresher, Views, Dashboard. Do not add an App or Owner column: the lane is the app (ux-rules 11).
25
25
  - **Typical lanes:** Customer Portal, Order Service, Data Warehouse, Reporting Platform. A lane without documentation uses `gap: true`.
26
26
  - **Node count guideline:** 15 to 30 nodes.
27
27
  - **Recommended inspector relations:**
@@ -1,8 +1,8 @@
1
1
  # UX and design rules
2
2
 
3
- The 10 rules ensure flow-studio pages remain readable, informative, accessible, and fast.
3
+ The 12 rules ensure flow-studio pages remain readable, informative, accessible, and fast.
4
4
 
5
- ## The 10 rules
5
+ ## The 12 rules
6
6
 
7
7
  ### 1. One screen
8
8
  - **Rule statement:** The full flow fits on one screen without requiring panning in the initial default view.
@@ -66,6 +66,20 @@ The 10 rules ensure flow-studio pages remain readable, informative, accessible,
66
66
 
67
67
  ---
68
68
 
69
+ ### 11. The lane is the owner
70
+ - **Rule statement:** A lane names the app, team or system that owns its row. No column repeats the owner.
71
+ - **Rationale:** An "Apps" column between the real stages and the pages adds a block that says nothing new. In the estate lineage it sat between the reporting tables and the pages, and the owner asked why it was there (2026-10-09).
72
+ - **Pre-flight checklist question:** Does any column hold one node per lane that only repeats the lane title?
73
+ - **Common mistake & fix:** Adding an "App" column because the pattern list showed one. *Fix:* Remove the column. Link the stage that feeds a page straight to the page; a page that reads no table stands alone.
74
+
75
+ ### 12. Draw a shared node once
76
+ - **Rule statement:** A database, job or table that several rows share is one node, centred on the rows it serves, with a link to each node downstream. A chip is only for a node that lives in another lane.
77
+ - **Rationale:** Copying a shared node into every row hides that it is shared and makes the reader compare copies. In the app lineage page, a database repeated in every source row looked like several databases (2026-10-09).
78
+ - **Pre-flight checklist question:** Does any node id appear as a chip inside the lane that already holds the real node?
79
+ - **Common mistake & fix:** One row per source, each with its own copy of the host, database and job. *Fix:* Draw each distinct node once, place it at the mean position of its rows, move it down if it would touch its neighbour, and de-duplicate links by their two ends.
80
+
81
+ ---
82
+
69
83
  ## Pre-flight checklist for agents
70
84
 
71
85
  Before presenting a generated flow page, verify:
@@ -77,6 +91,8 @@ Before presenting a generated flow page, verify:
77
91
  6. **Accessible names:** Does every button, input, tab, and control have an `aria-label` or visible text?
78
92
  7. **Keyboard navigation:** Can a user Tab through controls and press Escape to close the inspector?
79
93
  8. **Responsive breakdown:** Does the layout degrade cleanly down to 820x900?
94
+ 9. **No owner column:** Does any column only repeat the lane's owner? Remove it (rule 11).
95
+ 10. **Shared nodes once:** Is every shared database, job or table drawn once, not copied per row (rule 12)?
80
96
 
81
97
  ---
82
98
 
@@ -88,4 +104,6 @@ Before presenting a generated flow page, verify:
88
104
  - **Text spill:** Node labels exceed card width. *Fix:* Use short titles (under 24 characters) and set `subtitle` for auxiliary metadata.
89
105
  - **Permanent side panel:** The inspector remains open on load. *Fix:* The inspector must start closed and open only on selection.
90
106
  - **Flat visual hierarchy:** Every card looks identical. *Fix:* Vary card kinds (`card`, `group`, `gateway`, `chip`) and state tones (`ok`, `warn`, `crit`, `accent`).
107
+ - **Owner column:** A column of one node per lane that repeats the lane title. *Fix:* Remove it; the lane is the owner.
108
+ - **Copies of a shared node:** The same database or job drawn in every row. *Fix:* Draw it once and link it to each node downstream.
91
109
  - **Unlabelled controls:** Icon buttons without text lack screen reader descriptions. *Fix:* Provide descriptive `aria-label` attributes on all buttons.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "caddis-extras",
3
- "version": "1.3.41",
3
+ "version": "1.3.42",
4
4
  "description": "caddis-extras — the long-tail skills for the Antigravity (agy) caddis plugin; install alongside caddis for the full set."
5
5
  }
@@ -649,3 +649,41 @@ The methodology in this document was extracted from a real multi-agent UI-polish
649
649
  | Debug | 7 issues catalogued (A–G) | Fix-up plan created |
650
650
 
651
651
  This end-to-end cycle took **~10 sessions** with full artefact trail and zero rework of fundamental design decisions.
652
+
653
+
654
+ ---
655
+
656
+ ## agy as an implementer: what was measured (2026-10-08)
657
+
658
+ A 17-hour run with 14 lanes used agy (Antigravity) as the main implementer. Details: `.caddis/kb/agy-headless-implementer-limits.md` in the caddis source repo. Read the rule first, then the evidence.
659
+
660
+ **Rule:** send agy only small, exact, mechanical cards, test fixes and CI watching. Send design-fidelity work, cross-file refactors and judgment to Codex or Claude. Give Opus only short contract-sized tasks.
661
+
662
+ **Why (measured):**
663
+ - On that machine only the Flash models worked. `gemini-3.1-pro-high` failed with "not supported in the selected location".
664
+ - Small exact cards: 112 of 114 passed. A card to port an app shell from screenshots ran 40 minutes and changed one icon file.
665
+ - In `--print` mode agy started the full test suite as a background task and waited for a notification that never came (35 to 40 minutes lost, twice).
666
+ - On narrow gates it invented statuses, counts and names. 11 of 14 lanes needed up to 3 review rounds.
667
+
668
+ ### The agy card template
669
+
670
+ Every card that goes to agy has these sections. A card without them is not ready.
671
+
672
+ 1. `## Goal`: one sentence, one component or one page.
673
+ 2. `## Files`: a bullet list of every file the card may touch. The gate rejects any change outside it.
674
+ 3. `## Tests first`: the failing tests to write before the code, and the one targeted command that runs them.
675
+ 4. `## Rules for agy` (copy this block as it is):
676
+ - Edit first. Run foreground commands only.
677
+ - Never start a background task. Never wait for a notification.
678
+ - Never run the full test suite. The orchestrator gate runs it.
679
+ - No invented data: no made-up statuses, counts, names or sign-offs. If a fact is missing, say so in the card result.
680
+ - No fake controls. Do not remove a feature to make a test pass.
681
+ - Codes go through the shared formatter.
682
+ 5. `## Done when`: the exact observable result, and "report what you changed and what you could not do".
683
+
684
+ ### Orchestrator rules from the same run
685
+
686
+ - The gate runs the full suite, requires that a NEW commit exists, and stages only source folders.
687
+ - Restore known-noisy files (CRLF snapshot rewrites, screenshots) before checking for changes.
688
+ - Do not let Codex `-s workspace-write` run builds. Files it creates are owned by a sandbox user and can block worktree removal.
689
+ - Do not run `caddis-push` or `caddis update` while any `agy.exe` runs. The release now refuses or skips in that case.
@@ -70,7 +70,9 @@ In either route, derive node and link IDs from immutable business keys. Preserve
70
70
  8. **Short titles**: titles fit one line at column width. Long descriptions belong in subtitles or member lists.
71
71
  9. **Fixed node dimensions**: node kinds use fixed box sizes so font rendering does not shift layout.
72
72
  10. **Reduced motion**: removes all animations while preserving static borders and status glows.
73
- 11. **Thin links, dotted canvas**: links are hairlines (about 0.8 px, with a 6 px arrowhead) and the canvas background is faint dots, not a grid. Both come from tokens (`--edge-w`, `--dot`, ...), so an app can change them with one CSS line; see `references/design-language.md`.
73
+ 11. **The lane is the owner**: a lane is the app, team or system that owns its row. Never add a column that repeats the owner (an "Apps" or "Owner" column); it adds a block between the real stages and the pages and says nothing new.
74
+ 12. **Draw a shared node once**: when several rows use the same database, job or table, draw one node, centre it on the rows it serves, and link it to each node downstream. A chip (`kind: "chip"` with `ref`) is only for a node that lives in another lane, never a copy inside the same lane.
75
+ 13. **Thin links, dotted canvas**: links are hairlines (about 0.8 px, with a 6 px arrowhead) and the canvas background is faint dots, not a grid. Both come from tokens (`--edge-w`, `--dot`, ...), so an app can change them with one CSS line; see `references/design-language.md`.
74
76
 
75
77
  ## Model summary
76
78
 
@@ -21,7 +21,7 @@ You can start from these base states and adjust words to match your domain:
21
21
 
22
22
  - **Example file:** `examples/lineage.json`
23
23
  - **Scenario:** Trace data assets from ingestion sources through storage, pipelines, transformations, to consuming dashboards and applications.
24
- - **Typical columns:** Source, Host, Database, Pipeline, Tables, Refresher, Views, App, Dashboard.
24
+ - **Typical columns:** Source, Host, Database, Pipeline, Tables, Refresher, Views, Dashboard. Do not add an App or Owner column: the lane is the app (ux-rules 11).
25
25
  - **Typical lanes:** Customer Portal, Order Service, Data Warehouse, Reporting Platform. A lane without documentation uses `gap: true`.
26
26
  - **Node count guideline:** 15 to 30 nodes.
27
27
  - **Recommended inspector relations:**
@@ -1,8 +1,8 @@
1
1
  # UX and design rules
2
2
 
3
- The 10 rules ensure flow-studio pages remain readable, informative, accessible, and fast.
3
+ The 12 rules ensure flow-studio pages remain readable, informative, accessible, and fast.
4
4
 
5
- ## The 10 rules
5
+ ## The 12 rules
6
6
 
7
7
  ### 1. One screen
8
8
  - **Rule statement:** The full flow fits on one screen without requiring panning in the initial default view.
@@ -66,6 +66,20 @@ The 10 rules ensure flow-studio pages remain readable, informative, accessible,
66
66
 
67
67
  ---
68
68
 
69
+ ### 11. The lane is the owner
70
+ - **Rule statement:** A lane names the app, team or system that owns its row. No column repeats the owner.
71
+ - **Rationale:** An "Apps" column between the real stages and the pages adds a block that says nothing new. In the estate lineage it sat between the reporting tables and the pages, and the owner asked why it was there (2026-10-09).
72
+ - **Pre-flight checklist question:** Does any column hold one node per lane that only repeats the lane title?
73
+ - **Common mistake & fix:** Adding an "App" column because the pattern list showed one. *Fix:* Remove the column. Link the stage that feeds a page straight to the page; a page that reads no table stands alone.
74
+
75
+ ### 12. Draw a shared node once
76
+ - **Rule statement:** A database, job or table that several rows share is one node, centred on the rows it serves, with a link to each node downstream. A chip is only for a node that lives in another lane.
77
+ - **Rationale:** Copying a shared node into every row hides that it is shared and makes the reader compare copies. In the app lineage page, a database repeated in every source row looked like several databases (2026-10-09).
78
+ - **Pre-flight checklist question:** Does any node id appear as a chip inside the lane that already holds the real node?
79
+ - **Common mistake & fix:** One row per source, each with its own copy of the host, database and job. *Fix:* Draw each distinct node once, place it at the mean position of its rows, move it down if it would touch its neighbour, and de-duplicate links by their two ends.
80
+
81
+ ---
82
+
69
83
  ## Pre-flight checklist for agents
70
84
 
71
85
  Before presenting a generated flow page, verify:
@@ -77,6 +91,8 @@ Before presenting a generated flow page, verify:
77
91
  6. **Accessible names:** Does every button, input, tab, and control have an `aria-label` or visible text?
78
92
  7. **Keyboard navigation:** Can a user Tab through controls and press Escape to close the inspector?
79
93
  8. **Responsive breakdown:** Does the layout degrade cleanly down to 820x900?
94
+ 9. **No owner column:** Does any column only repeat the lane's owner? Remove it (rule 11).
95
+ 10. **Shared nodes once:** Is every shared database, job or table drawn once, not copied per row (rule 12)?
80
96
 
81
97
  ---
82
98
 
@@ -88,4 +104,6 @@ Before presenting a generated flow page, verify:
88
104
  - **Text spill:** Node labels exceed card width. *Fix:* Use short titles (under 24 characters) and set `subtitle` for auxiliary metadata.
89
105
  - **Permanent side panel:** The inspector remains open on load. *Fix:* The inspector must start closed and open only on selection.
90
106
  - **Flat visual hierarchy:** Every card looks identical. *Fix:* Vary card kinds (`card`, `group`, `gateway`, `chip`) and state tones (`ok`, `warn`, `crit`, `accent`).
107
+ - **Owner column:** A column of one node per lane that repeats the lane title. *Fix:* Remove it; the lane is the owner.
108
+ - **Copies of a shared node:** The same database or job drawn in every row. *Fix:* Draw it once and link it to each node downstream.
91
109
  - **Unlabelled controls:** Icon buttons without text lack screen reader descriptions. *Fix:* Provide descriptive `aria-label` attributes on all buttons.
@@ -1,8 +1,8 @@
1
1
  {
2
- "poolVersion": "1.3.136",
2
+ "poolVersion": "1.3.138",
3
3
  "bundles": {
4
- "antigravity-plugin": "1.3.136",
5
- "antigravity-plugin-extras": "1.3.41",
4
+ "antigravity-plugin": "1.3.138",
5
+ "antigravity-plugin-extras": "1.3.42",
6
6
  "codex": "unknown"
7
7
  }
8
8
  }
package/dist/cli.js CHANGED
@@ -5761,15 +5761,77 @@ var claudeAdapter = {
5761
5761
  // src/agents/agy.ts
5762
5762
  init_exec();
5763
5763
  init_which();
5764
- init_pkg();
5765
5764
  import { readFileSync as readFileSync2, existsSync as existsSync2, mkdtempSync, cpSync, rmSync } from "fs";
5766
5765
  import os from "os";
5767
5766
  import path2 from "path";
5767
+
5768
+ // src/util/agyProcs.ts
5769
+ init_exec();
5770
+ function parseTasklistCsv(stdout) {
5771
+ const pids = [];
5772
+ for (const line2 of stdout.split(/\r?\n/)) {
5773
+ const match = /^"agy\.exe","(\d+)"/i.exec(line2.trim());
5774
+ if (match) pids.push(Number(match[1]));
5775
+ }
5776
+ return pids;
5777
+ }
5778
+ function parsePgrep(stdout) {
5779
+ return stdout.split(/\r?\n/).map((line2) => line2.trim()).filter((line2) => /^\d+$/.test(line2)).map(Number);
5780
+ }
5781
+ async function listAgyPids() {
5782
+ if (process.platform === "win32") {
5783
+ const result2 = await run("tasklist", ["/FI", "IMAGENAME eq agy.exe", "/FO", "CSV", "/NH"], { timeout: 2e4 });
5784
+ return result2.ok ? parseTasklistCsv(result2.stdout) : [];
5785
+ }
5786
+ const result = await run("pgrep", ["-x", "agy"], { timeout: 1e4 });
5787
+ return result.ok ? parsePgrep(result.stdout) : [];
5788
+ }
5789
+
5790
+ // src/agents/agy.ts
5791
+ init_pkg();
5768
5792
  var BIN2 = "agy";
5769
5793
  var BUNDLE = "antigravity-plugin";
5770
5794
  var PLUGIN2 = "caddis";
5771
5795
  var EXTRAS_BUNDLE = "antigravity-plugin-extras";
5772
5796
  var EXTRAS_PLUGIN = "caddis-extras";
5797
+ var REQUIRED_PLUGIN_FILES = ["plugin.json", "hooks.json", "guard_agy.py"];
5798
+ var ALLOW_RUNNING_ENV = "CADDIS_AGY_ALLOW_RUNNING";
5799
+ function pluginDirFor(home = os.homedir(), plugin = PLUGIN2) {
5800
+ return path2.join(home, ".gemini", "config", "plugins", plugin);
5801
+ }
5802
+ function missingPluginFiles(home = os.homedir()) {
5803
+ const dir = pluginDirFor(home);
5804
+ return REQUIRED_PLUGIN_FILES.filter((file) => !existsSync2(path2.join(dir, file)));
5805
+ }
5806
+ function backupInstalledPlugin(home) {
5807
+ const dir = pluginDirFor(home);
5808
+ if (!existsSync2(dir)) return null;
5809
+ const holder = mkdtempSync(path2.join(os.tmpdir(), "caddis-agy-backup-"));
5810
+ const copy = path2.join(holder, PLUGIN2);
5811
+ cpSync(dir, copy, { recursive: true });
5812
+ let keep = false;
5813
+ return {
5814
+ location: copy,
5815
+ // Returns null when the old plugin is back, else the reason it could not be put back.
5816
+ restore: () => {
5817
+ try {
5818
+ rmSync(dir, { recursive: true, force: true });
5819
+ cpSync(copy, dir, { recursive: true });
5820
+ return null;
5821
+ } catch (error) {
5822
+ keep = true;
5823
+ return error instanceof Error ? error.message : String(error);
5824
+ }
5825
+ },
5826
+ cleanup: () => {
5827
+ if (keep) return;
5828
+ try {
5829
+ rmSync(holder, { recursive: true, force: true });
5830
+ } catch {
5831
+ }
5832
+ }
5833
+ };
5834
+ }
5773
5835
  function pluginManifestPath(home = os.homedir(), plugin = PLUGIN2) {
5774
5836
  return path2.join(home, ".gemini", "config", "plugins", plugin, "plugin.json");
5775
5837
  }
@@ -5890,7 +5952,21 @@ async function drive2(_action, options) {
5890
5952
  message: "dry run \u2014 nothing executed"
5891
5953
  };
5892
5954
  }
5955
+ if (process.env[ALLOW_RUNNING_ENV] !== "1") {
5956
+ const running = await listAgyPids();
5957
+ if (running.length > 0) {
5958
+ return {
5959
+ ok: false,
5960
+ skipped: false,
5961
+ steps: [],
5962
+ message: `agy is running (PID ${running.join(", ")}). Installing now can empty the caddis plugin under it, and agy then refuses every edit. Stop agy and run this again, or set ${ALLOW_RUNNING_ENV}=1 if you know no agy holds the plugin folder.`
5963
+ };
5964
+ }
5965
+ }
5966
+ const home = options.home ?? os.homedir();
5967
+ const backup = backupInstalledPlugin(home);
5893
5968
  const cleanups = [];
5969
+ if (backup) cleanups.push(backup.cleanup);
5894
5970
  try {
5895
5971
  const steps3 = [];
5896
5972
  for (const step of planned) {
@@ -5899,6 +5975,23 @@ async function drive2(_action, options) {
5899
5975
  cleanups.push(staged.cleanup);
5900
5976
  const execArgs = [...step.args.slice(0, -1), staged.installPath];
5901
5977
  const result = await run(step.cmd, execArgs);
5978
+ if (step.bundleDir === bundle) {
5979
+ const missing = missingPluginFiles(home);
5980
+ if (missing.length > 0) {
5981
+ let outcome = "there was nothing to restore";
5982
+ if (backup) {
5983
+ const failure = backup.restore();
5984
+ outcome = failure === null ? "the previous plugin was restored" : `the restore FAILED (${failure}); the old plugin is kept at ${backup.location}`;
5985
+ }
5986
+ steps3.push({ command, ok: false, code: result.code, output: `missing after the install: ${missing.join(", ")}` });
5987
+ return {
5988
+ ok: false,
5989
+ skipped: false,
5990
+ steps: steps3,
5991
+ message: `the caddis plugin is incomplete after the install (missing ${missing.join(", ")}); ` + outcome
5992
+ };
5993
+ }
5994
+ }
5902
5995
  steps3.push({
5903
5996
  command,
5904
5997
  ok: result.ok,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caddis/cli",
3
- "version": "0.4.50",
3
+ "version": "0.4.52",
4
4
  "description": "Install, update and diagnose the caddis dev harness across every coding agent you use (Claude Code, agy/Antigravity, and more).",
5
5
  "keywords": [
6
6
  "caddis",