@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
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
6
|
import json
|
|
7
|
+
import re
|
|
7
8
|
import sys
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from typing import Any
|
|
@@ -69,6 +70,59 @@ def load_schema(role: str) -> dict[str, Any]:
|
|
|
69
70
|
return schema
|
|
70
71
|
|
|
71
72
|
|
|
73
|
+
def extract_json(raw: str) -> Any:
|
|
74
|
+
"""Extract and parse JSON from raw string, handling markdown fences and delimiters."""
|
|
75
|
+
if not raw or not raw.strip():
|
|
76
|
+
raise json.JSONDecodeError("Expecting value: empty input", raw or "", 0)
|
|
77
|
+
|
|
78
|
+
text = raw.strip()
|
|
79
|
+
# 1. Try parsing directly
|
|
80
|
+
try:
|
|
81
|
+
return json.loads(text)
|
|
82
|
+
except json.JSONDecodeError:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
# 2. Extract from markdown code fence (```json ... ``` or ``` ... ```)
|
|
86
|
+
fence_pattern = re.compile(r"```+(?:json|JSON)?\s*\n([\s\S]*?)\n```+", re.MULTILINE)
|
|
87
|
+
matches = fence_pattern.findall(text)
|
|
88
|
+
for block in matches:
|
|
89
|
+
try:
|
|
90
|
+
return json.loads(block.strip())
|
|
91
|
+
except json.JSONDecodeError:
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
fence_pattern_inline = re.compile(r"```+(?:json|JSON)?\s*([\s\S]*?)\s*```+", re.MULTILINE)
|
|
95
|
+
matches_inline = fence_pattern_inline.findall(text)
|
|
96
|
+
for block in matches_inline:
|
|
97
|
+
try:
|
|
98
|
+
return json.loads(block.strip())
|
|
99
|
+
except json.JSONDecodeError:
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
# 3. Try outermost JSON object braces `{ ... }`
|
|
103
|
+
first_brace = text.find("{")
|
|
104
|
+
last_brace = text.rfind("}")
|
|
105
|
+
if first_brace != -1 and last_brace != -1 and last_brace > first_brace:
|
|
106
|
+
candidate = text[first_brace : last_brace + 1].strip()
|
|
107
|
+
try:
|
|
108
|
+
return json.loads(candidate)
|
|
109
|
+
except json.JSONDecodeError:
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
# 4. Try outermost JSON array brackets `[ ... ]`
|
|
113
|
+
first_bracket = text.find("[")
|
|
114
|
+
last_bracket = text.rfind("]")
|
|
115
|
+
if first_bracket != -1 and last_bracket != -1 and last_bracket > first_bracket:
|
|
116
|
+
candidate = text[first_bracket : last_bracket + 1].strip()
|
|
117
|
+
try:
|
|
118
|
+
return json.loads(candidate)
|
|
119
|
+
except json.JSONDecodeError:
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
# Fallback to direct json.loads to raise original JSONDecodeError
|
|
123
|
+
return json.loads(text)
|
|
124
|
+
|
|
125
|
+
|
|
72
126
|
def main() -> int:
|
|
73
127
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
74
128
|
parser.add_argument("--role", required=True, choices=ROLES, help="role contract to validate")
|
|
@@ -84,7 +138,8 @@ def main() -> int:
|
|
|
84
138
|
print(json.dumps(schema))
|
|
85
139
|
return 0
|
|
86
140
|
try:
|
|
87
|
-
|
|
141
|
+
raw_input = sys.stdin.read()
|
|
142
|
+
result = extract_json(raw_input)
|
|
88
143
|
except json.JSONDecodeError as error:
|
|
89
144
|
errors = [f"$: invalid JSON: {error.msg}"]
|
|
90
145
|
else:
|
|
@@ -34,12 +34,13 @@ EVENTS = {
|
|
|
34
34
|
"review.finding",
|
|
35
35
|
"role.selected",
|
|
36
36
|
"role.changed",
|
|
37
|
+
"operator.approved",
|
|
37
38
|
}
|
|
38
39
|
RUN_EVENTS = {"run.started", "run.resumed", "run.cancelled", "run.finished"}
|
|
39
40
|
ROUND_EVENTS = {"round.started", "round.finished"}
|
|
40
41
|
PHASE_EVENTS = {"phase.started", "phase.finished"}
|
|
41
42
|
SUBAGENT_EVENTS = {"subagent.started", "subagent.stopped"}
|
|
42
|
-
ISSUE_EVENTS = {"issue.done", "issue.blocked", "issue.paused", "refutation", "review.finding", "role.selected", "role.changed"}
|
|
43
|
+
ISSUE_EVENTS = {"issue.done", "issue.blocked", "issue.paused", "refutation", "review.finding", "role.selected", "role.changed", "operator.approved"}
|
|
43
44
|
FINISHED_EVENTS = {"run.cancelled", "run.finished"}
|
|
44
45
|
UNIT_ID_RE = re.compile(r"^[0-9a-f]{12}$")
|
|
45
46
|
RUN_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]*$")
|
|
@@ -15,24 +15,97 @@ def merge_dicts(base: dict, update: dict) -> dict:
|
|
|
15
15
|
base[k] = v
|
|
16
16
|
return base
|
|
17
17
|
|
|
18
|
+
def detect_codex(repo_root: Path) -> bool:
|
|
19
|
+
"""Detect presence of Codex CLI on PATH or .codex directory in project."""
|
|
20
|
+
return bool(
|
|
21
|
+
shutil.which("codex")
|
|
22
|
+
or (repo_root / ".codex").exists()
|
|
23
|
+
or os.environ.get("CODEX_HOME")
|
|
24
|
+
or os.environ.get("CODEX_CLI")
|
|
25
|
+
)
|
|
26
|
+
|
|
18
27
|
def main() -> None:
|
|
19
28
|
parser = argparse.ArgumentParser(description="Gantry Setup config writer")
|
|
20
29
|
parser.add_argument("--config", help="JSON config string")
|
|
21
30
|
parser.add_argument("--config-file", help="Path to JSON config file")
|
|
31
|
+
parser.add_argument("--harness", choices=["codex", "claude-code", "antigravity", "opencode"], help="Explicit target harness")
|
|
32
|
+
parser.add_argument("--verify-auth", action="store_true", help="Explicitly guide/verify authentication and discovery before finalizing")
|
|
22
33
|
args = parser.parse_args()
|
|
23
34
|
|
|
35
|
+
repo_root = Path.cwd()
|
|
36
|
+
config = None
|
|
37
|
+
|
|
24
38
|
if args.config_file:
|
|
25
39
|
config = json.loads(Path(args.config_file).read_text(encoding="utf-8"))
|
|
26
40
|
elif args.config:
|
|
27
41
|
config = json.loads(args.config)
|
|
42
|
+
elif args.harness == "codex":
|
|
43
|
+
config = {
|
|
44
|
+
"execution": {
|
|
45
|
+
"hostHarness": "codex",
|
|
46
|
+
"roles": {
|
|
47
|
+
"plan": {"harness": "codex", "model": "gpt-5.2-codex"},
|
|
48
|
+
"implement": {"harness": "codex", "model": "gpt-5.2-codex"},
|
|
49
|
+
"review": {"harness": "codex", "model": "gpt-5.2-codex"},
|
|
50
|
+
"critic": {"harness": "codex", "model": "gpt-5.2-codex"},
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
}
|
|
28
54
|
else:
|
|
55
|
+
# Interactive detection when no config is passed
|
|
56
|
+
if detect_codex(repo_root):
|
|
57
|
+
source = "PATH" if shutil.which("codex") else ".codex"
|
|
58
|
+
print(f"Detected Codex in environment ({source}).")
|
|
59
|
+
try:
|
|
60
|
+
ans = input("Configure Codex as host harness? [Y/n] ").strip().lower()
|
|
61
|
+
except (EOFError, KeyboardInterrupt):
|
|
62
|
+
ans = "n"
|
|
63
|
+
if ans in ("", "y", "yes"):
|
|
64
|
+
config = {
|
|
65
|
+
"execution": {
|
|
66
|
+
"hostHarness": "codex",
|
|
67
|
+
"roles": {
|
|
68
|
+
"plan": {"harness": "codex", "model": "gpt-5.2-codex"},
|
|
69
|
+
"implement": {"harness": "codex", "model": "gpt-5.2-codex"},
|
|
70
|
+
"review": {"harness": "codex", "model": "gpt-5.2-codex"},
|
|
71
|
+
"critic": {"harness": "codex", "model": "gpt-5.2-codex"},
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if config is None:
|
|
29
77
|
print("Error: No config provided", file=sys.stderr)
|
|
30
78
|
sys.exit(2)
|
|
31
79
|
|
|
80
|
+
if args.harness:
|
|
81
|
+
if "execution" not in config or not isinstance(config["execution"], dict):
|
|
82
|
+
config["execution"] = {}
|
|
83
|
+
config["execution"]["hostHarness"] = args.harness
|
|
84
|
+
|
|
85
|
+
is_codex = (
|
|
86
|
+
(args.harness == "codex")
|
|
87
|
+
or (isinstance(config, dict) and config.get("execution", {}).get("hostHarness") == "codex")
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if is_codex or args.verify_auth:
|
|
91
|
+
print("Codex host harness selected or detected.")
|
|
92
|
+
print("Guidance: Ensure Codex CLI is authenticated via `codex login` before execution.")
|
|
93
|
+
print("Testing Codex model discovery...")
|
|
94
|
+
scripts_dir = Path(__file__).resolve().parent
|
|
95
|
+
if str(scripts_dir) not in sys.path:
|
|
96
|
+
sys.path.insert(0, str(scripts_dir))
|
|
97
|
+
try:
|
|
98
|
+
import discovery
|
|
99
|
+
models = discovery.discover_codex_models(use_cache=False)
|
|
100
|
+
model_ids = [m["id"] for m in models]
|
|
101
|
+
print(f"Codex discovery verified: {len(models)} model(s) discovered ({', '.join(model_ids[:3])}).")
|
|
102
|
+
except Exception as exc:
|
|
103
|
+
print(f"Notice: Codex discovery check: {exc}")
|
|
104
|
+
print("Remediation: Run `codex login` to verify credentials before running Gantry tasks.")
|
|
105
|
+
|
|
32
106
|
print("Proposed .gantry/config.json:")
|
|
33
107
|
print(json.dumps(config, indent=2))
|
|
34
108
|
|
|
35
|
-
repo_root = Path.cwd()
|
|
36
109
|
gantry_dir = repo_root / ".gantry"
|
|
37
110
|
config_path = gantry_dir / "config.json"
|
|
38
111
|
|
|
@@ -119,7 +192,7 @@ def main() -> None:
|
|
|
119
192
|
if ag_hook_frag_path.exists():
|
|
120
193
|
ag_hook_frag = json.loads(ag_hook_frag_path.read_text(encoding="utf-8"))
|
|
121
194
|
else:
|
|
122
|
-
guard_cmd = 'python3 "
|
|
195
|
+
guard_cmd = 'python3 "skills/gantry/scripts/guard.py" PreToolUse --json'
|
|
123
196
|
ag_hook_frag = {
|
|
124
197
|
"hooks": {
|
|
125
198
|
"PreToolUse": [
|
|
@@ -150,18 +223,38 @@ def main() -> None:
|
|
|
150
223
|
else:
|
|
151
224
|
hooks_json_path.write_text(json.dumps(ag_hook_frag, indent=2) + "\n", encoding="utf-8")
|
|
152
225
|
|
|
226
|
+
is_codex_final = (
|
|
227
|
+
final_config.get("execution", {}).get("hostHarness") == "codex"
|
|
228
|
+
or args.harness == "codex"
|
|
229
|
+
)
|
|
230
|
+
|
|
153
231
|
agents_path = repo_root / "AGENTS.md"
|
|
154
232
|
content = agents_path.read_text(encoding="utf-8") if agents_path.exists() else ""
|
|
155
233
|
begin_marker = "<!-- gantry:begin -->"
|
|
156
234
|
end_marker = "<!-- gantry:end -->"
|
|
157
235
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
236
|
+
if is_codex_final:
|
|
237
|
+
new_section = (
|
|
238
|
+
f"{begin_marker}\n"
|
|
239
|
+
"## Gantry Repository Policy\n\n"
|
|
240
|
+
"This repository uses Gantry for its agentic SDLC.\n"
|
|
241
|
+
"Artifacts, templates, and checks are configured in `.gantry/config.json`.\n\n"
|
|
242
|
+
"### Codex Host Orchestration & Guardrails\n\n"
|
|
243
|
+
"When Codex operates as host harness or execution runner:\n"
|
|
244
|
+
"- Bounded Execution: Role agents (Implementer, Reviewer, Critic) are executed via bounded sub-processes (`codex exec` or cross-harness dispatch).\n"
|
|
245
|
+
"- Invariant Protection: Never hand-edit `ROADMAP.md` or issue checkboxes/status lines directly. Only `roadmap.py done` updates them after Critic acceptance.\n"
|
|
246
|
+
"- Branch Isolation: Direct commits to `main` are forbidden; all work proceeds on dedicated issue branches.\n"
|
|
247
|
+
"- Defense in Depth: Because Codex lacks native tool hooks, git hooks and adversarial Critic verification enforce delivery integrity and contract validation (`result.py`).\n"
|
|
248
|
+
f"{end_marker}\n"
|
|
249
|
+
)
|
|
250
|
+
else:
|
|
251
|
+
new_section = (
|
|
252
|
+
f"{begin_marker}\n"
|
|
253
|
+
"## Gantry Repository Policy\n\n"
|
|
254
|
+
"This repository uses Gantry for its agentic SDLC.\n"
|
|
255
|
+
"Artifacts, templates, and checks are configured in `.gantry/config.json`.\n"
|
|
256
|
+
f"{end_marker}\n"
|
|
257
|
+
)
|
|
165
258
|
|
|
166
259
|
if begin_marker in content and end_marker in content:
|
|
167
260
|
start = content.find(begin_marker)
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Wait for operator approval after Critic verification before starting Integrate."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import datetime
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from runlog import ISSUE_RE, RUN_ID_RE, UNIT_ID_RE, append_event, read_marker, resolve_hook_run, run_log_path, state_root as default_state_root, unit_id as derive_unit_id
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WaitGateError(ValueError):
|
|
18
|
+
"""The gate check cannot execute safely with the provided arguments."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def approval_marker_path(root: Path, unit: str, issue: str) -> Path:
|
|
22
|
+
return root / unit / "approvals" / f"{issue}.json"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def read_approval_marker(root: Path, unit: str, issue: str) -> dict | None:
|
|
26
|
+
path = approval_marker_path(root, unit, issue)
|
|
27
|
+
if not path.exists():
|
|
28
|
+
return None
|
|
29
|
+
try:
|
|
30
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
31
|
+
if isinstance(data, dict) and data.get("issue") == issue:
|
|
32
|
+
return data
|
|
33
|
+
except (OSError, json.JSONDecodeError):
|
|
34
|
+
pass
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def write_approval_marker(root: Path, unit: str, run: str, issue: str, source: str = "terminal") -> Path:
|
|
39
|
+
path = approval_marker_path(root, unit, issue)
|
|
40
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
payload = {
|
|
42
|
+
"unit": unit,
|
|
43
|
+
"run": run,
|
|
44
|
+
"issue": issue,
|
|
45
|
+
"approvedAt": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
46
|
+
"source": source,
|
|
47
|
+
}
|
|
48
|
+
path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
|
49
|
+
|
|
50
|
+
# Record operator.approved in Run log if run log exists
|
|
51
|
+
log_path = run_log_path(root, unit, run)
|
|
52
|
+
if log_path.exists():
|
|
53
|
+
try:
|
|
54
|
+
append_event(
|
|
55
|
+
log_path,
|
|
56
|
+
{
|
|
57
|
+
"ts": payload["approvedAt"],
|
|
58
|
+
"run": run,
|
|
59
|
+
"event": "operator.approved",
|
|
60
|
+
"issue": issue,
|
|
61
|
+
"data": {"approvedAt": payload["approvedAt"], "source": source},
|
|
62
|
+
},
|
|
63
|
+
)
|
|
64
|
+
except Exception:
|
|
65
|
+
pass
|
|
66
|
+
return path
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def wait_for_gate(
|
|
70
|
+
root: Path,
|
|
71
|
+
unit: str,
|
|
72
|
+
run: str,
|
|
73
|
+
issue: str,
|
|
74
|
+
timeout: float | None = None,
|
|
75
|
+
poll_interval: float = 0.2,
|
|
76
|
+
prompt: bool = True,
|
|
77
|
+
) -> int:
|
|
78
|
+
start_time = time.time()
|
|
79
|
+
marker = read_approval_marker(root, unit, issue)
|
|
80
|
+
if marker:
|
|
81
|
+
print(json.dumps({"approved": True, "issue": issue, "source": marker.get("source", "marker"), "approvedAt": marker.get("approvedAt")}))
|
|
82
|
+
return 0
|
|
83
|
+
|
|
84
|
+
user_input: list[str] = []
|
|
85
|
+
if prompt and sys.stdin.isatty():
|
|
86
|
+
sys.stderr.write(f"\n[GANTRY GATE] Issue {issue} passed Critic verification.\n")
|
|
87
|
+
sys.stderr.write("Press [Enter] or type 'y' to approve and proceed to Integrate (or approve in Dashboard UI): ")
|
|
88
|
+
sys.stderr.flush()
|
|
89
|
+
|
|
90
|
+
def _reader() -> None:
|
|
91
|
+
try:
|
|
92
|
+
line = sys.stdin.readline()
|
|
93
|
+
user_input.append(line.strip().lower())
|
|
94
|
+
except Exception:
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
t = threading.Thread(target=_reader, daemon=True)
|
|
98
|
+
t.start()
|
|
99
|
+
|
|
100
|
+
while True:
|
|
101
|
+
# 1. Check approval marker
|
|
102
|
+
marker = read_approval_marker(root, unit, issue)
|
|
103
|
+
if marker:
|
|
104
|
+
print(json.dumps({"approved": True, "issue": issue, "source": marker.get("source", "marker"), "approvedAt": marker.get("approvedAt")}))
|
|
105
|
+
return 0
|
|
106
|
+
|
|
107
|
+
# 2. Check terminal stdin if available
|
|
108
|
+
if user_input:
|
|
109
|
+
line = user_input[0]
|
|
110
|
+
if line in ("", "y", "yes", "approve", "ok", "1"):
|
|
111
|
+
write_approval_marker(root, unit, run, issue, source="terminal")
|
|
112
|
+
print(json.dumps({"approved": True, "issue": issue, "source": "terminal"}))
|
|
113
|
+
return 0
|
|
114
|
+
|
|
115
|
+
if timeout is not None and timeout > 0:
|
|
116
|
+
if (time.time() - start_time) >= timeout:
|
|
117
|
+
sys.stderr.write(f"\n[GANTRY GATE] Timeout waiting for operator approval for {issue}.\n")
|
|
118
|
+
return 1
|
|
119
|
+
|
|
120
|
+
time.sleep(poll_interval)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def main(argv: list[str] | None = None) -> int:
|
|
124
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
125
|
+
parser.add_argument("issue", nargs="?", help="Issue reference e.g. sample#01")
|
|
126
|
+
parser.add_argument("--issue", dest="opt_issue", help="Issue reference")
|
|
127
|
+
parser.add_argument("--unit", help="Execution unit ID")
|
|
128
|
+
parser.add_argument("--run", help="Run ID")
|
|
129
|
+
parser.add_argument("--state-root", help="State root directory")
|
|
130
|
+
parser.add_argument("--cwd", default=".", help="Current working directory")
|
|
131
|
+
parser.add_argument("--poll-interval", type=float, default=0.2, help="Poll interval in seconds")
|
|
132
|
+
parser.add_argument("--timeout", type=float, default=None, help="Timeout in seconds")
|
|
133
|
+
parser.add_argument("--check", action="store_true", help="Check approval status non-blocking and exit")
|
|
134
|
+
parser.add_argument("--approve", action="store_true", help="Approve gate immediately and exit")
|
|
135
|
+
args = parser.parse_args(argv)
|
|
136
|
+
|
|
137
|
+
issue = args.issue or args.opt_issue
|
|
138
|
+
if not issue or not ISSUE_RE.fullmatch(issue):
|
|
139
|
+
print(f"wait-gate error: valid issue reference required (e.g. sample#01), got {issue!r}", file=sys.stderr)
|
|
140
|
+
return 2
|
|
141
|
+
|
|
142
|
+
cwd = Path(args.cwd).resolve()
|
|
143
|
+
root = Path(args.state_root).expanduser().resolve() if args.state_root else default_state_root(None)
|
|
144
|
+
|
|
145
|
+
unit = args.unit
|
|
146
|
+
if not unit:
|
|
147
|
+
try:
|
|
148
|
+
unit = derive_unit_id(cwd)
|
|
149
|
+
except Exception:
|
|
150
|
+
unit = "000000000000"
|
|
151
|
+
if not UNIT_ID_RE.fullmatch(unit):
|
|
152
|
+
print(f"wait-gate error: invalid unit id {unit!r}", file=sys.stderr)
|
|
153
|
+
return 2
|
|
154
|
+
|
|
155
|
+
run = args.run
|
|
156
|
+
if not run:
|
|
157
|
+
env_run, _ = resolve_hook_run(cwd)
|
|
158
|
+
run = env_run or "run-default"
|
|
159
|
+
if not RUN_ID_RE.fullmatch(run):
|
|
160
|
+
print(f"wait-gate error: invalid run id {run!r}", file=sys.stderr)
|
|
161
|
+
return 2
|
|
162
|
+
|
|
163
|
+
if args.approve:
|
|
164
|
+
write_approval_marker(root, unit, run, issue, source="terminal")
|
|
165
|
+
print(json.dumps({"approved": True, "issue": issue, "source": "terminal"}))
|
|
166
|
+
return 0
|
|
167
|
+
|
|
168
|
+
if args.check:
|
|
169
|
+
marker = read_approval_marker(root, unit, issue)
|
|
170
|
+
if marker:
|
|
171
|
+
print(json.dumps({"approved": True, "issue": issue, "source": marker.get("source", "marker"), "approvedAt": marker.get("approvedAt")}))
|
|
172
|
+
return 0
|
|
173
|
+
return 1
|
|
174
|
+
|
|
175
|
+
return wait_for_gate(root, unit, run, issue, timeout=args.timeout, poll_interval=args.poll_interval)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
if __name__ == "__main__":
|
|
179
|
+
sys.exit(main())
|
|
@@ -12,10 +12,16 @@ changed from the dashboard.
|
|
|
12
12
|
|
|
13
13
|
## Open the dashboard
|
|
14
14
|
|
|
15
|
+
### Foreground server
|
|
15
16
|
```
|
|
16
17
|
python3 <skillDir>/../gantry/scripts/dashboard.py serve
|
|
17
18
|
```
|
|
18
19
|
|
|
20
|
+
### Background daemon
|
|
21
|
+
```
|
|
22
|
+
python3 <skillDir>/../gantry/scripts/dashboard.py start --daemon
|
|
23
|
+
```
|
|
24
|
+
|
|
19
25
|
- Binds `127.0.0.1` only; the script refuses any other `--host` value and exits non-zero
|
|
20
26
|
before opening a socket. There is no way to expose the dashboard beyond the local machine.
|
|
21
27
|
- Defaults to port `4600`; pass `--port 0` for an OS-assigned ephemeral port (the script
|
|
@@ -25,6 +31,17 @@ python3 <skillDir>/../gantry/scripts/dashboard.py serve
|
|
|
25
31
|
example when inspecting a fixture.
|
|
26
32
|
- Open `http://127.0.0.1:<port>/` in a browser once the server is listening.
|
|
27
33
|
|
|
34
|
+
## Daemon subcommands and status
|
|
35
|
+
|
|
36
|
+
- Query status: `python3 <skillDir>/../gantry/scripts/dashboard.py status [--json]`
|
|
37
|
+
- Terminate daemon: `python3 <skillDir>/../gantry/scripts/dashboard.py stop`
|
|
38
|
+
|
|
39
|
+
## Round lifecycle hooks
|
|
40
|
+
|
|
41
|
+
Gantry's round workflow automates dashboard lifecycle checks:
|
|
42
|
+
- **Pre-implementation hook**: Prior to starting the `Implement` phase, the workflow checks `dashboard.py status --json`. If inactive, it asks the operator whether to launch the dashboard with `dashboard.py start --daemon`. If approved, it launches the daemon and displays the URL. If the operator declines, it proceeds without prompting again. If already active, it logs the active URL without prompting.
|
|
43
|
+
- **Post-integration hook**: After completing `Integrate`, if `dashboard.py status --json` reports active, it asks the operator whether to terminate the dashboard with `dashboard.py stop`. If approved, it cleanly stops the daemon.
|
|
44
|
+
|
|
28
45
|
## What you see
|
|
29
46
|
|
|
30
47
|
- One swimlane per Run (`<repositoryRoot> — <run> [tier: …]`), across every execution unit
|
|
@@ -51,5 +68,7 @@ tests.
|
|
|
51
68
|
|
|
52
69
|
## Stopping the server
|
|
53
70
|
|
|
54
|
-
`Ctrl-C` in the terminal running `dashboard.py serve
|
|
55
|
-
|
|
71
|
+
- For the foreground server: `Ctrl-C` in the terminal running `dashboard.py serve`.
|
|
72
|
+
- For the daemon: `python3 <skillDir>/../gantry/scripts/dashboard.py stop`.
|
|
73
|
+
- From the browser: click the **SHUTDOWN** button in the dashboard rig header and confirm in the dialog.
|
|
74
|
+
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gantry-plan
|
|
3
|
+
description: Socratic Gate planning and tracer-bullet vertical slicing. Guides interactive goal discovery, challenges architectural assumptions, decomposes specs into thin vertical slices, audits token budgets, and validates through Plan Critic.
|
|
4
|
+
argument-hint: <spec-slug | spec-path | "free-text goal">
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Gantry Plan
|
|
8
|
+
|
|
9
|
+
A harness-neutral planning skill that unifies Socratic interviewing and tracer-bullet
|
|
10
|
+
vertical slicing (`to-issues` pattern). It turns unrefined goals or raw specs into
|
|
11
|
+
verifiable, end-to-end demonstrable Issues scheduled into execution waves.
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
Goal or Spec → [Socratic Gate / Spec Validation] → Tracer-Bullet Slicing
|
|
15
|
+
→ Budget Audit → Plan Critic → Operator Quiz → Approval Transition
|
|
16
|
+
→ Execution Handoff
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Dual Entry Modes
|
|
20
|
+
|
|
21
|
+
### 1. Free-Text Goal
|
|
22
|
+
|
|
23
|
+
When invoked with a free-text prompt or goal (e.g. `/gantry-plan "add webhook support"`):
|
|
24
|
+
1. **Context Exploration**: Explore repository context in the background:
|
|
25
|
+
- Glossary: `CONTEXT.md`
|
|
26
|
+
- Requirements: `PRD.md`
|
|
27
|
+
- Standing decisions: `docs/adr/`
|
|
28
|
+
- Existing modules under `scripts/` and `.agents/skills/`.
|
|
29
|
+
2. **Telemetry Initiation**: Emit a `phase.started` event in the Run log for milestone
|
|
30
|
+
`<slug>#00` with `phase: "Plan"` and `data.operatorWaiting: true`. The Gantry Kanban
|
|
31
|
+
dashboard displays an *"Awaiting Operator"* badge in the `Plan` column.
|
|
32
|
+
3. **Socratic Gate Interview**: Guide the operator across 4 structured phases:
|
|
33
|
+
- **Problem Statement & Context**: Pain point, concrete use case, user personas.
|
|
34
|
+
- **Architectural Boundaries & Seams**: Seam locations, CLI/API contracts, dependencies.
|
|
35
|
+
- **Scope & Non-Goals**: Strictly in-scope capabilities vs explicit out-of-scope boundaries.
|
|
36
|
+
- **Verifiable Criteria & Scenarios**: Acceptance criteria and primary Gherkin scenarios.
|
|
37
|
+
Each question presents a concise recommended answer based on repository conventions.
|
|
38
|
+
4. **Operator Response Logging**: When the operator answers, record `operator.approved`
|
|
39
|
+
in the Run log for `<slug>#00`.
|
|
40
|
+
5. **Spec Synthesis**: Generate `.scratch/<slug>/spec.md` conforming to the Gantry spec template
|
|
41
|
+
(`Blueprint`, `Contract`, `Definition of Done`, `Regression Guardrails`, `Scenarios`,
|
|
42
|
+
`Out of Scope`, `Changelog`).
|
|
43
|
+
6. **Spec Verification**: Verify the synthesized spec passes `python3 <skillDir>/../gantry/scripts/spec.py --check <spec-path>`.
|
|
44
|
+
|
|
45
|
+
### 2. Spec-Provided
|
|
46
|
+
|
|
47
|
+
When invoked with an existing spec path or slug (e.g. `/gantry-plan .scratch/auth/spec.md` or `/gantry-plan auth`):
|
|
48
|
+
1. Validate the spec structurally via `python3 <skillDir>/../gantry/scripts/spec.py --check <spec-path>`.
|
|
49
|
+
2. Run read-only Requirement Critic evaluation (checking for ambiguity, coherence, verifiability, and non-goals).
|
|
50
|
+
|
|
51
|
+
## Tracer-Bullet Vertical Slicing
|
|
52
|
+
|
|
53
|
+
Decompose the approved spec into end-to-end demonstrable vertical slices:
|
|
54
|
+
1. **Prefactoring First**: Isolate prefactoring into `<slug>#01` ("Make the change easy, then make the easy change").
|
|
55
|
+
Slice 01 has no implementation blockers. Subsequent slices depend on Slice 01 (`Blocked by: <slug>#01`).
|
|
56
|
+
2. **Vertical Slices**: Every slice cuts across all necessary layers (CLI/API, domain logic, tests) rather than
|
|
57
|
+
horizontal layers (no "database-only" or "frontend-only" issues).
|
|
58
|
+
3. **Issue Metadata**: Maintain standard Gantry issue headers:
|
|
59
|
+
- `Type: issue`
|
|
60
|
+
- `Status: draft`
|
|
61
|
+
- `Slice: <slug>#NN`
|
|
62
|
+
- `Spec: .scratch/<slug>/spec.md`
|
|
63
|
+
- `### Files to read`
|
|
64
|
+
- `## Acceptance criteria` (with `- [ ]` checkboxes)
|
|
65
|
+
- `## Blocked by`
|
|
66
|
+
4. **Context Budget Audit**: Verify each issue fits within model token windows via `budget.py`.
|
|
67
|
+
5. **Plan Critic**: Verify that:
|
|
68
|
+
- Blocker graph forms a valid directed acyclic graph (DAG) without cycles.
|
|
69
|
+
- Every issue has observable acceptance criteria.
|
|
70
|
+
- No horizontal slicing smells are present.
|
|
71
|
+
6. **Operator Quiz**: Quiz the operator on:
|
|
72
|
+
- Granularity (are slices sized for single-round TDD deliveries?).
|
|
73
|
+
- Dependency sequence (is prefactoring prioritized?).
|
|
74
|
+
- Split / merge preferences.
|
|
75
|
+
7. **Persist Issues**: Write issues to `.scratch/<slug>/issues/NN-<slug>.md`.
|
|
76
|
+
|
|
77
|
+
## Planning Approval Transition
|
|
78
|
+
|
|
79
|
+
Upon operator approval of the issue breakdown:
|
|
80
|
+
1. **Update Issue Status**: Set status of generated issues to `ready-for-agent`:
|
|
81
|
+
```bash
|
|
82
|
+
python3 <skillDir>/../gantry/scripts/roadmap.py status <ref> ready-for-agent
|
|
83
|
+
```
|
|
84
|
+
2. **Recompute Waves**: Update `ROADMAP.md` wave layout:
|
|
85
|
+
```bash
|
|
86
|
+
python3 <skillDir>/../gantry/scripts/roadmap.py waves
|
|
87
|
+
```
|
|
88
|
+
3. **Verify Integrity**: Ensure zero drift:
|
|
89
|
+
```bash
|
|
90
|
+
python3 <skillDir>/../gantry/scripts/roadmap.py check
|
|
91
|
+
```
|
|
92
|
+
4. **Complete Planning Milestone**: Record milestone completion in the Run log:
|
|
93
|
+
- Append `issue.done` for milestone `<slug>#00` with `phase: "Plan"`.
|
|
94
|
+
(Alternatively, use `python3 <skillDir>/../gantry/scripts/plan.py --approve <slug>`).
|
|
95
|
+
|
|
96
|
+
## Execution Handoff
|
|
97
|
+
|
|
98
|
+
- **Standalone Invocation (`/gantry-plan`)**:
|
|
99
|
+
Present the computed execution waves and prompt the operator:
|
|
100
|
+
> Planning approved and scheduled into Wave N. Would you like to launch execution now with `/gantry`?
|
|
101
|
+
Wait for explicit confirmation before launching `gantry`.
|
|
102
|
+
- **Delegated Invocation (from `/gantry`)**:
|
|
103
|
+
When delegated from `/gantry`, automatically proceed directly into Gantry's TDD
|
|
104
|
+
implementation round loop without additional prompting.
|
|
@@ -29,15 +29,21 @@ You are the sole conversational writer of repository policy.
|
|
|
29
29
|
- When Antigravity is detected (`.agents/` directory or `agy` CLI on PATH), `setup.py` generates or merges `.agents/hooks.json` to wire `PreToolUse` to `guard.py PreToolUse --json`.
|
|
30
30
|
- Unrelated repository policy, hook settings and Caveman opt-in are preserved during merges.
|
|
31
31
|
|
|
32
|
-
5. **
|
|
32
|
+
5. **Codex Host Harness & Defense in Depth**:
|
|
33
|
+
- Detect Codex in environment via `codex` CLI on PATH, `.codex` configuration, or explicit `--harness codex`.
|
|
34
|
+
- Guide operator to verify authentication (`codex login`) and test model discovery (`discovery.py --harness codex`) before finalizing configuration.
|
|
35
|
+
- Configure `execution.hostHarness: "codex"` and role model assignments in `.gantry/config.json`.
|
|
36
|
+
- Inject Codex-specific orchestration rules (bounded subprocess dispatch via `codex exec`) and defense-in-depth guardrail directives into the marked Gantry policy block in `AGENTS.md`.
|
|
37
|
+
|
|
38
|
+
6. **Constraints**:
|
|
33
39
|
- Creates no engine, database, MCP service, or automatic cleanup.
|
|
34
40
|
- All setup-generated policy, prompts, and marked content must be English.
|
|
35
41
|
|
|
36
|
-
|
|
42
|
+
7. **Applying the Policy**:
|
|
37
43
|
Once the operator confirms the settings, construct the JSON configuration and pipe it to `setup.py`:
|
|
38
44
|
|
|
39
45
|
```bash
|
|
40
46
|
python3 .agents/skills/gantry/scripts/setup.py --config '{...}'
|
|
41
47
|
```
|
|
42
48
|
|
|
43
|
-
The `setup.py` script renders the full proposed `.gantry/config.json` before writing, supports merge, overwrite, and abort for an existing policy, handles idempotent merging of the Claude Code hook fragment into `.claude/settings.json`, configures `.agents/hooks.json` when Antigravity is detected, and adds or replaces only the marked Gantry section in `AGENTS.md`. Do not modify these files directly.
|
|
49
|
+
The `setup.py` script renders the full proposed `.gantry/config.json` before writing, supports merge, overwrite, and abort for an existing policy, handles idempotent merging of the Claude Code hook fragment into `.claude/settings.json`, configures `.agents/hooks.json` when Antigravity is detected, supports `--harness codex` with discovery verification, and adds or replaces only the marked Gantry section in `AGENTS.md`. Do not modify these files directly.
|