@julioborges/gantry 1.0.1 → 1.0.4
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 +32 -1
- package/.agents/skills/gantry/capabilities/antigravity.json +15 -0
- package/.agents/skills/gantry/hooks/antigravity.hooks.json +26 -0
- package/.agents/skills/gantry/reference/plan-workflow.md +52 -14
- package/.agents/skills/gantry/reference/round-workflow.md +277 -11
- package/.agents/skills/gantry/scripts/budget.py +49 -7
- package/.agents/skills/gantry/scripts/caveman.py +243 -0
- package/.agents/skills/gantry/scripts/common.py +2 -0
- package/.agents/skills/gantry/scripts/discovery.py +262 -0
- package/.agents/skills/gantry/scripts/execution.py +797 -0
- package/.agents/skills/gantry/scripts/frontier.py +1 -1
- package/.agents/skills/gantry/scripts/guard.py +91 -25
- package/.agents/skills/gantry/scripts/runlog.py +38 -1
- package/.agents/skills/gantry/scripts/setup.py +39 -0
- package/.agents/skills/gantry-setup/SKILL.md +16 -3
- package/README.md +2 -0
- package/assets/gantry.png +0 -0
- package/package.json +23 -6
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Caveman lite discovery, installation guidance, and activation resolution."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from common import repo_root, resolve_policy
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_install_guidance(harness: str | None = None) -> str:
|
|
15
|
+
"""Return verified upstream skill-only installation guidance for the host harness."""
|
|
16
|
+
h = (harness or "claude-code").lower()
|
|
17
|
+
if "antigravity" in h or "agy" in h:
|
|
18
|
+
return (
|
|
19
|
+
"Clone Caveman into your Antigravity skills directory:\n"
|
|
20
|
+
" git clone https://github.com/JuliusBrussee/caveman.git ~/.gemini/config/skills/caveman\n"
|
|
21
|
+
" (or inside repository at .agents/skills/caveman)"
|
|
22
|
+
)
|
|
23
|
+
elif "opencode" in h:
|
|
24
|
+
return (
|
|
25
|
+
"Clone Caveman into your OpenCode skills directory:\n"
|
|
26
|
+
" git clone https://github.com/JuliusBrussee/caveman.git ~/.config/opencode/skills/caveman\n"
|
|
27
|
+
" (or inside repository at .agents/skills/caveman)"
|
|
28
|
+
)
|
|
29
|
+
elif "codex" in h:
|
|
30
|
+
return (
|
|
31
|
+
"Clone Caveman into your skills directory:\n"
|
|
32
|
+
" git clone https://github.com/JuliusBrussee/caveman.git .agents/skills/caveman"
|
|
33
|
+
)
|
|
34
|
+
else: # Claude Code / default
|
|
35
|
+
return (
|
|
36
|
+
"Install Caveman skill using npx skills:\n"
|
|
37
|
+
" npx skills add caveman\n"
|
|
38
|
+
" (or git clone https://github.com/JuliusBrussee/caveman.git ~/.claude/skills/caveman)"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def check_availability(harness: str | None = None, root: Path | None = None) -> dict:
|
|
43
|
+
"""Verify that the host harness can discover and read the Caveman skill."""
|
|
44
|
+
root_path = root.resolve() if root else repo_root()
|
|
45
|
+
home = Path.home()
|
|
46
|
+
|
|
47
|
+
# Allow environment override for testing/fixtures without touching global paths
|
|
48
|
+
env_override = os.environ.get("GANTRY_CAVEMAN_PATH")
|
|
49
|
+
candidate_paths: list[Path] = []
|
|
50
|
+
if env_override:
|
|
51
|
+
p = Path(env_override)
|
|
52
|
+
candidate_paths.append(p if p.name == "SKILL.md" else p / "SKILL.md")
|
|
53
|
+
|
|
54
|
+
# In-repository skill
|
|
55
|
+
candidate_paths.append(root_path / ".agents" / "skills" / "caveman" / "SKILL.md")
|
|
56
|
+
|
|
57
|
+
h = (harness or "").lower()
|
|
58
|
+
if "antigravity" in h:
|
|
59
|
+
candidate_paths.extend([
|
|
60
|
+
home / ".gemini" / "config" / "skills" / "caveman" / "SKILL.md",
|
|
61
|
+
home / ".agents" / "skills" / "caveman" / "SKILL.md",
|
|
62
|
+
])
|
|
63
|
+
elif "opencode" in h:
|
|
64
|
+
candidate_paths.extend([
|
|
65
|
+
home / ".config" / "opencode" / "skills" / "caveman" / "SKILL.md",
|
|
66
|
+
home / ".agents" / "skills" / "caveman" / "SKILL.md",
|
|
67
|
+
])
|
|
68
|
+
elif "codex" in h:
|
|
69
|
+
candidate_paths.extend([
|
|
70
|
+
home / ".codex" / "skills" / "caveman" / "SKILL.md",
|
|
71
|
+
home / ".agents" / "skills" / "caveman" / "SKILL.md",
|
|
72
|
+
])
|
|
73
|
+
else: # Claude Code or generic
|
|
74
|
+
candidate_paths.extend([
|
|
75
|
+
root_path / ".claude" / "skills" / "caveman" / "SKILL.md",
|
|
76
|
+
home / ".claude" / "skills" / "caveman" / "SKILL.md",
|
|
77
|
+
home / ".agents" / "skills" / "caveman" / "SKILL.md",
|
|
78
|
+
])
|
|
79
|
+
|
|
80
|
+
for candidate in candidate_paths:
|
|
81
|
+
if candidate.is_file():
|
|
82
|
+
try:
|
|
83
|
+
candidate.read_text(encoding="utf-8")
|
|
84
|
+
return {
|
|
85
|
+
"available": True,
|
|
86
|
+
"path": str(candidate.resolve()),
|
|
87
|
+
"harness": harness or "auto",
|
|
88
|
+
}
|
|
89
|
+
except (OSError, PermissionError) as exc:
|
|
90
|
+
return {
|
|
91
|
+
"available": False,
|
|
92
|
+
"reason": f"unreadable: {exc}",
|
|
93
|
+
"path": str(candidate.resolve()),
|
|
94
|
+
"install_guidance": get_install_guidance(harness),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
"available": False,
|
|
99
|
+
"reason": "not_found",
|
|
100
|
+
"install_guidance": get_install_guidance(harness),
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def resolve_activation(
|
|
105
|
+
policy: dict,
|
|
106
|
+
harness: str | None = None,
|
|
107
|
+
root: Path | None = None,
|
|
108
|
+
warned: bool = False,
|
|
109
|
+
) -> dict:
|
|
110
|
+
"""Resolve Caveman activation state and handle once-per-Run warning deduplication."""
|
|
111
|
+
pref = policy.get("caveman", False)
|
|
112
|
+
# Handle boolean or dict representation safely
|
|
113
|
+
preference_enabled = bool(pref.get("enabled", True) if isinstance(pref, dict) else pref)
|
|
114
|
+
|
|
115
|
+
if not preference_enabled:
|
|
116
|
+
return {
|
|
117
|
+
"preference": False,
|
|
118
|
+
"active": False,
|
|
119
|
+
"scope": "none",
|
|
120
|
+
"warning": None,
|
|
121
|
+
"warned": warned,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
avail = check_availability(harness=harness, root=root)
|
|
125
|
+
if avail["available"]:
|
|
126
|
+
return {
|
|
127
|
+
"preference": True,
|
|
128
|
+
"active": True,
|
|
129
|
+
"skill_path": avail["path"],
|
|
130
|
+
"scope": "conversational_and_summaries",
|
|
131
|
+
"warning": None,
|
|
132
|
+
"warned": warned,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# Preference enabled, but skill unavailable
|
|
136
|
+
warning = None
|
|
137
|
+
new_warned = warned
|
|
138
|
+
if not warned:
|
|
139
|
+
guidance = avail.get("install_guidance", get_install_guidance(harness))
|
|
140
|
+
warning = (
|
|
141
|
+
"Warning: Caveman lite is enabled in repository policy, but the Caveman skill was not "
|
|
142
|
+
"found or is unreadable in the host environment. Continuing with normal behavior.\n"
|
|
143
|
+
f"{guidance}"
|
|
144
|
+
)
|
|
145
|
+
new_warned = True
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
"preference": True,
|
|
149
|
+
"active": False,
|
|
150
|
+
"scope": "none",
|
|
151
|
+
"warning": warning,
|
|
152
|
+
"warned": new_warned,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def get_coordinating_instructions(active: bool = True) -> str:
|
|
157
|
+
"""Return coordinating agent instructions for Caveman lite conversational scope."""
|
|
158
|
+
if not active:
|
|
159
|
+
return ""
|
|
160
|
+
return (
|
|
161
|
+
"Caveman lite is active for this Run: use concise phrasing for conversational messages and "
|
|
162
|
+
"summaries. Specs, Issues, documentation, PR descriptions, Result Contracts, exact commands, "
|
|
163
|
+
"exact errors, and acceptance criteria retain full detail."
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def get_planning_instructions(active: bool = True) -> str:
|
|
168
|
+
"""Return planning agent prompt instructions for Caveman lite conversational scope."""
|
|
169
|
+
if not active:
|
|
170
|
+
return ""
|
|
171
|
+
return (
|
|
172
|
+
"Caveman lite is active: use concise phrasing for conversational messages and summaries. "
|
|
173
|
+
"Specs, draft Issues, persisted role results, exact errors, commands, and acceptance criteria "
|
|
174
|
+
"retain full detail."
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def get_round_instructions(active: bool = True) -> str:
|
|
179
|
+
"""Return round agent prompt instructions for Caveman lite conversational scope."""
|
|
180
|
+
if not active:
|
|
181
|
+
return ""
|
|
182
|
+
return (
|
|
183
|
+
"Caveman lite is active: use concise phrasing for conversational messages and summaries. "
|
|
184
|
+
"Code, documentation, lesson candidates, PR descriptions, Result Contracts, exact commands, "
|
|
185
|
+
"exact errors, acceptance criteria, and verification evidence retain full detail."
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def main() -> int:
|
|
190
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
191
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
192
|
+
|
|
193
|
+
check_parser = subparsers.add_parser("check", help="check host-harness skill discovery and readability")
|
|
194
|
+
check_parser.add_argument("--harness", default="claude-code", help="host harness name")
|
|
195
|
+
check_parser.add_argument("--cwd", default=".", help="repository root path")
|
|
196
|
+
check_parser.add_argument("--json", action="store_true", help="output JSON")
|
|
197
|
+
|
|
198
|
+
resolve_parser = subparsers.add_parser("resolve", help="resolve effective Caveman activation for Run")
|
|
199
|
+
resolve_parser.add_argument("--harness", default="claude-code", help="host harness name")
|
|
200
|
+
resolve_parser.add_argument("--cwd", default=".", help="repository root path")
|
|
201
|
+
resolve_parser.add_argument("--warned", action="store_true", help="whether warning was already emitted")
|
|
202
|
+
resolve_parser.add_argument("--json", action="store_true", help="output JSON")
|
|
203
|
+
|
|
204
|
+
subparsers.add_parser("guidance", help="print installation guidance")
|
|
205
|
+
|
|
206
|
+
args = parser.parse_args()
|
|
207
|
+
|
|
208
|
+
if args.command == "check":
|
|
209
|
+
root = Path(args.cwd).resolve()
|
|
210
|
+
res = check_availability(harness=args.harness, root=root)
|
|
211
|
+
if args.json:
|
|
212
|
+
print(json.dumps(res, indent=2))
|
|
213
|
+
else:
|
|
214
|
+
if res["available"]:
|
|
215
|
+
print(f"Caveman available: {res['path']}")
|
|
216
|
+
else:
|
|
217
|
+
print(f"Caveman not available: {res['reason']}")
|
|
218
|
+
print(res["install_guidance"])
|
|
219
|
+
return 0 if res["available"] else 1
|
|
220
|
+
|
|
221
|
+
elif args.command == "resolve":
|
|
222
|
+
root = Path(args.cwd).resolve()
|
|
223
|
+
policy = resolve_policy(root)
|
|
224
|
+
res = resolve_activation(policy, harness=args.harness, root=root, warned=args.warned)
|
|
225
|
+
if args.json:
|
|
226
|
+
print(json.dumps(res, indent=2))
|
|
227
|
+
else:
|
|
228
|
+
print(f"Caveman preference: {res['preference']}, active: {res['active']}")
|
|
229
|
+
if res["warning"]:
|
|
230
|
+
print(res["warning"])
|
|
231
|
+
return 0
|
|
232
|
+
|
|
233
|
+
elif args.command == "guidance":
|
|
234
|
+
print(get_install_guidance())
|
|
235
|
+
return 0
|
|
236
|
+
|
|
237
|
+
else:
|
|
238
|
+
parser.print_help()
|
|
239
|
+
return 0
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
if __name__ == "__main__":
|
|
243
|
+
sys.exit(main())
|
|
@@ -28,6 +28,8 @@ DEFAULT_POLICY = {
|
|
|
28
28
|
"hooks": {"record": [], "deny": []},
|
|
29
29
|
"budget": {"corrections": 2, "contextShare": 0.15},
|
|
30
30
|
"dashboard": {"staleAfterSeconds": 900},
|
|
31
|
+
"caveman": False,
|
|
32
|
+
"execution": {"roles": {}},
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
REF_RE = re.compile(r"`?([a-z0-9][a-z0-9-]*)#(\d{2,})`?")
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Runtime model and effort discovery across supported harnesses."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import datetime
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Callable
|
|
14
|
+
|
|
15
|
+
SUPPORTED_HARNESSES = {"antigravity", "claude-code", "codex", "opencode"}
|
|
16
|
+
PROGRESS_CHAR_RE = re.compile(r"^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏\s\r\x1b\[0-9;]*Fetching available models\.\.\.", re.MULTILINE)
|
|
17
|
+
ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-9;]*[a-zA-Z]")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DiscoveryError(Exception):
|
|
21
|
+
"""Raised when runtime model or effort discovery cannot be established."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def clean_ansi(text: str) -> str:
|
|
25
|
+
"""Remove ANSI escape sequences and carriage returns."""
|
|
26
|
+
return ANSI_ESCAPE_RE.sub("", text).replace("\r", "")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def parse_agy_models_output(
|
|
30
|
+
raw_output: str,
|
|
31
|
+
metadata_lookup: dict[str, Any] | None = None,
|
|
32
|
+
) -> list[dict[str, Any]]:
|
|
33
|
+
"""Parse stdout from `agy models` into a structured list of model dicts without inventing windows."""
|
|
34
|
+
cleaned = clean_ansi(raw_output)
|
|
35
|
+
models: list[dict[str, Any]] = []
|
|
36
|
+
seen: set[str] = set()
|
|
37
|
+
|
|
38
|
+
for line in cleaned.splitlines():
|
|
39
|
+
line = line.strip()
|
|
40
|
+
if not line or line.startswith("Fetching available models") or line.startswith("Usage:"):
|
|
41
|
+
continue
|
|
42
|
+
parts = line.split(None, 1)
|
|
43
|
+
if not parts:
|
|
44
|
+
continue
|
|
45
|
+
model_id = parts[0].strip()
|
|
46
|
+
display_name = parts[1].strip() if len(parts) > 1 else model_id
|
|
47
|
+
|
|
48
|
+
if model_id in seen:
|
|
49
|
+
continue
|
|
50
|
+
seen.add(model_id)
|
|
51
|
+
|
|
52
|
+
entry: dict[str, Any] = {
|
|
53
|
+
"id": model_id,
|
|
54
|
+
"name": display_name,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Attach verified context metadata or effort if declared/verified
|
|
58
|
+
if metadata_lookup and model_id in metadata_lookup:
|
|
59
|
+
meta = metadata_lookup[model_id]
|
|
60
|
+
if isinstance(meta, dict):
|
|
61
|
+
if "contextWindow" in meta:
|
|
62
|
+
entry["contextWindow"] = meta["contextWindow"]
|
|
63
|
+
if "supportedEfforts" in meta:
|
|
64
|
+
entry["supportedEfforts"] = meta["supportedEfforts"]
|
|
65
|
+
if "effort" in meta:
|
|
66
|
+
entry["effort"] = meta["effort"]
|
|
67
|
+
|
|
68
|
+
models.append(entry)
|
|
69
|
+
|
|
70
|
+
return models
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def fetch_paginated_catalog(fetcher: Callable[[str | None], dict[str, Any]]) -> list[dict[str, Any]]:
|
|
74
|
+
"""Fetch all pages from a paginated catalog source."""
|
|
75
|
+
all_models: list[dict[str, Any]] = []
|
|
76
|
+
page_token: str | None = None
|
|
77
|
+
|
|
78
|
+
while True:
|
|
79
|
+
res = fetcher(page_token)
|
|
80
|
+
models = res.get("models", [])
|
|
81
|
+
all_models.extend(models)
|
|
82
|
+
page_token = res.get("next_page_token") or res.get("nextPageToken")
|
|
83
|
+
if not page_token:
|
|
84
|
+
break
|
|
85
|
+
|
|
86
|
+
return all_models
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def discover_antigravity_models(runner: Callable[..., subprocess.CompletedProcess[str]] | None = None) -> list[dict[str, Any]]:
|
|
90
|
+
"""Discover executable models via `agy models`."""
|
|
91
|
+
agy_path = shutil.which("agy")
|
|
92
|
+
if not agy_path:
|
|
93
|
+
raise DiscoveryError("Missing discovery: agy CLI not found in PATH")
|
|
94
|
+
|
|
95
|
+
run_cmd = runner or subprocess.run
|
|
96
|
+
try:
|
|
97
|
+
proc = run_cmd([agy_path, "models"], capture_output=True, text=True, check=False)
|
|
98
|
+
except OSError as exc:
|
|
99
|
+
raise DiscoveryError(f"Missing discovery: failed to run agy models: {exc}") from exc
|
|
100
|
+
|
|
101
|
+
if proc.returncode != 0:
|
|
102
|
+
err = proc.stderr.strip() or proc.stdout.strip()
|
|
103
|
+
raise DiscoveryError(f"Missing discovery: agy models returned exit code {proc.returncode}: {err}")
|
|
104
|
+
|
|
105
|
+
cap_file = Path(__file__).resolve().parents[1] / "capabilities" / "antigravity.json"
|
|
106
|
+
metadata_lookup = {}
|
|
107
|
+
if cap_file.exists():
|
|
108
|
+
try:
|
|
109
|
+
metadata_lookup = json.loads(cap_file.read_text(encoding="utf-8")).get("models", {})
|
|
110
|
+
except Exception:
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
models = parse_agy_models_output(proc.stdout, metadata_lookup=metadata_lookup)
|
|
114
|
+
if not models:
|
|
115
|
+
raise DiscoveryError("Missing discovery: agy models returned an empty model list")
|
|
116
|
+
return models
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def discover_models(harness: str, runner: Callable[..., Any] | None = None) -> list[dict[str, Any]]:
|
|
120
|
+
"""Discover executable models for the given harness; fail closed on missing discovery."""
|
|
121
|
+
h = (harness or "").lower()
|
|
122
|
+
if h not in SUPPORTED_HARNESSES:
|
|
123
|
+
raise DiscoveryError(f"Missing discovery: unsupported harness {harness!r}. Supported: {sorted(SUPPORTED_HARNESSES)}")
|
|
124
|
+
|
|
125
|
+
if h == "antigravity":
|
|
126
|
+
return discover_antigravity_models(runner=runner)
|
|
127
|
+
elif h == "claude-code":
|
|
128
|
+
claude_path = shutil.which("claude")
|
|
129
|
+
if not claude_path:
|
|
130
|
+
raise DiscoveryError("Missing discovery: claude CLI not found in PATH")
|
|
131
|
+
cap_file = Path(__file__).resolve().parents[1] / "capabilities" / "claude-code.json"
|
|
132
|
+
if cap_file.exists():
|
|
133
|
+
cap = json.loads(cap_file.read_text(encoding="utf-8"))
|
|
134
|
+
return [{"id": m, "contextWindow": d["contextWindow"]} for m, d in cap.get("models", {}).items()]
|
|
135
|
+
raise DiscoveryError("Missing discovery: claude capability declaration missing")
|
|
136
|
+
elif h == "opencode":
|
|
137
|
+
opencode_path = shutil.which("opencode")
|
|
138
|
+
if not opencode_path:
|
|
139
|
+
raise DiscoveryError("Missing discovery: opencode CLI not found in PATH")
|
|
140
|
+
cap_file = Path(__file__).resolve().parents[1] / "capabilities" / "opencode.json"
|
|
141
|
+
if cap_file.exists():
|
|
142
|
+
cap = json.loads(cap_file.read_text(encoding="utf-8"))
|
|
143
|
+
return [{"id": m, "contextWindow": d["contextWindow"]} for m, d in cap.get("models", {}).items()]
|
|
144
|
+
raise DiscoveryError("Missing discovery: opencode capability declaration missing")
|
|
145
|
+
elif h == "codex":
|
|
146
|
+
codex_path = shutil.which("codex")
|
|
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")
|
|
154
|
+
|
|
155
|
+
raise DiscoveryError(f"Missing discovery: unhandled harness {harness}")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def sanitize_no_credentials(data: Any) -> Any:
|
|
159
|
+
"""Recursively scrub credential keys from dictionary."""
|
|
160
|
+
if isinstance(data, dict):
|
|
161
|
+
cleaned: dict[str, Any] = {}
|
|
162
|
+
for k, v in data.items():
|
|
163
|
+
k_lower = str(k).lower()
|
|
164
|
+
if any(secret in k_lower for secret in ("token", "secret", "password", "api_key", "auth")):
|
|
165
|
+
continue
|
|
166
|
+
cleaned[k] = sanitize_no_credentials(v)
|
|
167
|
+
return cleaned
|
|
168
|
+
elif isinstance(data, list):
|
|
169
|
+
return [sanitize_no_credentials(item) for item in data]
|
|
170
|
+
return data
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def create_catalog_entry(
|
|
174
|
+
harness: str,
|
|
175
|
+
models: list[dict[str, Any]],
|
|
176
|
+
provider: str = "",
|
|
177
|
+
account: str = "",
|
|
178
|
+
source_command: str = "",
|
|
179
|
+
) -> dict[str, Any]:
|
|
180
|
+
"""Create a catalog entry with provenance, freshness, and no credentials."""
|
|
181
|
+
entry = {
|
|
182
|
+
"harness": harness,
|
|
183
|
+
"provider": provider,
|
|
184
|
+
"account": account,
|
|
185
|
+
"models": models,
|
|
186
|
+
"provenance": {
|
|
187
|
+
"source": source_command or f"{harness} discovery",
|
|
188
|
+
"discovered_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
189
|
+
"freshness_seconds": 3600,
|
|
190
|
+
},
|
|
191
|
+
}
|
|
192
|
+
return sanitize_no_credentials(entry)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def is_catalog_valid(
|
|
196
|
+
catalog: dict[str, Any],
|
|
197
|
+
current_provider: str = "",
|
|
198
|
+
current_account: str = "",
|
|
199
|
+
) -> bool:
|
|
200
|
+
"""Check whether a catalog entry is valid for the current provider and account."""
|
|
201
|
+
if not isinstance(catalog, dict) or "models" not in catalog:
|
|
202
|
+
return False
|
|
203
|
+
if current_provider and catalog.get("provider") != current_provider:
|
|
204
|
+
return False
|
|
205
|
+
if current_account and catalog.get("account") != current_account:
|
|
206
|
+
return False
|
|
207
|
+
return True
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def validate_effort(
|
|
211
|
+
model_id: str,
|
|
212
|
+
effort: str | None,
|
|
213
|
+
supported_efforts: list[str] | None = None,
|
|
214
|
+
) -> dict[str, Any]:
|
|
215
|
+
"""Validate that the requested reasoning effort is supported by the model."""
|
|
216
|
+
if not effort:
|
|
217
|
+
return {"valid": True}
|
|
218
|
+
if not supported_efforts:
|
|
219
|
+
return {
|
|
220
|
+
"valid": False,
|
|
221
|
+
"error": f"Model {model_id} does not declare supported reasoning effort values.",
|
|
222
|
+
}
|
|
223
|
+
normalized_supported = [e.lower() for e in supported_efforts]
|
|
224
|
+
if effort.lower() not in normalized_supported:
|
|
225
|
+
return {
|
|
226
|
+
"valid": False,
|
|
227
|
+
"error": f"Unsupported effort value {effort!r} for model {model_id}. Supported: {supported_efforts}",
|
|
228
|
+
}
|
|
229
|
+
return {"valid": True}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def main() -> int:
|
|
233
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
234
|
+
parser.add_argument("--harness", default="antigravity", help="harness to discover models for")
|
|
235
|
+
parser.add_argument("--json", action="store_true", help="output JSON")
|
|
236
|
+
args = parser.parse_args()
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
models = discover_models(args.harness)
|
|
240
|
+
catalog = create_catalog_entry(
|
|
241
|
+
harness=args.harness,
|
|
242
|
+
models=models,
|
|
243
|
+
source_command=f"gantry discovery --harness {args.harness}",
|
|
244
|
+
)
|
|
245
|
+
if args.json:
|
|
246
|
+
print(json.dumps(catalog, indent=2))
|
|
247
|
+
else:
|
|
248
|
+
print(f"Discovered {len(models)} model(s) for {args.harness}:")
|
|
249
|
+
for m in models:
|
|
250
|
+
eff = f" (effort: {m['effort']})" if "effort" in m else ""
|
|
251
|
+
print(f" - {m['id']}: window={m.get('contextWindow')}{eff}")
|
|
252
|
+
return 0
|
|
253
|
+
except DiscoveryError as exc:
|
|
254
|
+
if args.json:
|
|
255
|
+
print(json.dumps({"error": str(exc)}, indent=2))
|
|
256
|
+
else:
|
|
257
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
258
|
+
return 1
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
if __name__ == "__main__":
|
|
262
|
+
sys.exit(main())
|