Skip to content

feat(handoff-search): section-grained full-text index of the handoff corpus (P1) - #1209

Merged
ZacxDev merged 14 commits into
mainfrom
feat/handoff-search-index-p1
Sep 3, 2026
Merged

feat(handoff-search): section-grained full-text index of the handoff corpus (P1)#1209
ZacxDev merged 14 commits into
mainfrom
feat/handoff-search-index-p1

Conversation

@ZacxDev

@ZacxDev ZacxDev commented Sep 1, 2026

Copy link
Copy Markdown
Member

What this is

P1 of a handoff-doc full-text search index. Two new modules plus a serverMode-gated systemd unit.

There is no server-side query over handoff-doc bodies today, and that is the gap this closes:

  • scripts/initiatives/sync.py stores derived metadata onlysummary is one parsed line, current_doc is a PATH, search_text is session prompts. Not one byte of a doc body reaches Postgres.
  • scripts/initiatives/viewer.py live-reads bodies off disk (read_doc_detail_live, 512 KB cap) and its search is client-side JS over what the page already shipped.

So the corpus is reachable only by a human who already knows which doc to open, and /resume and subagents re-derive findings that are already written down.

Scope is P1 only. No HTTP service, no network binding, no firewall change, no pgvector/embeddings.

Design

The corpus comes from git refs, not the working tree

Measured: git worktree list in devrc alone returns 143 entries. A working-tree scan therefore indexes a doc as it exists mid-edit in somebody's branch, the same doc N times over N worktrees, and stale orphan copies — and two runs an hour apart disagree for reasons unrelated to what anyone wrote.

Source is git ls-tree + git show against each repo's derived mainline, via the existing scripts/lib/git_mainline.py. Never hardcoded. Verified end to end on both repos this host holds:

repo derived ref docs sections
devrc origin/main 94 959
homelab-talos origin/trunk 54 506

A hardcoded main would have indexed nothing for the second — which is the exact failure git_mainline.py exists to prevent.

⚠ The brief's wider figure of ~424 docs / 8.6 MB over four repos (adding two client checkouts) is second-hand and was not re-derived here; the table above is what I ran.

An untracked doc is reported, never indexed

A handoff doc on disk and absent from the mainline ref is a durability hole — one git checkout by a concurrent session from silent deletion. Indexing it would let the search surface answer from the hole and thereby conceal it. The one that exists today reproduces:

🔴 DURABILITY HOLE — 1 handoff doc in homelab-talos is on DISK and NOT in the
   mainline ref, so it is NOT INDEXED and one `git checkout` from gone.
      claudedocs/handoff-<redacted>.md

Sections, not docs

goal | state | investigation | next_step | gotcha | verify, with ## Open investigations split per ### sub-block and ## Next steps split per ranked item.

Rationale: a "Ruled out" paragraph is the highest-value content in this corpus and is invisible at doc granularity — a few hundred bytes inside a 40 KB document, which ts_rank drowns. The retrieval unit should be the size of the finding.

The parser is borrowed, not reimplemented

Every heading/fence/item rule comes from scripts/lib/handoff_doc.py — the writer of these documents and therefore the executable authority on their shape: split_front_matter (the closed-----at-line-1 rule), split_sections (fence-aware H2 walk), canonical_prefix, _item_blocks (a ranked item is a block, not a line — 179 of 257 devrc items wrap), _FORCING, _unfenced.

Two private names are imported deliberately; that is cheaper than re-spelling them, and handoff_doc's own docstrings record three separate occasions where a second copy of one of these rules drifted. A test pins this module's per-item (rank, forcing_kind) sequence against handoff_doc.ranked_items so the two can never disagree about one document.

Two guards on the read surface

Recall banner on every response, in subsystem_recall.py's wording and posture (same agent reads both surfaces in one session; two spellings of one provenance claim is how a caveat stops being read): results are POINTERS TO VERIFY, nothing was re-derived, a hit may describe a gotcha that has since been fixed.

Silent-zero guard. Every response carries the literal indexed_docs=N indexed_sections=M, and the two zeros render with sentences that share no opening phrase:

NO MATCH — the index WAS searched and none of its 959 section(s) matched.
🔴 BROKEN INDEX — this table holds ZERO documents, so the query above ran against NOTHING.

status is hit / no-match / broken-index — values sharing no spelling — and broken-index exits non-zero, because an empty index is a broken environment, not a reading.

Derived and disposable

--rebuild truncates and re-derives. Git stays the system of record; nothing is stored that a re-run cannot rebuild.

Controls I ran

Mutation sweep, under PYTHONDONTWRITEBYTECODE=1 (a same-length edit in the same second is invisible to CPython's mtime-seconds+size cache and would score a live mutant as SURVIVED). Each old string was checked to occur exactly once before replacing. 9/9 killed, each by its own guard's specific assertion:

# guard mutation killed by its assertion
G1 closed-front-matter rule split_front_matter(text)fm = text test_an_unterminated_block_is_not_front_matter (+1) assert 'cg-9999' is None
G2 forcing closed vocabulary kind if kind in FORCING_KINDS else Nonekind or None test_an_unrecognised_kind_folds_to_none_not_to_itself (+3) assert 'sprocket' is None
G3 untracked direction operands of the set difference reversed test_untracked_docs_is_a_one_way_difference (+1) assert ('…handoff-c.md',) == ('…handoff-a.md',)
G4 silent-zero condition indexed_docs == 0< 0 test_an_empty_index_is_broken_not_a_no_match (+3) assert 'no-match' == 'broken-index'
G5 section boost (memory ranker) SECTION_BOOST.get(...)DEFAULT_BOOST test_investigation_and_gotcha_outrank_a_plain_section… assert ['goal','gotcha','investigation'] == ['investigation','gotcha','goal']
G6 section boost (SQL seam) CASE arm THEN {boost}THEN 1.0 test_the_boost_numbers_live_in_exactly_one_place assert "WHEN 'investigation' THEN 2.0" in "…THEN 1.0…"
G7 two-way prefix pin drop "findings" from PREFIX_SECTION test_every_canonical_prefix_has_a_section_and_vice_versa set inequality vs handoff_doc.CANONICAL_HEADING_PREFIXES
G8 git-ref sourcing git show ref:path → read from disk test_the_corpus_comes_from_the_ref_not_the_working_tree assert 'quixotry' in 'The grumbleflitch edit.'
PC positive control (known-caught) slug_for stops stripping handoff- 5 tests assert 'handoff-widget-relay' == 'widget-relay'

Each mutant was checked for reachability, not just breakability: G4 mutates the narrowest expression that can be wrong (the comparison operator, not the whole arm), and G5's fixture gives three sections identical token coverage so only the multiplier can order them.

Positive / negative control pair, through the same code path and the same store: zarfwidget (in exactly one fixture doc) returns exactly that doc with indexed_docs=2; hexapoddery (in none) returns 0 with indexed_docs=2 unchanged. The pair is what makes either readable — a reassuring zero alone is indistinguishable from a harness wired to nothing.

Fixture hygiene: every distinctive term is an invented nonsense word occurring in exactly one place, pairwise distinct and distinct from any constant an assertion names, so a mutant hardcoding a literal cannot survive by accident.

Gate — all three legs, stated separately

leg verdict
subset (nix develop … -c python3 -m pytest test_handoff_index.py -q) 66 passed
scripts/gate.sh (dev-host tier) GATE: RESULT=PASS exit=0 — pytest RESULT: PASS (exit=0), TOTAL collected=20211 passed=20208 skipped=3 failed=0; node RESULT: PASS (exit=0), tests=1449 pass=1449
nix build …#checks.x86_64-linux.pytests (alone) NIXBUILD_RC=0, runner's own line RESULT: PASS (exit=0), TOTAL collected=20211 passed=20208 skipped=3 failed=0
nix build …#checks.x86_64-linux.nodetests (alone) NIXBUILD_RC=0, runner's own line RESULT: PASS (exit=0), tests=1449 pass=1449 fail=0

The two nix checks were built one at a time, never combined. Verdicts are read from the runners' own RESULT: lines, not a piped exit code.

The first gate run was RED, and it was a real finding. test_git_mainline.py::test_the_LEDGER_of_importers_is_pinned_both_ways failed: git_mainline keeps a two-way-pinned ledger of its importers, and handoff_index.py arrived as a third without being enumerated. That seam guard did exactly its job; the ledger is updated with the reason. (An earlier run also failed on logrotate missing from PATH — a missing environment, not a code failure; the gate says so itself and names the fix, which is to run inside the repo's own dev shell.)

🔴 What I could NOT verify

The live-Postgres path is not exercised by anything. The authoritative gate runs in a nix sandbox with no cluster and no database, so for PostgresSectionStore:

  • the DDL has been read and never executed — nothing has confirmed Postgres accepts it, or that the GENERATED ALWAYS AS (to_tsvector('english', …)) STORED column is legal as written;
  • the generated tsv has never been computed and the GIN index never built;
  • ts_rank has never ordered anything — the ranking claims in the docstrings are about code that has been read, not run;
  • no row has ever been inserted, and the ON CONFLICT identity has never been exercised against a real unique index.

What is pinned hermetically: the SQL text the class builds, that the boost numbers live in exactly one table shared by both backends, the row shape against the declared columns, and every caller path above it. That is a claim about the query construction, not about the database.

Consequently the timer ships gated off (enableHandoffIndexSync = false), matching initiatives-sync's original posture for exactly this reason: a routine ship.sh must not be able to silently arm an unvalidated prod-write timer that creates a new schema object in a prod database. Flip it after a supervised --dry-run then --rebuild has been watched to work.

Not deployed. No home-manager switch was run. The unit exists in nix/home.nix and has never been started.

Ranking quality is unmeasured. The investigation ×2.0 / gotcha ×1.75 weights are a display preference in one named table a reader can argue with, not a measurement over labelled queries. subsystem_recall.py has a labelled fixture corpus for its scorer; this does not, and building one is P2 work.

The two backends rank differently and that is stated, not smoothed over. Postgres uses ts_rank over an english-stemmed tsvector; --offline counts distinct query tokens. They share the boost table and the recency tiebreak and nothing else, so an offline result must never be read as a prediction of the indexed one. Every response prints backend=.

On PR #1064 (feat/handoff-audit)

Read first, as briefed. It does not fit, and nothing is duplicated. scripts/handoff-audit.py globs claudedocs/handoff-*.md off disk — the working-tree source this module exists to avoid — and its parser is skill-audit.py's byte/heading walk aimed at budget measurement, not at retrieval units. The overlap is the corpus, not the reader. It borrows skill-audit's walk; this borrows handoff_doc's, which is the stronger authority for this purpose because it is the writer of the documents. Recorded in the module docstring so the next reader does not re-litigate it.

Files

file lines
scripts/lib/handoff_index.py 1131 (new)
scripts/lib/handoff_search.py 354 (new)
scripts/tests/test_handoff_index.py 694 (new)
nix/home.nix +92
scripts/tests/test_git_mainline.py +11 −1 (importer ledger)
scripts/README.md +2

Total 2,284 insertions, 1 deletion. The two lib/ modules are comment-heavy by this repo's convention — the reasoning that would otherwise be lost lives beside the code.

ZacxDev and others added 3 commits September 1, 2026 12:43
…corpus (P1)

There is no server-side query over handoff-doc BODIES today. `initiatives/sync.py`
stores derived metadata only (`summary` is one parsed line, `current_doc` is a
PATH, `search_text` is session prompts) and the viewer live-reads bodies off disk
with client-side JS search. So the corpus is reachable only by a human who already
knows which doc to open, and `/resume` and subagents re-derive findings that are
written down.

P1 = derivation + write + query CLI. No HTTP service, no network binding, no
pgvector — those are P2/P3.

THE CORPUS COMES FROM GIT REFS, NOT THE WORKING TREE
`git worktree list` in devrc alone returns 143 entries (measured), so a disk scan
indexes mid-edit branches, the same doc N times, and stale orphan copies, and two
runs an hour apart disagree for reasons unrelated to what anyone wrote. Each
repo's mainline is DERIVED via `lib/git_mainline.py` — never hardcoded. Verified
end to end: devrc resolves `origin/main` (94 docs / 959 sections), homelab-talos
resolves `origin/trunk` (54 / 506). A hardcoded `main` would have indexed nothing
for the second.

A doc on disk and ABSENT from the mainline ref is a DURABILITY HOLE: reported,
never indexed. Indexing it would let the search surface answer *from* the hole and
conceal it. The one that exists today reproduces
(`homelab-talos/claudedocs/handoff-limewire-torrent-comps.md`).

SECTIONS, NOT DOCS
`goal|state|investigation|next_step|gotcha|verify`, with `## Open investigations`
split per `### ` sub-block and `## Next steps` per ranked item. A "Ruled out"
paragraph is the highest-value content in this corpus and is invisible at doc
granularity — a few hundred bytes inside a 40 KB document that `ts_rank` drowns.

Every heading/fence/item rule is BORROWED from `lib/handoff_doc.py`, the writer of
these documents and therefore the authority on their shape (`split_front_matter`,
`split_sections`, `canonical_prefix`, `_item_blocks`, `_FORCING`, `_unfenced`). A
second copy would be a parser free to drift from the one measured against the real
corpus, which handoff_doc's own docstrings record happening three times.

TWO GUARDS ON THE READ SURFACE
- RECALL BANNER on every response (subsystem_recall's wording): results are
  POINTERS TO VERIFY, and a hit may describe a gotcha already fixed.
- SILENT-ZERO GUARD: every response carries `indexed_docs=N indexed_sections=M`,
  and a zero beside `indexed_docs=0` renders as `BROKEN INDEX` — sharing no phrase
  with the genuine `NO MATCH` — and exits non-zero.

The index is DERIVED and DISPOSABLE; `--rebuild` truncates and git stays the
system of record.

TESTING
66 hermetic tests. The DB sits behind `SectionStore`, a protocol with two real
implementations (the memory one is production code — `handoff_search --offline`),
so no test needs Postgres. Positive control (a term in exactly one fixture doc
returns exactly that doc, count off zero) and negative control (a term in none
returns 0 with indexed_docs non-zero) run through the same code path.

Mutation sweep under PYTHONDONTWRITEBYTECODE=1: 9/9 killed, each by its own
guard's specific assertion, plus a known-caught positive control.

The `git_mainline` importer ledger caught this module as a third importer and is
updated — that seam guard working exactly as designed.

NOT VERIFIED: the live-Postgres path. The gate runs in a nix sandbox with no
cluster and no database, so the DDL has been read and never executed, the
generated tsvector column never computed, and ts_rank never ordered anything. The
timer is therefore gated OFF by default (`enableHandoffIndexSync = false`) until a
supervised live run validates the write path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session-Id: 90310ba1-6dc2-4779-a616-2ccfe89457d7
…ndex-p1

Claude-Session-Id: 90310ba1-6dc2-4779-a616-2ccfe89457d7
…nd the silent-zero guard could not see a FILTER

First-round adversarial audit of #1209. Seven findings plus six nits; all fixed.
Every fix carries a regression test watched RED at the pre-fix source and GREEN
after: 56 failed / 69 passed at `05e0e0e9`'s modules, 136 passed after. A 25-mutant
sweep (PYTHONDONTWRITEBYTECODE=1, __pycache__ cleared between mutants, one
known-caught positive control) kills all 25, including the two the audit measured
SURVIVING against the old suite: `TRUNCATE` -> `pass` and `.glob` -> `.rglob`.

F1 (🔴) `--rebuild` emptied the table and exited 0 on TOTAL failure. Reproduced:
`main(["--repo","/bogus/a","--repo","/bogus/b","--rebuild"])` ran TRUNCATE+COMMIT
with `docs=0 sections=0` and returned 0, so the unit's `OnFailure=notify-failure@`
never fired. Two independent halves:
  * `rebuild_refusal()` refuses the truncate when ANY repo came back UNMEASURED or
    the derivation is empty, and exits RC_REFUSED (4). It reads a new STRUCTURAL
    `RepoDerivation.unmeasured` field, never a grep over the warning prose — a
    reworded warning would walk past a spelled guard.
  * `PostgresSectionStore.write(rows, rebuild=)` replaces `truncate()` + `upsert()`.
    ONE transaction, ONE commit, all-or-nothing per run, in
    `initiatives/sync.py::write_snapshot`'s shape. The old pair committed the
    TRUNCATE on its own, so any exception in the row loop left the table empty AND
    durable (`MailDB.__exit__` only closes; the inserts were discarded).
  * `upsert`'s docstring claimed crash-safety that was true of `upsert` alone and
    false of every `--rebuild` run — i.e. of every run the timer makes. Corrected.

F2 (🔴) the silent-zero guard could not see a SCOPED query. `--repo <never-indexed>`
— including an absolute path, the natural thing to type on a box that pre-exports
`$DEVRC` — rendered `NO MATCH — the index WAS searched … an answer about the
corpus` beside a reassuring `indexed_docs=352`. Both halves false. `stats()` now
takes the same filters `search()` does (one `_filter_predicates` definition feeding
both the SQL builders), a fourth status `empty-scope` (exit 4) is emitted for a
filter that selects nothing, and an unknown `--repo` is REJECTED naming
`SELECT DISTINCT repo`. `in_scope_docs=/in_scope_sections=` print beside the
totals whenever a filter is applied. Two `SCOPE_REASONS` keep "you typed a path"
distinguishable from "valid filter, empty corpus".

F3 (🟡) the durability report walked a different tree from the thing it was
differencing against: `.glob` on disk vs `git ls-tree -r`. A doc at
`claudedocs/sub/handoff-*.md` yielded `untracked=()` and printed the literal
all-clear. `.rglob`, and the all-clear is now conditional on having scanned a
document — zero docs prints NOT AN ALL-CLEAR.

F4 (🟡) slug was the basename, so two docs with one basename shared an identity and
`ON CONFLICT DO UPDATE` silently kept the last. Slug is now path-derived
(`claudedocs/` stripped, so every existing top-level identity is unchanged), AND
`identity_collisions()` reports any duplicate identity by occurrence count — which
also covers the case a unique slug cannot: two repos whose directory basenames
collide resolve to one label. `main` refuses to write one (RC_COLLISION=5).

F5 (🟡) one committed `\xff` killed the whole run for every repo: `_git` decoded
with the process locale under `text=True` and caught only OSError, inside a
per-document loop, and the unit sets no LANG/LC_ALL. Now `encoding="utf-8",
errors="replace"`. Deliberately NO `UnicodeDecodeError` in the except — the sweep
proved that clause unreachable, and an except naming an impossible exception reads
as coverage while providing none.

F6 (🟡) `\d{4}-\d{2}-\d{2}` admitted `2026-99-99`. Postgres rejects it INSIDE the
write transaction, after the truncate, deterministically — so one typo emptied the
index every 6h. Validated with `date.fromisoformat`; an unparseable date is treated
as absent and the scan continues to the next candidate.

F7 (🟡) `main([])` upserted 509 rows to production. Dry-run is now the DEFAULT and
`--write` is required (the unit's ExecStart gains it); `--write --dry-run` is a
usage error. `main`, `render_derivation`, `default_repos` and the store wiring are
now exercised end-to-end against a recording fake connection that asserts the
statement/commit ORDER.

Nits: the worktree count is stated in ONE place (re-measured, 148 in devrc) after
carrying 601 in the README and 143 in nix/home.nix inside one PR; both modules are
0755 to match their shebangs; `--limit` bounded >= 1; `--repo` disambiguated in
help + README (PATH here, LABEL there);
`test_the_row_shape_matches_the_columns_the_ddl_declares` now parses the CREATE
TABLE body instead of matching `f"{col},"` anywhere (an index column list satisfied
it); the `handoff-audit.py` comment says merged-as-f71ff648 rather than open PR
#1064.

`test_runtime_shebangs.py` gains one allowlist entry: the new executable-bit test
ASSERTS a shebang shape and writes no stub, which is the pinned shape (b) the
existing `test_session_stamp_seam.py` entry already covers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session-Id: 90310ba1-6dc2-4779-a616-2ccfe89457d7
@ZacxDev

ZacxDev commented Sep 1, 2026

Copy link
Copy Markdown
Member Author

First-round audit findings — all 7 + all 6 nits FIXED (98429e80)

Every fix carries a regression test watched red before and green after. The definitive matrix, taken by restoring 05e0e0e9's two modules under the new test file:

result
new tests vs. pre-fix handoff_index.py/handoff_search.py 56 failed, 69 passed
new tests vs. fixed modules 136 passed (126 in this file + the shebang guard's suite)

A 25-mutant sweep (PYTHONDONTWRITEBYTECODE=1, __pycache__ deleted between mutants, one known-caught positive control, baseline green before and after) kills 25/25 — including the two the audit measured SURVIVING against the old 66-test suite:

mutant audit's result now
TRUNCATE {TABLE}pass SURVIVED KILLEDtest_the_truncate_and_every_insert_share_ONE_transaction
.rglob.glob SURVIVED KILLEDtest_a_NESTED_untracked_doc_is_found

Both re-confirmed on the final tree after the last edit.


🔴 F1 — --rebuild truncated before it knew it had rows, exited 0 on total failure — FIXED

Reproduced first: main(["--repo","/bogus/a","--repo","/bogus/b","--rebuild"]) against a fake connection logged TRUNCATE / COMMIT with docs=0 sections=0 and returned 0, so OnFailure=notify-failure@%n.service never fired. Now returns 4 and issues no statement at all.

Three separate changes, because the finding was three defects:

  • (a) refusal + non-zero exit. rebuild_refusal(derivations, rows) refuses when any repo came back UNMEASURED or the derivation is empty → RC_REFUSED = 4. It reads a new structural field RepoDerivation.unmeasured ("no-such-directory" / "no-mainline-ref"), never a grep over the warning prose — a reworded warning would walk straight past a spelled guard. test_the_refusal_reads_the_structural_flag_not_the_warning_prose pins that as a differential: flag set + silent warnings ⇒ refuse; warnings shouting UNMEASURED + flag clear ⇒ allow. The checks run before the store is opened, and the tests inject an open_store that raises, so they assert the write did not happen rather than only that the code was non-zero.
  • (b) one transaction. truncate() + upsert() are replaced by write(rows, *, rebuild) — TRUNCATE and every INSERT in one transaction with a single commit, initiatives/sync.py::write_snapshot's shape. The guard is an ordering assertion over a recording fake connection: the sequence from the TRUNCATE onward must be exactly [TRUNCATE, INSERT×9, COMMIT]. That kills both the TRUNCATE → pass mutant and a mutant that re-inserts the old mid-write commit.
  • (c) the false docstring. upsert's crash-safety claim was true of upsert alone and false of every --rebuild run — i.e. of every run the timer makes. Rewritten to describe what the code does, with the old claim quoted and marked wrong.

Also: a partial derivation (one repo UNMEASURED out of three) still refuses, tested — that shape produces rows, so a guard keyed only on emptiness would let it truncate and replace the corpus with a subset of itself.

Deliberate scope boundary, stated in the code: a non---rebuild run over an UNMEASURED repo still exits 0. It destroys nothing, the warning prints either way, and making it fatal would fire the failure toast forever on a host whose $HOMELAB/$CIVITAI checkout is legitimately absent — the case nix/home.nix explicitly says is safe to configure. What is unrecoverable is truncating, and that is what refuses.

🔴 F2 — the silent-zero guard could not see a SCOPED query — FIXED

Reproduced independently: --repo devrc matched; --repo /home/zach/workspace/devrc and --repo totally-bogus-repo both returned NO MATCH — the index WAS searched … an answer about the corpus, not a broken tool beside indexed_docs=352. Both halves false.

  • stats() on both backends now takes the same repo/sections filters search() does. The SQL predicates come from one _filter_predicates() feeding both search_sql and the new stats_sql; the memory backend has one _selected() read by both its counter and its query. Pinned as a relationship, not by re-typing either clause.
  • New fourth status empty-scope with its own exit code 4 (broken-index keeps 3). EXIT_CODES is pinned two-way against STATUSES, with hit/no-match excluded as the two answers.
  • --repo is validated against SELECT DISTINCT repo (store.repos(); the derived label set offline) and rejected naming the known values.
  • in_scope_docs=/in_scope_sections= print beside the totals whenever a filter is applied — and not when it isn't, so two identical numbers never train the reader to skip both.
  • Two SCOPE_REASONS keep the two causes distinguishable in prose: unknown-repo ("NO REPO IS INDEXED UNDER THE LABEL 'x'. --repo takes a repo LABEL, not a path… Indexed labels: …") vs no-rows ("The filter is VALID … but this corpus holds no row under it"). This came out of the sweep: my first version collapsed the label check into the bare count, all tests stayed green, and the label list became unreachable prose. That mutant is now killed.

test_the_repo_filter_narrows_without_emptying_the_index is widened exactly as asked — it now passes three labels: one that narrows to a populated scope, one that hits, and one the index does not hold.

Live, post-fix:

indexed_docs=93 indexed_sections=968 backend=memory in_scope_docs=0 in_scope_sections=0
query='stash'  repo=totally-bogus

🔴 EMPTY SCOPE — your filter selects 0 of the index's 968 section(s) …
   This is NOT the answer 'the corpus does not mention that': the filter, not the corpus, is what is empty.
   NO REPO IS INDEXED UNDER THE LABEL 'totally-bogus'. --repo takes a repo LABEL, not a path …
   Indexed labels: devrc.

🟡 F3 — durability report blind to nested docs, false all-clear — FIXED

.glob.rglob, so the disk half walks the same tree git ls-tree -r does — the two sides of a set difference must walk the same shape or the difference is meaningless. The all-clear is now conditional on having scanned a document; zero docs prints NOT AN ALL-CLEAR — ZERO documents were scanned. Three tests: the nested hole is found (kills the mutant), the top-level one still is (no traded blind spot), and a committed nested doc is indexed and not reported as a hole (negative control).

🟡 F4 — slug not doc-unique — FIXED, both halves

Slug is now path-derived, with claudedocs/ stripped so every existing top-level identity is byte-unchanged and only a nested doc grows a directory component. Plus identity_collisions() reports any duplicate (repo, slug, section, ordinal) and main refuses to write one (RC_COLLISION = 5), surfaced in render_derivation too.

The detector counts occurrences, not distinct paths — and that correction came from a failing test: two repos whose directory basenames collide resolve to one label (default_repos uses Path(raw).name), so their identically-named docs share every identity with identical doc_paths, which a path-de-duplicating check reads as one document. A unique slug cannot cover that; the detector can.

🟡 F5 — one non-UTF-8 doc killed the entire run — FIXED

encoding="utf-8", errors="replace". replace not ignore: a U+FFFD is visible in a result and tells the reader the source is malformed. The test asserts the other doc in the same repo still reaches a row — "it did not raise" would also be satisfied by a run that silently produced nothing.

Deviation from the finding, deliberately: I did not add UnicodeDecodeError to the except. I did, then the sweep showed removing it left the whole suite green — with errors="replace" the decode cannot raise, so the clause is unreachable. claude/RULES.md says prove a guard reachable or don't write it; an except naming an impossible exception reads as coverage and provides none. The fix is the decode parameters, and the reasoning is in the docstring.

🟡 F6 — impossible date reaching a date column — FIXED

date.fromisoformat validation; an unparseable date is treated as absent, not fatal, and the scan continues to the next candidate in the same source, then the filename. Parametrized over 2026-99-99, 1234-56-78, 2026-02-30, 2026-00-01, 2026-13-01, with a positive control that uses the leap-year pair 2026-02-29 (rejected) / 2024-02-29 (accepted) — a validator that rejected everything would otherwise pass every case.

🟡 F7 — default invocation wrote to production; --dry-run untested — FIXED

Dry-run is now the default; --write is required to mutate, --write --dry-run is a usage error (rc 2). nix/home.nix's ExecStart gains --write, and the enableHandoffIndexSync comment's validation recipe now says --rebuild --write — because a validation run of bare --rebuild would derive, report, write nothing, exit 0 and be read as the supervised live run that switch is waiting for.

main, render_derivation, default_repos, the JSON surface and the store wiring are all now exercised end-to-end against a fake connection. The production seam is pinned too (main's open_store default is _maildb_store) — otherwise the injection could have silently replaced the real path while every test stayed green. import_maildb itself remains unexercised (it imports psycopg2, absent from the sandbox) and the test says so.

Nits — all six fixed

  • worktree count: stated in one place (the module docstring, re-measured 148 in devrc). README and nix/home.nix now point at it and quote no number — they had 601 and 143 inside one PR. Corpus figures re-measured too: devrc 94/968, homelab-talos 54/512.
  • exec bit: both modules 100644 → 100755, with a test pinning shebang-shape + mode together.
  • --limit: bounded >= MIN_LIMIT (1) at parse time, rc 2, parametrized over 0/-1/-10, with a positive control at --limit 1.
  • --repo ambiguity: disambiguated in both help texts and both README rows (PATH in the indexer, LABEL in the search CLI) — and the empty-scope message teaches it at the point of the mistake.
  • test_the_row_shape_matches_the_columns_the_ddl_declares: now parses the CREATE TABLE body and compares as a set, both directions, instead of matching f"{col}," anywhere (the UNIQUE index's column list satisfied it). Carries its own guard that the parser found 11 columns, so neither set comparison can be vacuously about an empty set.
  • stale comment: handoff-audit.py now reads "merged as f71ff648, on main".

One extra change, flagged

scripts/tests/test_runtime_shebangs.py gains one allowlist entry. The repo-wide scan caught the new executable-bit test — correctly by its own text-matching rules, since a quote followed by #! is one of its needles. This is the pinned shape (b) (asserts a shebang, writes no stub, execs nothing) that the existing test_session_stamp_seam.py entry already covers; the needle deliberately does not spell the prefix, or the guard's own self-check would flag the entry. The assertion was first reduced to startswith("#!") + "python3" in first — no env path — and that still tripped it, which is why the pin exists rather than a further reword.

…ndex-p1

Claude-Session-Id: 90310ba1-6dc2-4779-a616-2ccfe89457d7
@ZacxDev

ZacxDev commented Sep 1, 2026

Copy link
Copy Markdown
Member Author

The tekton/devrc-pytests red on 98429e80 was INHERITED, not mine — rebased onto 4162dab1

Recording the triage rather than the conclusion, because "a red I decided wasn't mine" is exactly the claim that deserves evidence.

The red: FAILING: TestAppendLands.test_a_bullet_is_appended_and_the_status_is_named | TOTAL collected=20465 passed=20459 skipped=3 (3 failed). State was failure, not error, so the gate genuinely ran. tekton/devrc-nodetests was green.

It is the store-api/cairn gate flake #1213 already measured. Three independent facts, none of them a theory about my diff:

# fact
1 The failing file is scripts/tests/test_cairn_write.py, added by #1210. git show --stat HEAD | grep -i cairn on my commit returns nothing — my diff cannot reach it.
2 #1216, a docs-only handoff PR, is red on the same file (TestTheActorComesFromTheTOKEN.test_a_FORGED_actor_in_the_body_is_DIS…). Its diff cannot reach cairn either.
3 My tree predated the fix. 1a4350f3"site the store on tmpfs so the gate stops failing", #1211 — landed on main at 14:30:17. git merge-base --is-ancestor 1a4350f3 98429e80false.

#1213 records the baseline directly: "5 store-api failures among 14 open PRs, 2026-09-01", and says plainly that #1211 must not be described as demonstrated until the flake rate is re-measured. The other open-PR reds are the gate working correctly on their own diffs — #1194 on the runtime-shebang scanner, #1177 on the skill-tiers.json ledger — so "the gate is just red" would have been the wrong generalisation, which #1213 also calls out.

Action, per #1183 ("a PR red your diff cannot reach is INHERITED … then rebase; don't debug the leg"): merged origin/main (4162dab1) → 24425b69. Clean merge, and it touched none of my files:

git diff --stat HEAD^1 HEAD -- scripts/lib/handoff_index.py scripts/lib/handoff_search.py scripts/tests/test_handoff_index.py
(empty)

git merge-base --is-ancestor 1a4350f3 HEAD is now true. Locally on the merged tree, test_handoff_index.py + test_runtime_shebangs.py + test_cairn_write.py164 passed.

🔴 What this does NOT claim. A green on the next Tekton run will not prove the flake is fixed — #1213 makes exactly that point, since the gate validating the fix is the thing being fixed. It will only mean this PR's red is gone. And my earlier gate.sh green was a claim about the dev-host tier, which is structurally blind to this: the sandbox tier is where it fires. I am re-running both nix build .#checks.x86_64-linux.{pytests,nodetests} locally one at a time, alongside a base-vs-branch control — the same derivation built from a detached worktree at 4162dab1 (my fix absent) — so attribution rests on a measurement rather than on this reasoning.

@ZacxDev

ZacxDev commented Sep 1, 2026

Copy link
Copy Markdown
Member Author

Correction to row 2 of the triage table above — the conclusion stands, but the stated evidence was wrong, and the corrected version is stronger.

#1216 is not red on the same file. Measured:

PR commit failing test file
#1209 (this one) d4b3472c TestTheActorComesFromTheTOKEN scripts/tests/test_subsystem_store_api.py
#1209 (this one) 98429e80 TestAppendLands scripts/tests/test_cairn_write.py
#1216 (docs-only) TestTheActorComesFromTheTOKEN scripts/tests/test_subsystem_store_api.py

The comment above named TestTheActorComesFromTheTOKEN while calling it "the same file" as the cairn failure — those are two different files, so that row disproved itself.

What the corrected evidence supports, more strongly than the original claim:

  1. This PR has failed on two different store-backed files across two commits, in unrelated ways.
  2. A docs-only PR is red on one of them, and its diff cannot reach either subsystem.
  3. No single change in this diff plausibly breaks two unrelated store-backed suites differently.

Both files spawn real loopback servers and drive a CLI subprocess, which is the shape #1211 addressed (1a4350f3, "site the store on tmpfs so the gate stops failing"). That commit is now an ancestor of this branch — verified with merge-base --is-ancestor, not inferred.

Unchanged from the original triage: this is not a claim that the flake is fixed, only that this PR's red was inherited rather than caused. The sandbox tier has still never been observed green on this branch; that measurement is outstanding and is what should decide the merge.

@ZacxDev

ZacxDev commented Sep 1, 2026

Copy link
Copy Markdown
Member Author

Round 1 audit claims, for the delta round to check against the diff.

1. F1 — `--rebuild` claims to refuse truncation when the derivation is empty or any repo is UNMEASURED, exiting rc 4; TRUNCATE and all inserts claim to run in ONE transaction with a single commit; `upsert`'s crash-safety docstring claims to have been corrected.
2. F2 — `stats()` claims to take `search()`'s filters through one shared predicate builder; a new `empty-scope` status (rc 4) claims to distinguish an unmatched filter from a silent corpus; `--repo` claims to be validated against the indexed repo set.
3. F3 — the durability scan claims to use `.rglob`, and the all-clear line claims to be conditional on having actually scanned.
4. F4 — the slug claims to be path-derived, and `identity_collisions()` claims to count colliding occurrences and exit rc 5.
5. F5 — git output claims to be decoded with `encoding="utf-8", errors="replace"`; `UnicodeDecodeError` was deliberately NOT added to the except clause, on the claim that it is unreachable under those parameters.
6. F6 — dates claim to be validated with `date.fromisoformat`, with an unparseable date treated as absent rather than fatal, and the scan continuing.
7. F7 — dry-run claims to be the default, with an explicit `--write` required to mutate, and the systemd unit's ExecStart claims to have been updated to match.
8. Nits — the worktree count claims to be stated once and scoped; both modules claim mode 100755; `--limit` claims to be bounded at >= 1; `--repo` path-vs-label claims to be disambiguated; the DDL column test claims to be tightened against matching an index definition; the stale "open PR #1064" comment claims to be updated.
9. Test claims — 25/25 mutants killed including the two the round-1 audit measured SURVIVING (`TRUNCATE → pass`, `.rglob → .glob`); red->green matrix 56 failed / 69 passed at the pre-fix modules -> 136 passed after.
10. Attribution claim — the sandbox-tier red on `98429e80` was inherited, not caused: the payload is byte-identical across `98429e80` and `24425b69` and only the base moved.

@ZacxDev

ZacxDev commented Sep 1, 2026

Copy link
Copy Markdown
Member Author

Gate results on 24425b69 — all three tiers green, with the control

Verdicts are the runners' own RESULT: lines, never a piped exit code. (Worth noting the first gate.sh run printed GATE_EXIT=0 from the tail pipeline while its real verdict was GATE: RESULT=FAIL exit=1 — the documented trap, hit live.)

1. Subset

nix develop -c python3 -m pytest scripts/tests/test_handoff_index.py test_handoff_doc.py test_handoff_audit.py test_git_mainline.py test_handoff_skill_size.py -q561 passed.

2. scripts/gate.sh --tier both (dev-host tier)

PASS  pytest  exit=0  verdict='RESULT: PASS (exit=0)'
PASS  node    exit=0  verdict='RESULT: PASS (exit=0)'
GATE: RESULT=PASS exit=0

pytest TOTAL collected=20465 passed=20462 skipped=3 failed=0 (30/30 targets PASS) · node TOTAL suites=5 files=41 tests=1449 pass=1449 fail=0.

Its first run was a genuine catch, not noise: test_no_test_writes_a_usr_bin_env_shebang_at_runtime flagged my new executable-bit test. Fixed via the pinned shape-(b) allowlist entry, the same one test_session_stamp_seam.py already uses.

3. The two nix sandbox checks — built ONE AT A TIME, never combined

Plus a base-vs-branch control: the same derivation from a detached worktree at 4162dab1 with my fix absent.

build nix-build-rc log runner verdict
control 4162dab1 pytests (my fix absent) 0 133 KB RESULT: PASS (exit=0) · collected=20353 passed=20350 skipped=3 failed=0
HEAD 24425b69 pytests 0 134 KB RESULT: PASS (exit=0) · collected=20479 passed=20476 skipped=3 failed=0
HEAD 24425b69 nodetests 0 437 KB RESULT: PASS (exit=0) · suites=5 files=41 tests=1449 pass=1449 fail=0

Guarded against the cached-empty-output case rather than assuming: each log opens with this derivation will be built:, so none was a replay — the 17s nodetests run in particular is real, not a no-op. panic: test timed out count is 0 in all three, and I counted per-target/per-suite result lines rather than reading a status. scripts/tests (collected=11482 confirms the target holding my new tests actually executed — a green from a target that never ran would be indistinguishable otherwise.

Tekton agrees independently, byte-for-byte on the same counts: tekton/devrc-pytests collected=20479 passed=20476 skipped=3 failed=0, tekton/devrc-nodetests tests=1449 pass=1449 fail=0. Both required contexts success.

The inherited red, closed by a controlled pair

My diff is identical across 98429e80 and 24425b69 — the merge touched none of my files. Only the base moved:

pytests
98429e80 (base lacks 1a4350f3) failureFAILING: TestAppendLands…, 3 failed
24425b69 (base has 1a4350f3) success — 0 failed

🔴 This does not say the store-api flake is fixed. #1213 makes exactly that point — the gate validating the fix is the thing being fixed, and its rank-1 verifier (re-measure the flake rate against the 5-of-14 baseline) has not run. It says only that this PR's red was inherited and is gone.

ZacxDev and others added 2 commits September 1, 2026 17:06
…ndex-p1

Claude-Session-Id: 90310ba1-6dc2-4779-a616-2ccfe89457d7
… the refusal guarding it was a permanently-red gate

Round-2 delta audit on #1209. Seven findings; all seven fixed.

F2 is the data loss. `--repo ~/workspace/devrc --rebuild --write` issued
`TRUNCATE initiatives.handoff_section` — no predicate, every repo —
re-inserted only devrc, printed `wrote 968 section row(s)` and exited 0.
homelab-talos's ~515 sections gone, from the exact command a human types to
refresh one repo. The delete is now scoped by `rebuild_delete_labels`: the
labels this run MEASURED, plus (unscoped runs only) any stored label the
config no longer names. Asymmetric on purpose — configured-but-unmeasured
keeps its rows, not-configured-at-all is collected, so scoping the delete
does not trade data loss for a stale corpus. `write(rebuild=True)` with an
empty scope now raises rather than defaulting to "everything".

F1 is the mirror. `rebuild_refusal` refused whenever ANY repo was UNMEASURED:
one present repo plus one absent one gave rc 4, nothing written — on a unit
with `OnFailure=notify-failure@%n.service` behind a 6h timer, i.e. a toast
4x/day forever with the index frozen, on a host whose only sin is not having
$CIVITAI checked out. It now refuses only when ALL repos are unmeasured or the
measured subset yields zero rows; a partial run indexes what resolved and says
`PARTIAL INDEX` on every surface including the write's own success line. What
makes that safe is F2's scoped delete, not the refusal. Both comments that
called an absent checkout "safe to add" are corrected.

F4: the all-clear asserted something about the DISK side and was gated on the
REF-side doc count. Measured: with a real durability hole present, `chmod
0o000 claudedocs/` turned `DURABILITY HOLE` into `warnings: none — every
handoff doc on disk is also in it`. And the `except OSError` around the old
`rglob` could never fire — `rglob` swallows the PermissionError inside
`os.scandir` (measured, CPython 3.12.14). Now `os.walk(onerror=…)`, a
`DiskScan` recording whether the walk ran and what it hid, and an all-clear
gated on that plus a non-empty disk side. `test_zero_documents_scanned_is_
NOT_an_all_clear` widened rather than paralleled.

F3: `--offline` diagnosed an unresolvable checkout as a broken Postgres index,
offering `--rebuild --write` and a systemd unit on a path that opens neither.
`unmeasured` now crosses the seam and gets its own status/exit code (6); the
no-repos case is a usage error (2) in the sibling CLI's own wording.

F5: `--json` replaced the renderer that owned the warning block, so the
machine surface carried rows and no durability-hole report. Warnings, an
`all_clear` boolean and its blockers now travel in the payload.

F6: `--dry-run` never evaluated the refusal, so the pre-flight nix/home.nix
documents passed for a config the real run refuses. Both gates now run in
both modes and return the same code.

F7: `empty-scope` keeps rc 4 for BOTH reasons, documented at length with the
counter-argument stated. The exit code is the one channel read without the
prose, and `no-rows` vs `no-match` are the two zeros this module exists to
separate.

Red->green: the 29 new/widened tests all FAIL against the pre-fix modules
(29 failed / 119 passed) and pass after (149 in the file, 562 across the
handoff+git_mainline+shebang subset). Mutation sweep 16/16 guards KILLED under
PYTHONDONTWRITEBYTECODE=1 with a fresh tree per mutant, plus a GREEN
no-op negative control and a KILLED known-caught positive control. The sweep
found one real gap: `all_clear_blockers`' incomplete-walk arm was unkillable
through the renderer (any warning short-circuits it), so it is reached
directly as a contract pin on the pure function and labelled as one.

Also: the round-1 "136 passed" figure did not reproduce; the accurate numbers
are stated above. Corpus figures re-measured over all four handles (439 docs /
3,978 sections) and re-labelled as a SCALE, since they have now moved three
times in this module's lifetime.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session-Id: 90310ba1-6dc2-4779-a616-2ccfe89457d7
@ZacxDev

ZacxDev commented Sep 1, 2026

Copy link
Copy Markdown
Member Author

Round-2 delta audit — all 7 findings fixed (0e4ea9fa)

Merged origin/main first (36a1fdb1). The only shared file was nix/home.nix; main's single change was the :alo espanso trigger at line ~302, mine are at 38 and 2876 — disjoint, and I read the merged region rather than trusting the clean ort merge.

Per-finding disposition

🟡 1 — the refusal was a permanently-red gate, and two comments called it safe · FIXED

scripts/lib/handoff_index.py rebuild_refusal · nix/home.nix:2868

Implemented exactly as directed: refuse only when ALL repos are unmeasured, or when the measured subset yields zero rows. A partial run indexes what resolved.

The loud half is a new pure partial_scope_warnings, printed by render_derivation (so --dry-run and --json both carry it) and repeated on the write's own success line, which now reads 🔴 THIS INDEX IS PARTIAL — that line is the one output a scripted caller reads alone, and wrote 968 section row(s) looks complete whether or not it is.

Both comments corrected. nix/home.nix no longer says an absent checkout is "safe to add"; it says it is supported, not free — expect a standing PARTIAL INDEX warning on every run, and do not read an unscoped query's silence as evidence that repo is silent. The rationale you asked for is encoded in rebuild_refusal's docstring, including that the thing making a partial run safe is not this guard but finding 2's scoped delete.

RED at 36a1fdb1 test_one_unmeasured_repo_among_several_is_NOT_refusedAssertionError: assert 'REFUSING --rebuild: 1 of 2 repo(s) came back UNMEASURED — gone (no-such-directory)…' is None
GREEN at 0e4ea9fa ✅ + 4 more: test_a_partial_derivation_says_PARTIAL_INDEX_loudly, test_an_ALL_unmeasured_derivation_is_still_refused, test_a_partial_derivation_with_no_rows_is_refused, test_a_partial_rebuild_WRITES_and_says_partial_through_main

Both ends of the range are pinned, because "refuse on ANY" and "refuse on ALL" differ only when the set is mixed.

🟡 2 — a scoped --rebuild --write truncated the whole table · FIXED (scoped delete)

handoff_index.py PostgresSectionStore.write + new pure rebuild_delete_labels

Took the preferred option: scope the delete, so the natural command is correct rather than erroring.

- TRUNCATE initiatives.handoff_section
+ DELETE FROM initiatives.handoff_section WHERE repo = ANY(%s)

The scope is decided in one place by rebuild_delete_labels(derivations, stored, *, scoped). You asked what happens to a repo that disappears from the config — it is still collected, and the asymmetry is the design:

repo state rows
configured and measured deleted + re-inserted
configured but UNMEASURED preserved — this is what makes finding 1's relaxation safe
not configured at all (unscoped run only) deleted — nothing will ever refresh it
any of the above, on a --repo-scoped run only the named repos; a scoped run was never told what the full config is

write(rebuild=True) with an empty scope now raises ValueError rather than defaulting to "everything". main cannot reach it (the refusal guarantees a measured repo), so it is reached directly in a test — it is a library-level belt for a future caller, and is labelled as one.

⚠ Noted in-code: a scoped DELETE is not as cheap as TRUNCATE (tuple versions + autovacuum vs. reclaiming the relation). At low-thousands of rows that is not worth buying a whole-table wipe with; stated so a future reader at a different scale re-derives it rather than inherits it.

RED at 36a1fdb1 test_a_scoped_rebuild_deletes_ONLY_the_repo_it_was_pointed_atassert 'TRUNCATE initiatives.handoff_section' == 'DELETE FROM initiatives.handoff_section WHERE repo = ANY(%s)', with the run's own stdout showing wrote 9 section row(s) … (after TRUNCATE, one transaction) and rc 0
GREEN at 0e4ea9fa ✅ + test_the_scope_is_the_MEASURED_labels_not_every_derived_one, test_a_FULL_run_still_collects_a_repo_that_left_the_config, test_a_rebuild_with_an_EMPTY_scope_raises_rather_than_wiping

The recording connection now captures bound params, not just SQL text — the defect is a delete whose WHERE clause is right and whose scope is wrong, and a text-only recorder cannot see that. RecordingConn.kinds() deliberately still spells TRUNCATE though nothing emits it: the assertions say "TRUNCATE" not in kinds, and a classifier that cannot spell the word satisfies that vacuously.

🟡 3 — --offline misdiagnosed an unmeasurable repo set · FIXED

handoff_search.py _offline_store / offline_targets / run_search

unmeasured now crosses the seam as the structural per-repo reason (never a grep over warning prose) and gets its own status unmeasured-corpus, exit code 6, and a rendered block that shares no opening phrase with any other zero — and, critically, names neither a table nor a unit, because every remedy the broken-index block offers is about a Postgres index this code path never touches.

Both measured variants, live:

(a) --offline --offline-repo /does/not/exist
🔴 UNMEASURABLE CORPUS — all 1 repo(s) this run was pointed at failed to resolve, so no
   corpus was ever built: exist (no-such-directory).
   This is NOT a broken index and NOT an answer about the corpus. There is no table to
   rebuild and no unit to check on this path — the repos themselves are what did not resolve.
rc=6      (no "BROKEN INDEX", no "--rebuild --write", no "handoff-index-sync")

(b) all four handles unset, no --offline-repo
handoff-search: no repos to index. Pass --offline-repo, or set one of: $DEVRC, $HOMELAB, $DATAPACKET, $CIVITAI
rc=2

Wording for (b) matches the sibling CLI, and is asserted as a cross-CLI differential rather than against a hand-copied literal — pinning the string would let the two front ends drift apart again the next time either is reworded.

RED at 36a1fdb1 test_an_UNRESOLVABLE_offline_repo_is_not_a_broken_index, test_no_repos_at_all_is_a_usage_error_in_the_siblings_wording, test_the_json_surface_carries_the_unmeasured_repos, test_the_five_statuses_render_with_no_shared_opening_phrase
GREEN at 0e4ea9fa

Boundary preserved and pinned: a partially unmeasured corpus that still holds rows answers normally (test_a_PARTIALLY_unmeasured_offline_corpus_still_answers). Widening this to "any unmeasured repo" would repeat finding 1's permanently-red mistake in the read path.

🟡 4 — the all-clear was gated on the REF count while asserting about the DISK side · FIXED

handoff_index.py new DiskScan + all_clear_blockers; TestTheAllClearIsEarned widened, not paralleled

🔴 The mechanism was not the one the finding named, and that matters. The old except OSError: return () could never fire: Path.rglob walks via os.scandir and swallows the PermissionError internally. Measured on CPython 3.12.14 — rglob over a chmod 0o000 directory returns [] and raises nothing. So the swallowed error was not being swallowed by that clause; it never reached it. The fix is os.walk(onerror=…), the only form that hands the error back.

DiskScan records paths / scanned / absent / errors, with complete = scanned and not errors, defaulting to scanned=False so an unset field can never license an all-clear. The sentence is now gated on the measurement it describes: every walk ran, hid nothing, and saw at least one path.

Reproduced end to end on one repo with a real hole, as a differential where only the permission bit changes:

                 BASELINE (36a1fdb1)                          FIXED (0e4ea9fa)
readable      🔴 DURABILITY HOLE — 1 handoff doc …         🔴 DURABILITY HOLE — 1 handoff doc …
chmod 0o000   ## warnings: none — every repo resolved a    ⚠ UNSCANNABLE DISK — probe: 1 directory
              mainline ref and every handoff doc on           under claudedocs/ could not be read …
              disk is also in it.                             /…/probe/claudedocs: Permission denied

The uncommitted doc had not moved in either column.

RED at 36a1fdb1 test_an_UNREADABLE_claudedocs_is_NOT_an_all_clear, test_a_disk_side_that_saw_ZERO_paths_is_NOT_an_all_clear, test_the_blockers_NAME_which_measurement_is_missing, test_an_incomplete_walk_is_named_in_the_blockers_LIST_itself, plus the widened test_a_real_scan_with_no_findings_IS_an_all_clear
GREEN at 0e4ea9fa

Also consolidated: the disk half now matches with _HANDOFF_NAME, the same predicate the ref half uses, instead of the HANDOFF_GLOB shell pattern — two spellings of "is this a handoff doc" across the two halves of one set difference is the duplicated predicate one level down from the rglob/glob bug already fixed here.

🟢 5 — --json suppressed the warnings · FIXED

--json now emits an object, not a bare row list: rows, warnings, all_clear (boolean), all_clear_blockers, totals, and a per-repo array carrying unmeasured, untracked, disk_scan_complete, disk_paths_seen, disk_scan_errors. Extracted derivation_warnings() so both renderers read one function. No consumers of the old list shape exist (P1, timer gated off), so this is a rename rather than a migration. Live:

$ handoff_index.py --repo ~/workspace/homelab-talos --json
keys      ['all_clear', 'all_clear_blockers', 'repos', 'rows', 'totals', 'warnings']
all_clear False
warnings  ['🔴 DURABILITY HOLE — 1 handoff doc in homelab-talos is on DISK and NOT in the mainline ref…']

RED: test_the_json_surface_carries_the_WARNINGS_the_text_one_prints + test_the_json_all_clear_is_a_BOOLEAN_a_caller_can_branch_on → GREEN.

🟢 6 — --dry-run never evaluated rebuild_refusal · FIXED

Both gates (collision and refusal — same class of hole) are now evaluated before the --write branch and return the same exit code in both modes; the only difference between the two runs is whether a store is opened. The dry run additionally says this was a DRY RUN — … the SAME check would stop the --write run with this same exit code.

Asserted as the pair, same argv one flag apart, because a test on the dry run alone cannot tell "the gate now fires in dry-run" from "the gate fires for everything" — and with test_a_dry_run_over_a_HEALTHY_config_still_exits_zero as the negative control, so a green dry-run remains obtainable and nix/home.nix's instruction stays usable. That comment is updated to say the dry-run half is only a pre-flight since it learned to fail.

🟢 7 — empty-scope rc 4 · KEPT, documented at length

Chose the first option. Rationale, stated with the counter-argument in front of it in the module docstring: the exit code is the one channel a scripted caller reads without the prose, and no-rows (searched ZERO sections) versus no-match (searched N>0, found nothing) are precisely the two zeros this module exists to keep apart — collapsing them there re-creates the defect at the only layer where the loud rendering cannot help. The contract is stated as "no non-answer exits 0", with no exceptions, because a rule with one carve-out is a rule people forget.

The cost is acknowledged, not waved away: a set -e script running --repo devrc --section gotcha over a corpus with no gotchas now dies. A caller that wants the zero should branch on scope_reason (in the text and in --json), not on the exit code.

The exit-code ledger was restructured to make this checkable. STATUSES is now partitioned three ways — EXIT_CODES (status-keyed), ANSWER_STATUSES (0 by definition), REASON_KEYED_STATUSES — with exit_code_for() as the one reader and SCOPE_REASON_EXIT_CODES pinned two-way against SCOPE_REASONS. The partition test asserts coverage and pairwise disjointness: overlap would mean two rules claim one status and the winner is whichever branch is tested first. Unknown-key fallback points non-zero, the direction a caller notices.

Full ledger: 0 hit/no-match · 2 usage · 3 broken-index · 4 empty-scope (both reasons) · 6 unmeasured-corpus.

Also — the "136 passed" figure

It does not reproduce, and it is dropped rather than restated. Re-measured on this tree:

scope count
test_handoff_index.py alone 149 passed (was 126 before this commit — your 126 reproduces exactly)
+ test_runtime_shebangs.py + test_git_mainline.py + test_handoff_doc.py 562 passed

Corpus figures got the same treatment. Re-measured over all four handles: 439 docs / 3,978 sections (devrc 96/992, homelab-talos 54/522, datapacket-talos 288/2450, civitai 1/14) — which also makes the brief's second-hand ~424 docs first-hand, and slightly low. The docstring now labels these a SCALE, not a value to check against: devrc read 94/968 earlier the same day and 959 before that, and this module has now carried three generations of the number.


Red → green matrix

Built by git archive HEAD of the pre-fix scripts/lib/ into a scratch tree, dropping in the new test file — so the modules are genuinely the ones the audit measured, not a reconstruction.

tree result
pre-fix modules (36a1fdb1) + new tests 29 failed, 119 passed
fixed modules (0e4ea9fa) + new tests 149 passed

Findings 1, 2 and 4 specifically: each fails on the current code with an assertion naming the observed wrong behaviour, quoted per-finding above — not merely with an import error.

Mutation sweep — 16/16 guards KILLED

Every mutant got its own fresh tree, the old string was asserted to occur exactly once before replacement, __pycache__ was cleared, and the runner ran under PYTHONDONTWRITEBYTECODE=1.

id guard verdict
G-A rebuild_refusal refuses only when ALL are unmeasured KILLED
G-B delete scope excludes UNMEASURED repos KILLED
G-C a full run collects repos dropped from the config KILLED
G-D a scoped run claims only what it was pointed at KILLED
G-E the delete carries a real predicate KILLED
G-F an unscoped rebuild raises KILLED
G-G DiskScan.complete — errors make a walk incomplete KILLED
G-H the walk reports its errors (onerror=) KILLED
G-I an incomplete walk blocks the all-clear KILLED (see below)
G-J a zero-path disk side blocks the all-clear KILLED
G-K the gates are evaluated in dry-run KILLED
G-L an unmeasurable corpus is not a broken index KILLED
G-M no repos is a usage error KILLED
G-N empty-scope is a non-answer KILLED
G-O --json carries the warnings KILLED
G-P a partial run says so KILLED
NC negative control — no-op replacement GREEN (the tree is green without a mutation, so every KILLED above is a fact about a guard and not about a broken harness)
PC positive controlslug_for stops stripping handoff- KILLED by 4 named parametrisations

Mutations are isolated to the narrowest expression that can be wrong: G-A mutates the comparison (len(bad) == len(derivations)len(bad) >= 1, i.e. exactly the old behaviour), not the enclosing if; G-I and G-J each leave the other two blocker arms intact.

🔴 The sweep found one real gap, and it is reported rather than smoothed over. G-I SURVIVED on the first pass. all_clear_blockers' incomplete-walk arm cannot change render_derivation's output at all: an incomplete walk always also emits an ⚠ UNSCANNABLE DISK warning (both read DiskScan.complete), and any warning sends the renderer down its if warnings: branch, which never consults the blockers. Through the text report the arm is a second copy of one predicate and is unkillable.

It is kept — because derivation_json publishes all_clear_blockers as its own machine-readable field, and a consumer asking "what stopped the all-clear" must get the complete answer from that list rather than parse prose out of warnings — and it is now reached directly as a contract pin on the pure function, with a hand-built derivation isolating the arm from the other two, plus a negative control on the same shape. Both the docstring and the test say plainly that this is a contract pin, not a regression test for a defect anyone observed in the rendered report.


Gate — all three legs, stated separately

leg verdict
1. subset nix develop … -c python3 -m pytest <4 files> -q -p no:cacheprovider 562 passed (test_handoff_index.py alone: 149 passed)
2. nix develop … -c bash scripts/gate.sh --tier both GATE: RESULT=PASS exit=0 · pytest RESULT: PASS (exit=0), TOTAL collected=20508 passed=20505 skipped=3 failed=0 (floor: 18404 = sum of 30 per-target floors) · node RESULT: PASS (exit=0), TOTAL suites=5 files=41 tests=1449 pass=1449 fail=0 skipped=0
3a. nix build …#checks.x86_64-linux.pytests --no-link -L (alone) NIXBUILD_RC=0, runner's own devrc-pytests> RESULT: PASS (exit=0), devrc-pytests> TOTAL collected=20508 passed=20505 skipped=3 failed=0. 134 KB of log — not the cached case.
3b. nix build …#checks.x86_64-linux.nodetests --no-link -L (alone) NIXBUILD_RC=0, runner's own devrc-nodetests> RESULT: PASS (exit=0), devrc-nodetests> TOTAL suites=5 files=41 tests=1449 pass=1449 fail=0. 437 KB of log — not cached.

The two nix checks were built one at a time, never combined, and I waited out two sibling sessions' nix builds before starting 3a (PIDs resolved by exec name plus /proc/<pid>/cmdline; no -f pattern anywhere near a kill). A third sibling build started during 3b — that run came back green, and a green under contention is trustworthy even though a red would not be.

Verdicts read from the runners' own RESULT: lines, never a piped exit code. Note those lines carry a devrc-pytests> prefix, so an anchored ^RESULT: grep matches nothing.

🔴 tekton/devrc-pytests is RED on 0e4ea9fa, and it is the tracked #1213 flake — third time on this PR

tekton/devrc-nodetests is green. tekton/devrc-pytests reports:

FAILED: pytests — FAILING: TestAppendLands.test_a_bullet_is_appended_and_the_status_is_named
                | TOTAL collected=20508  passed=20504  skipped=3

Recording the discriminating evidence rather than the conclusion:

# fact
1 The failing test is scripts/tests/test_cairn_write.py::TestAppendLands. git diff --name-only HEAD~1 HEAD returns nix/home.nix, scripts/README.md, scripts/lib/handoff_{index,search}.py, scripts/tests/test_handoff_index.pymy diff cannot reach it.
2 Same derivation, same tree, different environment: PASSES. Leg 3a above collected the identical 20508 and reported passed=20505 failed=0. Tekton's passed=20504 means exactly one test failed. So the failure is environment-dependent, not tree-dependent.
3 This exact test failed on this PR at 98429e80, and TestTheActorComesFromTheTOKEN in the sibling test_subsystem_store_api.py failed at d4b3472c — both before this commit existed. It also hit the docs-only PR #1216.
4 It is tracked as #1213 (store-api/cairn gate flake, measured 5-of-14 baseline). #1211 / 1a4350f3 reduced it and is already in this tree; round 1's own note says explicitly that this "does not say the store-api flake is fixed".

So the leading reading is the known flake, not this change — but I am stating it as a ranking with its evidence, not a verdict, and I have not re-run Tekton to confirm: Tekton triggers on push, and a re-run needs a fresh commit, which I did not want to manufacture. The merge is genuinely blocked until that check is green (enforce_admins: true, both contexts required), so this is yours to re-trigger or to weigh against #1213.

gh pr view 1209 --json mergeable,mergeStateStatusMERGEABLE / UNSTABLE — no conflicts; the instability is the check above.

Not done / not verified

  • The live-Postgres path is still unexercised, and this round changed it: TRUNCATE → a parameterised DELETE … WHERE repo = ANY(%s). What is pinned hermetically is the SQL text, the bound scope, and the statement/commit ordering through a recording connection. Nothing has confirmed Postgres accepts the statement, that ANY(%s) binds a Python list as psycopg2 is expected to, or that the delete and the inserts really are one transaction on a real server. A supervised --rebuild --write is still the gate before enableHandoffIndexSync is flipped, and it now has one more thing to watch than it did.
  • Not deployed. No home-manager switch was run; the unit still ships gated off.
  • nix/home.nix was syntax-checked only (nix-instantiate --parse), not switched.

@ZacxDev

ZacxDev commented Sep 1, 2026

Copy link
Copy Markdown
Member Author

Round 2 audit claims, for the round-3 delta to check against the diff.

1. Y1 — `rebuild_refusal` claims to refuse ONLY when all repos are unmeasured or the measured subset yields zero rows; a new pure `partial_scope_warnings` claims to be printed by `render_derivation`, carried in `--json`, and repeated on the write's success line as `🔴 THIS INDEX IS PARTIAL`; `nix/home.nix:2868` claims to no longer call an absent checkout "safe", and the module comment claims to no longer assert it avoided the toast-forever hazard.
2. Y2 — the unpredicated `TRUNCATE` claims to be replaced by `DELETE … WHERE repo = ANY(%s)`, with scope decided by a new pure `rebuild_delete_labels`; the scope claims to be deliberately asymmetric — configured-but-unmeasured PRESERVED, not-configured-at-all COLLECTED on unscoped runs only; `write(rebuild=True)` with an empty scope claims to raise.
3. Y3 — the structural `unmeasured` flag claims to cross the seam into `--offline`; a new `unmeasured-corpus` status (rc 6) claims to name neither a table nor a unit; the no-repos case claims to be rc 2 in the sibling CLI's wording, asserted as a cross-CLI differential; a partially-unmeasured corpus claims to still answer.
4. Y4 — a new `DiskScan` (paths/scanned/absent/errors/`complete`) claims to gate the all-clear on the walk having run, hidden nothing, and seen at least one path. The round-2 finding's NAMED MECHANISM is claimed to be WRONG: `rglob` is claimed to swallow `PermissionError` inside `os.scandir` (returning `[]`, raising nothing), making the previous `except OSError` unreachable, with `os.walk(onerror=…)` claimed as the real fix. The existing test claims to be widened rather than paralleled.
5. Y5 — `--json` claims to be an object carrying `warnings`, `all_clear`, `all_clear_blockers` and per-repo disk facts.
6. Y6 — both gates claim to be evaluated in dry-run with the same exit code as the write path, asserted as a pair with a healthy-config negative control.
7. Y7 — rc 4 claims to be KEPT for both empty-scope reasons, documented with the counter-argument stated; the ledger claims to be restructured into a three-way partition with `exit_code_for` as the single reader.
8. Numbers — the non-reproducing "136 passed" claims to be dropped; real figures claimed as 149 in `test_handoff_index.py` and 562 across the four-file subset; the corpus claims to be re-measured at 439 docs / 3,978 sections and relabelled a SCALE rather than a value.
9. Mutation claims — 16/16 guards KILLED on a fresh tree per mutant under `PYTHONDONTWRITEBYTECODE=1`; negative control (no-op) GREEN and positive control (`slug_for`) KILLED; one guard (`all_clear_blockers`' incomplete-walk arm) claims to have SURVIVED initially, to be unreachable through the rendered report, and to be kept as a contract pin LABELLED as an invariant rather than counted as regression coverage.
10. Gate claims — subset 562 passed; `gate.sh --tier both` PASS with `collected=20508 passed=20505 failed=0` and node `1449/1449`; both nix tiers built one at a time from real (non-cached) logs with the same collection count.

… — 62% of the corpus, every 6h, exit 0

Round-3 delta audit on #1209. Round 2 replaced an unpredicated TRUNCATE with a
scoped `DELETE … WHERE repo = ANY(%s)`; the scope it chose created two 🔴.

F1 — the PARTIAL-INDEX warning promised rows were preserved in the transaction
that deleted them. `partial_scope_warnings` reasons over CONFIGURED labels;
`rebuild_delete_labels` computed its extra "disappeared from config" set over
STORED labels. A renamed checkout makes one repo both: the table holds `civitai`,
`$CIVITAI` now yields `civitai-old` (UNMEASURED), so stored `civitai` reads as
"not configured at all" and was COLLECTED — while the run printed "Their existing
rows are left untouched (the rebuild delete is scoped to what MEASURED)".
Measured at base: `rebuild_delete_labels` returned ('plimforth', 'zarfrepo').

F2 — the timer's own environment made it delete the majority of the corpus. The
unit set only $DEVRC and $HOMELAB while a human --dry-run measures four repos, so
an armed unscoped `--rebuild --write` classified datapacket-talos and civitai as
disappeared-from-config: ~2,476 of ~4,008 sections deleted per 6h tick, exit 0,
`## warnings: none` printed above it, no PARTIAL fired because from that
environment nothing IS unmeasured. Measured at base through `main` over a
recording connection: DELETE bound to ['marganserrepo','trundlerepo','zarfrepo']
for a run told about one.

OPERATOR DECISION, implemented: a rebuild NEVER deletes rows for a repo it was
not told about. The collection moved to an explicit `--prune` the timer does not
pass; it requires --rebuild, is incompatible with --repo, and is REFUSED while
any repo is UNMEASURED (which is what makes F1's two-spellings collision
unreachable — it requires a configured-but-unmeasured label). Orphaned labels are
now REPORTED on every write with the command that removes them, so not collecting
them does not trade data loss for a silent stale corpus.

Defence in depth: the unit derives its Environment from nix/agent-handles.nix —
the same file programs/zsh and the opencode plugin read — so config and
environment cannot silently disagree, and a test fails if the two sets diverge.
Verified through the flake: the unit now carries all four REPO_ENV_HANDLES.

F2 aggravator: the delete scope was computed only inside the --write branch, so
--dry-run — the pre-flight home.nix documents — could not show it. With the
collection behind --prune, a default rebuild's scope is a fact about the
derivation alone, so dry-run prints `## rebuild delete scope`. --prune's extra
set lives in the table; a dry-run says in those words that it cannot show it.

F3 — `UNMEASURABLE CORPUS` counted FAILURES and called it ATTEMPTS: one repo
resolving and one absent printed "all 1 repo(s) this run was pointed at failed to
resolve, so no corpus was ever built" for a run pointed at 2. The status now
requires len(unmeasured) == len(targets), so "all" is true by construction.

F4 — Y3's defect survived in its sibling case: `--offline` over a repo that
RESOLVES and holds zero handoff docs still rendered `🔴 BROKEN INDEX … Rebuild it
(--rebuild --write) or check the handoff-index-sync unit`, rc 3, on a path that
opens no database. New status `derived-zero-docs` (rc 7) naming neither.
`broken-index` is now reachable only from a store that was READ.

F5 — `handoff_paths_in_ref` folded a failed `git ls-tree` and a genuinely empty
ref into one `()`. Survivable while it only moved a count; not once that MEASURED
set decides a DELETE — the repo landed in scope, its rows were deleted, none were
re-inserted, exit 0. It now returns None on git failure and `derive_repo` records
`unmeasured="ls-tree-failed"`.

Nits: `all_clear_blockers`' docstring described an aggregate arm in per-repo
language (the aggregate is correct — per-repo would be permanently red for any
repo without claudedocs/); `--json --write` emitted JSON then prose on stdout so
json.loads(stdout) raised — prose now goes to stderr in --json mode.

Tests: 26 new/updated assertions RED at 0e4ea9f, all 175 GREEN at HEAD. The F1
fixture was rewritten — the old one used `cantread`/`dropped-from-config` as
disjoint identities, so it structurally could not produce the collision; the new
values are pairwise distinct AND carry two spellings of one repo.

Claude-Session-Id: 90310ba1-6dc2-4779-a616-2ccfe89457d7
…ndex-p1

Claude-Session-Id: 90310ba1-6dc2-4779-a616-2ccfe89457d7
…ifier makes the two counts equal

The mutation sweep found one survivor: swapping the UNMEASURABLE CORPUS
renderer's `len(outcome.targets)` back to `len(outcome.unmeasured)` — the exact
F3 defect — stayed green across the whole suite.

Not an equivalent mutant, and not a gap in the fix. `run_search` only emits
`unmeasured-corpus` when `len(unmeasured) == len(targets)`, so EVERY fixture that
reaches that branch through the classifier makes the two constants coincide.
claude/RULES.md → a fixture that can only ever produce the constant's own value
cannot see a mutant that reads a different field, and it SURVIVES a fully green
suite.

`render` is its own function with its own contract, so it is now reached
directly with an outcome the classifier would never build — 3 repos pointed at,
2 unmeasured — where the two counts differ and only one is right. Re-swept: the
mutant is KILLED, 22/22 real mutants dead, positive control killed, negative
control (comment-only no-op) survived.

Claude-Session-Id: 90310ba1-6dc2-4779-a616-2ccfe89457d7
@ZacxDev

ZacxDev commented Sep 2, 2026

Copy link
Copy Markdown
Member Author

Round-3 delta audit — disposition

All five findings and both nits fixed in 45d143ca (merged with origin/main in 106840e3). Every fix carries a regression test watched RED at 0e4ea9fa and green at HEAD — 26 assertions red at base, 176 green now.

🔴 F1 — the PARTIAL-INDEX warning promised rows it deleted — FIXED

partial_scope_warnings reasons over configured labels; rebuild_delete_labels computed its "disappeared from config" set over stored labels. A renamed checkout is both.

Reproduced at base (scripts/lib/handoff_index.py:1605-1610 at 0e4ea9fa):

assert 'plimforth' not in hi.rebuild_delete_labels(ds, stored, scoped=False)
E   AssertionError: assert 'plimforth' not in ('plimforth', 'zarfrepo')

— the table's plimforth collected while the run printed "Their existing rows are left untouched (the rebuild delete is scoped to what MEASURED)".

Fix — the collection is behind an explicit --prune, and rebuild_refusal refuses any --prune run with an unmeasured repo. That makes the collision structurally unreachable: it requires a configured-but-unmeasured label. scripts/lib/handoff_index.py:1656-1737 (rebuild_delete_labels), :1578-1595 (rebuild_refusal's new --prune arm).

Fixture gap closed. The old test used cantread / dropped-from-config as disjoint identities, so it could not produce the collision. _renamed_checkout_fixture now carries two spellings of one repo — configured plimforth-renamed (unmeasured), stored plimforth — plus a genuine orphan wibbleton-retired so the test cannot pass by having nothing to collect. Values pairwise distinct.

Seam guard, not a component one — TestThePartialPromiseIsKept pins the relationship: whenever the PARTIAL sentence fires and a write is allowed, the delete scope ⊆ MEASURED, over the full scoped × prune matrix.

test base HEAD
test_a_FULL_run_never_deletes_a_repo_it_was_not_told_about RED GREEN
test_the_two_spellings_of_one_repo_no_longer_decide_a_delete RED GREEN
test_the_warning_and_the_delete_scope_cannot_contradict_each_other RED GREEN
test_the_collision_state_through_main_deletes_only_the_measured_repo RED GREEN
test_a_PRUNE_over_an_UNMEASURED_repo_is_REFUSED_by_name RED GREEN
test_PRUNE_is_what_collects_an_orphan… (positive control) RED GREEN

🔴 F2 — the timer's environment made it delete 62% of the corpus — FIXED

Reproduced at base through main over a recording connection, unscoped, table holding three labels, run told about one:

assert conn.params_for("DELETE") == [[["zarfrepo"]]]
E   AssertionError: assert [[['marganserrepo', 'trundlerepo', 'zarfrepo']]] == [[['zarfrepo']]]

Operator decision implemented as specified. A rebuild never deletes rows for a repo it was not told about. --prune requires --rebuild, is incompatible with --repo (a scoped run's argv is not the config; treating it as one would delete every repo the caller did not list), and is refused on any unmeasured repo. The timer's ExecStart does not pass it — pinned by test_the_timers_argv_does_not_carry_prune.

Not silently left, either. orphan_labels + orphan_label_warning (handoff_index.py:1739-1772) report every stored label the config does not name, on every write, with the command that removes them — so declining to auto-collect does not trade data loss for a stale corpus. A scoped run reports none, because it cannot know.

Defence in depth, as asked. nix/home.nix:2885-2937 now derives the unit's Environment from nix/agent-handles.nix — the same file programs/zsh and the opencode plugin read — instead of hardcoding two handles. Verified through the real flake (nix eval .#homeConfigurations.zach.config.systemd.user.services.handoff-index-sync.Service.Environment):

… "CIVITAI=%h/workspace/civit/civitai", "CIVITAI_CLI=…", "DATAPACKET=…",
   "DEVRC=%h/workspace/devrc", "HOMELAB=%h/workspace/homelab-talos"

TestTheUnitEnvironmentMatchesTheHandlesTheIndexerReads makes a divergence between REPO_ENV_HANDLES and the nix side a red gate rather than a silent delete, and also fails if the unit goes back to listing handles by hand.

Aggravator — dry-run could not show the delete scope. Half closed, half named. With the collection behind --prune, a default rebuild's scope is a fact about the derivation alone, so --dry-run now prints ## rebuild delete scope with the DELETE and KEPT sets and the line "This list is the COMPLETE delete scope." Under --prune the extra set lives only in the table, so the dry-run says "a --dry-run CANNOT show it" rather than printing a scope missing rows. scripts/README.md and the enableHandoffIndexSync comment both restate the boundary instead of the old wider claim.

🟡 F3 — UNMEASURABLE CORPUS miscount — FIXED

len(outcome.unmeasured) was a count of failures rendered as a count of attempts. The status now requires len(unmeasured) == len(targets), so "all N" is true by construction, and targets is carried on SearchOutcome and in --json. The one-resolved-one-absent case reclassifies (below) and names which path to fix: "1 of the 2 repo(s) named did not resolve AT ALL … the ones listed above are fine." handoff_search.py:386 + :452-468 (the classifier) and :543 (the renderer's denominator).

🟡 F4 — Y3's defect survived in its sibling case — FIXED

--offline over a repo that resolves and holds zero handoff docs rendered 🔴 BROKEN INDEX … Rebuild it (--rebuild --write) or check the handoff-index-sync unit, rc 3 — on a path that opens neither. New status derived-zero-docs, rc 7, sharing no spelling or opening phrase with the other five. broken-index is now reachable only from a store that was READ; a negative control pins that it still exists and still fires for the Postgres shape. handoff_search.py:208-212 (STATUSES), :228-230 (EXIT_CODES), :553-584 (the renderer's own block).

The six-status test now also asserts the two wrong remedies (--rebuild --write, handoff-index-sync) appear on exactly one branch — "shares no opening phrase" would have passed for a block that also told you to rebuild a table.

🟡 F5 — a failed git ls-tree decided a DELETE — FIXED

handoff_paths_in_ref now returns None on git failure and () only for a genuinely empty ref; derive_repo records unmeasured="ls-tree-failed" and returns early, keeping the repo out of the MEASURED set the delete scope is drawn from. handoff_index.py:536-567 (handoff_paths_in_ref), :1061-1074 (derive_repo's early return). The source-level differential is natural, not mocked — git ls-tree refs/heads/no-such-branch genuinely exits non-zero.

Nits — both fixed

  • all_clear_blockers' docstring described an aggregate arm in per-repo language. Wording fixed, logic unchanged, with the reason the aggregate is right recorded (per-repo would be permanently red for any repo without claudedocs/).
  • --json --write emitted JSON then prose on stdout. Prose now goes to stderr in --json mode via a single say() helper, so a new line cannot reintroduce it. Asserted by parsing stdout, not by grepping for an absent word.

Mutation sweep

23 mutants, fresh cp -a tree per mutant, PYTHONDONTWRITEBYTECODE=1, each isolated to the narrowest expression that can be wrong. 22/22 real mutants KILLED; positive control KILLED, negative control (comment-only no-op) SURVIVED, 0 malformed.

One survivor was found and fixed rather than explained away: swapping the renderer's len(targets) back to len(unmeasured) survived a fully green sweep, because run_search only emits unmeasured-corpus when the two are equal — every fixture reaching that branch through the classifier made the two constants coincide. test_the_UNMEASURABLE_sentence_reads_the_DENOMINATOR_not_the_failures reaches render directly with 3 targets / 2 unmeasured, and kills it.

Gate — three verdicts, reported separately

gate verdict
1. subset (nix develop … pytest test_handoff_index.py test_handoff_audit.py test_handoff_doc.py test_resume_state_handoff_resolution.py -q -p no:cacheprovider) 779 passed in 49.41s — and 176 passed for test_handoff_index.py alone on the clean committed tree
2. scripts/gate.sh --tier both (dev-host tier) pytestRESULT: PASS (exit=0), TOTAL collected=20590 passed=20587 skipped=3 failed=0 (floor: 18404)
nodeRESULT: PASS (exit=0), TOTAL suites=5 files=41 tests=1449 pass=1449 fail=0 (floor: 1367)
GATE: RESULT=PASS exit=0
3a. nix build .#checks.x86_64-linux.pytests (sandbox tier) devrc-pytests> RESULT: all good / devrc-pytests> RESULT: PASS (exit=0), TOTAL collected=20590 passed=20587 skipped=3 failed=0; NIXPY_RC=0; 0 panic: test timed out, 0 error:
3b. nix build .#checks.x86_64-linux.nodetests (sandbox tier) devrc-nodetests> RESULT: PASS (exit=0), TOTAL suites=5 files=41 tests=1449 pass=1449 fail=0; NIXNODE_RC=0; 8,834-line log, so not the cached-silent case

Both nix checks were built one at a time, never in one invocation. /proc was scanned by exec name for a sibling nix build before starting — none was running.

Live verification (not only tests)

--repo … --rebuild --prune            → rc 2  "--prune and --repo contradict each other…"
--prune (no --rebuild)                → rc 2  "--prune only widens a --rebuild's delete…"
--rebuild --prune, one repo renamed   → rc 4  "REFUSING --rebuild --prune: 1 of 2 repo(s) came back UNMEASURED…"
--rebuild (dry-run, this worktree)    → rc 0, prints:
    ## rebuild delete scope
      DELETE (measured, will be re-derived): devrc-handoff-search
      KEPT (configured but UNMEASURED): (none)
      No --prune, so a stored label this config does not name is REPORTED and kept,
      never deleted. This list is the COMPLETE delete scope.
handoff_search --offline <resolves-empty> <absent>  → rc 7
    🔴 ZERO HANDOFF DOCS DERIVED — the 1 repo(s) this run read (hz-empty) …
       Separately, 1 of the 2 repo(s) named did not resolve AT ALL: hz-absent … the ones listed above are fine.

The nix/home.nix change was verified by evaluating the real flake, not by parsing: nix eval .#homeConfigurations.zach.config.systemd.user.services.handoff-index-sync.Service.Environment returns all four REPO_ENV_HANDLES.

Stated plainly

  • The Postgres path is still exercised by no test. The gate has no database; every claim about PostgresSectionStore is about code that has been read, not run. The delete's bound scope is asserted against a recording connection, which is what the F1/F2 evidence rests on — not against a server.
  • --prune has never been run against a real table. Its argv gating, its refusal and its scope arithmetic are tested; the DELETE it would issue has only ever been recorded, not executed.
  • Merged-tree caveat. main moved twice during this work; 106840e3 merges origin/main and the upstream commits touch none of the five files here (git log 45d143ca..origin/main -- <those paths> is empty). All three gates ran on that merged tree. main can move again before merge.
  • The timer stays enableHandoffIndexSync = false. Nothing here arms it.

Line references above are against 926b82ce (post-merge). The one 0e4ea9fa reference is explicitly labelled — it names where the defect was, not where the fix is.

@ZacxDev

ZacxDev commented Sep 2, 2026

Copy link
Copy Markdown
Member Author

Round 3 audit claims, for the round-4 delta to check against the diff.

1. Z1 — the "disappeared from config" collection claims to have moved behind an explicit `--prune`, and a `--prune` run with ANY unmeasured repo claims to be refused, which is claimed to make the two-spellings collision structurally unreachable.
2. Z2 — `--prune` claims to require `--rebuild` and to be rejected with `--repo`; orphans claim to be REPORTED rather than collected, so not-collecting is claimed not to trade data loss for a stale corpus.
3. Z2b — `nix/home.nix` claims to derive the unit's `Environment` from `nix/agent-handles.nix`, claimed verified by evaluating the real flake with all four `REPO_ENV_HANDLES` present, and a divergence between module and nix claims to be a red gate.
4. Z2c — `--dry-run` claims to print `## rebuild delete scope`, and under `--prune` claims to say in those words that it CANNOT show the table-side set.
5. Z3 — the `UNMEASURABLE CORPUS` status claims to require `len(unmeasured) == len(targets)`, with `targets` carried on the outcome and in `--json`.
6. Z4 — a new status `derived-zero-docs` (rc 7) claims to exist, and `broken-index` claims to be reachable only from a store that was READ, with a negative control claimed to pin that it still fires for the Postgres shape.
7. Z5 — `_git` claims to return `None` on git failure versus `()` for a genuinely empty ref, with `derive_repo` recording `unmeasured="ls-tree-failed"`, and the differential claimed to be source-level rather than mocked.
8. Nits — the `all_clear_blockers` docstring claims to be reworded with logic unchanged; `--json` prose claims to go to stderr via one `say()`, asserted by PARSING stdout.
9. Test claims — the F1 fixture claims to have been replaced by `_renamed_checkout_fixture` carrying configured `plimforth-renamed` (unmeasured) + stored `plimforth` + orphan `wibbleton-retired`, no longer disjoint; `TestThePartialPromiseIsKept` claims to pin the RELATIONSHIP (delete scope subset of MEASURED whenever the warning fires and a write is allowed) over the full scoped x prune matrix.
10. Mutation claims — 23 mutants on a fresh tree each under `PYTHONDONTWRITEBYTECODE=1`, 22/22 real mutants killed, positive control killed, no-op control SURVIVED; one survivor (`len(targets)` -> `len(unmeasured)` in the renderer) claims to have been found and FIXED rather than explained away, with a test now reaching `render` directly at 3 targets / 2 unmeasured.
11. Gate claims — subset 779 passed and 176 for the target file; `gate.sh --tier both` PASS with `collected=20590 passed=20587 failed=0` and node 1449/1449; BOTH nix tiers built one at a time with non-cached logs.
12. Scope claim — the three gates ran on merged tree `106840e3`; the pushed head `926b82ce` adds one TEST-ONLY commit (26 lines in `test_handoff_index.py`) that those gate runs did not cover.

…ndex-p1

Claude-Session-Id: 90310ba1-6dc2-4779-a616-2ccfe89457d7
…— the same 62% delete, from the operator's shell (#1209 round 4)

Round-4 delta audit, seven findings. The 🔴 one is `--prune`'s precondition
being only half-checked.

F1 🔴 `--prune` refused on an UNMEASURED repo and nothing else. An UNSET
handle produces no derivation at all, so it can never BE unmeasured — the
guard was structurally blind to it, and **the narrowest configs produce zero
unmeasured repos, so it was weakest exactly where the risk is highest.**
MEASURED end to end at 0906bbb: `$DEVRC`/`$HOMELAB` set, `$DATAPACKET`/
`$CIVITAI` unset (the ordinary state on a host without those checkouts —
`nix/agent-handles.nix` existence-guards every handle), table holding all
four labels ⇒ 2 repos derived, both measured, `bad == []`, no refusal, DELETE
bound to `['civitairepo','datapacketrepo','devrcrepo','homelabrepo']`, rc 0.
Same blast radius as the unit's two-handle `Environment`, relocated to the
operator's own shell. New `prune_config_refusal` (pure) + `unset_repo_handles`
refuse it by name; `main` runs the config-width guard before the read-failure
one and the two messages open with different tokens so no assertion can
confuse them. Every non-prune path is untouched, so the timer cannot go red.

  Aggravator, same change: the ORPHANED LABELS warning fired in exactly that
  state, named those repos, and offered `--rebuild --prune --write` as its
  FIRST remedy — the tool suggesting the destructive command over the fix
  that keeps the data. Remedies reordered (re-add the handle first), and
  "Nothing will ever refresh them" corrected: this run speaks for THIS host's
  config, and the index is shared.

F2 🟡 After a successful `--prune`, the run reported the rows it had just
DELETEd as "They are NOT deleted … Remove them deliberately with `--rebuild
--prune --write`". `orphan_labels` read the PRE-write `stored` list; it now
subtracts the run's BOUND DELETE SCOPE — the thing that actually happened,
not an `args.prune` boolean that could drift from it. There was no
end-to-end test of the successful prune path through `main` at all; there is
now, plus its one-flag differential.

F3 🟡 `scripts/README.md`'s `lib/handoff_search.py` row still said FOUR zeros
and four exit codes; `derived-zero-docs`/rc 7 was missing. Corrected AND
pinned two ways against `handoff_search.STATUSES`/`EXIT_CODES`.

F4 🟢 The non-prune plan line promised "a stored label this config does not
name is REPORTED and kept" — a report that needs `store.repos()`, which a
dry-run never opens. `rebuild_plan_lines` now takes `write` and says what the
run it describes can actually do. Same field fixes the nit where the
`--prune` plan claimed "a --dry-run CANNOT show it" during `--write` runs.

F5 🟢 Both round-4 survivors killed, and shown dying:
  * `rebuild_plan_lines`' `if scoped:` → `if False:` SURVIVED (176 passed) at
    base — a scoped run fell through to the no-prune branch and printed
    "REPORTED and kept", false twice over since `orphan_labels` returns ()
    when scoped. Now DIED.
  * `orphan_label_warning`'s "only ever" → "ALWAYS" SURVIVED (176 passed) at
    base against three substring assertions. The sentence is now pinned as a
    WHOLE NORMALISED STRING. Now DIED. A cosmetic reword costs a test edit;
    that is the intended price of a machine-readable claim.

F6 🟢 The unit/module handle guard checked `REPO_ENV_HANDLES − declared` only
while `nix/home.nix` said it "fails if the two sets ever diverge" — and they
DID diverge (nix declares five repo handles, the module reads four) with the
suite green. WIDENED rather than merely re-described: the other direction is
now pinned against an ENUMERATED ledger (`CIVITAI_CLI`, with its reason), so
a new nix handle forces a decision instead of being silently excluded. The
home.nix comment now states the asymmetry instead of implying symmetry.

F7 🟢 REPRODUCED first, as asked — it does reproduce. Committing a handoff
doc and deleting its blob from `.git/objects` leaves `git ls-tree` listing
the path while `git show <ref>:<path>` fails: `derive_repo` returns `docs=0`
with `unmeasured is None`, and rc 7 said the repos "resolved a mainline ref
and hold no `claudedocs/handoff-*.md` in it" — sending the reader to WRITE a
doc that is already committed. Same `None`-vs-`()` conflation as
`handoff_paths_in_ref`, one function later. `RepoDerivation.unreadable`
carries the paths, the rc-7 renderer splits on it, and both machine surfaces
(`handoff_search --json`, `handoff_index --json`) carry the discriminator.

Nits: the orphan report is "on every `--write` run", not "every run"
(two comments + the README); `--prune --repo` without `--rebuild` now answers
the `--repo` conflict — the one that survives adding `--rebuild` — instead of
nudging toward the dangerous combination.

Testing: every fix carries a regression test watched RED at 0906bbb and
GREEN at HEAD (20 red at base, 200 green at HEAD). F6's widening is an
INVARIANT GUARD, not a regression test — it is green at base by construction
and is instead mutation-verified. 18-mutant battery, fresh tree per mutant,
`PYTHONDONTWRITEBYTECODE=1`, no-op negative control SURVIVED + known-caught
positive control DIED: 18/18 as expected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session-Id: 90310ba1-6dc2-4779-a616-2ccfe89457d7
@ZacxDev

ZacxDev commented Sep 2, 2026

Copy link
Copy Markdown
Member Author

Round-4 delta audit — all seven findings dispositioned

Base for every RED measurement below: 0906bbb6 (this branch merged with origin/main 5a82aaa9). Fixes are in 9fd093b2.

# finding disposition
F1 🔴 --prune's refusal blind to an UNSET handle FIXED — new guard, reproduced end-to-end first
F2 🟡 successful prune reports deleted rows as "NOT deleted" FIXED + the missing end-to-end test
F3 🟡 README says FOUR zeros / four exit codes FIXED and PINNED (it was cheap)
F4 🟢 non-prune plan promises a report a dry-run cannot make FIXED
F5 🟢 two surviving mutants BOTH KILLED, and shown surviving at base first
F6 🟢 one-way handle guard described as symmetric CHECK WIDENED (not just the description) — see below
F7 🟢 rc 7's sentence false when every doc fails to read REPRODUCED, then fixed

F1 🔴 — the guard was weakest exactly where the risk was highest

rebuild_refusal asks "did a configured repo fail to READ". An UNSET handle produces no derivation at all, so it can never be UNMEASURED — the guard is structurally blind to it, and the narrowest configs produce zero unmeasured repos.

Measured at 0906bbb6, two real readable repos and two unset handles, table holding all four labels:

rc                : 0
handles SET       : ['DEVRC', 'HOMELAB']
handles UNSET     : ['DATAPACKET', 'CIVITAI']
table held        : ('devrcrepo','homelabrepo','datapacketrepo','civitairepo')
DELETE bound to   : [['civitairepo','datapacketrepo','devrcrepo','homelabrepo']]

At 9fd093b2, same fixture, same argv:

rc                : 4
DELETE bound to   : (no DELETE issued)

prune_config_refusal(unset) (pure) + unset_repo_handles(env=None). main runs the config-width guard before the read-failure one, and the two messages open with different tokens (handle(s) are UNSET vs came back UNMEASURED) so an assertion cannot confuse them — the existing unmeasured test was asserting only the shared REFUSING --rebuild --prune prefix and has been hardened.

Consequence, stated plainly: --prune can now only be run from a host that has every checkout. That is the intended answer — pruning from a partial view is the thing that deletes the repos you cannot see. It cannot become a permanently-red gate: the timer never passes --prune, and a non-prune --rebuild --write over a narrow config is untouched (asserted, and mutation-verified).

Aggravator, fixed in the same change. The ORPHANED LABELS warning fired in exactly that state, named those repos, and offered --rebuild --prune --write as its first remedy. Remedies reordered (re-add the handle first, delete only when the repo is genuinely gone), and "Nothing will ever refresh them" corrected — this run speaks for THIS host's config, and the index is shared.

F2 🟡 — the report contradicted the transaction above it

At base, a successful --rebuild --prune --write:

⚠ ORPHANED LABELS — the table holds 1 repo label(s) this config does not name:
wibbleton-retired. … They are NOT deleted … Remove them deliberately with
`--rebuild --prune --write`.

…about the row it had just DELETEd. orphan_labels now takes deleted and subtracts the run's bound DELETE scope — the thing that actually happened, not an args.prune boolean that could drift from it. Without --prune the scope is the MEASURED labels, which are configured by construction, so the non-prune report is provably unchanged.

There was no end-to-end test of the successful prune path through main — the positive control only drove the pure functions, which is exactly why stored being the pre-write read went unnoticed. Added, with its one-flag differential.

F3 🟡 — pinned, because it was cheap

Row corrected to five zeros / five exit codes with 🔴 ZERO HANDOFF DOCS DERIVED (7). Pinned two ways against the module's own ledgers: every EXIT_CODES entry must appear in the row, and the count word must match len(STATUSES) - 1. Plus an instrument control so a short word-map cannot fail the pin for the wrong reason.

F4 🟢 / the plan-line nits

rebuild_plan_lines now takes write. Four branches instead of three: the dry-run non-prune line says it cannot list those labels (a dry-run opens no table) rather than promising they are REPORTED; the --write non-prune line points at the report it actually makes; and the --prune line no longer says "a --dry-run CANNOT show it" during --write runs.

F5 🟢 — both survivors, shown surviving and then dying

mutant base src + base tests HEAD src + HEAD tests
rebuild_plan_lines: if scoped:if False: SURVIVED (176 passed) DIED
orphan_label_warning: "only ever" → "ALWAYS" SURVIVED (176 passed) DIED

The prose one is now pinned as the whole normalised string, not three substrings. A cosmetic reword fails the suite — the intended cost of a machine-readable claim. The scoped one is killed by asserting the scoped sentence PRESENT and the no-prune sentence ABSENT; either assertion alone is walkable.

F6 🟢 — widened, not just re-described

The two sets did diverge (nix declares five repo handles, the module reads four) while nix/home.nix said the test "fails if the two sets ever diverge", and the suite was green.

I widened the check rather than only fixing the sentence, because the two directions are not the same kind of fact and only one of them was uncheckable:

  • module-reads-but-nix-does-not-export is a hazard (the unit sees a narrower config than a human) — forbidden outright, as before;
  • nix-exports-but-module-does-not-read is a choiceagent-handles.nix serves every agent shell, and CIVITAI_CLI is a client checkout with no handoff corpus. Now pinned against an enumerated ledger with its reason in the source, so a new handle forces a decision instead of being silently excluded.

⚠ This one is an INVARIANT GUARD, not regression coverage — it is green at base by construction, and I am labelling it as such rather than counting it in the red→green matrix. It is mutation-verified instead (adding a sixth nix handle kills it).

F7 🟢 — it reproduces

The auditor did not reproduce it; I did, before fixing. Commit a handoff doc, delete its blob from .git/objects:

ls-tree after unlink: ('claudedocs/handoff-widget-relay.md',)
doc_text_at_ref     : None
unmeasured: None  docs: 0  sections: 0
STATUS: derived-zero-docs   EXIT: 7
🔴 ZERO HANDOFF DOCS DERIVED — the 1 repo(s) this run read (zarfrepo) resolved a
mainline ref and hold no `claudedocs/handoff-*.md` in it …

The repo holds one. RepoDerivation.unreadable now carries the paths (the same None-vs-() conflation as handoff_paths_in_ref, one function later), the rc-7 renderer splits on it and points at git fsck rather than at writing a doc that already exists, and both machine surfaces carry the discriminator. The doc-free sentence is unchanged and asserted as the differential.


Testing

20 tests RED at 0906bbb6, 200 GREEN at HEAD in scripts/tests/test_handoff_index.py. The F1 and F2 base failures are the reproductions themselves — F1's base run opened the store for a run that must not write; F2's base run printed "They are NOT deleted" about wibbleton-retired in the same run that DELETEd it.

Three of the new tests are deliberate controls that are green at base and stay green (the complete-config differential, the non-prune blast-radius control, the without-prune orphan differential); F6's widening is an invariant guard as noted. Neither is counted as regression coverage.

Mutation battery — 18/18 as expected. Fresh tree per mutant, PYTHONDONTWRITEBYTECODE=1, each mutation isolated to the narrowest expression that can be wrong.

   noop-negative-control              expected=SURVIVED  got=SURVIVED  200 passed
   positive-control-delete-scope      expected=DIED      got=DIED
   F5a-plan-scoped-branch             expected=DIED      got=DIED
   F5b-orphan-only-ever               expected=DIED      got=DIED
   F1-refusal-always-none             expected=DIED      got=DIED
   F1-gate-not-wired                  expected=DIED      got=DIED
   F1-unset-detector-blind            expected=DIED      got=DIED
   F1-unset-detector-inverted         expected=DIED      got=DIED
   F2-orphan-ignores-deleted          expected=DIED      got=DIED
   F2-main-passes-no-scope            expected=DIED      got=DIED
   F4-prune-branch-ignores-write      expected=DIED      got=DIED
   F4-nonprune-always-claims-report   expected=DIED      got=DIED
   F7-derivation-drops-unreadable     expected=DIED      got=DIED
   F7-renderer-ignores-unreadable     expected=DIED      got=DIED
   F7-seam-drops-unreadable           expected=DIED      got=DIED
   F3-readme-stale-count              expected=DIED      got=DIED
   F3-readme-drops-rc7                expected=DIED      got=DIED
   F6-new-nix-handle-unledgered       expected=DIED      got=DIED

🔴 A NEW finding I am NOT fixing here, stated plainly

Reproducing F7 exposed a sibling data-loss path that F7 itself does not close, and it is the same class as the ls-tree bug this PR already fixed:

A repo whose mainline lists handoff docs that git cannot produce is classified MEASURED (unmeasured is None, docs == 0). rebuild_delete_labels reads that MEASURED set to decide what a --rebuild may DELETE — so such a repo's rows are deleted and zero re-inserted, at rc 0. It is not silent (a per-doc ⚠ UNREADABLE warning is printed) and it is caught when it is the ONLY repo (rebuild_refusal's ZERO-rows arm), but with another repo contributing rows the rebuild proceeds and that repo's corpus is destroyed.

The structural fix is to make tracked non-empty AND docs == 0 an UNMEASURED reason, which changes the delete scope, the rc-7/rc-6 split and unmeasured-corpus's prose — a behaviour change with its own audit round, not a delta-fix. F7 as filed was scoped to the false SENTENCE, and that is what this commit closes.

Closing condition: a PR that makes an all-docs-unreadable repo structurally UNMEASURED, with an end-to-end delete-scope test showing its rows survive a --rebuild --write alongside a repo that does contribute rows. Checked by that PR merging.


Gate — all four runs, on the MERGED tree (9fd093b2, with origin/main 5a82aaa9 as an ancestor)

I am the gate here (branch protection is off), so both tiers were run, and the nix checks one at a time per this repo's contention rule.

Tier 1 — dev host, scripts/gate.sh --tier both

  FAIL  pytest  exit=1  verdict='RESULT: FAIL (exit=1)'
  PASS  node    exit=0  verdict='RESULT: PASS (exit=0)'
GATE: RESULT=FAIL exit=1
  TOTAL collected=20691  passed=20687  skipped=3  failed=1  (floor: 18404 = sum of 30 per-target floors)
  TOTAL suites=5 files=41 tests=1449 pass=1449 fail=0 skipped=0  (floor: 1367)

Tier 2 — nix sandbox, built separately

devrc-pytests>   TOTAL collected=20691 passed=20687 skipped=3 failed=1 (floor: 18404)
devrc-pytests> RESULT: FAIL (exit=1)
error: Cannot build '…-devrc-pytests.drv'  (builder failed with exit code 1)

devrc-nodetests>   TOTAL suites=5 files=41 tests=1449 pass=1449 fail=0 skipped=0  (floor: 1367)
devrc-nodetests> RESULT: PASS (exit=0)

NIXBUILD_PYTESTS_RC=0 also appears in my pytests transcript — that is the documented | tail trap swallowing nix's status, not a pass. The authoritative reading is the runner's own RESULT: line and error: Cannot build, both quoted above.

The one failure is PRE-EXISTING and is not reachable from this diff. Both tiers fail on exactly one test, and only that one:

scripts/collector/keylog/tests/test_espanso_detect.py::test_live_existing_resolutions_not_made_ambiguous
E  AssertionError: search terms regressed:
   {'recom': (':rna', None, [':acq', ':rna']), 'recommend': (':rna', None, [':acq', ':rna'])}

The discriminating control: that test reads nix/home.nix out of the tree, so I ran it against a clean origin/main (5a82aaa9) checkout — byte-identical failure, same two terms, same snippets. main is red on it today; this PR does not touch espanso snippets, and my only nix/home.nix edit is a comment block in the handoff-index-sync unit.

Scope of that claim, stated: I verified the espanso failure is identical at origin/main by running the test directly against an origin/main tree. I did not build the origin/main pytests derivation as well — that is a second ~15-minute build on a box that had two other agents' nix builds running through this session, and the test's only input is a file I diffed.

@ZacxDev

ZacxDev commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

Round 4 audit claims, for the round-5 delta to check against the diff.

1. W1 — new pure `unset_repo_handles()` and `prune_config_refusal()` claim to refuse `--prune` when any configured handle is UNSET, wired ahead of `rebuild_refusal`; claimed reproduced at base (2 handles set, 2 unset, table holding 4 → no refusal, DELETE bound to all four, rc 0) and refused at HEAD (rc 4, no DELETE).
2. W1b — `orphan_label_warning` claims to reorder its remedies so re-adding the handle comes first, and to have dropped the false "Nothing will ever refresh them".
3. W1c — a claimed CONSEQUENCE: `--prune` now REQUIRES every checkout present. The timer is claimed never to pass `--prune`, and non-prune rebuilds claimed untouched (asserted + mutation-verified), so it is claimed it cannot go permanently red.
4. W2 — `orphan_labels(..., deleted=)` claims to be called with the BOUND DELETE SCOPE rather than an `args.prune` bool, so a successful prune no longer reports the rows it deleted as "NOT deleted"; a missing end-to-end prune test through `main` claims to have been added, plus a one-flag differential.
5. W3 — `scripts/README.md:117` claims to say five zeros / five exit codes including rc 7, pinned two ways against `handoff_search.STATUSES`/`EXIT_CODES` with an instrument control on the count-word map.
6. W4 — `rebuild_plan_lines(..., write=)` claims four branches, also fixing the "a --dry-run CANNOT show it" nit on `--write` runs.
7. W5 — both previously-surviving mutants claim to be KILLED, each shown SURVIVED at base (176 passed) and DIED at HEAD; the prose one claims to be pinned as a WHOLE NORMALISED STRING (`_EXPECTED_ORPHAN_WARNING`), not substrings.
8. W6 — the unit/module handle guard claims to have been WIDENED rather than re-described, pinned against an enumerated ledger (`CIVITAI_CLI` + reason). Claimed explicitly to be an INVARIANT GUARD, green at base by construction, mutation-verified, and NOT counted as regression coverage.
9. W7 — claimed REPRODUCED first (delete a committed doc's blob: `ls-tree` lists it, `git show` fails, `docs=0`, `unmeasured=None`, rc 7 wrongly claims the repo holds no docs), then fixed via `RepoDerivation.unreadable` with the renderer split and carried on both `--json` surfaces.
10. Test claims — 20 RED at `0906bbb6` → 200 GREEN at HEAD; three new tests declared deliberate controls green at BOTH ends, plus W6's invariant guard, none counted as regression coverage.
11. Mutation claims — 18/18 as expected, fresh tree per mutant, `PYTHONDONTWRITEBYTECODE=1`, narrowest-expression mutations, no-op negative control SURVIVED and known-caught positive control DIED; one mutant that failed to apply claims to have been fixed and re-run rather than scored.
12. Gate claims — HEAD is the merged tree (merged `origin/main` 5a82aaa9 first). `gate.sh --tier both` FAIL exit 1 on exactly one test; both nix tiers built one at a time, nodetests PASS, pytests FAIL on the same single test. The sole failure is claimed PRE-EXISTING ON MAIN (`test_espanso_detect.py::test_live_existing_resolutions_not_made_ambiguous`), claimed verified byte-identical against a clean `origin/main` checkout — but explicitly NOT verified by building main's own pytests derivation as a second control.
13. Filed-not-fixed — a sibling path W7 does not close: a repo whose mainline LISTS handoff docs git cannot produce is classified MEASURED, so `rebuild_delete_labels` deletes its rows and re-inserts zero at rc 0, caught only when it is the sole repo. Claimed to need its own round because the structural fix changes the delete scope and the rc-6/rc-7 split.

…ndex-p1

Claude-Session-Id: 90310ba1-6dc2-4779-a616-2ccfe89457d7
…ndex-p1

Claude-Session-Id: 90310ba1-6dc2-4779-a616-2ccfe89457d7
…it prints in — four were false, and the plan block described a run that never ran (#1209 round 5)

Two SHAPE sweeps, not four point fixes.

SWEEP 1 — operator guidance that names a command without checking the state
that command requires. Grepped every remedy/next-step sentence the two modules
emit and checked each against the state it is printed in. 4 of 14 were wrong;
all 4 fixed, the other 10 hold.

  1. `prune_config_refusal` offered "Set every handle and re-run --prune from a
     host that has all the checkouts — or drop --prune". For the case an
     operator most often runs --prune for — a RETIRED repo — NEITHER exists:
     `nix/agent-handles.nix` existence-guards every handle on its directory, so
     a deleted checkout is an unset handle on EVERY host, and dropping --prune
     un-blocks the rebuild while leaving the exact rows the operator came to
     remove. The route that works — drop the handle from agent-handles.nix AND
     REPO_ENV_HANDLES, ship, then prune — was named nowhere. Now named.
  2. `orphan_label_warning` recommended `--rebuild --prune --write` for a repo
     "genuinely gone", the one state its own parenthetical rules out. Same bug
     printed in two places; same fix.
  3. `rebuild_refusal`'s ALL-unmeasured arm offered "re-run without --rebuild to
     refresh what CAN be measured" in the one state where nothing can be.
     MEASURED, same argv minus --rebuild: rc 0, `wrote 0 section row(s)`, store
     opened — the silent success the sentence above it gives as the reason to
     refuse.
  4. `handoff_search`'s NO MATCH told every reader to "widen --repo /
     --section", including the common unfiltered case where there is no filter
     to widen. `outcome.filtered` was already read two lines above.

SWEEP 2 — sentences written for the wrong run. `rebuild_plan_lines` is handed
`args.write` (the INTENT) and its branches describe an OUTCOME, and `main`
printed the block ABOVE every gate. MEASURED, both rc 4 with the store never
opened: `--rebuild --prune --write` with one handle unset printed "this run
opens the table, and the full bound scope is printed with the row count below"
(recorded SQL log `[]`); `--rebuild --write` all-unmeasured printed "is REPORTED
(see ORPHANED LABELS below) and kept" above a run that emitted no such section.
A third branch had the same falsity mode via a collision or zero-row derivation.

Fixed by ORDERING — the block now prints only after every gate passes — chosen
over threading a "this run will actually write" boolean, because that boolean IS
"no gate fired": a second spelling of the gate's own verdict, free to drift from
it (the argument `orphan_labels` already makes for taking the BOUND SCOPE rather
than an `args.prune` flag). Ordering also makes the two sentences that speak
about a DIFFERENT run sound, since the gates are mode-identical.

Two unpinned claims, both mutants the audit found SURVIVING a 200-test suite,
both now killed by their own specific test:

  * the guard ORDER `main` claims (`prune_config_refusal` before
    `rebuild_refusal`) — the existing test built the two states SEPARATELY, and
    order is unobservable in a state where only one guard can fire. The COMBINED
    state (a handle UNSET *and* a set handle pointing at a bad path) is now
    built and pinned. No safety consequence either way; which DIAGNOSIS the
    operator reads first is the stake.
  * every W7 fixture had exactly ONE unreadable doc, so `d.unreadable[:1]` is
    the identity and survived. Two repos with 3 and 4 unreadable docs — every
    number an assertion names pairwise distinct and distinct from 1 and from the
    repo count.

RECORDED, NOT FIXED (its own round): a repo that MEASURES and whose every doc is
UNREADABLE has its rows deleted and zero re-inserted at rc 0. MEASURED on two
repos, one healthy: `DELETE ['plimforthrepo','zarfrepo']`, two INSERTs both for
zarfrepo, rc 0. `RepoDerivation.unreadable` is the discriminator and sits one
function away. The note is in `rebuild_delete_labels`, in the tree, because a
gap that lives only in a review thread stops being read.

Prose fixes are pinned as WHOLE NORMALISED STRINGS (`_EXPECTED_ORPHAN_WARNING`,
new `_EXPECTED_PRUNE_CONFIG_REFUSAL`) — a cosmetic reword now fails the suite,
which is the intended cost. The retired-repo remedy also has a BEHAVIOURAL
proof, not just words: three runs over one fixture showing that only the config
change unblocks the prune, while both old remedies stay dead ends.

Red->green: 10 new/updated tests fail on pre-change library code at the same
base; 217 pass after (was 200). Mutation sweep: 12 mutants, all killed, with a
no-op negative control (survived) and a known-caught positive control (killed).

Claude-Session-Id: 90310ba1-6dc2-4779-a616-2ccfe89457d7
@ZacxDev

ZacxDev commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

Round 5 — per-finding disposition

Commit 918fc9aa, on the merged tree (origin/main = de677683; the round started against 2c6b2ac9 and re-merged after it moved — the one intervening commit was espanso-only and disjoint from every file here).

Both reported 🟡s turned out to be one shape each, so both were swept rather than point-fixed.

🟡-1 — a remedy sentence is a claim about the state it is printed in — FIXED (4 instances)

Swept every remedy/next-step sentence the two modules emit — 14 of them. 4 were wrong; the other 10 hold.

# site what was false
1 handoff_index.prune_config_refusal "Set every handle and re-run --prune from a host that has all the checkouts — or drop --prune". For a retired repo neither exists: nix/agent-handles.nix existence-guards every handle on its directory, so a deleted checkout is an unset handle on every host, and dropping --prune un-blocks the rebuild while leaving the exact rows the operator came to remove.
2 handoff_index.orphan_label_warning Recommended --rebuild --prune --write for a repo "genuinely gone" — the one state its own parenthetical rules out. Same bug printed in a second place.
3 handoff_index.rebuild_refusal, ALL-unmeasured arm "re-run without --rebuild to refresh what CAN be measured", in the one state where nothing can be. Measured, same argv minus --rebuild: rc 0, wrote 0 section row(s), store opened — the silent success the sentence directly above it gives as the reason to refuse.
4 handoff_search.render, no-match "widen --repo / --section" printed unconditionally, including the common unfiltered case where there is no filter to widen — and it reads as "your scope caused this zero" when the scope was the whole index. outcome.filtered was already being read two lines above.

The route that actually works for a retired repo — drop the handle from nix/agent-handles.nix and handoff_index.REPO_ENV_HANDLES, ship, then prune — was named nowhere in the tool. It is now named in both (1) and (2), and the test at test_every_handle_the_indexer_reads_is_exported_by_the_unit already records the inverse direction.

🟡-2 — the plan sentences were written for the wrong run — FIXED

rebuild_plan_lines is handed args.write (the intent) and its branches describe an outcome, while main printed the block above every gate. Reproduced both reports, each at rc 4 with the store never opened:

  • --rebuild --prune --write, 3 handles set / 1 unset → "this run opens the table, and the full bound scope is printed with the row count below", recorded SQL log [].
  • --rebuild --write, all repos unmeasured → "is REPORTED (see ORPHANED LABELS below) and kept", no such section ever emitted.

A third branch had the same falsity mode: --prune --dry-run's "The --write run prints the full bound scope", reachable via a collision or a zero-row derivation.

Chose ordering over a "will actually write" boolean, and the reason is the one this file already argues for orphan_labels: that boolean is "no gate fired", so it would be a second spelling of the gate's own verdict, free to drift from it. Ordering fixes all four branches at once — including the two that speak about a different run, which become sound by construction because the gates are mode-identical. rebuild_plan_lines now carries the call-site precondition in its docstring, and TestThePlanDescribesTheRunThatActuallyHappens tests it rather than asserting it.

Operator-visible consequence, documented in nix/home.nix and scripts/README.md: a refusing run — dry or write — now shows the refusal and no ## rebuild delete scope block.

🟢-1 — the guard order is now pinned — FIXED

The existing test built the two states separately, and order is unobservable where only one guard can fire. The combined state (a handle UNSET and a set handle pointing at a bad path) is now built. No safety consequence either way — both refuse at rc 4; the stake is which diagnosis the operator reads first.

🟢-2 — N>1 unreadable docs are now measured — FIXED

Every W7 fixture had exactly one unreadable doc, so d.unreadable[:1] is the identity. Two repos with 3 and 4 unreadable docs (7 total): every number an assertion names is pairwise distinct and distinct from 1 and from the repo count, so no mutant can survive by landing on a value the fixture could only produce. The N==1 boundary case is kept as a control.


Evidence

Red → green. 10 new/updated tests fail against the pre-change library at the same base and pass after (git show <base>:scripts/lib/handoff_{index,search}.py into an otherwise-identical tree): the 3 handoff_index prose pins + the 2 existing _EXPECTED_ORPHAN_WARNING consumers, the unfiltered no-match test, and all 4 refusing-plan tests. 217 passed after, was 200.

The rest are not regression tests and are not claimed as such: the guard-order and N>1 tests pin behaviour that was already correct — their evidence is the mutant, not a red baseline. Same for the behavioural retired-repo test and the two positive controls.

The two previously-surviving mutants, now dying — each with its own guard's failure, not a bystander's:

G1 guard-order swap        was: ** SURVIVED ** (200 passed)
                           now: killed — TestTheTwoPruneGuardsHaveAFIXEDOrder::
                                test_the_config_width_guard_wins_when_BOTH_could_fire
G2 d.unreadable[:1]        was: ** SURVIVED ** (200 passed)
                           now: killed — 3 tests in
                                TestTheUnreadableReportIsMeasuredForMoreThanOneDoc

Mutation sweep — fresh tree per mutant, .git removed from each copy, PYTHONDONTWRITEBYTECODE=1, -p no:cacheprovider, narrowest expression:

negative control (no-op)                     ** SURVIVED **   <- the harness can be silent
positive control (DEFAULT_BOOST -> 999)      killed           <- and it can go red
G1 guard-order swap                          killed
G2 d.unreadable[:1]                          killed
M1  plan printed BEFORE the gate             killed (4 tests)
M2  outcome.filtered -> True                 killed
M3  outcome.filtered -> False                killed
M4  derivations[:1] (repo dimension)         killed (3 tests)
M5  len(outcome.unreadable) -> literal 1     killed
M6  plan block deleted outright              killed (9 tests) <- absence-only assertions
                                                                cannot pass a deleted block
M7  old all-unmeasured remedy restored       killed
M8  prune refusal drops the retired route    killed
M9  orphan tail reverted                     killed
M10 non-prune rebuild also collects          killed            <- proves step 2 of the
                                                                 behavioural test is load-bearing

The harness refused two ambiguous patterns (pattern occurs 2 times, expected 1) rather than mutating the wrong site — noting it because a mutation harness that silently picks an occurrence is the failure mode.

Gate — merged tree, base de677683, both tiers, and the tiers named separately:

  • dev-host tier, scripts/gate.sh --tier both: GATE: RESULT=PASS exit=0PASS pytest exit=0 verdict='RESULT: PASS (exit=0)', PASS node exit=0 verdict='RESULT: PASS (exit=0)' (node: TOTAL suites=5 files=41 tests=1449 pass=1449 fail=0, floor 1367).
  • sandbox tier, built one at a time:
    • nix build .#checks.x86_64-linux.pytests -Ldevrc-pytests> RESULT: PASS (exit=0)
    • nix build .#checks.x86_64-linux.nodetests -Ldevrc-nodetests> RESULT: PASS (exit=0), real NIX_BUILD_EXIT=0 (unpiped, so the status is nix's own)

Sibling agents were running their own nix build for part of the pytests run. A green under contention stays trustworthy (a contended run fails loudly, it does not fake a pass); the nodetests run was clean of that anyway.


Still open — not fixed here

  1. 🟡-3 — a repo that MEASURES but whose every doc is UNREADABLE has its rows deleted and zero re-inserted, at rc 0. Left alone as filed, because the structural fix moves the delete scope and the rc-6/rc-7 split, so the two front ends have to move together. It is now recorded in the tree (rebuild_delete_labels' docstring), not only in a review thread, with the measurement: two repos, one healthy, one with every doc blob removed from .git/objectsDELETE ['plimforthrepo','zarfrepo'], two INSERTs both for zarfrepo, rc 0. RepoDerivation.unreadable is the discriminator and sits one function away. It needs a second healthy repo — alone, the zero-rows arm stops it, which is why the single-repo path looks safe.

  2. Nothing in this suite executes PostgresSectionStore against a database. Its DDL is never accepted by a server, its generated tsv column is never computed, ts_rank orders nothing, and the sandbox has no Postgres. What is pinned is the SQL text, the shared boost table, and every caller path above it. A green run here is not evidence that the indexed path works — the file's own module docstring says so, and it is still true after this round.

  3. The gate was run on the tree at de677683. main moves often here and strict is off by design, so a later merge is not covered by these numbers.

@ZacxDev
ZacxDev merged commit 45930d6 into main Sep 3, 2026
0 of 2 checks passed
@ZacxDev
ZacxDev deleted the feat/handoff-search-index-p1 branch September 3, 2026 16:37
ZacxDev added a commit that referenced this pull request Sep 3, 2026
…a code failure (#1244)

`tekton/devrc-pytests` fails on `test_cairn_write.py::TestAppendLands::
test_a_bullet_is_appended_and_the_status_is_named` on PRs whose diff cannot reach
it. The gate prints

    AssertionError: 🔴 cairn: the write did NOT happen — ... unreachable: timed out
    assert 7 == 0

which is a sentence about the write CLIENT describing what was a disk stall.

MECHANISM, read off the CI log rather than inferred by analogy. On `devrc-ci-jfg67`
the store root is `/tmp/nix-build-devrc-pytests.drv-0/…/popen-gw1/…` — the step
container's ephemeral layer — and the client reports `timed out`, not a refused
connection. `server.py:_replace_bytes` fsyncs the file and then the parent directory
INSIDE the request, before the response is written; `run_cairn` passes `--timeout 5`;
so one fsync slower than five seconds is a refusal at exit 7. That bound is TWELVE
TIMES tighter than the store-api half's `HANG_TIMEOUT` of 60.0, which is why this
file is the more frequent casualty of the same node contention.

🔴 The store-api root cause was NOT assumed to transfer. `test_cairn_write.py`
imports `http.server` and stands up its own loopback servers, imports no store-api
`server.py`, and carried no `MECHANISM` classifier — all verified before relying on
any of it.

WHAT THIS CHANGES: the message, and nothing else.

* `scripts/testlib/hang_mechanism.py` (new) — a `MECHANISM =` verdict from the live
  thread stacks, the shape `test_subsystem_store_api.py` already had. Under the
  reproduction the failure now reads `MECHANISM = SERVER_BLOCKED_IN_FSYNC (handler
  threads=2 [...=BLOCKED_ELSEWHERE ...=SERVER_BLOCKED_IN_FSYNC], accept loop
  parked=True)` plus the store's filesystem.
* 🔴 DIAGNOSIS, NOT TOLERANCE. No bound moved, nothing retries, no test was made to
  pass. A gate that reports a code failure for an I/O stall trains everyone to click
  through, and that was the whole cost.
* 🔴 The headline is deliberately NOT a consensus of the handler threads. Two servers
  are live here — the store and the shim in front of it — so on a timeout they
  legitimately disagree (shim in `urlopen`, store in `fsync`). Measured. A rule
  requiring agreement would answer AMBIGUOUS for the textbook case.
* 🔴 It scans frames' SOURCE LINES, never their FILENAME — the known, unfixed defect
  `_HUNG_SERVER_RULES` carries, where a worktree named `devrc-fsync` misclassifies
  every hang. `test_subsystem_store_api.py` was NOT rewired onto the shared module;
  it has its own tests and that is its own edit. Two copies today, known debt.
* `scripts/ci-repro/slow_cairn_fsync.py` (new) — stalls `os.fsync` in the test
  process. Not `LD_PRELOAD`: the server is in-process while the client is a
  subprocess, so preloading would stall both sides and muddy which timed out.

MEASURED on `origin/main` at 946a51f, store on tmpfs for BOTH runs (i.e. with
`store_siting`'s mitigation fully in force — this is a LATENCY dependency, not a
filesystem one):

  control (inert)                 4 passed in 3.69s, intercepted_fsyncs=8
  armed SLOW_CAIRN_FSYNC_S=8      4 failed in 28.95s, text identical to CI

`intercepted_fsyncs=8` is the positive control: the append path really does issue the
two `_replace_bytes` fsyncs per write. Both selftest arms watched to fire (inert-run
abort; zero-fsync abort on a `2 passed` selection).

Mutation matrix on the new classifier — every mutant killed, each by the right test:

  filename folded back into the scanned text  -> path test, with its own message
  `fsync` rule removed from RULES             -> 5 tests
  consensus rule (disagreement => no verdict) -> headline test
  classify always answers fsync               -> 3 tests, incl. the pull-apart
  outermost frame wins                        -> innermost test

NOT FIXED HERE, and neither is claimed: the stall itself is node-local device
contention, whose levers are infra and ranked in the ci-repro README. The disk-siting
half was already fixed by b4fde33 (#1219). ⚠ A PR branched before that commit still
carries the old disk-backed fixture — branch protection sets `strict: false` and
never rebases — which is why #1209 and #1233 were still failing.


Claude-Session-Id: 90310ba1-6dc2-4779-a616-2ccfe89457d7

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ZacxDev added a commit that referenced this pull request Sep 3, 2026
…s, an untracked doc rescued, an (#1264)

Claude-Session-Id: 90310ba1-6dc2-4779-a616-2ccfe89457d7
ZacxDev added a commit that referenced this pull request Sep 4, 2026
… a probe is about to be re-run (#1295)

`scripts/lib/handoff_search.py` shipped in #1209 and NOTHING called it:
`git grep handoff_search` found the module, its sibling writer, a README row and
a shebang test. An index with no reader is a cost with no benefit, and it fails
silently — every `/resume` went on re-deriving findings the corpus already held.

WHERE, AND WHY THIS SITE

`/resume` step 3, immediately beside the existing per-item pre-flight
(`git -C <repo> log --since=<doc-date>`), not step 4's `cairn recall` block.

  * The value is the `Ruled out:` bullets, and they only discriminate against a
    QUESTION. At orientation time you have a topic slug, which mostly retrieves
    the doc you are already reading. At step 3 you have the open item in hand,
    so the cross-doc hits are the ones that stop a re-run.
  * Step 4 is a different corpus (the subsystem-index store), not this one.
  * The adjacent command already carries exactly this rhetoric — "the cost is
    one command; the cost of skipping it is a whole session".

NOT the `/handoff` "Run this first" template: `test_handoff_skill_size.py`
leaves ~291 B under its enforced floor, so the line would have cost an eviction
of an instruction in the same commit — and a template only reaches docs written
AFTER it, while this tool's whole value is retrieval over the 379 docs that
already exist. NOT a wrapper script either: the CLI is already the right shape
and already renders the banner, the `indexed_docs=` scope line and the five
zero-causes; a wrapper would have to re-render or swallow them, and swallowing
is the failure the tool was built to prevent.

`--offline` deliberately: it answers from git refs with no database. The
Postgres path has never been executed by any test and the sync unit ships
disarmed (`enableHandoffIndexSync = false`), so wiring the consumer to it would
have made the step a connection error.

WHAT THE GUARD PINS (scripts/tests/test_resume_handoff_search_wiring.py)

Three claims, each DERIVED from the tool rather than restated:
  * the command as a WHOLE NORMALISED STRING — an edit dropping `--offline`
    goes red instead of silently starting to require a database;
  * the SEAM — every flag the skill prescribes is probed behaviourally against
    `handoff_search.main()`, so a rename in the tool breaks the test rather than
    leaving the skill prescribing a dead command;
  * the silent-zero contract — the non-answer exit codes are read out of
    `EXIT_CODES` / `SCOPE_REASON_EXIT_CODES`, so carving out a sixth zero goes
    red until `/resume` is taught about it.

Every contract assertion runs against a slice delimited by two pinned sentinels,
so a hit elsewhere in the 40 KB body cannot satisfy it.

RED -> GREEN, base d86b4e4: 3 failed / 3 passed before the skill edit;
6 passed after.

MUTATION SWEEP — fresh minimal repo mirror per mutant,
PYTHONDONTWRITEBYTECODE=1, 7 mutants, all killed by the SPECIFIC guard whose
claim each violates:
  M1 skill drops --offline                -> the shape pin
  M2 skill drops exit code 7              -> the derived ledger
  M3 skill drops POINTER TO VERIFY        -> the posture check
  M4 whole block deleted                  -> 3 guards, incl. the sentinel
  M5 tool renames --offline               -> the seam probe
  M6 tool adds a 6th non-answer code      -> the derived ledger
  M7 tool stops printing indexed_docs=    -> the posture check
Controls: an unmutated mirror and a whitespace-only skill edit both green.

The first build of that harness reported 8 kills off a RED no-op control — the
mirror omitted a transitive import (`handoff_doc`), so every "kill" was a fact
about the harness. Recorded because that is the whole reason the control exists.

BYTES: claude/skills/resume/SKILL.md 40,581 -> 41,852 (+1,271). That file has no
enforced ceiling (`git grep -l MIN_HEADROOM_BYTES scripts/`). ~40% of the delta
is the four-code non-answer ledger, which is the honesty half of the wiring: a
consumer that prints the hits and drops the distinction hands the reader "the
corpus is silent" when the truth was "the index is empty".


Claude-Session-Id: 90310ba1-6dc2-4779-a616-2ccfe89457d7

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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