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