@julioborges/gantry 1.0.5 → 1.1.0
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/.agents/skills/gantry/SKILL.md +15 -1
- package/.agents/skills/gantry/capabilities/codex.json +4 -3
- package/.agents/skills/gantry/dashboard/static/app.js +847 -35
- package/.agents/skills/gantry/dashboard/static/history.html +180 -0
- package/.agents/skills/gantry/dashboard/static/index.html +152 -7
- package/.agents/skills/gantry/dashboard/static/style.css +1177 -36
- package/.agents/skills/gantry/hooks/antigravity.hooks.json +2 -2
- package/.agents/skills/gantry/reference/plan-workflow.md +13 -0
- package/.agents/skills/gantry/reference/round-workflow.md +90 -2
- package/.agents/skills/gantry/scripts/common.py +3 -0
- package/.agents/skills/gantry/scripts/dashboard.py +664 -11
- package/.agents/skills/gantry/scripts/discovery.py +117 -8
- package/.agents/skills/gantry/scripts/execution.py +131 -14
- package/.agents/skills/gantry/scripts/guard.py +6 -0
- package/.agents/skills/gantry/scripts/plan.py +658 -0
- package/.agents/skills/gantry/scripts/result.py +56 -1
- package/.agents/skills/gantry/scripts/runlog.py +2 -1
- package/.agents/skills/gantry/scripts/setup.py +102 -9
- package/.agents/skills/gantry/scripts/wait_gate.py +179 -0
- package/.agents/skills/gantry-dashboard/SKILL.md +21 -2
- package/.agents/skills/gantry-plan/SKILL.md +104 -0
- package/.agents/skills/gantry-setup/SKILL.md +9 -3
- package/README.md +125 -98
- package/package.json +2 -1
|
@@ -116,6 +116,122 @@ def discover_antigravity_models(runner: Callable[..., subprocess.CompletedProces
|
|
|
116
116
|
return models
|
|
117
117
|
|
|
118
118
|
|
|
119
|
+
_DISCOVERY_CACHE: dict[str, list[dict[str, Any]]] = {}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def clear_discovery_cache() -> None:
|
|
123
|
+
"""Clear in-memory discovery cache."""
|
|
124
|
+
_DISCOVERY_CACHE.clear()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def parse_codex_models_output(
|
|
128
|
+
raw_output: str,
|
|
129
|
+
metadata_lookup: dict[str, Any] | None = None,
|
|
130
|
+
) -> list[dict[str, Any]]:
|
|
131
|
+
"""Parse stdout from `codex models` into a structured list of model dicts without inventing windows."""
|
|
132
|
+
cleaned = clean_ansi(raw_output)
|
|
133
|
+
models: list[dict[str, Any]] = []
|
|
134
|
+
seen: set[str] = set()
|
|
135
|
+
|
|
136
|
+
for line in cleaned.splitlines():
|
|
137
|
+
line = line.strip()
|
|
138
|
+
if not line or line.startswith("Fetching") or line.startswith("Usage:") or line.startswith("Models:"):
|
|
139
|
+
continue
|
|
140
|
+
parts = line.split(None, 1)
|
|
141
|
+
if not parts:
|
|
142
|
+
continue
|
|
143
|
+
model_id = parts[0].strip().strip("-*• ")
|
|
144
|
+
if not model_id:
|
|
145
|
+
continue
|
|
146
|
+
display_name = parts[1].strip() if len(parts) > 1 else model_id
|
|
147
|
+
|
|
148
|
+
if model_id in seen:
|
|
149
|
+
continue
|
|
150
|
+
seen.add(model_id)
|
|
151
|
+
|
|
152
|
+
entry: dict[str, Any] = {
|
|
153
|
+
"id": model_id,
|
|
154
|
+
"name": display_name,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if metadata_lookup and model_id in metadata_lookup:
|
|
158
|
+
meta = metadata_lookup[model_id]
|
|
159
|
+
if isinstance(meta, dict):
|
|
160
|
+
if "contextWindow" in meta:
|
|
161
|
+
entry["contextWindow"] = meta["contextWindow"]
|
|
162
|
+
if "supportedEfforts" in meta:
|
|
163
|
+
entry["supportedEfforts"] = meta["supportedEfforts"]
|
|
164
|
+
if "effort" in meta:
|
|
165
|
+
entry["effort"] = meta["effort"]
|
|
166
|
+
|
|
167
|
+
models.append(entry)
|
|
168
|
+
|
|
169
|
+
return models
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def get_codex_version(runner: Callable[..., Any] | None = None) -> str:
|
|
173
|
+
"""Extract version from `codex --version`."""
|
|
174
|
+
codex_path = shutil.which("codex")
|
|
175
|
+
if not codex_path and runner is None:
|
|
176
|
+
raise DiscoveryError("Missing discovery: codex CLI not found in PATH")
|
|
177
|
+
|
|
178
|
+
run_cmd = runner or subprocess.run
|
|
179
|
+
try:
|
|
180
|
+
proc = run_cmd([codex_path or "codex", "--version"], capture_output=True, text=True, check=False)
|
|
181
|
+
except OSError as exc:
|
|
182
|
+
raise DiscoveryError(f"Missing discovery: failed to run codex --version: {exc}") from exc
|
|
183
|
+
|
|
184
|
+
if proc.returncode != 0:
|
|
185
|
+
err = proc.stderr.strip() or proc.stdout.strip()
|
|
186
|
+
raise DiscoveryError(f"Missing discovery: codex --version returned exit code {proc.returncode}: {err}")
|
|
187
|
+
|
|
188
|
+
cleaned = clean_ansi(proc.stdout or proc.stderr).strip()
|
|
189
|
+
match = re.search(r"(\d+\.\d+(?:\.\d+)?)", cleaned)
|
|
190
|
+
if not match:
|
|
191
|
+
raise DiscoveryError(f"Missing discovery: could not parse codex version from {cleaned!r}")
|
|
192
|
+
return match.group(1)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def discover_codex_models(
|
|
196
|
+
runner: Callable[..., Any] | None = None,
|
|
197
|
+
use_cache: bool = True,
|
|
198
|
+
) -> list[dict[str, Any]]:
|
|
199
|
+
"""Discover executable models via `codex models` with metadata lookup and caching."""
|
|
200
|
+
if use_cache and "codex" in _DISCOVERY_CACHE and runner is None:
|
|
201
|
+
return list(_DISCOVERY_CACHE["codex"])
|
|
202
|
+
|
|
203
|
+
codex_path = shutil.which("codex")
|
|
204
|
+
if not codex_path and runner is None:
|
|
205
|
+
raise DiscoveryError("Missing discovery: codex CLI not found in PATH")
|
|
206
|
+
|
|
207
|
+
run_cmd = runner or subprocess.run
|
|
208
|
+
try:
|
|
209
|
+
proc = run_cmd([codex_path or "codex", "models"], capture_output=True, text=True, check=False)
|
|
210
|
+
except OSError as exc:
|
|
211
|
+
raise DiscoveryError(f"Missing discovery: failed to run codex models: {exc}") from exc
|
|
212
|
+
|
|
213
|
+
if proc.returncode != 0:
|
|
214
|
+
err = proc.stderr.strip() or proc.stdout.strip()
|
|
215
|
+
raise DiscoveryError(f"Missing discovery: codex models returned exit code {proc.returncode}: {err}")
|
|
216
|
+
|
|
217
|
+
cap_file = Path(__file__).resolve().parents[1] / "capabilities" / "codex.json"
|
|
218
|
+
metadata_lookup = {}
|
|
219
|
+
if cap_file.exists():
|
|
220
|
+
try:
|
|
221
|
+
metadata_lookup = json.loads(cap_file.read_text(encoding="utf-8")).get("models", {})
|
|
222
|
+
except Exception:
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
models = parse_codex_models_output(proc.stdout, metadata_lookup=metadata_lookup)
|
|
226
|
+
if not models:
|
|
227
|
+
raise DiscoveryError("Missing discovery: codex models returned an empty model list")
|
|
228
|
+
|
|
229
|
+
if use_cache and runner is None:
|
|
230
|
+
_DISCOVERY_CACHE["codex"] = models
|
|
231
|
+
|
|
232
|
+
return models
|
|
233
|
+
|
|
234
|
+
|
|
119
235
|
def discover_models(harness: str, runner: Callable[..., Any] | None = None) -> list[dict[str, Any]]:
|
|
120
236
|
"""Discover executable models for the given harness; fail closed on missing discovery."""
|
|
121
237
|
h = (harness or "").lower()
|
|
@@ -143,14 +259,7 @@ def discover_models(harness: str, runner: Callable[..., Any] | None = None) -> l
|
|
|
143
259
|
return [{"id": m, "contextWindow": d["contextWindow"]} for m, d in cap.get("models", {}).items()]
|
|
144
260
|
raise DiscoveryError("Missing discovery: opencode capability declaration missing")
|
|
145
261
|
elif h == "codex":
|
|
146
|
-
|
|
147
|
-
if not codex_path:
|
|
148
|
-
raise DiscoveryError("Missing discovery: codex CLI not found in PATH")
|
|
149
|
-
cap_file = Path(__file__).resolve().parents[1] / "capabilities" / "codex.json"
|
|
150
|
-
if cap_file.exists():
|
|
151
|
-
cap = json.loads(cap_file.read_text(encoding="utf-8"))
|
|
152
|
-
return [{"id": m, "contextWindow": d["contextWindow"]} for m, d in cap.get("models", {}).items()]
|
|
153
|
-
raise DiscoveryError("Missing discovery: codex capability declaration missing")
|
|
262
|
+
return discover_codex_models(runner=runner)
|
|
154
263
|
|
|
155
264
|
raise DiscoveryError(f"Missing discovery: unhandled harness {harness}")
|
|
156
265
|
|
|
@@ -63,6 +63,10 @@ class ProtocolFailureError(Exception):
|
|
|
63
63
|
"""Raised when an external role result is missing, undecodable or fails the schema contract."""
|
|
64
64
|
|
|
65
65
|
|
|
66
|
+
class ExecutionFailureError(RuntimeError):
|
|
67
|
+
"""Raised when an external harness command fails at runtime (crash, timeout, non-zero exit)."""
|
|
68
|
+
|
|
69
|
+
|
|
66
70
|
def parse_semver(version_str: str) -> tuple[int, int, int]:
|
|
67
71
|
match = re.search(r"(\d+)\.(\d+)(?:\.(\d+))?", version_str)
|
|
68
72
|
if not match:
|
|
@@ -73,11 +77,67 @@ def parse_semver(version_str: str) -> tuple[int, int, int]:
|
|
|
73
77
|
return (major, minor, patch)
|
|
74
78
|
|
|
75
79
|
|
|
80
|
+
def validate_codex_auth(runner: Callable[..., Any] | None = None) -> dict[str, Any]:
|
|
81
|
+
"""Validate Codex authentication via `codex login status`."""
|
|
82
|
+
cli_name = CLI_NAMES.get("codex", "codex")
|
|
83
|
+
if runner is None and not shutil.which(cli_name):
|
|
84
|
+
return {
|
|
85
|
+
"valid": False,
|
|
86
|
+
"error": f"Codex CLI binary '{cli_name}' not found on PATH. Please install Codex CLI or ensure it is in your PATH.",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
run_cmd = runner or subprocess.run
|
|
90
|
+
try:
|
|
91
|
+
try:
|
|
92
|
+
proc = run_cmd([cli_name, "login", "status"], capture_output=True, text=True, check=False)
|
|
93
|
+
except TypeError:
|
|
94
|
+
proc = run_cmd([cli_name, "login", "status"])
|
|
95
|
+
except FileNotFoundError:
|
|
96
|
+
return {
|
|
97
|
+
"valid": False,
|
|
98
|
+
"error": f"Codex CLI binary '{cli_name}' not found on PATH. Please install Codex CLI or ensure it is in your PATH.",
|
|
99
|
+
}
|
|
100
|
+
except Exception as exc:
|
|
101
|
+
return {
|
|
102
|
+
"valid": False,
|
|
103
|
+
"error": f"Codex CLI authentication check failed: {exc}. Run 'codex login' to authenticate.",
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if proc.returncode != 0:
|
|
107
|
+
err = (getattr(proc, "stderr", "") or "").strip() or (getattr(proc, "stdout", "") or "").strip()
|
|
108
|
+
return {
|
|
109
|
+
"valid": False,
|
|
110
|
+
"error": f"Codex CLI is unauthenticated ({err or f'exit code {proc.returncode}'}). Run 'codex login' to authenticate.",
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
stdout = (getattr(proc, "stdout", "") or "").strip().lower()
|
|
114
|
+
if "not logged in" in stdout or "unauthenticated" in stdout or "no credentials" in stdout:
|
|
115
|
+
return {
|
|
116
|
+
"valid": False,
|
|
117
|
+
"error": f"Codex CLI is unauthenticated ({proc.stdout.strip()}). Run 'codex login' to authenticate.",
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {"valid": True}
|
|
121
|
+
|
|
122
|
+
|
|
76
123
|
def validate_harness_version(
|
|
77
124
|
harness: str,
|
|
78
125
|
runner: Callable[..., Any] | None = None,
|
|
79
126
|
) -> dict[str, Any]:
|
|
80
127
|
cli_name = CLI_NAMES.get(harness, harness)
|
|
128
|
+
if runner is None and not shutil.which(cli_name):
|
|
129
|
+
if harness == "codex":
|
|
130
|
+
return {
|
|
131
|
+
"valid": False,
|
|
132
|
+
"version": None,
|
|
133
|
+
"error": f"Codex CLI binary '{cli_name}' not found on PATH. Please install Codex CLI or ensure it is in your PATH.",
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
"valid": False,
|
|
137
|
+
"version": None,
|
|
138
|
+
"error": f"Harness CLI '{cli_name}' not found on PATH",
|
|
139
|
+
}
|
|
140
|
+
|
|
81
141
|
try:
|
|
82
142
|
if runner:
|
|
83
143
|
try:
|
|
@@ -87,6 +147,12 @@ def validate_harness_version(
|
|
|
87
147
|
else:
|
|
88
148
|
proc = subprocess.run([cli_name, "--version"], capture_output=True, text=True, check=False)
|
|
89
149
|
if proc.returncode != 0:
|
|
150
|
+
if harness == "codex":
|
|
151
|
+
return {
|
|
152
|
+
"valid": False,
|
|
153
|
+
"version": None,
|
|
154
|
+
"error": f"Harness CLI {cli_name} authentication/execution validation failed: return code {proc.returncode}. Run 'codex login' to authenticate.",
|
|
155
|
+
}
|
|
90
156
|
return {
|
|
91
157
|
"valid": False,
|
|
92
158
|
"version": None,
|
|
@@ -105,6 +171,18 @@ def validate_harness_version(
|
|
|
105
171
|
}
|
|
106
172
|
ver_formatted = f"{version_tuple[0]}.{version_tuple[1]}.{version_tuple[2]}"
|
|
107
173
|
return {"valid": True, "version": ver_formatted}
|
|
174
|
+
except FileNotFoundError:
|
|
175
|
+
if harness == "codex":
|
|
176
|
+
return {
|
|
177
|
+
"valid": False,
|
|
178
|
+
"version": None,
|
|
179
|
+
"error": f"Codex CLI binary '{cli_name}' not found on PATH. Please install Codex CLI or ensure it is in your PATH.",
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
"valid": False,
|
|
183
|
+
"version": None,
|
|
184
|
+
"error": f"Harness CLI '{cli_name}' not found on PATH",
|
|
185
|
+
}
|
|
108
186
|
except Exception as exc:
|
|
109
187
|
return {
|
|
110
188
|
"valid": False,
|
|
@@ -320,6 +398,14 @@ def validate_selection(
|
|
|
320
398
|
"role": role,
|
|
321
399
|
"error": ver_check["error"],
|
|
322
400
|
}
|
|
401
|
+
if harness == "codex":
|
|
402
|
+
auth_check = validate_codex_auth(runner=runner)
|
|
403
|
+
if not auth_check["valid"]:
|
|
404
|
+
return {
|
|
405
|
+
"valid": False,
|
|
406
|
+
"role": role,
|
|
407
|
+
"error": auth_check["error"],
|
|
408
|
+
}
|
|
323
409
|
|
|
324
410
|
return {"valid": True, "role": role}
|
|
325
411
|
|
|
@@ -344,7 +430,10 @@ def build_dispatch_command(
|
|
|
344
430
|
elif h == "opencode":
|
|
345
431
|
return ["opencode", "run", prompt, "--model", model]
|
|
346
432
|
elif h == "codex":
|
|
347
|
-
|
|
433
|
+
cmd = ["codex", "exec", prompt, "--model", model]
|
|
434
|
+
if effort:
|
|
435
|
+
cmd.extend(["--effort", effort])
|
|
436
|
+
return cmd
|
|
348
437
|
else:
|
|
349
438
|
raise ValueError(f"Unsupported harness: {harness}")
|
|
350
439
|
|
|
@@ -387,15 +476,9 @@ def parse_and_validate_result(
|
|
|
387
476
|
if not raw_output or not raw_output.strip():
|
|
388
477
|
raise ProtocolFailureError(f"Protocol failure: empty output returned for role {role}")
|
|
389
478
|
|
|
390
|
-
text = raw_output.strip()
|
|
391
|
-
if text.startswith("```json") and text.endswith("```"):
|
|
392
|
-
text = text[7:-3].strip()
|
|
393
|
-
elif text.startswith("```") and text.endswith("```"):
|
|
394
|
-
text = text[3:-3].strip()
|
|
395
|
-
|
|
396
479
|
try:
|
|
397
|
-
data =
|
|
398
|
-
except json.JSONDecodeError as exc:
|
|
480
|
+
data = result.extract_json(raw_output)
|
|
481
|
+
except (json.JSONDecodeError, ValueError) as exc:
|
|
399
482
|
raise ProtocolFailureError(f"Protocol failure: invalid JSON output for role {role}: {exc}") from exc
|
|
400
483
|
|
|
401
484
|
if not isinstance(data, dict):
|
|
@@ -456,6 +539,7 @@ def dispatch_role(
|
|
|
456
539
|
unit_id: str | None = None,
|
|
457
540
|
state_root: str | None = None,
|
|
458
541
|
retry_on_invalid: bool = True,
|
|
542
|
+
timeout: int | float | None = None,
|
|
459
543
|
) -> dict[str, Any]:
|
|
460
544
|
"""Execute a bounded role invocation in the selected native harness."""
|
|
461
545
|
cwd_path = Path(cwd).resolve()
|
|
@@ -481,6 +565,10 @@ def dispatch_role(
|
|
|
481
565
|
if not eff_val["valid"]:
|
|
482
566
|
raise ValueError(eff_val["error"])
|
|
483
567
|
|
|
568
|
+
eff_timeout = timeout
|
|
569
|
+
if eff_timeout is None and isinstance(policy, dict):
|
|
570
|
+
eff_timeout = policy.get("execution", {}).get("timeout")
|
|
571
|
+
|
|
484
572
|
if run_id and unit_id:
|
|
485
573
|
try:
|
|
486
574
|
root = runlog.state_root(state_root)
|
|
@@ -508,10 +596,25 @@ def dispatch_role(
|
|
|
508
596
|
|
|
509
597
|
cmd = build_dispatch_command(harness, model, prompt, effort=effort)
|
|
510
598
|
run_fn = runner or subprocess.run
|
|
511
|
-
|
|
599
|
+
run_kwargs: dict[str, Any] = {"cwd": str(cwd_path), "capture_output": True, "text": True, "check": False}
|
|
600
|
+
if eff_timeout is not None:
|
|
601
|
+
run_kwargs["timeout"] = eff_timeout
|
|
602
|
+
|
|
603
|
+
try:
|
|
604
|
+
try:
|
|
605
|
+
proc = run_fn(cmd, **run_kwargs)
|
|
606
|
+
except TypeError:
|
|
607
|
+
proc = run_fn(cmd, cwd=str(cwd_path))
|
|
608
|
+
except subprocess.TimeoutExpired as exc:
|
|
609
|
+
raise ExecutionFailureError(f"Harness {harness} execution timed out after {eff_timeout}s: {exc}") from exc
|
|
610
|
+
except Exception as exc:
|
|
611
|
+
if isinstance(exc, (ExecutionFailureError, UnsupportedHarnessVersionError, ModelFallbackError, ProtocolFailureError)):
|
|
612
|
+
raise
|
|
613
|
+
raise ExecutionFailureError(f"Harness {harness} execution failed to start or crashed: {exc}") from exc
|
|
614
|
+
|
|
512
615
|
if proc.returncode != 0:
|
|
513
|
-
err = (proc
|
|
514
|
-
raise
|
|
616
|
+
err = (getattr(proc, "stderr", "") or "").strip() or (getattr(proc, "stdout", "") or "").strip()
|
|
617
|
+
raise ExecutionFailureError(f"Harness {harness} execution failed (exit {proc.returncode}): {err}")
|
|
515
618
|
|
|
516
619
|
check_model_fallback(model, proc.stdout)
|
|
517
620
|
|
|
@@ -523,9 +626,21 @@ def dispatch_role(
|
|
|
523
626
|
if retry_on_invalid:
|
|
524
627
|
retry_prompt = f"{prompt}\n\nYour prior result was invalid: {exc}\nReturn the complete {role} result contract."
|
|
525
628
|
retry_cmd = build_dispatch_command(harness, model, retry_prompt, effort=effort)
|
|
526
|
-
|
|
629
|
+
try:
|
|
630
|
+
try:
|
|
631
|
+
proc2 = run_fn(retry_cmd, **run_kwargs)
|
|
632
|
+
except TypeError:
|
|
633
|
+
proc2 = run_fn(retry_cmd, cwd=str(cwd_path))
|
|
634
|
+
except subprocess.TimeoutExpired as exc2:
|
|
635
|
+
raise ExecutionFailureError(f"Harness {harness} retry execution timed out after {eff_timeout}s: {exc2}") from exc2
|
|
636
|
+
except Exception as exc2:
|
|
637
|
+
if isinstance(exc2, (ExecutionFailureError, UnsupportedHarnessVersionError, ModelFallbackError, ProtocolFailureError)):
|
|
638
|
+
raise
|
|
639
|
+
raise ExecutionFailureError(f"Harness {harness} retry failed to start or crashed: {exc2}") from exc2
|
|
640
|
+
|
|
527
641
|
if proc2.returncode != 0:
|
|
528
|
-
|
|
642
|
+
err2 = (getattr(proc2, "stderr", "") or "").strip() or (getattr(proc2, "stdout", "") or "").strip()
|
|
643
|
+
raise ExecutionFailureError(f"Harness {harness} retry failed (exit {proc2.returncode}): {err2}")
|
|
529
644
|
check_model_fallback(model, proc2.stdout)
|
|
530
645
|
data = parse_and_validate_result(role, proc2.stdout)
|
|
531
646
|
if role in ("critic", "requirement-critic", "plan-critic"):
|
|
@@ -685,6 +800,7 @@ def main() -> int:
|
|
|
685
800
|
dispatch_parser.add_argument("--run-id", help="Run ID for logging")
|
|
686
801
|
dispatch_parser.add_argument("--unit-id", help="Unit ID for logging")
|
|
687
802
|
dispatch_parser.add_argument("--state-root", help="State root directory")
|
|
803
|
+
dispatch_parser.add_argument("--timeout", type=float, help="execution timeout in seconds")
|
|
688
804
|
dispatch_parser.add_argument("--json", action="store_true", help="output JSON")
|
|
689
805
|
|
|
690
806
|
failures_parser = subparsers.add_parser("unresolved-failures", help="check for unresolved paused executions")
|
|
@@ -750,6 +866,7 @@ def main() -> int:
|
|
|
750
866
|
run_id=getattr(args, "run_id", None),
|
|
751
867
|
unit_id=getattr(args, "unit_id", None),
|
|
752
868
|
state_root=getattr(args, "state_root", None),
|
|
869
|
+
timeout=getattr(args, "timeout", None),
|
|
753
870
|
)
|
|
754
871
|
print(json.dumps(res, indent=2))
|
|
755
872
|
return 0
|
|
@@ -569,8 +569,14 @@ def main() -> int:
|
|
|
569
569
|
|
|
570
570
|
payload = read_payload()
|
|
571
571
|
cwd = Path(args.cwd).resolve()
|
|
572
|
+
name = tool_name(payload) if payload else ""
|
|
573
|
+
normalized_name = re.sub(r"[^a-z]", "", name)
|
|
572
574
|
|
|
573
575
|
def allow() -> int:
|
|
576
|
+
if args.event == "PostToolUse" and (not args.run_id or normalized_name != "invokesubagent"):
|
|
577
|
+
if args.json:
|
|
578
|
+
print("{}")
|
|
579
|
+
return 0
|
|
574
580
|
if args.json:
|
|
575
581
|
print(json.dumps({"decision": "allow"}, separators=(",", ":")))
|
|
576
582
|
else:
|