Repository navigation
Conversation
Move the manifest-overrides loop in apply_codex_mcp to run LAST, after preserved scalars/tables, so overrides always win -- including over a pre-existing field of the same name on the target entry. Previously a pre-existing value silently discarded a manifest override, and this also diverged from the planned Hermes writer's contract (overrides beat everything). Also, per code review on cea5e5e: - Raise SharedSurfaceError on a name_map collision (two shared servers mapping to the same target name) instead of silent last-write-wins data loss, detected up front before the write loop. - Raise SharedSurfaceError when an existing mcp_servers entry is not a table, instead of a bare AttributeError. - Classify preserved sub-tables via isinstance against tomlkit.items.(Table, AoT, InlineTable) instead of hasattr(x, "items"), which misclassified array-of-tables values. - Reworded the docstrings/comments: verified empirically that on tomlkit 0.15.0 the scalars-before-tables write ordering is not load-bearing (Container reorders regardless of assignment order); kept the ordering as defense-in-depth but stopped implying it is the only thing preventing corruption. Corrected the misleading docstring on test_codex_output_reparses_with_values_in_the_right_tables to match. Added tests: test_codex_override_beats_existing_field, test_codex_name_map_renames_server, test_codex_name_map_collision_raises, test_codex_rejects_non_table_existing_entry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…string The sentence still described overrides being written with the scalars, which was true before the precedence fix moved them last.
Adds apply_hermes_mcp for merging shared MCP servers into a parsed Hermes config.yaml document via ruamel.yaml round-trip (preserves comments/ordering, unlike a PyYAML safe_dump round-trip). Precedence matches apply_codex_mcp: managed transport fields, then whatever already existed on the entry, then manifest overrides last. Adds hermes_gateway_alive(home) to detect a live Hermes gateway via gateway.pid + tasklist/os.kill(0), since the gateway rewrites its own config.yaml on shutdown and would clobber a write made while it's running. Task 7 wires the liveness check and the writer into materialize(). Hoists the name_map collision guard shared by apply_codex_mcp and apply_hermes_mcp into ensure_no_name_map_collisions, and adds a malformed-existing-entry guard to the Hermes writer mirroring Codex's non-table check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The ruamel settings are a hard prerequisite for the Task 9 fidelity gate: a bare YAML() reflows the whole Hermes config (398-line diff), while indent(mapping=2,sequence=4,offset=2)+width=4096 collapses it to a single irreducible hunk. Also records the two known-benign diffs the gate must allow (Hermes personalities reflow, stale Gemini settings). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Inert until Task 7 adds the dispatch loops -- unknown target keys are ignored by the current materialize(). Hermes carries a name_map because .mcp.json calls it 'agentmemory' while Hermes keys it 'Agent Memory', and overrides for the enabled/timeout fields Hermes expects.
The AppData Hermes home has serena deliberately set enabled:false. Forcing enabled:true there would silently re-enable it, changing operator configuration the propagator has no business touching. The .toilet home already has serena enabled, so its override is a no-op and stays.
…-run "clean" mislabel) - summary label for exit 1 under --check now reads "DRIFT (or step error — see output above)" instead of asserting DRIFT with false certainty: every materializer also exits 1 on an unhandled step error (missing manifest, bad JSON, ...), and re-running without --check on a crash would silently overwrite a broken config with "fixed drift". - a --dry-run-only run no longer claims "clean" when it has pending writes it never applied; steps are now labelled "planned" and the footer says so. - documented that --check takes precedence when --check and --dry-run are both given (verified: CHECK wording, drift-driven exit 1). - fixed docstring wording implying the runner forwards --workspace-root; it does not. - factored summary/exit-code aggregation into a pure summarize() function so it's unit-testable without spawning six subprocesses; added coverage for all-ok, drift-under-check, hard-failure exit codes, mixed batches, and the dry-run-only "planned" path. - renamed and tightened the dry-run CLI test (was tautological), added regression tests for the "planned" vs "clean" wording and for the --check + --dry-run combination.
Behavior was already correct but unpinned (noted in code review). A step exiting nonzero during dry-run-only must surface as FAILED and drive exit 1, not be absorbed into the 'planned' framing.
Edited the corpus source, not the projections -- the projections are generated and regenerating them is what makes the change canonical. Adds the standing rule that .mcp.json is the single source of truth for MCP servers and harness configs must never be hand-edited.
…s, fail-open pid check Code review of 82745d7 (Task 6) returned CHANGES REQUESTED. Four fixes: 1. CRITICAL: apply_hermes_mcp unconditionally deleted then re-added every MANAGED_TRANSPORT_FIELDS key on every merge. ruamel's CommentedMap.__setitem__ preserves position for an existing key -- it's the del that moves a key to the end -- so this reordered every touched entry on every run, including a true no-op merge, which would have broken Task 9's fidelity gate (regenerate must produce no spurious diff against a verified-good config). Fixed to only clear the fields belonging to the transport being vacated. 2. env handling on a transport switch now mirrors apply_codex_mcp's precedence exactly: a shared entry's env replaces an existing one when it defines one, an undefined env preserves the existing stdio env untouched, and converting to http drops a stale stdio env instead of leaving it next to type: http. 3. A present-but-null YAML entry (e.g. `serena:` with nothing after it) parses to None, which `.get()` can't distinguish from "key absent" -- it was slipping past the malformed-entry guard and crashing later with TypeError. Membership is now checked explicitly (`target_name in servers`), so null hits the same "not a mapping" SharedSurfaceError as any other malformed entry. 4. _pid_alive failed OPEN (returned False / "not alive") when it couldn't determine liveness at all -- a missing/blocked tasklist, or any other subprocess failure, silently let a write proceed against a config a live gateway might still be watching. It now fails CLOSED (treated as alive) whenever liveness can't be positively determined, while a missing/corrupt gateway.pid still correctly fails open (a live gateway always writes a valid pidfile on startup, so its absence is evidence of a crashed/stale gateway, not a running one). Adds regression tests for all four, most notably test_hermes_noop_merge_preserves_key_order for Fix 1. Verified against real-file copies (deleted after use, never committed): a no-op merge against .toilet/hermes/config.yaml with the ruamel config from c226115 (indent(mapping=2,sequence=4,offset=2), width=4096) produces a diff confined entirely to the personalities: block reflow (the known irreducible ruamel artifact) -- zero lines touch mcp_servers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…gating Task 7 -- integration only; the Codex/Hermes writers, transport classifier, and gateway-liveness guard already existed but nothing dispatched to them. - resolve_manifest_path() now also expands %VAR%/$VAR (needed for hermes_user's `%LOCALAPPDATA%/hermes/config.yaml`), not just `~`. - Add target_in_scope(): repo-scope targets always run, user-scope targets require --include-user-scope since they write outside the repo. - Dispatch codex/codex_user through apply_codex_mcp via tomlkit, and hermes_workspace/hermes_user through apply_hermes_mcp via ruamel with the exact preserve_quotes/indent/width settings the fidelity gate requires. - Hermes targets skip (not write) when config_path doesn't exist -- there is no update-only-if-present primitive, so the dispatch checks .exists() itself rather than letting check_or_write_text fabricate a config. - Hermes targets refuse (REFUSED + drift=True) when hermes_gateway_alive() reports a live PID, since a running gateway rewrites its own config on shutdown. This fires under --check too, by design: --check exists so an operator can discover blockers before a real write, and a live-gateway block is exactly that kind of pre-write-discoverable fact. - Add --include-user-scope CLI flag. - Add materialize()-level dispatch tests (previously zero coverage) using synthetic tmp_path manifests, with the five downstream per-surface manifests stubbed out so real repo files are never touched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ulting Quality-review fixes on top of 79dd0d6 (spec compliance passed cleanly; these are risk/robustness issues). - Fix 1 (CRITICAL): a REFUSED Hermes write set drift_found=True but main() only turned drift into exit 1 under --check, so a real run with a blocked gateway still exited 0 -- indistinguishable from success to callers like refresh_agent_surfaces.py that key off exit code alone. main() now checks for any REFUSED result and exits 1 unconditionally, before the --check drift branch. materialize()'s 2-tuple return signature is unchanged. - Fix 2: tomlkit.parse()/ruamel .load() on a hand-maintained Codex/Hermes config were unguarded. Since `results` only prints after materialize() returns in full, a mid-run parse crash discarded every result computed so far and silently skipped the five downstream sub-materializers (roster/memory/context/hook/skill). Both parses are now wrapped and re-raised as one actionable SharedSurfaceError naming the path. - Fix 3: os.path.expandvars() leaves an undefined %VAR%/$VAR as a literal string instead of raising, which resolved to a bogus path nested inside the workspace tree and was indistinguishable from a legitimately-absent config (both read as SKIP). resolve_manifest_path() now detects a residual unresolved reference and raises naming the variable. - Fix 4: target_in_scope() defaulted an absent `scope` field to "repo" (write unconditionally) -- the one thing this gate exists to prevent for a target that forgets to set `scope`. Absence is now equally strict as a garbage value: both raise. Verified target_in_scope() is only called from the two new Codex/Hermes loops; gemini/opencode/vibe are unaffected. - Fix 5: config_path pointing at a directory passed `.exists()` and then raised a raw PermissionError/IsADirectoryError from read_text()/open(). Handled by the same try/except as Fix 2, in the same place. Added coverage for all five: a process-exit-code test (via main() with a monkeypatched sys.argv against a synthetic tmp_path manifest, never the real workspace), malformed-TOML and malformed-YAML dispatch tests, an undefined-env-var test, a directory-config_path test for both writers, and an updated target_in_scope test asserting the missing-scope case now raises. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Truth Status flipped to Verified, backed by the Task 9 gate results against live harness configs. Records the three review-caught defects that the implementing agents' own tests missed.
The test pinned exit 1 against ai-roster's pre-existing drift. Running the propagator for real cleaned that drift up and the test failed -- it was asserting workspace state, not the runner's contract. Now it asserts only the CHECK-over-PLAN wording; exit-code semantics are covered by the state-independent summarize unit tests.
The Verified status was briefly asserted while --check still reported drift on .codex/config.toml. Records the cause (one-time tomlkit whitespace on first table insertion), the resolution (defb907), and the discipline it implies: --check must be re-run after the write.
… keys Two defects a holistic review found in territory Task 5/7 owns: 1. apply_codex_mcp's `elif preserved_env is not None` fired for ANY non-stdio transport, including http -- so converting a stdio server to url-only kept its old `env` table forever, re-preserved on every run. These configs carry credentials, so a stale env block surviving a transport switch is a hygiene problem, and this is exactly the stdio->http scenario the writer exists to handle (two real servers made that switch on 2026-07-17 and will again). apply_hermes_mcp already guarded this correctly; Codex had no equivalent test, which is how the asymmetry survived review. Fixed by nesting the env assignment inside `if transport == "stdio"` so http never re-attaches a preserved env. Added test_codex_stdio_to_http_clears_stale_env and test_codex_http_to_stdio_clears_stale_url, mirroring the Hermes pair. 2. materialize() dispatches on a fixed set of literal target keys with no validation that every manifest key is one of them -- a typo'd key or a not-yet-wired 7th harness was a silent no-op (empty results, drift=False, exit 0, nothing written). Added a module-level KNOWN_TARGET_KEYS constant and a loud SharedSurfaceError in resolve_manifest naming the unrecognized key(s) and the known set. Verified against the real shared-agent-surface.json (all 7 current keys pass) and added test_resolve_manifest_rejects_unknown_target_key + test_resolve_manifest_accepts_all_known_target_keys. `scripts/refresh_agent_surfaces.py --check` still reports all 6 steps clean after this change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…surface The Claude Desktop app spawns claude.exe with an inline --mcp-config declaring agentmemory as 'cmd /c npx -y @agentmemory/mcp'. It collides with and overrides the .mcp.json entry, so Desktop sessions bypass the JanuScope lens entirely -- no audit, no instruction injection, and the npx -y spawn pattern we removed everywhere else. Verified empirically: memory_diagnose succeeded while the audit sink did not grow. The propagator cannot reach this surface (CLI arg, not a file), so the single-source-of-truth claim is scoped to the six file-based surfaces.
Adopt Codex hooks (currently hand-written and unmanaged), add Hermes, wire agentmemory into the three hook paths where it earns its cost, and relocate hook scripts to FLOSS/hooks/ last -- the move invalidates both Codex trusted_hash and Hermes script-mtime approval, so it happens once. Also records the operator correction that plugins/ is a staging shelf, not a canonical surface. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codex pins hooks.json by content hash, so the projection must reproduce
it byte-for-byte or every trust entry in ~/.codex/config.toml goes stale.
Add a codex target to shared-hook-surface.json with a new
payload_shape: "hooks_only" marker, and teach
materialize_shared_hook_surface.py (build_target_payload) to emit the
bare {"hooks": {...}} shape for that target instead of merging into
whatever else lives at the settings path -- unlike claude/gemini, whose
settings files carry unrelated keys the merge path must preserve.
Verified `--check` reports OK (no drift) against the real
.codex/hooks.json before and after; sha256 and the three
hooks.state trusted_hash entries in ~/.codex/config.toml are
unchanged. Adds a unit test asserting the hooks_only shape excludes
unrelated keys while the merge shape still preserves them.
Gives materialize_shared_hook_surface.py two capabilities Task 3's
upcoming Hermes target will need, without adding that target itself:
- format: "json" | "yaml" per target (default "json", so claude/gemini/
codex are unaffected). A "yaml" target reads its existing file with
ruamel.yaml using the same preserve_quotes/indent/width config already
established for Hermes MCP writes in materialize_shared_agent_surface.py,
merges the managed hook block into the parsed document in place (so
comments, key order, and unrelated content survive), and writes it back.
A missing file is SKIPPED, never fabricated. ruamel.yaml is imported
lazily via a require_module() helper mirrored from the sibling
materializer, so a missing package only fails a yaml-format target.
- event_map per target, e.g. {"PreToolUse": "pre_tool_call"}. Manifest
events absent from a target's event_map are omitted for that target
rather than passed through raw -- an unmapped event landing in a
harness that doesn't understand it is a silently broken hook. No
event_map declared means events pass through unchanged, preserving
existing behavior for claude/gemini/codex.
Verified: 139/139 tests pass (128 prior + 11 new), all 6
refresh_agent_surfaces.py --check steps report ok, and sha256 of
.claude/settings.json, .gemini/settings.json, and .codex/hooks.json are
byte-identical before/after -- these are unused capabilities until Task 3
wires up a Hermes target.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Task 3: wires the shared hook manifest into Hermes's config.yaml under .toilet/hermes/, using the YAML round-trip writer and event_map capability landed in 1c18ddf. - shared-hook-surface.json: new `hermes_workspace` target -- `format: yaml`, `settings_path: .toilet/hermes/config.yaml`, `event_map` renaming PreToolUse/PostToolUse to Hermes's snake_case pre_tool_call/post_tool_call. SessionStart is intentionally left out of the map (and out of this target's hooks) -- it has no clean Hermes equivalent, and the existing event_map mechanism already omits any manifest event absent from it. No hook script changes needed: Hermes's wire protocol already matches what hook_pre_write.py/hook_post_write.py read and emit. - materialize_shared_hook_surface.py: `apply_yaml_target` now refuses to write when a live Hermes gateway is guarding the target directory (a `gateway.pid` naming a running process), reusing `hermes_gateway_alive` from materialize_shared_agent_surface.py rather than reimplementing the check -- this is the same config.yaml the MCP Hermes target writes, and a running gateway rewrites it on shutdown, clobbering anything written underneath. The refusal fires before --check/--dry-run branch, same rationale as the MCP writer: an operator should learn about the blocker from a dry pass, not only after a failed write. main() now exits 1 on any REFUSED result unconditionally, mirroring materialize_shared_agent_surface.py's main(). Verified: 145/145 tests pass (139 prior + 6 new), all 6 refresh_agent_surfaces.py --check steps report ok, sha256 of .claude/settings.json, .gemini/settings.json, and .codex/hooks.json are byte-identical before/after. Materialized .toilet/hermes/config.yaml by hand: top-level key count went from 67 to 68 (only `hooks` added), mcp_servers/custom_providers/_config_version/the trailing commented fallback_model block all survived untouched, and no session_start/ SessionStart leaked in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…-start/post-write
Adds scripts/agentmemory_client.py, a stdlib-only MCP-over-stdio client for
agentmemory (localhost:3111 has no usable REST surface -- verified 404 on
every plain HTTP path; MCP stdio via `node bin.mjs` is the only working call
path, ~240ms per initialize+tools/call round trip). save()/recall() never
raise, never block past their timeout, kill the child on timeout, and never
write to stdout -- hooks speak their protocol over stdout, so a stray byte
there would corrupt it.
Wires it into exactly the three places where the cost is justified:
- hook_bg_round.py (DETACHED, already ~10s): saves the resolved consensus
outcome (claim id, outcome, mean, variance, voter roster), and a terse
"edit accepted" note forwarded from hook_post_write.py's fast path.
- session_start_inject.py (critical path): recalls up to 3 recent
memories with a hard 0.8s timeout, degrading silently to the pre-
existing contract-only behavior on any failure; render_memory_section
defensively re-trims/bounds output regardless of caller behavior.
- hook_post_write.py: the synchronous <100ms fast path makes NO memory
call itself -- it only passes an already-bounded terse edit summary as
an extra argv to the DETACHED hook_bg_round.py subprocess, which is the
one that actually calls agentmemory.
hook_pre_write.py is untouched per spec (latency-critical, nothing durable
to say).
31 new tests, all against a stubbed fake MCP stdio server (or a poisoned
agentmemory_client module for the fast-path-never-calls-memory proof) --
none touch a live agentmemory instance. Full suite: 176 passed (145
baseline + 31 new). Manually verified once against the real agentmemory
server: save() -> True, recall() found the saved marker text back.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ce references Moves the five harness-invoked hook scripts (hook_pre_write.py, hook_post_write.py, hook_bg_round.py, session_start_inject.py, agentmemory_client.py) out of scripts/ into a dedicated hooks/ directory via git mv, so history follows and hooks are no longer mixed in with the 34+ human-invoked scripts. Internal reference fixes: - hook_post_write.py's spawn_background_round now points at hooks/hook_bg_round.py - hook_bg_round.py's two agentmemory_client sys.path inserts now target hooks/ - hook_post_write.py's spec_gate import was silently broken by the move (it resolved spec_gate.py via Path(__file__).parent, which is now hooks/ instead of scripts/, where spec_gate.py still lives) -- fixed to resolve via REPO_ROOT / "scripts" explicitly - session_start_inject.py's self-referential agentmemory_client import still works (both files moved together) -- renamed the misleading scripts_dir local to hooks_dir Surface regeneration: - shared-hook-surface.json hook_scripts + every generated command string now point at hooks/ - Regenerated all six refresh_agent_surfaces.py projections; the `hook` step is fully clean. (`agent-surface`/`ai-roster` still show pre-existing, unrelated DRIFT on .agent-surface/harness/ai-roster.json from before this session.) - Fixed the canonical skill-corpus source (flossi0ullk-consensus-gateway/SKILL.md + references/test-map.md) and regenerated skill projections into every harness's skills/ dir - Renamed the four affected keys in docs/specs/spec-registry.json to their new hooks/ paths (only that 4-line hunk staged; spec-registry.json also carries unrelated pre-existing uncommitted drift, left untouched) - Fixed stale scripts/hook_ prose references in ADR-8, AGENTIC_OPERATING_MODEL.md, METAHARNESS_OPERATING_MODEL.md, and the legacy FLOSS/.claude/settings.json duplicate (relative-path hooks, used when a session launches from FLOSS/ directly) - Also patched .agents/skills/flossi0ullk-consensus-gateway/ (an orphaned mirror outside the current skill materializer's target list) Test fixes: the four hook-adjacent test files (test_agentmemory_client.py, test_hook_bg_round_memory_wiring.py, test_hook_post_write_memory_wiring.py, test_session_start_inject_memory_wiring.py) now load their modules from hooks/ instead of scripts/. All 94 scripts/tests pass. Deliberately left unfixed (documented as findings, not bugs): the dated session-note plan docs (2026-07-24-mcp-migration-fixup.md, 2026-04-16-external-adoption-and-shared-surface.md), the archive/reports_and_logs transcripts, .claude/memories/project_memory.json, and config.json's cached slash-command template -- all are historical/point-in-time records or ephemeral caches, not live pointers, and editing them would misrepresent what was true at the time. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…arser
Hermes's agent/shell_hooks.py::_parse_single_entry reads a flat
{matcher?, command} mapping per hook entry, not the Claude-Code-nested
{matcher, hooks: [{type, command}]} shape the materializer was writing
for every target. Every hermes_workspace entry silently failed the
"missing a non-empty 'command' field" check and parsed to zero hooks,
even with hooks_auto_accept true -- confirmed against the real Hermes
CLI (hermes hooks list showed 0 configured before, 2 after).
Adds a per-target `entry_shape: "nested" | "flat"` manifest field
(alongside the existing payload_shape/format/event_map), defaulting to
"nested" so claude/gemini/codex are untouched (sha256-verified). Sets
hermes_workspace to entry_shape: "flat" and corrects its matcher from
Claude vocabulary (Write|Edit|MultiEdit) to Hermes's real file-editing
tool names (write_file|patch, confirmed against tools/file_tools.py).
Adds unit coverage for the flatten transform plus a regression test
that mirrors Hermes's own command-field validation rule against the
generated output.
Ran the equivalence gate that was recorded pending: litellm and omniroute produce identical weights (-0.400 x3), identical mean and variance, both PASS; omniroute faster (7.0s vs 10.8s). Two caveats disclosed rather than smoothed over: a prompt-bleed artifact in one voter's rationale on the omniroute path, and single-claim-shape scope. Consensus ratification is BLOCKED, not merely pending: the governed claim gate requires a consent_ref.decision_action_hash, no convention for it exists (0 of 105 packets carry one), and the check is a bare presence test. Deciding what it anchors to is ADR-12 consent-gate design, which ADR-5 forbids starting silently.
Operator decision 2026-07-26: leave consensus ratification until the consent gate (ADR-12) exists. ADR-19 stands on operator consent and is live. Adds an agent-memory entry so a future agent does not re-attempt the claim and rediscover the same E_GOVERNED_PROVENANCE_REQUIRED wall.
A Hermes agent flipped its own hooks_auto_accept from false to true without asking (config_version unchanged, so not a migration). Hermes gates write_approval for memory/skills but has no gate on its own config, and a Hermes agent has terminal access to the plain writable YAML anyway -- so no in-Hermes rule can be the durable control. Add a guarded_keys declaration per Hermes target (hooks_auto_accept, approvals.mode/cron_mode/mcp_reload_confirm/destructive_slash_confirm) and a read-only checker in materialize_shared_agent_surface.py that resolves dotted key paths against the live config and reports drift loudly (GUARD DRIFT ...), distinguishing an absent key from a wrong value. Report-only: never restores, never raises, never writes. Critical: the guard runs in the Hermes dispatch loop BEFORE the target_in_scope write-gate, so a user-scope target (e.g. the AppData hermes_user home, where the real flip happened) is still guarded on a routine repo-scope --check even without --include-user-scope. Scope gating protects writes; a guard check writes nothing, so gating it behind that flag would leave exactly the hole that let this go unnoticed. Verified against the real incident: refresh_agent_surfaces.py --check now reports "GUARD DRIFT hermes_user hooks_auto_accept: expected False, found True" while hermes_workspace stays clean, with both Hermes config files' sha256 unchanged before/after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Operator ratified the value Hermes had set autonomously. The guard now holds the line at the chosen posture rather than the old default, and correctly flags hermes_workspace (.toilet), which is still false. Deliberately not flipping .toilet's actual value -- writing a security control without being asked is the behavior this guard exists to catch.
… propagate upstream The operator did not pick one of the three same-name-collision options. They set the requirement: never overwrite a skill that has changed where it is installed, because Hermes and Codex evolve their skills from use, and propagate the appropriate improvements back into the shared skill base. Recorded verbatim in the round-3 RESULT.md; the options are kept but marked superseded. That requirement reaches further than collisions: install overwrites any managed projection whose content differs from the shared base, and prune removes a withdrawn one regardless of content. Both predate this PR and are now open against the requirement. Nothing is implemented; a design is proposed in the round-4 packet for approval. o2-skill-inventory/ is a read-only inventory of what is installed today: all 27 managed projections are identical to the shared base in each of the five targets, so nothing evolved has been lost; the harness-evolved skills (33 in Hermes, 6 Codex, 6 OpenCode, 3 Claude) are unmanaged directories that install and prune never touch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…vation requirement A fix-sweep packet for 42e4d05..0472ce1: the response to the third audit (9a23477, F4 with O1 and O4; 69c4cbd, the KEEP wording from the internal review) and three docs commits. Source and test changes in separate patches, per the convention. Beyond the usual contents (three-bucket ask, per-test red table, provenance including the two parallel sessions, unverified ledger), it carries the operator's O2 decision verbatim: never overwrite a skill that changed where it is installed, and propagate appropriate improvements back to the shared skill base. It states plainly that install and prune contradict that today, both pre-existing, and that a harness-added binary file already crashes install (reproduced, unfixed). It then offers a proposed design for critique before it is built -- a byte-digest baseline in the marker, three-way reconciliation that keeps evolved copies and stages proposals, never auto-merging into canon -- modelled on the baseline Hermes itself keeps in .bundled_manifest, with the questions it leaves open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fourth external audit requested changes on three findings in the
skill materializer, and the operator set the rule they all point at:
a skill that has changed where it is installed is never overwritten --
Hermes and Codex evolve their skills from use -- and the change is
surfaced for propagation back to the shared base, with --check still
passing ("it should pass overall, but surface those modifications").
F5 (P2, introduced by 9a23477): the marker-only rewrite opened the
existing marker in place, so a symlinked marker wrote its target and a
hard-linked one rewrote every name for that inode. Markers are now
published by publish_marker(): a fresh file created exclusively, then
os.replace, which swaps the directory entry instead of following it.
The previous marker survives any failure.
F6 (P2, pre-existing): on Python 3.12 a symlink loop in a recorded
workspace_root raises RuntimeError from resolve(), which aborted the
whole refresh, --check included. recorded_workspace() and the owner
comparison now treat it as unusable ownership, like OSError/ValueError.
F7 (P2): equality was decided on text read in universal-newline mode,
so a CRLF script matched its LF source and the marker-only path blessed
a broken copy. Comparison is now by the sha256 of every payload file's
bytes (payload_digests), which also stops a binary file a harness added
from crashing the refresh, and stops a torn marker from being decoded.
Preservation, the part of the operator's requirement those corrections
need: the marker records `installed_files`, the byte digests as
installed. Install then separates the cases instead of overwriting any
difference:
installed == source -> nothing, or re-baseline the marker
installed == baseline != source -> UPDATE (only the shared base moved)
source == baseline != installed -> EVOLVED, kept and surfaced
all three differ -> CONFLICT, kept and surfaced
no baseline, and they differ -> DIVERGED, kept and surfaced
Ownership is settled first: only a projection whose marker names this
workspace is ever written. A legacy marker is adopted only when its
payload equals the shared base byte for byte. Another workspace's
projection, an unmanaged directory with a listed name, an unreadable
marker and a linked directory are kept and surfaced, never taken over.
An update re-reads the installed copy immediately before removing it
and abandons the update if it changed. The pruner removes a withdrawn
projection only when it provably equals its baseline; otherwise it is
kept and surfaced. Surfaced lines start with KEEP and are not drift.
Not built: staging proposals or any upstream propagation (where
proposals live is still open), category folders, and deactivation of
a withdrawn skill that is kept for its learnings.
Tests, written first: 18 new cases. On the unfixed code 13 failed on
behaviour (writes through links, the RuntimeError, a blessed CRLF copy,
UnicodeDecodeError, evolved/foreign/unmanaged/withdrawn content
overwritten or removed, a linked directory replaced), 3 only on a
missing field or function (update baseline, convergence baseline, the
race), and the real symlink loop passed on Windows. Run natively under
WSL Ubuntu / Python 3.12.3 against the unfixed code, the real loop
fails with pathlib's own "RuntimeError: Symlink loop"; after the fix
all 47 pass there. Existing withdrawal tests now carry a baseline,
since removal now requires one.
Read-only --check of the real workspace across all five targets: 24
skills per target would be ADOPTED (marker only); 3 per target are
DIVERGED -- flossi0ullk-shared-surface, subagent-driven-development and
writing-skills -- all line-ending-only differences (installed CRLF,
shared base now LF), which the old text comparison hid.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Internal review of b011298, three-bucket, by a subagent and by one model through OmniRoute, plus the author's own pass: - Generated caches counted as a change. Running a skill's Python script writes __pycache__ into the installed copy; counted as payload, it marked the skill EVOLVED for ever and blocked every update from the shared base. __pycache__, *.pyc/*.pyo, .DS_Store, Thumbs.db and desktop.ini are now neither digested nor copied. (Buried in the OmniRoute review's first item, which otherwise misread the code.) - A copy failing midway left a half-installed directory with no marker, which every later run would read as UNMANAGED and never repair; the subagent judged this "safe by design". Everything under the target was created by that copy and equals the shared base, so it is removed and the next run installs again. A FileExistsError -- something appeared between the check and the copy -- is kept, not cleaned up, since copytree creates nothing before refusing. - The pruner read the marker twice; one that vanished in between made the second read None and raised AttributeError. Found by the author and independently by the subagent. A marker gone at the second read now means no baseline, so the projection is kept. - payload_digests followed links, reading files outside the skill, and a directory loop a harness left in one could raise on Python 3.12 (found by the subagent). It now walks without following links and records each link as its target text; the copy preserves links so both sides agree. The shared base has no symlinks today. - os.walk and rglob skip what they cannot list. A partial digest can equal the baseline and license removing content it never saw: under WSL Ubuntu / Python 3.12.3 the previous commit's code did exactly that on a chmod-000 directory, and died in rmtree with "Permission denied: 'private'" part-way through the removal. An unreadable directory is now an error, reported as UNREADABLE, nothing touched. Tests, written first, 9 cases: on b011298's code all fail on Windows and natively under WSL (the real chmod-000 case is POSIX-only and skipped on Windows). The simulated unreadable case patches os.scandir, which b011298's rglob did not consult on Windows, so its Windows red shows only that there was no UNREADABLE path; the POSIX case is the real evidence. Windows: 54 passed, 1 skipped. WSL: 55 passed. Read-only --check of the real workspace is unchanged: 24 ADOPT and 3 line-ending-only DIVERGED per target. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Carries the 2026-09-29 round-4 audit of 42e4d05..0472ce1 into external-audit-2026-09-29-round4/ (35 files, credential-scanned, content-identical; three normalized to LF on commit, counted blob by blob). RESULT.md records a disposition for every item: F5, F6 and F7 fixed in b011298; O1 fixed as a side effect; O2 implemented for install and prune, with proposal staging and upstream propagation not built; O3 still unreproduced here and worked on separately; O4 agreed; O5 accepted and borne out -- a byte-level check found 3 skills per target differing from the shared base by line endings only, which the text inventory reported identical. The round-3 RESULT's inventory wording is corrected in place, quoting what it claimed. It also records the operator's three decisions: --check passes on harness modifications and surfaces them; category folders only if the spec supports them -- it does not (Agent Skills, Claude Code, Codex, OpenCode and Gemini CLI read one level; only Hermes nests), so installs stay flat and categories stay metadata; proposal location and baseline granularity open, with per-file digests built as a reversible assumption. Two side findings are recorded, not acted on: Hermes's origin-hash manifest is the model followed, and Codex documents $HOME/.agents/skills rather than the ~/.codex/skills this manifest targets. internal-reviews.md carries both internal reviews of b011298 verbatim; real-workspace-check/ carries the read-only --check output (24 adopted, 3 line-ending-only DIVERGED per target) and the operator action that reconciles the three. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9a9822d497
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… discrimination PACKET.md and the source and test patches for b011298 and 176c8c2, the response to the fourth audit. Both patches apply in order to 6ea7620 and reproduce 9a9822d's two files byte for byte. Recounting the discrimination for the packet showed the round-4 RESULT.md had it wrong: 176c8c2 added 8 cases, not 9, and the POSIX-only one is skipped on Windows, so 'all failed on both platforms' did not hold. Corrected in place with the old claim quoted. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…lls; correct two round-4 claims Checked the Codex loader at rust-v0.128.0 (the installed version). The user layer scans $CODEX_HOME/skills, marked deprecated but kept for backward compatibility, and $HOME/.agents/skills, so our Codex projections are discovered today. The loader also searches each root for SKILL.md to a depth of six directories, so the round-4 RESULT.md claim that nesting would hide skills from four of five harnesses was wrong for Codex. Both claims are corrected in place with the originals quoted, and the unsent round-5 packet's out-of-scope note is updated to match. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 420cd8970e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…w threads The materializers and the daemon had one-line registry stubs and no statement of what they must never do, so the invariants existed only as review findings against finished code. This writes them down first, with clause IDs the tests will cite: repo-scope containment (LSI-A1), event-map injectivity (LSI-A2), preservation and verified removal (LSI-A3, A4), reclaim under the claim guard (LSI-B1), the degenerate-voter truncation ratio (SYN-D1..D4) and the zero-correlation Kish case (MRP-K1). The conformance table names tests that do not exist yet. They are written next, from these clauses, and then the code. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… on next touch Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…code) Each test cites the clause in docs/specs/local-surface-invariants.spec.md (LSI-*), the synthesizer spec (SYN-D*) or the review protocol (MRP-K1) that it enforces. Written from the clauses, before the code. Run against the code at the spec commit: 13 failed, 312 passed, 1 skipped. Red: SYN-D2 near-median whole answer called truncated; MRP-K1 zero-correlation panel refused; LSI-A2 colliding event map accepted; LSI-A1 repo-scope escape accepted for codex, antigravity and hermes_workspace; LSI-A4 learning deleted after the pruner's digest, and an unrenamable copy removed; LSI-B1 reclaim snapshots outside the guard and no NOT_RECLAIMED on a busy guard; two daemon structure tests now point at the guarded body. Weak reds, stated: the stranded-copy test fails on a missing REMOVING_PREFIX rather than on behavior, and the daemon structure tests fail on a missing function. A guard, not a red: the update-comparison race test passes on the old code, which already re-read the installed copy. The old doctored-digest version of that test is removed because it cannot model taking the copy aside. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nd tests above Implements what docs/specs/local-surface-invariants.spec.md, the synthesizer spec and the review protocol now say, so that the tests in the previous commit pass. - SYN-D2: a response is called truncated only below DEGENERATE_TRUNCATION_RATIO of the panel median. The constant was declared and never applied, so any whole answer shorter than the median and ending in a letter, colon or emoji was reported as a voter that returned no position. - MRP-K1: the Kish refusal is for a negative mean correlation. A mean of exactly zero is a valid measurement with n_eff = k, and was returned as NaN. - LSI-A2: two declared source events mapping to one native event are refused, naming both. The second used to replace the first silently. - LSI-A1: a repo-scoped target in the agent surface materializer must resolve inside the workspace, for codex, antigravity and hermes, as the hook and skill materializers already required. - LSI-A4: the pruner and the update path now take the installed copy aside, verify it against the baseline there, and delete it or put it back. Comparing and then removing left a window in which a harness write was deleted. A copy that cannot be renamed is kept. - LSI-B1: --reclaim-claim holds the claim guard across both snapshots and both removals, and answers NOT_RECLAIMED if it cannot take it. Verification: full suite 1146 passed, 8 skipped, 1 deselected; spec_gate OK (108 registered); ruff 8, unchanged; native Linux 3.12.3 skill, hook and review-independence tests pass. Not addressed, deliberately: the two Hermes patch-verification threads. They were answered in the thread on 2026-08-25 and 2026-08-31 as left open, because a V4A parser that is subtly wrong would write a false VERIFIED into a permanent chain. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…mplementation The review of 6809053 found clauses that were under-specified or that the code did not meet, so the clauses are corrected before the tests and code that follow. LSI-A1: refusal happens before any write, unscoped manifest paths count as repo scope, and an unresolvable check refuses. LSI-A2: an event with no hooks takes no part. LSI-A3: baseline equality proves no harness change and does not preserve uncommitted source revisions. LSI-A4: three distinct outcomes (changed, in use, unreadable), a stranded copy of a listed skill is put back, any exception restores, and the residual window and the unverified scanner behavior are stated instead of implied away. New LSI-A5 (skill names are single path components) and LSI-A6 (a failed install removes only what is provably the shared base). LSI-B1: any failure to take the guard is NOT_RECLAIMED. SYN-D4: whitespace-only counts as empty. MRP-K1: a mean within 1e-12 of zero counts as zero. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…findings Run against the code at 6809053: 23 failed, 335 passed, 1 skipped. Red for behavior: whitespace-only flagged as truncated (SYN-D4); a mean of -2.8e-17 refused (MRP-K1); an empty event raising a collision (LSI-A2); an unresolvable scope check returning (LSI-A1 c); a refused hook target leaving the registry written (LSI-A1 a); a blocked rename called EVOLVED and an unreadable copy called changed (LSI-A4 c); an interrupted verification not restoring the copy and an undeletable copy aborting the run (LSI-A4 e); a stranded copy of a listed skill not put back (LSI-A4 d); skill names with separators, .. or absolute paths accepted (LSI-A5); a failed marker publish deleting a harness's file (LSI-A6); and five unscoped agent-surface paths plus the half-written-surface case writing outside or before the refusal (LSI-A1 a, b). Characterization, green on 6809053 by design: check and dry-run never rename, the restore-failure message, relative, tilde and env forms of an outside path, and the two reclaim tests that put the removals under the guard (red only against the code before 6809053). A test that passed for the wrong reason was caught and fixed before this commit: the opencode case stopped on a missing file before writing, so the outside file is now created and must stay byte-identical. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found by an existing test while implementing the clause: a failed copy can leave a truncated file, which is the install's own and has to be removed even though it does not equal the shared base. Only after a failed marker publish, when the copy had completed, does a file that differs mean someone else wrote it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…lauses and tests Implements the amended clauses in docs/specs/local-surface-invariants.spec.md so that the tests in 327cad3 pass. - LSI-A4: remove_if_unchanged reports changed, in use, unreadable and stuck as distinct outcomes, restores the copy on any exception including an interruption, reports a verified copy it cannot fully delete instead of aborting the run, and verifies a stranded copy in place. The next run puts a stranded copy of a listed skill back before installing beside it. The docstring no longer says the window is only a rename and a read, and says that only Codex's scanner is known to skip the dot-prefixed name. - LSI-A5: a skill's frontmatter name must be a single path component. It is joined to the target root, so ../x installed outside it. - LSI-A6: a failed marker publish removes the directory only if every file equals the shared base; a failed copy still removes it whole. The marker is re-checked for ownership when the pruner re-reads it. - LSI-A1: the agent surface resolves and checks every target before writing any, including mirrors, the context pack, gemini, opencode and vibe, which had no containment check. The hook surface checks every target before writing the registry, and a scope check that cannot resolve a path refuses instead of returning. - LSI-A2: an event with no hooks takes no part in the collision check and never replaces a non-empty definition list. - SYN-D4 whitespace-only responses are empty. MRP-K1 a mean within 1e-12 of zero is zero. Verification: full suite 1179 passed, 8 skipped, 1 deselected; spec_gate OK (108 registered); ruff 8, unchanged; native Linux 3.12.3 skill, hook and review-independence tests: 112 passed. The real skill, agent and hook manifests pass the new name and containment rules (checked read-only). Not changed, with reasons in the review record: --check reporting content drift that a write run may then keep as in use (documented as LSI-A4 f); no lock between materializer runs (a separate branch is working on locking); and the UPDATE comment's claim about history, which is corrected rather than the behavior, since baseline equality is the preservation rule and not an archive. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e dispositions internal-reviews.md carries the prompt and the return of the subagent review of 332bc6e..6809053 verbatim. RESULT.md gives a disposition for each thread and each finding, including the ones not accepted and why, and the corrected order of work. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
CodeQL's js/reflected-xss (high) on skill-corpus/superpowers-brainstorming/scripts/server.cjs fails the PR's CodeQL check. The earlier triage (72f0240) found the value unreachable by an attacker because it must equal the secret token, and that stands. The clause makes the sink safe on its own terms: the page embeds the server's token, and the embedded JSON cannot close its script element. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nt (LSI-C1) Run against the unfixed server.cjs: 2 failed, 1 passed. Red: a key containing </script> ends the element, and U+2028/U+2029 are written raw into the string literal. The ordinary-token round trip is a guard. The function is evaluated from the shipped source with node, because it is not exported and importing the module runs its start-up code. The tests skip without node. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…t close the script element CodeQL js/reflected-xss (high) at server.cjs:412 failed the PR's CodeQL check. The reflected value was only reachable after it had been compared against the secret token, so it was never attacker-controlled, but the sink did not depend on that. - LSI-C1 a: bootstrapPage is given TOKEN, not the query value. They are equal once the comparison passes, so nothing changes for a caller, and no request string reaches the response. - LSI-C1 b: the embedded JSON escapes <, >, & and U+2028/U+2029, so no value can end the script element or the string literal. Verification: the three new tests pass (two were red before), full suite 1182 passed, 8 skipped, 1 deselected, spec_gate OK, ruff unchanged at 8, node --check clean. CodeQL itself can only be confirmed by the analysis this push triggers. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0a5529ac3a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…rted transports Two review threads on the synthesizer. SYN-P1: a caller-supplied voter_pool was labelled local unconditionally, which exempted it from the survivor independence re-check even when it was online; the mode is derived from its transports instead. SYN-T1: an unrecognised transport fell through to LiteLLM, so a misspelled ollama could send a prompt the caller meant to keep local to a cloud provider; it is a configuration error. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Against the code before the fix: 5 failed, 36 passed. Red for behavior: a correlated online override is tiered instead of degraded (it came back unmeasured), an unknown transport reaches a backend, and a pool with an unknown transport is dispatched. Weak reds, stated: the override-mode helper and the pool validator do not exist yet. A first draft of two of these tests failed with a NameError instead (pytest is imported inside each test in this file, not at the top). That was caught by reading the failure reason, fixed, and re-run against the old code before this commit. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… unknown transport is refused SYN-P1: synthesize() labelled every caller-supplied voter_pool as local, which exempted it from the survivor independence re-check even when every voter was online. The mode is now derived from the transports: local only when every voter is on ollama, online when none is, mixed otherwise, so a correlated online override, or an outage that leaves a correlated subset, degrades the run. SYN-T1: generate() sent any unrecognised transport to LiteLLM, so a misspelled ollama could send a prompt the caller meant to keep local to a cloud provider. The supported values are ollama, flowith and litellm; case and whitespace are ignored; anything else is a ValueError naming the voters, raised before any dispatch. The resolved pools only ever produce the three supported values. Verification: full suite 1187 passed, 8 skipped, 1 deselected; spec_gate OK; ruff unchanged at 8; the five new tests pass. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Rejoins six weeks of parallel work and lands the corrections found along the way.
origin/mainis fully contained in this branch (0 ahead, 124 behind as of2026-08-24), so there is no content to reconcile here — only review.
PR #38 has since merged to main on its own (merge commit
873cc0c,2026-08-18). It is an ancestor of both main and this branch, so the merge
described below is now history rather than the point of the PR. What remains
unique here is the salvage line.
Merge
pr/38(65 commits) merged into thewip/salvage-20260804line (93 commits).Merge-base
3e82a33, 2026-07-05. 13 conflicts, each resolved on its merits.Both sides won on different files, and one conflict caught a real bug:
workers/commons-gateway/*— ourmanifest.tsusedFLOSS/-prefixedpaths, producing a doubled URL segment, and listed
INDEX.md, which is not in thisrepo.
voters.py— PR38's_PERSONA_SHARED_GATE_SYSTEMhardening ("apersona may never waive or narrow the shared checklist") prefixed onto our
clean-room persona text, on both personas.
provenance.py—_is_ancestorand_ignored_chain_positionareindependent features, not rival versions.
Licence
FLOSSI_U/and the reuseledger, but
FLOSS/LICENSEstill held GPL-3.0 text. Now the AGPL SPDX line,byte-identical to
FLOSSI_U/LICENSE. GPL text archived, ADR-7 → Verified.MOMUS_PERSONA_SYSTEMwas adapted from oh-my-opencode(SUL-1.0, incompatible with AGPL). Replaced by a clean-room
EXECUTABILITY_REVIEWER_SYSTEMwritten from this repo's own consensus schema.ADR-12
The stated unblock — "pin compatible client+tryorama versions for the 0.6 line" —
cannot be made to exist: no Tryorama version pairs with hc 0.6.1, and Tryorama is
retired upstream in favour of Rust
sweettest. Verifiedholochain 0.6.3shipssweettest, soconsent_gate.test.tscan be ported without a substratemigration. Sequencing recorded deliberately: tests first, 0.7 migration second.
Also separates the two blockers the ADR had conflated — the sweettest port proves
the zome; the consent anchor is what blocks ADR-19.
entry_has_consent()onlychecks
decision_action_hashis a non-empty string and never resolves it.Provenance spec
v1.5 delta items D2 (multisig headroom) and D3 (evidence types
file/log/activity/source_chain) applied to spec, schema andEVIDENCE_TYPES. D1 held —blocked on ADR-12.
ADR-20 — provenance validator reconciliation
Added 2026-08-23. The provenance spine has run since 2026-08-10 and landed zero
claims: hooks fire, packets are signed, and the validator rejects 100% of them.
Two causes, both reproduced at head, both documented and neither yet fixed —
ADR-20 is Proposed and carries no code.
_EVIDENCE_REF_TYPESinprovenance.py:564still holds the original six values and is the set
validate_packetactuallyenforces, so a schema-valid packet is rejected downstream. Second occurrence of
this pattern after
spec_gate.GATED_SURFACES.permanently invalidates every earlier packet naming it, and those stay in the
pchain. The spec's Audit Disposition section defines a
supersededstatus forexactly this; it was never built.
An adversarial ensemble audit (6 voters, 5 provider surfaces, 6 model families)
rewrote the proposed remedy and is recorded in the ADR, including the finding that
the synthesizer's "6/6 unanimous" label was wrong — one voter answered nothing, one
dissented on blast radius, and the sequencing question split three ways.
Verification
spec_gate --check: 108 registered, 0 missing, 0 reuse violations, 7 stale(non-fatal; 5 are pre-existing
FLOSS/scripts/hook_*.pypath drift — those fileslive in
FLOSS/hooks/)origin/mainmerged in 2026-08-24 (CI and dependabot fixes only, clean)Not included
The preservation spine is not in this branch. It ships via #43
(
feat/preservation-spine-standalone) with #59 stacked on top; that line is aheadof the local spine work, which has been abandoned.