@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,131 @@
1
+ #!/usr/bin/env python3
2
+ # codeArbiter — statusline box-drawing layout (extracted from statusline.py,
3
+ # architecture-004).
4
+ #
5
+ # Owns the sectioned box shell (top/row/sep/bottom) and the two small layout
6
+ # helpers (left+right justify, equal-width columns) segments use to lay content
7
+ # out inside a row. Carries no segment CONTENT — purely the box structure and
8
+ # column math, built on the ANSI/width-aware primitives in _colorlib.
9
+ #
10
+ # Design principles (mirroring _ledgerlib.py):
11
+ # - Stdlib only; no third-party imports ever.
12
+ # - Zero side effects at import time.
13
+ #
14
+ # Public API:
15
+ # Box(width) the box shell: .top/.row/.sep/.bottom/.render
16
+ # lr(left, right, inner) -> str left+right justified within inner
17
+ # cols(cells, inner) -> (str, list) equal-column layout with tee-aligned separators
18
+
19
+ try:
20
+ import _colorlib
21
+ _RESET = _colorlib.RESET
22
+ _V0 = _colorlib.V0
23
+ _TL, _TR, _BL, _BR = _colorlib.TL, _colorlib.TR, _colorlib.BL, _colorlib.BR
24
+ _H, _V, _LT, _RT = _colorlib.H, _colorlib.V, _colorlib.LT, _colorlib.RT
25
+ _TD, _TU, _DOTH = _colorlib.TD, _colorlib.TU, _colorlib.DOTH
26
+ _pad, _vlen, _clip, _gradient_h = (_colorlib.pad, _colorlib.vlen,
27
+ _colorlib.clip, _colorlib.gradient_h)
28
+ _box_gradient_h = _colorlib.box_gradient_h
29
+ _ANSI = _colorlib.ANSI
30
+ except Exception: # pragma: no cover — never let an import break the statusline
31
+ _colorlib = None
32
+ _RESET = _V0 = ""
33
+ _TL = _TR = _BL = _BR = _H = _V = _LT = _RT = _TD = _TU = _DOTH = ""
34
+
35
+ def _pad(s, w):
36
+ return s
37
+
38
+ def _vlen(s):
39
+ return len(s)
40
+
41
+ def _clip(s, w):
42
+ return s[:w]
43
+
44
+ def _gradient_h(text, width, c_from=None, c_to=None):
45
+ return text
46
+
47
+ _box_gradient_h = _gradient_h
48
+
49
+ import re as _re
50
+ _ANSI = _re.compile(r"\033\[[0-9;]*m")
51
+
52
+
53
+ def sync_palette():
54
+ """Refresh captured color exports after _colorlib runtime activation."""
55
+ global _RESET, _V0, _gradient_h, _box_gradient_h
56
+ if _colorlib is not None:
57
+ _RESET, _V0 = _colorlib.RESET, _colorlib.V0
58
+ _gradient_h = _colorlib.gradient_h
59
+ _box_gradient_h = _colorlib.box_gradient_h
60
+
61
+ class Box:
62
+ def __init__(self, width):
63
+ self.W = width
64
+ self.inner = width - 4 # "│ " + content + " │"
65
+ self.lines = []
66
+
67
+ def top(self, title, badge=""):
68
+ left = f"{_V0}{_TL}{_H}{_H}{_RESET} " # 4 visible cols
69
+ b = f" {badge} " if badge else " "
70
+ budget = max(1, self.W - 5 - _vlen(b)) # keep room for ╮ + >=0 fill
71
+ title = _clip(title, budget) if _vlen(title) > budget else title
72
+ t = _gradient_h(_ANSI.sub("", title), _vlen(title))
73
+ used = 3 + _vlen(title) + _vlen(b)
74
+ fillw = max(0, self.W - 1 - used - 1)
75
+ fill = _box_gradient_h(_H * fillw, fillw) if fillw else ""
76
+ self.lines.append(f"{left}{t}{b}{fill}{_V0}{_TR}{_RESET}")
77
+
78
+ def row(self, content):
79
+ self.lines.append(f"{_V0}{_V}{_RESET} {_pad(content, self.inner)} {_V0}{_V}{_RESET}")
80
+
81
+ def sep(self, tees=None):
82
+ mid = [_DOTH] * (self.inner + 2)
83
+ for k in (tees or []):
84
+ if 0 <= k + 1 < len(mid):
85
+ mid[k + 1] = _TD
86
+ self.lines.append(f"{_V0}{_LT}{''.join(mid)}{_RT}{_RESET}")
87
+
88
+ def bottom(self, tees=None):
89
+ mid = [_H] * (self.inner + 2)
90
+ for k in (tees or []):
91
+ if 0 <= k + 1 < len(mid):
92
+ mid[k + 1] = _TU
93
+ self.lines.append(f"{_V0}{_BL}{''.join(mid)}{_BR}{_RESET}")
94
+
95
+ def render(self):
96
+ # No trailing newline: a multi-line statusline that ends in '\n' adds a
97
+ # phantom row, so the host's height accounting drifts on re-render and the
98
+ # bar eventually clears itself. Lines are separated, not terminated.
99
+ return "\n".join(self.lines)
100
+
101
+
102
+ def lr(left, right, inner):
103
+ """Left + right justified within inner; never exceeds inner (clips left)."""
104
+ right = _clip(right, inner)
105
+ rw = _vlen(right)
106
+ left = _clip(left, max(0, inner - rw - 1))
107
+ gap = max(1, inner - _vlen(left) - rw)
108
+ return left + " " * gap + right
109
+
110
+
111
+ def cols(cells, inner):
112
+ """Lay out cells in equal columns with ' │ ' joins. Each cell is clamped to
113
+ its column via pad(), so total visible width == inner exactly and the
114
+ separator tees line up with the content joins. Returns (content, bounds)."""
115
+ n = len(cells)
116
+ if n == 1:
117
+ return _pad(cells[0], inner), []
118
+ seps = n - 1
119
+ avail = inner - seps * 3
120
+ base = avail // n
121
+ widths = [base] * n
122
+ widths[-1] = avail - base * (n - 1)
123
+ parts, bounds, pos = [], [], 0
124
+ for i, c in enumerate(cells):
125
+ parts.append(_pad(c, widths[i]))
126
+ pos += widths[i]
127
+ if i < n - 1:
128
+ parts.append(f" {_V0}{_V}{_RESET} ")
129
+ bounds.append(pos + 1)
130
+ pos += 3
131
+ return "".join(parts), bounds
@@ -0,0 +1,304 @@
1
+ #!/usr/bin/env python3
2
+ # codeArbiter — statusline color palette, box-drawing glyphs, and ANSI/width-aware
3
+ # string primitives (extracted from statusline.py, architecture-004).
4
+ #
5
+ # Owns the neon-violet palette, native-safe glyph set, the ANSI-escape regex, and
6
+ # the terminal-column-width-aware string ops (clip/pad/vlen) the box layout relies
7
+ # on to stay aligned regardless of color codes or wide (CJK/emoji) glyphs. Carries
8
+ # NO rendering-decision logic (no segment content, no box structure) — purely the
9
+ # color/glyph vocabulary and the width primitives built on it.
10
+ #
11
+ # Design principles (mirroring _ledgerlib.py):
12
+ # - Stdlib only; no third-party imports ever.
13
+ # - Zero side effects at import time.
14
+ # - Pure functions, fully testable with synthetic input.
15
+ #
16
+ # Public API:
17
+ # fg(r,g,b) -> str truecolor foreground SGR
18
+ # bg(r,g,b) -> str truecolor background SGR
19
+ # vlen(s) -> int visible terminal-column width (ANSI-aware, wide-glyph-aware)
20
+ # clip(s, w) -> str truncate to <= w visible columns (ANSI-preserving)
21
+ # pad(s, w) -> str pad/clip to exactly w visible columns
22
+ # gradient_h(text, width, c_from, c_to) -> str per-character violet sheen
23
+ # strip_control(s) -> str strip C0 control bytes + OSC/CSI escape sequences
24
+
25
+ import json
26
+ import os
27
+ import re
28
+ import unicodedata
29
+ from dataclasses import dataclass, replace
30
+ from types import MappingProxyType
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class RGB:
35
+ r: int
36
+ g: int
37
+ b: int
38
+
39
+ def __iter__(self):
40
+ return iter((self.r, self.g, self.b))
41
+
42
+ def __getitem__(self, index):
43
+ return (self.r, self.g, self.b)[index]
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class Palette:
48
+ accent_deep: RGB
49
+ accent_mid: RGB
50
+ accent_primary: RGB
51
+ accent_bright: RGB
52
+ text_muted: RGB
53
+ text_normal: RGB
54
+ text_on_accent: RGB
55
+ semantic_ok: RGB
56
+ semantic_warn: RGB
57
+ semantic_danger: RGB
58
+ gradient_from: RGB
59
+ gradient_to: RGB
60
+
61
+
62
+ def _palette(values):
63
+ return Palette(*(RGB(*value) for value in values))
64
+
65
+
66
+ BUILTIN_PALETTES = MappingProxyType({
67
+ "violet": _palette(((108, 70, 180), (150, 92, 230), (178, 102, 255), (208, 140, 255),
68
+ (150, 150, 162), (232, 232, 240), (18, 14, 26),
69
+ (120, 220, 150), (255, 184, 76), (255, 86, 110),
70
+ (120, 80, 200), (205, 140, 255))),
71
+ "blue": _palette(((35, 91, 155), (52, 130, 205), (69, 155, 255), (125, 190, 255),
72
+ (145, 158, 174), (232, 240, 248), (8, 22, 36),
73
+ (107, 214, 158), (255, 187, 79), (255, 91, 109),
74
+ (40, 105, 190), (113, 190, 255))),
75
+ "green": _palette(((31, 112, 78), (42, 157, 103), (55, 201, 132), (111, 231, 169),
76
+ (142, 164, 153), (230, 242, 235), (7, 30, 20),
77
+ (83, 219, 139), (255, 190, 76), (255, 91, 105),
78
+ (33, 126, 86), (102, 225, 159))),
79
+ "amber": _palette(((145, 87, 20), (199, 123, 29), (240, 157, 43), (255, 199, 104),
80
+ (165, 154, 139), (244, 237, 226), (35, 22, 7),
81
+ (100, 210, 137), (255, 184, 76), (255, 83, 98),
82
+ (166, 96, 20), (255, 190, 76))),
83
+ "mono": _palette(((104, 104, 110), (145, 145, 152), (190, 190, 198), (226, 226, 232),
84
+ (145, 145, 152), (232, 232, 236), (18, 18, 20),
85
+ (199, 224, 205), (235, 213, 174), (239, 174, 181),
86
+ (112, 112, 118), (218, 218, 224))),
87
+ })
88
+
89
+ MAX_THEME_BYTES = 16 * 1024
90
+ _CUSTOM_FIELDS = MappingProxyType({
91
+ "accent": MappingProxyType({"deep": "accent_deep", "mid": "accent_mid",
92
+ "primary": "accent_primary", "bright": "accent_bright"}),
93
+ "text": MappingProxyType({"muted": "text_muted", "normal": "text_normal",
94
+ "on_accent": "text_on_accent"}),
95
+ "semantic": MappingProxyType({"ok": "semantic_ok", "warn": "semantic_warn",
96
+ "danger": "semantic_danger"}),
97
+ "gradient": MappingProxyType({"from": "gradient_from", "to": "gradient_to"}),
98
+ })
99
+ _HEX = re.compile(r"^#[0-9a-fA-F]{6}$")
100
+
101
+
102
+ def _read_custom(path):
103
+ """Read one bounded local JSON object; return None for every failure mode."""
104
+ try:
105
+ path = os.path.expanduser(os.path.expandvars(path))
106
+ if os.path.getsize(path) > MAX_THEME_BYTES:
107
+ return None
108
+ with open(path, "rb") as handle:
109
+ raw = handle.read(MAX_THEME_BYTES + 1)
110
+ if len(raw) > MAX_THEME_BYTES:
111
+ return None
112
+ value = json.loads(raw.decode("utf-8"))
113
+ return value if isinstance(value, dict) else None
114
+ except (OSError, UnicodeError, ValueError, TypeError, RecursionError):
115
+ return None
116
+
117
+
118
+ def _resolve_palette(theme_name=None, custom_path=None):
119
+ """Return (effective theme name, palette), falling back wholly to violet."""
120
+ violet = BUILTIN_PALETTES["violet"]
121
+ name = str(theme_name or "violet").strip().lower()
122
+ if name != "custom":
123
+ return (name, BUILTIN_PALETTES[name]) if name in BUILTIN_PALETTES else ("violet", violet)
124
+ path = custom_path or os.environ.get("CODEARBITER_THEME_FILE") or os.path.join(
125
+ "~", ".codearbiter", "statusline-theme.json")
126
+ data = _read_custom(path)
127
+ if data is None:
128
+ return "violet", violet
129
+ updates = {}
130
+ for group_name, fields in _CUSTOM_FIELDS.items():
131
+ group = data.get(group_name)
132
+ if not isinstance(group, dict):
133
+ continue
134
+ for key, field_name in fields.items():
135
+ value = group.get(key)
136
+ if isinstance(value, str) and _HEX.fullmatch(value):
137
+ updates[field_name] = RGB(int(value[1:3], 16), int(value[3:5], 16), int(value[5:7], 16))
138
+ return "custom", replace(violet, **updates)
139
+
140
+
141
+ def resolve_palette(theme_name=None, custom_path=None):
142
+ """Resolve a built-in or partial custom palette, always falling back to violet."""
143
+ return _resolve_palette(theme_name, custom_path)[1]
144
+
145
+
146
+ def fg(r, g, b):
147
+ return f"\033[38;2;{r};{g};{b}m"
148
+
149
+
150
+ RESET, BOLD, DIM, ITAL = "\033[0m", "\033[1m", "\033[2m", "\033[3m"
151
+
152
+
153
+ def bg(r, g, b):
154
+ return f"\033[48;2;{r};{g};{b}m"
155
+
156
+ # neon-violet ramp (dark -> bright) — used for the box sheen and accents
157
+ ACTIVE_THEME_NAME = str(os.environ.get("CODEARBITER_THEME") or "violet").strip().lower()
158
+ if ACTIVE_THEME_NAME not in (*BUILTIN_PALETTES, "custom"):
159
+ ACTIVE_THEME_NAME = "violet"
160
+ ACTIVE_PALETTE = (BUILTIN_PALETTES["violet"] if ACTIVE_THEME_NAME == "custom"
161
+ else resolve_palette(ACTIVE_THEME_NAME))
162
+
163
+
164
+ def _project_palette(name, palette):
165
+ global ACTIVE_THEME_NAME, ACTIVE_PALETTE
166
+ global V0, V1, V2, V3, GREY, WHITE, OK, WARN, DANGER, PILL_FG
167
+ ACTIVE_THEME_NAME, ACTIVE_PALETTE = name, palette
168
+ V0 = fg(*palette.accent_deep)
169
+ V1 = fg(*palette.accent_mid)
170
+ V2 = fg(*palette.accent_primary)
171
+ V3 = fg(*palette.accent_bright)
172
+ GREY = fg(*palette.text_muted)
173
+ WHITE = fg(*palette.text_normal)
174
+ OK = fg(*palette.semantic_ok)
175
+ WARN = fg(*palette.semantic_warn)
176
+ DANGER = fg(*palette.semantic_danger)
177
+ PILL_FG = fg(*palette.text_on_accent)
178
+
179
+
180
+ def activate_palette(theme_name=None, custom_path=None):
181
+ """Resolve and project the process palette at render time.
182
+
183
+ Custom themes may touch the filesystem, so callers must invoke this from a
184
+ runtime path rather than relying on module import. Repeated calls re-resolve
185
+ the current environment deterministically.
186
+ """
187
+ name = str(theme_name if theme_name is not None else
188
+ (os.environ.get("CODEARBITER_THEME") or "violet")).strip().lower()
189
+ if name not in (*BUILTIN_PALETTES, "custom"):
190
+ name = "violet"
191
+ effective_name, palette = _resolve_palette(name, custom_path)
192
+ _project_palette(effective_name, palette)
193
+ return palette
194
+
195
+
196
+ # Built-ins are pure data and retain legacy import-time exports. Custom stays
197
+ # violet until the explicit render-time activation above.
198
+ _project_palette(ACTIVE_THEME_NAME, ACTIVE_PALETTE)
199
+
200
+ # native-safe glyphs only
201
+ TL, TR, BL, BR = "╭", "╮", "╰", "╯"
202
+ H, V, LT, RT = "─", "│", "├", "┤"
203
+ TD, TU = "┬", "┴" # tee down / up (column joins)
204
+ DOTH = "┄" # dotted horizontal (section separators)
205
+ DOT = "●"
206
+ SUBDOT = "◦"
207
+ ELL = "…"
208
+ BFULL, BEMPTY = "█", "░"
209
+ DN, UP = "↓", "↑"
210
+ ARR = "▸"
211
+ BOLT = "↯"
212
+ SPARK = "▁▂▃▄▅▆▇█"
213
+
214
+ ANSI = re.compile(r"\033\[[0-9;]*m")
215
+
216
+ # Host-controlled strings (a model display name, a subagent's derived label)
217
+ # reach the render path verbatim. Neither the host payload nor a subagent
218
+ # transcript is a trusted source, so any C0 control byte or OSC/CSI escape
219
+ # sequence embedded in one is terminal-escape injection into the user's
220
+ # prompt line — strip it before the string is ever composed into a row.
221
+ _OSC_RE = re.compile(r"\033\][^\007\033]*(?:\007|\033\\)?")
222
+ _CSI_RE = re.compile(r"\033\[[0-?]*[ -/]*[@-~]?")
223
+ _C0_RE = re.compile(r"[\000-\037]")
224
+
225
+
226
+ def strip_control(s):
227
+ """Strip OSC/CSI escape sequences and any remaining C0 control byte
228
+ (0x00-0x1f) from a host- or transcript-derived string. Non-string input
229
+ passes through unchanged; this must never raise."""
230
+ if not isinstance(s, str):
231
+ return s
232
+ s = _OSC_RE.sub("", s)
233
+ s = _CSI_RE.sub("", s)
234
+ return _C0_RE.sub("", s)
235
+
236
+
237
+ def _cw(ch):
238
+ """Terminal column width of one character: 0 for a combining mark, 2 for an
239
+ East-Asian Wide/Fullwidth glyph (CJK, many emoji), 1 otherwise. Ambiguous-
240
+ width glyphs (box-drawing, blocks, arrows used in this bar) render as 1 in a
241
+ non-CJK terminal, so they stay 1 — only genuinely wide content counts as 2."""
242
+ if unicodedata.combining(ch):
243
+ return 0
244
+ return 2 if unicodedata.east_asian_width(ch) in ("W", "F") else 1
245
+
246
+
247
+ def vlen(s):
248
+ """Visible terminal-column width of s, ignoring ANSI codes and counting wide
249
+ glyphs as 2 columns — so the box never overflows on CJK/emoji content."""
250
+ return sum(_cw(c) for c in ANSI.sub("", s))
251
+
252
+
253
+ def clip(s, w):
254
+ """Truncate a (possibly ANSI-colored) string to <= w visible columns,
255
+ preserving color codes and appending an ellipsis + RESET when cut."""
256
+ if w <= 0:
257
+ return ""
258
+ if vlen(s) <= w:
259
+ return s
260
+ out, vis, i, n = [], 0, 0, len(s)
261
+ limit = w - 1 # leave one column for the ellipsis
262
+ while i < n:
263
+ m = ANSI.match(s, i)
264
+ if m:
265
+ out.append(m.group(0))
266
+ i = m.end()
267
+ continue
268
+ cw = _cw(s[i])
269
+ if vis + cw > limit: # a wide glyph that would cross the limit stops here
270
+ break
271
+ out.append(s[i])
272
+ vis += cw
273
+ i += 1
274
+ return "".join(out) + ELL + RESET
275
+
276
+
277
+ def pad(s, w):
278
+ """Return s at exactly w visible columns: clip if over, space-pad if under."""
279
+ v = vlen(s)
280
+ if v > w:
281
+ return clip(s, w)
282
+ return s + " " * (w - v)
283
+
284
+
285
+ def gradient_h(text, width, c_from=None, c_to=None):
286
+ """Per-character violet sheen across `width` columns (dark->bright)."""
287
+ c_from = ACTIVE_PALETTE.gradient_from if c_from is None else c_from
288
+ c_to = ACTIVE_PALETTE.gradient_to if c_to is None else c_to
289
+ out = []
290
+ n = max(1, width - 1)
291
+ for i, ch in enumerate(text):
292
+ t = i / n
293
+ r = int(c_from[0] + (c_to[0] - c_from[0]) * t)
294
+ g = int(c_from[1] + (c_to[1] - c_from[1]) * t)
295
+ b = int(c_from[2] + (c_to[2] - c_from[2]) * t)
296
+ out.append(f"\033[38;2;{r};{g};{b}m{ch}")
297
+ return "".join(out) + RESET
298
+
299
+
300
+ def box_gradient_h(text, width):
301
+ """Header-border sheen, retaining the original violet bytes by default."""
302
+ if ACTIVE_THEME_NAME == "violet":
303
+ return gradient_h(text, width, (90, 60, 150), (170, 110, 240))
304
+ return gradient_h(text, width)
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env python3
2
+ # codeArbiter — durable-vs-ephemeral install-root detection.
3
+ #
4
+ # WHY THIS EXISTS (found in-session on 2026-07-25, after it broke the
5
+ # maintainer's statusline three times in one day):
6
+ #
7
+ # A plugin cannot own a statusLine and ${CLAUDE_PLUGIN_ROOT} is NOT expanded
8
+ # inside settings.json, so codeArbiter has to write an ABSOLUTE, version-pinned
9
+ # renderer path into the user's GLOBAL ~/.claude/settings.json — and re-point it
10
+ # on every SessionStart so a plugin update does not leave the old version wired.
11
+ # That refresh had no notion of a root that will not survive the session. When a
12
+ # session starts inside a git worktree (subagents routinely run in
13
+ # <repo>/.claude/worktrees/<id>/), the plugin root IS the worktree's plugins/ca,
14
+ # and the heal happily pinned the user's global config there:
15
+ #
16
+ # "...\.claude\worktrees\wf_58ee3fa6-de1-8\plugins\ca\hooks\statusline.py"
17
+ #
18
+ # The worktree is then pruned — that is what worktrees are FOR — and the
19
+ # statusline renders nothing until someone notices and re-runs /ca:statusline.
20
+ #
21
+ # This module answers exactly one question, for callers about to write a path
22
+ # into a long-lived config: may this path be trusted to still be there?
23
+ #
24
+ # TWO INDEPENDENT SIGNALS, because each alone has a blind spot:
25
+ # 1. LEXICAL (has_worktree_segment) — a `worktrees` or `*-worktrees` path
26
+ # segment. Free, no I/O at all, works even when the checkout is already
27
+ # gone. Covers the confirmed `.claude/worktrees/<id>/` case and the
28
+ # `../.codearbiter-worktrees/<slug>` layout the using-git-worktrees skill
29
+ # creates.
30
+ # 2. GIT (in_linked_worktree) — the path lives in a LINKED worktree, read
31
+ # straight off git's own on-disk marker. Generalizes to a worktree parked
32
+ # anywhere under any name, which the lexical rule cannot see.
33
+ #
34
+ # NO SUBPROCESS, DELIBERATELY. This predicate is consulted on EVERY SessionStart,
35
+ # before the dormant gate, in every repo on the machine. An earlier draft shelled
36
+ # out to `git rev-parse --git-dir --git-common-dir`; that probe fails by design
37
+ # in the ordinary plugin-cache install (which is not a git repository at all), so
38
+ # it bought nothing in the common case while charging every session a process
39
+ # spawn plus a timeout hazard. Git writes the answer to disk anyway: a LINKED
40
+ # worktree's `.git` is a FILE reading `gitdir: <common>/.git/worktrees/<name>`,
41
+ # while a normal checkout's `.git` is a DIRECTORY. A handful of stat() calls
42
+ # gives the same answer, with no dependency on git being installed at all.
43
+ #
44
+ # FAILURE DIRECTION IS "DURABLE", DELIBERATELY. Anything unreadable, unparseable
45
+ # or absent reads as durable. Reading uncertainty as "ephemeral" would silently
46
+ # disable the self-heal for users whose installs are perfectly fine — a far worse
47
+ # outcome than the residual it protects against.
48
+ #
49
+ # The lexical rule is deliberately a little broad (any `worktrees` segment, not
50
+ # only one under `.claude`). The asymmetry justifies it: a false positive costs
51
+ # an un-refreshed pin the user fixes with one explicit /ca:statusline run, while
52
+ # a false negative corrupts their global config.
53
+ #
54
+ # Design principles (mirroring _gitexec.py / _gitlib.py):
55
+ # - Stdlib only; no third-party imports ever (ADR-0004).
56
+ # - Zero side effects at import time.
57
+ # - NEVER raises. This sits on the SessionStart path; it must not be the thing
58
+ # that crashes startup, whatever it is handed.
59
+ #
60
+ # Public API:
61
+ # has_worktree_segment(path) -> bool lexical signal, no I/O
62
+ # in_linked_worktree(path) -> bool git's on-disk marker, no subprocess
63
+ # is_ephemeral_path(path) -> bool either signal
64
+
65
+ import os
66
+ import re
67
+
68
+ # The walk from a plugin root to the filesystem root is a handful of levels; the
69
+ # bound exists only so a pathological path can never spin this on the hot path.
70
+ MAX_ANCESTOR_WALK = 64
71
+
72
+ # A `.git` FILE is at most a line or two. Reading a bounded prefix means a
73
+ # directory entry that is unexpectedly enormous cannot stall startup.
74
+ GITFILE_READ_BYTES = 4096
75
+
76
+ _SEP_RE = re.compile(r"[\\/]+")
77
+
78
+
79
+ def _segments(path):
80
+ """Path segments of `path`, split on BOTH separators (a Windows path may be
81
+ read from a POSIX host and vice versa). Empty tuple on any non-string."""
82
+ if not isinstance(path, str) or not path:
83
+ return ()
84
+ return tuple(part for part in _SEP_RE.split(path) if part)
85
+
86
+
87
+ def has_worktree_segment(path):
88
+ """True iff `path` contains a segment named `worktrees` or `*-worktrees`
89
+ (case-insensitive). Purely lexical — no filesystem access, no subprocess.
90
+
91
+ Matches `<repo>/.claude/worktrees/<id>/...` (Claude Code's own subagent
92
+ worktrees) and `../.codearbiter-worktrees/<slug>/...` (the
93
+ using-git-worktrees skill). Compares whole SEGMENTS, never substrings: a
94
+ directory literally called `worktrees-archive` is a name of its own, not a
95
+ container of worktrees, and matching it would decline to heal a perfectly
96
+ durable root."""
97
+ for part in _segments(path):
98
+ low = part.lower()
99
+ if low == "worktrees" or low.endswith("-worktrees"):
100
+ return True
101
+ return False
102
+
103
+
104
+ def _ancestors(path):
105
+ """`path` (or its directory, if it names a file) and every parent above it,
106
+ nearest first, bounded by MAX_ANCESTOR_WALK. Yields nothing for junk."""
107
+ if not isinstance(path, str) or not path:
108
+ return
109
+ try:
110
+ cur = os.path.abspath(path)
111
+ if not os.path.isdir(cur):
112
+ cur = os.path.dirname(cur)
113
+ except Exception: # noqa: BLE001 — a probe must never crash its caller
114
+ return
115
+ for _ in range(MAX_ANCESTOR_WALK):
116
+ if not cur:
117
+ return
118
+ yield cur
119
+ parent = os.path.dirname(cur)
120
+ if parent == cur: # filesystem root: dirname is a fixed point
121
+ return
122
+ cur = parent
123
+
124
+
125
+ def _gitfile_points_at_worktree(gitfile):
126
+ """True iff the `.git` FILE at `gitfile` is a LINKED WORKTREE pointer.
127
+
128
+ Git writes `gitdir: <path>` into `.git` for both linked worktrees and
129
+ submodules, and the two must not be confused: a worktree points into
130
+ `<common>/.git/worktrees/<name>` (disposable), a submodule into
131
+ `<super>/.git/modules/<name>` (an ordinary long-lived checkout). The
132
+ `worktrees` segment in the TARGET is what separates them."""
133
+ try:
134
+ with open(gitfile, "rb") as f:
135
+ head = f.read(GITFILE_READ_BYTES)
136
+ lines = head.decode("utf-8", "replace").splitlines()
137
+ if not lines:
138
+ return False
139
+ first = lines[0].strip()
140
+ if not first.lower().startswith("gitdir:"):
141
+ return False
142
+ # split(":", 1) — the FIRST colon only, so a Windows target like
143
+ # `C:/Users/...` survives intact.
144
+ return has_worktree_segment(first.split(":", 1)[1].strip())
145
+ except Exception: # noqa: BLE001 — unreadable reads as durable, by design
146
+ return False
147
+
148
+
149
+ def in_linked_worktree(path):
150
+ """True iff `path` sits inside a LINKED git worktree.
151
+
152
+ Walks up from `path` to the first `.git` it meets — the boundary of the
153
+ repository the path actually belongs to — and decides there:
154
+ - `.git` is a DIRECTORY -> the main worktree. Durable. Stop.
155
+ - `.git` is a FILE -> a linked worktree or a submodule; the pointer's
156
+ target says which. Stop either way.
157
+ - no `.git` at any level -> not in a repository at all (the ordinary
158
+ plugin-cache install). Durable.
159
+ Stopping at the NEAREST boundary is load-bearing: a real checkout nested
160
+ below some unrelated worktree marker must read as durable.
161
+
162
+ `path` may name a file (its directory is walked) or a directory, and need
163
+ not exist. Never raises."""
164
+ try:
165
+ for cur in _ancestors(path):
166
+ gitpath = os.path.join(cur, ".git")
167
+ if os.path.isdir(gitpath):
168
+ return False
169
+ if os.path.isfile(gitpath):
170
+ return _gitfile_points_at_worktree(gitpath)
171
+ return False
172
+ except Exception: # noqa: BLE001 — a probe must never crash its caller
173
+ return False
174
+
175
+
176
+ def is_ephemeral_path(path):
177
+ """True iff `path` must NOT be pinned into a long-lived global config.
178
+
179
+ Checks the free lexical signal FIRST and short-circuits on it, so the
180
+ confirmed production case is decided with zero I/O. Never raises."""
181
+ try:
182
+ if has_worktree_segment(path):
183
+ return True
184
+ return in_linked_worktree(path)
185
+ except Exception: # noqa: BLE001 — a probe must never crash its caller
186
+ return False
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env python3
2
+ # codeArbiter — shared entry-point dispatch helper (jscpd dedup, #320/#334 item 2).
3
+ #
4
+ # Every hook/tool entry file (doctor.py, pre-edit.py, pre-write.py, pre-read.py,
5
+ # git-enforce.py, post-write-edit.py, migration-pass.py, security-pass.py,
6
+ # statusline.py, boardsync.py, metrics.py, preview.py, taskwrite.py, babysit.py,
7
+ # init-codearbiter.py, ...) carries the identically-shaped `run(host, argv=None)`
8
+ # body: wire the process-cached Host live (#257, ADR-0011) via a `set_host`
9
+ # callable BEFORE `main` runs, invoke `main` (with or without `argv`), and either
10
+ # discard its return (always `return 0`) or propagate it as the exit code —
11
+ # jscpd flags this near-duplicate boilerplate across ~15 files. This module
12
+ # hoists the shared BODY only; each caller keeps its own `run(host, argv=None)`
13
+ # signature/docstring and passes its own `main`/`set_host` callables plus the
14
+ # two booleans that capture its file's variant behavior (verbatim-equivalent —
15
+ # behavior is unchanged, not "improved").
16
+ #
17
+ # Zero import-time side effects; pure function; no filesystem/git access.
18
+ #
19
+ # Public API:
20
+ # dispatch(host, argv, main_fn, set_host_fn, pass_argv, propagate_result) -> int
21
+ # Wire `host` via set_host_fn(host), call main_fn() or main_fn(argv)
22
+ # (per pass_argv), then return that result or 0 (per propagate_result).
23
+
24
+ def dispatch(host, argv, main_fn, set_host_fn, pass_argv, propagate_result):
25
+ """Shared body of an entry file's `run(host, argv=None)`.
26
+
27
+ set_host_fn(host) -- primes the process-cached Host (#257) BEFORE
28
+ main_fn runs, so any get_host() call downstream
29
+ resolves to the SAME instance the caller passed
30
+ here (no second hostapi.load_host()).
31
+ main_fn()/main_fn(argv) -- per `pass_argv`: some entry files' main() takes
32
+ no arguments, others take `argv=None`.
33
+ return result or 0 -- per `propagate_result`: some callers discard
34
+ main_fn's return value (always exit 0, matching
35
+ the old bare `main()` guard); others propagate
36
+ main_fn's return value as the exit code
37
+ (matching the old `sys.exit(main())` guard).
38
+ """
39
+ set_host_fn(host)
40
+ result = main_fn(argv) if pass_argv else main_fn()
41
+ return result if propagate_result else 0