@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,411 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# codeArbiter - protected-state registry: which project-state files are
|
|
3
|
+
# guarded, and under what POLICY (issue #564, workstream B1).
|
|
4
|
+
#
|
|
5
|
+
# Three consumers need materially different write semantics, so a flat
|
|
6
|
+
# marker-gated registry (the H-11 ADR-authoring pattern) is wrong for two of
|
|
7
|
+
# them. Each registry entry will carry exactly one of:
|
|
8
|
+
#
|
|
9
|
+
# marker-gated - a Write/Edit/shell mutation is admitted only under a
|
|
10
|
+
# fresh authoring marker (the H-11 pattern: a
|
|
11
|
+
# `<stem>-authoring` marker under .codearbiter/.markers/,
|
|
12
|
+
# minted immediately before the write and removed at lane
|
|
13
|
+
# exit). First consumer: release-targets.md.
|
|
14
|
+
# helper-only - Write, Edit, and shell naming the file are hard-blocked
|
|
15
|
+
# with NO marker path at all. The sanctioned helper's own
|
|
16
|
+
# Python file I/O is the only route - its argv never
|
|
17
|
+
# lexically names the file, so it is invisible to all three
|
|
18
|
+
# flanks by construction. Consumer: open-tasks.md via
|
|
19
|
+
# taskwrite.py.
|
|
20
|
+
# append-only - mutation is admitted only via the helper's append verb.
|
|
21
|
+
# Consumer: done-tasks.md via the archive verb.
|
|
22
|
+
#
|
|
23
|
+
# THE REGISTRY IS THE DELIVERABLE, NOT THE ENTRIES (spec B1). This module is
|
|
24
|
+
# generic machinery over a policy-tagged path->policy map, not hardcoded
|
|
25
|
+
# per-consumer logic. release-targets.md, open-tasks.md, and done-tasks.md
|
|
26
|
+
# were enrolled by their own tasks (B-13/B-14/B-15) below, so no consumer is
|
|
27
|
+
# special-cased in the machinery itself.
|
|
28
|
+
#
|
|
29
|
+
# Library design invariants (mirrors every other _*lib.py, coding-standards.md):
|
|
30
|
+
# - Zero side effects at import time - no git calls, no file I/O on import.
|
|
31
|
+
# - Pure functions, testable with synthetic input; filesystem access will be
|
|
32
|
+
# isolated to `_hooklib.marker_fresh`, the one named reader this module
|
|
33
|
+
# calls (reused rather than re-implemented, per house rule).
|
|
34
|
+
#
|
|
35
|
+
# Public API (filled in task-by-task; see history for the T-01/T-04/T-05
|
|
36
|
+
# slices):
|
|
37
|
+
# ProtectedPolicy enum: MARKER_GATED / HELPER_ONLY /
|
|
38
|
+
# APPEND_ONLY, str-valued
|
|
39
|
+
# ("marker-gated"/"helper-only"/
|
|
40
|
+
# "append-only"). Constructing from an
|
|
41
|
+
# unknown value raises ValueError
|
|
42
|
+
# (internal error, not user input).
|
|
43
|
+
# REGISTRY -> dict[str, ProtectedPolicy] the live path->policy map.
|
|
44
|
+
# Three consumers are registered below
|
|
45
|
+
# (release-targets.md, open-tasks.md,
|
|
46
|
+
# done-tasks.md); a future consumer adds
|
|
47
|
+
# its own entry rather than being
|
|
48
|
+
# hardcoded in this module.
|
|
49
|
+
# lookup_policy(rel_path, registry=None) -> ProtectedPolicy | None
|
|
50
|
+
# the policy registered for a
|
|
51
|
+
# repo-relative path. Both the query
|
|
52
|
+
# path and every registry key are
|
|
53
|
+
# separator-normalized before
|
|
54
|
+
# comparison, or None if the path
|
|
55
|
+
# carries no policy. `registry` defaults
|
|
56
|
+
# to the module-level REGISTRY; a caller
|
|
57
|
+
# (a test, or a future flank) may pass a
|
|
58
|
+
# synthetic dict instead. Reached only
|
|
59
|
+
# through `classify_protected`
|
|
60
|
+
# (_protectedlib.py) in the wired path -
|
|
61
|
+
# never called directly with an ad hoc
|
|
62
|
+
# dict, which would drop the
|
|
63
|
+
# raw-and-realpath symlink coverage that
|
|
64
|
+
# dispatch provides.
|
|
65
|
+
# resolve_registered_path(fpath, root, registry=None)
|
|
66
|
+
# -> (rel_path, ProtectedPolicy) | (None,
|
|
67
|
+
# None). The T-06/T-07 flank helper: once
|
|
68
|
+
# classify_protected has already reported
|
|
69
|
+
# "state" for `fpath`, this resolves
|
|
70
|
+
# WHICH registered path matched and WHICH
|
|
71
|
+
# policy it carries, trying both the raw
|
|
72
|
+
# normalized path and its
|
|
73
|
+
# realpath-resolved repo-relative form -
|
|
74
|
+
# the SAME two forms classify_protected
|
|
75
|
+
# itself tries (#162) - so the flank
|
|
76
|
+
# resolves the identical entry
|
|
77
|
+
# classify_protected saw rather than
|
|
78
|
+
# re-deriving membership through an
|
|
79
|
+
# independent check.
|
|
80
|
+
# MARKER_FRESHNESS_MINUTES -> int re-exported from _hooklib (issue #567):
|
|
81
|
+
# the single H-11 marker window (30),
|
|
82
|
+
# now declared exactly once and
|
|
83
|
+
# imported by every flank rather than
|
|
84
|
+
# independently hardcoded.
|
|
85
|
+
# marker_name_for(rel_path) -> str a marker basename for a marker-gated
|
|
86
|
+
# path, encoding the path BELOW the
|
|
87
|
+
# repo's .codearbiter/ project-state
|
|
88
|
+
# root (not just the basename) so
|
|
89
|
+
# same-named files in different
|
|
90
|
+
# sub-directories never share a marker
|
|
91
|
+
# (e.g. ".codearbiter/release-targets.md"
|
|
92
|
+
# -> "release-targets-authoring", the
|
|
93
|
+
# pinned literal for the sole current
|
|
94
|
+
# consumer; "docs/release-targets.md"
|
|
95
|
+
# -> "docs__release-targets-authoring").
|
|
96
|
+
# Always a single flat, traversal-safe
|
|
97
|
+
# filename; degenerate input (empty,
|
|
98
|
+
# None, a bare directory) never raises
|
|
99
|
+
# and never collides with a real path's
|
|
100
|
+
# derived name.
|
|
101
|
+
# marker_gated_write_admitted(rel_path, root,
|
|
102
|
+
# minutes=MARKER_FRESHNESS_MINUTES) -> bool
|
|
103
|
+
# True iff a fresh `<stem>-authoring`
|
|
104
|
+
# marker exists under
|
|
105
|
+
# .codearbiter/.markers/ for `rel_path`.
|
|
106
|
+
# False on an absent marker AND on a
|
|
107
|
+
# stale one - both cases delegate
|
|
108
|
+
# entirely to _hooklib.marker_fresh, so
|
|
109
|
+
# there is exactly one freshness
|
|
110
|
+
# implementation in the codebase. For
|
|
111
|
+
# `marker-gated` only; the flank wiring
|
|
112
|
+
# (pre-write.py, pre-edit.py,
|
|
113
|
+
# _bashguardlib.py per-class dispatch)
|
|
114
|
+
# is hook ID H-22 (H-21 is taken),
|
|
115
|
+
# built in later tasks of this slice -
|
|
116
|
+
# this function supplies the check the
|
|
117
|
+
# flanks will share, and does not
|
|
118
|
+
# itself call block()/remind().
|
|
119
|
+
|
|
120
|
+
from __future__ import annotations
|
|
121
|
+
|
|
122
|
+
import os
|
|
123
|
+
from enum import Enum
|
|
124
|
+
|
|
125
|
+
from _hooklib import MARKER_FRESHNESS_MINUTES, marker_fresh
|
|
126
|
+
from _pathnorm import norm_path, raw_repo_rel, repo_rel
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class ProtectedPolicy(str, Enum):
|
|
130
|
+
"""The three write-admission policies a protected-state registry entry
|
|
131
|
+
can carry (spec B1). str-valued so a member compares equal to its plain
|
|
132
|
+
string value (`ProtectedPolicy.MARKER_GATED == "marker-gated"`) and
|
|
133
|
+
round-trips through JSON without a second mapping layer.
|
|
134
|
+
|
|
135
|
+
Interpolation caveat: use `.value`, not the member itself. `str(member)` /
|
|
136
|
+
an f-string / `%s` on a bare member yields `"ProtectedPolicy.MARKER_GATED"`
|
|
137
|
+
on Python 3.11+ (and the plain value on <=3.10) - the mixin does not make
|
|
138
|
+
those two forms agree across interpreters. Only `.value` (or the
|
|
139
|
+
already-proven `==` against a plain string) is the stable contract.
|
|
140
|
+
|
|
141
|
+
Constructing an unknown value (`ProtectedPolicy("bogus")`) raises
|
|
142
|
+
ValueError via the stdlib Enum machinery - deliberately NOT caught here.
|
|
143
|
+
An unrecognized policy string reaching this constructor is a typo'd
|
|
144
|
+
registry entry - an internal programming error, not malformed user
|
|
145
|
+
input - so it is correct to raise rather than degrade."""
|
|
146
|
+
|
|
147
|
+
MARKER_GATED = "marker-gated"
|
|
148
|
+
HELPER_ONLY = "helper-only"
|
|
149
|
+
APPEND_ONLY = "append-only"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# The live protected-state registry: repo-relative path (separator-
|
|
153
|
+
# normalized) -> ProtectedPolicy. B1 (this module) built the registry
|
|
154
|
+
# mechanism; release-targets.md/open-tasks.md/done-tasks.md were added by
|
|
155
|
+
# their own later tasks (B-13/B-14/B-15), each a one-line entry, which is the
|
|
156
|
+
# whole point of building this as a registry instead of a per-file hook
|
|
157
|
+
# branch. A future consumer adds one more entry here, not a new branch.
|
|
158
|
+
REGISTRY: dict[str, ProtectedPolicy] = {
|
|
159
|
+
# B-13/T-33 (spec 2.6). The declared release-target file carries
|
|
160
|
+
# per-row `pre-tag`, `rebuild`, and `generate` shell commands that
|
|
161
|
+
# `/ca:release` executes before composing a tag, on a lane that later
|
|
162
|
+
# holds `contents: write`. Planting a command in it is therefore a
|
|
163
|
+
# code-execution path, which is why writing it costs a fresh authoring
|
|
164
|
+
# marker rather than being an ordinary edit (ADR-0024, DECISION-0035).
|
|
165
|
+
#
|
|
166
|
+
# MARKER_GATED, not HELPER_ONLY: unlike `open-tasks.md` -- whose sole
|
|
167
|
+
# blessed writer is `taskwrite.py`, writing through Python file I/O
|
|
168
|
+
# whose argv never names the file and is therefore invisible to every
|
|
169
|
+
# flank by construction -- this file has THREE sanctioned authors
|
|
170
|
+
# (`context-creation`, the release skill's back-fill lane, and its
|
|
171
|
+
# row-edit path), all of which mint the marker. A hard block would
|
|
172
|
+
# leave them no route; the marker is the route.
|
|
173
|
+
".codearbiter/release-targets.md": ProtectedPolicy.MARKER_GATED,
|
|
174
|
+
|
|
175
|
+
# B-14/T-66. HELPER_ONLY, with NO marker path at all — deliberately
|
|
176
|
+
# unlike release-targets.md above.
|
|
177
|
+
#
|
|
178
|
+
# `taskwrite.py` is the board's sole blessed writer and writes through
|
|
179
|
+
# Python file I/O whose argv never names the file, so it is invisible
|
|
180
|
+
# to every lexical flank by construction. A marker would therefore add
|
|
181
|
+
# nothing for the helper while ADMITTING an agent that hand-composes
|
|
182
|
+
# board markdown under it — the inversion of the goal.
|
|
183
|
+
#
|
|
184
|
+
# Enrolment is non-circular, but NOT for the reason this comment used
|
|
185
|
+
# to give. It claimed the guard "never sees the helper's write", which
|
|
186
|
+
# is true of the write and irrelevant to the guard: the guard reads the
|
|
187
|
+
# COMMAND LINE, and the helper's own launcher is `python3
|
|
188
|
+
# ".../taskwrite.py" <verb> …`. The interpreter leg matched that
|
|
189
|
+
# `python3` and blocked every invocation whose free-text argv named an
|
|
190
|
+
# enrolled basename — including `done`/`archive` on an ID-less task
|
|
191
|
+
# whose title names one, where the title IS the target and rewording it
|
|
192
|
+
# would itself require a board write. The workstream-B adversary drove
|
|
193
|
+
# the real `pre-bash.py` and confirmed it.
|
|
194
|
+
#
|
|
195
|
+
# What actually makes it non-circular is the interpreter leg's
|
|
196
|
+
# inline-code requirement (`_INTERP_INLINE_CODE` in `_bashguardlib`):
|
|
197
|
+
# running a FILE and passing the basename as argv is data, executing a
|
|
198
|
+
# STRING that names it is a write. Pinned by
|
|
199
|
+
# `test_filename_as_helper_argv_data_passes` and
|
|
200
|
+
# `test_helper_write_verbs_pass_when_the_title_names_the_file`, both on
|
|
201
|
+
# the real invocation shape — the prior pins asserted a bare
|
|
202
|
+
# `taskwrite add …` that no surface emits.
|
|
203
|
+
".codearbiter/open-tasks.md": ProtectedPolicy.HELPER_ONLY,
|
|
204
|
+
|
|
205
|
+
# B-15/T-65. APPEND_ONLY: a completed task has exactly one permanent
|
|
206
|
+
# record, so mutation is admitted through an append verb alone. The
|
|
207
|
+
# archival sweep only ever appends here — `taskwrite archive` writes
|
|
208
|
+
# done-tasks BEFORE removing from open-tasks, so an interrupted run
|
|
209
|
+
# leaves a duplicate the next run dedups, never a lost record.
|
|
210
|
+
".codearbiter/done-tasks.md": ProtectedPolicy.APPEND_ONLY,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _canon(rel_path):
|
|
215
|
+
"""Canonical comparison form of a repo-relative path: separator-
|
|
216
|
+
normalized (`norm_path`), then whitespace-stripped, `./`-prefix-
|
|
217
|
+
stripped (repeatable — "././x" too), doubled-slash-collapsed,
|
|
218
|
+
trailing-slash-stripped, and finally case-folded.
|
|
219
|
+
|
|
220
|
+
Applied to BOTH sides of every `lookup_policy` comparison (the query
|
|
221
|
+
path AND every registry key) so a spelling difference on either side
|
|
222
|
+
degrades to "still matches" rather than "silently matches nothing" —
|
|
223
|
+
the same "a malformed key degrades the way a malformed query path does"
|
|
224
|
+
principle `lookup_policy`'s own docstring states, extended to cover the
|
|
225
|
+
specific spellings its docstring already promised but the OLD
|
|
226
|
+
norm_path-only comparison silently missed (#564 follow-up, finding F2):
|
|
227
|
+
a leading `./`, a trailing slash, a doubled slash, and a leading space.
|
|
228
|
+
|
|
229
|
+
Case-folded — not merely separator-normalized — for a second,
|
|
230
|
+
independent reason (finding F1): `_bashguardlib.py`'s H-22 shell-flank
|
|
231
|
+
regexes compile with `re.I` (`_state_write_res`), so a case-sensitive
|
|
232
|
+
equality check here would let the two flanks disagree on whether a
|
|
233
|
+
differently-cased spelling of a registered path is protected. On a
|
|
234
|
+
case-preserving-but-insensitive filesystem (default macOS/APFS,
|
|
235
|
+
Windows/NTFS) that disagreement is a live fail-open: `_protectedlib.
|
|
236
|
+
classify_protected` resolves through `os.path.realpath`, which does
|
|
237
|
+
NOT canonicalize case for an EXISTING path on a case-insensitive mount
|
|
238
|
+
(posixpath.realpath never folds case at all; even `nt.realpath`, which
|
|
239
|
+
does resolve an existing file's on-disk case, cannot help a NOT-YET-
|
|
240
|
+
created file — exactly the Write that creates a protected-state file
|
|
241
|
+
for the first time) — so `Write(".codearbiter/Open-Tasks.md")` could
|
|
242
|
+
reach this equality check with a case that never gets folded away
|
|
243
|
+
before comparison. This module deliberately picks ONE fixed rule,
|
|
244
|
+
case-INSENSITIVE, GLOBALLY, rather than "whatever this host's
|
|
245
|
+
filesystem happens to do": matching host behavior is not obviously
|
|
246
|
+
right either (it varies per platform AND per volume on the same
|
|
247
|
+
platform), and a fixed global rule is the only option `_bashguardlib.py`
|
|
248
|
+
can mirror without itself inspecting the filesystem. Choosing
|
|
249
|
+
case-INSENSITIVE (not case-sensitive) only WIDENS what H-22 protects —
|
|
250
|
+
consistent with this codebase's "ambiguity resolves CLOSED" stance
|
|
251
|
+
(module comment, `_bashguardlib.py`) — at the cost of a same-directory
|
|
252
|
+
file whose name differs from a registered path ONLY by case (e.g. a
|
|
253
|
+
genuinely different `OPEN-TASKS.MD`) being treated as protected too; a
|
|
254
|
+
registry entry choosing a name that collides with a real sibling file
|
|
255
|
+
under a case change is expected to be rare enough that this is judged
|
|
256
|
+
the right trade."""
|
|
257
|
+
p = norm_path(rel_path).strip()
|
|
258
|
+
while p.startswith("./"):
|
|
259
|
+
p = p[2:]
|
|
260
|
+
while "//" in p:
|
|
261
|
+
p = p.replace("//", "/")
|
|
262
|
+
p = p.rstrip("/")
|
|
263
|
+
return p.lower()
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def lookup_policy(rel_path, registry=None):
|
|
267
|
+
"""The ProtectedPolicy registered for `rel_path`, or None if it carries
|
|
268
|
+
no policy. Both `rel_path` and every registry key are canonicalized
|
|
269
|
+
(`_canon`, above) before comparison — separator-normalized, `./`/
|
|
270
|
+
doubled-slash/trailing-slash/leading-space tolerant, and
|
|
271
|
+
case-INSENSITIVE (deliberately, globally — see `_canon`'s docstring for
|
|
272
|
+
why) — so a Windows backslash path, a `./`-prefixed or trailing-slash
|
|
273
|
+
query, or a differently-cased spelling all match a registry entry, AND
|
|
274
|
+
a registry entry that was itself typo'd any of those ways still matches
|
|
275
|
+
rather than silently protecting nothing - a malformed *key* degrades the
|
|
276
|
+
same way a malformed query path does; only a malformed *policy* (see
|
|
277
|
+
ProtectedPolicy) is an internal error worth raising on.
|
|
278
|
+
|
|
279
|
+
`registry` defaults to the module-level REGISTRY; a test (or a future
|
|
280
|
+
caller) may pass a synthetic dict instead, which is what keeps this
|
|
281
|
+
function generic machinery rather than something wired to a specific
|
|
282
|
+
consumer set. This module must be reached through `classify_protected`
|
|
283
|
+
(`_protectedlib.py`), which runs every classifier against both the raw
|
|
284
|
+
and realpath-resolved forms of a path - a flank that calls
|
|
285
|
+
`lookup_policy` directly with its own ad hoc dict re-opens the symlink
|
|
286
|
+
alias this module does not itself guard against."""
|
|
287
|
+
if registry is None:
|
|
288
|
+
registry = REGISTRY
|
|
289
|
+
normalized = _canon(rel_path)
|
|
290
|
+
for key, policy in registry.items():
|
|
291
|
+
if _canon(key) == normalized:
|
|
292
|
+
return policy
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def resolve_registered_path(fpath, root, registry=None):
|
|
297
|
+
"""The `(rel_path, policy)` pair a Write/Edit/shell target resolves to,
|
|
298
|
+
once `_protectedlib.classify_protected` has already reported "state" for
|
|
299
|
+
it - or `(None, None)` if it turns out to carry no registry entry after
|
|
300
|
+
all (a caller that checks this before ever consulting
|
|
301
|
+
classify_protected, or a stale class set).
|
|
302
|
+
|
|
303
|
+
Tries BOTH the raw (symlink-unresolved) repo-relative form
|
|
304
|
+
(`raw_repo_rel`) and the realpath-resolved repo-relative form
|
|
305
|
+
(`repo_rel`), in that order - the SAME two-form symlink-safety property
|
|
306
|
+
(#162) classify_protected's four legacy classes get automatically from
|
|
307
|
+
running a regex `.search()` over the raw normalized path text.
|
|
308
|
+
|
|
309
|
+
That automatic coverage does NOT transfer for free to this module's
|
|
310
|
+
EQUALITY-based lookup (finding F3, #564 follow-up): `norm_path(fpath)` -
|
|
311
|
+
almost always an ABSOLUTE path, since every host sends one - is never
|
|
312
|
+
equal to a repo-relative registry key, so trying it as the "raw" leg was
|
|
313
|
+
inert (it could never match anything). Worse, it made symlink coverage
|
|
314
|
+
the WRONG WAY ROUND versus the other four classes: when the REGISTERED
|
|
315
|
+
PATH ITSELF is a symlink pointing somewhere unregistered,
|
|
316
|
+
`os.path.realpath` resolves the ONLY spelling a host actually sends
|
|
317
|
+
(the absolute path) straight through the symlink to that unregistered
|
|
318
|
+
target, and the dead raw leg supplied no alternative route back to the
|
|
319
|
+
registered name — so the write was silently ADMITTED, the opposite of
|
|
320
|
+
the legacy classes' behavior in the equivalent scenario (a regex
|
|
321
|
+
`.search()` still matches the raw path text regardless of where it
|
|
322
|
+
realpaths to). `raw_repo_rel` fixes this: computed by pure lexical
|
|
323
|
+
arithmetic against `root` (no `os.path.realpath` call), it still names
|
|
324
|
+
the registered entry syntactically even when the path is a symlink, so
|
|
325
|
+
that spelling now resolves correctly too - restoring the SAME
|
|
326
|
+
"protected either way you spell it" guarantee the legacy classes
|
|
327
|
+
already had; the realpath leg still exists for the mirror-image case
|
|
328
|
+
(a symlinked DIRECTORY whose visible path lacks the registered prefix
|
|
329
|
+
but resolves into it).
|
|
330
|
+
|
|
331
|
+
So a flank reaching this function resolves the IDENTICAL entry
|
|
332
|
+
classify_protected saw, rather than re-deriving membership through an
|
|
333
|
+
independent check. That independent-check shape is exactly what #564's
|
|
334
|
+
design forbids ("no second, parallel lookup") - this function only ever
|
|
335
|
+
RESOLVES what classify_protected already decided; it never decides
|
|
336
|
+
membership on its own account.
|
|
337
|
+
|
|
338
|
+
`registry` defaults to the module-level REGISTRY, matching
|
|
339
|
+
`lookup_policy`'s own parameter shape, for the same reason: a test (or a
|
|
340
|
+
future caller) may pass a synthetic dict."""
|
|
341
|
+
for p in (raw_repo_rel(fpath, root), repo_rel(fpath, root)):
|
|
342
|
+
if not p:
|
|
343
|
+
continue
|
|
344
|
+
policy = lookup_policy(p, registry)
|
|
345
|
+
if policy is not None:
|
|
346
|
+
return p, policy
|
|
347
|
+
return None, None
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def marker_name_for(rel_path):
|
|
351
|
+
"""The `<...>-authoring` marker basename for a marker-gated `rel_path`
|
|
352
|
+
(e.g. "release-targets.md" -> "release-targets-authoring") - the pattern
|
|
353
|
+
named in the sprint's pre-run dispositions for every future marker-gated
|
|
354
|
+
consumer, not just the first.
|
|
355
|
+
|
|
356
|
+
Encodes the normalized relative path below the repo's single
|
|
357
|
+
`.codearbiter/` project-state root, not just the basename, so two
|
|
358
|
+
registry entries that merely share a filename in different
|
|
359
|
+
sub-directories (".codearbiter/release-targets.md" vs
|
|
360
|
+
".codearbiter/nested/release-targets.md", or "a/x.md" vs "b/x.yml") mint
|
|
361
|
+
two distinct markers rather than one shared one - minting the marker for
|
|
362
|
+
one would otherwise admit a write to the other. A single leading
|
|
363
|
+
`.codearbiter` segment is dropped before encoding rather than treated as
|
|
364
|
+
disambiguating structure: every registry entry lives there by
|
|
365
|
+
construction (this module's whole domain), so keeping it out of the
|
|
366
|
+
encoding is what leaves the pinned literal for the sole current
|
|
367
|
+
marker-gated consumer unchanged (`.codearbiter/release-targets.md` ->
|
|
368
|
+
`release-targets-authoring`) while still telling apart two DIFFERENT
|
|
369
|
+
sub-directories. A literal `-` inside a directory segment is escaped
|
|
370
|
+
(doubled) before segments are joined with `-`, so a raw `-` in the
|
|
371
|
+
encoded name always marks a genuine directory boundary - this closes the
|
|
372
|
+
same-string collision a naive join would allow between, e.g., a
|
|
373
|
+
"prefix-name" directory and a "prefix"/"name" nested pair. `.`, `..`, and
|
|
374
|
+
empty segments are dropped before encoding (not merely trusted to
|
|
375
|
+
`os.path.basename`), so a stray `./` prefix, a trailing slash, or a
|
|
376
|
+
crafted `../` segment can never reach the returned name; the result is
|
|
377
|
+
always a single flat filename, never a path, so it stays contained under
|
|
378
|
+
.codearbiter/.markers/. Never raises: an empty/None `rel_path`, or one
|
|
379
|
+
with no real path segment (".", "..", "/"), has no directory or file to
|
|
380
|
+
encode and degrades to the fixed sentinel "-authoring" - a name no real
|
|
381
|
+
(non-empty) rel_path can ever produce, since a real segment always
|
|
382
|
+
contributes a non-empty stem.
|
|
383
|
+
|
|
384
|
+
Residual (accepted): the escaping above closes hyphen/segment-boundary
|
|
385
|
+
ambiguity but is not a fully bijective encoding against adversarially
|
|
386
|
+
crafted underscore runs (e.g. a directory literally named "a_" holding
|
|
387
|
+
"_b.md" can alias "a" holding "__b.md"). The registry's entries are a
|
|
388
|
+
small, curated, human-authored set (spec B1/B2), not attacker-chosen
|
|
389
|
+
directory names, so this is judged out of proportion to close fully
|
|
390
|
+
here."""
|
|
391
|
+
parts = [p for p in norm_path(rel_path).split("/") if p not in ("", ".", "..")]
|
|
392
|
+
if not parts:
|
|
393
|
+
return "-authoring"
|
|
394
|
+
if parts[0] == ".codearbiter" and len(parts) > 1:
|
|
395
|
+
parts = parts[1:]
|
|
396
|
+
stem = os.path.splitext(parts[-1])[0]
|
|
397
|
+
dir_tag = "-".join(p.replace("-", "--") for p in parts[:-1])
|
|
398
|
+
encoded = f"{dir_tag}__{stem}" if dir_tag else stem
|
|
399
|
+
return f"{encoded}-authoring"
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def marker_gated_write_admitted(rel_path, root, minutes=MARKER_FRESHNESS_MINUTES):
|
|
403
|
+
"""True iff a Write/Edit/shell mutation of a `marker-gated` `rel_path` is
|
|
404
|
+
admitted: a `<stem>-authoring` marker exists under
|
|
405
|
+
.codearbiter/.markers/ and was touched within `minutes` (the H-11
|
|
406
|
+
pattern). False on an absent marker AND on a stale one - both cases
|
|
407
|
+
delegate entirely to _hooklib.marker_fresh (the one filesystem reader
|
|
408
|
+
this module calls), so there is exactly one freshness implementation in
|
|
409
|
+
the codebase."""
|
|
410
|
+
marker = os.path.join(root, ".codearbiter", ".markers", marker_name_for(rel_path))
|
|
411
|
+
return marker_fresh(marker, minutes)
|