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,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())
@@ -0,0 +1,108 @@
1
+ """Token-cost run class, metrics, and AC-2 comparability helpers (US-0080 / DEC-0062)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ from typing import Any, Mapping
8
+
9
+ # Canonical metric keys (DEC-0062 §1) — totals / per-phase rows
10
+ METRIC_KEYS = (
11
+ "cache_read_tokens",
12
+ "input_tokens",
13
+ "output_tokens",
14
+ "phase_call_count",
15
+ "cache_creation_tokens",
16
+ "orchestrator_call_estimate",
17
+ )
18
+
19
+ REQUIRED_TOTAL_KEYS = (
20
+ "cache_read_tokens",
21
+ "input_tokens",
22
+ "output_tokens",
23
+ )
24
+
25
+
26
+ def canonical_json_dumps(obj: Mapping[str, Any]) -> str:
27
+ return json.dumps(obj, sort_keys=True, separators=(",", ":"))
28
+
29
+
30
+ def compute_run_class_hash(run_class: Mapping[str, Any]) -> str:
31
+ """SHA-256 hex (lowercase) of canonical JSON for DEC-0062 §2 tuple."""
32
+ payload = canonical_json_dumps(run_class)
33
+ return hashlib.sha256(payload.encode("utf-8")).hexdigest()
34
+
35
+
36
+ def compute_strict_proof_hash(
37
+ orchestrator_run_id: str,
38
+ runtime_proof_id: str,
39
+ phase_id: str,
40
+ role: str,
41
+ proof_issued_at: str,
42
+ proof_ttl_seconds: int,
43
+ ) -> str:
44
+ """DEC-0038-style sorted-key JSON SHA-256 over the strict-proof tuple fields."""
45
+ obj = {
46
+ "orchestrator_run_id": orchestrator_run_id,
47
+ "runtime_proof_id": runtime_proof_id,
48
+ "phase_id": phase_id,
49
+ "role": role,
50
+ "proof_issued_at": proof_issued_at,
51
+ "proof_ttl_seconds": int(proof_ttl_seconds),
52
+ }
53
+ return hashlib.sha256(canonical_json_dumps(obj).encode("utf-8")).hexdigest()
54
+
55
+
56
+ def validate_run_totals(row: Mapping[str, Any]) -> list[str]:
57
+ """Return diagnostic strings for invalid totals; empty if OK."""
58
+ errs: list[str] = []
59
+ for k in REQUIRED_TOTAL_KEYS:
60
+ if k not in row:
61
+ errs.append(f"missing_metric:{k}")
62
+ continue
63
+ v = row[k]
64
+ if not isinstance(v, int) or v < 0:
65
+ errs.append(f"invalid_non_negative_int:{k}")
66
+ if "metric_source" not in row or not str(row.get("metric_source", "")).strip():
67
+ errs.append("missing_metric_source")
68
+ return errs
69
+
70
+
71
+ def compare_cache_read_reduction(
72
+ baseline: Mapping[str, Any],
73
+ target: Mapping[str, Any],
74
+ *,
75
+ reduction_fraction: float = 0.5,
76
+ ) -> tuple[bool, str]:
77
+ """
78
+ AC-2: target must achieve >= reduction_fraction lower cache_read_tokens
79
+ vs baseline when run_class_hash matches. Otherwise TOKEN_COST_RUN_CLASS_MISMATCH.
80
+ """
81
+ hb = baseline.get("run_class_hash")
82
+ ht = target.get("run_class_hash")
83
+ if not isinstance(hb, str) or not isinstance(ht, str) or hb != ht:
84
+ return False, "TOKEN_COST_RUN_CLASS_MISMATCH: run_class_hash differs or missing"
85
+
86
+ tb = baseline.get("totals") or {}
87
+ tt = target.get("totals") or {}
88
+ if not isinstance(tb, Mapping) or not isinstance(tt, Mapping):
89
+ return False, "TOKEN_COST_RUN_CLASS_MISMATCH: totals object missing"
90
+
91
+ br = tb.get("cache_read_tokens")
92
+ tr = tt.get("cache_read_tokens")
93
+ if not isinstance(br, int) or not isinstance(tr, int) or br < 0 or tr < 0:
94
+ return False, "TOKEN_COST_RUN_CLASS_MISMATCH: invalid cache_read_tokens"
95
+
96
+ if br == 0:
97
+ if tr == 0:
98
+ return True, "ok: baseline and target zero cache_read_tokens"
99
+ return False, "TOKEN_COST_RUN_CLASS_MISMATCH: baseline zero but target non-zero"
100
+
101
+ max_target = int(br * (1.0 - reduction_fraction))
102
+ if tr > max_target:
103
+ return (
104
+ False,
105
+ f"TOKEN_COST_RUN_CLASS_MISMATCH: cache_read_tokens {tr} exceeds "
106
+ f"50% reduction threshold (baseline {br}, max_target {max_target})",
107
+ )
108
+ return True, "ok: cache_read_tokens meets 50% reduction vs baseline"