stargate-cli 1.0.0__py3-none-any.whl
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.
- stargate/__init__.py +0 -0
- stargate/__main__.py +3 -0
- stargate/agent.py +194 -0
- stargate/agents.yaml +148 -0
- stargate/cli.py +286 -0
- stargate/commit.py +253 -0
- stargate/config.py +422 -0
- stargate/core.py +341 -0
- stargate/detect.py +140 -0
- stargate/doctor.py +407 -0
- stargate/fanout.py +1152 -0
- stargate/prompts/architect.md +28 -0
- stargate/prompts/developer.md +27 -0
- stargate/prompts/fanout.md +44 -0
- stargate/prompts/fixer.md +35 -0
- stargate/prompts/reviewer.md +86 -0
- stargate/publish.py +105 -0
- stargate/run.py +847 -0
- stargate/source.py +66 -0
- stargate/stages.py +1131 -0
- stargate_cli-1.0.0.dist-info/METADATA +1427 -0
- stargate_cli-1.0.0.dist-info/RECORD +26 -0
- stargate_cli-1.0.0.dist-info/WHEEL +5 -0
- stargate_cli-1.0.0.dist-info/entry_points.txt +2 -0
- stargate_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- stargate_cli-1.0.0.dist-info/top_level.txt +1 -0
stargate/doctor.py
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
"""`stargate doctor`: report the effective configuration and, on request,
|
|
2
|
+
probe each distinct agent for the capabilities its role needs."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shlex
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import tempfile
|
|
9
|
+
import time
|
|
10
|
+
import uuid
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import yaml
|
|
16
|
+
|
|
17
|
+
from .config import (
|
|
18
|
+
AGENT_RETRIES_DEFAULT,
|
|
19
|
+
AGENT_RETRY_BACKOFF_DEFAULT,
|
|
20
|
+
PROMPTS,
|
|
21
|
+
ROLES,
|
|
22
|
+
TEST_COMMAND_PLACEHOLDER,
|
|
23
|
+
agent_command,
|
|
24
|
+
agent_entry,
|
|
25
|
+
agent_env,
|
|
26
|
+
blocking_severities,
|
|
27
|
+
commit_enabled,
|
|
28
|
+
env_summary,
|
|
29
|
+
expand_test_command,
|
|
30
|
+
find_prompt,
|
|
31
|
+
prompt_dirs,
|
|
32
|
+
pull_request_command,
|
|
33
|
+
task_sources,
|
|
34
|
+
test_command_grant,
|
|
35
|
+
token_cap,
|
|
36
|
+
value_source,
|
|
37
|
+
)
|
|
38
|
+
from .core import StargateError
|
|
39
|
+
from .detect import detection_mode, selected_test_command
|
|
40
|
+
|
|
41
|
+
PROBE_TIMEOUT_DEFAULT = 120
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
PROBE_CAPABILITIES = ("read", "write")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class Capability:
|
|
49
|
+
"""A file operation that a probe must demonstrate, not merely describe."""
|
|
50
|
+
|
|
51
|
+
kind: str
|
|
52
|
+
path: Path
|
|
53
|
+
marker: str = ""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def unique_agents(config: dict[str, Any]) -> dict[Any, tuple[list[str], Any, dict[str, Any]]]:
|
|
57
|
+
"""Distinct agent invocations: the four default roles map onto two commands,
|
|
58
|
+
and probing per role would bill twice for nothing.
|
|
59
|
+
|
|
60
|
+
Identity is command AND environment. Two roles running the same command
|
|
61
|
+
under different credentials are two different things to verify -- deduping
|
|
62
|
+
on the command alone would report one of them without ever calling it.
|
|
63
|
+
Keeping the entry that declared the probe also keeps its prompt and
|
|
64
|
+
capability expectation together when only a later duplicate declares one.
|
|
65
|
+
"""
|
|
66
|
+
agents: dict[Any, tuple[list[str], Any, dict[str, Any]]] = {}
|
|
67
|
+
for role in ROLES:
|
|
68
|
+
entry = agent_entry(config, role)
|
|
69
|
+
declared = entry.get("env") or {}
|
|
70
|
+
key = (
|
|
71
|
+
tuple(agent_command(config, role)),
|
|
72
|
+
tuple(sorted((str(k), v) for k, v in declared.items()))
|
|
73
|
+
if isinstance(declared, dict) else None,
|
|
74
|
+
)
|
|
75
|
+
names, prober, first = agents.get(key, ([], None, entry))
|
|
76
|
+
names.append(config["workflow"][role])
|
|
77
|
+
agents[key] = (
|
|
78
|
+
names,
|
|
79
|
+
prober if prober is not None else (
|
|
80
|
+
entry if entry.get("probe") is not None else None
|
|
81
|
+
),
|
|
82
|
+
first,
|
|
83
|
+
)
|
|
84
|
+
return agents
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def probe_one(command: tuple[str, ...], prompt: str, cwd: Path, output: Path,
|
|
88
|
+
timeout: float | None, env: dict[str, str] | None,
|
|
89
|
+
test_command: str,
|
|
90
|
+
capability: Capability | None = None) -> str:
|
|
91
|
+
"""Empty string on success, otherwise the reason it failed."""
|
|
92
|
+
writes_final = any("{output}" in part for part in command)
|
|
93
|
+
cmd = [part.replace("{output}", str(output)) for part in command]
|
|
94
|
+
cmd = expand_test_command(cmd, test_command)
|
|
95
|
+
try:
|
|
96
|
+
proc = subprocess.run(
|
|
97
|
+
[*cmd, prompt], cwd=cwd, text=True, stdout=subprocess.PIPE,
|
|
98
|
+
stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL, timeout=timeout,
|
|
99
|
+
env=env,
|
|
100
|
+
)
|
|
101
|
+
except subprocess.TimeoutExpired:
|
|
102
|
+
return f"probe timed out after {timeout}s"
|
|
103
|
+
except OSError as exc:
|
|
104
|
+
return str(exc)
|
|
105
|
+
|
|
106
|
+
if proc.returncode:
|
|
107
|
+
return proc.stdout.strip() or f"agent exited with status {proc.returncode}"
|
|
108
|
+
# Exit 0 while writing nothing to {output} is the false positive this flag
|
|
109
|
+
# exists to remove: invoke_agent would kill the run at the first real stage.
|
|
110
|
+
if writes_final:
|
|
111
|
+
if not (output.read_text() if output.exists() else "").strip():
|
|
112
|
+
return ("agent declares {output} but wrote nothing; check that its "
|
|
113
|
+
"CLI supports the configured flag")
|
|
114
|
+
if capability is None:
|
|
115
|
+
return ""
|
|
116
|
+
if capability.kind == "write":
|
|
117
|
+
if not capability.path.is_file() or not capability.path.stat().st_size:
|
|
118
|
+
return (
|
|
119
|
+
f"agent exited 0 but did not write {capability.path.name}; "
|
|
120
|
+
"its file-editing tools are not working"
|
|
121
|
+
)
|
|
122
|
+
return ""
|
|
123
|
+
|
|
124
|
+
# A prose-only answer cannot guess this marker, so returning it proves the
|
|
125
|
+
# role actually used its file-reading tools in the isolated repository.
|
|
126
|
+
answer = (
|
|
127
|
+
output.read_text() if output.exists() else ""
|
|
128
|
+
) if writes_final else proc.stdout
|
|
129
|
+
if capability.marker not in answer:
|
|
130
|
+
return (
|
|
131
|
+
"agent exited 0 but did not return the marker seeded in "
|
|
132
|
+
f"{capability.path.name}; its file-reading tools are not working"
|
|
133
|
+
)
|
|
134
|
+
return ""
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def probe_agents(config: dict[str, Any], test_command: str) -> bool:
|
|
138
|
+
"""Make one real, billable call per distinct agent. Opt-in only."""
|
|
139
|
+
print("\nAgent probes:")
|
|
140
|
+
git_bin = shutil.which("git")
|
|
141
|
+
if not git_bin:
|
|
142
|
+
print(" SKIP probes (git is required for the isolated probe directory)")
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
settings = config.get("settings", {})
|
|
146
|
+
timeout = float(settings.get("probe_timeout_seconds", PROBE_TIMEOUT_DEFAULT)) or None
|
|
147
|
+
ok = True
|
|
148
|
+
with tempfile.TemporaryDirectory(prefix="stargate-doctor-") as tmp:
|
|
149
|
+
cwd = Path(tmp)
|
|
150
|
+
try:
|
|
151
|
+
# Probes run outside the repo: the default agents are
|
|
152
|
+
# --sandbox workspace-write, and codex refuses a non-git directory.
|
|
153
|
+
subprocess.run([git_bin, "init", "-q"], cwd=cwd, check=True,
|
|
154
|
+
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
|
155
|
+
except (OSError, subprocess.CalledProcessError) as exc:
|
|
156
|
+
detail = (getattr(exc, "stdout", None) or str(exc)).strip()
|
|
157
|
+
print(" FAIL probe setup")
|
|
158
|
+
print(" " + detail.replace("\n", "\n "))
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
for index, (key, (names, prober, entry)) in enumerate(
|
|
162
|
+
unique_agents(config).items()
|
|
163
|
+
):
|
|
164
|
+
command = key[0]
|
|
165
|
+
label = ", ".join(dict.fromkeys(names))
|
|
166
|
+
if overrides := env_summary(entry):
|
|
167
|
+
label += f" (env: {overrides})"
|
|
168
|
+
if prober is None:
|
|
169
|
+
if entry.get("probe_expect") is not None:
|
|
170
|
+
print(f" FAIL {label} (probe_expect needs a probe prompt)")
|
|
171
|
+
ok = False
|
|
172
|
+
continue
|
|
173
|
+
print(f" SKIP {label} (no probe configured)")
|
|
174
|
+
continue
|
|
175
|
+
prompt = prober.get("probe")
|
|
176
|
+
if not isinstance(prompt, str) or not prompt.strip():
|
|
177
|
+
print(f" FAIL {label} (probe must be a non-empty string)")
|
|
178
|
+
ok = False
|
|
179
|
+
continue
|
|
180
|
+
capability = None
|
|
181
|
+
expect = prober.get("probe_expect")
|
|
182
|
+
probe_path = cwd / f"probe-{index}.txt"
|
|
183
|
+
if expect is not None:
|
|
184
|
+
if expect not in PROBE_CAPABILITIES:
|
|
185
|
+
print(
|
|
186
|
+
f" FAIL {label} (probe_expect must be one of "
|
|
187
|
+
f"{', '.join(PROBE_CAPABILITIES)})"
|
|
188
|
+
)
|
|
189
|
+
ok = False
|
|
190
|
+
continue
|
|
191
|
+
marker = f"stargate-{uuid.uuid4().hex[:12]}"
|
|
192
|
+
if expect == "read":
|
|
193
|
+
probe_path.write_text(marker + "\n")
|
|
194
|
+
capability = Capability(expect, probe_path, marker)
|
|
195
|
+
label += f" ({expect})"
|
|
196
|
+
prompt = prompt.replace("{probe_file}", str(probe_path))
|
|
197
|
+
started = time.monotonic()
|
|
198
|
+
error = probe_one(
|
|
199
|
+
command, prompt, cwd, cwd / f"output-{index}.txt", timeout,
|
|
200
|
+
agent_env(entry), test_command, capability,
|
|
201
|
+
)
|
|
202
|
+
print(f" {'FAIL' if error else 'OK':4} {label} [{time.monotonic() - started:.1f}s]")
|
|
203
|
+
if error:
|
|
204
|
+
print(" " + error.replace("\n", "\n "))
|
|
205
|
+
ok = False
|
|
206
|
+
return ok
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def doctor(
|
|
210
|
+
config: dict[str, Any],
|
|
211
|
+
layers: list[tuple[Path, dict[str, Any]]],
|
|
212
|
+
script_dir: Path,
|
|
213
|
+
*,
|
|
214
|
+
probe: bool = False,
|
|
215
|
+
explicit_config: bool = False,
|
|
216
|
+
) -> int:
|
|
217
|
+
print("stargate doctor\n")
|
|
218
|
+
qualifier = (
|
|
219
|
+
"explicit --config; used exactly as given"
|
|
220
|
+
if explicit_config else "most specific first"
|
|
221
|
+
)
|
|
222
|
+
print(f"Config sources ({qualifier}):")
|
|
223
|
+
packaged_path = (script_dir / "agents.yaml").resolve()
|
|
224
|
+
for index, (path, _) in enumerate(layers, 1):
|
|
225
|
+
suffix = " (packaged defaults)" if path == packaged_path else ""
|
|
226
|
+
print(f" [{index}] {path}{suffix}")
|
|
227
|
+
print()
|
|
228
|
+
settings = config.get("settings", {})
|
|
229
|
+
mode = detection_mode(config)
|
|
230
|
+
commit_enabled(config)
|
|
231
|
+
configured_test_command = str(
|
|
232
|
+
settings.get("test_command", "") or ""
|
|
233
|
+
).strip()
|
|
234
|
+
test_command, candidates = selected_test_command(config, Path.cwd())
|
|
235
|
+
commands = {
|
|
236
|
+
role: expand_test_command(agent_command(config, role), test_command)
|
|
237
|
+
for role in ROLES
|
|
238
|
+
}
|
|
239
|
+
ok = True
|
|
240
|
+
binaries = {"git"}
|
|
241
|
+
try:
|
|
242
|
+
sources = task_sources(config)
|
|
243
|
+
pr_command = pull_request_command(config)
|
|
244
|
+
except StargateError as exc:
|
|
245
|
+
# Returns instead of setting ok=False like blocking_severities does, and
|
|
246
|
+
# the difference is position: this runs before the probe block, so
|
|
247
|
+
# continuing would pay for real agent calls under a config `run` refuses.
|
|
248
|
+
print(f"\nERROR {exc}")
|
|
249
|
+
return 1
|
|
250
|
+
if pr_command:
|
|
251
|
+
binaries.add(pr_command[0])
|
|
252
|
+
for source in sources:
|
|
253
|
+
for command in source["commands"]:
|
|
254
|
+
binaries.add(command[0])
|
|
255
|
+
for role in ROLES:
|
|
256
|
+
binaries.add(commands[role][0])
|
|
257
|
+
|
|
258
|
+
for binary in sorted(binaries):
|
|
259
|
+
path = shutil.which(binary)
|
|
260
|
+
state = "FOUND" if path else "MISSING"
|
|
261
|
+
print(f"{state:8} {binary:12} {path or ''}")
|
|
262
|
+
ok = ok and bool(path)
|
|
263
|
+
print(
|
|
264
|
+
"\nFOUND means the executable is on PATH. Authentication, credits, quota\n"
|
|
265
|
+
"and model availability are NOT checked -- an agent can still fail on its\n"
|
|
266
|
+
"first call (e.g. \"Credit balance is too low\")."
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
if probe:
|
|
270
|
+
ok = probe_agents(config, test_command) and ok
|
|
271
|
+
|
|
272
|
+
packaged = yaml.safe_load((script_dir / "agents.yaml").read_text()) or {}
|
|
273
|
+
mine, theirs = config.get("version"), packaged.get("version")
|
|
274
|
+
if mine is not None and theirs is not None and mine != theirs:
|
|
275
|
+
print(
|
|
276
|
+
f"\nWARN config version {mine} differs from the packaged version "
|
|
277
|
+
f"{theirs}.\n Newer defaults may be missing; compare against "
|
|
278
|
+
f"{script_dir / 'agents.yaml'}."
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
blocking_severities(config)
|
|
283
|
+
except StargateError as exc:
|
|
284
|
+
# Reporting a value that `run` will refuse would make doctor the wrong
|
|
285
|
+
# place to find out, which is the one thing it exists for.
|
|
286
|
+
print(f"\nERROR {exc}")
|
|
287
|
+
ok = False
|
|
288
|
+
|
|
289
|
+
print("\nEffective settings:")
|
|
290
|
+
for key, default in (
|
|
291
|
+
("max_review_loops", 2),
|
|
292
|
+
("blocking_severities", []),
|
|
293
|
+
("max_fanout_tasks", 8),
|
|
294
|
+
("max_parallel_tasks", 2),
|
|
295
|
+
("test_command", ""),
|
|
296
|
+
("test_command_detection", "report"),
|
|
297
|
+
("commit", True),
|
|
298
|
+
("max_task_tokens", 0),
|
|
299
|
+
("agent_timeout_seconds", 1800),
|
|
300
|
+
("agent_retries", AGENT_RETRIES_DEFAULT),
|
|
301
|
+
("agent_retry_backoff_seconds", AGENT_RETRY_BACKOFF_DEFAULT),
|
|
302
|
+
("test_timeout_seconds", 900),
|
|
303
|
+
("worktree_root", ""),
|
|
304
|
+
("prompts_dir", ""),
|
|
305
|
+
):
|
|
306
|
+
value = settings.get(key, default)
|
|
307
|
+
print(f" {key:22} {value!r} {value_source(layers, 'settings', key)}")
|
|
308
|
+
|
|
309
|
+
print("\nTest command:")
|
|
310
|
+
if configured_test_command:
|
|
311
|
+
source = value_source(layers, "settings", "test_command")
|
|
312
|
+
print(f" configured {configured_test_command!r} {source}")
|
|
313
|
+
elif mode == "off":
|
|
314
|
+
print(" (not configured; detection is off)")
|
|
315
|
+
else:
|
|
316
|
+
print(" (not configured)")
|
|
317
|
+
for candidate in candidates:
|
|
318
|
+
print(f" detected {candidate.command:16} {candidate.source}")
|
|
319
|
+
if not candidates:
|
|
320
|
+
print(" No likely project test command detected.")
|
|
321
|
+
elif mode == "auto":
|
|
322
|
+
print(f" Detection is automatic; {candidates[0].command!r} will run.")
|
|
323
|
+
else:
|
|
324
|
+
print(
|
|
325
|
+
" Detection is report-only. To use the first candidate, "
|
|
326
|
+
"add to .stargate.yaml:"
|
|
327
|
+
)
|
|
328
|
+
print(" settings:")
|
|
329
|
+
print(f" test_command: {candidates[0].command!r}")
|
|
330
|
+
|
|
331
|
+
cap = token_cap(config)
|
|
332
|
+
print("\nAgents:")
|
|
333
|
+
architect_declares_test_command = False
|
|
334
|
+
for role in ROLES:
|
|
335
|
+
agent_name = config["workflow"][role]
|
|
336
|
+
agent_source = value_source(layers, "agents", agent_name)
|
|
337
|
+
workflow_source = value_source(layers, "workflow", role)
|
|
338
|
+
entry = agent_entry(config, role)
|
|
339
|
+
raw_command = agent_command(config, role)
|
|
340
|
+
declares_test_command = any(
|
|
341
|
+
TEST_COMMAND_PLACEHOLDER in part for part in raw_command
|
|
342
|
+
)
|
|
343
|
+
print(
|
|
344
|
+
f" {role:10} {agent_source:9} "
|
|
345
|
+
f"{shlex.join(commands[role])}"
|
|
346
|
+
)
|
|
347
|
+
if workflow_source != agent_source:
|
|
348
|
+
print(f" {'':10} {'':9} └─ role mapped by {workflow_source}")
|
|
349
|
+
if overrides := env_summary(entry):
|
|
350
|
+
print(f" {'':10} {'':9} └─ env: {overrides}")
|
|
351
|
+
if declares_test_command:
|
|
352
|
+
architect_declares_test_command = (
|
|
353
|
+
architect_declares_test_command or role == "architect"
|
|
354
|
+
)
|
|
355
|
+
if grant := test_command_grant(test_command):
|
|
356
|
+
print(
|
|
357
|
+
f" {'':10} {'':9} └─ may run the test command: {grant}"
|
|
358
|
+
)
|
|
359
|
+
elif test_command:
|
|
360
|
+
print(
|
|
361
|
+
f" {'':10} {'':9} └─ {{test_command}}: {test_command!r} "
|
|
362
|
+
"contains a permission-pattern metacharacter or control "
|
|
363
|
+
"character; it was not interpolated and the grant is dropped"
|
|
364
|
+
)
|
|
365
|
+
else:
|
|
366
|
+
print(
|
|
367
|
+
f" {'':10} {'':9} └─ {{test_command}}: no test command "
|
|
368
|
+
"will run; the grant and its flag are dropped"
|
|
369
|
+
)
|
|
370
|
+
if cap:
|
|
371
|
+
meters = "reports usage" if entry.get("usage_pattern") else "no usage_pattern"
|
|
372
|
+
print(f" {'':10} {'':9} └─ {meters}")
|
|
373
|
+
|
|
374
|
+
if architect_declares_test_command:
|
|
375
|
+
print(
|
|
376
|
+
"\nWARN the architect declares {test_command}. It runs in your real "
|
|
377
|
+
"repository,\n not the worktree, so this can grant command "
|
|
378
|
+
"execution there."
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
print("\nTask sources:")
|
|
382
|
+
if not sources:
|
|
383
|
+
print(" (none configured; 'stargate run --from' needs one)")
|
|
384
|
+
for source in sources:
|
|
385
|
+
print(f" {', '.join(source['hosts'])}")
|
|
386
|
+
for command in source["commands"]:
|
|
387
|
+
print(f" $ {shlex.join(command)}")
|
|
388
|
+
if overrides := env_summary(source):
|
|
389
|
+
print(f" └─ env: {overrides}")
|
|
390
|
+
|
|
391
|
+
if pr_command:
|
|
392
|
+
print("\nPull request:")
|
|
393
|
+
print(f" {shlex.join(pr_command)}")
|
|
394
|
+
if overrides := env_summary(config["pull_request"]):
|
|
395
|
+
print(f" └─ env: {overrides}")
|
|
396
|
+
print(" Published only when you pass --pr; configuration alone never publishes.")
|
|
397
|
+
|
|
398
|
+
print("\nPrompts:")
|
|
399
|
+
dirs = prompt_dirs(config, script_dir)
|
|
400
|
+
for role in PROMPTS:
|
|
401
|
+
try:
|
|
402
|
+
print(f" {role:10} {find_prompt(dirs, role)}")
|
|
403
|
+
except StargateError as exc:
|
|
404
|
+
print(f" {role:10} MISSING ({exc})")
|
|
405
|
+
ok = False
|
|
406
|
+
|
|
407
|
+
return 0 if ok else 1
|