@arbiterforge/ca-pi 0.6.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 (206) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +558 -0
  3. package/package.json +35 -0
  4. package/plugins/ca-pi/CHANGELOG.md +1030 -0
  5. package/plugins/ca-pi/COMMANDS.md +90 -0
  6. package/plugins/ca-pi/ORCHESTRATOR.md +159 -0
  7. package/plugins/ca-pi/SKILLS.md +47 -0
  8. package/plugins/ca-pi/SPRINT.md +142 -0
  9. package/plugins/ca-pi/agents/INDEX.md +31 -0
  10. package/plugins/ca-pi/agents/architecture-drift-reviewer.md +86 -0
  11. package/plugins/ca-pi/agents/auth-crypto-reviewer.md +60 -0
  12. package/plugins/ca-pi/agents/backend-author.md +60 -0
  13. package/plugins/ca-pi/agents/checkpoint-aggregator.md +111 -0
  14. package/plugins/ca-pi/agents/coverage-auditor.md +71 -0
  15. package/plugins/ca-pi/agents/decision-challenger.md +116 -0
  16. package/plugins/ca-pi/agents/dependency-reviewer.md +79 -0
  17. package/plugins/ca-pi/agents/design-quality-reviewer.md +80 -0
  18. package/plugins/ca-pi/agents/finding-triage.md +86 -0
  19. package/plugins/ca-pi/agents/frontend-author.md +64 -0
  20. package/plugins/ca-pi/agents/grader.md +173 -0
  21. package/plugins/ca-pi/agents/infra-author.md +64 -0
  22. package/plugins/ca-pi/agents/map-deps.md +35 -0
  23. package/plugins/ca-pi/agents/map-structure.md +37 -0
  24. package/plugins/ca-pi/agents/migration-reviewer.md +65 -0
  25. package/plugins/ca-pi/agents/scout.md +127 -0
  26. package/plugins/ca-pi/agents/security-reviewer.md +72 -0
  27. package/plugins/ca-pi/agents/tribunal-lens-reviewer.md +65 -0
  28. package/plugins/ca-pi/extensions/codearbiter-child.js +1885 -0
  29. package/plugins/ca-pi/extensions/codearbiter.js +9802 -0
  30. package/plugins/ca-pi/generated/command-catalog.json +197 -0
  31. package/plugins/ca-pi/generated/roles.json +213 -0
  32. package/plugins/ca-pi/helpers/windows-supervisor.js +205 -0
  33. package/plugins/ca-pi/hooks/_activationlib.py +196 -0
  34. package/plugins/ca-pi/hooks/_arbiterstatelib.py +208 -0
  35. package/plugins/ca-pi/hooks/_babysitlib.py +76 -0
  36. package/plugins/ca-pi/hooks/_bashguardlib.py +1667 -0
  37. package/plugins/ca-pi/hooks/_boxlib.py +131 -0
  38. package/plugins/ca-pi/hooks/_colorlib.py +304 -0
  39. package/plugins/ca-pi/hooks/_durabilitylib.py +186 -0
  40. package/plugins/ca-pi/hooks/_entrylib.py +41 -0
  41. package/plugins/ca-pi/hooks/_fmtlib.py +161 -0
  42. package/plugins/ca-pi/hooks/_gitexec.py +45 -0
  43. package/plugins/ca-pi/hooks/_githooks.py +920 -0
  44. package/plugins/ca-pi/hooks/_gitlib.py +110 -0
  45. package/plugins/ca-pi/hooks/_hooklib.py +595 -0
  46. package/plugins/ca-pi/hooks/_host.py +115 -0
  47. package/plugins/ca-pi/hooks/_intentlib.py +242 -0
  48. package/plugins/ca-pi/hooks/_ledgerlib.py +1035 -0
  49. package/plugins/ca-pi/hooks/_metricslib.py +709 -0
  50. package/plugins/ca-pi/hooks/_pathnorm.py +74 -0
  51. package/plugins/ca-pi/hooks/_planfilelib.py +664 -0
  52. package/plugins/ca-pi/hooks/_previewlib.py +193 -0
  53. package/plugins/ca-pi/hooks/_protectedlib.py +312 -0
  54. package/plugins/ca-pi/hooks/_protectedstatelib.py +411 -0
  55. package/plugins/ca-pi/hooks/_provenancelib.py +971 -0
  56. package/plugins/ca-pi/hooks/_prunelib.py +1398 -0
  57. package/plugins/ca-pi/hooks/_prunepolicy.py +235 -0
  58. package/plugins/ca-pi/hooks/_readinjectlib.py +1080 -0
  59. package/plugins/ca-pi/hooks/_releaselib.py +2657 -0
  60. package/plugins/ca-pi/hooks/_scopelib.py +262 -0
  61. package/plugins/ca-pi/hooks/_segmentslib.py +278 -0
  62. package/plugins/ca-pi/hooks/_sensitivelib.py +270 -0
  63. package/plugins/ca-pi/hooks/_sessionlib.py +78 -0
  64. package/plugins/ca-pi/hooks/_sloplib.py +244 -0
  65. package/plugins/ca-pi/hooks/_standuplib.py +214 -0
  66. package/plugins/ca-pi/hooks/_subagentslib.py +219 -0
  67. package/plugins/ca-pi/hooks/_taskboardlib.py +1088 -0
  68. package/plugins/ca-pi/hooks/_updatelib.py +278 -0
  69. package/plugins/ca-pi/hooks/babysit.py +47 -0
  70. package/plugins/ca-pi/hooks/boardsync.py +129 -0
  71. package/plugins/ca-pi/hooks/doctor.py +420 -0
  72. package/plugins/ca-pi/hooks/git-enforce.py +325 -0
  73. package/plugins/ca-pi/hooks/hostapi.py +460 -0
  74. package/plugins/ca-pi/hooks/init-codearbiter.py +225 -0
  75. package/plugins/ca-pi/hooks/metrics.py +62 -0
  76. package/plugins/ca-pi/hooks/migration-pass.py +129 -0
  77. package/plugins/ca-pi/hooks/pi-bridge.py +543 -0
  78. package/plugins/ca-pi/hooks/post-write-edit.py +231 -0
  79. package/plugins/ca-pi/hooks/pre-bash.py +90 -0
  80. package/plugins/ca-pi/hooks/pre-edit.py +284 -0
  81. package/plugins/ca-pi/hooks/pre-read.py +81 -0
  82. package/plugins/ca-pi/hooks/pre-write.py +217 -0
  83. package/plugins/ca-pi/hooks/preview.py +69 -0
  84. package/plugins/ca-pi/hooks/prune-transcript.py +232 -0
  85. package/plugins/ca-pi/hooks/releasehash.py +216 -0
  86. package/plugins/ca-pi/hooks/security-pass.py +139 -0
  87. package/plugins/ca-pi/hooks/session-start.py +1218 -0
  88. package/plugins/ca-pi/hooks/statusline.py +736 -0
  89. package/plugins/ca-pi/hooks/taskwrite.py +351 -0
  90. package/plugins/ca-pi/hooks/update-refresh.py +51 -0
  91. package/plugins/ca-pi/hooks/wire-statusline.py +435 -0
  92. package/plugins/ca-pi/includes/anti-slop-design/INDEX.md +55 -0
  93. package/plugins/ca-pi/includes/anti-slop-design/color.md +43 -0
  94. package/plugins/ca-pi/includes/anti-slop-design/core.md +244 -0
  95. package/plugins/ca-pi/includes/anti-slop-design/images.md +32 -0
  96. package/plugins/ca-pi/includes/anti-slop-design/layout.md +45 -0
  97. package/plugins/ca-pi/includes/anti-slop-design/medium-cli.md +39 -0
  98. package/plugins/ca-pi/includes/anti-slop-design/medium-dataviz.md +43 -0
  99. package/plugins/ca-pi/includes/anti-slop-design/medium-diagram.md +35 -0
  100. package/plugins/ca-pi/includes/anti-slop-design/medium-documents.md +70 -0
  101. package/plugins/ca-pi/includes/anti-slop-design/medium-slides.md +30 -0
  102. package/plugins/ca-pi/includes/anti-slop-design/medium-web.md +39 -0
  103. package/plugins/ca-pi/includes/anti-slop-design/typography.md +51 -0
  104. package/plugins/ca-pi/includes/author-tdd-workflow.md +14 -0
  105. package/plugins/ca-pi/includes/compaction-charter.md +16 -0
  106. package/plugins/ca-pi/includes/cut-docs.md +16 -0
  107. package/plugins/ca-pi/includes/dev-mode.md +30 -0
  108. package/plugins/ca-pi/includes/farm.md +237 -0
  109. package/plugins/ca-pi/includes/fresh-verification.md +14 -0
  110. package/plugins/ca-pi/includes/harvest.md +69 -0
  111. package/plugins/ca-pi/includes/maturity-coverage.md +102 -0
  112. package/plugins/ca-pi/includes/pi-host-notes.md +69 -0
  113. package/plugins/ca-pi/includes/redirect.md +69 -0
  114. package/plugins/ca-pi/includes/reference-map.md +22 -0
  115. package/plugins/ca-pi/includes/review-matrix.md +14 -0
  116. package/plugins/ca-pi/includes/reviewer-contract.md +53 -0
  117. package/plugins/ca-pi/includes/routing-table.md +47 -0
  118. package/plugins/ca-pi/includes/security-gate-record.md +22 -0
  119. package/plugins/ca-pi/includes/smarts/core.md +90 -0
  120. package/plugins/ca-pi/includes/smarts/decision-log-format.md +56 -0
  121. package/plugins/ca-pi/routines/INDEX.md +32 -0
  122. package/plugins/ca-pi/routines/brainstorming/SKILL.md +122 -0
  123. package/plugins/ca-pi/routines/commit-gate/SKILL.md +151 -0
  124. package/plugins/ca-pi/routines/context-check/SKILL.md +85 -0
  125. package/plugins/ca-pi/routines/context-creation/SKILL.md +171 -0
  126. package/plugins/ca-pi/routines/crypto-compliance/SKILL.md +41 -0
  127. package/plugins/ca-pi/routines/debug/SKILL.md +99 -0
  128. package/plugins/ca-pi/routines/decision-lifecycle/SKILL.md +104 -0
  129. package/plugins/ca-pi/routines/decision-lifecycle/references/adr-template.md +74 -0
  130. package/plugins/ca-pi/routines/decision-variance/SKILL.md +147 -0
  131. package/plugins/ca-pi/routines/decompose/SKILL.md +168 -0
  132. package/plugins/ca-pi/routines/dispatching-parallel-agents/SKILL.md +76 -0
  133. package/plugins/ca-pi/routines/executing-plans/SKILL.md +83 -0
  134. package/plugins/ca-pi/routines/finishing-a-development-branch/SKILL.md +91 -0
  135. package/plugins/ca-pi/routines/post-merge-cleanup/SKILL.md +233 -0
  136. package/plugins/ca-pi/routines/refactor/SKILL.md +91 -0
  137. package/plugins/ca-pi/routines/release/SKILL.md +315 -0
  138. package/plugins/ca-pi/routines/secret-handling/SKILL.md +67 -0
  139. package/plugins/ca-pi/routines/security-architecture/SKILL.md +63 -0
  140. package/plugins/ca-pi/routines/skill-author/SKILL.md +108 -0
  141. package/plugins/ca-pi/routines/skill-author/references/skill-template.md +58 -0
  142. package/plugins/ca-pi/routines/subagent-driven-development/SKILL.md +149 -0
  143. package/plugins/ca-pi/routines/subagent-driven-development/references/farm-dispatch.md +145 -0
  144. package/plugins/ca-pi/routines/tdd/SKILL.md +139 -0
  145. package/plugins/ca-pi/routines/tribunal/SKILL.md +109 -0
  146. package/plugins/ca-pi/routines/tribunal/references/ai-markers.md +29 -0
  147. package/plugins/ca-pi/routines/tribunal/references/cost-and-models.md +64 -0
  148. package/plugins/ca-pi/routines/tribunal/references/finding-record.md +27 -0
  149. package/plugins/ca-pi/routines/tribunal/references/issue-filing.md +47 -0
  150. package/plugins/ca-pi/routines/tribunal/references/lenses/appsec.md +22 -0
  151. package/plugins/ca-pi/routines/tribunal/references/lenses/architecture.md +23 -0
  152. package/plugins/ca-pi/routines/tribunal/references/lenses/coverage.md +20 -0
  153. package/plugins/ca-pi/routines/tribunal/references/lenses/infra.md +24 -0
  154. package/plugins/ca-pi/routines/tribunal/references/lenses/migration.md +22 -0
  155. package/plugins/ca-pi/routines/tribunal/references/lenses/observability.md +21 -0
  156. package/plugins/ca-pi/routines/tribunal/references/lenses/performance.md +22 -0
  157. package/plugins/ca-pi/routines/tribunal/references/lenses/reliability.md +23 -0
  158. package/plugins/ca-pi/routines/tribunal/references/lenses/secrets-supply.md +22 -0
  159. package/plugins/ca-pi/routines/tribunal/references/lenses/test-fidelity.md +24 -0
  160. package/plugins/ca-pi/routines/tribunal/references/lenses/typesafety.md +21 -0
  161. package/plugins/ca-pi/routines/tribunal/references/report.md +19 -0
  162. package/plugins/ca-pi/routines/tribunal/references/schemas.md +58 -0
  163. package/plugins/ca-pi/routines/tribunal/references/telemetry.md +28 -0
  164. package/plugins/ca-pi/routines/tribunal/references/triage.md +53 -0
  165. package/plugins/ca-pi/routines/using-git-worktrees/SKILL.md +85 -0
  166. package/plugins/ca-pi/routines/writing-plans/SKILL.md +129 -0
  167. package/plugins/ca-pi/routines/writing-plans/references/farm-plan.md +50 -0
  168. package/plugins/ca-pi/skills/ca-add-dep/SKILL.md +88 -0
  169. package/plugins/ca-pi/skills/ca-adr/SKILL.md +30 -0
  170. package/plugins/ca-pi/skills/ca-adr-status/SKILL.md +30 -0
  171. package/plugins/ca-pi/skills/ca-arbiter/SKILL.md +36 -0
  172. package/plugins/ca-pi/skills/ca-audit/SKILL.md +51 -0
  173. package/plugins/ca-pi/skills/ca-btw/SKILL.md +23 -0
  174. package/plugins/ca-pi/skills/ca-checkpoint/SKILL.md +50 -0
  175. package/plugins/ca-pi/skills/ca-chore/SKILL.md +58 -0
  176. package/plugins/ca-pi/skills/ca-cleanup/SKILL.md +55 -0
  177. package/plugins/ca-pi/skills/ca-commands/SKILL.md +21 -0
  178. package/plugins/ca-pi/skills/ca-commit/SKILL.md +27 -0
  179. package/plugins/ca-pi/skills/ca-conflict/SKILL.md +61 -0
  180. package/plugins/ca-pi/skills/ca-context-check/SKILL.md +32 -0
  181. package/plugins/ca-pi/skills/ca-create-context/SKILL.md +32 -0
  182. package/plugins/ca-pi/skills/ca-debug/SKILL.md +42 -0
  183. package/plugins/ca-pi/skills/ca-decompose/SKILL.md +30 -0
  184. package/plugins/ca-pi/skills/ca-dev/SKILL.md +42 -0
  185. package/plugins/ca-pi/skills/ca-doctor/SKILL.md +44 -0
  186. package/plugins/ca-pi/skills/ca-feature/SKILL.md +105 -0
  187. package/plugins/ca-pi/skills/ca-fix/SKILL.md +42 -0
  188. package/plugins/ca-pi/skills/ca-init/SKILL.md +56 -0
  189. package/plugins/ca-pi/skills/ca-metrics/SKILL.md +80 -0
  190. package/plugins/ca-pi/skills/ca-new-skill/SKILL.md +34 -0
  191. package/plugins/ca-pi/skills/ca-override/SKILL.md +72 -0
  192. package/plugins/ca-pi/skills/ca-pr/SKILL.md +61 -0
  193. package/plugins/ca-pi/skills/ca-preview/SKILL.md +86 -0
  194. package/plugins/ca-pi/skills/ca-prune/SKILL.md +100 -0
  195. package/plugins/ca-pi/skills/ca-reconcile/SKILL.md +43 -0
  196. package/plugins/ca-pi/skills/ca-refactor/SKILL.md +43 -0
  197. package/plugins/ca-pi/skills/ca-release/SKILL.md +57 -0
  198. package/plugins/ca-pi/skills/ca-review/SKILL.md +63 -0
  199. package/plugins/ca-pi/skills/ca-spike/SKILL.md +41 -0
  200. package/plugins/ca-pi/skills/ca-sprint/SKILL.md +44 -0
  201. package/plugins/ca-pi/skills/ca-standup/SKILL.md +112 -0
  202. package/plugins/ca-pi/skills/ca-status/SKILL.md +56 -0
  203. package/plugins/ca-pi/skills/ca-task/SKILL.md +61 -0
  204. package/plugins/ca-pi/skills/ca-threat-model/SKILL.md +45 -0
  205. package/plugins/ca-pi/skills/ca-tribunal/SKILL.md +44 -0
  206. package/plugins/ca-pi/skills/ca-watch/SKILL.md +78 -0
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env python3
2
+ # codeArbiter — pre-tag content-hash confirmation (A-2.10).
3
+ #
4
+ # A row's `pre-tag` commands are operator-authored shell that `/ca:release`
5
+ # EXECUTES before composing a tag. `.codearbiter/release-targets.md` is
6
+ # marker-gated under H-22, so changing those commands already costs a fresh
7
+ # authoring marker — but that gate is about WRITING the file. Nothing made the
8
+ # release lane notice that the commands it is about to run are not the ones
9
+ # anybody last looked at.
10
+ #
11
+ # This closes that: the lane records a hash of a row's pre-tag content when an
12
+ # operator confirms it, and refuses to run silently once that hash changes. A
13
+ # changed hash is not a block — it is a re-confirmation prompt. Per ADR-0010
14
+ # this is COOPERATIVE-grade: an agent that skips the check is not stopped by
15
+ # the operating system, only by the lane's own discipline. Stated plainly here
16
+ # rather than implied, because a reader who mistakes it for enforcement would
17
+ # over-trust it.
18
+ #
19
+ # WHY A PYTHON PRODUCER, not a shell redirect. H-19 (`pre-write.py`) blocks
20
+ # Write-tool writes to `.codearbiter/.markers/*` outright, and
21
+ # `_bashguardlib`'s companion rule blocks shell redirects and interpreter
22
+ # invocations that name a gate marker. Both exist because a hand-forged marker
23
+ # is a forged gate pass. The confirmation record therefore needs a sanctioned
24
+ # producer, which is this module.
25
+ #
26
+ # Ships to every governance plugin byte-identically via tools/sync-core.py, so
27
+ # it carries NO fact about any particular repository: the target name, the
28
+ # commands, and the project root all arrive as arguments or from the declared
29
+ # file.
30
+ #
31
+ # Public API:
32
+ # pre_tag_digest(commands) -> str
33
+ # confirmation_path(root, target) -> str
34
+ # read_confirmation(root, target) -> str | None
35
+ # record_confirmation(root, target, digest) -> str
36
+ # confirmation_state(root, target, commands) -> str
37
+ # main(argv) -> int
38
+ #
39
+ # CLI:
40
+ # releasehash.py digest <target> print the current pre-tag digest
41
+ # releasehash.py check <target> exit 0 confirmed, 1 changed, 2 never
42
+ # confirmed, 3/4 declared-file states
43
+ # releasehash.py record <target> mint the confirmation (the sanctioned
44
+ # producer; run only after an operator has
45
+ # actually READ the commands)
46
+
47
+ from __future__ import annotations
48
+
49
+ import hashlib
50
+ import os
51
+ import sys
52
+
53
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
54
+ import _releaselib # noqa: E402 — sibling module, flat hooks/ layout
55
+
56
+ DIGEST_VERSION = "1"
57
+ MARKER_DIRNAME = ".markers"
58
+ CONFIRMED_PREFIX = "pre-tag-confirmed-"
59
+
60
+ CONFIRMED = "confirmed"
61
+ CHANGED = "changed"
62
+ NEVER = "never-confirmed"
63
+ NO_COMMANDS = "no-commands"
64
+
65
+
66
+ def pre_tag_digest(commands):
67
+ """A stable digest of a row's `pre-tag` command list.
68
+
69
+ ORDER-SENSITIVE on purpose. The commands run in declared order and a
70
+ reordering can change what a release checks — a check that ran after a
71
+ rebuild is not the same check as one that ran before it — so swapping
72
+ two entries must invalidate the confirmation, not preserve it.
73
+
74
+ Each command is length-prefixed before joining, so `["ab", "c"]` and
75
+ `["a", "bc"]` cannot collide into one digest. Versioned, so a future
76
+ change to this construction cannot be mistaken for a content change
77
+ by an old recorded value.
78
+
79
+ Pure and non-raising: a non-list, or a list with non-string members,
80
+ degrades to the digest of an empty list rather than raising inside a
81
+ release lane.
82
+ """
83
+ if not isinstance(commands, (list, tuple)):
84
+ commands = []
85
+ parts = [DIGEST_VERSION]
86
+ for command in commands:
87
+ if not isinstance(command, str):
88
+ continue
89
+ parts.append(f"{len(command)}:{command}")
90
+ joined = "\n".join(parts)
91
+ return hashlib.sha256(joined.encode("utf-8")).hexdigest()
92
+
93
+
94
+ def confirmation_path(root, target):
95
+ """Where `target`'s confirmation is recorded.
96
+
97
+ One marker PER TARGET, never a shared one: a multi-target repository
98
+ confirming `ca`'s commands must not thereby confirm `ca-pi`'s.
99
+ """
100
+ # The sanitized stem alone is NOT injective: every disallowed character
101
+ # folds to "_", so `a.b` and `a_b` -- two legal, distinct target names --
102
+ # would share one marker, and confirming either would silently confirm
103
+ # the other. That is precisely the cross-target confirmation this
104
+ # function promises not to do. A short digest of the RAW name restores
105
+ # the one-marker-per-target property while keeping the stem readable for
106
+ # whoever is looking in `.markers/` by eye.
107
+ raw = str(target)
108
+ safe = "".join(ch if (ch.isalnum() or ch in "-_") else "_" for ch in raw)
109
+ digest = hashlib.sha256(raw.encode("utf-8")).hexdigest()[:8]
110
+ return os.path.join(root, ".codearbiter", MARKER_DIRNAME,
111
+ CONFIRMED_PREFIX + f"{safe}-{digest}")
112
+
113
+
114
+ def read_confirmation(root, target):
115
+ """The recorded digest for `target`, or None when never confirmed.
116
+
117
+ Never raises: an unreadable or empty marker reads as "never confirmed",
118
+ which is the conservative answer — it prompts rather than admits.
119
+ """
120
+ try:
121
+ with open(confirmation_path(root, target), encoding="utf-8") as handle:
122
+ value = handle.read().strip()
123
+ except (OSError, UnicodeError):
124
+ return None
125
+ return value or None
126
+
127
+
128
+ def record_confirmation(root, target, digest):
129
+ """Mint the confirmation marker. Returns the path written.
130
+
131
+ THE SANCTIONED PRODUCER. Call this only after an operator has actually
132
+ read the commands — recording a digest nobody looked at converts this
133
+ from a confirmation into a rubber stamp, which is worse than not
134
+ having it, because the lane then reports "confirmed" about content no
135
+ human has seen.
136
+ """
137
+ path = confirmation_path(root, target)
138
+ os.makedirs(os.path.dirname(path), exist_ok=True)
139
+ with open(path, "w", encoding="utf-8", newline="\n") as handle:
140
+ handle.write(str(digest).strip() + "\n")
141
+ return path
142
+
143
+
144
+ def confirmation_state(root, target, commands):
145
+ """One of `confirmed` / `changed` / `never-confirmed` / `no-commands`.
146
+
147
+ `no-commands` is deliberately distinct from `confirmed`: a row that
148
+ declares no pre-tag commands has nothing to confirm, and reporting it
149
+ as confirmed would claim an operator approved something that does not
150
+ exist. The lane treats both as "proceed", but only one of them is a
151
+ statement about human review.
152
+ """
153
+ if not commands:
154
+ return NO_COMMANDS
155
+ recorded = read_confirmation(root, target)
156
+ if recorded is None:
157
+ return NEVER
158
+ return CONFIRMED if recorded == pre_tag_digest(commands) else CHANGED
159
+
160
+
161
+ def _row_commands(target, targets_path=None):
162
+ path = targets_path or _releaselib.default_targets_path()
163
+ rows = _releaselib.load_targets(path)
164
+ row = next((r for r in rows if r["target"] == target), None)
165
+ if row is None:
166
+ return None
167
+ return row.get("pre_tag") or []
168
+
169
+
170
+ def main(argv=None):
171
+ """CLI. See the module header for subcommands and exit codes."""
172
+ argv = list(sys.argv[1:] if argv is None else argv)
173
+ if len(argv) != 2 or argv[0] not in ("digest", "check", "record"):
174
+ sys.stderr.write("usage: releasehash.py {digest|check|record} <target>\n")
175
+ return 2
176
+ command, target = argv
177
+ root = os.path.dirname(os.path.dirname(_releaselib.default_targets_path()))
178
+
179
+ try:
180
+ commands = _row_commands(target)
181
+ except _releaselib.ReleaseTargetsError as error:
182
+ sys.stderr.write(f"{type(error).__name__}: {error}\n")
183
+ return _releaselib._targets_error_exit_code(error)
184
+ if commands is None:
185
+ sys.stderr.write(f"unknown release target: {target}\n")
186
+ return 2
187
+
188
+ if command == "digest":
189
+ print(pre_tag_digest(commands))
190
+ return 0
191
+
192
+ if command == "record":
193
+ path = record_confirmation(root, target, pre_tag_digest(commands))
194
+ print(f"recorded pre-tag confirmation for {target}: {path}")
195
+ return 0
196
+
197
+ state = confirmation_state(root, target, commands)
198
+ print(state)
199
+ if state in (CONFIRMED, NO_COMMANDS):
200
+ return 0
201
+ if state == CHANGED:
202
+ sys.stderr.write(
203
+ f"releasehash: {target}'s pre-tag commands have CHANGED since they "
204
+ "were last confirmed. Read them, and re-confirm with "
205
+ f"`releasehash.py record {target}` before releasing. These commands "
206
+ "are executed by the release lane, so a change nobody has read is "
207
+ "the case this check exists for.\n")
208
+ return 1
209
+ sys.stderr.write(
210
+ f"releasehash: {target}'s pre-tag commands have never been confirmed. "
211
+ f"Read them, then run `releasehash.py record {target}`.\n")
212
+ return 2
213
+
214
+
215
+ if __name__ == "__main__":
216
+ sys.exit(main())
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env python3
2
+ # codeArbiter v2 — record a security-gate pass BOUND to the lines it approved.
3
+ #
4
+ # The crypto-compliance / secret-handling skills (and /ca:override's heavier
5
+ # security ceiling) run this ON PASS, replacing the old `touch` of an empty
6
+ # marker. An empty marker only proved that *some* gate passed *recently*; any
7
+ # different crypto/secret change committed inside the 30-minute freshness
8
+ # window rode through on it (a TOCTOU hole). This helper hashes every added
9
+ # line in the working tree (vs HEAD, plus untracked files) that matches the
10
+ # shared CRYPTO_RE/SECRET_RE and writes the digests to
11
+ # .codearbiter/.markers/security-gate-passed; pre-bash.py H-09b/H-10b then
12
+ # admits a commit only when every sensitive line being committed is in the
13
+ # recorded set.
14
+ #
15
+ # Invoked by skill prose as (interpreter resolved once by presence, never a
16
+ # `python3 X || python X` fold -- #577; recording itself stays idempotent):
17
+ # "$PY" "<plugin>/hooks/security-pass.py"
18
+
19
+ import os
20
+ import subprocess
21
+
22
+ from _gitexec import git_executable
23
+ import sys
24
+
25
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
26
+ import hostapi # noqa: E402 — host seam (ADR-0011)
27
+ import _entrylib # noqa: E402 — shared run() dispatch (jscpd dedup)
28
+ from _hooklib import ( # noqa: E402
29
+ CRYPTO_RE, SECRET_RE, SECURITY_DIFF_GIT_ARGS, is_sensitive_scan_exempt,
30
+ line_digest, marker_root, project_root, sensitive_scan_added_lines,
31
+ set_host, utf8_stdio, warn, write_text_atomic,
32
+ )
33
+
34
+ MAX_UNTRACKED_BYTES = 1_000_000 # an untracked blob bigger than this is not reviewable prose
35
+
36
+
37
+ def run_git(args, cwd):
38
+ return subprocess.run(
39
+ [git_executable()] + args, cwd=cwd, capture_output=True, text=True,
40
+ encoding="utf-8", errors="replace", timeout=30,
41
+ )
42
+
43
+
44
+ def file_lines(root, rel):
45
+ p = os.path.join(root, rel)
46
+ try:
47
+ if os.path.getsize(p) > MAX_UNTRACKED_BYTES:
48
+ return []
49
+ with open(p, encoding="utf-8", errors="replace") as f:
50
+ return f.read().splitlines()
51
+ except Exception: # noqa: BLE001
52
+ return []
53
+
54
+
55
+ def candidate_lines(root):
56
+ """Every line the next commit could introduce: added lines of the
57
+ worktree-vs-HEAD diff (staged and unstaged alike), plus the full content
58
+ of untracked files — `git diff HEAD` never shows those, but they land in
59
+ the staged diff the moment commit-gate stages them.
60
+
61
+ Excludes gate-events.log (#279, is_sensitive_scan_exempt): the crypto/
62
+ secret gate's own machine-written audit sink, which structurally echoes
63
+ the detector's message text back at itself. The diff branch drops those
64
+ lines via the shared path-aware `sensitive_scan_added_lines`; the
65
+ unborn-branch and untracked-file branches skip the file outright since
66
+ they read whole-file content rather than a diff.
67
+
68
+ The diff is read via SECURITY_DIFF_GIT_ARGS (not a bare `["diff", ...]`):
69
+ it pins the `a/`/`b/` prefix format `sensitive_scan_added_lines` depends
70
+ on for path attribution, regardless of the caller's `diff.mnemonicPrefix`
71
+ / `diff.noprefix` / external-diff config (#279 review MEDIUM-1)."""
72
+ lines = []
73
+ diff = run_git([*SECURITY_DIFF_GIT_ARGS, "HEAD"], root)
74
+ if diff.returncode == 0:
75
+ lines += sensitive_scan_added_lines(diff.stdout)
76
+ else:
77
+ # Unborn branch (no HEAD yet): every tracked file is new content.
78
+ ls = run_git(["ls-files"], root)
79
+ for rel in ls.stdout.splitlines():
80
+ if is_sensitive_scan_exempt(rel):
81
+ continue
82
+ lines += file_lines(root, rel)
83
+ untracked = run_git(["ls-files", "--others", "--exclude-standard"], root)
84
+ for rel in untracked.stdout.splitlines():
85
+ if is_sensitive_scan_exempt(rel):
86
+ continue
87
+ lines += file_lines(root, rel)
88
+ return lines
89
+
90
+
91
+ def main():
92
+ utf8_stdio()
93
+ root = project_root()
94
+ if not os.path.isdir(os.path.join(root, ".codearbiter")):
95
+ warn("no .codearbiter/ here — security-pass.py records nothing outside "
96
+ "an initialized repo")
97
+ sys.exit(1)
98
+ sensitive = [ln for ln in candidate_lines(root)
99
+ if CRYPTO_RE.search(ln) or SECRET_RE.search(ln)]
100
+ # #604: the MARKER root is deliberately NOT `root` above. `root` (plain
101
+ # project_root()) must stay wherever this process is actually running —
102
+ # candidate_lines(root) just scanned exactly that tree's diff, and binding
103
+ # digests to a DIFFERENT tree would review lines nobody staged. But in a
104
+ # linked git worktree, `root` names the worktree's own (gitignored,
105
+ # never-checked-out) `.codearbiter/.markers/`, while the H-09b/H-10b
106
+ # guard reads the marker from the MAIN checkout (D-2) — marker_root()
107
+ # gives the write the same main-checkout answer the guard's read already
108
+ # has, without moving the scan.
109
+ write_root = marker_root()
110
+ marker_dir = os.path.join(write_root, ".codearbiter", ".markers")
111
+ os.makedirs(marker_dir, exist_ok=True)
112
+ marker = os.path.join(marker_dir, "security-gate-passed")
113
+ digests = sorted({line_digest(ln) for ln in sensitive})
114
+ # Atomic write (migration-002): a crash mid-write never leaves a half-written
115
+ # marker, which the backstop would read as an unrecognized digest and force a
116
+ # spurious gate re-run.
117
+ write_text_atomic(marker, "\n".join(digests) + ("\n" if digests else ""))
118
+ print(f"security-gate pass recorded: {len(digests)} sensitive line(s) "
119
+ f"bound to {os.path.relpath(marker, write_root)}")
120
+
121
+
122
+ def run(host, argv=None):
123
+ """Host-seam entry point (ADR-0011): the __main__ guard calls this with the
124
+ plugin's loaded Host. Wraps main() unchanged — main() still communicates
125
+ via sys.exit/stdout/stderr, and its return value stays discarded exactly
126
+ as the old bare `main()` guard discarded it (so the process still exits 0
127
+ on a normal fall-through).
128
+
129
+ Wires `host` live (#257): primes `_hooklib`'s process-cached Host via
130
+ `set_host()` BEFORE main() runs, so any `get_host()` call downstream
131
+ resolves to the SAME instance the caller passed here — no second
132
+ `hostapi.load_host()`, and `run(fake_host)` genuinely exercises
133
+ `fake_host`."""
134
+ return _entrylib.dispatch(host, argv, main, set_host,
135
+ pass_argv=False, propagate_result=False)
136
+
137
+
138
+ if __name__ == "__main__":
139
+ sys.exit(run(hostapi.load_host()) or 0)