@caddis/cli 0.4.52 → 0.4.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundles/antigravity-plugin/CADDIS-GLOSSARY.md +1 -0
- package/bundles/antigravity-plugin/plugin.json +1 -1
- package/bundles/antigravity-plugin/repo_freshness.py +132 -5
- package/bundles/antigravity-plugin/scripts/caddis_checkout_guard.py +223 -0
- package/bundles/antigravity-plugin/scripts/caddis_docs_pr.py +284 -0
- package/bundles/antigravity-plugin/scripts/caddis_exit.py +2 -0
- package/bundles/antigravity-plugin/scripts/caddis_frontmatter.py +1 -0
- package/bundles/antigravity-plugin/scripts/caddis_gate.py +226 -195
- package/bundles/antigravity-plugin/scripts/caddis_glossary.py +3 -3
- package/bundles/antigravity-plugin/scripts/caddis_governor.py +48 -4
- package/bundles/antigravity-plugin/scripts/caddis_inventory.py +38 -0
- package/bundles/antigravity-plugin/scripts/caddis_lanes.py +294 -127
- package/bundles/antigravity-plugin/scripts/caddis_lineage_publish.py +458 -0
- package/bundles/antigravity-plugin/scripts/caddis_merge_chain.py +237 -0
- package/bundles/antigravity-plugin/scripts/caddis_notify.py +121 -0
- package/bundles/antigravity-plugin/scripts/caddis_prod_check.py +380 -0
- package/bundles/antigravity-plugin/scripts/caddis_redact.py +99 -0
- package/bundles/antigravity-plugin/scripts/caddis_relay_git.py +2 -2
- package/bundles/antigravity-plugin/scripts/caddis_review.py +1309 -0
- package/bundles/antigravity-plugin/scripts/caddis_roadmap.py +2056 -0
- package/bundles/antigravity-plugin/scripts/caddis_roadmap_run.py +2572 -0
- package/bundles/antigravity-plugin/scripts/caddis_routing.py +3 -2
- package/bundles/antigravity-plugin/scripts/caddis_rows.py +476 -0
- package/bundles/antigravity-plugin/scripts/caddis_spawn.py +8 -9
- package/bundles/antigravity-plugin/scripts/caddis_spend.py +415 -0
- package/bundles/antigravity-plugin/scripts/caddis_tidy.py +357 -20
- package/bundles/antigravity-plugin/scripts/caddis_todo.py +55 -0
- package/bundles/antigravity-plugin/scripts/caddis_workstreams.py +178 -19
- package/bundles/antigravity-plugin/scripts/caddis_worktree_close.py +349 -0
- package/bundles/antigravity-plugin/scripts/check_doc_coverage.py +0 -56
- package/bundles/antigravity-plugin/scripts/claudster_doctor.py +1 -1
- package/bundles/antigravity-plugin/scripts/enforcement_inventory.py +0 -1
- package/bundles/antigravity-plugin/scripts/oss_review.py +2 -2
- package/bundles/antigravity-plugin/scripts/roadmap-recheck.ps1 +89 -0
- package/bundles/antigravity-plugin/scripts/roadmap-run.ps1 +88 -0
- package/bundles/antigravity-plugin/scripts/setup_project_ai.py +43 -1
- package/bundles/antigravity-plugin/session_end_agy.py +3 -3
- package/bundles/antigravity-plugin/skills/_registry.md +2 -6
- package/bundles/antigravity-plugin/skills/best-practices/agents/best-practices-referencer.md +1 -1
- package/bundles/antigravity-plugin/skills/best-practices/agents/codebase-context-builder.md +1 -1
- package/bundles/antigravity-plugin/skills/best-practices/agents/task-intent-analyzer.md +1 -1
- package/bundles/antigravity-plugin/skills/catchup/SKILL.md +7 -0
- package/bundles/antigravity-plugin/skills/cloud-phases/SKILL.md +1 -1
- package/bundles/antigravity-plugin/skills/cross-review/SKILL.md +5 -2
- package/bundles/antigravity-plugin/skills/feature-plan/SKILL.md +29 -83
- package/bundles/antigravity-plugin/skills/handoff/SKILL.md +77 -48
- package/bundles/antigravity-plugin/skills/implement/SKILL.md +87 -147
- package/bundles/antigravity-plugin/skills/migrate-dir/SKILL.md +1 -1
- package/bundles/antigravity-plugin/skills/mmr/SKILL.md +5 -2
- package/bundles/antigravity-plugin/skills/office-documents-official/SKILL.md +24 -0
- package/bundles/antigravity-plugin/skills/park/SKILL.md +0 -1
- package/bundles/antigravity-plugin/skills/pr-merge/SKILL.md +8 -14
- package/bundles/antigravity-plugin/skills/prd/SKILL.md +2 -10
- package/bundles/antigravity-plugin/skills/roadmap/SKILL.md +259 -0
- package/bundles/antigravity-plugin/skills/roadmap/assets/plan-resume-prompt.md +77 -0
- package/bundles/antigravity-plugin/skills/roadmap/assets/render-check.template.mjs +68 -0
- package/bundles/antigravity-plugin/skills/roadmap/assets/review-protocol.md +78 -0
- package/bundles/antigravity-plugin/skills/roadmap/assets/roadmap.template.md +32 -0
- package/bundles/antigravity-plugin/skills/roadmap/assets/value-check.template.py +161 -0
- package/bundles/antigravity-plugin/skills/ship/SKILL.md +14 -14
- package/bundles/antigravity-plugin/skills/ship-merge/SKILL.md +6 -0
- package/bundles/antigravity-plugin/skills/ship-pr/SKILL.md +33 -14
- package/bundles/antigravity-plugin/skills/spawn-session/SKILL.md +4 -8
- package/bundles/antigravity-plugin/warm_start_agy.py +1 -3
- package/bundles/antigravity-plugin-extras/plugin.json +1 -1
- package/bundles/antigravity-plugin-extras/skills/_registry.md +1 -2
- package/bundles/antigravity-plugin-extras/skills/agent-orchestration/SKILL.md +5 -5
- package/bundles/antigravity-plugin-extras/skills/cross-review/SKILL.md +5 -2
- package/bundles/antigravity-plugin-extras/skills/runner-move/SKILL.md +214 -0
- package/bundles/codex/.codex/skills/docs/office-documents-official/SKILL.md +24 -0
- package/bundles/codex/.codex/skills/workflow/best-practices/agents/best-practices-referencer.md +1 -1
- package/bundles/codex/.codex/skills/workflow/best-practices/agents/codebase-context-builder.md +1 -1
- package/bundles/codex/.codex/skills/workflow/best-practices/agents/task-intent-analyzer.md +1 -1
- package/bundles/codex/.codex/skills/workflow/roadmap/SKILL.md +259 -0
- package/bundles/codex/.codex/skills/workflow/roadmap/assets/plan-resume-prompt.md +77 -0
- package/bundles/codex/.codex/skills/workflow/roadmap/assets/render-check.template.mjs +68 -0
- package/bundles/codex/.codex/skills/workflow/roadmap/assets/review-protocol.md +78 -0
- package/bundles/codex/.codex/skills/workflow/roadmap/assets/roadmap.template.md +32 -0
- package/bundles/codex/.codex/skills/workflow/roadmap/assets/value-check.template.py +161 -0
- package/bundles/codex/AGENTS.md +5 -4
- package/bundles/manifest.json +3 -3
- package/package.json +1 -1
- package/bundles/antigravity-plugin/skills/advisory-hub/SKILL.md +0 -233
- package/bundles/antigravity-plugin/skills/advisory-hub/reference/context-template.md +0 -214
- package/bundles/antigravity-plugin/skills/advisory-hub/reference/hub-handbook.md +0 -233
- package/bundles/antigravity-plugin/skills/advisory-hub/reference/report-contract.md +0 -94
- package/bundles/antigravity-plugin/skills/advisory-hub/reference/succession.md +0 -118
- package/bundles/antigravity-plugin/skills/resume/SKILL.md +0 -6
- package/bundles/antigravity-plugin/skills/review/SKILL.md +0 -6
- package/bundles/antigravity-plugin/skills/spawn-hub/SKILL.md +0 -341
- package/bundles/antigravity-plugin/skills/statusline/SKILL.md +0 -6
- package/bundles/antigravity-plugin/skills/validate-phase/SKILL.md +0 -143
- package/bundles/antigravity-plugin-extras/skills/data-lineage/SKILL.md +0 -158
- package/bundles/antigravity-plugin-extras/skills/data-lineage/scripts/convert_doc_to_confluence.py +0 -1430
- package/bundles/antigravity-plugin-extras/skills/golden-plan/SKILL.md +0 -586
- package/bundles/codex/.codex/skills/workflow/advisory-hub/SKILL.md +0 -233
- package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/context-template.md +0 -214
- package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/hub-handbook.md +0 -233
- package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/report-contract.md +0 -94
- package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/succession.md +0 -118
|
@@ -44,6 +44,7 @@ import json
|
|
|
44
44
|
import re
|
|
45
45
|
import subprocess
|
|
46
46
|
import sys
|
|
47
|
+
import time
|
|
47
48
|
from dataclasses import dataclass, field
|
|
48
49
|
from pathlib import Path
|
|
49
50
|
|
|
@@ -105,6 +106,7 @@ class Inventory:
|
|
|
105
106
|
comms_open: list[dict] = field(default_factory=list)
|
|
106
107
|
scripts: list[dict] = field(default_factory=list)
|
|
107
108
|
stale_artifacts: list[dict] = field(default_factory=list)
|
|
109
|
+
stale_plans: list[dict] = field(default_factory=list)
|
|
108
110
|
environment_map: str = "missing"
|
|
109
111
|
tests: str = "not measured — run the suite, never quote a remembered number"
|
|
110
112
|
|
|
@@ -275,6 +277,37 @@ def stale_artifacts(root: Path) -> list[dict]:
|
|
|
275
277
|
return out
|
|
276
278
|
|
|
277
279
|
|
|
280
|
+
def stale_plans(root: Path, days: int = 14) -> list[dict]:
|
|
281
|
+
"""Top-level plans in `status: doing` or `status: current` whose last git commit is older
|
|
282
|
+
than `days` days."""
|
|
283
|
+
d = root / ART / "plans"
|
|
284
|
+
if not d.is_dir() or not (root / ".git").exists():
|
|
285
|
+
return []
|
|
286
|
+
now = time.time()
|
|
287
|
+
cutoff = days * 86400
|
|
288
|
+
out: list[dict] = []
|
|
289
|
+
for f in sorted(d.glob("*.md")):
|
|
290
|
+
if f.name.lower() == "readme.md":
|
|
291
|
+
continue
|
|
292
|
+
fm = frontmatter(f)
|
|
293
|
+
status = (fm.get("status") or "").strip()
|
|
294
|
+
if status.lower() not in ("doing", "current"):
|
|
295
|
+
continue
|
|
296
|
+
rel = f"{ART}/plans/{f.name}"
|
|
297
|
+
ts = _run(["git", "log", "-1", "--format=%ct", "--", rel], root)
|
|
298
|
+
if not ts.isdigit():
|
|
299
|
+
continue
|
|
300
|
+
age_seconds = now - int(ts)
|
|
301
|
+
if age_seconds > cutoff:
|
|
302
|
+
out.append({
|
|
303
|
+
"file": rel,
|
|
304
|
+
"title": title_of(f),
|
|
305
|
+
"status": status,
|
|
306
|
+
"days": int(age_seconds // 86400),
|
|
307
|
+
})
|
|
308
|
+
return out
|
|
309
|
+
|
|
310
|
+
|
|
278
311
|
def collect(root: Path, with_tests: bool = False) -> Inventory:
|
|
279
312
|
inv = Inventory(repo=root.name)
|
|
280
313
|
inv.branch = _run(["git", "rev-parse", "--abbrev-ref", "HEAD"], root)
|
|
@@ -288,6 +321,7 @@ def collect(root: Path, with_tests: bool = False) -> Inventory:
|
|
|
288
321
|
inv.comms_open = scan_comms(root)
|
|
289
322
|
inv.scripts = scan_scripts(root)
|
|
290
323
|
inv.stale_artifacts = stale_artifacts(root)
|
|
324
|
+
inv.stale_plans = stale_plans(root)
|
|
291
325
|
inv.environment_map = environment_map_state(root)
|
|
292
326
|
if with_tests:
|
|
293
327
|
# Run it. A handover once claimed 20 tests when there were 19, and the incoming Hub found
|
|
@@ -402,6 +436,10 @@ def render_md(inv: Inventory) -> str:
|
|
|
402
436
|
"Each of these was committed BEFORE the script that builds it last changed, so it may",
|
|
403
437
|
"describe a state that no longer exists. Rebuild before quoting any of them.", ""]
|
|
404
438
|
L += _table(inv.stale_artifacts, [("Artefact", "artifact"), ("Built by", "generator")])
|
|
439
|
+
if inv.stale_plans:
|
|
440
|
+
L += ["## Stale plans (status doing/current, no commit in 14 days)", ""]
|
|
441
|
+
L += _table(inv.stale_plans, [("File", "file"), ("Title", "title"),
|
|
442
|
+
("Status", "status"), ("Days", "days")])
|
|
405
443
|
for kind, rows in inv.artifacts.items():
|
|
406
444
|
if not rows:
|
|
407
445
|
continue
|
|
@@ -34,6 +34,8 @@ except ModuleNotFoundError as exc:
|
|
|
34
34
|
from caddis_heavy_slot import HeavySlotBusy, heavy_slot
|
|
35
35
|
|
|
36
36
|
import caddis_routing
|
|
37
|
+
import caddis_redact
|
|
38
|
+
import caddis_spend
|
|
37
39
|
|
|
38
40
|
if hasattr(sys.stdout, "reconfigure"):
|
|
39
41
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
@@ -63,8 +65,11 @@ DEFAULT_MODELS = {
|
|
|
63
65
|
"glm": "glm-5.3[1m]",
|
|
64
66
|
"codex-sol": "gpt-5.6-sol",
|
|
65
67
|
"codex-astra": "gpt-6-astra",
|
|
66
|
-
"agy": "gemini-
|
|
68
|
+
"agy": "gemini-4-argon",
|
|
67
69
|
}
|
|
70
|
+
AGY_FALLBACK_MODEL = "gemini-3.8-flash" # used when `agy models` does not offer the default (other teams)
|
|
71
|
+
AGY_PROBE_TIMEOUT_S = 30
|
|
72
|
+
_AGY_MODELS_CACHE: str | None = None
|
|
68
73
|
DEFAULT_EFFORT = {
|
|
69
74
|
"claude": "high",
|
|
70
75
|
"glm": "medium",
|
|
@@ -110,7 +115,9 @@ BUDGET_PATTERNS: dict[str, tuple[str, ...]] = {
|
|
|
110
115
|
r"Quota exceeded\. Check your plan",
|
|
111
116
|
r"workspace is out of credits",
|
|
112
117
|
),
|
|
113
|
-
|
|
118
|
+
# caddis_spend refusing over the global EUR cap: the chain moves on, like a provider quota.
|
|
119
|
+
"agy": (r"RESOURCE_EXHAUSTED.*quota", r"Individual quota reached", r"(?m)^caddis_spend: over cap",
|
|
120
|
+
r"(?m)^caddis_spend: could not lock"),
|
|
114
121
|
}
|
|
115
122
|
CONFIG_PATTERNS: dict[str, tuple[str, ...]] = {
|
|
116
123
|
"claude": (),
|
|
@@ -127,7 +134,9 @@ CONFIG_PATTERNS: dict[str, tuple[str, ...]] = {
|
|
|
127
134
|
r"model is not supported when using Codex",
|
|
128
135
|
r"Model metadata for .* not found",
|
|
129
136
|
),
|
|
130
|
-
"agy": (r"invalid model selection", r"is not recognized as a known model"
|
|
137
|
+
"agy": (r"invalid model selection", r"is not recognized as a known model",
|
|
138
|
+
r"(?m)^caddis_spend: spend cap not configured",
|
|
139
|
+
r"(?m)^caddis_spend: could not check the spend cap"),
|
|
131
140
|
}
|
|
132
141
|
LOAD_WORDING = (
|
|
133
142
|
r"(?i)\bcpu\s+burner",
|
|
@@ -559,6 +568,12 @@ def _windows_process_names(pids: list[int]) -> dict[int, str]:
|
|
|
559
568
|
return names
|
|
560
569
|
|
|
561
570
|
|
|
571
|
+
def _refused_run(returncode: int, message: str) -> LaneRun:
|
|
572
|
+
"""A lane that never started: the spend cap refused it. pid 0, no time spent."""
|
|
573
|
+
return LaneRun(returncode=returncode, output=message + "\n", timed_out=False, duration_s=0.0,
|
|
574
|
+
pid=0, started=time.time())
|
|
575
|
+
|
|
576
|
+
|
|
562
577
|
def run_lane(
|
|
563
578
|
argv: list[str],
|
|
564
579
|
cwd: Path,
|
|
@@ -570,7 +585,25 @@ def run_lane(
|
|
|
570
585
|
clock=time.monotonic,
|
|
571
586
|
sleep=time.sleep,
|
|
572
587
|
probe=default_probe,
|
|
588
|
+
spend_caller: str = "lane",
|
|
573
589
|
) -> LaneRun:
|
|
590
|
+
# Every agy launch is booked against the global spend cap first (caddis_spend). ci_watch reaches
|
|
591
|
+
# agy through here too, so each call is booked exactly once. Any spend problem refuses the lane
|
|
592
|
+
# (fail closed) and never crashes the unattended chain.
|
|
593
|
+
spend_id = None
|
|
594
|
+
if Path(argv[0]).stem.lower() == "agy":
|
|
595
|
+
spend_home = Path.home()
|
|
596
|
+
try:
|
|
597
|
+
spend_id = caddis_spend.reserve(spend_home, spend_caller, datetime.now(timezone.utc))
|
|
598
|
+
except caddis_spend.NotConfigured:
|
|
599
|
+
return _refused_run(3, caddis_spend.NOT_CONFIGURED_MESSAGE)
|
|
600
|
+
except TimeoutError as exc:
|
|
601
|
+
return _refused_run(3, str(exc))
|
|
602
|
+
except Exception as exc: # an unreadable log or file error: refuse, never crash
|
|
603
|
+
return _refused_run(3, f"caddis_spend: could not check the spend cap: {exc!r}")
|
|
604
|
+
if spend_id is None:
|
|
605
|
+
return _refused_run(1, "caddis_spend: over cap: agy refused ("
|
|
606
|
+
+ caddis_spend.status_line(spend_home, datetime.now(timezone.utc)) + ")")
|
|
574
607
|
job_name = f"Local\\caddis-governor-{os.getpid()}-{secrets.token_hex(8)}" if sys.platform == "win32" else None
|
|
575
608
|
full = governed_argv(argv, job_name)
|
|
576
609
|
if not stdin_prompt:
|
|
@@ -582,15 +615,23 @@ def run_lane(
|
|
|
582
615
|
popen_options = {}
|
|
583
616
|
if sys.platform != "win32":
|
|
584
617
|
popen_options["start_new_session"] = True
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
618
|
+
try:
|
|
619
|
+
proc = subprocess.Popen(
|
|
620
|
+
full,
|
|
621
|
+
cwd=cwd,
|
|
622
|
+
stdin=subprocess.PIPE if stdin_prompt else subprocess.DEVNULL,
|
|
623
|
+
stdout=subprocess.PIPE,
|
|
624
|
+
stderr=subprocess.STDOUT,
|
|
625
|
+
env=env,
|
|
626
|
+
**popen_options,
|
|
627
|
+
)
|
|
628
|
+
except OSError:
|
|
629
|
+
if spend_id is not None: # nothing started, nothing was spent
|
|
630
|
+
try:
|
|
631
|
+
caddis_spend.settle(spend_home, spend_id, (0, 0), datetime.now(timezone.utc))
|
|
632
|
+
except Exception:
|
|
633
|
+
pass
|
|
634
|
+
raise
|
|
594
635
|
timed_out = stalled = False
|
|
595
636
|
idle_seconds = 0
|
|
596
637
|
processes = None
|
|
@@ -682,9 +723,24 @@ def run_lane(
|
|
|
682
723
|
kill_governed(proc)
|
|
683
724
|
raise
|
|
684
725
|
|
|
726
|
+
# Mask key values before any caller sees the lane's output (printed tails, ci-watch reports); packets are
|
|
727
|
+
# masked again in write_packet, which also covers the gate and review tails.
|
|
728
|
+
output = caddis_redact.redact(b"".join(chunks).decode("utf-8", errors="replace"),
|
|
729
|
+
caddis_redact.load_secrets(Path.home()))
|
|
730
|
+
if spend_id is not None:
|
|
731
|
+
# Tokens only from JSON mode: in text mode the model's own lines could pose as a usage document.
|
|
732
|
+
if proc.returncode == 127:
|
|
733
|
+
counts = (0, 0) # the governor found no agy binary: nothing ran, nothing was spent
|
|
734
|
+
else:
|
|
735
|
+
counts = caddis_spend.tokens_from_output(output) if caddis_spend.json_mode(argv) else None
|
|
736
|
+
try:
|
|
737
|
+
caddis_spend.settle(spend_home, spend_id, counts, datetime.now(timezone.utc),
|
|
738
|
+
duration_s=clock() - monotonic_started)
|
|
739
|
+
except Exception as exc: # the pending reserve keeps counting; never lose the lane's result
|
|
740
|
+
output += f"\ncaddis_spend: could not settle {spend_id}: {exc!r}\n"
|
|
685
741
|
return LaneRun(
|
|
686
742
|
returncode=proc.returncode,
|
|
687
|
-
output=
|
|
743
|
+
output=output,
|
|
688
744
|
timed_out=timed_out,
|
|
689
745
|
duration_s=clock() - monotonic_started,
|
|
690
746
|
pid=proc.pid,
|
|
@@ -966,8 +1022,11 @@ def run_dir(repo_root: Path, plan_stem: str) -> Path:
|
|
|
966
1022
|
def write_packet(path: Path, packet: dict) -> Path:
|
|
967
1023
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
968
1024
|
temporary = path.with_name(path.name + ".tmp")
|
|
1025
|
+
# Every string in the packet (gate and review tails come from model-written code) is masked here,
|
|
1026
|
+
# the one place all packets pass through.
|
|
1027
|
+
safe = caddis_redact.redact_obj(packet, caddis_redact.load_secrets(Path.home()))
|
|
969
1028
|
temporary.write_text(
|
|
970
|
-
json.dumps(
|
|
1029
|
+
json.dumps(safe, indent=2, sort_keys=True) + "\n",
|
|
971
1030
|
encoding="utf-8",
|
|
972
1031
|
)
|
|
973
1032
|
os.replace(temporary, path)
|
|
@@ -1003,14 +1062,64 @@ def idle_timeout(cfg: dict, override: int | None = None) -> int:
|
|
|
1003
1062
|
return value
|
|
1004
1063
|
|
|
1005
1064
|
|
|
1006
|
-
def
|
|
1065
|
+
def _probe_agy_models() -> str:
|
|
1066
|
+
"""The text of `agy models` (one model id per line), or "" when agy is missing, fails or prints nothing. Only a
|
|
1067
|
+
non-empty listing is cached for the process: a failed probe is not cached, so a later call asks agy again."""
|
|
1068
|
+
global _AGY_MODELS_CACHE
|
|
1069
|
+
if _AGY_MODELS_CACHE:
|
|
1070
|
+
return _AGY_MODELS_CACHE
|
|
1071
|
+
agy = shutil.which("agy")
|
|
1072
|
+
if not agy:
|
|
1073
|
+
return ""
|
|
1074
|
+
try:
|
|
1075
|
+
proc = subprocess.run(
|
|
1076
|
+
[agy, "models"], capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
1077
|
+
timeout=AGY_PROBE_TIMEOUT_S, check=False, stdin=subprocess.DEVNULL,
|
|
1078
|
+
)
|
|
1079
|
+
except (OSError, subprocess.SubprocessError):
|
|
1080
|
+
return ""
|
|
1081
|
+
listing = (proc.stdout or "") if proc.returncode == 0 else ""
|
|
1082
|
+
if listing.strip():
|
|
1083
|
+
_AGY_MODELS_CACHE = listing
|
|
1084
|
+
return listing
|
|
1085
|
+
|
|
1086
|
+
|
|
1087
|
+
# The agy default is used at three effort levels: ci-watch uses -low, lanes -medium, review -high. Keep it only
|
|
1088
|
+
# when `agy models` offers all three exact ids.
|
|
1089
|
+
AGY_DEFAULT_EFFORT_IDS = ("low", "medium", "high")
|
|
1090
|
+
_AGY_FALLBACK_NOTED = False
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
def _agy_default_model(probe=None) -> str:
|
|
1094
|
+
"""The configured default agy model when `agy models` lists every `<default>-low`, `-medium` and `-high` id
|
|
1095
|
+
exactly, else AGY_FALLBACK_MODEL. The fallback note goes to stderr once per process."""
|
|
1096
|
+
global _AGY_FALLBACK_NOTED
|
|
1097
|
+
probe = probe or _probe_agy_models
|
|
1098
|
+
default = DEFAULT_MODELS["agy"]
|
|
1099
|
+
try:
|
|
1100
|
+
listing = probe()
|
|
1101
|
+
except (OSError, subprocess.SubprocessError):
|
|
1102
|
+
listing = ""
|
|
1103
|
+
offered = {line.split()[0] for line in (listing or "").splitlines() if line.split()} # id, then a name
|
|
1104
|
+
if all(f"{default}-{level}" in offered for level in AGY_DEFAULT_EFFORT_IDS):
|
|
1105
|
+
return default
|
|
1106
|
+
if not _AGY_FALLBACK_NOTED:
|
|
1107
|
+
_AGY_FALLBACK_NOTED = True
|
|
1108
|
+
print(f"[caddis-lanes] agy: {default} not offered here; using {AGY_FALLBACK_MODEL}", file=sys.stderr)
|
|
1109
|
+
return AGY_FALLBACK_MODEL
|
|
1110
|
+
|
|
1111
|
+
|
|
1112
|
+
def resolve_models(cfg: dict, *, probe=None) -> dict[str, str]:
|
|
1007
1113
|
models = DEFAULT_MODELS.copy()
|
|
1008
|
-
|
|
1114
|
+
configured = cfg.get("models", {})
|
|
1115
|
+
for key, value in configured.items():
|
|
1009
1116
|
if key not in LANE_IDS:
|
|
1010
1117
|
raise LaneConfigError(f"unknown lane in [lanes.models]: {key}")
|
|
1011
1118
|
if not isinstance(value, str) or not value:
|
|
1012
1119
|
raise LaneConfigError(f"empty model id for lane {key}")
|
|
1013
1120
|
models[key] = value
|
|
1121
|
+
if "agy" not in configured: # a config pin wins and is never probed
|
|
1122
|
+
models["agy"] = _agy_default_model(probe)
|
|
1014
1123
|
return models
|
|
1015
1124
|
|
|
1016
1125
|
|
|
@@ -1206,48 +1315,125 @@ def next_lane(
|
|
|
1206
1315
|
return candidates[0] if candidates else None
|
|
1207
1316
|
|
|
1208
1317
|
|
|
1209
|
-
|
|
1210
|
-
|
|
1318
|
+
# Why gh waits and agy only summarises (measured 2026-10-15): agy `--print` moves a long command
|
|
1319
|
+
# (`gh pr checks --watch`) into its own background task, reports "root agent idle", then ends that task
|
|
1320
|
+
# on exit and returns no report. agy also does not read stdin in text mode, so any data it needs must
|
|
1321
|
+
# sit inside the prompt. So `gh` always does the waiting, and agy gets one short, tool-free job after a
|
|
1322
|
+
# failure: read the failed log (placed in the prompt) and name the failing job and first error.
|
|
1323
|
+
_CI_SUMMARY_MAX_CHARS = 300
|
|
1324
|
+
_CI_LOG_TAIL_LINES = 80
|
|
1325
|
+
_CI_LOG_TAIL_CHARS = 8000
|
|
1326
|
+
_CI_AGY_TIMEOUT_S = 300
|
|
1327
|
+
_CI_GH_SHORT_TIMEOUT_S = 60
|
|
1328
|
+
_RUN_ID_IN_URL = re.compile(r"/actions/runs/(\d+)")
|
|
1329
|
+
|
|
1330
|
+
|
|
1331
|
+
def _print_ci_watch(result: str, checks: str, merge_state: str, failing: str, watcher: str,
|
|
1332
|
+
summary: str | None = None) -> None:
|
|
1333
|
+
print(f"RESULT: {result}")
|
|
1334
|
+
print(f"CHECKS: {checks}")
|
|
1335
|
+
print(f"MERGE_STATE: {merge_state}")
|
|
1336
|
+
print(f"FAILING: {failing}")
|
|
1337
|
+
print(f"WATCHER: {watcher}")
|
|
1338
|
+
print(f"SUMMARY: {summary or 'none'}")
|
|
1339
|
+
|
|
1340
|
+
|
|
1341
|
+
def _parse_pr_checks(output: str) -> list[tuple[str, str, str]]:
|
|
1342
|
+
"""Read `gh pr checks` rows (tab-separated: name, state, elapsed, url) as (name, state, url).
|
|
1211
1343
|
|
|
1212
|
-
|
|
1213
|
-
or extended block returns None, and the caller then falls back to `gh` without saying why:
|
|
1214
|
-
that is safe (the watch prompt pins the order) but invisible, so keep the prompt and this
|
|
1215
|
-
reader in step.
|
|
1344
|
+
`--watch` may print the table more than once; a later row for the same name replaces an earlier one.
|
|
1216
1345
|
"""
|
|
1217
|
-
|
|
1218
|
-
reports: list[dict[str, str]] = []
|
|
1346
|
+
rows: dict[str, tuple[str, str]] = {}
|
|
1219
1347
|
for line in output.splitlines():
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
line, re.I,
|
|
1223
|
-
)
|
|
1224
|
-
if not match:
|
|
1348
|
+
parts = line.split("\t")
|
|
1349
|
+
if len(parts) < 2 or not parts[0].strip():
|
|
1225
1350
|
continue
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1351
|
+
url = parts[3].strip() if len(parts) > 3 else ""
|
|
1352
|
+
rows[parts[0].strip()] = (parts[1].strip().lower(), url)
|
|
1353
|
+
return [(name, state, url) for name, (state, url) in rows.items()]
|
|
1354
|
+
|
|
1355
|
+
|
|
1356
|
+
def _ci_gh_run(run_gh, argv: list[str], repo: Path, timeout_s: float):
|
|
1357
|
+
"""One short gh call. Returns the CompletedProcess, or None on any start or time-out problem."""
|
|
1358
|
+
try:
|
|
1359
|
+
return run_gh(argv, cwd=repo, capture_output=True, text=True,
|
|
1360
|
+
encoding="utf-8", errors="replace", timeout=timeout_s)
|
|
1361
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
1236
1362
|
return None
|
|
1237
|
-
|
|
1238
|
-
|
|
1363
|
+
|
|
1364
|
+
|
|
1365
|
+
def _ci_failure_summary(
|
|
1366
|
+
log_tail: str, *, agy_path: str, repo: Path, remaining_s: float, run_agy,
|
|
1367
|
+
) -> str | None:
|
|
1368
|
+
"""Ask agy for a one-line failure summary. The log is inside the prompt. None on any problem."""
|
|
1369
|
+
masked_values = caddis_redact.load_secrets(Path.home())
|
|
1370
|
+
prompt = (
|
|
1371
|
+
"Do not run any tools. Read this CI failure log and reply with one line: the failing job "
|
|
1372
|
+
"and the first failing test or error line. LOG:\n" + caddis_redact.redact(log_tail, masked_values)
|
|
1373
|
+
)
|
|
1374
|
+
try:
|
|
1375
|
+
model = resolve_models(load_lanes_config(repo))["agy"]
|
|
1376
|
+
effort = "low" # a one-line summary needs no deep thinking
|
|
1377
|
+
argv = [
|
|
1378
|
+
# The log is untrusted (a PR can write anything into it), so this call can use no tools:
|
|
1379
|
+
# plan mode and the sandbox, and never --dangerously-skip-permissions. Checked 2026-10-15:
|
|
1380
|
+
# with these flags agy wrote a plan and did not run the command a prompt asked for.
|
|
1381
|
+
agy_path, "--model", f"{model}-{effort}", "--mode", "plan", "--sandbox",
|
|
1382
|
+
"--output-format", "json", "--print",
|
|
1383
|
+
]
|
|
1384
|
+
# The idle watchdog is off for ci-watch (as before); the wall-clock limit still applies.
|
|
1385
|
+
runner = ((lambda *a: run_lane(*a, idle_timeout_s=0, spend_caller="ci-watch"))
|
|
1386
|
+
if run_agy is _LANE_RUNNER else run_agy)
|
|
1387
|
+
run = runner(argv, repo, prompt, False, min(_CI_AGY_TIMEOUT_S, remaining_s),
|
|
1388
|
+
child_env(os.environ, "agy", model))
|
|
1389
|
+
except LaneConfigError as exc:
|
|
1390
|
+
print(f"[caddis-lanes] ci-watch: agy summary skipped ({exc})", file=sys.stderr)
|
|
1391
|
+
return None
|
|
1392
|
+
except OSError as exc:
|
|
1393
|
+
print(f"[caddis-lanes] ci-watch: agy could not start ({exc}); no summary", file=sys.stderr)
|
|
1239
1394
|
return None
|
|
1240
|
-
if
|
|
1395
|
+
if run.timed_out:
|
|
1396
|
+
print("[caddis-lanes] ci-watch: agy summary timed out; no summary", file=sys.stderr)
|
|
1241
1397
|
return None
|
|
1242
|
-
|
|
1398
|
+
if run.returncode != 0:
|
|
1399
|
+
print(f"[caddis-lanes] ci-watch: agy exited {run.returncode}; no summary", file=sys.stderr)
|
|
1400
|
+
return None
|
|
1401
|
+
reply = caddis_redact.redact(caddis_spend.result_text(run.output), masked_values)
|
|
1402
|
+
for line in reply.splitlines():
|
|
1403
|
+
line = re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", line).strip()
|
|
1404
|
+
if line:
|
|
1405
|
+
return line[:_CI_SUMMARY_MAX_CHARS]
|
|
1406
|
+
print("[caddis-lanes] ci-watch: agy gave an empty reply; no summary", file=sys.stderr)
|
|
1407
|
+
return None
|
|
1243
1408
|
|
|
1244
1409
|
|
|
1245
|
-
def
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1410
|
+
def _ci_summary_from_log(gh_path, run_id, repo_args, repo, run_gh, remaining, agy_path, run_agy) -> str | None:
|
|
1411
|
+
"""Fetch the failed log, then ask agy for one line. None (with a stderr note) on any problem."""
|
|
1412
|
+
if run_id is None:
|
|
1413
|
+
print("[caddis-lanes] ci-watch: no run id for the failed check; no summary", file=sys.stderr)
|
|
1414
|
+
return None
|
|
1415
|
+
left = remaining()
|
|
1416
|
+
if left <= 0:
|
|
1417
|
+
print("[caddis-lanes] ci-watch: time budget used up; no summary", file=sys.stderr)
|
|
1418
|
+
return None
|
|
1419
|
+
log = _ci_gh_run(
|
|
1420
|
+
run_gh, [gh_path, "run", "view", run_id, "--log-failed", *repo_args],
|
|
1421
|
+
repo, min(_CI_GH_SHORT_TIMEOUT_S, left),
|
|
1422
|
+
)
|
|
1423
|
+
text = (log.stdout or "") if log is not None and log.returncode == 0 else ""
|
|
1424
|
+
tail = "\n".join(text.splitlines()[-_CI_LOG_TAIL_LINES:]).strip()[-_CI_LOG_TAIL_CHARS:]
|
|
1425
|
+
if not tail:
|
|
1426
|
+
print("[caddis-lanes] ci-watch: no failed log available; no summary", file=sys.stderr)
|
|
1427
|
+
return None
|
|
1428
|
+
left = remaining()
|
|
1429
|
+
if left <= 0:
|
|
1430
|
+
print("[caddis-lanes] ci-watch: time budget used up; no summary", file=sys.stderr)
|
|
1431
|
+
return None
|
|
1432
|
+
try:
|
|
1433
|
+
return _ci_failure_summary(tail, agy_path=agy_path, repo=repo, remaining_s=left, run_agy=run_agy)
|
|
1434
|
+
except Exception as exc: # agy must never change the result or the exit code
|
|
1435
|
+
print(f"[caddis-lanes] ci-watch: agy summary failed ({exc!r}); no summary", file=sys.stderr)
|
|
1436
|
+
return None
|
|
1251
1437
|
|
|
1252
1438
|
|
|
1253
1439
|
def ci_watch(
|
|
@@ -1263,7 +1449,13 @@ def ci_watch(
|
|
|
1263
1449
|
run_gh=subprocess.run,
|
|
1264
1450
|
clock=time.monotonic,
|
|
1265
1451
|
) -> int:
|
|
1266
|
-
"""Wait for CI with
|
|
1452
|
+
"""Wait for CI with `gh`, using one shared time budget. agy only summarises a failure.
|
|
1453
|
+
|
|
1454
|
+
`gh pr checks --watch` (or `gh run watch`) does the waiting. On a PR the check table gives
|
|
1455
|
+
CHECKS and FAILING, and one short `gh pr view` gives MERGE_STATE. When the result is `fail` and the
|
|
1456
|
+
`ci-watch` routing chain holds agy, agy reads the last lines of the failed log (inside its prompt)
|
|
1457
|
+
and returns one line for SUMMARY. agy never changes the result or the exit code.
|
|
1458
|
+
"""
|
|
1267
1459
|
if target_kind not in {"pr", "run"} or target_id <= 0 or timeout_s <= 0:
|
|
1268
1460
|
print("[caddis-lanes] ci-watch: target and timeout must be positive", file=sys.stderr)
|
|
1269
1461
|
_print_ci_watch("timeout", "n/a", "n/a", "none", "gh")
|
|
@@ -1281,73 +1473,10 @@ def ci_watch(
|
|
|
1281
1473
|
_print_ci_watch("timeout", "n/a", "n/a", "none", "gh")
|
|
1282
1474
|
return EXIT_CONFIG
|
|
1283
1475
|
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
poll = f"gh pr checks {target_id}{repo_opt}"
|
|
1287
|
-
state = f"gh pr view {target_id} --json mergeStateStatus,headRefOid{repo_opt}"
|
|
1288
|
-
else:
|
|
1289
|
-
poll = f"gh run view {target_id}{repo_opt}"
|
|
1290
|
-
state = "n/a"
|
|
1291
|
-
prompt = (
|
|
1292
|
-
"Read-only CI watch. Poll with " + poll + " until every check has finished or the time runs out. "
|
|
1293
|
-
+ (f"For the PR, also poll {state}. " if target_kind == "pr" else "")
|
|
1294
|
-
+ "Never merge, push, comment, re-run, approve, close or edit anything. "
|
|
1295
|
-
+ "Then print exactly this block as the last thing you output, with actual values:\n"
|
|
1296
|
-
"RESULT: pass|fail|timeout\n"
|
|
1297
|
-
"CHECKS: <n passed>/<n total>\n"
|
|
1298
|
-
"MERGE_STATE: <mergeStateStatus or n/a>\n"
|
|
1299
|
-
"FAILING: <comma-separated failing check names, or none>"
|
|
1300
|
-
)
|
|
1476
|
+
def remaining() -> float:
|
|
1477
|
+
return timeout_s - (clock() - started)
|
|
1301
1478
|
|
|
1302
|
-
|
|
1303
|
-
if watcher == "claude":
|
|
1304
|
-
break
|
|
1305
|
-
if watcher != "agy":
|
|
1306
|
-
print(f"[caddis-lanes] ci-watch: unsupported watcher {watcher}; skipping", file=sys.stderr)
|
|
1307
|
-
continue
|
|
1308
|
-
agy_path = which("agy")
|
|
1309
|
-
if agy_path is None:
|
|
1310
|
-
break
|
|
1311
|
-
remaining = timeout_s - (clock() - started)
|
|
1312
|
-
if remaining <= 0:
|
|
1313
|
-
_print_ci_watch("timeout", "n/a", "n/a", "none", "agy")
|
|
1314
|
-
return EXIT_CONFIG
|
|
1315
|
-
try:
|
|
1316
|
-
model = resolve_models(load_lanes_config(repo))["agy"]
|
|
1317
|
-
effort = DEFAULT_EFFORT["agy"]
|
|
1318
|
-
except LaneConfigError as exc:
|
|
1319
|
-
print(f"[caddis-lanes] {exc}", file=sys.stderr)
|
|
1320
|
-
_print_ci_watch("timeout", "n/a", "n/a", "none", "agy")
|
|
1321
|
-
return EXIT_CONFIG
|
|
1322
|
-
agy_argv = [
|
|
1323
|
-
agy_path, "--model", f"{model}-{effort}",
|
|
1324
|
-
"--dangerously-skip-permissions", "--output-format", "text", "--print",
|
|
1325
|
-
]
|
|
1326
|
-
try:
|
|
1327
|
-
# A CI watcher legitimately sits idle while CI runs, so the idle watchdog is off for it;
|
|
1328
|
-
# the wall-clock limit (`remaining`) still applies.
|
|
1329
|
-
watcher = (lambda *a: run_lane(*a, idle_timeout_s=0)) if run_agy is _LANE_RUNNER else run_agy
|
|
1330
|
-
run = watcher(agy_argv, repo, prompt, False, remaining, child_env(os.environ, "agy", model))
|
|
1331
|
-
except OSError as exc:
|
|
1332
|
-
print(f"[caddis-lanes] ci-watch: agy could not start ({exc}); watching with gh", file=sys.stderr)
|
|
1333
|
-
break
|
|
1334
|
-
outcome = classify_exit("agy", run.returncode, run.output, run.timed_out)
|
|
1335
|
-
if run.timed_out:
|
|
1336
|
-
_print_ci_watch("timeout", "n/a", "n/a", "none", "agy")
|
|
1337
|
-
return EXIT_CONFIG
|
|
1338
|
-
if outcome in {RESULT_OUT_OF_BUDGET, RESULT_CONFIG_ERROR}:
|
|
1339
|
-
reason = "out of budget" if outcome == RESULT_OUT_OF_BUDGET else "configuration error"
|
|
1340
|
-
print(f"[caddis-lanes] ci-watch: agy {reason}; watching with gh", file=sys.stderr)
|
|
1341
|
-
break
|
|
1342
|
-
report = _ci_watch_block(run.output)
|
|
1343
|
-
if report is not None:
|
|
1344
|
-
result = report["RESULT"].lower()
|
|
1345
|
-
_print_ci_watch(result, report["CHECKS"], report["MERGE_STATE"], report["FAILING"], "agy")
|
|
1346
|
-
return {"pass": EXIT_OK, "fail": EXIT_FAIL, "timeout": EXIT_CONFIG}[result]
|
|
1347
|
-
break
|
|
1348
|
-
|
|
1349
|
-
remaining = timeout_s - (clock() - started)
|
|
1350
|
-
if remaining <= 0:
|
|
1479
|
+
if remaining() <= 0:
|
|
1351
1480
|
print("[caddis-lanes] ci-watch: time budget expired before gh", file=sys.stderr)
|
|
1352
1481
|
_print_ci_watch("timeout", "n/a", "n/a", "none", "gh")
|
|
1353
1482
|
return EXIT_CONFIG
|
|
@@ -1356,16 +1485,15 @@ def ci_watch(
|
|
|
1356
1485
|
print("[caddis-lanes] ci-watch: gh not found on PATH", file=sys.stderr)
|
|
1357
1486
|
_print_ci_watch("timeout", "n/a", "n/a", "none", "gh")
|
|
1358
1487
|
return EXIT_CONFIG
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
command.extend(["--repo", repo_slug])
|
|
1488
|
+
repo_args = ["--repo", repo_slug] if repo_slug else []
|
|
1489
|
+
if target_kind == "pr":
|
|
1490
|
+
command = [gh_path, "pr", "checks", str(target_id), "--watch", "--interval", "30", *repo_args]
|
|
1491
|
+
else:
|
|
1492
|
+
command = [gh_path, "run", "watch", str(target_id), "--exit-status", "--interval", "30", *repo_args]
|
|
1365
1493
|
try:
|
|
1366
1494
|
completed = run_gh(
|
|
1367
1495
|
command, cwd=repo, capture_output=True, text=True,
|
|
1368
|
-
encoding="utf-8", errors="replace", timeout=remaining,
|
|
1496
|
+
encoding="utf-8", errors="replace", timeout=remaining(),
|
|
1369
1497
|
)
|
|
1370
1498
|
except subprocess.TimeoutExpired:
|
|
1371
1499
|
print("[caddis-lanes] ci-watch: gh timed out", file=sys.stderr)
|
|
@@ -1382,10 +1510,48 @@ def ci_watch(
|
|
|
1382
1510
|
code = EXIT_OK if completed.returncode == 0 else (
|
|
1383
1511
|
EXIT_FAIL if completed.returncode == 1 and not no_checks else EXIT_CONFIG
|
|
1384
1512
|
)
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1513
|
+
result = "pass" if code == EXIT_OK else "fail" if code == EXIT_FAIL else "timeout"
|
|
1514
|
+
|
|
1515
|
+
checks, merge_state = "n/a", "n/a"
|
|
1516
|
+
failing = "see gh output" if code == EXIT_FAIL else "none"
|
|
1517
|
+
run_id: str | None = str(target_id) if target_kind == "run" else None
|
|
1518
|
+
if target_kind == "pr" and code != EXIT_CONFIG:
|
|
1519
|
+
rows = _parse_pr_checks(completed.stdout or "")
|
|
1520
|
+
if rows:
|
|
1521
|
+
# `skipping` is not a failure: count it as passed.
|
|
1522
|
+
passed = sum(1 for _, state, _ in rows if state in {"pass", "skipping"})
|
|
1523
|
+
checks = f"{passed}/{len(rows)}"
|
|
1524
|
+
# Failed checks first; cancelled ones only when nothing failed (they name the wrong run).
|
|
1525
|
+
bad = ([(name, url) for name, state, url in rows if state == "fail"]
|
|
1526
|
+
or [(name, url) for name, state, url in rows if state == "cancel"])
|
|
1527
|
+
if code == EXIT_FAIL and bad:
|
|
1528
|
+
failing = ", ".join(name for name, _ in bad)
|
|
1529
|
+
for _, url in bad:
|
|
1530
|
+
found = _RUN_ID_IN_URL.search(url)
|
|
1531
|
+
if found:
|
|
1532
|
+
run_id = found.group(1)
|
|
1533
|
+
break
|
|
1534
|
+
left = remaining()
|
|
1535
|
+
if left > 0:
|
|
1536
|
+
view = _ci_gh_run(
|
|
1537
|
+
run_gh,
|
|
1538
|
+
[gh_path, "pr", "view", str(target_id), "--json", "mergeStateStatus",
|
|
1539
|
+
"-q", ".mergeStateStatus", *repo_args],
|
|
1540
|
+
repo, min(_CI_GH_SHORT_TIMEOUT_S, left),
|
|
1541
|
+
)
|
|
1542
|
+
value = (view.stdout or "").strip() if view is not None and view.returncode == 0 else ""
|
|
1543
|
+
merge_state = value or "n/a"
|
|
1544
|
+
|
|
1545
|
+
summary: str | None = None
|
|
1546
|
+
# Walk the chain in order: agy summarises only when it comes before claude (claude = no helper).
|
|
1547
|
+
helper = next((w for w in watchers if w in {"agy", "claude"}), None)
|
|
1548
|
+
if code == EXIT_FAIL and helper == "agy":
|
|
1549
|
+
agy_path = which("agy")
|
|
1550
|
+
if agy_path is not None:
|
|
1551
|
+
summary = _ci_summary_from_log(
|
|
1552
|
+
gh_path, run_id, repo_args, repo, run_gh, remaining, agy_path, run_agy,
|
|
1553
|
+
)
|
|
1554
|
+
_print_ci_watch(result, checks, merge_state, failing, "gh+agy" if summary else "gh", summary)
|
|
1389
1555
|
return code
|
|
1390
1556
|
|
|
1391
1557
|
|
|
@@ -1744,6 +1910,7 @@ def _run_command(
|
|
|
1744
1910
|
args.timeout,
|
|
1745
1911
|
child_env(dict(os.environ), lane, model),
|
|
1746
1912
|
idle_timeout_s=idle_timeout_s,
|
|
1913
|
+
spend_caller=f"lane:{lane}:{plan.stem}:p{args.phase}",
|
|
1747
1914
|
)
|
|
1748
1915
|
killed = guard_killed_since(lane_run.started, {lane_run.pid})
|
|
1749
1916
|
result = classify_exit(
|