@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,435 @@
1
+ #!/usr/bin/env python3
2
+ # codeArbiter — statusline wire-up. Writes (or removes) the statusLine.command
3
+ # in the user's ~/.claude/settings.json so the renderer at hooks/statusline.py
4
+ # runs everywhere.
5
+ #
6
+ # A plugin cannot own a statusLine, and ${CLAUDE_PLUGIN_ROOT} is NOT expanded
7
+ # inside settings.json, so the absolute path must be resolved and written at
8
+ # install time — which is exactly what this does.
9
+ #
10
+ # Usage:
11
+ # python wire-statusline.py install # back up any existing line, wire ours
12
+ # python wire-statusline.py uninstall # restore the backed-up line (or remove)
13
+ # python wire-statusline.py status # report current wiring, change nothing
14
+ # python wire-statusline.py refresh # self-heal a stale ca-owned path only
15
+ # # (no-op unless ours AND changed) — run
16
+ # # from SessionStart so a plugin update
17
+ # # re-points the pin automatically
18
+ #
19
+ # Options (mainly for testing):
20
+ # --settings PATH target settings.json (default: ~/.claude/settings.json)
21
+ # --plugin-root PATH plugin root (default: $CLAUDE_PLUGIN_ROOT or this script's parent)
22
+ # --interp CMD interpreter token for the command
23
+ # (default: this Python's own absolute path, i.e. sys.executable)
24
+
25
+ import argparse
26
+ import json
27
+ import os
28
+ import re
29
+ import sys
30
+
31
+ # Self-sufficient regardless of how this file is loaded (direct `python
32
+ # wire-statusline.py` run, or importlib spec_from_file_location as
33
+ # session-start.py and the test suite both do) — always resolve _hooklib
34
+ # relative to THIS file rather than relying on the caller's sys.path state.
35
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
36
+ import _durabilitylib # noqa: E402 — "may this path be pinned?" (see below)
37
+ import _hooklib # noqa: E402
38
+ import hostapi # noqa: E402 — host seam (ADR-0011): plugin-root resolution
39
+
40
+ BACKUP_KEY = "_codearbiterStatuslineBackup" # holds the prior statusLine value
41
+ OWNER_KEY = "_codearbiterStatuslineOwner" # exact command last written by us
42
+ SPINNER_BACKUP_KEY = "_codearbiterSpinnerVerbsBackup" # holds prior spinnerVerbs
43
+ _COMMAND_RE = re.compile(
44
+ r'''^\s*(?P<interp>"[^"]*"|'[^']*'|\S+)\s+'''
45
+ r'''(?P<script>"[^"]*"|'[^']*'|\S+)\s*$''')
46
+ _PYTHON_EXE_RE = re.compile(r"python(?:\d+(?:\.\d+)*)?(?:\.exe)?$")
47
+
48
+ ARBITER_SPINNER_VERBS = {
49
+ "mode": "replace",
50
+ "verbs": [
51
+ "Deliberating",
52
+ "Weighing the evidence",
53
+ "Consulting precedent",
54
+ "Reviewing the docket",
55
+ "Summoning the council",
56
+ "In chambers",
57
+ "Examining exhibits",
58
+ "Drafting the ruling",
59
+ "Calling order",
60
+ "Overruling prior context",
61
+ "Sustaining the objection",
62
+ "Issuing findings",
63
+ "Cross-examining the codebase",
64
+ "Invoking arbitration",
65
+ "Rendering judgment",
66
+ ],
67
+ }
68
+
69
+
70
+ def plugin_root(opt):
71
+ if opt:
72
+ return os.path.abspath(opt)
73
+ # Host seam (ADR-0011): CLAUDE_PLUGIN_ROOT then this script's parent —
74
+ # exactly the prior inline lookup (hostapi.py lives in the same hooks/ dir,
75
+ # so its file-relative fallback names the same root). get_host() (#257),
76
+ # not a direct hostapi.load_host(): resolves the SAME Host run(host)
77
+ # injected instead of triggering a second disk load.
78
+ return os.path.abspath(_hooklib.get_host().plugin_root())
79
+
80
+
81
+ def settings_path(opt):
82
+ if opt:
83
+ return os.path.abspath(opt)
84
+ return os.path.join(os.path.expanduser("~"), ".claude", "settings.json")
85
+
86
+
87
+ def default_interp(opt):
88
+ if opt:
89
+ return opt
90
+ # Prefer this Python's own absolute path: bare `python` is PATH-dependent and
91
+ # renders a blank bar when it resolves to nothing or the wrong interpreter.
92
+ if sys.executable:
93
+ return sys.executable
94
+ return "python" if os.name == "nt" else "python3"
95
+
96
+
97
+ def build_command(interp, script_abs):
98
+ # Quote BOTH tokens; the host pipes the statusline JSON to this command on
99
+ # stdin. On Windows an unquoted interp path (even one without spaces) makes
100
+ # Claude Code's statusLine runner silently emit nothing — a blank bar.
101
+ return f'"{interp}" "{script_abs}"'
102
+
103
+
104
+ def _owned_command(command):
105
+ """Recognize legacy versioned codeArbiter renderer commands.
106
+
107
+ Accept the exact two-token command shape only when the script argument ends
108
+ in codearbiter/{ca|ca-codex}/VERSION/hooks/statusline.py.
109
+ """
110
+ parsed = _python_script(command)
111
+ if parsed is None:
112
+ return False
113
+ script, windows_path = parsed
114
+ suffix = re.split(r"[\\/]", script)[-8:]
115
+ if len(suffix) < 8:
116
+ return False
117
+ expected = [".claude", "plugins", "cache", "codearbiter", None,
118
+ None, "hooks", "statusline.py"]
119
+ compared = [part.lower() for part in suffix] if windows_path else suffix
120
+ fixed = [part.lower() if windows_path and part is not None else part
121
+ for part in expected]
122
+ return (
123
+ compared[0:4] == fixed[0:4]
124
+ and compared[4] == "ca"
125
+ and bool(suffix[5])
126
+ and compared[6:] == fixed[6:]
127
+ )
128
+
129
+
130
+ def _python_script(command):
131
+ if not isinstance(command, str):
132
+ return None
133
+ match = _COMMAND_RE.fullmatch(command)
134
+ if not match:
135
+ return None
136
+ script = match.group("script")
137
+ if len(script) >= 2 and script[0] == script[-1] and script[0] in "\"'":
138
+ script = script[1:-1]
139
+ windows_path = bool(re.match(r"^[A-Za-z]:[\\/]", script)) or "\\" in script
140
+ interp = match.group("interp")
141
+ if len(interp) >= 2 and interp[0] == interp[-1] and interp[0] in "\"'":
142
+ interp = interp[1:-1]
143
+ interp_name = re.split(r"[\\/]", interp)[-1]
144
+ if not _PYTHON_EXE_RE.fullmatch(interp_name.lower() if windows_path else interp_name):
145
+ return None
146
+ return script, windows_path
147
+
148
+
149
+ def _legacy_source_command(command):
150
+ """Recognize the exact pre-metadata source-tree install path."""
151
+ parsed = _python_script(command)
152
+ if parsed is None:
153
+ return False
154
+ script, windows_path = parsed
155
+ suffix = re.split(r"[\\/]", script)[-5:]
156
+ expected = ["codeArbiter", "plugins", "ca", "hooks", "statusline.py"]
157
+ if windows_path:
158
+ suffix = [part.lower() for part in suffix]
159
+ expected = [part.lower() for part in expected]
160
+ return suffix == expected
161
+ def is_ours(statusline, settings=None):
162
+ command = statusline.get("command") if isinstance(statusline, dict) else statusline
163
+ if (isinstance(settings, dict)
164
+ and isinstance(command, str)
165
+ and settings.get(OWNER_KEY) == command):
166
+ return True
167
+ if not isinstance(statusline, dict):
168
+ if isinstance(statusline, str):
169
+ return _owned_command(statusline) or _legacy_source_command(statusline)
170
+ return False
171
+ return (_owned_command(statusline.get("command"))
172
+ or _legacy_source_command(statusline.get("command")))
173
+
174
+
175
+ def owned_statusline(command):
176
+ return {
177
+ "type": "command",
178
+ "command": command,
179
+ "padding": 0,
180
+ }
181
+
182
+
183
+ def load_settings(path):
184
+ if not os.path.exists(path):
185
+ return {}, False
186
+ try:
187
+ with open(path, encoding="utf-8") as f:
188
+ text = f.read()
189
+ return (json.loads(text) if text.strip() else {}), True
190
+ except ValueError as e:
191
+ raise SystemExit(
192
+ f"REFUSING TO WRITE: {path} is not valid JSON ({e}). "
193
+ "Fix it by hand, then re-run - I will not clobber an unparseable settings file.")
194
+
195
+
196
+ def save_settings(path, data):
197
+ """Write `data` to `path` atomically (reliability-009).
198
+
199
+ Routed through _hooklib.write_text_atomic, which stages to a UNIQUE
200
+ per-process temp file (tempfile.mkstemp, not a fixed `path + ".tmp"`
201
+ sibling name) before os.replace(). settings.json is the user's WHOLE host
202
+ configuration, not a ca-owned file: two sessions racing a heal/install
203
+ right after a plugin update previously both staged to the same fixed
204
+ `.tmp` name and could clobber each other's temp content on interleave. A
205
+ unique name per call removes that collision entirely."""
206
+ os.makedirs(os.path.dirname(path), exist_ok=True)
207
+ text = json.dumps(data, indent=2) + "\n"
208
+ _hooklib.write_text_atomic(path, text)
209
+
210
+
211
+ def cmd_status(settings, exists, script_abs):
212
+ sl = settings.get("statusLine")
213
+ print(f"settings.json: {'present' if exists else 'absent'}")
214
+ print(f"statusline.py: {script_abs} ({'found' if os.path.exists(script_abs) else 'MISSING'})")
215
+ if sl is None:
216
+ print("statusLine.command: (none set)")
217
+ else:
218
+ cmd = sl.get("command") if isinstance(sl, dict) else sl
219
+ print(f"statusLine.command: {cmd}")
220
+ print("wired to codeArbiter: " + ("YES" if is_ours(sl, settings) else "no (a different statusline owns it)"))
221
+ if BACKUP_KEY in settings:
222
+ b = settings[BACKUP_KEY]
223
+ print(f"backup on file: {b.get('command') if isinstance(b, dict) else b}")
224
+ sv = settings.get("spinnerVerbs")
225
+ if sv is None:
226
+ print("spinnerVerbs: (none set)")
227
+ else:
228
+ count = len(sv.get("verbs", [])) if isinstance(sv, dict) else "?"
229
+ owned = SPINNER_BACKUP_KEY in settings
230
+ print(f"spinnerVerbs: {count} verb(s), {'codeArbiter' if owned else 'user-owned'}")
231
+
232
+
233
+ def cmd_install(settings, path, script_abs, interp):
234
+ # Never pin a root that will not outlive the session (see refresh_if_stale's
235
+ # docstring for the defect). `install` is EXPLICIT — a human ran
236
+ # /ca:statusline, or passed `--plugin-root <worktree>` by hand — so this
237
+ # refuses LOUDLY where `refresh` degrades silently: a quiet no-op would leave
238
+ # that human believing the statusline was wired.
239
+ if _durabilitylib.is_ephemeral_path(script_abs):
240
+ raise SystemExit(
241
+ f"REFUSING TO WIRE: {script_abs} is inside a git worktree (or another "
242
+ "root that will not survive being pruned).\n"
243
+ "~/.claude/settings.json is GLOBAL and holds an ABSOLUTE path, so "
244
+ "pinning it here breaks your statusline the moment this checkout goes "
245
+ "away.\nRe-run from your real install (the plugin cache, or your main "
246
+ "checkout), or pass --plugin-root pointing at it.")
247
+ if not os.path.exists(script_abs):
248
+ raise SystemExit(f"ERROR: renderer not found at {script_abs}; nothing wired.")
249
+ new_cmd = build_command(interp, script_abs)
250
+ current = settings.get("statusLine")
251
+ if is_ours(current, settings):
252
+ # already ours: just refresh the path (e.g. after a plugin upgrade)
253
+ settings["statusLine"] = owned_statusline(new_cmd)
254
+ settings[OWNER_KEY] = new_cmd
255
+ _install_spinner_verbs(settings, refresh=True)
256
+ save_settings(path, settings)
257
+ print(f"REFRESHED codeArbiter statusline path -> {new_cmd}")
258
+ return
259
+ # Back up whatever is there. If a stale backup exists from an earlier cycle
260
+ # but the user has since wired a DIFFERENT third-party statusline, the live
261
+ # one wins — overwriting it without a fresh backup would lose the user's
262
+ # current line and restore the wrong one on uninstall.
263
+ if BACKUP_KEY not in settings or current is not None:
264
+ settings[BACKUP_KEY] = current # may be None
265
+ settings["statusLine"] = owned_statusline(new_cmd)
266
+ settings[OWNER_KEY] = new_cmd
267
+ _install_spinner_verbs(settings, refresh=False)
268
+ save_settings(path, settings)
269
+ print(f"WIRED codeArbiter statusline -> {new_cmd}")
270
+ prior = settings[BACKUP_KEY]
271
+ if prior is not None:
272
+ pc = prior.get("command") if isinstance(prior, dict) else prior
273
+ print(f"backed up prior statusLine: {pc}")
274
+ else:
275
+ print("no prior statusLine existed; uninstall will simply remove ours.")
276
+
277
+
278
+ def refresh_if_stale(settings, script_abs, interp):
279
+ """Self-heal a ca-owned statusLine whose command has gone stale (e.g. it points
280
+ at a previous plugin-version dir after an update). Mutates `settings` IN PLACE
281
+ and returns True iff something changed.
282
+
283
+ Scope is deliberately narrow — this is NOT install:
284
+ - statusLine is ours AND its command != the desired current command -> rewrite, True
285
+ - statusLine is ours and already current -> no change, False
286
+ - statusLine is a third-party line, or absent -> never touched, False
287
+
288
+ Returning a changed-flag lets the caller persist ONLY on a real change, so a
289
+ steady-state session start never churns settings.json.
290
+
291
+ NON-DURABLE ROOTS ARE INERT (found in-session 2026-07-25, after it broke the
292
+ maintainer's statusline three times in one day). settings.json is GLOBAL and
293
+ the pin is ABSOLUTE, but the running plugin root need not be long-lived: a
294
+ session started inside a git worktree (subagents run in
295
+ `<repo>/.claude/worktrees/<id>/`) resolved the root to that worktree and this
296
+ function cheerfully re-pointed the user's global config at it. The worktree
297
+ is then pruned — its entire purpose — and the statusline renders nothing.
298
+
299
+ So: when `script_abs` is not durable, LEAVE THE EXISTING PIN ALONE. Not
300
+ healed, not cleared, no error — the same silent degrade the rest of this path
301
+ already performs. It is emphatically NOT a kill-switch: a genuinely stale pin
302
+ from a real plugin-cache update still heals, because that root IS durable.
303
+ Clearing instead of skipping was considered and rejected — a user whose only
304
+ session that day is a worktree session would lose a working statusline over a
305
+ condition that resolves itself the next time they start from a real install.
306
+
307
+ The guard lives HERE, at the mutation itself, rather than only in
308
+ `cmd_refresh`: `session-start.heal_statusline_wiring` calls this function
309
+ DIRECTLY and never goes through `cmd_refresh`, so a guard one level up would
310
+ have left the confirmed corruption path wide open."""
311
+ if _durabilitylib.is_ephemeral_path(script_abs):
312
+ return False
313
+ current = settings.get("statusLine")
314
+ if not is_ours(current, settings):
315
+ return False
316
+ desired = build_command(interp, script_abs)
317
+ cur_cmd = current.get("command") if isinstance(current, dict) else current
318
+ if cur_cmd == desired and settings.get(OWNER_KEY) == desired:
319
+ return False
320
+ settings["statusLine"] = owned_statusline(desired)
321
+ settings[OWNER_KEY] = desired
322
+ return True
323
+
324
+
325
+ def cmd_refresh(settings, path, script_abs, interp):
326
+ """SessionStart self-heal entry. Refresh a stale ca-owned path and persist ONLY
327
+ if it changed; otherwise leave settings.json untouched (no mtime churn)."""
328
+ if not os.path.exists(script_abs):
329
+ # Renderer missing (mid-update?) — do nothing rather than write a path that
330
+ # 404s. A later session with the file present will heal it.
331
+ return
332
+ if refresh_if_stale(settings, script_abs, interp):
333
+ save_settings(path, settings)
334
+ print(f"REFRESHED stale codeArbiter statusline path -> {settings['statusLine']['command']}")
335
+
336
+
337
+ def _install_spinner_verbs(settings, refresh):
338
+ current_sv = settings.get("spinnerVerbs")
339
+ already_ours = SPINNER_BACKUP_KEY in settings
340
+ if already_ours:
341
+ # Refresh: update to latest verb list, preserve the backup.
342
+ settings["spinnerVerbs"] = ARBITER_SPINNER_VERBS
343
+ return
344
+ # First install: back up whatever is there (may be None), then set ours.
345
+ if not refresh or current_sv is not None:
346
+ settings[SPINNER_BACKUP_KEY] = current_sv
347
+ settings["spinnerVerbs"] = ARBITER_SPINNER_VERBS
348
+
349
+
350
+ def cmd_uninstall(settings, path, script_abs):
351
+ current = settings.get("statusLine")
352
+ current_cmd = current.get("command") if isinstance(current, dict) else current
353
+ if OWNER_KEY in settings and settings.get(OWNER_KEY) != current_cmd:
354
+ # The user replaced our line after install. Never restore an older backup
355
+ # over that newer choice; only discard our now-stale bookkeeping.
356
+ settings.pop(OWNER_KEY, None)
357
+ settings.pop(BACKUP_KEY, None)
358
+ _uninstall_spinner_verbs(settings)
359
+ save_settings(path, settings)
360
+ print("codeArbiter statusline was replaced; preserved the current line.")
361
+ return
362
+ if not is_ours(current, settings) and BACKUP_KEY not in settings:
363
+ print("codeArbiter statusline is not wired here; nothing to do.")
364
+ return
365
+ if BACKUP_KEY in settings:
366
+ prior = settings.pop(BACKUP_KEY)
367
+ if prior is None:
368
+ settings.pop("statusLine", None)
369
+ print("REMOVED codeArbiter statusline; no prior line to restore.")
370
+ else:
371
+ settings["statusLine"] = prior
372
+ pc = prior.get("command") if isinstance(prior, dict) else prior
373
+ print(f"RESTORED prior statusLine: {pc}")
374
+ else:
375
+ settings.pop("statusLine", None)
376
+ print("REMOVED codeArbiter statusline.")
377
+ settings.pop(OWNER_KEY, None)
378
+ _uninstall_spinner_verbs(settings)
379
+ save_settings(path, settings)
380
+
381
+
382
+ def _uninstall_spinner_verbs(settings):
383
+ if SPINNER_BACKUP_KEY not in settings:
384
+ return
385
+ prior_sv = settings.pop(SPINNER_BACKUP_KEY)
386
+ if prior_sv is None:
387
+ settings.pop("spinnerVerbs", None)
388
+ else:
389
+ settings["spinnerVerbs"] = prior_sv
390
+
391
+
392
+ def main(argv=None):
393
+ ap = argparse.ArgumentParser(add_help=True)
394
+ ap.add_argument("action", nargs="?", default="status",
395
+ choices=["install", "uninstall", "status", "refresh"])
396
+ ap.add_argument("--settings")
397
+ ap.add_argument("--plugin-root")
398
+ ap.add_argument("--interp")
399
+ args = ap.parse_args(argv)
400
+
401
+ root = plugin_root(args.plugin_root)
402
+ script_abs = os.path.join(root, "hooks", "statusline.py")
403
+ spath = settings_path(args.settings)
404
+ interp = default_interp(args.interp)
405
+
406
+ settings, exists = load_settings(spath)
407
+
408
+ if args.action == "status":
409
+ cmd_status(settings, exists, script_abs)
410
+ elif args.action == "install":
411
+ cmd_install(settings, spath, script_abs, interp)
412
+ elif args.action == "uninstall":
413
+ cmd_uninstall(settings, spath, script_abs)
414
+ elif args.action == "refresh":
415
+ cmd_refresh(settings, spath, script_abs, interp)
416
+
417
+
418
+ def run(host, argv=None):
419
+ """Host-seam entry point (ADR-0011): the __main__ guard calls this with the
420
+ plugin's loaded Host. Wraps main(argv) unchanged — main()'s return value
421
+ stays discarded exactly as the old bare `main()` guard discarded it (so
422
+ the process still exits 0 on a normal fall-through).
423
+
424
+ Wires `host` live (#257): primes `_hooklib`'s process-cached Host via
425
+ `set_host()` BEFORE main() runs, so `plugin_root()`'s `get_host()` call
426
+ resolves to the SAME instance the caller passed here — no second
427
+ `hostapi.load_host()`, and `run(fake_host)` genuinely exercises
428
+ `fake_host`."""
429
+ _hooklib.set_host(host)
430
+ main(argv)
431
+ return 0
432
+
433
+
434
+ if __name__ == "__main__":
435
+ sys.exit(run(hostapi.load_host()) or 0)
@@ -0,0 +1,55 @@
1
+ # anti-slop-design — bundle router (surface scan)
2
+
3
+ A lazy-loaded design reference for any codeArbiter producer that emits a human-facing,
4
+ formatted deliverable. Load this router first, then pull only the leaves the medium needs.
5
+ Never bulk-read the whole bundle: a website task pulls the web leaf and never sees slide design.
6
+
7
+ **Scope of authority:** generated, user-facing artifacts only (UI, PR descriptions, CHANGELOG,
8
+ and any report/slide/chart a producer is told to render). This bundle does NOT govern codeArbiter's
9
+ own internal framework docs (`ORCHESTRATOR.md`, the `INDEX.md` files, skill/agent bodies), whose
10
+ house style is set elsewhere.
11
+
12
+ ## How to use the bundle
13
+
14
+ 1. Establish the **Design Read** (see `core.md` §1): one line naming medium, audience, register,
15
+ aesthetic family. The **medium** picks your row in the load map below.
16
+ 2. Always load `core.md` (the decision layer: philosophy, dials, universal laws, universal tells,
17
+ the core pre-flight check).
18
+ 3. Load **exactly** the leaves your medium's row lists. The map is mandatory and medium-driven, not a
19
+ judgment call — two agents on the same artifact must load the same leaves, so the review is
20
+ reproducible. Do not bulk-read leaves your row does not name.
21
+ 4. Before delivering, run the core pre-flight plus the loaded medium leaf's pre-flight slice.
22
+
23
+ ## Load map
24
+
25
+ Match your medium to one row and load every leaf it names (after `core.md`).
26
+
27
+ | Medium | Load (after `core.md`) |
28
+ |---|---|
29
+ | Web / UI / landing / component / portfolio | `typography` + `color` + `layout` + `images` + `medium-web` |
30
+ | Report / memo / whitepaper / resume | `typography` + `images` + `medium-documents` |
31
+ | PR description / CHANGELOG (Markdown) | `medium-documents` (§7.A.1) — copy-laws focus, no craft leaves |
32
+ | Dashboard / chart / data figure / technical review | `typography` + `color` + `layout` + `medium-dataviz` |
33
+ | Slide deck / presentation | `typography` + `color` + `layout` + `images` + `medium-slides` |
34
+ | CLI / terminal output | `medium-cli` |
35
+ | Diagram (architecture / flow / sequence / entity) | `color` + `layout` + `medium-diagram` |
36
+ | Table / spec sheet (standalone) | `medium-documents` (§7.E) |
37
+
38
+ **Not yet covered** (no dedicated leaf — apply `core` laws and the nearest medium leaf, and flag the
39
+ gap): HTML email, forms / input UI beyond `medium-web`, notifications / toasts, and social / OG cards.
40
+
41
+ ## Leaves
42
+
43
+ | Leaf | Holds |
44
+ |---|---|
45
+ | [core](core.md) | Why slop happens, the Design Read, the four Dials, universal anti-slop laws, universal tells, the core pre-flight. **Always loaded.** |
46
+ | [typography](typography.md) | Body and display type, hierarchy, measure, italic descenders. Medium-aware. |
47
+ | [color](color.md) | Palette defaults to avoid, accent discipline, contrast and accessibility. |
48
+ | [layout](layout.md) | First-impression law, grid and rhythm, anti-center bias, cards and elevation. |
49
+ | [images](images.md) | Real images first, logos, last-resort placeholders. |
50
+ | [medium-documents](medium-documents.md) | Reports, memos, whitepapers, resumes, PR/CHANGELOG prose, tables, spec sheets. Doc pre-flight slice. |
51
+ | [medium-dataviz](medium-dataviz.md) | Dashboards, charts, technical-review figures. Chart pre-flight slice. |
52
+ | [medium-slides](medium-slides.md) | Presentations and slide decks. Slide pre-flight slice. |
53
+ | [medium-web](medium-web.md) | Web and interactive interfaces. Web pre-flight slice. |
54
+ | [medium-cli](medium-cli.md) | Terminal / CLI output: status, reports, logs, TUI. CLI pre-flight slice. |
55
+ | [medium-diagram](medium-diagram.md) | Architecture / flow / sequence / entity diagrams. Diagram pre-flight slice. |
@@ -0,0 +1,43 @@
1
+ # anti-slop-design · color
2
+
3
+ Load it for any medium that renders color (per the INDEX load map). As with type, the named palettes
4
+ below are *current* reflexes and reasonable reaches, not a closed list; the rule is a palette chosen
5
+ for the brief, not a specific set of hex values.
6
+
7
+ ## 5.A The defaults to avoid
8
+
9
+ - **No AI-purple / blue glow** as a reflex. No automatic violet button glows, no random neon
10
+ gradients. Use a neutral base (zinc, slate, stone, warm grey) with a single high-contrast accent
11
+ (emerald, electric blue, deep rose, burnt orange, cobalt). Purple is fine *when the brand asks for
12
+ it*, executed with intent.
13
+ - **No premium-consumer beige+brass+oxblood+espresso** as a reflex for cookware/wellness/artisan/luxury
14
+ briefs. This palette is so over-used the brand becomes invisible. Rotate to a different family (cold
15
+ luxury silver-grey, forest green + bone, black + tan, cobalt + cream, terracotta + slate, monochrome
16
+ + one saturated pop) unless the brand names those colors.
17
+ - **No rainbow.** Max one accent. Saturation under ~80% by default. One palette per artifact, not warm
18
+ greys in one section and cool in another.
19
+
20
+ ## 5.B Contrast and accessibility
21
+
22
+ For **screen and interactive output**, contrast is mandatory, not optional:
23
+
24
+ - Body text meets WCAG AA (4.5:1) against its background; large text meets 3:1; aim AAA for primary
25
+ reading.
26
+ - Audit every interactive element: no white text on a white button, no light placeholder on a
27
+ near-white field, no ghost button on a photo without a scrim or stroke.
28
+
29
+ For **print and other static** output, AA ratios are not literally measurable but the spirit holds:
30
+ keep text legible against its ground. These two always apply regardless of medium:
31
+
32
+ - Never use pure `#000000` or pure `#ffffff` for large fills; off-black and off-white preserve depth.
33
+ - Color is never the *only* carrier of meaning (colorblind users). Pair it with text, shape, or
34
+ position.
35
+
36
+ ## Tells (color)
37
+
38
+ Universal visual tells (AI-purple/blue glow, rainbow palettes, pure `#000`/`#fff` fills) live in core
39
+ §8. Color-specific tells:
40
+
41
+ - Beige+brass+oxblood reflex for any "premium" brief.
42
+ - More than one accent without a documented reason.
43
+ - Color as the sole carrier of meaning.