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,134 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Validate release changelog / version-doc consistency (US-0100 / DEC-0085).
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+ import os
10
+ import re
11
+ import sys
12
+
13
+ _SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
14
+ _REPO_ROOT = os.path.normpath(os.path.join(_SCRIPT_DIR, ".."))
15
+ if _REPO_ROOT not in sys.path:
16
+ sys.path.insert(0, _REPO_ROOT)
17
+ if _SCRIPT_DIR not in sys.path:
18
+ sys.path.insert(0, _SCRIPT_DIR)
19
+
20
+ import release_changelog_lib as rcl # noqa: E402
21
+
22
+ REMEDIATION = {
23
+ rcl.RELEASE_CHANGELOG_VERSION_MISSING: "Set explicit semver on queue row or pass valid --semver.",
24
+ rcl.RELEASE_CHANGELOG_DUPLICATE_VERSION: "Resolve fingerprint mismatch; do not reuse semver for different work items.",
25
+ rcl.RELEASE_CHANGELOG_WORK_ITEM_GAP: "Run derive/build_version_doc or complete sprint notes + backlog refs.",
26
+ rcl.RELEASE_CHANGELOG_ORDER_INVALID: "Reorder CHANGELOG.md semver sections newest-first.",
27
+ rcl.RELEASE_CHANGELOG_UNRELEASED_MISSING: "Add mandatory ## [Unreleased] header at top of CHANGELOG.md.",
28
+ rcl.RELEASE_CHANGELOG_QUEUE_DRIFT: "Re-run bind_queue_release_version or reconcile queue release_version.",
29
+ rcl.RELEASE_CHANGELOG_VERSION_DOC_MISSING: "Run build_version_doc or release_changelog_backfill before gh -F attach.",
30
+ rcl.RELEASE_CHANGELOG_SPRINT_ORPHAN: "Include released sprint in semver doc or [Unreleased] via backfill.",
31
+ rcl.RELEASE_CHANGELOG_BACKFILL_AMBIGUOUS: "Fix manifest collision; one semver intent per coalesce group.",
32
+ rcl.RELEASE_CHANGELOG_IDEMPOTENCY_VIOLATION: "Remove duplicate work-item bullets for same version; re-run derive.",
33
+ }
34
+
35
+
36
+ def _emit(code: str, detail: str = "") -> None:
37
+ msg = code if not detail else f"{code}: {detail}"
38
+ print(msg, file=sys.stderr)
39
+ rem = REMEDIATION.get(code, "See docs/engineering/runbook.md § Version-scoped release docs.")
40
+ print(f"Remediation: {rem}", file=sys.stderr)
41
+
42
+
43
+ def validate(repo_root: str, enforce: bool) -> int:
44
+ errors: list[str] = []
45
+ cl_path = rcl.changelog_path(repo_root)
46
+ if not os.path.isfile(cl_path):
47
+ errors.append(rcl.RELEASE_CHANGELOG_UNRELEASED_MISSING)
48
+ else:
49
+ text = rcl.read_utf8(cl_path)
50
+ if "## [Unreleased]" not in text:
51
+ errors.append(rcl.RELEASE_CHANGELOG_UNRELEASED_MISSING)
52
+ # semver order newest-first (skip Unreleased)
53
+ versions: list[str] = []
54
+ for m in re.finditer(r"^##\s+\[(.+?)\]", text, re.MULTILINE):
55
+ v = m.group(1)
56
+ if v.lower() == "unreleased":
57
+ continue
58
+ try:
59
+ versions.append(rcl.normalize_semver(v))
60
+ except rcl.ReleaseChangelogError:
61
+ errors.append(rcl.RELEASE_CHANGELOG_VERSION_MISSING)
62
+ # Simple order check: compare as strings after normalization (pre-release aware minimal)
63
+ if len(versions) > 1:
64
+ for i in range(len(versions) - 1):
65
+ if versions[i] < versions[i + 1]:
66
+ errors.append(rcl.RELEASE_CHANGELOG_ORDER_INVALID)
67
+ break
68
+
69
+ rows = rcl.parse_queue_rows(repo_root)
70
+ for row in rows:
71
+ if row.status != "released":
72
+ continue
73
+ rv = row.release_version.strip()
74
+ if not rv:
75
+ continue
76
+ try:
77
+ norm = rcl.normalize_semver(rv)
78
+ except rcl.ReleaseChangelogError:
79
+ errors.append(rcl.RELEASE_CHANGELOG_VERSION_MISSING)
80
+ continue
81
+ vdoc = rcl.version_doc_path(repo_root, norm)
82
+ if not os.path.isfile(vdoc):
83
+ errors.append(rcl.RELEASE_CHANGELOG_VERSION_DOC_MISSING)
84
+ section = rcl.extract_changelog_section(norm, repo_root)
85
+ if section is None:
86
+ errors.append(rcl.RELEASE_CHANGELOG_VERSION_MISSING)
87
+ # work item gap: story_refs should appear in version doc
88
+ if os.path.isfile(vdoc):
89
+ doc_text = rcl.read_utf8(vdoc)
90
+ for token in re.split(r"[,;\s]+", row.story_refs):
91
+ token = token.strip()
92
+ if token and token not in doc_text:
93
+ errors.append(rcl.RELEASE_CHANGELOG_WORK_ITEM_GAP)
94
+
95
+ # queue drift: coalesce groups vs bound semver
96
+ groups = rcl.coalesce_sprints_by_semver(rows, repo_root)
97
+ for semver, sids in groups.items():
98
+ for sid in sids:
99
+ row = next((r for r in rows if r.sprint_id == sid), None)
100
+ if row and row.release_version.strip():
101
+ try:
102
+ if rcl.normalize_semver(row.release_version) != semver:
103
+ errors.append(rcl.RELEASE_CHANGELOG_QUEUE_DRIFT)
104
+ except rcl.ReleaseChangelogError:
105
+ errors.append(rcl.RELEASE_CHANGELOG_QUEUE_DRIFT)
106
+
107
+ seen_errors = sorted(set(errors))
108
+ for code in seen_errors:
109
+ _emit(code)
110
+ if seen_errors and enforce:
111
+ return 1
112
+ if not seen_errors:
113
+ print("[RELEASE_CHANGELOG_VALIDATE_OK]")
114
+ return 0
115
+ if not enforce:
116
+ print("[RELEASE_CHANGELOG_VALIDATE_WARN]")
117
+ return 0
118
+ return 1
119
+
120
+
121
+ def main() -> int:
122
+ parser = argparse.ArgumentParser(description=__doc__)
123
+ parser.add_argument("--repo", default=_REPO_ROOT, help="Repository root")
124
+ parser.add_argument(
125
+ "--enforce",
126
+ action="store_true",
127
+ help="Exit non-zero on any fail code (release gate / release-all.sh).",
128
+ )
129
+ args = parser.parse_args()
130
+ return validate(os.path.abspath(args.repo), args.enforce)
131
+
132
+
133
+ if __name__ == "__main__":
134
+ raise SystemExit(main())
@@ -0,0 +1,243 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Summarize .cursor/remote.json for operators (US-0084 / US-0064 alignment).
4
+
5
+ Stdout: non-secret labels and env-reference names only (no key material).
6
+ Stderr: errors and skip reasons.
7
+
8
+ Exit codes:
9
+ 0 OK or REMOTE_EXECUTION=0 skip (DEC-0070)
10
+ 1 usage / CLI error
11
+ 2 config missing or unreadable
12
+ 3 invalid JSON
13
+ 4 schema / contract mismatch vs runbook remote.json contract
14
+ 5 reserved (unused; DEC-0070 maps REMOTE_EXECUTION=0 to 0)
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import json
21
+ import os
22
+ import re
23
+ import sys
24
+ from pathlib import Path
25
+ from typing import Any
26
+
27
+ ENV_VAR_NAME = re.compile(r"^[A-Z][A-Z0-9_]*$")
28
+ ALLOWED_TYPES = frozenset({"docker", "ssh", "vm"})
29
+ AUTH_MODES = frozenset({"none", "env"})
30
+
31
+
32
+ def _truthy_remote_execution(raw: str | None) -> bool:
33
+ if raw is None:
34
+ return False
35
+ return raw.strip() in {"1", "true", "TRUE", "yes", "YES", "on", "ON"}
36
+
37
+
38
+ def _parse_args(argv: list[str]) -> tuple[argparse.Namespace, list[str]]:
39
+ p = argparse.ArgumentParser(description=__doc__.split("\n\n")[0], add_help=True)
40
+ p.add_argument(
41
+ "--config",
42
+ default=None,
43
+ help="Path to remote JSON (default: REMOTE_CONFIG env or .cursor/remote.json)",
44
+ )
45
+ return p.parse_known_args(argv)
46
+
47
+
48
+ def _config_path(explicit: str | None) -> Path:
49
+ if explicit:
50
+ return Path(explicit).expanduser()
51
+ env = os.environ.get("REMOTE_CONFIG")
52
+ if env:
53
+ return Path(env).expanduser()
54
+ return Path(".cursor/remote.json")
55
+
56
+
57
+ def _validate_env_ref(name: str, value: Any, prefix: str) -> str | None:
58
+ if not isinstance(value, str):
59
+ return f"{prefix}: {name} must be a string env-reference name"
60
+ if not ENV_VAR_NAME.match(value):
61
+ return f"{prefix}: {name} must look like an env var name (A-Z_0-9)"
62
+ return None
63
+
64
+
65
+ def validate_and_summarize(path: Path, data: dict[str, Any]) -> tuple[list[str], list[str]]:
66
+ """Return (stdout_lines, error_messages)."""
67
+ errs: list[str] = []
68
+ out: list[str] = []
69
+ out.append(f"remote_config={path.as_posix()}")
70
+
71
+ if not isinstance(data.get("version"), int):
72
+ errs.append("[REMOTE_CONFIG_ERROR] root.version: expected integer")
73
+ if not isinstance(data.get("defaultTarget"), str) or not data["defaultTarget"]:
74
+ errs.append("[REMOTE_CONFIG_ERROR] root.defaultTarget: expected non-empty string")
75
+ targets = data.get("targets")
76
+ if not isinstance(targets, list) or not targets:
77
+ errs.append("[REMOTE_CONFIG_ERROR] root.targets: expected non-empty array")
78
+
79
+ if errs:
80
+ return out, errs
81
+
82
+ by_id: dict[str, dict[str, Any]] = {}
83
+ for i, t in enumerate(targets):
84
+ prefix = f"targets[{i}]"
85
+ if not isinstance(t, dict):
86
+ errs.append(f"[REMOTE_CONFIG_ERROR] {prefix}: expected object")
87
+ continue
88
+ tid = t.get("id")
89
+ if not isinstance(tid, str) or not tid:
90
+ errs.append(f"[REMOTE_CONFIG_ERROR] {prefix}.id: expected non-empty string")
91
+ ttype = t.get("type")
92
+ if ttype not in ALLOWED_TYPES:
93
+ errs.append(
94
+ f"[REMOTE_CONFIG_ERROR] {prefix}.type: expected one of {sorted(ALLOWED_TYPES)}"
95
+ )
96
+ if not isinstance(t.get("enabled"), bool):
97
+ errs.append(f"[REMOTE_CONFIG_ERROR] {prefix}.enabled: expected boolean")
98
+ if not isinstance(t.get("host"), str) or not t["host"]:
99
+ errs.append(f"[REMOTE_CONFIG_ERROR] {prefix}.host: expected non-empty string")
100
+ port = t.get("port")
101
+ if not isinstance(port, int) or not (1 <= port <= 65535):
102
+ errs.append(f"[REMOTE_CONFIG_ERROR] {prefix}.port: expected integer 1..65535")
103
+ if not isinstance(t.get("workspaceRoot"), str) or not t["workspaceRoot"]:
104
+ errs.append(
105
+ f"[REMOTE_CONFIG_ERROR] {prefix}.workspaceRoot: expected non-empty string"
106
+ )
107
+ auth = t.get("auth")
108
+ if auth is not None:
109
+ if not isinstance(auth, dict):
110
+ errs.append(f"[REMOTE_CONFIG_ERROR] {prefix}.auth: expected object")
111
+ else:
112
+ mode = auth.get("mode")
113
+ if mode not in AUTH_MODES:
114
+ errs.append(
115
+ f"[REMOTE_CONFIG_ERROR] {prefix}.auth.mode: expected one of {sorted(AUTH_MODES)}"
116
+ )
117
+ elif mode == "env":
118
+ env_keys = (
119
+ "tokenEnv",
120
+ "passwordEnv",
121
+ "privateKeyPathEnv",
122
+ "usernameEnv",
123
+ )
124
+ any_ref = False
125
+ for k in env_keys:
126
+ if k not in auth:
127
+ continue
128
+ any_ref = True
129
+ err = _validate_env_ref(k, auth[k], f"{prefix}.auth")
130
+ if err:
131
+ errs.append(f"[REMOTE_CONFIG_ERROR] {err}")
132
+ if not any_ref:
133
+ errs.append(
134
+ f"[REMOTE_CONFIG_ERROR] {prefix}.auth: mode=env requires at least one *Env field"
135
+ )
136
+ if isinstance(tid, str) and tid:
137
+ by_id[tid] = t
138
+
139
+ default = data["defaultTarget"]
140
+ if default not in by_id:
141
+ errs.append(
142
+ f"[REMOTE_CONFIG_ERROR] defaultTarget={default!r} not found in targets[].id"
143
+ )
144
+ elif not by_id[default].get("enabled"):
145
+ errs.append(
146
+ f"[REMOTE_CONFIG_ERROR] defaultTarget={default!r} must reference an enabled target"
147
+ )
148
+
149
+ if errs:
150
+ return out, errs
151
+
152
+ out.append(f"defaultTarget={data['defaultTarget']}")
153
+ for t in targets:
154
+ assert isinstance(t, dict)
155
+ tid = t["id"]
156
+ parts = [
157
+ f"id={tid}",
158
+ f"type={t['type']}",
159
+ f"enabled={t['enabled']!s}",
160
+ f"host={t['host']}",
161
+ f"port={t['port']}",
162
+ f"workspaceRoot={t['workspaceRoot']}",
163
+ ]
164
+ auth = t.get("auth")
165
+ if isinstance(auth, dict):
166
+ parts.append(f"auth.mode={auth.get('mode')}")
167
+ for k in (
168
+ "tokenEnv",
169
+ "passwordEnv",
170
+ "privateKeyPathEnv",
171
+ "usernameEnv",
172
+ ):
173
+ if k in auth and isinstance(auth[k], str):
174
+ parts.append(f"auth.{k}={auth[k]}")
175
+ out.append("target:" + " ".join(parts))
176
+
177
+ return out, []
178
+
179
+
180
+ def main(argv: list[str] | None = None) -> int:
181
+ args, rest = _parse_args(argv or sys.argv[1:])
182
+ if rest:
183
+ print(
184
+ f"[REMOTE_CONFIG_SUMMARY] unknown arguments: {' '.join(rest)}",
185
+ file=sys.stderr,
186
+ )
187
+ return 1
188
+ if not _truthy_remote_execution(os.environ.get("REMOTE_EXECUTION")):
189
+ print(
190
+ "[REMOTE_CONFIG_SUMMARY] REMOTE_EXECUTION!=1: skip validation/summary "
191
+ "(zero overhead; DEC-0070 / US-0084).",
192
+ file=sys.stderr,
193
+ )
194
+ return 0
195
+
196
+ path = _config_path(args.config)
197
+ if not path.is_file():
198
+ print(
199
+ f"[REMOTE_CONFIG_ERROR] {path}: file missing or unreadable. "
200
+ "Fix: create config or set REMOTE_EXECUTION=0.",
201
+ file=sys.stderr,
202
+ )
203
+ return 2
204
+ try:
205
+ raw = path.read_text(encoding="utf-8")
206
+ except OSError as e:
207
+ print(
208
+ f"[REMOTE_CONFIG_ERROR] {path}: read failed ({e}). Fix: permissions/path.",
209
+ file=sys.stderr,
210
+ )
211
+ return 2
212
+ try:
213
+ data = json.loads(raw)
214
+ except json.JSONDecodeError as e:
215
+ print(
216
+ f"[REMOTE_CONFIG_ERROR] {path}: invalid JSON ({e}). Fix: syntax.",
217
+ file=sys.stderr,
218
+ )
219
+ return 3
220
+ if not isinstance(data, dict):
221
+ print(
222
+ f"[REMOTE_CONFIG_ERROR] {path}: expected JSON object at root.",
223
+ file=sys.stderr,
224
+ )
225
+ return 4
226
+
227
+ lines, errs = validate_and_summarize(path, data)
228
+ if errs:
229
+ for line in errs:
230
+ print(line, file=sys.stderr)
231
+ print(
232
+ f"[REMOTE_CONFIG_ERROR] {path}: contract check failed. "
233
+ "Fix: align with docs/engineering/runbook.md remote contract (US-0084).",
234
+ file=sys.stderr,
235
+ )
236
+ return 4
237
+ for line in lines:
238
+ print(line)
239
+ return 0
240
+
241
+
242
+ if __name__ == "__main__":
243
+ raise SystemExit(main())
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Shared sync / push eligibility evaluation for validate-and-push (merged scratchpad).
4
+
5
+ Imports installer.merge_scratchpad_layers / validate_merged_scratchpad only — no
6
+ duplicate DEC-0055 precedence logic. Emits machine-readable JSON on stdout; reason
7
+ codes only (no planning-shaped tokens) on stderr for errors.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import fnmatch
14
+ import json
15
+ import os
16
+ import re
17
+ import sys
18
+ from pathlib import Path
19
+ from typing import List, Tuple
20
+
21
+ # Repo root (parent of scripts/)
22
+ _ROOT = Path(__file__).resolve().parent.parent
23
+ if str(_ROOT) not in sys.path:
24
+ sys.path.insert(0, str(_ROOT))
25
+
26
+ import installer # noqa: E402
27
+
28
+ _VALID_MODES = frozenset(
29
+ {"disabled", "manual", "by_phase", "by_milestone", "custom_phase_list"}
30
+ )
31
+
32
+ def _parse_csv(val: str) -> List[str]:
33
+ if not val or not val.strip():
34
+ return []
35
+ return [p.strip() for p in val.split(",") if p.strip()]
36
+
37
+
38
+ def _merge_and_validate(root: str) -> Tuple[dict, List[str]]:
39
+ ok, diags = installer.validate_merged_scratchpad(root)
40
+ if not ok:
41
+ return {}, diags
42
+ merged, _paths = installer.merge_scratchpad_layers(root)
43
+ return merged, []
44
+
45
+
46
+ def eval_policy(root: str, _branch: str) -> Tuple[bool, str, List[str]]:
47
+ """
48
+ Pre-test gate: merged scratchpad validation + SYNC_POLICY_MODE + ALLOW_AUTO_PUSH
49
+ + custom_phase_list boundary (SYNC_PHASE_BOUNDARY env).
50
+ Returns (ok, reason_code, scratchpad_diagnostics).
51
+ """
52
+ merged, diags = _merge_and_validate(root)
53
+ if diags:
54
+ return False, "SCRATCHPAD_MERGE_ERROR", diags
55
+
56
+ mode = (merged.get("SYNC_POLICY_MODE") or "").strip().lower()
57
+ if not mode or mode not in _VALID_MODES:
58
+ mode = "manual"
59
+ if mode == "disabled":
60
+ return False, "SYNC_DISABLED", []
61
+ if mode == "manual":
62
+ return False, "MANUAL_MODE_NO_AUTO", []
63
+
64
+ allow = (merged.get("ALLOW_AUTO_PUSH") or "").strip()
65
+ if allow != "1":
66
+ return False, "AUTO_PUSH_NOT_ENABLED", []
67
+
68
+ if mode == "custom_phase_list":
69
+ phases = [p.strip().lower() for p in _parse_csv(merged.get("SYNC_CUSTOM_PHASES", ""))]
70
+ boundary = (os.environ.get("SYNC_PHASE_BOUNDARY") or "").strip().lower()
71
+ if not boundary:
72
+ return False, "SYNC_TRIGGER_NOT_ELIGIBLE", []
73
+ if not phases or boundary not in phases:
74
+ return False, "SYNC_TRIGGER_NOT_ELIGIBLE", []
75
+
76
+ # by_phase / by_milestone: invocation counts as eligible boundary (DEC-0058 §4).
77
+ return True, "", []
78
+
79
+
80
+ def _branch_allowed(branch: str, allowlist_csv: str) -> bool:
81
+ patterns = _parse_csv(allowlist_csv)
82
+ if not patterns:
83
+ return False
84
+ for pat in patterns:
85
+ if fnmatch.fnmatchcase(branch, pat):
86
+ return True
87
+ if branch == pat:
88
+ return True
89
+ return False
90
+
91
+
92
+ def _scan_qa_findings_blocking(repo: Path) -> bool:
93
+ """True if blocking markers found (DEC-0058 §6)."""
94
+ sprints = repo / "sprints"
95
+ if not sprints.is_dir():
96
+ return False
97
+ for p in sorted(sprints.iterdir()):
98
+ if not p.is_dir() or len(p.name) != 5 or not p.name.startswith("S"):
99
+ continue
100
+ if not p.name[1:].isdigit():
101
+ continue
102
+ qf = p / "qa-findings.md"
103
+ if not qf.is_file():
104
+ continue
105
+ text = qf.read_text(encoding="utf-8", errors="replace")
106
+ if "BLOCKING_QA_FINDINGS" in text:
107
+ return True
108
+ in_blocking = False
109
+ for line in text.splitlines():
110
+ if re.match(r"^##\s+blocking\s*$", line, re.IGNORECASE):
111
+ in_blocking = True
112
+ continue
113
+ if re.match(r"^##\s+\S", line):
114
+ in_blocking = False
115
+ if in_blocking and re.search(r"^-\s+\[\s+\]", line):
116
+ return True
117
+ if re.search(r"^-\s+\[\s+\]", line) and re.search(
118
+ r"BLOCKING|FAIL", line, re.IGNORECASE
119
+ ):
120
+ return True
121
+ return False
122
+
123
+
124
+ def _count_sprint_qa_findings(repo: Path) -> int:
125
+ n = 0
126
+ sprints = repo / "sprints"
127
+ if not sprints.is_dir():
128
+ return 0
129
+ for p in sprints.iterdir():
130
+ if not p.is_dir() or len(p.name) != 5 or not p.name.startswith("S"):
131
+ continue
132
+ if not p.name[1:].isdigit():
133
+ continue
134
+ if (p / "qa-findings.md").is_file():
135
+ n += 1
136
+ return n
137
+
138
+
139
+ def eval_post_test(root: str, branch: str) -> Tuple[bool, str, List[str]]:
140
+ """
141
+ After tests: branch allowlist + PRE_QA + BLOCKING_QA_FINDINGS scan.
142
+ """
143
+ merged, diags = _merge_and_validate(root)
144
+ if diags:
145
+ return False, "SCRATCHPAD_MERGE_ERROR", diags
146
+
147
+ allow_csv = merged.get("AUTO_PUSH_BRANCH_ALLOWLIST") or ""
148
+ if not _branch_allowed(branch, allow_csv):
149
+ return False, "BRANCH_NOT_ALLOWLISTED", []
150
+
151
+ b = branch.strip()
152
+ if b not in ("main", "master"):
153
+ if _count_sprint_qa_findings(Path(root)) == 0:
154
+ return False, "PRE_QA_AUTOPUSH_FORBIDDEN", []
155
+
156
+ if _scan_qa_findings_blocking(Path(root)):
157
+ return False, "BLOCKING_QA_FINDINGS", []
158
+
159
+ return True, "", []
160
+
161
+
162
+ def main() -> int:
163
+ parser = argparse.ArgumentParser(description="Sync/push gate evaluation for validate-and-push.")
164
+ sub = parser.add_subparsers(dest="cmd", required=True)
165
+
166
+ p_pol = sub.add_parser("policy", help="Pre-test merged scratchpad + sync policy gate.")
167
+ p_pol.add_argument("--root", required=True)
168
+ p_pol.add_argument("--branch", required=True)
169
+
170
+ p_post = sub.add_parser("post", help="Post-test allowlist + QA scan gate.")
171
+ p_post.add_argument("--root", required=True)
172
+ p_post.add_argument("--branch", required=True)
173
+
174
+ args = parser.parse_args()
175
+ root = os.path.abspath(args.root)
176
+
177
+ if args.cmd == "policy":
178
+ ok, reason, diags = eval_policy(root, args.branch)
179
+ if diags:
180
+ for d in diags:
181
+ print(d, file=sys.stderr)
182
+ print(json.dumps({"ok": False, "reason_code": "SCRATCHPAD_MERGE_ERROR"}))
183
+ return 2
184
+ print(json.dumps({"ok": ok, "reason_code": reason or None}))
185
+ return 0 if ok else 2
186
+
187
+ ok, reason, pdiags = eval_post_test(root, args.branch)
188
+ if pdiags:
189
+ for d in pdiags:
190
+ print(d, file=sys.stderr)
191
+ print(json.dumps({"ok": False, "reason_code": "SCRATCHPAD_MERGE_ERROR"}))
192
+ return 2
193
+ print(json.dumps({"ok": ok, "reason_code": reason or None}))
194
+ return 0 if ok else 2
195
+
196
+
197
+ if __name__ == "__main__":
198
+ raise SystemExit(main())
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env python3
2
+ """Compare two token-cost run JSON records for AC-2 (same run_class_hash, 50% cache read)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import os
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ _SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
13
+ if _SCRIPT_DIR not in sys.path:
14
+ sys.path.insert(0, _SCRIPT_DIR)
15
+
16
+ import token_cost_lib # noqa: E402
17
+
18
+
19
+ def main() -> int:
20
+ p = argparse.ArgumentParser(description=__doc__)
21
+ p.add_argument("baseline", type=Path, help="JSON file with run_class_hash + totals")
22
+ p.add_argument("target", type=Path, help="JSON file with run_class_hash + totals")
23
+ p.add_argument(
24
+ "--reduction-fraction",
25
+ type=float,
26
+ default=0.5,
27
+ help="Required fractional reduction in cache_read_tokens (default 0.5)",
28
+ )
29
+ args = p.parse_args()
30
+ base = json.loads(args.baseline.read_text(encoding="utf-8"))
31
+ tgt = json.loads(args.target.read_text(encoding="utf-8"))
32
+ ok, msg = token_cost_lib.compare_cache_read_reduction(
33
+ base, tgt, reduction_fraction=args.reduction_fraction
34
+ )
35
+ print(msg)
36
+ return 0 if ok else 2
37
+
38
+
39
+ if __name__ == "__main__":
40
+ sys.exit(main())