@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,262 @@
1
+ #!/usr/bin/env python3
2
+ # codeArbiter - path SCOPE detection: which repo paths are database migrations
3
+ # (H-14), CI/CD workflow files (H-15), and deployment / IaC manifests (H-16),
4
+ # including the project's own overrides declared in security-controls.md.
5
+ #
6
+ # Extracted from _hooklib (issue #321, architecture-002) as slice 3. The
7
+ # cleanest seam of the four, measured the same way as the others: this cluster
8
+ # references NOTHING from the rest of _hooklib, and NOTHING in the rest of
9
+ # _hooklib references it. Its only dependency is norm_path, already on the
10
+ # _pathnorm floor. Zero edges in either direction.
11
+ #
12
+ # WHY THESE BELONG TOGETHER: they are one mechanism wearing three labels. All
13
+ # three predicates are `path_in_globs(rel, root, <DEFAULTS>, <DECL_RE>)` over the
14
+ # same glob compiler, the same security-controls.md reader, and the same two
15
+ # caches - so a change to how a project DECLARES a scope (the `<scope>-paths`
16
+ # lines) lands in one place instead of three. The defaults differ; the machinery
17
+ # does not.
18
+ #
19
+ # The caches are deliberate and mtime-keyed: every Write/Edit hook calls these,
20
+ # and re-reading plus re-compiling security-controls.md per call showed up as
21
+ # real latency on a large repo.
22
+ #
23
+ # _hooklib re-exports every public name below, so no consumer changed and the
24
+ # pre-existing hook suites prove parity without moving.
25
+
26
+ from __future__ import annotations
27
+
28
+ import os
29
+ import re
30
+
31
+ from _pathnorm import norm_path
32
+
33
+
34
+ # Migration-path detection (H-14). Shared by migration-pass.py (the producer)
35
+ # and pre-bash.py (the backstop) so the two never drift on what counts as a
36
+ # migration. Default globs cover the common ORM/migration ecosystems; a project
37
+ # extends or narrows the set via a `migration-paths` block in
38
+ # security-controls.md. `**` matches any run of path segments (including none);
39
+ # `*`/`?` stay within one segment.
40
+ MIGRATION_DEFAULT_GLOBS = (
41
+ "**/migrations/**",
42
+ "**/migrate/**",
43
+ "**/db/migrate/**",
44
+ "**/alembic/versions/*.py",
45
+ "**/prisma/migrations/**",
46
+ )
47
+ _MIG_DECL_RE = re.compile(
48
+ r"<!--\s*migration-paths\s*-->(.*?)<!--\s*/migration-paths\s*-->", re.S | re.I)
49
+
50
+ # CI/CD workflow detection (H-15, #73). Advisory only — no commit gate; the
51
+ # defaults cover the common CI ecosystems and a project extends/narrows them via
52
+ # a `ci-paths` block in security-controls.md (same `+`/`-` grammar as migrations).
53
+ CI_DEFAULT_GLOBS = (
54
+ ".github/workflows/**",
55
+ ".circleci/**",
56
+ "**/.gitlab-ci.yml",
57
+ "**/Jenkinsfile",
58
+ "**/azure-pipelines.yml",
59
+ "**/bitbucket-pipelines.yml",
60
+ )
61
+ _CI_DECL_RE = re.compile(
62
+ r"<!--\s*ci-paths\s*-->(.*?)<!--\s*/ci-paths\s*-->", re.S | re.I)
63
+
64
+ # Deployment / IaC detection (H-16, #73). Advisory only. Defaults cover the
65
+ # common container/orchestration/IaC manifests; extend/narrow via a
66
+ # `deploy-paths` block in security-controls.md.
67
+ DEPLOY_DEFAULT_GLOBS = (
68
+ "**/Dockerfile",
69
+ "**/Dockerfile.*",
70
+ "**/docker-compose*.yml",
71
+ "**/docker-compose*.yaml",
72
+ "**/*.tf",
73
+ "**/*.tfvars",
74
+ "**/k8s/**",
75
+ "**/helm/**",
76
+ "**/kustomization.yaml",
77
+ "**/kustomization.yml",
78
+ "**/Procfile",
79
+ )
80
+ _DEPLOY_DECL_RE = re.compile(
81
+ r"<!--\s*deploy-paths\s*-->(.*?)<!--\s*/deploy-paths\s*-->", re.S | re.I)
82
+
83
+
84
+ def _glob_to_re(glob):
85
+ """Compile a forward-slash glob into a full-path regex. `**/` is an optional
86
+ run of leading segments, `**` is any chars, `*`/`?` stay within a segment."""
87
+ g = norm_path(glob)
88
+ out, i = ["^"], 0
89
+ while i < len(g):
90
+ if g[i:i + 3] == "**/":
91
+ out.append("(?:.*/)?")
92
+ i += 3
93
+ elif g[i:i + 2] == "**":
94
+ out.append(".*")
95
+ i += 2
96
+ elif g[i] == "*":
97
+ out.append("[^/]*")
98
+ i += 1
99
+ elif g[i] == "?":
100
+ out.append("[^/]")
101
+ i += 1
102
+ else:
103
+ out.append(re.escape(g[i]))
104
+ i += 1
105
+ out.append("$")
106
+ return re.compile("".join(out))
107
+
108
+
109
+ # performance-002: the DEFAULT glob tuples are module constants, so compile each
110
+ # to a regex ONCE at module load instead of per glob per path_in_globs() call.
111
+ # A single post-write-edit.py invocation otherwise recompiled up to 44 regexes
112
+ # (5 migration + 6 CI + 11 deploy x the calls that hit them). These compiled
113
+ # tuples line up 1:1 with their string tuples; the matcher uses them directly
114
+ # for the defaults and only compiles the per-controls custom globs on demand.
115
+ _MIGRATION_DEFAULT_RES = tuple(_glob_to_re(g) for g in MIGRATION_DEFAULT_GLOBS)
116
+ _CI_DEFAULT_RES = tuple(_glob_to_re(g) for g in CI_DEFAULT_GLOBS)
117
+ _DEPLOY_DEFAULT_RES = tuple(_glob_to_re(g) for g in DEPLOY_DEFAULT_GLOBS)
118
+
119
+ # Map each default string tuple to its precompiled regex tuple, so the matcher
120
+ # can look up the right precompiled set from the `defaults` argument alone
121
+ # (preserving the existing public signatures of scope_globs/path_in_globs).
122
+ _DEFAULT_RES_BY_GLOBS = {
123
+ MIGRATION_DEFAULT_GLOBS: _MIGRATION_DEFAULT_RES,
124
+ CI_DEFAULT_GLOBS: _CI_DEFAULT_RES,
125
+ DEPLOY_DEFAULT_GLOBS: _DEPLOY_DEFAULT_RES,
126
+ }
127
+
128
+
129
+ # performance-001: hooks are EPHEMERAL single-shot processes (one invocation
130
+ # then exit), so a module-level cache lives for exactly one invocation — there
131
+ # is NO cross-invocation persistence. Within that one process, scope_globs reads
132
+ # security-controls.md on every is_migration_path/is_ci_path/is_deploy_path call
133
+ # (2-3 reads per hook). Cache the controls text keyed by (root, mtime) so a hit
134
+ # skips the read; the mtime key keeps it correct even on an intra-process change
135
+ # (the file is re-read when its mtime moves), and keys the absent-file state too.
136
+ _CONTROLS_CACHE = {}
137
+
138
+
139
+ def _controls_mtime(root):
140
+ """mtime of `root`'s security-controls.md, or None when absent/unreadable.
141
+ The cache key — distinct mtimes (and the None absent-state) bust the cache."""
142
+ try:
143
+ return os.path.getmtime(
144
+ os.path.join(root, ".codearbiter", "security-controls.md"))
145
+ except Exception: # noqa: BLE001 — no controls file -> None (defaults only)
146
+ return None
147
+
148
+
149
+ def _read_controls(root):
150
+ """The repo's security-controls.md text, or "" when absent/unreadable.
151
+
152
+ Process-cached keyed by (root, mtime): a cache hit skips the file read, and
153
+ the mtime component invalidates the entry whenever the file changes (or is
154
+ created/removed), so verdicts are unchanged. Single-shot hook process only —
155
+ no cross-invocation persistence."""
156
+ mtime = _controls_mtime(root)
157
+ key = (root, mtime)
158
+ cached = _CONTROLS_CACHE.get(key)
159
+ if cached is not None:
160
+ return cached[0]
161
+ try:
162
+ with open(os.path.join(root, ".codearbiter", "security-controls.md"),
163
+ encoding="utf-8", errors="replace") as f:
164
+ text = f.read()
165
+ except Exception: # noqa: BLE001 — no controls file -> defaults only
166
+ text = ""
167
+ # Cache the text AND the compiled custom globs per scope (filled lazily by
168
+ # scope_globs) under the same mtime key, so a custom-glob set compiles at
169
+ # most once per (root, mtime) instead of once per path_in_globs() call.
170
+ _CONTROLS_CACHE[key] = (text, {})
171
+ return text
172
+
173
+
174
+ def _custom_re_cache(root):
175
+ """The per-(root, mtime) dict that caches compiled custom-glob regexes for
176
+ this controls revision. Populated lazily by scope_globs. Returns a throwaway
177
+ dict only if the controls entry is somehow missing (defensive; the read
178
+ above always seeds it first)."""
179
+ entry = _CONTROLS_CACHE.get((root, _controls_mtime(root)))
180
+ return entry[1] if entry is not None else {}
181
+
182
+
183
+ def scope_globs(root, defaults, decl_re):
184
+ """(includes, excludes) for one scope category: the built-in `defaults` plus
185
+ any declaration block matched by `decl_re` in security-controls.md
186
+ (`+ glob` extends, `- glob` excludes). Shared by every path-glob scope
187
+ detector (migration/CI/deploy) so they never drift on the grammar."""
188
+ includes, excludes = list(defaults), []
189
+ m = decl_re.search(_read_controls(root))
190
+ if not m:
191
+ return includes, excludes
192
+ for ln in m.group(1).splitlines():
193
+ ln = ln.strip()
194
+ if ln.startswith("+ "):
195
+ includes.append(ln[2:].strip())
196
+ elif ln.startswith("- "):
197
+ excludes.append(ln[2:].strip())
198
+ return includes, excludes
199
+
200
+
201
+ def _scope_res(root, defaults, decl_re):
202
+ """(include_res, exclude_res) as compiled regexes for one scope category.
203
+ Default globs use the module-precompiled regexes (zero per-call compilation);
204
+ any per-controls custom globs are compiled at most once per (root, mtime) and
205
+ cached. Equivalent to compiling each string from scope_globs() — verdicts are
206
+ identical; only the regex work is amortised."""
207
+ includes, excludes = scope_globs(root, defaults, decl_re)
208
+ default_res = _DEFAULT_RES_BY_GLOBS.get(defaults)
209
+ if default_res is None:
210
+ # Unknown defaults set (no precompiled tuple) — compile everything.
211
+ return ([_glob_to_re(g) for g in includes],
212
+ [_glob_to_re(g) for g in excludes])
213
+ # Defaults occupy the head of `includes` (scope_globs builds list(defaults)
214
+ # then appends customs); reuse the precompiled regexes for that head and
215
+ # compile only the trailing customs. Excludes are all custom.
216
+ custom_cache = _custom_re_cache(root)
217
+
218
+ def _compile(g):
219
+ r = custom_cache.get(g)
220
+ if r is None:
221
+ r = _glob_to_re(g)
222
+ custom_cache[g] = r
223
+ return r
224
+
225
+ n = len(defaults)
226
+ include_res = list(default_res) + [_compile(g) for g in includes[n:]]
227
+ exclude_res = [_compile(g) for g in excludes]
228
+ return include_res, exclude_res
229
+
230
+
231
+ def path_in_globs(rel, root, defaults, decl_re):
232
+ """True iff `rel` (a repo-relative path) matches an include glob and no
233
+ exclude glob for the given scope category. Excludes win — the false-positive
234
+ escape hatch. The one matcher behind is_migration_path/is_ci_path/
235
+ is_deploy_path."""
236
+ rel = norm_path(rel).lstrip("/")
237
+ include_res, exclude_res = _scope_res(root, defaults, decl_re)
238
+ if any(r.match(rel) for r in exclude_res):
239
+ return False
240
+ return any(r.match(rel) for r in include_res)
241
+
242
+
243
+ def migration_globs(root):
244
+ """(includes, excludes) for migration detection: defaults plus any
245
+ `migration-paths` declaration in security-controls.md."""
246
+ return scope_globs(root, MIGRATION_DEFAULT_GLOBS, _MIG_DECL_RE)
247
+
248
+
249
+ def is_migration_path(rel, root):
250
+ """True iff `rel` is a database migration (H-14). Excludes win — the
251
+ escape hatch for a project whose `migrations/` dir holds non-DB files."""
252
+ return path_in_globs(rel, root, MIGRATION_DEFAULT_GLOBS, _MIG_DECL_RE)
253
+
254
+
255
+ def is_ci_path(rel, root):
256
+ """True iff `rel` is a CI/CD workflow file (H-15, advisory)."""
257
+ return path_in_globs(rel, root, CI_DEFAULT_GLOBS, _CI_DECL_RE)
258
+
259
+
260
+ def is_deploy_path(rel, root):
261
+ """True iff `rel` is a deployment / IaC manifest (H-16, advisory)."""
262
+ return path_in_globs(rel, root, DEPLOY_DEFAULT_GLOBS, _DEPLOY_DECL_RE)
@@ -0,0 +1,278 @@
1
+ #!/usr/bin/env python3
2
+ # codeArbiter — statusline segment renderers (extracted from statusline.py,
3
+ # architecture-004).
4
+ #
5
+ # Owns the individual content segments the box assembles into rows: the
6
+ # context-window bar, the 5h/7d rate-limit cells, the model/effort pill, the
7
+ # Session/Today usage row, the diff-churn / PR / transcript-pruner segments, and
8
+ # the /dev redshift recolor. NOT the update-available marker (`seg_update` /
9
+ # `plugin_root_for_render` stay in statusline.py — a test patches
10
+ # `statusline.plugin_root_for_render` directly, so that seam must resolve through
11
+ # the entry-point module's own globals, not a lib's).
12
+ #
13
+ # Design principles (mirroring _ledgerlib.py):
14
+ # - Stdlib only; no third-party imports ever.
15
+ # - Zero side effects at import time.
16
+ # - Never raise on malformed input — callers already wrap these in safe(), but
17
+ # each function still degrades on bad data rather than relying solely on that.
18
+ #
19
+ # Public API:
20
+ # seg_ctx_lines(data, w) -> [str, str]
21
+ # seg_window_cells(data) -> list[str]
22
+ # seg_window_inline(data) -> str
23
+ # model_pill(model, effort="") -> str
24
+ # usage_row(label, tin, tout, cost, trail="") -> str
25
+ # seg_lines(data) -> str|None
26
+ # seg_pr(data) -> str|None
27
+ # seg_prune(data, sid) -> str|None
28
+ # redshift(s) -> str
29
+
30
+ import json
31
+ import os
32
+ import re
33
+ import time
34
+
35
+ try:
36
+ import _colorlib
37
+ _fg, _bg = _colorlib.fg, _colorlib.bg
38
+ RESET, BOLD = _colorlib.RESET, _colorlib.BOLD
39
+ V0, V2, V3 = _colorlib.V0, _colorlib.V2, _colorlib.V3
40
+ GREY, WHITE, OK, WARN, DANGER, PILL_FG = (_colorlib.GREY, _colorlib.WHITE, _colorlib.OK,
41
+ _colorlib.WARN, _colorlib.DANGER, _colorlib.PILL_FG)
42
+ _VBAR = _colorlib.V
43
+ BFULL, BEMPTY, DN, UP, ARR, BOLT = (_colorlib.BFULL, _colorlib.BEMPTY, _colorlib.DN,
44
+ _colorlib.UP, _colorlib.ARR, _colorlib.BOLT)
45
+ DIM = _colorlib.DIM
46
+ except Exception: # pragma: no cover — never let an import break the statusline
47
+ _colorlib = None
48
+ RESET = BOLD = DIM = ""
49
+ V0 = V2 = V3 = GREY = WHITE = OK = WARN = DANGER = PILL_FG = ""
50
+ _VBAR = "|"
51
+ BFULL, BEMPTY, DN, UP, ARR, BOLT = "#", ".", "v", "^", ">", "!"
52
+
53
+ def _bg(r, g, b):
54
+ return ""
55
+
56
+
57
+ def sync_palette():
58
+ """Refresh captured color exports after _colorlib runtime activation."""
59
+ global RESET, BOLD, DIM, V0, V2, V3, GREY, WHITE, OK, WARN, DANGER, PILL_FG
60
+ if _colorlib is not None:
61
+ RESET, BOLD, DIM = _colorlib.RESET, _colorlib.BOLD, _colorlib.DIM
62
+ V0, V2, V3 = _colorlib.V0, _colorlib.V2, _colorlib.V3
63
+ GREY, WHITE, OK = _colorlib.GREY, _colorlib.WHITE, _colorlib.OK
64
+ WARN, DANGER, PILL_FG = _colorlib.WARN, _colorlib.DANGER, _colorlib.PILL_FG
65
+
66
+ try:
67
+ import _fmtlib
68
+ fmt_tok, usd_fine, human_dur, to_epoch = (_fmtlib.fmt_tok, _fmtlib.usd_fine,
69
+ _fmtlib.human_dur, _fmtlib.to_epoch)
70
+ num = _fmtlib.num
71
+ except Exception: # pragma: no cover — never let an import break the statusline
72
+ _fmtlib = None
73
+
74
+ def num(x, default=0.0):
75
+ try:
76
+ return float(x)
77
+ except (TypeError, ValueError):
78
+ return default
79
+
80
+ def fmt_tok(n):
81
+ return str(int(num(n)))
82
+
83
+ def usd_fine(c):
84
+ return f"${num(c):.2f}"
85
+
86
+ def human_dur(secs):
87
+ return f"{int(max(0, secs))}s"
88
+
89
+ def to_epoch(v):
90
+ return None
91
+
92
+
93
+ def get(d, *path, default=None):
94
+ cur = d
95
+ for k in path:
96
+ if not isinstance(cur, dict) or k not in cur or cur[k] is None:
97
+ return default
98
+ cur = cur[k]
99
+ return cur
100
+
101
+
102
+ # --------------------------------------------------------------------------- context / rate limits
103
+ def seg_ctx_lines(data, w):
104
+ """Two lines of context-window detail for the right column: a gradient bar +
105
+ used% on top, then resident/window + headroom-to-compaction below. The bar
106
+ scales to the available width w."""
107
+ cw = get(data, "context_window", default={}) or {}
108
+ pf = num(cw.get("used_percentage"), None)
109
+ if pf is None:
110
+ return [f"{GREY}ctx --{RESET}", ""]
111
+ pctf = max(0.0, min(100.0, pf))
112
+ size_raw = cw.get("context_window_size")
113
+ has_size = size_raw is not None
114
+ size = int(num(size_raw, 200000)) or 200000
115
+ win = "1M" if size >= 1_000_000 else f"{size // 1000}K"
116
+ resident = round(pctf / 100.0 * size)
117
+ col = V2 if pctf < 75 else (WARN if pctf < 90 else DANGER)
118
+ barw = max(10, min(46, w - 12))
119
+ filled = max(0, min(barw, round(pctf / 100 * barw)))
120
+ # below WARN: the violet sheen; at/over the 75/90 thresholds the FILL itself
121
+ # carries the threshold color so budget pressure shows on the dominant
122
+ # element, not just the trailing % glyph.
123
+ fill = _colorlib.gradient_h(BFULL * filled, max(1, filled)) if (pctf < 75 and _colorlib) else f"{col}{BFULL * filled}"
124
+ bar = fill + f"{V0}" + BEMPTY * (barw - filled) + RESET
125
+ pdisp = "<1%" if 0 < pctf < 1 else f"{pctf:.0f}%"
126
+ line1 = f"{GREY}ctx{RESET} {bar} {col}{pdisp}{RESET}"
127
+ thresh = num(os.environ.get("CODEARBITER_COMPACT_AT"), 92.0)
128
+ head = thresh - pctf
129
+ if head <= 0:
130
+ comp = f"{DANGER}{BOLT} compact imminent{RESET}"
131
+ else:
132
+ ccol = OK if head > 30 else (WARN if head > 12 else DANGER)
133
+ comp = f"{GREY}{BOLT} compact{RESET} {ccol}~{head:.0f}%{RESET}"
134
+ # only assert resident/window when the host actually sent the window size;
135
+ # otherwise show just the (percentage-based) compaction headroom, no fake /200K.
136
+ line2 = f"{DIM}{fmt_tok(resident)} / {win}{RESET} {comp}" if has_size else comp
137
+ return [line1, line2]
138
+
139
+
140
+ def seg_window_cells(data):
141
+ """5h / 7d rate-limit cells: used% + reset countdown. The countdown shows when
142
+ the host sends rate_limits.*.resets_at (ISO string or epoch); absent that, the
143
+ cell still shows the used%. Rendered wherever rate_limits exists."""
144
+ cells = []
145
+ now = time.time()
146
+ for key, lbl in (("five_hour", "5h"), ("seven_day", "7d")):
147
+ p = get(data, "rate_limits", key, "used_percentage")
148
+ if p is None:
149
+ continue
150
+ pf = num(p, None)
151
+ if pf is None:
152
+ continue
153
+ c = V2 if pf < 75 else (WARN if pf < 90 else DANGER)
154
+ cell = f"{GREY}{lbl}{RESET} {c}{pf:.0f}%{RESET}"
155
+ r = to_epoch(get(data, "rate_limits", key, "resets_at"))
156
+ if r:
157
+ dt = r - now
158
+ # a reset already in the past is stale data, not "rolls over now"
159
+ tail = f"{WHITE}{human_dur(dt)}{RESET}" if dt > 0 else f"{GREY}--{RESET}"
160
+ cell += f" {GREY}{ARR}{RESET} {tail}"
161
+ cells.append(cell)
162
+ return cells
163
+
164
+
165
+ def seg_window_inline(data):
166
+ """Compact single-string rate-limit readout for the header line."""
167
+ cells = seg_window_cells(data)
168
+ return f" {V0}·{RESET} ".join(cells) if cells else ""
169
+
170
+
171
+ EFF_DISP = {"low": "Low", "medium": "Medium", "high": "High",
172
+ "xhigh": "XHigh", "max": "Max", "ultracode": "Ultracode"}
173
+
174
+
175
+ def model_pill(model, effort=""):
176
+ """A gradient-filled pill carrying the model AND its effort level, e.g.
177
+ " Opus 4.8 │ Ultracode ", keyed by family — Fable gold, Opus violet, Sonnet
178
+ blue, Haiku green. The background ramps dark->bright across the pill (a sheen
179
+ matching the box). Plain block ends (half-circle caps don't align in a
180
+ monospace cell)."""
181
+ m = str(model)
182
+ ml = m.lower()
183
+ if "fable" in ml:
184
+ c = (235, 184, 90) # gold — the tier above Opus
185
+ elif "opus" in ml:
186
+ c = (188, 120, 255) # violet
187
+ elif "sonnet" in ml:
188
+ c = (96, 174, 235) # blue
189
+ elif "haiku" in ml:
190
+ c = (120, 220, 150) # green
191
+ else:
192
+ c = (150, 150, 162) # grey (unknown)
193
+ eff = str(effort).strip().lower()
194
+ body = f" {m} {_VBAR} {EFF_DISP.get(eff, eff.capitalize())} " if eff else f" {m} "
195
+ lo = tuple(int(v * 0.5) for v in c) # dark start of the sheen
196
+ n = max(1, len(body) - 1)
197
+ cells = []
198
+ for idx, ch in enumerate(body):
199
+ t = idx / n
200
+ rr = int(lo[0] + (c[0] - lo[0]) * t)
201
+ gg = int(lo[1] + (c[1] - lo[1]) * t)
202
+ bb = int(lo[2] + (c[2] - lo[2]) * t)
203
+ cells.append(f"{_bg(rr, gg, bb)}{PILL_FG}{BOLD}{ch}")
204
+ return "".join(cells) + RESET
205
+
206
+
207
+ def usage_row(label, tin, tout, cost, trail=""):
208
+ """One row of the Session/Today mini-table: label │ ↓in ↑out │ $cost. in/out are
209
+ fresh (sent) tokens; `cost` is Claude Code's real session cost (cost.total_cost_usd),
210
+ not an estimate. `trail` carries extras (session age, burn sparkline)."""
211
+ base = (f"{GREY}{label:<7}{RESET} {V0}{_VBAR}{RESET} "
212
+ f"{V2}{DN}{RESET} {WHITE}{fmt_tok(tin):>6}{RESET} "
213
+ f"{V2}{UP}{RESET} {WHITE}{fmt_tok(tout):>6}{RESET} {V0}{_VBAR}{RESET} "
214
+ f"{OK}{usd_fine(cost)}{RESET}")
215
+ return base + (f" {trail}" if trail else "")
216
+
217
+
218
+ def seg_lines(data):
219
+ add = get(data, "cost", "total_lines_added")
220
+ rem = get(data, "cost", "total_lines_removed")
221
+ if add is None and rem is None:
222
+ return None
223
+ return f"{GREY}diff{RESET} {OK}+{fmt_tok(add)}{RESET}{GREY}/{RESET}{DANGER}-{fmt_tok(rem)}{RESET}"
224
+
225
+
226
+ def seg_pr(data):
227
+ num_ = get(data, "pr", "number")
228
+ if not num_:
229
+ return None
230
+ state = str(get(data, "pr", "state") or "").lower()
231
+ checks = str(get(data, "pr", "checks") or "").lower()
232
+ mark, col = "", V2
233
+ if checks in ("pass", "passing", "success", "green"):
234
+ mark, col = "✓", OK
235
+ elif checks in ("fail", "failing", "error", "red"):
236
+ mark, col = "✕", DANGER
237
+ elif checks in ("pending", "running", "queued"):
238
+ mark, col = "…", WARN
239
+ scol = OK if state == "open" else (V3 if state == "merged" else GREY)
240
+ tail = f" {col}{mark}{RESET}" if mark else ""
241
+ return f"{GREY}PR{RESET} {scol}#{num_}{RESET}{tail}"
242
+
243
+
244
+ def seg_prune(data, sid):
245
+ """Transcript-pruner indicator: model-context reduction and age of the last
246
+ prune for this session, read from ~/.codearbiter/prune-state.json (written
247
+ by prune-transcript.py). Fail-soft — returns None on any problem, and never
248
+ raises, so it can never break statusline rendering."""
249
+ if not sid:
250
+ return None
251
+ try:
252
+ p = os.path.join(os.path.expanduser("~"), ".codearbiter", "prune-state.json")
253
+ with open(p, encoding="utf-8") as f:
254
+ st = json.load(f)
255
+ rec = st.get(sid) if isinstance(st, dict) else None
256
+ if not isinstance(rec, dict):
257
+ return None
258
+ file_freed = num(rec.get("file_bytes_freed", rec.get("freed_bytes")), 0)
259
+ if file_freed <= 0:
260
+ return None
261
+ age = human_dur(max(0, time.time() - num(rec.get("last_run_ts"), time.time())))
262
+ if "context_est_tokens_freed" not in rec:
263
+ pct = num(rec.get("file_pct", rec.get("pct")), 0)
264
+ return f"{GREY}✂{RESET} {WHITE}file:{pct:.0f}%{RESET} {GREY}{age}{RESET}"
265
+ context_tokens = max(0, num(rec.get("context_est_tokens_freed"), 0))
266
+ return (f"{GREY}✂{RESET} {WHITE}ctx:{fmt_tok(context_tokens)}{RESET} "
267
+ f"{GREY}{age}{RESET}")
268
+ except Exception: # noqa: BLE001
269
+ return None
270
+
271
+
272
+ def redshift(s):
273
+ """/dev tell: recolor every truecolor SGR to a red of matching brightness, so the
274
+ WHOLE bar turns alarm-red — a glaring sign orchestration is suspended."""
275
+ def repl(m):
276
+ lum = max(int(m.group(2)), int(m.group(3)), int(m.group(4)))
277
+ return f"\033[{m.group(1)};2;{min(255, 96 + lum)};{lum // 6};{lum // 7}m"
278
+ return re.sub(r"\033\[(38|48);2;(\d+);(\d+);(\d+)m", repl, s)