@julioborges/gantry 1.0.6 → 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.
@@ -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
- result = json.load(sys.stdin)
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
 
@@ -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
- new_section = (
159
- f"{begin_marker}\n"
160
- "## Gantry Repository Policy\n\n"
161
- "This repository uses Gantry for its agentic SDLC.\n"
162
- "Artifacts, templates, and checks are configured in `.gantry/config.json`.\n"
163
- f"{end_marker}\n"
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` stops it; nothing it does needs
55
- cleanup, because it never wrote anything.
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. **Constraints**:
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
- 6. **Applying the Policy**:
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.