Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
e7cadcf
docs(state): spec and plan the portable-release + protected-state sprint
SUaDtL Jul 31, 2026
e2e88aa
feat(hooks): add the protected-state registry with per-entry policies…
SUaDtL Jul 31, 2026
56387ee
feat(hooks): wire the protected-state registry into all three flanks …
SUaDtL Jul 31, 2026
6a903e6
docs(decisions): ratify ADR-0024, the protected-state executable-inpu…
SUaDtL Jul 31, 2026
0664506
feat(release): split _releaselib into portable mechanism and declared…
SUaDtL Jul 31, 2026
11a938e
feat(release): make the shim CI-safe and wire the resolution trace (#…
SUaDtL Jul 31, 2026
4542169
test(release): prove portability in a clean consumer repo, as a ratch…
SUaDtL Jul 31, 2026
51ebb27
test(release): prove this repo still releases, from live HEAD (#563)
SUaDtL Jul 31, 2026
0a4e9a4
test(release): drive the lane from the prose, not the library (#563)
SUaDtL Jul 31, 2026
2c83998
feat(release): the skill resolves its target from declared data, not …
SUaDtL Jul 31, 2026
f199962
feat(release): add the back-fill lane, prose and mechanism together (…
SUaDtL Jul 31, 2026
77b706f
fix(test): make the back-fill absence proof own its state (#563)
SUaDtL Jul 31, 2026
3c23b51
fix(release): close the defects an agent found by following the prose…
SUaDtL Jul 31, 2026
f99d090
feat(release): gate the agent-judgment proof against skill drift (#563)
SUaDtL Jul 31, 2026
73bd884
fix(release): close two seams between the prose and the tooling (#563)
SUaDtL Jul 31, 2026
0dd8d6e
fix(release): remove a this-repo path from the shipped skill and sour…
SUaDtL Jul 31, 2026
6f25151
test(release): repoint the lane anchor and enforce anchor uniqueness …
SUaDtL Jul 31, 2026
436d567
fix(release): make the relocated tag command the lane's captured invo…
SUaDtL Jul 31, 2026
fd3d8d6
fix(release): stop destroying tag messages, and correct two arguments…
SUaDtL Jul 31, 2026
e9b59bf
test(release): invert the assertion that encoded the tag-stripping de…
SUaDtL Jul 31, 2026
02caf16
fix(release): derive the commit window instead of substituting a sent…
SUaDtL Jul 31, 2026
91c36b7
fix(release): floor the derived version on the manifest, not on 0.0.0…
SUaDtL Jul 31, 2026
c46090d
fix(release): one base version, one comparison, and a remedy when it …
SUaDtL Jul 31, 2026
5192ef7
feat(release): record the agent-judgment proof and wire its freshness…
SUaDtL Jul 31, 2026
8835bf2
docs(release): make the ledger and the proof record say what actually…
SUaDtL Jul 31, 2026
2602e50
feat(release): run declared pre-tag commands through a helper, not pr…
SUaDtL Jul 31, 2026
f870652
fix(release): the pre-tag tree assertion is "changed nothing new", no…
SUaDtL Jul 31, 2026
25f216d
feat(hooks): enrol release-targets.md in the H-22 protected-state reg…
SUaDtL Jul 31, 2026
eb94af1
fix(release): commit before tagging, and detect mutation by content n…
SUaDtL Jul 31, 2026
a31d398
feat(release): derive a first-release baseline from the adoption comm…
SUaDtL Jul 31, 2026
26ad016
fix(release): exit 6 is a broken declaration, plus the first-release …
SUaDtL Jul 31, 2026
088f03a
fix(release): anchor the adoption-commit path to the project dir, not…
SUaDtL Jul 31, 2026
cbc93af
feat(release): a tested classifier for the lane's last hand-rolled ar…
SUaDtL Jul 31, 2026
102ba26
fix(release): assert every declared manifest bumped, and retract a cl…
SUaDtL Jul 31, 2026
70c37a4
feat(release): wire the window classifier into Phase 1, closing the l…
SUaDtL Jul 31, 2026
56244cd
feat(release): record the run-13 proof; the freshness gate is green (…
SUaDtL Aug 1, 2026
22a1575
refactor(ci): derive tag namespaces from the declared file, not a con…
SUaDtL Aug 1, 2026
42b2c65
feat(release): record the declared rows' own paths as provenance drif…
SUaDtL Aug 1, 2026
366cc79
feat(release): confirm a row's pre-tag content by hash before executi…
SUaDtL Aug 1, 2026
baa6476
feat(release): derive a row's applicable assertions from its declared…
SUaDtL Aug 1, 2026
3558bdd
feat(release): select the release target by name, never by input posi…
SUaDtL Aug 1, 2026
d32a0be
refactor(ci): the shim derives its target register from the declared …
SUaDtL Aug 1, 2026
5157776
feat(hooks): taskwrite carries a rationale sub-bullet; debug stops wr…
SUaDtL Aug 1, 2026
728451d
test(board): pin debug's helper conversion, and make this runner's -k…
SUaDtL Aug 1, 2026
28a33af
feat(hooks): seed the board through the helper, and pin the circulari…
SUaDtL Aug 1, 2026
4da38c3
feat(hooks): the archival sweep — a per-item archive verb that surviv…
SUaDtL Aug 1, 2026
063b0b4
feat(hooks): enrol both board files and give standup the per-item swe…
SUaDtL Aug 1, 2026
6860e13
feat(release): a portability guard for shipped skills, and the surfac…
SUaDtL Aug 1, 2026
1864c33
fix(hooks): remediate the workstream-B adversary HIGHs in the board a…
SUaDtL Aug 1, 2026
ba585bc
test(hooks): pin the editor-open decision and the read boundary besid…
SUaDtL Aug 1, 2026
e27e58c
fix(ci): make the branch actually green, and update the docs site for…
SUaDtL Aug 1, 2026
a5b7a67
fix(hooks): the unreadable-archive refusal must not be an OSError; ti…
SUaDtL Aug 1, 2026
7632c5e
docs(ci): document the two public functions whose siblings were alrea…
SUaDtL Aug 1, 2026
5db3aad
fix(hooks): six confirmed Major findings from review, two of them reg…
SUaDtL Aug 1, 2026
d06b2c1
fix(ci): close two gaps in the portability guard and drop the token f…
SUaDtL Aug 1, 2026
8f10140
fix(release): four Major skill findings and three test-hygiene ones (…
SUaDtL Aug 1, 2026
fdd25b4
feat(release): give the lane readers for the row fields it forbids re…
SUaDtL Aug 1, 2026
5bafba9
fix(release): the row reader executed the shell it was only meant to …
SUaDtL Aug 1, 2026
84d8bc1
fix(release): make the CI-red trio portable across runners (#563)
SUaDtL Aug 1, 2026
cc6f8a7
fix(ci): teach the lane extractor the two spellings this branch intro…
SUaDtL Aug 1, 2026
069d607
fix(ci): the bash probe leaked a hang, and its stated cause was wrong…
SUaDtL Aug 1, 2026
ec983fc
fix(release): remediate the two HIGHs from blind exercise run 16 (#563)
SUaDtL Aug 1, 2026
9feb515
fix(release): the run-16 remediation introduced two references of its…
SUaDtL Aug 1, 2026
b725158
fix(release): remediate the two HIGHs from blind exercise run 17 (#563)
SUaDtL Aug 1, 2026
b74ace5
docs(release): say why the rebuild subshell's eval is not the banned …
SUaDtL Aug 1, 2026
6bbf64b
fix(release): drop a force-push instruction, and record run 18 honest…
SUaDtL Aug 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions .codearbiter/.provenance/release-targets.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{
"created": "2026-07-31",
"doc": "release-targets",
"entries": [
{
"drift_trigger": true,
"hash": "a39e18a12f1a3aefaebd5ac80b5519711d6aba84",
"path": "CHANGELOG.md"
},
{
"drift_trigger": true,
"hash": "ae70e34bb2fea3bbef23b71e00a01c71725da90f",
"path": "package.json"
},
{
"drift_trigger": true,
"hash": "7a8c2e5487ccea497a3014cf93c1632453cf471e",
"path": "plugins/ca-codex/.codex-plugin/plugin.json"
},
{
"drift_trigger": true,
"hash": "9199e83dbc60ffb09dc5321ff8990d80200cab46",
"path": "plugins/ca-codex/CHANGELOG.md"
},
{
"drift_trigger": true,
"hash": "9c1adadf3757e7f995c939b8941702504bf2ad4d",
"path": "plugins/ca-pi/CHANGELOG.md"
},
{
"drift_trigger": true,
"hash": "b5812f8616f351ba2db9acafff5164a4e2939c87",
"path": "plugins/ca-pi/extensions/codearbiter-child.js"
},
{
"drift_trigger": true,
"hash": "7e5890e95d06e7f1455a6ee99641c11862f25b12",
"path": "plugins/ca-pi/extensions/codearbiter.js"
},
{
"drift_trigger": true,
"hash": "ee1837f4f453bc598eff08cc4ebb71a306e8b06d",
"path": "plugins/ca-pi/package.json"
},
{
"drift_trigger": true,
"hash": "4778082d5ea1466d96a6bb0640eeee58bbb30f0e",
"path": "plugins/ca-sandbox/.claude-plugin/plugin.json"
},
{
"drift_trigger": true,
"hash": "d2a8c793bcaaceb3649a06076a27533f79eb7161",
"path": "plugins/ca-sandbox/CHANGELOG.md"
},
{
"drift_trigger": true,
"hash": "6bbd0dc0a4f0a6ab52b1b97e2467c53228861ee9",
"path": "plugins/ca-sandbox/tools/claude-inside.js"
},
{
"drift_trigger": true,
"hash": "937c440af255c8b6aa97a4edcd4283ec7130c665",
"path": "plugins/ca-sandbox/tools/sandbox.js"
},
{
"drift_trigger": true,
"hash": "8c98f20f9e4de1cdec10d6f99922e520347a3ba9",
"path": "plugins/ca/.claude-plugin/plugin.json"
},
{
"drift_trigger": true,
"hash": "ab5064e747807cd68d2433b2aab234883c25d42b",
"path": "plugins/ca/tools/farm.js"
}
],
"interview_derived": false,
"schema": 1
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
status: accepted
date: 2026-07-31
title: Protected-state registry is a declared executable-input boundary with cooperative, friction-grade markers
decided-by: SUaDtL@users.noreply.github.com
supersedes: none
governs: core/pysrc/_protectedstatelib.py, core/pysrc/_protectedlib.py, core/pysrc/_bashguardlib.py, .codearbiter/release-targets.md
---

# ADR-0024 — Protected-state registry is a declared executable-input boundary with cooperative, friction-grade markers

## Status

Accepted — ratified 2026-07-31 by SUaDtL@users.noreply.github.com. Content pre-approved at the sprint's Phase 1 gate the same day.

## Context

`H-22` introduces a registry of protected project-state files, each carrying a policy — `marker-gated`, `helper-only`, or `append-only` — enforced across `pre-write.py`, `pre-edit.py`, and `_bashguardlib.py`. Its first consumer, `.codearbiter/release-targets.md`, carries per-row `pre-tag` shell commands that `/ca:release` executes before composing a tag (DECISION-0034).

That makes `release-targets.md` **executable input**: a repository file whose contents the framework runs. ADR-0002 already established the trust model for this class in `plan.json` — operator-authored, PR-reviewed, length-capped, with the boundary declared rather than an allowlist imposed. This ADR records where the new case matches that precedent and, more importantly, where it does not.

Three differences from `plan.json` were raised in review and are named here rather than papered over with a citation:

1. **Authorship.** ADR-0002 rests on "operator-authored". Here the model drafts the rows — `context-creation` elicits them, and a back-fill lane proposes a detected shape. Model-drafted shell later executed by the skill is a different trust class than operator-typed shell.
2. **Review discipline.** In a consumer repo nothing guarantees `release-targets.md` edits are PR-reviewed. A third-party PR touching it plants commands the maintainer's next `/ca:release` runs.
3. **Write protection.** It is the only *executable* file under `.codearbiter/`, and the location chosen for it — a separate file rather than a `CONTEXT.md` block, on context-economy grounds — does not inherit `CONTEXT.md`'s existing guard.

## Decision

The registry is a declared executable-input boundary, protected by **write-gating rather than content inspection**.

- `release-targets.md` is registered `marker-gated`. Mutations are admitted only under a fresh `release-targets-authoring` marker, minted immediately before the write and removed at lane exit.
- Each `pre-tag` entry is capped at **1024 characters**, following ADR-0002's precedent.
- `pre-tag` commands are **check-only** and may never mutate the tree (DECISION-0034). The clean-tree assertion is unconditional, so a rogue command's writes surface before tagging.
- The resolved `pre-tag` list is content-hashed; a change forces re-confirmation, so a silently edited command cannot ride an earlier approval.
- **No content predicate ever grants admission.** A guard that reads file content to decide whether to permit a write converts content into an authorization signal, which is launderable by anyone who can write the content.

**The marker is audit friction, not authorization.** It is self-mintable via shell by design, per ADR-0010's cooperative-attestation posture. Its value is the trail and the deliberate pause, not unforgeability. `GATE_MARKER_NAMES` — the shell-flank blocklist — enumerates only markers that convert a BLOCK into an ALLOW, and authoring markers are correctly outside it. Widening that list generically over every registered marker would brick every minting lane while stopping no non-cooperative agent, since such an agent shell-mints regardless.

**Case handling is global, not host-derived.** Both flanks treat registered paths case-insensitively and tolerate a `./` prefix, trailing slash, doubled slash, and surrounding whitespace. Matching the host filesystem was rejected: case-sensitivity varies by platform *and* by volume on the same platform, and `realpath` cannot fold case for a path that does not yet exist — exactly a Write creating a protected file for the first time. A fixed rule both flanks apply without consulting the filesystem only widens what is protected, never narrows it.

## Accepted residuals

Declared here and in `security-controls.md`. Undeclared is not the same as accepted.

- **Shell indirection.** `f=…; sed -i "$f"` and novel interpreter spellings evade the lexical flank. The same residual `CONTEXT.md` already carries under ADR-0010, inherent to a cooperative guard.
- **Lexical false blocks.** The shell flank matches a registered file's bare basename with no directory requirement — forced by the need to catch a bare `tee open-tasks.md`. A description containing a write verb and the filename false-blocks. `/ca:override` is the sanctioned bypass.
- **`touch` is excluded.** Two reviewers split. A security pass traced every mtime consumer under `.codearbiter/` and found none feeding an admission decision — `marker_gated_write_admitted` stats the *marker*, not the protected file, so even back-dating admits nothing. An adversarial pass countered that `DECISIONS_WRITE_RE` *does* include `touch`, because for H-11 creation itself is the violation, and that `touch` on an absent board creates an empty board outside the sanctioned helper. The admission analysis was judged decisive; the creation case is recorded as the known cost.
- **Merge-conflict resolution.** A conflict in a `helper-only` file has no helper verb, so resolution routes through logged `/ca:override`.

## Reopen conditions

- If `gate-events.log` shows board-conflict overrides recurring, build a `taskwrite resolve` verb. Never punch an exception into the guard.
- If `release-targets.md` ever ingests untrusted or third-party content, the operator-authored premise is void and this decision must be revisited — inheriting ADR-0002's own reopen trigger.
- If any registry entry ever needs a non-forgeable marker, that marker joins `GATE_MARKER_NAMES` deliberately and gains a sanctioned producer, as a reviewed one-line widening.

## Alternatives considered

- **A content allowlist on `pre-tag` commands** — rejected for the reason ADR-0002 rejected it: it over-engineers a trusted-operator input and risks refusing legitimate commands.
- **Uniform marker-gating for all three consumers** — rejected. Marker-gating `open-tasks.md` would *admit* an agent hand-composing board markdown under a marker, while its sanctioned helper is already invisible to every flank by construction. The correct policy there is a hard block with no marker path.
- **A disk-loaded registry** — rejected. It would let a consumer repo un-protect its own board by editing a file.
- **Storing the rows in `CONTEXT.md`** to inherit its write guard — rejected on context economy: `CONTEXT.md` is read every session, release configuration only when tagging.

## Consequences

Easier: an explicit trust model, so a reviewer knows `release-targets.md` is executable input and reviews it as such; and one registry that a fourth protected file joins as a row rather than as a new hook branch.

Harder: correctness depends on write-gating and PR-review discipline rather than on content validation, and the lexical flank will occasionally false-block a legitimate command.

## Risks

A malicious or mistaken `pre-tag` entry runs arbitrary shell on the maintainer's host at release time. Accepted because the entry is write-gated, length-capped, check-only, content-hashed against silent change, and surfaced for confirmation. The residual is a cooperative-guard residual, not a sandbox.
Loading
Loading