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