@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,196 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# codeArbiter - ACTIVATION: which Host this process is running under, where the
|
|
3
|
+
# project root is, and whether the arbiter is switched on for it.
|
|
4
|
+
#
|
|
5
|
+
# Extracted from _hooklib (issue #321, architecture-002) as slice 4, the last and
|
|
6
|
+
# the one with real teeth. Measured like the others: this cluster references
|
|
7
|
+
# NOTHING from the rest of _hooklib. The edge runs the other way for the first
|
|
8
|
+
# time - _log_gate_event (which stays behind) calls get_host() and
|
|
9
|
+
# project_root() - so _hooklib imports from here and never the reverse. Still
|
|
10
|
+
# one-way, still no cycle.
|
|
11
|
+
#
|
|
12
|
+
# WHY THESE BELONG TOGETHER: they are the three answers every hook needs before
|
|
13
|
+
# it can decide anything, and they share two process-lifetime caches. `_HOST` and
|
|
14
|
+
# `_ROOT_CACHE` exist because a hook is a single-shot process whose host identity
|
|
15
|
+
# and project root cannot change mid-run; arbiter_active reads the root to find
|
|
16
|
+
# CONTEXT.md, and project_root reads the host. Splitting them would put a cache
|
|
17
|
+
# in one module and its only writer in another.
|
|
18
|
+
#
|
|
19
|
+
# ON THE MUTABLE GLOBALS, because this is the trap in this slice:
|
|
20
|
+
# `_HOST` is deliberately NOT re-exported by _hooklib. `from _activationlib
|
|
21
|
+
# import _HOST` binds the VALUE at import time, so a later set_host() would
|
|
22
|
+
# rebind the name HERE and leave _hooklib holding a stale None forever - the
|
|
23
|
+
# exact silent-divergence bug the partition exists to prevent. The three
|
|
24
|
+
# accessors ARE re-exported, and they all read and write this module's global,
|
|
25
|
+
# so every caller sees one cache no matter which module it imported them from.
|
|
26
|
+
# That is the opposite of slice 3's lesson (re-export the privates) and for a
|
|
27
|
+
# precise reason: a FUNCTION binding stays live, a VALUE binding does not.
|
|
28
|
+
# Verified: nothing in the repo reads _hooklib._HOST directly.
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import os
|
|
33
|
+
import re
|
|
34
|
+
|
|
35
|
+
import hostapi
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# The loaded Host is process-cached: hooks are single-shot processes, and the
|
|
39
|
+
# host's identity cannot change mid-process (its methods read env/payload state
|
|
40
|
+
# live at call time, so caching the OBJECT changes no verdict).
|
|
41
|
+
_HOST = None
|
|
42
|
+
|
|
43
|
+
def get_host():
|
|
44
|
+
"""The process's Host instance (hostapi.load_host(), cached)."""
|
|
45
|
+
global _HOST
|
|
46
|
+
if _HOST is None:
|
|
47
|
+
_HOST = hostapi.load_host()
|
|
48
|
+
return _HOST
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def set_host(host):
|
|
52
|
+
"""Dependency-injection seam (#257 architecture-001/performance-002).
|
|
53
|
+
|
|
54
|
+
Primes the module-cached `_HOST` that `get_host()` reads. Every entry
|
|
55
|
+
script's `run(host, argv=None)` calls this BEFORE `main()`, so the Host
|
|
56
|
+
instance the `__main__` guard already resolved via `hostapi.load_host()`
|
|
57
|
+
is the SAME object `get_host()` serves inside `main()` — closing two
|
|
58
|
+
defects at once: (1) `main()` no longer triggers its own redundant
|
|
59
|
+
`hostapi.load_host()` (a second `_host.py` load per invocation), and
|
|
60
|
+
(2) `run(host)` stops silently ignoring its `host` argument — a test that
|
|
61
|
+
calls `run(fake_host)` now genuinely exercises `fake_host`, not whatever
|
|
62
|
+
`load_host()` resolves from disk. In production the injected host IS the
|
|
63
|
+
`load_host()` result the guard already computed, so this changes no
|
|
64
|
+
behavior — it only removes the redundant second load and makes the
|
|
65
|
+
existing `run(host)` parameter live."""
|
|
66
|
+
global _HOST
|
|
67
|
+
_HOST = host
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def reset_host():
|
|
71
|
+
"""Test-only: clear the injected/cached `_HOST` so the next `get_host()`
|
|
72
|
+
lazy-loads afresh. Production hook processes are single-shot and never need
|
|
73
|
+
this; but `set_host()` makes `_HOST` a process-lifetime singleton, so a test
|
|
74
|
+
that calls `run(fake_host)` must reset it in tearDown — otherwise the fake
|
|
75
|
+
leaks into any later in-process test that calls `get_host()` without its own
|
|
76
|
+
patch, silently running against the wrong host and masking a gate
|
|
77
|
+
regression (security review #257, LOW)."""
|
|
78
|
+
global _HOST
|
|
79
|
+
_HOST = None
|
|
80
|
+
|
|
81
|
+
# project_root() memoization (performance-001/003, #260). A hook is a
|
|
82
|
+
# single-shot process, so CLAUDE_PROJECT_DIR and the process cwd cannot
|
|
83
|
+
# change mid-process — but the resolved VALUE is cached keyed on those two
|
|
84
|
+
# inputs (not unconditionally) rather than as one bare value, so an env/cwd
|
|
85
|
+
# change is a cache MISS, never a stale hit. This keeps the production
|
|
86
|
+
# single-shot contract (the same hook process always sees an unchanging
|
|
87
|
+
# env/cwd, so it resolves at most once) while staying correct for the
|
|
88
|
+
# in-process integration-test harnesses that legitimately re-target
|
|
89
|
+
# project_root() across many fixtures/envs within one Python process
|
|
90
|
+
# (`python -m unittest discover` runs the whole suite in ONE interpreter —
|
|
91
|
+
# an unconditional single-value cache would leak the FIRST test's resolved
|
|
92
|
+
# root into every later test that calls project_root() or warn()/block()/
|
|
93
|
+
# remind() in-process). A payload's `cwd` is deliberately NOT part of the
|
|
94
|
+
# cache key: within one real hook process the payload is parsed at most once
|
|
95
|
+
# and never changes, so a payload-bearing call and a later no-payload call in
|
|
96
|
+
# the SAME (env, cwd) context are the SAME logical resolution and must return
|
|
97
|
+
# the SAME value — exactly the "payload-bearing first call, later no-arg
|
|
98
|
+
# calls stay consistent" contract. (A payload-only scenario — Codex, no
|
|
99
|
+
# CLAUDE_PROJECT_DIR — still resolves once: the first call's payload wins and
|
|
100
|
+
# is cached against the current (env, cwd); env/cwd don't change either.)
|
|
101
|
+
_ROOT_CACHE = {}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _root_cache_key():
|
|
105
|
+
return (os.environ.get("CLAUDE_PROJECT_DIR"), os.getcwd())
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _reset_root_cache():
|
|
109
|
+
"""Test-only: drop every memoized project_root() resolution. Production
|
|
110
|
+
hook processes never need this (each is single-shot); integration tests
|
|
111
|
+
that simulate MANY logical hook invocations in one Python process and
|
|
112
|
+
need a resolution to be genuinely re-computed (rather than served from an
|
|
113
|
+
still-valid (env, cwd) cache entry) call this between scenarios."""
|
|
114
|
+
_ROOT_CACHE.clear()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def marker_root(payload=None):
|
|
118
|
+
"""The root `.codearbiter/.markers/` gate passes (security-pass.py,
|
|
119
|
+
migration-pass.py, and the H-09b/H-10b/H-14 guards) are written to and
|
|
120
|
+
read from (#604) — see `hostapi.Host.marker_root`'s docstring for why
|
|
121
|
+
this is NOT the same thing as `project_root()` in a linked worktree.
|
|
122
|
+
|
|
123
|
+
Not memoized like `project_root()` above: called at most once or twice
|
|
124
|
+
per hook process (the marker checks, or a single `security-pass.py` /
|
|
125
|
+
`migration-pass.py` run), so the extra git spawn a linked-worktree
|
|
126
|
+
escalation occasionally costs is not worth a second cache to avoid."""
|
|
127
|
+
return get_host().marker_root(payload)
|
|
128
|
+
|
|
129
|
+
ARBITER_RE = re.compile(r"^\s*arbiter:\s*enabled\s*$", re.I)
|
|
130
|
+
|
|
131
|
+
def frontmatter_enabled_text(text):
|
|
132
|
+
"""(enabled, malformed) for CONTEXT.md *content* (see frontmatter_enabled).
|
|
133
|
+
Split out so the #159 Write/Edit guard can vet the RESULTING content of an
|
|
134
|
+
edit — 'does this edit keep the repo arbiter-enabled?' — without going to
|
|
135
|
+
disk, sharing one parser with the on-disk activation check so the two never
|
|
136
|
+
disagree on what 'enabled' means."""
|
|
137
|
+
lines = (text or "").split("\n")
|
|
138
|
+
if not lines:
|
|
139
|
+
return (False, False)
|
|
140
|
+
first = lines[0].lstrip("") # tolerate a leading UTF-8 BOM
|
|
141
|
+
if first.strip() != "---":
|
|
142
|
+
return (False, False) # no opening delimiter — dormant, not malformed
|
|
143
|
+
found = False
|
|
144
|
+
for ln in lines[1:]:
|
|
145
|
+
if ln.strip() == "---":
|
|
146
|
+
return (found, False) # closing delimiter — decision is final
|
|
147
|
+
if ARBITER_RE.match(ln):
|
|
148
|
+
found = True
|
|
149
|
+
return (False, True) # opened but never closed — malformed
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def frontmatter_enabled(ctx_path):
|
|
153
|
+
"""Return (enabled, malformed) for CONTEXT.md ON DISK. `enabled` iff
|
|
154
|
+
`arbiter: enabled` appears in a properly-closed leading YAML frontmatter
|
|
155
|
+
block. `malformed` iff a block opens (`---` on line 1) but never closes — the
|
|
156
|
+
fail-loud case. A file with no frontmatter at all is simply dormant (not
|
|
157
|
+
malformed). Unreadable file -> (False, False)."""
|
|
158
|
+
try:
|
|
159
|
+
with open(ctx_path, encoding="utf-8", errors="replace") as f:
|
|
160
|
+
text = f.read()
|
|
161
|
+
except Exception: # noqa: BLE001
|
|
162
|
+
return (False, False)
|
|
163
|
+
return frontmatter_enabled_text(text)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def arbiter_active(root):
|
|
167
|
+
"""True iff this repo opted in (`arbiter: enabled` in CONTEXT.md frontmatter).
|
|
168
|
+
Every enforcement hook gates on this so the plugin is genuinely dormant in
|
|
169
|
+
repos that never opted in — the plugin.json activation contract."""
|
|
170
|
+
enabled, _ = frontmatter_enabled(os.path.join(root, ".codearbiter", "CONTEXT.md"))
|
|
171
|
+
return enabled
|
|
172
|
+
|
|
173
|
+
def project_root(payload=None):
|
|
174
|
+
"""The project root. `CLAUDE_PROJECT_DIR` is the harness's own authoritative
|
|
175
|
+
signal and is trusted first: a hook subprocess is not guaranteed to start
|
|
176
|
+
with the project directory as its cwd, and a `git rev-parse` from elsewhere
|
|
177
|
+
can resolve to a different repo entirely (e.g. the plugin's own marketplace
|
|
178
|
+
clone). The env-first read also saves one git spawn per hook invocation.
|
|
179
|
+
Test harnesses that spawn hooks into fixture repos must pin the variable to
|
|
180
|
+
the fixture, as the production harness pins it to the project.
|
|
181
|
+
|
|
182
|
+
The resolution itself lives on the Host seam (hostapi.Host.project_root,
|
|
183
|
+
ADR-0011) — this function keeps its public signature (now accepting an
|
|
184
|
+
optional `payload`, architecture-006/#260, so a caller that already has
|
|
185
|
+
the parsed hook payload can hand it through to the payload-cwd leg) and
|
|
186
|
+
delegates, so every existing no-arg caller/import keeps working unchanged.
|
|
187
|
+
|
|
188
|
+
Memoized per (CLAUDE_PROJECT_DIR, process cwd) — see _ROOT_CACHE above for
|
|
189
|
+
the full contract (performance-001/003, #260): at most one resolution
|
|
190
|
+
(and at most one git spawn) per that key, so the repeated project_root()
|
|
191
|
+
reads inside block()/remind()/warn()'s gate-event logging don't each pay
|
|
192
|
+
a fresh subprocess."""
|
|
193
|
+
key = _root_cache_key()
|
|
194
|
+
if key not in _ROOT_CACHE:
|
|
195
|
+
_ROOT_CACHE[key] = get_host().project_root(payload)
|
|
196
|
+
return _ROOT_CACHE[key]
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# codeArbiter — statusline arbiter-state parsing (extracted from statusline.py,
|
|
3
|
+
# architecture-004).
|
|
4
|
+
#
|
|
5
|
+
# Owns reading the .codearbiter/ project-state directory into the compact dict the
|
|
6
|
+
# arbiter segment renders: frontmatter parsing, override/question counting, the
|
|
7
|
+
# mtime-keyed cache (a StopHook-driven statusline re-renders on every tool-call
|
|
8
|
+
# completion, so re-parsing 5 small files on every render would be wasteful), and
|
|
9
|
+
# the dev-mode marker check.
|
|
10
|
+
#
|
|
11
|
+
# The task-in-flight count and the arbiter-enabled gate are OWNED elsewhere
|
|
12
|
+
# (_taskboardlib.count_in_flight / _hooklib.frontmatter_enabled) so the box and the
|
|
13
|
+
# enforcement hooks agree on both contracts. This module never imports them
|
|
14
|
+
# directly — the caller (statusline.py) passes its own guarded-imported references
|
|
15
|
+
# through, so a test that monkeypatches statusline's fallback (e.g.
|
|
16
|
+
# `mod._count_in_flight = None`) is observed correctly on the next call.
|
|
17
|
+
#
|
|
18
|
+
# Design principles (mirroring _ledgerlib.py):
|
|
19
|
+
# - Stdlib only; no third-party imports ever.
|
|
20
|
+
# - Zero side effects at import time.
|
|
21
|
+
# - Never raise on malformed user input — degrade to a safe default.
|
|
22
|
+
#
|
|
23
|
+
# Public API:
|
|
24
|
+
# frontmatter(path) -> dict parsed leading YAML frontmatter block
|
|
25
|
+
# frontmatter_text(text) -> dict same parse, given already-read text (performance-003)
|
|
26
|
+
# count_matches(path, pattern) -> int regex match count in a file (0 on any I/O error)
|
|
27
|
+
# count_matches_text(text, pattern) -> int same count, given already-read text (performance-003)
|
|
28
|
+
# arbiter_state(root, count_in_flight=None, read_board=None, frontmatter_enabled=None,
|
|
29
|
+
# ctx_text=None, ot_text=None, oq_text=None) -> dict|None
|
|
30
|
+
# dev_active(root) -> bool True when the /dev marker is present
|
|
31
|
+
#
|
|
32
|
+
# performance-003 (#194): SessionStart's main() already reads CONTEXT.md,
|
|
33
|
+
# open-tasks.md, and open-questions.md before the display-only governance line
|
|
34
|
+
# is ever rendered. The optional ctx_text/ot_text/oq_text kwargs on
|
|
35
|
+
# arbiter_state let a caller that already holds that content thread it through
|
|
36
|
+
# instead of paying for a second disk read of the same file in the same
|
|
37
|
+
# process. None (the default) means "not supplied" -> falls back to the
|
|
38
|
+
# original read-from-disk behavior, so every existing caller (the standalone
|
|
39
|
+
# statusline.py render, direct arbiter_state(root) calls in tests) is
|
|
40
|
+
# unaffected.
|
|
41
|
+
|
|
42
|
+
import os
|
|
43
|
+
import re
|
|
44
|
+
|
|
45
|
+
# mtime-keyed memo: statusline.py is a short-lived subprocess, but a single render
|
|
46
|
+
# can resolve arbiter_state more than once (safe() probes), and the StopHook fires
|
|
47
|
+
# the whole script on every tool-call completion. Caching on max(input mtime) makes
|
|
48
|
+
# the 5 .codearbiter/ reads happen at most once per (root, change), re-reading only
|
|
49
|
+
# when one of the inputs actually changes between renders.
|
|
50
|
+
_ARBITER_CACHE = {} # root -> (mtime_key, result)
|
|
51
|
+
_ARBITER_FILES = ("CONTEXT.md", "overrides.log", "last-checkpoint",
|
|
52
|
+
"open-tasks.md", "open-questions.md", "sprint-active")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def frontmatter_text(text):
|
|
56
|
+
"""Parse a properly-closed leading YAML frontmatter block out of already-read
|
|
57
|
+
`text` into a key map (see frontmatter() for the on-disk counterpart — this is
|
|
58
|
+
the pure text half, extracted for performance-003 so a caller holding the
|
|
59
|
+
content already can skip a second disk read). Tolerates a leading UTF-8 BOM
|
|
60
|
+
character (\\ufeff) on the first line the same way frontmatter()'s utf-8-sig
|
|
61
|
+
decode does, regardless of how the caller's text was originally decoded."""
|
|
62
|
+
fm = {}
|
|
63
|
+
lines = (text or "").splitlines()
|
|
64
|
+
if not lines:
|
|
65
|
+
return fm
|
|
66
|
+
first = lines[0].lstrip("")
|
|
67
|
+
if first.strip() != "---":
|
|
68
|
+
return fm
|
|
69
|
+
closed = False
|
|
70
|
+
for ln in lines[1:]:
|
|
71
|
+
if ln.strip() == "---":
|
|
72
|
+
closed = True
|
|
73
|
+
break
|
|
74
|
+
m = re.match(r"^([A-Za-z0-9_-]+):\s*(.*)$", ln)
|
|
75
|
+
if m:
|
|
76
|
+
fm[m.group(1)] = m.group(2).strip()
|
|
77
|
+
# A valid YAML frontmatter block is bounded by BOTH delimiters; an unterminated
|
|
78
|
+
# block (no closing "---") is malformed — don't honor keys parsed to EOF.
|
|
79
|
+
return fm if closed else {}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def frontmatter(path):
|
|
83
|
+
"""Parse a properly-closed leading YAML frontmatter block into a key map. The
|
|
84
|
+
*arbiter-enabled* decision is NOT made here — that activation contract is owned
|
|
85
|
+
by _hooklib.frontmatter_enabled (see arbiter_state) so the box and the
|
|
86
|
+
enforcement hooks read it one way. This reader exists only to surface the
|
|
87
|
+
remaining display keys (e.g. `stage`) the boolean gate doesn't carry."""
|
|
88
|
+
try:
|
|
89
|
+
# utf-8-sig transparently strips a leading BOM (Windows editors / PowerShell
|
|
90
|
+
# Out-File default to UTF-8-with-BOM); plain utf-8 would leave it on line 1
|
|
91
|
+
# and break the "---" frontmatter check.
|
|
92
|
+
with open(path, encoding="utf-8-sig", errors="replace") as f:
|
|
93
|
+
text = f.read()
|
|
94
|
+
except OSError:
|
|
95
|
+
return {}
|
|
96
|
+
return frontmatter_text(text)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def count_matches_text(text, pattern):
|
|
100
|
+
"""Regex match count against already-read `text` (see count_matches() for the
|
|
101
|
+
on-disk counterpart — the pure text half, extracted for performance-003)."""
|
|
102
|
+
return len(re.findall(pattern, text or "", re.MULTILINE))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def count_matches(path, pattern):
|
|
106
|
+
try:
|
|
107
|
+
with open(path, encoding="utf-8", errors="replace") as f:
|
|
108
|
+
text = f.read()
|
|
109
|
+
except OSError:
|
|
110
|
+
return 0
|
|
111
|
+
return count_matches_text(text, pattern)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _arbiter_mtime_key(cad):
|
|
115
|
+
"""Max mtime across the arbiter input files (missing files stat as -1.0). Two
|
|
116
|
+
renders with the same key saw identical inputs, so the cached state is valid."""
|
|
117
|
+
latest = -1.0
|
|
118
|
+
for nm in _ARBITER_FILES:
|
|
119
|
+
try:
|
|
120
|
+
latest = max(latest, os.stat(os.path.join(cad, nm)).st_mtime)
|
|
121
|
+
except OSError:
|
|
122
|
+
pass
|
|
123
|
+
return latest
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _arbiter_enabled(ctx_path, frontmatter_enabled=None, ctx_text=None):
|
|
127
|
+
"""The arbiter-enabled gate, owned by _hooklib.frontmatter_enabled when the lib
|
|
128
|
+
is importable (so the box and the enforcement hooks agree on the activation
|
|
129
|
+
contract). Falls back to the local frontmatter() parser only if the import
|
|
130
|
+
failed — the defensive degrade path, never a hard dependency.
|
|
131
|
+
|
|
132
|
+
When `ctx_text` is supplied (performance-003: the caller already read
|
|
133
|
+
CONTEXT.md), the decision is made from that text — no re-read of ctx_path."""
|
|
134
|
+
if ctx_text is not None:
|
|
135
|
+
return frontmatter_text(ctx_text).get("arbiter", "").lower() == "enabled"
|
|
136
|
+
if frontmatter_enabled is not None:
|
|
137
|
+
try:
|
|
138
|
+
enabled, _malformed = frontmatter_enabled(ctx_path)
|
|
139
|
+
return enabled
|
|
140
|
+
except Exception: # noqa: BLE001 — degrade to the local parser, never crash
|
|
141
|
+
pass
|
|
142
|
+
return frontmatter(ctx_path).get("arbiter", "").lower() == "enabled"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def arbiter_state(root, count_in_flight=None, read_board=None, frontmatter_enabled=None,
|
|
146
|
+
ctx_text=None, ot_text=None, oq_text=None):
|
|
147
|
+
cad = os.path.join(root, ".codearbiter")
|
|
148
|
+
mkey = _arbiter_mtime_key(cad)
|
|
149
|
+
cached = _ARBITER_CACHE.get(root)
|
|
150
|
+
if cached is not None and cached[0] == mkey:
|
|
151
|
+
return cached[1]
|
|
152
|
+
result = _arbiter_state_uncached(cad, count_in_flight, read_board, frontmatter_enabled,
|
|
153
|
+
ctx_text=ctx_text, ot_text=ot_text, oq_text=oq_text)
|
|
154
|
+
_ARBITER_CACHE[root] = (mkey, result)
|
|
155
|
+
return result
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _arbiter_state_uncached(cad, count_in_flight=None, read_board=None, frontmatter_enabled=None,
|
|
159
|
+
ctx_text=None, ot_text=None, oq_text=None):
|
|
160
|
+
ctx_path = os.path.join(cad, "CONTEXT.md")
|
|
161
|
+
if not _arbiter_enabled(ctx_path, frontmatter_enabled, ctx_text=ctx_text):
|
|
162
|
+
return None
|
|
163
|
+
# performance-003: reuse the caller's already-read CONTEXT.md/open-tasks.md/
|
|
164
|
+
# open-questions.md text when supplied, instead of a second disk read. `None`
|
|
165
|
+
# (the default) preserves the exact original read-from-disk behavior.
|
|
166
|
+
fm = frontmatter_text(ctx_text) if ctx_text is not None else frontmatter(ctx_path)
|
|
167
|
+
total_over = count_matches(os.path.join(cad, "overrides.log"), r"^(?!\s*#)(?!\s*$).+")
|
|
168
|
+
# last-checkpoint holds the override COUNT at the last /ca:checkpoint. A value
|
|
169
|
+
# outside [0, total] is not a valid count (e.g. a timestamp from a stale writer)
|
|
170
|
+
# -> fail safe to 0 so overrides are surfaced, never silently hidden.
|
|
171
|
+
try:
|
|
172
|
+
with open(os.path.join(cad, "last-checkpoint"), encoding="utf-8") as f:
|
|
173
|
+
base = int(f.read().strip() or "0")
|
|
174
|
+
except (OSError, ValueError):
|
|
175
|
+
base = 0
|
|
176
|
+
if base < 0 or base > total_over:
|
|
177
|
+
base = 0
|
|
178
|
+
ot_path = os.path.join(cad, "open-tasks.md")
|
|
179
|
+
if count_in_flight is not None:
|
|
180
|
+
board_text = ot_text if ot_text is not None else (read_board(ot_path) or "")
|
|
181
|
+
tasks = count_in_flight(board_text)
|
|
182
|
+
else:
|
|
183
|
+
# Degraded fallback (only if _taskboardlib failed to import): mirror
|
|
184
|
+
# count_in_flight's done-exclusion inline so the segment never silently
|
|
185
|
+
# re-inflates to the pre-schema count. Never crashes the box.
|
|
186
|
+
tasks = (count_matches_text(ot_text, r"^- (?!\[[xX]\])") if ot_text is not None
|
|
187
|
+
else count_matches(ot_path, r"^- (?!\[[xX]\])"))
|
|
188
|
+
q = (count_matches_text(oq_text, r"CONFIRM-[0-9]+") if oq_text is not None
|
|
189
|
+
else count_matches(os.path.join(cad, "open-questions.md"), r"CONFIRM-[0-9]+"))
|
|
190
|
+
return {
|
|
191
|
+
"stage": fm.get("stage", "-"),
|
|
192
|
+
"tasks": tasks,
|
|
193
|
+
"q": q,
|
|
194
|
+
"over": max(0, total_over - base),
|
|
195
|
+
"sprint": os.path.exists(os.path.join(cad, "sprint-active")),
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def dev_active(root):
|
|
200
|
+
"""True when /dev developer-override mode is on — signalled by a transient marker
|
|
201
|
+
the orchestrator drops on /dev and clears on /arbiter (a local UI flag, not a log).
|
|
202
|
+
|
|
203
|
+
Presence-only by design, unaffected by #271 C-5's session-scoped clearing:
|
|
204
|
+
the marker still means "dev mode is on for SOMEONE" regardless of which
|
|
205
|
+
session owns it. Session-scoping only changes WHEN SessionStart is willing
|
|
206
|
+
to clear a live marker (never a different, possibly still-live session's
|
|
207
|
+
own marker) — it does not change what "present" means to this reader."""
|
|
208
|
+
return os.path.exists(os.path.join(root, ".codearbiter", ".markers", "dev-active"))
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""pr-babysitter config resolution (Feature 2).
|
|
2
|
+
|
|
3
|
+
A pure config-resolution unit for the pr-babysitter command. This module holds
|
|
4
|
+
NO command behavior — only the env-driven resolver that decides whether the
|
|
5
|
+
babysitter is enabled and how it reacts to a red gate. The command behavior is
|
|
6
|
+
authored as prose in a later task.
|
|
7
|
+
|
|
8
|
+
Environment variables
|
|
9
|
+
---------------------
|
|
10
|
+
CODEARBITER_BABYSIT
|
|
11
|
+
Master switch. Accepted "on" spellings (case-insensitive): ``on``, ``true``,
|
|
12
|
+
``1``. Anything else — including absent, empty, or an unknown value —
|
|
13
|
+
resolves to OFF. Default: OFF (PB-8). Even when set on, the babysitter is
|
|
14
|
+
two-layer gated on arbiter dormancy: it stays OFF unless ``arbiter_active``
|
|
15
|
+
reports the repo opted in (PB-10). It is NEVER auto-enabled.
|
|
16
|
+
|
|
17
|
+
CODEARBITER_BABYSIT_ONRED
|
|
18
|
+
What to do when the gate goes red. Normalized lowercase; one of ``propose``
|
|
19
|
+
or ``branch``. Any unknown, empty, or absent value resolves to ``propose``.
|
|
20
|
+
Default: ``propose`` (PB-5).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
_ON_VALUES = ("on", "true", "1")
|
|
24
|
+
_ONRED_VALUES = ("propose", "branch")
|
|
25
|
+
_ONRED_DEFAULT = "propose"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def babysit_config(env, root, arbiter_active=None):
|
|
29
|
+
"""Resolve the pr-babysitter config from ``env`` (a dict) for ``root``.
|
|
30
|
+
|
|
31
|
+
``env`` and ``root`` are explicit parameters — this resolver never reads or
|
|
32
|
+
mutates ``os.environ``. ``arbiter_active`` is injected (defaulting to
|
|
33
|
+
``_hooklib.arbiter_active``) so tests can pass a stub for the dormancy gate.
|
|
34
|
+
|
|
35
|
+
Returns a dict with at least ``enabled`` (bool) and ``on_red`` (str).
|
|
36
|
+
"""
|
|
37
|
+
if arbiter_active is None:
|
|
38
|
+
import _hooklib
|
|
39
|
+
arbiter_active = _hooklib.arbiter_active
|
|
40
|
+
|
|
41
|
+
raw = (env.get("CODEARBITER_BABYSIT", "off") or "off").lower()
|
|
42
|
+
switched_on = raw in _ON_VALUES
|
|
43
|
+
# PB-10: two-layer gate — the env switch never overrides arbiter dormancy.
|
|
44
|
+
enabled = switched_on and bool(arbiter_active(root))
|
|
45
|
+
|
|
46
|
+
on_red = (env.get("CODEARBITER_BABYSIT_ONRED", "") or "").lower()
|
|
47
|
+
if on_red not in _ONRED_VALUES:
|
|
48
|
+
on_red = _ONRED_DEFAULT # PB-5
|
|
49
|
+
|
|
50
|
+
return {"enabled": enabled, "on_red": on_red}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def main(argv=None):
|
|
54
|
+
"""CLI shim: resolve the babysitter config against the live environment and
|
|
55
|
+
print it as one JSON line, so /ca:pr and /ca:watch invoke this single
|
|
56
|
+
resolver instead of re-implementing the flag check in prose (no drift from
|
|
57
|
+
the accepted on|true|1 spellings or the PB-10 dormancy gate). Fail-safe:
|
|
58
|
+
any error degrades to the OFF default and still exits 0 — a broken resolver
|
|
59
|
+
must never become a reason to auto-attach a watcher.
|
|
60
|
+
|
|
61
|
+
Import-only module (no `__main__` entry point, per the thin-entry-hook
|
|
62
|
+
convention): the thin entry hook `babysit.py` imports and calls this
|
|
63
|
+
function; this underscore module is never run as a script directly."""
|
|
64
|
+
import argparse
|
|
65
|
+
import json
|
|
66
|
+
import os
|
|
67
|
+
|
|
68
|
+
parser = argparse.ArgumentParser(add_help=True)
|
|
69
|
+
parser.add_argument("--root", default=os.getcwd())
|
|
70
|
+
args = parser.parse_args(argv)
|
|
71
|
+
try:
|
|
72
|
+
cfg = babysit_config(os.environ, args.root)
|
|
73
|
+
except Exception: # noqa: BLE001
|
|
74
|
+
cfg = {"enabled": False, "on_red": _ONRED_DEFAULT}
|
|
75
|
+
print(json.dumps(cfg))
|
|
76
|
+
return 0
|