@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,325 @@
1
+ #!/usr/bin/env python3
2
+ # codeArbiter — git-level enforcement backstop (#161).
3
+ #
4
+ # Installed into .git/hooks/pre-commit and .git/hooks/pre-push by _githooks.py.
5
+ # It runs at the GIT OPERATION itself, OUTSIDE Claude Code — so a commit built
6
+ # through shell indirection (`g=git; c=commit; $g $c -m x`) still triggers it,
7
+ # unlike the PreToolUse Bash hook (pre-bash.py), which only ever sees the literal
8
+ # command string and is defeated by variable/expansion spellings. The two layers
9
+ # are complementary: pre-bash.py gives fast, well-worded feedback on the common
10
+ # spellings; this is the spelling-proof backstop underneath.
11
+ #
12
+ # It mirrors pre-bash.py's commit/push gates and reuses the SAME detection
13
+ # primitives from _hooklib (CRYPTO_RE / SECRET_RE / line_digest / content_digest
14
+ # / is_migration_path / marker_fresh / sensitive_scan_added_lines), so the two
15
+ # enforcement points can never drift on what counts as sensitive or as a
16
+ # migration, on how a gate-pass marker binds to the lines it approved, or on
17
+ # which path (gate-events.log, #279) is exempt from the sensitive-line scan.
18
+ #
19
+ # Contract: a git hook aborts its operation on ANY non-zero exit, so a BLOCK is
20
+ # exit 1 with a loud stderr message. Exit 0 allows. The security/migration scans
21
+ # fail CLOSED on a git read error (pre-bash's "ambiguity resolves CLOSED"
22
+ # stance); a repo that is not arbiter-enabled is a no-op (exit 0).
23
+
24
+ import os
25
+ import subprocess
26
+ import sys
27
+
28
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
29
+ import hostapi # noqa: E402 — host seam (ADR-0011)
30
+ import _entrylib # noqa: E402 — shared run() dispatch (jscpd dedup)
31
+ from _gitexec import git_executable # noqa: E402
32
+ from _hooklib import ( # noqa: E402
33
+ CRYPTO_RE, MARKER_FRESHNESS_MINUTES, SECRET_RE, SECURITY_DIFF_GIT_ARGS,
34
+ arbiter_active, content_digest, is_migration_path, line_digest,
35
+ marker_fresh, sensitive_scan_added_lines, set_host, utf8_stdio,
36
+ )
37
+
38
+
39
+ def repo_root():
40
+ """The repo THIS git hook is firing in — deliberately does NOT use
41
+ _hooklib.project_root() (reliability-005, #190).
42
+
43
+ git-enforce.py is installed as `.git/hooks/pre-commit`/`pre-push` of
44
+ whatever repo the git OPERATION targets. _hooklib.project_root() trusts
45
+ CLAUDE_PROJECT_DIR first — the right contract for a Claude Code hook
46
+ subprocess, but the wrong one here: a `git -C ../otherRepo commit` issued
47
+ from a Claude session inherits CLAUDE_PROJECT_DIR pointing at the
48
+ session's repo, yet the pre-commit hook that fires is ../otherRepo's own,
49
+ invoked with its OWN cwd/GIT_DIR — never the session's. Trusting
50
+ CLAUDE_PROJECT_DIR here scanned the session's repo (branch/staged
51
+ diff/markers) while the commit actually landed in ../otherRepo, both a
52
+ fail-open (a dirty ../otherRepo passes if the session repo is clean) and
53
+ a false-block (a dirty session repo blocks a clean ../otherRepo commit).
54
+
55
+ Git invokes pre-commit/pre-push with the process cwd set to the target
56
+ repo's work-tree top-level, so `git rev-parse --show-toplevel` run with NO
57
+ cwd override (inheriting this process's actual cwd) resolves the repo the
58
+ hook is actually running in. GIT_DIR/GIT_WORK_TREE (set by git for some
59
+ invocation shapes, e.g. `git -C X commit` invoking the hook with those
60
+ exported) are honored by that same `git rev-parse` call automatically, so
61
+ no separate env read is needed here."""
62
+ try:
63
+ out = subprocess.run(
64
+ [git_executable(), "rev-parse", "--show-toplevel"],
65
+ capture_output=True, text=True, encoding="utf-8", errors="replace",
66
+ timeout=5,
67
+ )
68
+ if out.returncode == 0:
69
+ return out.stdout.strip()
70
+ except Exception: # noqa: BLE001
71
+ pass
72
+ return os.getcwd()
73
+
74
+
75
+ def _git(args, cwd):
76
+ try:
77
+ return subprocess.run(
78
+ [git_executable()] + args, cwd=cwd, capture_output=True, text=True,
79
+ encoding="utf-8", errors="replace", timeout=10,
80
+ )
81
+ except Exception: # noqa: BLE001
82
+ return None
83
+
84
+
85
+ def block(tag, msg):
86
+ print(f"BLOCKED [{tag}]: {msg}", file=sys.stderr)
87
+ sys.exit(1)
88
+
89
+
90
+ # The most recent git-read failure, surfaced in the H-01 fail-closed block
91
+ # message — same rationale and format as pre-bash.py's _READ_ERRS: "git
92
+ # unavailable or timed out" alone is not enough evidence to root-cause a false
93
+ # block.
94
+ _READ_ERRS = []
95
+
96
+
97
+ def _note_read_err(argv, detail):
98
+ _READ_ERRS.append(f"`{' '.join(argv)}` -> {(detail or '').strip()[:200]}")
99
+
100
+
101
+ def _read_err_hint():
102
+ return f" Underlying git error: {_READ_ERRS[-1]}" if _READ_ERRS else ""
103
+
104
+
105
+ def is_protected_branch(branch):
106
+ return (branch or "").lower() in ("main", "master")
107
+
108
+
109
+ def current_branch(cwd):
110
+ """The current branch name, "" for a legitimate detached HEAD, or None when
111
+ git could not answer (spawn failure/timeout/nonzero exit). reliability-001
112
+ (#189): the None sentinel lets H-01 fail CLOSED on a git-read error instead
113
+ of the prior `else ""`, which collapsed "unknown" and "detached, not on a
114
+ protected tip" into the same value and let a commit through."""
115
+ argv = ["branch", "--show-current"]
116
+ r = _git(argv, cwd)
117
+ if r is None or r.returncode != 0:
118
+ _note_read_err(["git"] + argv, (r.stderr if r else None) or "git spawn failed")
119
+ return None
120
+ return r.stdout.strip()
121
+
122
+
123
+ def head_on_protected_tip(cwd):
124
+ """True when HEAD (typically detached) points at a protected branch's tip —
125
+ a commit there still lands on main/master's history. Mirrors pre-bash.py.
126
+
127
+ reliability-001 (#189): returns None (not False) when git could not answer,
128
+ so H-01 fails CLOSED on a git-read error rather than concluding "not on a
129
+ protected tip" from a failed read."""
130
+ argv = ["show-ref", "--head", "refs/heads/main", "refs/heads/master"]
131
+ r = _git(argv, cwd)
132
+ if r is None or r.returncode not in (0, 1):
133
+ _note_read_err(["git"] + argv, (r.stderr if r else None) or "git spawn failed")
134
+ return None
135
+ head_sha, protected = None, set()
136
+ for ln in r.stdout.splitlines():
137
+ parts = ln.split()
138
+ if len(parts) != 2:
139
+ continue
140
+ sha, ref = parts
141
+ if ref == "HEAD":
142
+ head_sha = sha
143
+ elif ref in ("refs/heads/main", "refs/heads/master"):
144
+ protected.add(sha)
145
+ return head_sha is not None and head_sha in protected
146
+
147
+
148
+ def cached_added_lines(cwd):
149
+ """Added (`+`) lines of the staged diff — exactly what a commit records
150
+ (including `commit -a` sweeps, which git has already staged by pre-commit
151
+ time). None on a git read error -> caller fails closed.
152
+
153
+ Excludes gate-events.log (#279): `sensitive_scan_added_lines` walks the
154
+ diff path-aware, dropping lines that belong to the crypto/secret gate's
155
+ own machine-written audit sink, so this backstop's view of "sensitive
156
+ lines" never drifts from the producer (security-pass.py) or the
157
+ PreToolUse gate (pre-bash.py) — see the shared helper's docstring in
158
+ `_hooklib`. Reads the diff via SECURITY_DIFF_GIT_ARGS (pinned a/ b/
159
+ prefixes, no external diff) so a hostile `diff.mnemonicPrefix`/
160
+ `diff.noprefix`/external-diff config can never break that attribution
161
+ (#279 review MEDIUM-1) — a merge commit's `--cc` combined-diff sections
162
+ are also handled correctly (MEDIUM-2): the diff walk resets attribution on
163
+ ANY `diff ` section header, not just `diff --git`."""
164
+ r = _git([*SECURITY_DIFF_GIT_ARGS, "--cached"], cwd)
165
+ if r is None or r.returncode != 0:
166
+ return None
167
+ return sensitive_scan_added_lines(r.stdout)
168
+
169
+
170
+ def cached_names(cwd):
171
+ r = _git(["diff", "--cached", "--name-only"], cwd)
172
+ if r is None or r.returncode != 0:
173
+ return None
174
+ return {p for p in r.stdout.splitlines() if p.strip()}
175
+
176
+
177
+ def read_worktree(cwd, rel):
178
+ p = rel if os.path.isabs(rel) else os.path.join(cwd, rel)
179
+ try:
180
+ if os.path.getsize(p) > 1_000_000:
181
+ return None
182
+ with open(p, encoding="utf-8", errors="replace") as f:
183
+ return f.read()
184
+ except Exception: # noqa: BLE001
185
+ return None
186
+
187
+
188
+ def _marker_set(root, name):
189
+ try:
190
+ with open(os.path.join(root, ".codearbiter", ".markers", name),
191
+ encoding="utf-8") as f:
192
+ return set(f.read().split())
193
+ except Exception: # noqa: BLE001
194
+ return set()
195
+
196
+
197
+ def pre_commit(root):
198
+ cwd = root
199
+ # H-01: no commit onto a protected branch (or a detached HEAD on its tip).
200
+ # A git-read failure fails CLOSED (reliability-001, #189) — the ambiguity
201
+ # otherwise resolves to "not protected", the same hole H-09b/H-14 below
202
+ # already close.
203
+ branch = current_branch(cwd)
204
+ if branch is None:
205
+ block("H-01", "branch state could not be determined (git unavailable or timed "
206
+ "out) — failing closed (ORCHESTRATOR §2, #161 git backstop)." +
207
+ _read_err_hint())
208
+ tip = None
209
+ if not branch:
210
+ tip = head_on_protected_tip(cwd)
211
+ if tip is None:
212
+ block("H-01", "HEAD's protected-branch-tip state could not be determined "
213
+ "(git unavailable or timed out) — failing closed (ORCHESTRATOR "
214
+ "§2, #161 git backstop)." + _read_err_hint())
215
+ if is_protected_branch(branch) or (not branch and tip):
216
+ target = branch or "main/master (detached HEAD)"
217
+ block("H-01", f"Direct commit to {target} is prohibited (ORCHESTRATOR §3) — this is "
218
+ f"the git-level backstop (#161). Create a feature branch.")
219
+
220
+ # H-09b / H-10b: a commit introducing crypto/secret changes needs a fresh,
221
+ # line-covering security-gate pass. Reuses the exact _hooklib primitives.
222
+ added = cached_added_lines(cwd)
223
+ if added is None:
224
+ block("H-09b", "the staged diff for the crypto/secret scan could not be read — "
225
+ "failing closed (ORCHESTRATOR §2).")
226
+ sensitive = [ln for ln in added if CRYPTO_RE.search(ln) or SECRET_RE.search(ln)]
227
+ if sensitive:
228
+ joined = "\n".join(added)
229
+ touches_crypto = bool(CRYPTO_RE.search(joined))
230
+ kind = "crypto/TLS" if touches_crypto else "secret"
231
+ tag = "H-09b" if touches_crypto else "H-10b"
232
+ skill = "crypto-compliance" if touches_crypto else "secret-handling"
233
+ marker = os.path.join(root, ".codearbiter", ".markers", "security-gate-passed")
234
+ if not marker_fresh(marker, MARKER_FRESHNESS_MINUTES):
235
+ block(tag, f"This commit introduces {kind} changes, but no security-gate pass is "
236
+ f"recorded (#161 git backstop). Run the {skill} gate, then commit.")
237
+ approved = _marker_set(root, "security-gate-passed")
238
+ uncovered = [ln for ln in sensitive if line_digest(ln) not in approved]
239
+ if uncovered:
240
+ block(tag, f"{len(uncovered)} {kind} line(s) in this commit are not covered by the "
241
+ f"recorded security-gate pass (#161 git backstop) — re-run the {skill} "
242
+ f"gate so it reviews the current diff, then commit.")
243
+
244
+ # H-14: a staged migration needs a content-bound migration-review pass.
245
+ names = cached_names(cwd)
246
+ if names is None:
247
+ block("H-14", "the staged file list for the migration scan could not be read — "
248
+ "failing closed (ORCHESTRATOR §2).")
249
+ migs = sorted(p for p in names if is_migration_path(p, root))
250
+ if migs:
251
+ approved = _marker_set(root, "migration-gate-passed")
252
+ uncovered = []
253
+ for rel in migs:
254
+ text = read_worktree(cwd, rel)
255
+ if text is None or content_digest(text) not in approved:
256
+ uncovered.append(rel)
257
+ if uncovered:
258
+ block("H-14", f"{len(uncovered)} staged migration file(s) lack a recorded "
259
+ f"migration-review pass (#161 git backstop): {', '.join(uncovered)}. "
260
+ f"Run the migration-review gate, then commit.")
261
+
262
+
263
+ def pre_push(root):
264
+ cwd = root
265
+ # Git feeds pre-push one line per ref: `<local ref> <local sha> <remote ref>
266
+ # <remote sha>`. A deletion has an all-zero local sha; a create has an
267
+ # all-zero remote sha.
268
+ data = ""
269
+ try:
270
+ data = sys.stdin.read()
271
+ except Exception: # noqa: BLE001
272
+ data = ""
273
+ for ln in data.splitlines():
274
+ parts = ln.split()
275
+ if len(parts) < 4:
276
+ continue
277
+ _lref, lsha, rref, rsha = parts[0], parts[1], parts[2], parts[3]
278
+ # H-01: no push whose destination is a protected branch (main/master),
279
+ # in any refspec spelling — `HEAD:main`, `feature:main`, `:main`.
280
+ if rref.startswith("refs/heads/") and is_protected_branch(rref.rsplit("/", 1)[-1]):
281
+ block("H-01", f"Pushing to a protected branch ('{rref}') is prohibited "
282
+ f"(ORCHESTRATOR §3, #161 git backstop) — main moves only via a merged PR.")
283
+ # H-02: no force / non-fast-forward update. Skip creates/deletes
284
+ # (all-zero sha). A non-ff update is one where the remote sha is NOT an
285
+ # ancestor of the local sha; treat an unresolvable check as force (closed).
286
+ zero = lambda s: set(s) == {"0"} # noqa: E731
287
+ if lsha and rsha and not zero(lsha) and not zero(rsha):
288
+ anc = _git(["merge-base", "--is-ancestor", rsha, lsha], cwd)
289
+ if anc is None or anc.returncode != 0:
290
+ block("H-02", "Force-push / non-fast-forward update is prohibited "
291
+ "(ORCHESTRATOR §3, #161 git backstop).")
292
+
293
+
294
+ def main():
295
+ utf8_stdio()
296
+ root = repo_root()
297
+ if not arbiter_active(root):
298
+ sys.exit(0)
299
+ phase = sys.argv[1] if len(sys.argv) > 1 else ""
300
+ if phase == "pre-commit":
301
+ pre_commit(root)
302
+ elif phase == "pre-push":
303
+ pre_push(root)
304
+ # Unknown phase: no-op allow.
305
+ sys.exit(0)
306
+
307
+
308
+ def run(host, argv=None):
309
+ """Host-seam entry point (ADR-0011): the __main__ guard calls this with the
310
+ plugin's loaded Host. Wraps main() unchanged — main() still communicates
311
+ via sys.exit/stdout/stderr, and its return value stays discarded exactly
312
+ as the old bare `main()` guard discarded it (so the process still exits 0
313
+ on a normal fall-through).
314
+
315
+ Wires `host` live (#257): primes `_hooklib`'s process-cached Host via
316
+ `set_host()` BEFORE main() runs, so any `get_host()` call downstream
317
+ resolves to the SAME instance the caller passed here — no second
318
+ `hostapi.load_host()`, and `run(fake_host)` genuinely exercises
319
+ `fake_host`."""
320
+ return _entrylib.dispatch(host, argv, main, set_host,
321
+ pass_argv=False, propagate_result=False)
322
+
323
+
324
+ if __name__ == "__main__":
325
+ sys.exit(run(hostapi.load_host()) or 0)