@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,81 @@
1
+ #!/usr/bin/env python3
2
+ # codeArbiter — PreToolUse:Read hook: file-scoped just-in-time context injection.
3
+ #
4
+ # Thin entry point for the file-scoped-context-injection spec (AC-03, AC-10).
5
+ # All testable logic lives in _readinjectlib.py; this file wires the Claude Code
6
+ # hook protocol to the lib's compute_injection / allow_output helpers.
7
+ #
8
+ # Control flow:
9
+ # 1. utf8_stdio(); root = project_root().
10
+ # 2. If not arbiter_active(root): sys.exit(0) — dormant in non-opted-in repos.
11
+ # 3. Parse input: data = read_input(); extract file_path from tool_input(data);
12
+ # session_id = data.get("session_id", "").
13
+ # 4. rel = repo_rel(file_path, root). If rel is falsy (outside repo): sys.exit(0).
14
+ # 5. ctx = compute_injection(root, session_id, rel) — handles the .codearbiter/
15
+ # self-read guard (AC-10), per-(session, file) dedup (AC-09), four-tier index
16
+ # lookup, budget assembly, and marker write internally.
17
+ # 6. If ctx non-empty: print allow_output(ctx) as JSON (AC-03) and exit 0.
18
+ # If ctx empty (miss, self-read, or dedup): exit 0 with no output — silent allow.
19
+ #
20
+ # Design invariants:
21
+ # - Always allows the Read — never emits a deny or block decision.
22
+ # - Emits allow_output(ctx) ONLY when ctx is non-empty (AC-10: miss and self-read
23
+ # must be silent; "Done looks like" requires no output on a non-governed Read).
24
+ # - Fail-open (AC-12): any exception degrades to sys.exit(0) with no stdout
25
+ # output. A hook crash must NEVER block or stall a Read. The outer
26
+ # except Exception clause is the last-resort safety net; sys.exit(0) calls
27
+ # inside the try block raise SystemExit (a BaseException, not an Exception)
28
+ # so they propagate normally and are never swallowed by this clause.
29
+
30
+ import json
31
+ import os
32
+ import sys
33
+
34
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
35
+ import hostapi # noqa: E402 — host seam (ADR-0011)
36
+ import _entrylib # noqa: E402 — shared run() dispatch (jscpd dedup)
37
+ from _hooklib import ( # noqa: E402
38
+ arbiter_active, project_root, read_input, repo_rel, set_host, tool_input,
39
+ utf8_stdio,
40
+ )
41
+ from _readinjectlib import allow_output, compute_injection # noqa: E402
42
+
43
+
44
+ def main():
45
+ try:
46
+ utf8_stdio()
47
+ root = project_root()
48
+ if not arbiter_active(root):
49
+ sys.exit(0)
50
+ data = read_input()
51
+ file_path = tool_input(data).get("file_path", "")
52
+ session_id = data.get("session_id", "")
53
+ rel = repo_rel(file_path, root)
54
+ if not rel:
55
+ sys.exit(0)
56
+ ctx = compute_injection(root, session_id, rel)
57
+ if ctx:
58
+ print(json.dumps(allow_output(ctx)))
59
+ sys.exit(0)
60
+ except Exception: # noqa: BLE001 — AC-12: fail-open, never block a Read
61
+ sys.exit(0)
62
+
63
+
64
+ def run(host, argv=None):
65
+ """Host-seam entry point (ADR-0011): the __main__ guard calls this with the
66
+ plugin's loaded Host. Wraps main() unchanged — main() still communicates
67
+ via sys.exit/stdout/stderr, and its return value stays discarded exactly
68
+ as the old bare `main()` guard discarded it (so the process still exits 0
69
+ on a normal fall-through).
70
+
71
+ Wires `host` live (#257): primes `_hooklib`'s process-cached Host via
72
+ `set_host()` BEFORE main() runs, so any `get_host()` call downstream
73
+ resolves to the SAME instance the caller passed here — no second
74
+ `hostapi.load_host()`, and `run(fake_host)` genuinely exercises
75
+ `fake_host`."""
76
+ return _entrylib.dispatch(host, argv, main, set_host,
77
+ pass_argv=False, propagate_result=False)
78
+
79
+
80
+ if __name__ == "__main__":
81
+ sys.exit(run(hostapi.load_host()) or 0)
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env python3
2
+ # codeArbiter v2 — PreToolUse(Write) guard. Activation + audit-log + marker + ADR
3
+ # integrity. Python port of pre-write.sh (#25): no jq, fails loud, blocks via
4
+ # exit 2.
5
+ #
6
+ # Guards (all on the arbiter-enabled repo only):
7
+ # H-18 .codearbiter/CONTEXT.md is the activation switch every hook gates on
8
+ # (#159) — a Write may not flip `arbiter: enabled` off or corrupt the
9
+ # frontmatter, which would make every gate dormant.
10
+ # H-19 .codearbiter/.markers/* are the gate-pass tokens that turn a hard-gate
11
+ # BLOCK into an allow (#160) — never writable via the Write tool.
12
+ # H-05 audit logs (overrides.log, triage.log, sprint-log.md, decisions/decision-log.md) are append-only.
13
+ # H-11 ADRs under decisions/ are authored only via /adr.
14
+ # H-22 the protected-state registry (B1/#564): a marker-gated entry admits
15
+ # only under a fresh authoring marker; a helper-only or append-only
16
+ # entry is hard-blocked with NO marker path at all — one generic
17
+ # branch resolves the entry's policy via _protectedstatelib and
18
+ # applies it, so a fourth registered file needs a registry entry,
19
+ # never a new hook branch.
20
+ #
21
+ # Every protected-path decision resolves symlinks (#162): classify_protected()
22
+ # checks the raw path AND its realpath-resolved repo-relative form, so a symlink
23
+ # alias whose visible path lacks `.codearbiter/` can't launder a write past a
24
+ # guard.
25
+
26
+ import os
27
+ import sys
28
+ import traceback
29
+
30
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
31
+ import hostapi # noqa: E402 — host seam (ADR-0011)
32
+ import _entrylib # noqa: E402 — shared run() dispatch (jscpd dedup)
33
+ from _hooklib import ( # noqa: E402
34
+ MARKER_FRESHNESS_MINUTES, arbiter_active, block, classify_protected,
35
+ frontmatter_enabled_text, get_host, marker_fresh, project_root,
36
+ read_input, set_host, utf8_stdio,
37
+ )
38
+ from _protectedstatelib import ( # noqa: E402
39
+ ProtectedPolicy, marker_gated_write_admitted, resolve_registered_path,
40
+ )
41
+
42
+
43
+ def _guard_op(root, op):
44
+ """Run the four protected-path guards against ONE canonical file op
45
+ (hostapi.Host.iter_file_ops). Under Claude Code this receives exactly the
46
+ one "write" op its Write payload maps to, so every check, message, and
47
+ check ORDER below is byte-identical to the pre-seam single-payload body.
48
+ The "edit"/"delete" kinds arrive only from a host whose write tool batches
49
+ per-file operations (Codex's apply_patch, ADR-0011 M2) — a patch hunk's
50
+ resulting content cannot be replayed the way pre-edit.py replays an Edit,
51
+ so those kinds resolve CLOSED on the content-sensitive guards."""
52
+ fpath = op.get("file_path", "") or ""
53
+ kind = op.get("kind", "write")
54
+
55
+ # H-21: an "opaque" op is a host's signal that a write-batching payload
56
+ # carried a recognizable envelope the adapter could NOT decompose into
57
+ # per-file ops (e.g. an apply_patch variant outside the mirrored grammar).
58
+ # Whatever it touches is unknowable here, so it can never pass unguarded —
59
+ # ambiguity resolves CLOSED (ORCHESTRATOR §2). Claude Code never emits
60
+ # this kind; its Write payload always maps to exactly one "write" op.
61
+ if kind == "opaque":
62
+ block("H-21", "This patch envelope could not be decomposed into per-file operations, "
63
+ "so its targets cannot be guarded. Failing closed. Re-issue the change "
64
+ "as a plain patch in the documented apply_patch grammar (or split it "
65
+ "into smaller single-file patches).")
66
+
67
+ classes = classify_protected(fpath, root)
68
+
69
+ # H-19: the gate-pass markers are the mechanism that turns a hard-gate BLOCK
70
+ # into an allow (#160). They are recorded only by the sanctioned producers
71
+ # (security-pass.py / migration-pass.py) and the /adr `touch` — a Write here
72
+ # is a hand-forged marker. Block outright (any kind: write, edit, delete).
73
+ if "marker" in classes:
74
+ block("H-19", "The .codearbiter/.markers/ gate tokens are not writable via the Write "
75
+ "tool (#160) — a hand-written marker forges a security/migration/ADR gate "
76
+ "pass. Markers are recorded only by the sanctioned gate producers.")
77
+
78
+ # H-18: CONTEXT.md is the master switch arbiter_active() reads (#159). A Write
79
+ # is a full overwrite, so vet the RESULTING content: allow only if it still
80
+ # carries a well-formed `arbiter: enabled` frontmatter. Disabling arbiter
81
+ # from inside the repo it governs is exactly the kill-switch this closes (a
82
+ # human can still opt out by editing the file outside the agent's tools).
83
+ if "context" in classes:
84
+ if kind == "write":
85
+ enabled, malformed = frontmatter_enabled_text(op.get("content", "") or "")
86
+ if malformed or not enabled:
87
+ block("H-18", "This Write would remove or alter the `arbiter: enabled` frontmatter in "
88
+ ".codearbiter/CONTEXT.md (#159) — the activation switch every enforcement "
89
+ "hook reads. Disabling it from inside the repo would make every gate "
90
+ "dormant. Keep `arbiter: enabled` in a well-formed frontmatter block.")
91
+ else:
92
+ # A patch edit's resulting frontmatter cannot be verified from its
93
+ # hunks, and a delete removes the activation switch outright —
94
+ # ambiguity resolves CLOSED (ORCHESTRATOR §2). /ca:override is the
95
+ # sanctioned escape hatch.
96
+ block("H-18", "This patch operation edits or deletes .codearbiter/CONTEXT.md (#159) — "
97
+ "the activation switch every enforcement hook reads — and its resulting "
98
+ "frontmatter cannot be verified from patch hunks. Failing closed; use the "
99
+ "sanctioned init path (or "
100
+ + get_host().cmd_ref("override") + ").")
101
+
102
+ # H-05: the audit logs (and the /sprint decision record) are append-only —
103
+ # a Write is a full overwrite. (path set: _hooklib.is_audit_log)
104
+ if "audit" in classes:
105
+ if kind == "write":
106
+ block("H-05", "The .codearbiter audit logs (overrides.log, triage.log, sprint-log.md, decisions/decision-log.md) "
107
+ "are append-only (ORCHESTRATOR §7). Append with Edit or '>>', never Write.")
108
+ # A patch edit is positional (it can rewrite interior lines) and a
109
+ # patch delete destroys the trail — neither can be a verifiable pure
110
+ # append the way pre-edit.py's tail-anchored Edit can.
111
+ block("H-05", "The .codearbiter audit logs (overrides.log, triage.log, sprint-log.md, decisions/decision-log.md) "
112
+ "are append-only (ORCHESTRATOR §7). A patch cannot express a verifiable "
113
+ "pure append; append with '>>' instead.")
114
+
115
+ # H-11: ADRs may only be authored via /adr (the skill drops the marker first).
116
+ # Any .md anywhere under decisions/ is covered — a non-numbered draft or a
117
+ # nested path is still an immutable decision artifact. (set: is_decisions_path)
118
+ if "decisions" in classes:
119
+ if kind == "delete":
120
+ block("H-11", "ADR files under .codearbiter/decisions/ are immutable history "
121
+ "(ORCHESTRATOR §6) — deleting one is prohibited, marker or not.")
122
+ marker = os.path.join(root, ".codearbiter", ".markers", "adr-authoring-active")
123
+ if not os.path.isfile(marker):
124
+ block("H-11", "ADR files are authored only via /adr (ORCHESTRATOR §3) — user "
125
+ "attribution required. Subagent-authored ADRs are prohibited.")
126
+ if not marker_fresh(marker, MARKER_FRESHNESS_MINUTES):
127
+ block("H-11", f"ADR authoring marker is stale (>{MARKER_FRESHNESS_MINUTES} min). Re-run /adr.")
128
+
129
+ # H-22: the protected-state registry (B1/#564) — ONE generic branch,
130
+ # regardless of `kind`. classify_protected already decided "state" is a
131
+ # hit; resolve_registered_path only resolves WHICH registered path
132
+ # matched and WHICH policy it carries — never a second, independent
133
+ # membership check (T-06 design ruling). marker-gated admits only under a
134
+ # fresh authoring marker; helper-only and append-only are
135
+ # flank-IDENTICAL here — both hard-block unconditionally, with NO marker
136
+ # path at all. The policies differ only in what the SANCTIONED HELPER's
137
+ # own file I/O is allowed to do, which this tool-call flank never sees by
138
+ # construction (its argv/content never reaches this guard).
139
+ if "state" in classes:
140
+ rel, policy = resolve_registered_path(fpath, root)
141
+ if kind == "delete":
142
+ # A DELETE is never authoring, so no authoring marker admits it —
143
+ # for any policy, marker-gated included. The marker means "the
144
+ # sanctioned lane is writing a row right now"; reading it as
145
+ # "...and may therefore remove the file entirely" turns a
146
+ # self-mintable, friction-grade token (ADR-0024) into a delete
147
+ # capability over protected state, which is strictly more than
148
+ # the lane it gates can legitimately need.
149
+ block("H-22", f"'{rel}' is protected project state (#564) — deleting it is "
150
+ f"never admitted, including under a fresh authoring marker. "
151
+ f"An authoring lane writes rows; it does not remove the file. "
152
+ + get_host().cmd_ref("override") + " if this is deliberate.")
153
+ if policy == ProtectedPolicy.MARKER_GATED:
154
+ if not marker_gated_write_admitted(rel, root):
155
+ block("H-22", f"'{rel}' is marker-gated protected project state (#564) — a "
156
+ f"Write is admitted only under a fresh authoring marker. Run "
157
+ f"the sanctioned authoring lane, or "
158
+ + get_host().cmd_ref("override") + ".")
159
+ else:
160
+ block("H-22", f"'{rel}' is protected project state (#564) — the Write tool may "
161
+ f"never target it; there is no marker path for this policy. Use "
162
+ f"the sanctioned helper.")
163
+
164
+
165
+ def _run(root):
166
+ # The host seam (ADR-0011, M2): iter_file_ops maps this host's native
167
+ # payload to canonical per-file ops. Claude's Write payload maps to exactly
168
+ # one "write" op (same fields the pre-seam body read straight off
169
+ # tool_input), so the Claude verdict path is unchanged; Codex's apply_patch
170
+ # envelope fans out to one op per touched file, each hitting the same
171
+ # guards.
172
+ for op in get_host().iter_file_ops(read_input()):
173
+ _guard_op(root, op)
174
+ sys.exit(0)
175
+
176
+
177
+ def main():
178
+ utf8_stdio()
179
+ root = project_root()
180
+ if not arbiter_active(root):
181
+ sys.exit(0)
182
+ # reliability-002 (#189): scoped to arbiter-enabled repos only (above), so a
183
+ # dormant/non-codeArbiter repo can never be bricked by a crash here. An
184
+ # uncaught exception in the scan below must fail CLOSED (exit 2 = BLOCK),
185
+ # not exit 1 — a non-2 exit is a NON-blocking error under the Claude Code
186
+ # hook contract (_hooklib.py:11-15) and would silently ALLOW the Write.
187
+ # read_input()'s documented fail-OPEN parse behavior is unaffected: it
188
+ # catches its own errors and returns {} before this wrapper is reached.
189
+ try:
190
+ _run(root)
191
+ except SystemExit:
192
+ raise
193
+ except Exception: # noqa: BLE001 — the fail-closed backstop of last resort
194
+ traceback.print_exc(file=sys.stderr)
195
+ block("H-00", "pre-write guard crashed while scanning this Write — failing "
196
+ "closed (ORCHESTRATOR §2) rather than silently allowing an "
197
+ "unscanned write. See the traceback above; retry, or report it.")
198
+
199
+
200
+ def run(host, argv=None):
201
+ """Host-seam entry point (ADR-0011): the __main__ guard calls this with the
202
+ plugin's loaded Host. Wraps main() unchanged — main() still communicates
203
+ via sys.exit/stdout/stderr, and its return value stays discarded exactly
204
+ as the old bare `main()` guard discarded it (so the process still exits 0
205
+ on a normal fall-through).
206
+
207
+ Wires `host` live (#257): primes `_hooklib`'s process-cached Host via
208
+ `set_host()` BEFORE main() runs, so main()'s `get_host()` call resolves
209
+ to the SAME instance the caller passed here — no second
210
+ `hostapi.load_host()`, and `run(fake_host)` genuinely exercises
211
+ `fake_host`."""
212
+ return _entrylib.dispatch(host, argv, main, set_host,
213
+ pass_argv=False, propagate_result=False)
214
+
215
+
216
+ if __name__ == "__main__":
217
+ sys.exit(run(hostapi.load_host()) or 0)
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env python3
2
+ # codeArbiter — thin entry point for /ca:preview (architecture-006/#179).
3
+ #
4
+ # Wraps _previewlib.collect_diff / scan_secrets and prints the JSON-serialized
5
+ # form command prose needs, so the field-access/serialization step (reaching
6
+ # into ChangedFile.kinds and SecretFinding._asdict()) is import-covered
7
+ # (py_compile + unit tests) instead of living as inline `python -c`
8
+ # multi-statement blocks coupled to the lib's namedtuple internals. Mirrors
9
+ # doctor.py / taskwrite.py: entry point stays thin, all logic lives in
10
+ # _previewlib.
11
+ #
12
+ # Invoked by commands/preview.md as (interpreter resolved once by presence,
13
+ # never a `python3 X || python X` fold -- #577):
14
+ # "$PY" "<plugin>/hooks/preview.py" diff
15
+ # "$PY" "<plugin>/hooks/preview.py" secrets
16
+ #
17
+ # Read-only, mirrors _previewlib's own read-only contract (git rev-parse/diff/
18
+ # ls-files and read-only file opens only): this wrapper writes nothing.
19
+
20
+ import argparse
21
+ import json
22
+ import os
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 _hooklib # noqa: E402 — set_host DI seam (#257)
28
+ import _entrylib # noqa: E402 — shared run() dispatch (jscpd dedup)
29
+ import _previewlib # noqa: E402
30
+
31
+
32
+ def diff_json():
33
+ """JSON-serializable form of collect_diff(): path -> sorted kind list."""
34
+ return {p: sorted(cf.kinds) for p, cf in _previewlib.collect_diff().items()}
35
+
36
+
37
+ def secrets_json():
38
+ """JSON-serializable form of scan_secrets(): list of finding dicts, each
39
+ already carrying the redacted (never plaintext) snippet."""
40
+ return [f._asdict() for f in _previewlib.scan_secrets()]
41
+
42
+
43
+ def main(argv=None):
44
+ parser = argparse.ArgumentParser(prog="preview", add_help=True)
45
+ parser.add_argument("mode", choices=("diff", "secrets"))
46
+ args = parser.parse_args(argv)
47
+
48
+ result = diff_json() if args.mode == "diff" else secrets_json()
49
+ json.dump(result, sys.stdout)
50
+ sys.stdout.write("\n")
51
+ return 0
52
+
53
+
54
+ def run(host, argv=None):
55
+ """Host-seam entry point (ADR-0011): the __main__ guard calls this with the
56
+ plugin's loaded Host. Delegates to main(argv) and returns its exit code,
57
+ exactly as the old `sys.exit(main())` guard propagated it.
58
+
59
+ Wires `host` live (#257): primes `_hooklib`'s process-cached Host via
60
+ `set_host()` BEFORE main() runs, so any `get_host()` call downstream
61
+ resolves to the SAME instance the caller passed here — no second
62
+ `hostapi.load_host()`, and `run(fake_host)` genuinely exercises
63
+ `fake_host`."""
64
+ return _entrylib.dispatch(host, argv, main, _hooklib.set_host,
65
+ pass_argv=True, propagate_result=True)
66
+
67
+
68
+ if __name__ == "__main__":
69
+ sys.exit(run(hostapi.load_host()) or 0)
@@ -0,0 +1,232 @@
1
+ #!/usr/bin/env python3
2
+ # codeArbiter — session-transcript pruner entry point.
3
+ #
4
+ # CLI today (Phase 1); a hook mode (UserPromptSubmit/PreCompact) is wired in a
5
+ # later phase. The pruning engine lives in _prunelib.py.
6
+ #
7
+ # prune-transcript.py <path|session-id> [--execute] [--tier T]
8
+ # [--strategies a,b] [--keep-recent N] [--max-bytes N]
9
+ # prune-transcript.py audit <path> # read-only integrity report
10
+ # prune-transcript.py report <path> # size / est-token breakdown only
11
+ #
12
+ # Dry-run is the default. --execute is required to write, and refuses a live
13
+ # transcript (mtime within LIVE_SECS) — the live session is only ever touched
14
+ # by the hook path. Stdlib only.
15
+
16
+ import argparse
17
+ import glob
18
+ import json
19
+ import os
20
+ import sys
21
+ import time
22
+
23
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
24
+ import hostapi # noqa: E402 — host seam (ADR-0011)
25
+ from _prunelib import ( # noqa: E402
26
+ Config, audit, est_tokens, hook_run, load_lines,
27
+ )
28
+ # Aliased (ADR-0011): `run(host)` is now this script's host-seam entry point,
29
+ # so the pruning engine's `run` cedes the module-level name here.
30
+ from _prunelib import run as _engine_run # noqa: E402
31
+
32
+
33
+ def utf8_stdio():
34
+ for s in (sys.stdout, sys.stderr):
35
+ try:
36
+ s.reconfigure(encoding="utf-8", errors="replace")
37
+ except Exception: # noqa: BLE001
38
+ pass
39
+
40
+
41
+ def staleness_check(payload):
42
+ """(CONFIRM-09) WARN, never block, when an active /dev or /sprint flow's
43
+ marker has sat past the staleness window with no matching audit-log
44
+ activity — see _hooklib.staleness_warning for the detection rule and
45
+ security-controls.md § Audit trail for the accepted-strategy rationale.
46
+
47
+ Only runs when the repo has opted in (arbiter_active). Best-effort: ANY
48
+ failure (missing _hooklib import, a broken root, an unreadable marker)
49
+ degrades to doing nothing — this must never affect prune-transcript.py's
50
+ hook-mode exit code (always 0) or block the user's prompt."""
51
+ try:
52
+ import _hooklib
53
+ # Resolve through the host seam (ADR-0011), not the raw payload cwd —
54
+ # a session whose cwd is a repo subdirectory must still resolve the
55
+ # repo root (CLAUDE_PROJECT_DIR -> payload cwd -> git toplevel ->
56
+ # process cwd), or this WARN silently never fires (#264).
57
+ root = _hooklib.get_host().project_root(payload)
58
+ if not _hooklib.arbiter_active(root):
59
+ return
60
+ for msg in _hooklib.staleness_warning(root):
61
+ _hooklib.warn(msg)
62
+ except Exception: # noqa: BLE001 — a missed warn is acceptable; a crash is not
63
+ pass
64
+
65
+
66
+ def resolve(path_or_id):
67
+ """Accept a direct path or a bare session id (resolved under ~/.claude)."""
68
+ if os.path.isfile(path_or_id):
69
+ return path_or_id
70
+ hits = glob.glob(os.path.expanduser(
71
+ f"~/.claude/projects/*/{path_or_id}.jsonl"))
72
+ if len(hits) == 1:
73
+ return hits[0]
74
+ if len(hits) > 1:
75
+ sys.exit(f"ambiguous session id (matches {len(hits)} files); pass a path")
76
+ sys.exit(f"no such transcript: {path_or_id}")
77
+
78
+
79
+ def is_live(path, live_secs):
80
+ try:
81
+ return (time.time() - os.path.getmtime(path)) < live_secs
82
+ except OSError:
83
+ return False
84
+
85
+
86
+ def _fmt_bytes(n):
87
+ n = float(n)
88
+ for unit in ("B", "K", "M", "G"):
89
+ if abs(n) < 1024 or unit == "G":
90
+ return f"{int(n)}B" if unit == "B" else f"{n:.1f}{unit}"
91
+ n /= 1024
92
+
93
+
94
+ def print_report(res):
95
+ rows = res["strategies"]
96
+ if rows:
97
+ print(f"{'strategy':<24}{'scope':>11}{'lines':>7}{'before':>12}"
98
+ f"{'after':>12}{'est≈ saved':>12}")
99
+ for name, r in rows.items():
100
+ saved = est_tokens(r["bytes_before"] - r["bytes_after"])
101
+ print(f"{name:<24}{r['metric_scope']:>11}{r['lines']:>7}"
102
+ f"{r['bytes_before']:>12}"
103
+ f"{r['bytes_after']:>12}{saved:>12}")
104
+ print(f"\nfile: {_fmt_bytes(res['bytes_before'])} -> "
105
+ f"{_fmt_bytes(res['bytes_after'])} "
106
+ f"({res['file_pct']:.1f}% smaller, "
107
+ f"{res['file_bytes_freed']} bytes freed)")
108
+ print(f"context: {res['context_est_tokens_freed']} estimated tokens freed")
109
+ if res["validation_errors"]:
110
+ print("VALIDATION FAILED:", file=sys.stderr)
111
+ for e in res["validation_errors"]:
112
+ print(" -", e, file=sys.stderr)
113
+ print(f"verdict: {res['verdict']}")
114
+
115
+
116
+ def cmd_audit(path):
117
+ with open(path, "rb") as f:
118
+ data = f.read()
119
+ results = audit(data)
120
+ width = max(len(lvl) for lvl, _ in results)
121
+ for lvl, line in results:
122
+ print(f"{lvl:<{width}} {line}")
123
+ return 1 if any(lvl == "FAIL" for lvl, _ in results) else 0
124
+
125
+
126
+ def cmd_report(path):
127
+ with open(path, "rb") as f:
128
+ data = f.read()
129
+ lines = load_lines(data)
130
+ n = sum(1 for ln in lines if ln.obj is not None)
131
+ print(f"{os.path.basename(path)}: {len(data)} bytes, {len(lines)} lines "
132
+ f"({n} JSON), est≈{est_tokens(len(data))} tokens")
133
+ return 0
134
+
135
+
136
+ def main(argv=None):
137
+ utf8_stdio()
138
+ argv = list(sys.argv[1:] if argv is None else argv)
139
+
140
+ # Serialized-transcript hook mode: invoked with no argv and one hook JSON
141
+ # payload on stdin. Semantic hosts use their native compaction event adapter
142
+ # instead; direct manual execution remains limited to inactive copies.
143
+ if not argv:
144
+ raw = ""
145
+ try:
146
+ if not sys.stdin.isatty():
147
+ raw = sys.stdin.read()
148
+ except Exception: # noqa: BLE001
149
+ raw = ""
150
+ if raw.strip():
151
+ try:
152
+ payload = json.loads(raw)
153
+ except Exception: # noqa: BLE001
154
+ payload = {}
155
+ if isinstance(payload, dict) and payload.get("hook_event_name"):
156
+ # CONFIRM-09 staleness-warn: runs unconditionally on
157
+ # UserPromptSubmit (independent of CODEARBITER_PRUNE, which
158
+ # gates hook_run's own pruning logic below) — a WARN-only
159
+ # check must not depend on an unrelated opt-in env var.
160
+ if payload.get("hook_event_name") == "UserPromptSubmit":
161
+ staleness_check(payload)
162
+ # This codec below rewrites Claude-Code-format transcript
163
+ # JSONL; semantic hosts plan through _prunepolicy without
164
+ # rewriting an active session. The staleness-warn
165
+ # above already ran — it reads the .codearbiter audit logs,
166
+ # not the transcript, so it is host-neutral (ADR-0011).
167
+ import _hooklib
168
+ if not _hooklib.get_host().has_prunable_transcript:
169
+ return 0
170
+ return hook_run(payload)
171
+ print("usage: prune-transcript.py <path|session-id> [--execute] ...",
172
+ file=sys.stderr)
173
+ return 2
174
+
175
+ if argv and argv[0] in ("audit", "report"):
176
+ if len(argv) < 2:
177
+ sys.exit(f"usage: prune-transcript.py {argv[0]} <path>")
178
+ path = resolve(argv[1])
179
+ return cmd_audit(path) if argv[0] == "audit" else cmd_report(path)
180
+
181
+ ap = argparse.ArgumentParser(prog="prune-transcript.py")
182
+ ap.add_argument("target", help="transcript path or session id")
183
+ ap.add_argument("--execute", action="store_true",
184
+ help="write changes (default: dry-run)")
185
+ ap.add_argument("--tier", default=None, choices=["gentle", "standard", "aggressive"])
186
+ ap.add_argument("--strategies", default=None,
187
+ help="explicit comma-separated strategy list (overrides --tier)")
188
+ ap.add_argument("--keep-recent", type=int, default=None)
189
+ ap.add_argument("--max-bytes", type=int, default=None)
190
+ args = ap.parse_args(argv)
191
+
192
+ path = resolve(args.target)
193
+ cfg = Config.from_env()
194
+ if args.tier:
195
+ cfg.tier = args.tier
196
+ if args.strategies:
197
+ cfg.strategies = [s.strip() for s in args.strategies.split(",") if s.strip()]
198
+ if args.keep_recent is not None:
199
+ cfg.keep_recent = args.keep_recent
200
+ if args.max_bytes is not None:
201
+ cfg.max_bytes = args.max_bytes
202
+ cfg.execute = args.execute
203
+
204
+ if cfg.execute and is_live(path, cfg.live_secs):
205
+ sys.exit(f"refusing --execute on a live transcript (modified < {cfg.live_secs}s ago).\n"
206
+ "Run against a copy, or let the hook handle the live session.")
207
+
208
+ session = os.path.splitext(os.path.basename(path))[0]
209
+ res = _engine_run(path, cfg, session=session)
210
+ print_report(res)
211
+ return 0 if not res["validation_errors"] else 1
212
+
213
+
214
+ def run(host, argv=None):
215
+ """Host-seam entry point (ADR-0011): the __main__ guard calls this with the
216
+ plugin's loaded Host. Delegates to main(argv) and returns its exit code,
217
+ exactly as the old `sys.exit(main())` guard propagated it.
218
+
219
+ Wires `host` live (#257): primes `_hooklib`'s process-cached Host via
220
+ `set_host()` BEFORE main() runs, so main()'s `_hooklib.get_host()` call
221
+ (hook-mode path) resolves to the SAME instance the caller passed here —
222
+ no second `hostapi.load_host()`, and `run(fake_host)` genuinely exercises
223
+ `fake_host`. Local import mirrors this module's existing lazy `_hooklib`
224
+ import convention (staleness_check) — the CLI (argv) paths never touch
225
+ _hooklib, so it stays off that path's import surface."""
226
+ import _hooklib
227
+ _hooklib.set_host(host)
228
+ return main(argv)
229
+
230
+
231
+ if __name__ == "__main__":
232
+ sys.exit(run(hostapi.load_host()) or 0)