its-magic 0.1.2-9 → 0.1.3-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.
Files changed (237) hide show
  1. package/README.md +1602 -755
  2. package/bin/its-magic.js +121 -11
  3. package/bin/postinstall.js +93 -0
  4. package/installer.ps1 +759 -0
  5. package/installer.py +1036 -0
  6. package/installer.sh +649 -0
  7. package/package.json +18 -14
  8. package/scripts/check_intake_template_parity.py +312 -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 +523 -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/CHANGELOG.md +11 -0
  65. package/template/README.md +1602 -0
  66. package/template/decisions/DEC-0001.md +11 -0
  67. package/template/decisions/DEC-0002.md +11 -0
  68. package/template/docs/developer/README.md +44 -0
  69. package/template/docs/engineering/architecture.md +9 -0
  70. package/template/docs/engineering/artifact-ordering-policy.md +48 -0
  71. package/template/docs/engineering/artifact-ownership-policy.md +57 -0
  72. package/template/docs/engineering/auto-orchestration-reference.md +1211 -0
  73. package/{docs → template/docs}/engineering/codebase-map.md +0 -5
  74. package/template/docs/engineering/compatibility-report.md +20 -0
  75. package/template/docs/engineering/compatibility-signals.md +7 -0
  76. package/template/docs/engineering/component-scope-report.md +16 -0
  77. package/template/docs/engineering/component-scope.md +19 -0
  78. package/template/docs/engineering/context/installer-owned-paths.manifest +96 -0
  79. package/template/docs/engineering/context/readme-section-affinity.json +30 -0
  80. package/template/docs/engineering/decisions.md +16 -0
  81. package/template/docs/engineering/legacy-drift-audit.md +21 -0
  82. package/template/docs/engineering/manifests/components/api-gateway.manifest.yaml +12 -0
  83. package/template/docs/engineering/manifests/registry.manifest.yaml +20 -0
  84. package/template/docs/engineering/manifests/repo.manifest.yaml +11 -0
  85. package/template/docs/engineering/phase-context.md +16 -0
  86. package/template/docs/engineering/release-targets.json +123 -0
  87. package/template/docs/engineering/research.md +29 -0
  88. package/template/docs/engineering/runbook.md +2391 -0
  89. package/template/docs/engineering/runtime-connectivity.md +81 -0
  90. package/template/docs/engineering/security-review.md +33 -0
  91. package/template/docs/engineering/spec-pack/README.md +20 -0
  92. package/template/docs/engineering/state-archive/README.md +15 -0
  93. package/template/docs/engineering/state.md +20 -0
  94. package/template/docs/engineering/status-normalization-report.md +19 -0
  95. package/template/docs/engineering/token-cost-parity-manifest.md +16 -0
  96. package/template/docs/engineering/us-0084-remote-e2e.md +44 -0
  97. package/template/docs/product/acceptance.md +1 -0
  98. package/template/docs/product/backlog.md +5 -0
  99. package/template/docs/product/vision.md +11 -0
  100. package/template/docs/user-guides/README.md +40 -0
  101. package/{handoffs → template/handoffs}/dev_to_qa.md +0 -3
  102. package/{handoffs → template/handoffs}/po_to_tl.md +0 -3
  103. package/{handoffs → template/handoffs}/qa_to_dev.md +1 -2
  104. package/template/handoffs/release_notes.md +51 -0
  105. package/template/handoffs/release_queue.md +47 -0
  106. package/template/handoffs/release_to_dev.md +31 -0
  107. package/template/handoffs/releases/Sxxxx-release-notes.md +62 -0
  108. package/template/handoffs/releases/vX.Y.Z-release-notes.md.example +21 -0
  109. package/template/handoffs/resume_brief.md +14 -0
  110. package/{handoffs → template/handoffs}/tl_to_dev.md +0 -2
  111. package/template/handoffs/token_cost_runs/README.md +26 -0
  112. package/template/its_magic/.its-magic-version +1 -0
  113. package/template/its_magic/README.md +9 -0
  114. package/template/scripts/auto_outer_driver.py +521 -0
  115. package/template/scripts/caveman_compress_input.py +903 -0
  116. package/template/scripts/check_downstream_ci_guard.py +67 -0
  117. package/template/scripts/check_intake_template_parity.py +312 -0
  118. package/template/scripts/check_token_cost_parity.py +69 -0
  119. package/template/scripts/dev_environment_lib.py +601 -0
  120. package/template/scripts/doc_profile_lib.py +415 -0
  121. package/template/scripts/downstream_ci_guard_lib.py +222 -0
  122. package/template/scripts/enforce-triad-hot-surface.py +753 -0
  123. package/template/scripts/guard_installer_publish.py +88 -0
  124. package/template/scripts/intake_bug_resume_brief_refresh.py +303 -0
  125. package/template/scripts/intake_bug_routing_guard.py +67 -0
  126. package/template/scripts/intake_evidence_lib.py +802 -0
  127. package/template/scripts/intake_evidence_validate.py +73 -0
  128. package/template/scripts/materialize_codebase_map.py +184 -0
  129. package/template/scripts/pack_json_validate.py +130 -0
  130. package/template/scripts/project_readme_coverage_lib.py +417 -0
  131. package/template/scripts/readme_feature_coverage_lib.py +608 -0
  132. package/{scripts → template/scripts}/release-all.sh +27 -4
  133. package/template/scripts/release_changelog_backfill.py +153 -0
  134. package/template/scripts/release_changelog_lib.py +544 -0
  135. package/template/scripts/release_changelog_validate.py +134 -0
  136. package/template/scripts/remote_config_summary.py +243 -0
  137. package/template/scripts/sync_push_gates.py +198 -0
  138. package/template/scripts/token_cost_compare.py +40 -0
  139. package/template/scripts/token_cost_lib.py +108 -0
  140. package/template/scripts/uat_probe_lib.py +868 -0
  141. package/template/scripts/validate-and-push.ps1 +280 -0
  142. package/template/scripts/validate-and-push.sh +243 -0
  143. package/template/scripts/validate_doc_profile.py +103 -0
  144. package/template/scripts/validate_project_readme_coverage.py +151 -0
  145. package/template/scripts/validate_readme_feature_coverage.py +140 -0
  146. package/template/sprints/S0001/progress.md +1 -0
  147. package/template/sprints/S0001/qa-findings.md +9 -0
  148. package/template/sprints/S0001/release-findings.md +24 -0
  149. package/template/sprints/S0001/sprint.md +9 -0
  150. package/template/sprints/S0001/summary.md +7 -0
  151. package/template/sprints/S0001/tasks.md +1 -0
  152. package/template/sprints/S0001/uat.json +6 -0
  153. package/template/sprints/S0001/uat.md +5 -0
  154. package/template/sprints/quick/Q0001/summary.md +1 -0
  155. package/{sprints → template/sprints}/quick/Q0001/task.json +0 -1
  156. package/.cursor/agents/curator.mdc +0 -21
  157. package/.cursor/agents/dev.mdc +0 -20
  158. package/.cursor/agents/po.mdc +0 -19
  159. package/.cursor/agents/qa.mdc +0 -19
  160. package/.cursor/agents/release.mdc +0 -19
  161. package/.cursor/agents/tech-lead.mdc +0 -21
  162. package/.cursor/commands/gsd-architecture.md +0 -29
  163. package/.cursor/commands/gsd-auto.md +0 -27
  164. package/.cursor/commands/gsd-discovery.md +0 -27
  165. package/.cursor/commands/gsd-execute.md +0 -32
  166. package/.cursor/commands/gsd-intake.md +0 -28
  167. package/.cursor/commands/gsd-map-codebase.md +0 -25
  168. package/.cursor/commands/gsd-milestone-complete.md +0 -24
  169. package/.cursor/commands/gsd-milestone-start.md +0 -26
  170. package/.cursor/commands/gsd-pause.md +0 -25
  171. package/.cursor/commands/gsd-plan-verify.md +0 -26
  172. package/.cursor/commands/gsd-qa.md +0 -28
  173. package/.cursor/commands/gsd-quick.md +0 -24
  174. package/.cursor/commands/gsd-refresh-context.md +0 -26
  175. package/.cursor/commands/gsd-release.md +0 -29
  176. package/.cursor/commands/gsd-research.md +0 -28
  177. package/.cursor/commands/gsd-resume.md +0 -26
  178. package/.cursor/commands/gsd-sprint-plan.md +0 -30
  179. package/.cursor/commands/gsd-verify-work.md +0 -25
  180. package/.cursor/hooks.json +0 -26
  181. package/.cursor/plans/cursor-gsd-team-kit_8cfee9b8.plan.md +0 -57
  182. package/.cursor/remote.json +0 -18
  183. package/.cursor/rules/gsd-core.mdc +0 -18
  184. package/.cursor/rules/gsd-handoffs.mdc +0 -10
  185. package/.cursor/rules/gsd-quality.mdc +0 -15
  186. package/.cursor/scratchpad.md +0 -34
  187. package/.github/workflows/ci.yml +0 -47
  188. package/decisions/DEC-0001.md +0 -21
  189. package/decisions/DEC-0002.md +0 -21
  190. package/docs/engineering/architecture.md +0 -354
  191. package/docs/engineering/decisions.md +0 -6
  192. package/docs/engineering/research.md +0 -11
  193. package/docs/engineering/runbook.md +0 -32
  194. package/docs/engineering/state.md +0 -33
  195. package/docs/product/acceptance.md +0 -6
  196. package/docs/product/backlog.md +0 -7
  197. package/docs/product/vision.md +0 -46
  198. package/gsd-installer.ps1 +0 -189
  199. package/gsd-installer.py +0 -195
  200. package/gsd-installer.sh +0 -201
  201. package/handoffs/release_notes.md +0 -14
  202. package/handoffs/resume_brief.md +0 -8
  203. package/milestones/M0001/milestone.json +0 -7
  204. package/milestones/M0001/phases.json +0 -9
  205. package/milestones/M0001/progress.md +0 -3
  206. package/milestones/M0001/summary.md +0 -3
  207. package/scripts/generate-release-notes.ps1 +0 -74
  208. package/scripts/generate-release-notes.sh +0 -63
  209. package/scripts/release-all.ps1 +0 -423
  210. package/sprints/S0001/progress.md +0 -4
  211. package/sprints/S0001/qa-findings.md +0 -113
  212. package/sprints/S0001/sprint.md +0 -70
  213. package/sprints/S0001/summary.md +0 -46
  214. package/sprints/S0001/tasks.md +0 -35
  215. package/sprints/S0001/uat.json +0 -8
  216. package/sprints/S0001/uat.md +0 -8
  217. package/sprints/quick/Q0001/summary.md +0 -3
  218. /package/{.cursor/rules/gsd-escalation.mdc → template/.cursor/rules/escalation.mdc} +0 -0
  219. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/acceptance.json +0 -0
  220. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/acceptance.md +0 -0
  221. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/architecture.json +0 -0
  222. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/architecture.md +0 -0
  223. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/decision.json +0 -0
  224. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/decision.md +0 -0
  225. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/handoff.json +0 -0
  226. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/handoff.md +0 -0
  227. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/milestone.json +0 -0
  228. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/phase-context.json +0 -0
  229. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/plan-verify.json +0 -0
  230. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/sprint.json +0 -0
  231. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/sprint.md +0 -0
  232. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/story.json +0 -0
  233. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/story.md +0 -0
  234. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/uat.json +0 -0
  235. /package/{docs → template/docs}/engineering/context/phase-template.json +0 -0
  236. /package/{docs → template/docs}/engineering/dependencies.json +0 -0
  237. /package/{sprints → template/sprints}/S0001/plan-verify.json +0 -0
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env python3
2
+ """Prepublish / CI guard: installer.sh LF + POSIX-safe startup tokens (US-0084 / AC-2).
3
+
4
+ BUG-0008: reject CR bytes in installer-owned-paths.manifest (CRLF breaks POSIX awk section match).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import shutil
10
+ import subprocess
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ ROOT = Path(__file__).resolve().parents[1]
15
+ INSTALLER_SH = ROOT / "installer.sh"
16
+ INSTALLER_MANIFESTS = (
17
+ ROOT / "docs" / "engineering" / "context" / "installer-owned-paths.manifest",
18
+ ROOT / "template" / "docs" / "engineering" / "context" / "installer-owned-paths.manifest",
19
+ )
20
+
21
+ FORBIDDEN_TOKENS = (
22
+ "set -euo",
23
+ "set -o pipefail",
24
+ "set -eu -o pipefail",
25
+ "set -o errexit",
26
+ "set -o nounset",
27
+ )
28
+
29
+
30
+ def main() -> int:
31
+ if not INSTALLER_SH.is_file():
32
+ print("guard_installer_publish: installer.sh missing", file=sys.stderr)
33
+ return 1
34
+ data = INSTALLER_SH.read_bytes()
35
+ if b"\r" in data:
36
+ print(
37
+ "guard_installer_publish: CR/LF (\\r) bytes found in installer.sh — "
38
+ "use LF only; see docs/engineering/runbook.md (US-0084).",
39
+ file=sys.stderr,
40
+ )
41
+ return 1
42
+ for man in INSTALLER_MANIFESTS:
43
+ if not man.is_file():
44
+ continue
45
+ mdata = man.read_bytes()
46
+ if b"\r" in mdata:
47
+ print(
48
+ f"guard_installer_publish: CR/LF (\\r) bytes found in {man.relative_to(ROOT)} — "
49
+ "use LF only (.gitattributes *.manifest; BUG-0008).",
50
+ file=sys.stderr,
51
+ )
52
+ return 1
53
+ text = data.decode("utf-8", errors="replace")
54
+ for token in FORBIDDEN_TOKENS:
55
+ if token in text:
56
+ print(
57
+ f"guard_installer_publish: forbidden startup token {token!r} in installer.sh",
58
+ file=sys.stderr,
59
+ )
60
+ return 1
61
+ dash = shutil.which("dash")
62
+ if dash:
63
+ r = subprocess.run(
64
+ [dash, "-n", str(INSTALLER_SH)],
65
+ cwd=ROOT,
66
+ capture_output=True,
67
+ text=True,
68
+ encoding="utf-8",
69
+ errors="replace",
70
+ )
71
+ if r.returncode != 0:
72
+ print(
73
+ "guard_installer_publish: dash -n installer.sh failed:\n"
74
+ + (r.stderr or r.stdout or ""),
75
+ file=sys.stderr,
76
+ )
77
+ return 1
78
+ else:
79
+ print(
80
+ "guard_installer_publish: dash not on PATH; skipping dash -n "
81
+ "(Python CRLF + token checks still enforced).",
82
+ file=sys.stderr,
83
+ )
84
+ return 0
85
+
86
+
87
+ if __name__ == "__main__":
88
+ raise SystemExit(main())
@@ -0,0 +1,303 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Atomic refresh of handoffs/resume_brief.md after successful /intake bug persistence (DEC-0069 / BUG-0005).
4
+
5
+ Idempotent: same inputs yield the same latest-pointer section. Uses temp file + os.replace for atomicity.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import os
12
+ import re
13
+ import sys
14
+ import tempfile
15
+ from pathlib import Path
16
+
17
+ import bug_issue_lib as bi
18
+ import bug_issue_validate as biv
19
+
20
+
21
+ def bug_status(backlog_text: str, bug_id: str) -> str | None:
22
+ section = bi.extract_bug_section(backlog_text)
23
+ if not section:
24
+ return None
25
+ for issue in bi.parse_bug_issues(section):
26
+ if issue.bug_id == bug_id:
27
+ return issue.status
28
+ return None
29
+
30
+
31
+ def upsert_latest_orchestration_pointer(full_text: str, latest_block: str) -> str:
32
+ """Replace ## Latest orchestration pointer section or insert after # Resume Brief."""
33
+ latest_block = latest_block.rstrip("\n") + "\n"
34
+ lines = full_text.splitlines(keepends=True)
35
+ if not lines:
36
+ return "# Resume Brief\n\n" + latest_block
37
+
38
+ out: list[str] = []
39
+ i = 0
40
+ replaced = False
41
+ while i < len(lines):
42
+ line = lines[i]
43
+ if line.startswith("## Latest orchestration pointer"):
44
+ out.append(latest_block)
45
+ i += 1
46
+ while i < len(lines) and not lines[i].startswith("## "):
47
+ i += 1
48
+ replaced = True
49
+ continue
50
+ out.append(line)
51
+ i += 1
52
+
53
+ if replaced:
54
+ return "".join(out)
55
+
56
+ text = "".join(lines)
57
+ stripped = text.lstrip("\n")
58
+ if stripped.startswith("# Resume Brief"):
59
+ return text.rstrip("\n") + "\n\n" + latest_block
60
+ return "# Resume Brief\n\n" + latest_block + text.lstrip("\n")
61
+
62
+
63
+ def build_latest_pointer_markdown(
64
+ *,
65
+ bug_id: str,
66
+ intake_boundary_utc: str,
67
+ orchestrator_run_id: str | None,
68
+ intake_evidence_ref: str | None,
69
+ sprint_id: str | None,
70
+ ) -> str:
71
+ orch = orchestrator_run_id if orchestrator_run_id else "(unknown)"
72
+ ev = intake_evidence_ref if intake_evidence_ref else "(none)"
73
+ spr = sprint_id if sprint_id else "(none)"
74
+ return f"""## Latest orchestration pointer — post-bug-intake (DEC-0069)
75
+
76
+ - **Boundary**: successful **`/intake bug`** persistence (**`US-0045`**) — **`intake_boundary_utc={intake_boundary_utc}`**
77
+ - **`bug_id`**: **`{bug_id}`** — must remain **`OPEN`** in **`docs/product/backlog.md`** (authority); this refresh is rejected if backlog shows **DONE**
78
+ - **Intake evidence ref**: `{ev}`
79
+ - **`orchestrator_run_id`**: `{orch}` (boundary metadata when known; optional at intake)
80
+ - **Contract**: default **`/auto`** continuation targets **`discovery`** for this OPEN bug (not a stale pre-intake **`intake`** resume target)
81
+
82
+ ## Current status
83
+
84
+ - **Active bug**: **`{bug_id}`** — **OPEN** per **`docs/product/backlog.md`** at refresh time
85
+
86
+ ## Intended resume phase
87
+
88
+ `discovery`
89
+
90
+ ## Resume target
91
+
92
+ - bug_id={bug_id}
93
+ - story_id=(none)
94
+ - sprint_id={spr}
95
+ - boundary=post-bug-intake (**DEC-0069**)
96
+
97
+ ## Latest auto breadcrumb seed
98
+
99
+ - requested_start_from=(none)
100
+ - resolved_start_phase=discovery
101
+ - resolution_source=resume_brief
102
+ - resolution_status=resolved
103
+ - stop_reason=intake_complete
104
+ - stop_phase=intake
105
+ - next_scheduled_phase=discovery
106
+ - bug_id={bug_id}
107
+ - story_id=(none)
108
+ - sprint_id={spr}
109
+ - orchestrator_run_id={orch}
110
+ - intake_boundary_utc={intake_boundary_utc}
111
+ """
112
+
113
+
114
+ def extract_brief_bug_id(brief_text: str) -> str | None:
115
+ for line in brief_text.splitlines():
116
+ s = line.strip()
117
+ m = re.match(r"^-\s*bug_id=(BUG-\d{4})\s*$", s)
118
+ if m:
119
+ return m.group(1)
120
+ return None
121
+
122
+
123
+ def validate_brief_open_bug_alignment(brief_text: str, backlog_text: str) -> list[str]:
124
+ """Writer-side guard: brief bug_id must match an OPEN row in backlog (US-0045)."""
125
+ errors: list[str] = []
126
+ bid = extract_brief_bug_id(brief_text)
127
+ if not bid:
128
+ errors.append("INTAKE_RESUME_BRIEF_PARSE_BUG_ID_MISSING")
129
+ return errors
130
+
131
+ st = bug_status(backlog_text, bid)
132
+ if st is None:
133
+ errors.append(f"INTAKE_RESUME_BRIEF_BACKLOG_BUG_UNKNOWN:{bid}")
134
+ return errors
135
+ if st != "OPEN":
136
+ errors.append(f"INTAKE_RESUME_BRIEF_BACKLOG_CONTRADICTION:{bid}:status={st}")
137
+ if "`discovery`" not in brief_text and "resolved_start_phase=discovery" not in brief_text:
138
+ errors.append("INTAKE_RESUME_BRIEF_DISCOVERY_PHASE_MISSING")
139
+ return errors
140
+
141
+
142
+ def atomic_write(path: Path, content: str) -> None:
143
+ path.parent.mkdir(parents=True, exist_ok=True)
144
+ fd, tmp = tempfile.mkstemp(
145
+ dir=path.parent,
146
+ prefix=".resume_brief_tmp_",
147
+ suffix=".md",
148
+ )
149
+ try:
150
+ with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as f:
151
+ f.write(content)
152
+ os.replace(tmp, path)
153
+ except Exception:
154
+ try:
155
+ os.unlink(tmp)
156
+ except OSError:
157
+ pass
158
+ raise
159
+
160
+
161
+ def _fail(code: str, detail: str = "") -> int:
162
+ msg = code
163
+ if detail:
164
+ msg += f": {detail}"
165
+ print(msg, file=sys.stderr)
166
+ return 1
167
+
168
+
169
+ def self_test() -> int:
170
+ block = build_latest_pointer_markdown(
171
+ bug_id="BUG-0999",
172
+ intake_boundary_utc="2026-04-03T12:00:00Z",
173
+ orchestrator_run_id="auto-test",
174
+ intake_evidence_ref="handoffs/intake_evidence/x.json",
175
+ sprint_id="S0001",
176
+ )
177
+ old = "# Resume Brief\n\n## Latest orchestration pointer — old\n\n- x\n\n## Checkpoint\n\nkeep\n"
178
+ new = upsert_latest_orchestration_pointer(old, block)
179
+ if "keep" not in new:
180
+ return _fail("SELFTEST_FAILED", "lost tail section")
181
+ if "BUG-0999" not in new or "resolved_start_phase=discovery" not in new:
182
+ return _fail("SELFTEST_FAILED", "missing expected content")
183
+ if "## Latest orchestration pointer — old" in new:
184
+ return _fail("SELFTEST_FAILED", "old latest not replaced")
185
+ good_backlog = """## Bug issues (canonical)
186
+
187
+ ### BUG-0999 — T
188
+ - Status: OPEN
189
+ - environment: e
190
+ - steps_to_reproduce: s
191
+ - expected: x
192
+ - actual: y
193
+ - evidence_refs: z
194
+ """
195
+ errs = validate_brief_open_bug_alignment(new, good_backlog)
196
+ if errs:
197
+ return _fail("SELFTEST_FAILED", str(errs))
198
+ bad_backlog = good_backlog.replace("OPEN", "DONE")
199
+ errs2 = validate_brief_open_bug_alignment(new, bad_backlog)
200
+ if not any("CONTRADICTION" in e for e in errs2):
201
+ return _fail("SELFTEST_FAILED", "expected contradiction on DONE")
202
+ print("[INTAKE_BUG_RESUME_BRIEF_REFRESH_OK]")
203
+ return 0
204
+
205
+
206
+ def main() -> int:
207
+ ap = argparse.ArgumentParser(description=__doc__)
208
+ ap.add_argument("--bug-id", default="", help="Persisted BUG-#### id (required unless --self-test)")
209
+ ap.add_argument("--backlog", default="docs/product/backlog.md")
210
+ ap.add_argument("--resume-brief", default="handoffs/resume_brief.md")
211
+ ap.add_argument(
212
+ "--intake-boundary-utc",
213
+ default="",
214
+ help="RFC3339 UTC timestamp for intake completion boundary (required unless --validate-file)",
215
+ )
216
+ ap.add_argument("--orchestrator-run-id", default="", help="Optional orchestrator run id")
217
+ ap.add_argument("--intake-evidence", default="", help="Optional intake evidence path or ref")
218
+ ap.add_argument("--sprint-id", default="", help="Optional sprint id")
219
+ ap.add_argument("--dry-run", action="store_true", help="Print body only; do not write")
220
+ ap.add_argument("--validate-file", action="store_true", help="Validate existing brief vs backlog; no write")
221
+ ap.add_argument("--self-test", action="store_true")
222
+ args = ap.parse_args()
223
+ if args.self_test:
224
+ return self_test()
225
+
226
+ if not args.bug_id:
227
+ return _fail("INTAKE_RESUME_BRIEF_INVALID_BUG_ID", "missing --bug-id")
228
+ if not re.fullmatch(r"BUG-\d{4}", args.bug_id):
229
+ return _fail("INTAKE_RESUME_BRIEF_INVALID_BUG_ID", args.bug_id)
230
+
231
+ try:
232
+ backlog_text = Path(args.backlog).read_text(encoding="utf-8")
233
+ except OSError as e:
234
+ return _fail("INTAKE_RESUME_BRIEF_IO_ERROR", str(e))
235
+
236
+ berr, _ = biv.validate_backlog(backlog_text)
237
+ if berr:
238
+ for e in berr:
239
+ print(e, file=sys.stderr)
240
+ return _fail("INTAKE_RESUME_BRIEF_BACKLOG_INVALID", berr[0])
241
+
242
+ resume_path = Path(args.resume_brief)
243
+ if args.validate_file:
244
+ if not resume_path.is_file():
245
+ return _fail("INTAKE_RESUME_BRIEF_MISSING", str(resume_path))
246
+ brief_text = resume_path.read_text(encoding="utf-8")
247
+ bid_file = extract_brief_bug_id(brief_text)
248
+ if bid_file != args.bug_id:
249
+ return _fail(
250
+ "INTAKE_RESUME_BRIEF_BUG_ID_MISMATCH",
251
+ f"cli={args.bug_id} brief={bid_file}",
252
+ )
253
+ verr = validate_brief_open_bug_alignment(brief_text, backlog_text)
254
+ if verr:
255
+ for e in verr:
256
+ print(e, file=sys.stderr)
257
+ return 1
258
+ print("[INTAKE_RESUME_BRIEF_VALIDATE_OK]")
259
+ return 0
260
+
261
+ if not args.intake_boundary_utc.strip():
262
+ return _fail("INTAKE_RESUME_BRIEF_BOUNDARY_UTC_REQUIRED", "supply --intake-boundary-utc")
263
+
264
+ st = bug_status(backlog_text, args.bug_id)
265
+ if st is None:
266
+ return _fail("INTAKE_RESUME_BRIEF_BUG_NOT_FOUND", args.bug_id)
267
+ if st != "OPEN":
268
+ return _fail(
269
+ "INTAKE_RESUME_BRIEF_BACKLOG_CONTRADICTION",
270
+ f"{args.bug_id} must be OPEN for discovery default; got {st}",
271
+ )
272
+
273
+ block = build_latest_pointer_markdown(
274
+ bug_id=args.bug_id,
275
+ intake_boundary_utc=args.intake_boundary_utc,
276
+ orchestrator_run_id=args.orchestrator_run_id or None,
277
+ intake_evidence_ref=args.intake_evidence or None,
278
+ sprint_id=args.sprint_id or None,
279
+ )
280
+
281
+ prior = resume_path.read_text(encoding="utf-8") if resume_path.is_file() else ""
282
+ merged = upsert_latest_orchestration_pointer(prior, block)
283
+ verr = validate_brief_open_bug_alignment(merged, backlog_text)
284
+ if verr:
285
+ for e in verr:
286
+ print(e, file=sys.stderr)
287
+ return 1
288
+
289
+ if args.dry_run:
290
+ print(merged)
291
+ return 0
292
+
293
+ try:
294
+ atomic_write(resume_path, merged)
295
+ except OSError as e:
296
+ return _fail("INTAKE_RESUME_BRIEF_WRITE_FAILED", str(e))
297
+
298
+ print("[INTAKE_BUG_RESUME_BRIEF_REFRESH_OK]")
299
+ return 0
300
+
301
+
302
+ if __name__ == "__main__":
303
+ raise SystemExit(main())
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Fail-closed guard: defect-shaped prose must not persist as US-xxxx without bug routing (DEC-0061 §5).
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+ import re
10
+ import sys
11
+
12
+ # Strong defect signals (deterministic heuristic — PO still must set INTAKE_WORK_ITEM_KIND or /intake bug)
13
+ _REPRO = re.compile(
14
+ r"\b(steps\s+to\s+reproduce|steps_to_reproduce|repro\s+steps|reproduction\s+steps)\b",
15
+ re.IGNORECASE,
16
+ )
17
+ _DEFECT = re.compile(
18
+ r"\b(bug|regression|defect|crash|stack\s+trace|broken|throws\s+exception)\b",
19
+ re.IGNORECASE,
20
+ )
21
+
22
+
23
+ def prose_looks_like_defect(text: str) -> bool:
24
+ low = text.lower()
25
+ if not _DEFECT.search(low):
26
+ return False
27
+ if _REPRO.search(low):
28
+ return True
29
+ if "expected" in low and "actual" in low:
30
+ return True
31
+ return False
32
+
33
+
34
+ def main() -> int:
35
+ ap = argparse.ArgumentParser(
36
+ description="If work item kind is story but prose looks like a defect report, fail with INTAKE_BUG_ROUTING_REQUIRED."
37
+ )
38
+ ap.add_argument("--kind", choices=("story", "bug"), required=True)
39
+ ap.add_argument("--file", help="Path to prose file (title+summary)")
40
+ ap.add_argument("--stdin", action="store_true", help="Read prose from stdin")
41
+ args = ap.parse_args()
42
+
43
+ if args.kind == "bug":
44
+ print("[INTAKE_BUG_ROUTING_OK] kind=bug")
45
+ return 0
46
+
47
+ if args.stdin:
48
+ text = sys.stdin.read()
49
+ elif args.file:
50
+ text = open(args.file, encoding="utf-8").read()
51
+ else:
52
+ print("INTAKE_BUG_ROUTING_GUARD_ERROR: provide --file or --stdin", file=sys.stderr)
53
+ return 2
54
+
55
+ if prose_looks_like_defect(text):
56
+ print(
57
+ "INTAKE_BUG_ROUTING_REQUIRED: defect-shaped prose with INTAKE_WORK_ITEM_KIND=story "
58
+ "(set INTAKE_WORK_ITEM_KIND=bug and/or use `/intake bug` per DEC-0061 §5)",
59
+ file=sys.stderr,
60
+ )
61
+ return 3
62
+ print("[INTAKE_BUG_ROUTING_OK] kind=story")
63
+ return 0
64
+
65
+
66
+ if __name__ == "__main__":
67
+ raise SystemExit(main())