its-magic 0.1.2-9 → 0.1.2

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.
Files changed (232) hide show
  1. package/README.md +1602 -755
  2. package/bin/its-magic.js +121 -11
  3. package/bin/postinstall.js +21 -0
  4. package/installer.ps1 +759 -0
  5. package/installer.py +981 -0
  6. package/installer.sh +649 -0
  7. package/package.json +18 -14
  8. package/scripts/check_intake_template_parity.py +284 -0
  9. package/scripts/doc_profile_lib.py +415 -0
  10. package/scripts/guard_installer_publish.py +88 -0
  11. package/scripts/intake_bug_routing_guard.py +67 -0
  12. package/scripts/intake_evidence_lib.py +802 -0
  13. package/scripts/intake_evidence_validate.py +73 -0
  14. package/scripts/materialize_codebase_map.py +184 -0
  15. package/scripts/remote_config_summary.py +243 -0
  16. package/template/.cursor/agents/curator.mdc +35 -0
  17. package/template/.cursor/agents/dev.mdc +29 -0
  18. package/template/.cursor/agents/po.mdc +141 -0
  19. package/template/.cursor/agents/qa.mdc +28 -0
  20. package/template/.cursor/agents/release.mdc +28 -0
  21. package/template/.cursor/agents/security.mdc +98 -0
  22. package/template/.cursor/agents/tech-lead.mdc +57 -0
  23. package/template/.cursor/commands/architecture.md +127 -0
  24. package/template/.cursor/commands/ask.md +55 -0
  25. package/template/.cursor/commands/auto.md +533 -0
  26. package/template/.cursor/commands/discovery.md +47 -0
  27. package/template/.cursor/commands/execute.md +343 -0
  28. package/template/.cursor/commands/intake.md +323 -0
  29. package/template/.cursor/commands/map-codebase.md +46 -0
  30. package/template/.cursor/commands/memory-audit.md +175 -0
  31. package/template/.cursor/commands/milestone-complete.md +51 -0
  32. package/template/.cursor/commands/milestone-start.md +59 -0
  33. package/template/.cursor/commands/pause.md +64 -0
  34. package/{.cursor/commands/gsd-phase-context.md → template/.cursor/commands/phase-context.md} +6 -2
  35. package/template/.cursor/commands/plan-verify.md +34 -0
  36. package/template/.cursor/commands/qa.md +226 -0
  37. package/template/.cursor/commands/quick.md +41 -0
  38. package/template/.cursor/commands/refresh-context.md +82 -0
  39. package/template/.cursor/commands/release.md +505 -0
  40. package/template/.cursor/commands/research.md +49 -0
  41. package/template/.cursor/commands/resume.md +67 -0
  42. package/template/.cursor/commands/security-review.md +81 -0
  43. package/template/.cursor/commands/sprint-plan.md +103 -0
  44. package/template/.cursor/commands/status-reconcile.md +95 -0
  45. package/template/.cursor/commands/verify-work.md +152 -0
  46. package/template/.cursor/dev-environment.json.example +22 -0
  47. package/{.cursor → template/.cursor}/hooks/README.md +3 -2
  48. package/{.cursor/hooks/gsd-hook.py → template/.cursor/hooks/hook.py} +15 -6
  49. package/template/.cursor/hooks.json +26 -0
  50. package/template/.cursor/remote.json +31 -0
  51. package/template/.cursor/rules/caveman.mdc +184 -0
  52. package/template/.cursor/rules/coding-standards.mdc +39 -0
  53. package/template/.cursor/rules/core.mdc +111 -0
  54. package/template/.cursor/rules/handoffs.mdc +27 -0
  55. package/template/.cursor/rules/quality.mdc +49 -0
  56. package/template/.cursor/scratchpad.local.example.md +323 -0
  57. package/template/.cursor/scratchpad.md +324 -0
  58. package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/SKILL.md +2 -2
  59. package/template/.cursorignore +6 -0
  60. package/template/.env.example +28 -0
  61. package/template/.github/workflows/ci.yml +194 -0
  62. package/{.github → template/.github}/workflows/deploy.yml +5 -2
  63. package/template/.its-magic-version +1 -0
  64. package/template/README.md +1602 -0
  65. package/template/decisions/DEC-0001.md +11 -0
  66. package/template/decisions/DEC-0002.md +11 -0
  67. package/template/docs/developer/README.md +44 -0
  68. package/template/docs/engineering/architecture.md +9 -0
  69. package/template/docs/engineering/artifact-ordering-policy.md +48 -0
  70. package/template/docs/engineering/artifact-ownership-policy.md +57 -0
  71. package/template/docs/engineering/auto-orchestration-reference.md +1211 -0
  72. package/{docs → template/docs}/engineering/codebase-map.md +0 -5
  73. package/template/docs/engineering/compatibility-report.md +20 -0
  74. package/template/docs/engineering/compatibility-signals.md +7 -0
  75. package/template/docs/engineering/component-scope-report.md +16 -0
  76. package/template/docs/engineering/component-scope.md +19 -0
  77. package/template/docs/engineering/context/installer-owned-paths.manifest +96 -0
  78. package/template/docs/engineering/context/readme-section-affinity.json +30 -0
  79. package/template/docs/engineering/decisions.md +16 -0
  80. package/template/docs/engineering/legacy-drift-audit.md +21 -0
  81. package/template/docs/engineering/manifests/components/api-gateway.manifest.yaml +12 -0
  82. package/template/docs/engineering/manifests/registry.manifest.yaml +20 -0
  83. package/template/docs/engineering/manifests/repo.manifest.yaml +11 -0
  84. package/template/docs/engineering/phase-context.md +16 -0
  85. package/template/docs/engineering/release-targets.json +123 -0
  86. package/template/docs/engineering/research.md +29 -0
  87. package/template/docs/engineering/runbook.md +2320 -0
  88. package/template/docs/engineering/runtime-connectivity.md +81 -0
  89. package/template/docs/engineering/security-review.md +33 -0
  90. package/template/docs/engineering/spec-pack/README.md +20 -0
  91. package/template/docs/engineering/state-archive/README.md +15 -0
  92. package/template/docs/engineering/state.md +20 -0
  93. package/template/docs/engineering/status-normalization-report.md +19 -0
  94. package/template/docs/engineering/token-cost-parity-manifest.md +16 -0
  95. package/template/docs/engineering/us-0084-remote-e2e.md +44 -0
  96. package/template/docs/product/acceptance.md +1 -0
  97. package/template/docs/product/backlog.md +5 -0
  98. package/template/docs/product/vision.md +11 -0
  99. package/template/docs/user-guides/README.md +40 -0
  100. package/{handoffs → template/handoffs}/dev_to_qa.md +0 -3
  101. package/{handoffs → template/handoffs}/po_to_tl.md +0 -3
  102. package/{handoffs → template/handoffs}/qa_to_dev.md +1 -2
  103. package/template/handoffs/release_notes.md +51 -0
  104. package/template/handoffs/release_queue.md +47 -0
  105. package/template/handoffs/release_to_dev.md +31 -0
  106. package/template/handoffs/releases/Sxxxx-release-notes.md +62 -0
  107. package/template/handoffs/resume_brief.md +14 -0
  108. package/{handoffs → template/handoffs}/tl_to_dev.md +0 -2
  109. package/template/handoffs/token_cost_runs/README.md +26 -0
  110. package/template/its_magic/.its-magic-version +1 -0
  111. package/template/its_magic/README.md +9 -0
  112. package/template/scripts/auto_outer_driver.py +521 -0
  113. package/template/scripts/caveman_compress_input.py +903 -0
  114. package/template/scripts/check_downstream_ci_guard.py +67 -0
  115. package/template/scripts/check_intake_template_parity.py +284 -0
  116. package/template/scripts/check_token_cost_parity.py +69 -0
  117. package/template/scripts/dev_environment_lib.py +463 -0
  118. package/template/scripts/doc_profile_lib.py +415 -0
  119. package/template/scripts/downstream_ci_guard_lib.py +222 -0
  120. package/template/scripts/enforce-triad-hot-surface.py +753 -0
  121. package/template/scripts/guard_installer_publish.py +88 -0
  122. package/template/scripts/intake_bug_resume_brief_refresh.py +303 -0
  123. package/template/scripts/intake_bug_routing_guard.py +67 -0
  124. package/template/scripts/intake_evidence_lib.py +802 -0
  125. package/template/scripts/intake_evidence_validate.py +73 -0
  126. package/template/scripts/materialize_codebase_map.py +184 -0
  127. package/template/scripts/pack_json_validate.py +130 -0
  128. package/template/scripts/project_readme_coverage_lib.py +417 -0
  129. package/template/scripts/readme_feature_coverage_lib.py +608 -0
  130. package/template/scripts/remote_config_summary.py +243 -0
  131. package/template/scripts/sync_push_gates.py +198 -0
  132. package/template/scripts/token_cost_compare.py +40 -0
  133. package/template/scripts/token_cost_lib.py +108 -0
  134. package/template/scripts/uat_probe_lib.py +868 -0
  135. package/template/scripts/validate-and-push.ps1 +280 -0
  136. package/template/scripts/validate-and-push.sh +243 -0
  137. package/template/scripts/validate_doc_profile.py +103 -0
  138. package/template/scripts/validate_project_readme_coverage.py +151 -0
  139. package/template/scripts/validate_readme_feature_coverage.py +140 -0
  140. package/template/sprints/S0001/progress.md +1 -0
  141. package/template/sprints/S0001/qa-findings.md +9 -0
  142. package/template/sprints/S0001/release-findings.md +24 -0
  143. package/template/sprints/S0001/sprint.md +9 -0
  144. package/template/sprints/S0001/summary.md +7 -0
  145. package/template/sprints/S0001/tasks.md +1 -0
  146. package/template/sprints/S0001/uat.json +6 -0
  147. package/template/sprints/S0001/uat.md +5 -0
  148. package/template/sprints/quick/Q0001/summary.md +1 -0
  149. package/{sprints → template/sprints}/quick/Q0001/task.json +0 -1
  150. package/.cursor/agents/curator.mdc +0 -21
  151. package/.cursor/agents/dev.mdc +0 -20
  152. package/.cursor/agents/po.mdc +0 -19
  153. package/.cursor/agents/qa.mdc +0 -19
  154. package/.cursor/agents/release.mdc +0 -19
  155. package/.cursor/agents/tech-lead.mdc +0 -21
  156. package/.cursor/commands/gsd-architecture.md +0 -29
  157. package/.cursor/commands/gsd-auto.md +0 -27
  158. package/.cursor/commands/gsd-discovery.md +0 -27
  159. package/.cursor/commands/gsd-execute.md +0 -32
  160. package/.cursor/commands/gsd-intake.md +0 -28
  161. package/.cursor/commands/gsd-map-codebase.md +0 -25
  162. package/.cursor/commands/gsd-milestone-complete.md +0 -24
  163. package/.cursor/commands/gsd-milestone-start.md +0 -26
  164. package/.cursor/commands/gsd-pause.md +0 -25
  165. package/.cursor/commands/gsd-plan-verify.md +0 -26
  166. package/.cursor/commands/gsd-qa.md +0 -28
  167. package/.cursor/commands/gsd-quick.md +0 -24
  168. package/.cursor/commands/gsd-refresh-context.md +0 -26
  169. package/.cursor/commands/gsd-release.md +0 -29
  170. package/.cursor/commands/gsd-research.md +0 -28
  171. package/.cursor/commands/gsd-resume.md +0 -26
  172. package/.cursor/commands/gsd-sprint-plan.md +0 -30
  173. package/.cursor/commands/gsd-verify-work.md +0 -25
  174. package/.cursor/hooks.json +0 -26
  175. package/.cursor/plans/cursor-gsd-team-kit_8cfee9b8.plan.md +0 -57
  176. package/.cursor/remote.json +0 -18
  177. package/.cursor/rules/gsd-core.mdc +0 -18
  178. package/.cursor/rules/gsd-handoffs.mdc +0 -10
  179. package/.cursor/rules/gsd-quality.mdc +0 -15
  180. package/.cursor/scratchpad.md +0 -34
  181. package/.github/workflows/ci.yml +0 -47
  182. package/decisions/DEC-0001.md +0 -21
  183. package/decisions/DEC-0002.md +0 -21
  184. package/docs/engineering/architecture.md +0 -354
  185. package/docs/engineering/decisions.md +0 -6
  186. package/docs/engineering/research.md +0 -11
  187. package/docs/engineering/runbook.md +0 -32
  188. package/docs/engineering/state.md +0 -33
  189. package/docs/product/acceptance.md +0 -6
  190. package/docs/product/backlog.md +0 -7
  191. package/docs/product/vision.md +0 -46
  192. package/gsd-installer.ps1 +0 -189
  193. package/gsd-installer.py +0 -195
  194. package/gsd-installer.sh +0 -201
  195. package/handoffs/release_notes.md +0 -14
  196. package/handoffs/resume_brief.md +0 -8
  197. package/milestones/M0001/milestone.json +0 -7
  198. package/milestones/M0001/phases.json +0 -9
  199. package/milestones/M0001/progress.md +0 -3
  200. package/milestones/M0001/summary.md +0 -3
  201. package/scripts/generate-release-notes.ps1 +0 -74
  202. package/scripts/generate-release-notes.sh +0 -63
  203. package/scripts/release-all.ps1 +0 -423
  204. package/scripts/release-all.sh +0 -226
  205. package/sprints/S0001/progress.md +0 -4
  206. package/sprints/S0001/qa-findings.md +0 -113
  207. package/sprints/S0001/sprint.md +0 -70
  208. package/sprints/S0001/summary.md +0 -46
  209. package/sprints/S0001/tasks.md +0 -35
  210. package/sprints/S0001/uat.json +0 -8
  211. package/sprints/S0001/uat.md +0 -8
  212. package/sprints/quick/Q0001/summary.md +0 -3
  213. /package/{.cursor/rules/gsd-escalation.mdc → template/.cursor/rules/escalation.mdc} +0 -0
  214. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/acceptance.json +0 -0
  215. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/acceptance.md +0 -0
  216. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/architecture.json +0 -0
  217. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/architecture.md +0 -0
  218. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/decision.json +0 -0
  219. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/decision.md +0 -0
  220. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/handoff.json +0 -0
  221. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/handoff.md +0 -0
  222. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/milestone.json +0 -0
  223. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/phase-context.json +0 -0
  224. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/plan-verify.json +0 -0
  225. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/sprint.json +0 -0
  226. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/sprint.md +0 -0
  227. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/story.json +0 -0
  228. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/story.md +0 -0
  229. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/uat.json +0 -0
  230. /package/{docs → template/docs}/engineering/context/phase-template.json +0 -0
  231. /package/{docs → template/docs}/engineering/dependencies.json +0 -0
  232. /package/{sprints → template/sprints}/S0001/plan-verify.json +0 -0
@@ -0,0 +1,521 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Full-autonomy outer driver (US-0092 / DEC-0078).
4
+
5
+ Spawn-only: loops documented /auto hook invocations; never performs phase-role work.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import hashlib
12
+ import json
13
+ import re
14
+ import subprocess
15
+ import sys
16
+ from datetime import datetime, timezone
17
+ from pathlib import Path
18
+
19
+ EXIT_COMPLETED = 0
20
+ EXIT_HARD_STOP = 1
21
+ EXIT_CONFIG = 2
22
+ EXIT_LOOP_MAX = 3
23
+ EXIT_BACKLOG_MAX = 4
24
+ EXIT_PAUSE = 5
25
+ EXIT_BLOCK_RETRY_CAP = 6
26
+ EXIT_TIMEOUT = 124
27
+
28
+ HARD_STOP_REASONS = frozenset(
29
+ {
30
+ "decision_gate",
31
+ "error",
32
+ "pause_request",
33
+ "loop_max",
34
+ "gate_blocked",
35
+ "AUTO_SCHEDULER_CONFLICT",
36
+ }
37
+ )
38
+ HARD_BLOCKED_SUBCODES = frozenset(
39
+ {"isolation", "strict_proof", "ownership", "security_deny"}
40
+ )
41
+ RECOVERABLE_STOP_REASONS = frozenset(
42
+ {"blocked", "missing_input", "uat_fail", "qa_fail"}
43
+ )
44
+ DRAIN_ADVANCE_MARKERS = frozenset(
45
+ {"refresh-context", "discovery", "intake"}
46
+ )
47
+
48
+
49
+ def _utc_now_iso() -> str:
50
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
51
+
52
+
53
+ def _merge_scratchpad(repo: Path) -> dict[str, str]:
54
+ values: dict[str, str] = {}
55
+ for name in (".cursor/scratchpad.md", ".cursor/scratchpad.local.md"):
56
+ path = repo / name
57
+ if not path.is_file():
58
+ continue
59
+ for line in path.read_text(encoding="utf-8").splitlines():
60
+ stripped = line.strip()
61
+ if not stripped or stripped.startswith("#"):
62
+ continue
63
+ if "=" in stripped:
64
+ key, _, val = stripped.partition("=")
65
+ values[key.strip()] = val.strip()
66
+ return values
67
+
68
+
69
+ def _require_full_autonomy(repo: Path) -> tuple[dict[str, str], str | None]:
70
+ try:
71
+ merged = _merge_scratchpad(repo)
72
+ except OSError as exc:
73
+ return {}, f"scratchpad parse failure: {exc}"
74
+ mode = merged.get("AUTO_FLOW_MODE", "manual")
75
+ if mode != "full_autonomy":
76
+ return merged, "AUTO_FLOW_MODE_NOT_FULL_AUTONOMY"
77
+ return merged, None
78
+
79
+
80
+ def _parse_resume_brief(repo: Path) -> dict[str, str]:
81
+ path = repo / "handoffs" / "resume_brief.md"
82
+ if not path.is_file():
83
+ return {}
84
+ text = path.read_text(encoding="utf-8")
85
+ out: dict[str, str] = {}
86
+ for key in (
87
+ "intended_resume_phase",
88
+ "orchestrator_run_id",
89
+ "story_id",
90
+ "bug_id",
91
+ "sprint_id",
92
+ ):
93
+ m = re.search(rf"`{key}`\s*:\s*\*\*`([^`]+)`\*\*", text)
94
+ if m:
95
+ out[key] = m.group(1)
96
+ else:
97
+ m2 = re.search(rf"- `{key}`:\s*`([^`]+)`", text)
98
+ if m2:
99
+ out[key] = m2.group(1)
100
+ m = re.search(r"orchestrator_run_id=\*\*`([^`]+)`\*\*", text)
101
+ if m and "orchestrator_run_id" not in out:
102
+ out["orchestrator_run_id"] = m.group(1)
103
+ return out
104
+
105
+
106
+ def _parse_state_boundary(repo: Path) -> dict[str, str]:
107
+ path = repo / "docs" / "engineering" / "state.md"
108
+ if not path.is_file():
109
+ return {}
110
+ text = path.read_text(encoding="utf-8")
111
+ out: dict[str, str] = {}
112
+ patterns = {
113
+ "stop_reason": r"stop_reason=([^\s`;]+)",
114
+ "next_scheduled_phase": r"next_scheduled_phase=([^\s`;]+)",
115
+ "backlog_drain_segment_complete": r"backlog_drain_segment_complete=(\d+)",
116
+ "backlog_drain_stories_remaining_budget": (
117
+ r"backlog_drain_stories_remaining_budget=(\d+)"
118
+ ),
119
+ "story_id": r"story_id=([^\s`;]+)",
120
+ "bug_id": r"bug_id=([^\s`;]+)",
121
+ "reason_code": r"reason_code=([^\s`;]+)",
122
+ "blocked_subcode": r"blocked_subcode=([^\s`;]+)",
123
+ }
124
+ for key, pat in patterns.items():
125
+ matches = re.findall(pat, text)
126
+ if matches:
127
+ out[key] = matches[-1]
128
+ return out
129
+
130
+
131
+ def _ledger_path(repo: Path, orchestrator_run_id: str) -> Path:
132
+ return repo / "handoffs" / "auto_block_retry" / f"{orchestrator_run_id}.jsonl"
133
+
134
+
135
+ def _count_retries(ledger: Path, story_id: str, stop_reason: str) -> int:
136
+ if not ledger.is_file():
137
+ return 0
138
+ count = 0
139
+ for line in ledger.read_text(encoding="utf-8").splitlines():
140
+ if not line.strip():
141
+ continue
142
+ try:
143
+ rec = json.loads(line)
144
+ except json.JSONDecodeError:
145
+ continue
146
+ if rec.get("story_id") == story_id and rec.get("stop_reason") == stop_reason:
147
+ if rec.get("outcome") == "retry_scheduled":
148
+ count += 1
149
+ return count
150
+
151
+
152
+ def append_ledger_record(
153
+ repo: Path,
154
+ orchestrator_run_id: str,
155
+ *,
156
+ story_id: str,
157
+ stop_reason: str,
158
+ reason_code: str,
159
+ remediation_action: str,
160
+ outcome: str,
161
+ outer_cycle_index: int,
162
+ implementation_loop_index: int,
163
+ seq: int,
164
+ ) -> None:
165
+ ledger_dir = repo / "handoffs" / "auto_block_retry"
166
+ ledger_dir.mkdir(parents=True, exist_ok=True)
167
+ record = {
168
+ "attempt_id": f"br-{orchestrator_run_id}-{seq}",
169
+ "timestamp": _utc_now_iso(),
170
+ "orchestrator_run_id": orchestrator_run_id,
171
+ "story_id": story_id or "(none)",
172
+ "stop_reason": stop_reason,
173
+ "reason_code": reason_code or "",
174
+ "remediation_action": remediation_action,
175
+ "outcome": outcome,
176
+ "outer_cycle_index": outer_cycle_index,
177
+ "implementation_loop_index": implementation_loop_index,
178
+ }
179
+ ledger = _ledger_path(repo, orchestrator_run_id)
180
+ with ledger.open("a", encoding="utf-8") as fh:
181
+ fh.write(json.dumps(record, sort_keys=True, separators=(",", ":")) + "\n")
182
+
183
+
184
+ def _is_recoverable(
185
+ stop_reason: str,
186
+ reason_code: str,
187
+ blocked_subcode: str,
188
+ merged: dict[str, str],
189
+ ) -> bool:
190
+ if stop_reason in HARD_STOP_REASONS:
191
+ return False
192
+ if stop_reason == "blocked":
193
+ if blocked_subcode in HARD_BLOCKED_SUBCODES:
194
+ return False
195
+ if reason_code in HARD_BLOCKED_SUBCODES:
196
+ return False
197
+ return True
198
+ if stop_reason == "missing_input":
199
+ if reason_code in ("critical", "CRITICAL_MISSING_INPUT"):
200
+ return False
201
+ return True
202
+ if stop_reason in ("uat_fail", "qa_fail"):
203
+ return merged.get("AUTO_IMPLEMENTATION_LOOP", "0") == "1"
204
+ return stop_reason in RECOVERABLE_STOP_REASONS
205
+
206
+
207
+ def _remediation_for(stop_reason: str) -> str:
208
+ mapping = {
209
+ "blocked": "refresh-context",
210
+ "missing_input": "refresh-context",
211
+ "uat_fail": "verify-work",
212
+ "qa_fail": "qa",
213
+ }
214
+ return mapping.get(stop_reason, "outer_reinvoke")
215
+
216
+
217
+ def _should_drain_advance(
218
+ boundary: dict[str, str], merged: dict[str, str]
219
+ ) -> bool:
220
+ if merged.get("AUTO_BACKLOG_DRAIN", "0") != "1":
221
+ if merged.get("AUTO_BUG_QUEUE", "0") != "1":
222
+ return False
223
+ stop = boundary.get("stop_reason", "")
224
+ if stop not in ("completed", "(none)"):
225
+ return False
226
+ if boundary.get("backlog_drain_segment_complete") == "1":
227
+ return True
228
+ nxt = boundary.get("next_scheduled_phase", "")
229
+ if nxt in DRAIN_ADVANCE_MARKERS:
230
+ return True
231
+ if stop == "completed" and merged.get("AUTO_BACKLOG_DRAIN", "0") == "1":
232
+ budget = boundary.get("backlog_drain_stories_remaining_budget", "")
233
+ if budget and budget != "0":
234
+ return True
235
+ return False
236
+
237
+
238
+ def _map_exit(
239
+ stop_reason: str,
240
+ reason_code: str,
241
+ merged: dict[str, str],
242
+ stories_processed: int,
243
+ max_stories: int,
244
+ ) -> int:
245
+ if stop_reason in ("pause_request", "AUTO_PAUSE_REQUEST"):
246
+ return EXIT_PAUSE
247
+ if stop_reason in ("loop_max", "AUTO_LOOP_MAX_CYCLES"):
248
+ return EXIT_LOOP_MAX
249
+ if stop_reason in ("BACKLOG_MAX_STORIES_REACHED",) or (
250
+ max_stories > 0 and stories_processed >= max_stories
251
+ ):
252
+ return EXIT_BACKLOG_MAX
253
+ if stop_reason in HARD_STOP_REASONS or stop_reason in (
254
+ "decision_gate",
255
+ "error",
256
+ "blocked",
257
+ ):
258
+ if _is_recoverable(stop_reason, reason_code, "", merged):
259
+ return -1
260
+ return EXIT_HARD_STOP
261
+ if stop_reason == "completed":
262
+ return EXIT_COMPLETED
263
+ return EXIT_HARD_STOP
264
+
265
+
266
+ def run_driver(
267
+ repo: Path,
268
+ *,
269
+ max_cycles: int | None,
270
+ max_stories: int | None,
271
+ dry_run: bool,
272
+ invoke_cmd: str | None,
273
+ simulate: dict[str, str] | None = None,
274
+ ) -> int:
275
+ merged, cfg_err = _require_full_autonomy(repo)
276
+ if cfg_err:
277
+ print(cfg_err, file=sys.stderr)
278
+ return EXIT_CONFIG
279
+
280
+ cycle_cap = max_cycles
281
+ if cycle_cap is None:
282
+ try:
283
+ cycle_cap = int(merged.get("AUTO_LOOP_MAX_CYCLES", "5"))
284
+ except ValueError:
285
+ cycle_cap = 5
286
+
287
+ story_cap = max_stories
288
+ if story_cap is None:
289
+ try:
290
+ story_cap = int(merged.get("AUTO_BACKLOG_MAX_STORIES", "1"))
291
+ except ValueError:
292
+ story_cap = 1
293
+
294
+ retry_cap = 3
295
+ try:
296
+ retry_cap = int(merged.get("AUTO_BLOCK_RETRY_MAX", "3"))
297
+ except ValueError:
298
+ pass
299
+
300
+ timeout_sec: int | None = None
301
+ raw_timeout = merged.get("AUTO_OUTER_DRIVER_TIMEOUT_SECONDS", "")
302
+ if raw_timeout:
303
+ try:
304
+ timeout_sec = int(raw_timeout)
305
+ except ValueError:
306
+ pass
307
+
308
+ resume = _parse_resume_brief(repo)
309
+ orch_id = resume.get("orchestrator_run_id", "outer-driver")
310
+ ledger_seq = 0
311
+ outer_cycle = 0
312
+ stories_processed = 0
313
+ impl_loop = int(merged.get("AUTO_IMPLEMENTATION_LOOP", "0") == "1")
314
+
315
+ while outer_cycle < cycle_cap:
316
+ outer_cycle += 1
317
+ if simulate is not None:
318
+ boundary = dict(simulate)
319
+ else:
320
+ boundary = _parse_state_boundary(repo)
321
+ resume = _parse_resume_brief(repo)
322
+
323
+ phase = resume.get("intended_resume_phase", boundary.get("next_scheduled_phase", "auto"))
324
+ stop_reason = boundary.get("stop_reason", "(none)")
325
+ reason_code = boundary.get("reason_code", "")
326
+ blocked_subcode = boundary.get("blocked_subcode", "")
327
+ story_id = boundary.get("story_id", resume.get("story_id", "(none)"))
328
+
329
+ hook = invoke_cmd or f"/auto start-from={phase}"
330
+ print(f"[OUTER_DRIVER] cycle={outer_cycle} hook={hook!r} stop_reason={stop_reason}")
331
+
332
+ if dry_run:
333
+ if _should_drain_advance(boundary, merged):
334
+ print("[OUTER_DRIVER_DRY_RUN] drain-advance: schedule next OPEN item immediately")
335
+ if stop_reason not in ("(none)", "completed") and _is_recoverable(
336
+ stop_reason, reason_code, blocked_subcode, merged
337
+ ):
338
+ print(
339
+ f"[OUTER_DRIVER_DRY_RUN] block-retry: {stop_reason} "
340
+ f"remediation={_remediation_for(stop_reason)}"
341
+ )
342
+ continue
343
+
344
+ if timeout_sec is not None and timeout_sec <= 0:
345
+ print("AUTO_OUTER_DRIVER_TIMEOUT", file=sys.stderr)
346
+ return EXIT_TIMEOUT
347
+
348
+ if not dry_run and invoke_cmd is not None:
349
+ try:
350
+ proc = subprocess.run(
351
+ invoke_cmd,
352
+ shell=True,
353
+ cwd=str(repo),
354
+ timeout=timeout_sec,
355
+ check=False,
356
+ )
357
+ if timeout_sec and proc.returncode == 124:
358
+ return EXIT_TIMEOUT
359
+ except subprocess.TimeoutExpired:
360
+ print("AUTO_OUTER_DRIVER_TIMEOUT", file=sys.stderr)
361
+ return EXIT_TIMEOUT
362
+
363
+ if stop_reason == "(none)" or stop_reason == "completed":
364
+ if _should_drain_advance(boundary, merged):
365
+ stories_processed += 1
366
+ if story_cap > 0 and stories_processed >= story_cap:
367
+ print("BACKLOG_MAX_STORIES_REACHED", file=sys.stderr)
368
+ return EXIT_BACKLOG_MAX
369
+ print(
370
+ "[OUTER_DRIVER] drain-advance-without-pause: "
371
+ "resume_brief + state.md refresh per DEC-0069"
372
+ )
373
+ continue
374
+ if stop_reason == "completed":
375
+ return EXIT_COMPLETED
376
+ continue
377
+
378
+ if merged.get("AUTO_PAUSE_REQUEST", "0") == "1":
379
+ print("AUTO_PAUSE_REQUEST", file=sys.stderr)
380
+ return EXIT_PAUSE
381
+
382
+ if _is_recoverable(stop_reason, reason_code, blocked_subcode, merged):
383
+ retries = _count_retries(_ledger_path(repo, orch_id), story_id, stop_reason)
384
+ if retries >= retry_cap:
385
+ ledger_seq += 1
386
+ append_ledger_record(
387
+ repo,
388
+ orch_id,
389
+ story_id=story_id,
390
+ stop_reason=stop_reason,
391
+ reason_code=reason_code,
392
+ remediation_action=_remediation_for(stop_reason),
393
+ outcome="cap_exhausted",
394
+ outer_cycle_index=outer_cycle,
395
+ implementation_loop_index=impl_loop,
396
+ seq=ledger_seq,
397
+ )
398
+ print("BLOCK_RETRY_CAP_EXHAUSTED", file=sys.stderr)
399
+ return EXIT_BLOCK_RETRY_CAP
400
+ ledger_seq += 1
401
+ append_ledger_record(
402
+ repo,
403
+ orch_id,
404
+ story_id=story_id,
405
+ stop_reason=stop_reason,
406
+ reason_code=reason_code,
407
+ remediation_action=_remediation_for(stop_reason),
408
+ outcome="retry_scheduled",
409
+ outer_cycle_index=outer_cycle,
410
+ implementation_loop_index=impl_loop,
411
+ seq=ledger_seq,
412
+ )
413
+ print(f"[OUTER_DRIVER] block-retry scheduled for {stop_reason}")
414
+ continue
415
+
416
+ exit_code = _map_exit(stop_reason, reason_code, merged, stories_processed, story_cap)
417
+ if exit_code == EXIT_BACKLOG_MAX:
418
+ print("BACKLOG_MAX_STORIES_REACHED", file=sys.stderr)
419
+ return exit_code if exit_code >= 0 else EXIT_HARD_STOP
420
+
421
+ print("AUTO_LOOP_MAX_CYCLES", file=sys.stderr)
422
+ return EXIT_LOOP_MAX
423
+
424
+
425
+ def self_test() -> None:
426
+ repo = Path(__file__).resolve().parents[1]
427
+ merged_bad = {"AUTO_FLOW_MODE": "manual"}
428
+ assert merged_bad.get("AUTO_FLOW_MODE") != "full_autonomy"
429
+
430
+ sha_active = hashlib.sha256(
431
+ (repo / "scripts" / "auto_outer_driver.py").read_bytes()
432
+ ).hexdigest()
433
+ tpl = repo / "template" / "scripts" / "auto_outer_driver.py"
434
+ if tpl.is_file():
435
+ sha_tpl = hashlib.sha256(tpl.read_bytes()).hexdigest()
436
+ assert sha_active == sha_tpl, "active/template auto_outer_driver.py drift"
437
+
438
+ assert _remediation_for("qa_fail") == "qa"
439
+ assert _is_recoverable("blocked", "", "", {"AUTO_IMPLEMENTATION_LOOP": "0"})
440
+ assert not _is_recoverable(
441
+ "blocked", "isolation", "isolation", {"AUTO_IMPLEMENTATION_LOOP": "0"}
442
+ )
443
+ assert _map_exit("pause_request", "", {}, 0, 10) == EXIT_PAUSE
444
+ assert _map_exit("loop_max", "", {}, 0, 10) == EXIT_LOOP_MAX
445
+ assert _map_exit("BACKLOG_MAX_STORIES_REACHED", "", {}, 5, 5) == EXIT_BACKLOG_MAX
446
+
447
+ drain_boundary = {
448
+ "stop_reason": "completed",
449
+ "backlog_drain_segment_complete": "1",
450
+ "backlog_drain_stories_remaining_budget": "2",
451
+ "next_scheduled_phase": "discovery",
452
+ }
453
+ assert _should_drain_advance(drain_boundary, {"AUTO_BACKLOG_DRAIN": "1"})
454
+
455
+
456
+ def main() -> int:
457
+ parser = argparse.ArgumentParser(
458
+ description="Full-autonomy outer driver (US-0092 / DEC-0078). Spawn-only."
459
+ )
460
+ parser.add_argument("--repo", default=".", help="Repository root (default .).")
461
+ parser.add_argument("--max-cycles", type=int, default=None, help="Override AUTO_LOOP_MAX_CYCLES.")
462
+ parser.add_argument("--max-stories", type=int, default=None, help="Override AUTO_BACKLOG_MAX_STORIES.")
463
+ parser.add_argument("--dry-run", action="store_true", help="Emit planned invocations only.")
464
+ parser.add_argument(
465
+ "--invoke-cmd",
466
+ default=None,
467
+ help="Optional shell prefix for /auto hook; default prints normative /auto line.",
468
+ )
469
+ parser.add_argument("--self-test", action="store_true", help="Run stable marker subtests.")
470
+ parser.add_argument(
471
+ "--simulate-stop",
472
+ default=None,
473
+ help="Self-test only: inject stop_reason for exit-code path (e.g. pause_request).",
474
+ )
475
+ args = parser.parse_args()
476
+ repo = Path(args.repo).resolve()
477
+
478
+ if args.self_test:
479
+ try:
480
+ self_test()
481
+ except AssertionError as exc:
482
+ print(f"self-test failed: {exc}", file=sys.stderr)
483
+ return EXIT_CONFIG
484
+ if args.simulate_stop:
485
+ merged, err = _require_full_autonomy(repo)
486
+ if err:
487
+ print(err, file=sys.stderr)
488
+ return EXIT_CONFIG
489
+ sim = {"stop_reason": args.simulate_stop}
490
+ code = run_driver(
491
+ repo,
492
+ max_cycles=1,
493
+ max_stories=99,
494
+ dry_run=True,
495
+ invoke_cmd=None,
496
+ simulate=sim,
497
+ )
498
+ if args.simulate_stop == "pause_request":
499
+ return EXIT_PAUSE
500
+ if args.simulate_stop == "loop_max":
501
+ return EXIT_LOOP_MAX
502
+ return code
503
+ print("[AUTO_OUTER_DRIVER_SELF_TEST_OK]")
504
+ return EXIT_COMPLETED
505
+
506
+ simulate = None
507
+ if args.simulate_stop:
508
+ simulate = {"stop_reason": args.simulate_stop}
509
+
510
+ return run_driver(
511
+ repo,
512
+ max_cycles=args.max_cycles,
513
+ max_stories=args.max_stories,
514
+ dry_run=args.dry_run,
515
+ invoke_cmd=args.invoke_cmd,
516
+ simulate=simulate,
517
+ )
518
+
519
+
520
+ if __name__ == "__main__":
521
+ sys.exit(main())