@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.
- package/LICENSE +661 -0
- package/README.md +558 -0
- package/package.json +35 -0
- package/plugins/ca-pi/CHANGELOG.md +1030 -0
- package/plugins/ca-pi/COMMANDS.md +90 -0
- package/plugins/ca-pi/ORCHESTRATOR.md +159 -0
- package/plugins/ca-pi/SKILLS.md +47 -0
- package/plugins/ca-pi/SPRINT.md +142 -0
- package/plugins/ca-pi/agents/INDEX.md +31 -0
- package/plugins/ca-pi/agents/architecture-drift-reviewer.md +86 -0
- package/plugins/ca-pi/agents/auth-crypto-reviewer.md +60 -0
- package/plugins/ca-pi/agents/backend-author.md +60 -0
- package/plugins/ca-pi/agents/checkpoint-aggregator.md +111 -0
- package/plugins/ca-pi/agents/coverage-auditor.md +71 -0
- package/plugins/ca-pi/agents/decision-challenger.md +116 -0
- package/plugins/ca-pi/agents/dependency-reviewer.md +79 -0
- package/plugins/ca-pi/agents/design-quality-reviewer.md +80 -0
- package/plugins/ca-pi/agents/finding-triage.md +86 -0
- package/plugins/ca-pi/agents/frontend-author.md +64 -0
- package/plugins/ca-pi/agents/grader.md +173 -0
- package/plugins/ca-pi/agents/infra-author.md +64 -0
- package/plugins/ca-pi/agents/map-deps.md +35 -0
- package/plugins/ca-pi/agents/map-structure.md +37 -0
- package/plugins/ca-pi/agents/migration-reviewer.md +65 -0
- package/plugins/ca-pi/agents/scout.md +127 -0
- package/plugins/ca-pi/agents/security-reviewer.md +72 -0
- package/plugins/ca-pi/agents/tribunal-lens-reviewer.md +65 -0
- package/plugins/ca-pi/extensions/codearbiter-child.js +1885 -0
- package/plugins/ca-pi/extensions/codearbiter.js +9802 -0
- package/plugins/ca-pi/generated/command-catalog.json +197 -0
- package/plugins/ca-pi/generated/roles.json +213 -0
- package/plugins/ca-pi/helpers/windows-supervisor.js +205 -0
- package/plugins/ca-pi/hooks/_activationlib.py +196 -0
- package/plugins/ca-pi/hooks/_arbiterstatelib.py +208 -0
- package/plugins/ca-pi/hooks/_babysitlib.py +76 -0
- package/plugins/ca-pi/hooks/_bashguardlib.py +1667 -0
- package/plugins/ca-pi/hooks/_boxlib.py +131 -0
- package/plugins/ca-pi/hooks/_colorlib.py +304 -0
- package/plugins/ca-pi/hooks/_durabilitylib.py +186 -0
- package/plugins/ca-pi/hooks/_entrylib.py +41 -0
- package/plugins/ca-pi/hooks/_fmtlib.py +161 -0
- package/plugins/ca-pi/hooks/_gitexec.py +45 -0
- package/plugins/ca-pi/hooks/_githooks.py +920 -0
- package/plugins/ca-pi/hooks/_gitlib.py +110 -0
- package/plugins/ca-pi/hooks/_hooklib.py +595 -0
- package/plugins/ca-pi/hooks/_host.py +115 -0
- package/plugins/ca-pi/hooks/_intentlib.py +242 -0
- package/plugins/ca-pi/hooks/_ledgerlib.py +1035 -0
- package/plugins/ca-pi/hooks/_metricslib.py +709 -0
- package/plugins/ca-pi/hooks/_pathnorm.py +74 -0
- package/plugins/ca-pi/hooks/_planfilelib.py +664 -0
- package/plugins/ca-pi/hooks/_previewlib.py +193 -0
- package/plugins/ca-pi/hooks/_protectedlib.py +312 -0
- package/plugins/ca-pi/hooks/_protectedstatelib.py +411 -0
- package/plugins/ca-pi/hooks/_provenancelib.py +971 -0
- package/plugins/ca-pi/hooks/_prunelib.py +1398 -0
- package/plugins/ca-pi/hooks/_prunepolicy.py +235 -0
- package/plugins/ca-pi/hooks/_readinjectlib.py +1080 -0
- package/plugins/ca-pi/hooks/_releaselib.py +2657 -0
- package/plugins/ca-pi/hooks/_scopelib.py +262 -0
- package/plugins/ca-pi/hooks/_segmentslib.py +278 -0
- package/plugins/ca-pi/hooks/_sensitivelib.py +270 -0
- package/plugins/ca-pi/hooks/_sessionlib.py +78 -0
- package/plugins/ca-pi/hooks/_sloplib.py +244 -0
- package/plugins/ca-pi/hooks/_standuplib.py +214 -0
- package/plugins/ca-pi/hooks/_subagentslib.py +219 -0
- package/plugins/ca-pi/hooks/_taskboardlib.py +1088 -0
- package/plugins/ca-pi/hooks/_updatelib.py +278 -0
- package/plugins/ca-pi/hooks/babysit.py +47 -0
- package/plugins/ca-pi/hooks/boardsync.py +129 -0
- package/plugins/ca-pi/hooks/doctor.py +420 -0
- package/plugins/ca-pi/hooks/git-enforce.py +325 -0
- package/plugins/ca-pi/hooks/hostapi.py +460 -0
- package/plugins/ca-pi/hooks/init-codearbiter.py +225 -0
- package/plugins/ca-pi/hooks/metrics.py +62 -0
- package/plugins/ca-pi/hooks/migration-pass.py +129 -0
- package/plugins/ca-pi/hooks/pi-bridge.py +543 -0
- package/plugins/ca-pi/hooks/post-write-edit.py +231 -0
- package/plugins/ca-pi/hooks/pre-bash.py +90 -0
- package/plugins/ca-pi/hooks/pre-edit.py +284 -0
- package/plugins/ca-pi/hooks/pre-read.py +81 -0
- package/plugins/ca-pi/hooks/pre-write.py +217 -0
- package/plugins/ca-pi/hooks/preview.py +69 -0
- package/plugins/ca-pi/hooks/prune-transcript.py +232 -0
- package/plugins/ca-pi/hooks/releasehash.py +216 -0
- package/plugins/ca-pi/hooks/security-pass.py +139 -0
- package/plugins/ca-pi/hooks/session-start.py +1218 -0
- package/plugins/ca-pi/hooks/statusline.py +736 -0
- package/plugins/ca-pi/hooks/taskwrite.py +351 -0
- package/plugins/ca-pi/hooks/update-refresh.py +51 -0
- package/plugins/ca-pi/hooks/wire-statusline.py +435 -0
- package/plugins/ca-pi/includes/anti-slop-design/INDEX.md +55 -0
- package/plugins/ca-pi/includes/anti-slop-design/color.md +43 -0
- package/plugins/ca-pi/includes/anti-slop-design/core.md +244 -0
- package/plugins/ca-pi/includes/anti-slop-design/images.md +32 -0
- package/plugins/ca-pi/includes/anti-slop-design/layout.md +45 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-cli.md +39 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-dataviz.md +43 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-diagram.md +35 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-documents.md +70 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-slides.md +30 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-web.md +39 -0
- package/plugins/ca-pi/includes/anti-slop-design/typography.md +51 -0
- package/plugins/ca-pi/includes/author-tdd-workflow.md +14 -0
- package/plugins/ca-pi/includes/compaction-charter.md +16 -0
- package/plugins/ca-pi/includes/cut-docs.md +16 -0
- package/plugins/ca-pi/includes/dev-mode.md +30 -0
- package/plugins/ca-pi/includes/farm.md +237 -0
- package/plugins/ca-pi/includes/fresh-verification.md +14 -0
- package/plugins/ca-pi/includes/harvest.md +69 -0
- package/plugins/ca-pi/includes/maturity-coverage.md +102 -0
- package/plugins/ca-pi/includes/pi-host-notes.md +69 -0
- package/plugins/ca-pi/includes/redirect.md +69 -0
- package/plugins/ca-pi/includes/reference-map.md +22 -0
- package/plugins/ca-pi/includes/review-matrix.md +14 -0
- package/plugins/ca-pi/includes/reviewer-contract.md +53 -0
- package/plugins/ca-pi/includes/routing-table.md +47 -0
- package/plugins/ca-pi/includes/security-gate-record.md +22 -0
- package/plugins/ca-pi/includes/smarts/core.md +90 -0
- package/plugins/ca-pi/includes/smarts/decision-log-format.md +56 -0
- package/plugins/ca-pi/routines/INDEX.md +32 -0
- package/plugins/ca-pi/routines/brainstorming/SKILL.md +122 -0
- package/plugins/ca-pi/routines/commit-gate/SKILL.md +151 -0
- package/plugins/ca-pi/routines/context-check/SKILL.md +85 -0
- package/plugins/ca-pi/routines/context-creation/SKILL.md +171 -0
- package/plugins/ca-pi/routines/crypto-compliance/SKILL.md +41 -0
- package/plugins/ca-pi/routines/debug/SKILL.md +99 -0
- package/plugins/ca-pi/routines/decision-lifecycle/SKILL.md +104 -0
- package/plugins/ca-pi/routines/decision-lifecycle/references/adr-template.md +74 -0
- package/plugins/ca-pi/routines/decision-variance/SKILL.md +147 -0
- package/plugins/ca-pi/routines/decompose/SKILL.md +168 -0
- package/plugins/ca-pi/routines/dispatching-parallel-agents/SKILL.md +76 -0
- package/plugins/ca-pi/routines/executing-plans/SKILL.md +83 -0
- package/plugins/ca-pi/routines/finishing-a-development-branch/SKILL.md +91 -0
- package/plugins/ca-pi/routines/post-merge-cleanup/SKILL.md +233 -0
- package/plugins/ca-pi/routines/refactor/SKILL.md +91 -0
- package/plugins/ca-pi/routines/release/SKILL.md +315 -0
- package/plugins/ca-pi/routines/secret-handling/SKILL.md +67 -0
- package/plugins/ca-pi/routines/security-architecture/SKILL.md +63 -0
- package/plugins/ca-pi/routines/skill-author/SKILL.md +108 -0
- package/plugins/ca-pi/routines/skill-author/references/skill-template.md +58 -0
- package/plugins/ca-pi/routines/subagent-driven-development/SKILL.md +149 -0
- package/plugins/ca-pi/routines/subagent-driven-development/references/farm-dispatch.md +145 -0
- package/plugins/ca-pi/routines/tdd/SKILL.md +139 -0
- package/plugins/ca-pi/routines/tribunal/SKILL.md +109 -0
- package/plugins/ca-pi/routines/tribunal/references/ai-markers.md +29 -0
- package/plugins/ca-pi/routines/tribunal/references/cost-and-models.md +64 -0
- package/plugins/ca-pi/routines/tribunal/references/finding-record.md +27 -0
- package/plugins/ca-pi/routines/tribunal/references/issue-filing.md +47 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/appsec.md +22 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/architecture.md +23 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/coverage.md +20 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/infra.md +24 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/migration.md +22 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/observability.md +21 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/performance.md +22 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/reliability.md +23 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/secrets-supply.md +22 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/test-fidelity.md +24 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/typesafety.md +21 -0
- package/plugins/ca-pi/routines/tribunal/references/report.md +19 -0
- package/plugins/ca-pi/routines/tribunal/references/schemas.md +58 -0
- package/plugins/ca-pi/routines/tribunal/references/telemetry.md +28 -0
- package/plugins/ca-pi/routines/tribunal/references/triage.md +53 -0
- package/plugins/ca-pi/routines/using-git-worktrees/SKILL.md +85 -0
- package/plugins/ca-pi/routines/writing-plans/SKILL.md +129 -0
- package/plugins/ca-pi/routines/writing-plans/references/farm-plan.md +50 -0
- package/plugins/ca-pi/skills/ca-add-dep/SKILL.md +88 -0
- package/plugins/ca-pi/skills/ca-adr/SKILL.md +30 -0
- package/plugins/ca-pi/skills/ca-adr-status/SKILL.md +30 -0
- package/plugins/ca-pi/skills/ca-arbiter/SKILL.md +36 -0
- package/plugins/ca-pi/skills/ca-audit/SKILL.md +51 -0
- package/plugins/ca-pi/skills/ca-btw/SKILL.md +23 -0
- package/plugins/ca-pi/skills/ca-checkpoint/SKILL.md +50 -0
- package/plugins/ca-pi/skills/ca-chore/SKILL.md +58 -0
- package/plugins/ca-pi/skills/ca-cleanup/SKILL.md +55 -0
- package/plugins/ca-pi/skills/ca-commands/SKILL.md +21 -0
- package/plugins/ca-pi/skills/ca-commit/SKILL.md +27 -0
- package/plugins/ca-pi/skills/ca-conflict/SKILL.md +61 -0
- package/plugins/ca-pi/skills/ca-context-check/SKILL.md +32 -0
- package/plugins/ca-pi/skills/ca-create-context/SKILL.md +32 -0
- package/plugins/ca-pi/skills/ca-debug/SKILL.md +42 -0
- package/plugins/ca-pi/skills/ca-decompose/SKILL.md +30 -0
- package/plugins/ca-pi/skills/ca-dev/SKILL.md +42 -0
- package/plugins/ca-pi/skills/ca-doctor/SKILL.md +44 -0
- package/plugins/ca-pi/skills/ca-feature/SKILL.md +105 -0
- package/plugins/ca-pi/skills/ca-fix/SKILL.md +42 -0
- package/plugins/ca-pi/skills/ca-init/SKILL.md +56 -0
- package/plugins/ca-pi/skills/ca-metrics/SKILL.md +80 -0
- package/plugins/ca-pi/skills/ca-new-skill/SKILL.md +34 -0
- package/plugins/ca-pi/skills/ca-override/SKILL.md +72 -0
- package/plugins/ca-pi/skills/ca-pr/SKILL.md +61 -0
- package/plugins/ca-pi/skills/ca-preview/SKILL.md +86 -0
- package/plugins/ca-pi/skills/ca-prune/SKILL.md +100 -0
- package/plugins/ca-pi/skills/ca-reconcile/SKILL.md +43 -0
- package/plugins/ca-pi/skills/ca-refactor/SKILL.md +43 -0
- package/plugins/ca-pi/skills/ca-release/SKILL.md +57 -0
- package/plugins/ca-pi/skills/ca-review/SKILL.md +63 -0
- package/plugins/ca-pi/skills/ca-spike/SKILL.md +41 -0
- package/plugins/ca-pi/skills/ca-sprint/SKILL.md +44 -0
- package/plugins/ca-pi/skills/ca-standup/SKILL.md +112 -0
- package/plugins/ca-pi/skills/ca-status/SKILL.md +56 -0
- package/plugins/ca-pi/skills/ca-task/SKILL.md +61 -0
- package/plugins/ca-pi/skills/ca-threat-model/SKILL.md +45 -0
- package/plugins/ca-pi/skills/ca-tribunal/SKILL.md +44 -0
- 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)
|