Skip to content

feat: reconcile hook files per entry with host-local enablement state - #161

Merged
levifig merged 10 commits into
mainfrom
hooks-entry-reconciliation
Aug 10, 2026
Merged

feat: reconcile hook files per entry with host-local enablement state#161
levifig merged 10 commits into
mainfrom
hooks-entry-reconciliation

Conversation

@levifig

@levifig levifig commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Codex and Cursor hooks.json files stop being whole-file managed artifacts guarded by a digest. The build emits a per-target hook catalog (event, hook id, desired template, signatures, identity stems, frozen release cohorts) as the single identity authority, and install/upgrade converge Loaf-owned entries per hook point: add missing-and-enabled, update present-but-different, remove retired, and absorb cohort-absent entries once as disabled records β€” while every non-Loaf entry survives value-identical and order-stable. Ownership is by construction (trusted executable forms gated by signature or identity-stem match, exact normalized manifest paths, a frozen legacy allowlist; quote-aware tokenization; Windows command parity), so drift refusals are gone as a class while integrity violations still fail closed preserving the file.

Enablement lives in the global SQLite state, user-scoped and host-local: absence means enabled, absorption provenance is immutable, records tombstone, and the absorb-and-mark write is transactional behind a durable per-target marker restricted to the enumerated cohorts of the prior installed release β€” including the six pre-reset 2.0.0-alpha releases whose manifests the old refusal bug had frozen in place. loaf hooks list/enable/disable operates the records from the installed catalog and reprojects through the full reconciler inside a per-target lock. The hook-projection artifact kind is retired: digest and merge machinery deleted, obsolete manifest rows read tolerantly and dropped after the marker is durable, and loaf config check diagnoses five enablement-aware states with --fix routed through the reconciler.

Canary evidence from the live machine is recorded in the Change folder: zero drift refusals, the operator's deleted Codex hook absorbed as a disabled record instead of re-added, all 34 foreign entries untouched (comparator-proven), an idempotent second run, a one-entry verb round-trip with surviving provenance, and a healthy config check. Every implementation slice passed an adversarial review gate to approval, and the pre-fix dry-run that caught the frozen pre-reset manifest is retained as evidence.

Closes docs/changes/20260808-hooks-entry-reconciliation.

levifig added 10 commits August 8, 2026 16:25
Entry-level reconciliation for Codex and Cursor hooks.json: converge
Loaf-owned entries per hook point, leave every other entry untouched,
and absorb pre-existing deletions once as host-local disabled records
behind a durable marker. Thirteen decisions cover ownership by
construction (signature-or-stem recognition with a frozen legacy
allowlist), deterministic catalog pairing, honest lock/crash semantics,
and a five-state config check. Five task packets, live-machine fixtures,
and a presence-aware comparator ride in research/. Adversarially
reviewed to approval across five rounds.
…econciliation shape

state-dedupe claimed 0.2.21, so the five literal 0.2.21 mentions in
shape.md (Hypothesis, Scope, Observable Workflow, Decision 2, H1) now
read release-neutral. The absorption mechanics are unaffected: the
cohort anchors to the prior installed version via package_version, not
a version literal. 0.2.20 references are factual and stay.
Migration 0013 adds three user-scoped, host-local tables: hook_enablements
(mint-once opaque IDs, UNIQUE(target, event, hook_id), absence-means-enabled,
immutable absorbed_at, tombstone retention), hook_absorption_markers (one
durable run-once gate per target), and hook_trusted_paths (current install
path per target with retained history, single-current enforced by a partial
unique index). Accessors cover effective-enablement reads, toggle upserts
that never re-mint IDs or touch absorbed_at, per-target listing, and a
transactional absorb-and-mark write proven all-or-nothing under injected
failure at every stage. Schema docs and version-pinned tests updated.

Delivers TASK-001 of docs/changes/20260808-hooks-entry-reconciliation
(implementation Grok 4.5, review gpt-5.6-luna xhigh: approved, two
advisory test gaps closed).
The build now emits a per-target hook catalog (.loaf-hook-catalog.json:
event, hook_id, desired template, signatures, identity stems, frozen
0.2.20 cohorts) as the single identity authority, validated non-empty,
object-templated, and mutually non-overlapping at build time. Install
and upgrade converge Loaf-owned entries per event section through a
closed ownership predicate (trusted executable forms gated by signature
or stem, exact normalized manifest paths, frozen legacy allowlist;
quote-aware tokenization so single-quoted $HOME stays literal; Windows
command/commandWindows parity) and three-pass deterministic pairing.
Every other entry is invisible: goldens prove all foreign entries in the
live fixtures survive value-identical and order-stable, and runtime
post-verify compares them by value and order.

Absorption runs once per target behind a durable marker written with the
disabled records in one transaction, restricted to the prior installed
version's cohort (enumerated version: its cohort; no manifest: the full
frozen cohort; unenumerated version: nothing). Absorption records become
durable before the adapter manifest is replaced; the file projects after,
under a per-target advisory lock held from state read through an atomic
pre-rename-verified publish. Integrity violations (malformed JSON,
symlinks, concurrent modification, foreign state databases) fail closed
preserving the file; drift refusals are unreachable for hook files, and
manifest-less distributions get an actionable stale-distribution error
before any surface mutates. Dry-run planning is provably read-only,
including WAL journal mode, via the new state.Store.IsUninitialized
read-only probe. config check --fix gains minimal state wiring so it
keeps working through the reconciler; its five-state diagnosis rework
remains with the projection-kind retirement.

Delivers TASK-002 of docs/changes/20260808-hooks-entry-reconciliation
(three adversarial gpt-5.6-luna xhigh review rounds; blocker curve
9 -> 4 -> 0; plan-output action vocabulary operator-approved).
loaf hooks list shows every catalog hook for installed targets with its
event, effective enablement, projected-in-file state, and absorption
provenance; tombstoned records stay unlisted. enable and disable operate
the enablement record and immediately reproject through the full
reconciler inside the per-target lock β€” record first, then converge β€”
reporting the toggled entry as the headline and any other convergence
honestly underneath. Targets resolve from the installed distribution and
its built catalog; no repo surface is read at runtime. Harnesses whose
hooks ride other mechanisms (Claude Code, OpenCode, Amp) get a routing
explanation rather than an unknown-target error, and unknown targets
name only the reconciled ones. CLI reference and command surfaces
regenerated.

Delivers TASK-003 of docs/changes/20260808-hooks-entry-reconciliation
(two gpt-5.6-luna xhigh review rounds; both findings fixed and
falsified).
The whole-file hook machinery is deleted: digest computation, drift
refusal planning, and the Cursor/Codex merge paths are gone, along with
the manifest rows that carried them β€” build manifests no longer emit a
whole-file row for reconciled hooks files, and Claude Code's
plugin-internal hooks file is reclassified as the ordinary hook-file it
always was. Installed manifests from prior versions read tolerantly: a
retired row is identified only by an unambiguous kind field (ordered
single-kind match), stripped at the raw-byte layer before strict
validation so defects inside it can never abort an upgrade, and dropped
on the next write β€” sequenced after the absorption marker is durable.
Defects that make identification ambiguous (duplicate kind, trailing
non-JSON bytes down to the exact four whitespace characters) preserve
the original bytes for the strict pass to reject, so tolerance never
launders a malformed live document. Integrity preconditions survive
untouched.

loaf config check now diagnoses hooks with five enablement-aware states
(disabled-and-correctly-absent, enabled-and-in-sync, enabled-but-stale,
enabled-and-missing, disabled-but-present), never mentions foreign
entries, resolves Codex through CODEX_HOME (the same file --fix
writes), stays read-only on plain checks, and --fix converges through
the reconciler with no private refresh path.

Delivers TASK-004 of docs/changes/20260808-hooks-entry-reconciliation
(four gpt-5.6-luna xhigh review rounds; every fix falsified).
The capability receipts pin the native binary's SHA-256, so this
Change's Go work staled them; task commits stayed source-only and this
is the one re-record at the arc's end. All three smokes ran against
installed clients and passed: Claude Code 2.1.226 returned the
SessionStart marker model-visible through the candidate plugin, Codex
0.147.0 observed the native hook output in an isolated CODEX_HOME, and
OpenCode 1.18.13 returned the marker through the plugin transformation
while proving root-session lookup. Claude Code and Codex moved releases
since the last recording, so their capability records move with their
receipts; OpenCode's record keeps its version and gains only the new
receipt path. Reverting the pointer updates reproduces the six
SHA-mismatch failures against the rebuilt binary, so the records now
describe the binary that actually exists rather than a fixture massaged
into agreement.
The canary machine's Codex manifest is frozen at package_version
2.0.0-alpha.19: the drift-refusal class this Change deletes had blocked
every manifest write since the version renumbering, so the recorded
version predates the 0.2.x line and the cohort lookup found nothing to
absorb β€” the operator's deleted hook planned as an enabled re-add
instead of a disabled absorption. The catalog now enumerates the six
pre-reset releases whose built output provably ships the 0.2.20 hook-id
set (2.0.0-alpha.14 through .19, verified per release at the reachable
pre-squash commits; alpha.13 and the 0.1.x-era builds differ and stay
unenumerated), attaching the same cohort to each as literal entries β€”
no family or prefix matching, and unlisted versions still absorb
nothing. Each enumerated version carries its own absorption regression
test with a length cross-check against the production list.

Reopens and closes the TASK-002 absorption scope from TASK-005 canary
evidence (focused gpt-5.6-luna xhigh review: approve; advisory closed).
The pre-reset cohort fix changed the native binary, staling the three
receipts recorded hours earlier. All three smokes re-ran against the
same client versions and passed; only the receipts and binaries move β€”
the capability records already point at these paths.
The real upgrade on the canary machine, captured under research/canary/:
zero drift refusals; the Codex session-start-loaf entry absorbed as a
disabled record with durable provenance instead of re-added (the
pre-fix dry-run that caught the frozen 2.0.0-alpha.19 manifest is
retained as evidence of the defect the run surfaced); both live files
value-identical and order-stable through the upgrade with every foreign
entry untouched; a quiet second run with no second absorption; the verb
round-trip editing exactly one entry in each direction with absorption
provenance surviving the toggles; and config check reporting the
disabled hook healthy. H1 through H4 hold as written. The state-schema
migration transcript and the alpha.19 pre-emptying backup that anchors
the cohort enumeration ride along as provenance.

Delivers TASK-005 of docs/changes/20260808-hooks-entry-reconciliation.
@levifig
levifig merged commit 66f06de into main Aug 10, 2026
1 check passed
@levifig
levifig deleted the hooks-entry-reconciliation branch August 10, 2026 08:47
@levifig levifig mentioned this pull request Aug 10, 2026
levifig added a commit that referenced this pull request Aug 10, 2026
## [0.2.21] - 2026-08-10

### Added

- `loaf state migrate alias-orphans` retires entity rows orphaned from the alias registry β€” the damage left when a project rekey invalidated every derived ID and a later import re-minted the tree. Classification proves twin-ship (legacy-salt derivation, or window-gated content identity) and iterates to a fixed point; unproven rows refuse by default and take explicit `--retire`/`--realias` dispositions that rehearse in preview; apply takes a backup first and writes an fsynced rollback manifest that restores every deleted row ([#159](#159)).
- `loaf state migrate journal-duplicates` is the sibling repair for journal entries, which carry no aliases: identical `(type, scope, message)` twins across the two import windows retire their older copy, ambiguous groups refuse, and the reference sweep shares one polymorphic-table enumeration with alias-orphans so the two repairs cannot drift ([#159](#159)).
- `loaf state doctor` now checks alias parity: for every project and aliased entity table, raw row counts must equal alias-reachable counts with zero dead aliases. Divergence reports as an error diagnostic naming the repair command β€” identity damage is detectable the day it happens instead of discoverable by accident at housekeeping ([#159](#159), ADR-028).
- `loaf hooks list`, `loaf hooks enable`, and `loaf hooks disable` operate individual hooks from the installed catalog. Enablement lives in the global database, user-scoped and host-local β€” absence means enabled, disable records tombstone rather than delete β€” and every verb reprojects the target's hook file through the full reconciler under a per-target lock ([#161](#161)).

### Changed

- Codex and Cursor hook files are no longer whole-file managed artifacts guarded by a digest. Install and upgrade now converge Loaf-owned entries per hook point β€” adding missing ones, updating changed ones, removing retired ones, and absorbing an operator-deleted entry once as a disabled record β€” while every non-Loaf entry survives value-identical and order-stable. Drift refusals are gone as a class, including the refusal on user-modified `hooks.json` files frozen since the pre-reset releases; integrity violations still fail closed and preserve the file ([#161](#161)).
- Markdown import resolves identity through the alias registry before deriving an ID β€” existing entities, sources, and journal rows are reused instead of re-minted, colliding sparks receive numbered aliases, and a spark whose message normalizes to an empty slug gets a content-hash alias so no row is born unreachable. Derived IDs are now mint-once opaque keys (ADR-028); for healthy databases a re-import is byte-stable ([#159](#159)).

### Fixed

- A project rekey followed by a markdown re-import could silently fork the identity space β€” every artifact re-inserted as an invisible twin, aliases re-pointed to the copies, and the list surfaces disagreeing with the housekeeping scanner about how much work exists. The importer fix removes the cause; the migrations repair existing damage; doctor parity detects any recurrence ([#159](#159)).

Claude-Session: https://claude.ai/code/session_011wW8VzMJvENoeSY6QxWYkK
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant