coord: announce yourself to the other sessions, and stop the collision gate crying wolf - #133
Merged
wshallwshall merged 13 commits intoAug 2, 2026
Merged
Conversation
Every coordination control in this repo is PULL-based: a new session discovers its peers from the SessionStart banner and the peers learn nothing until someone trips the collision gate. That is too late for the collision that costs the most -- two sessions building the same THING in different files, where nothing file-shaped can catch it. This closes the push direction. It ASKS, it cannot send. Hooks are shell commands and session messaging is MCP, so the hook prints the instruction, the live peer roster and the id-resolution rule at the first prompt that has intent to report; the model does the sending. UserPromptSubmit, not SessionStart: at SessionStart a session knows it exists and nothing else, so it can only say hello -- the interrupt without the information. THE ID RULE IS THE PAYLOAD, and it is counter-intuitive enough that the text states it with its evidence. The registry id in this repo's banners is NOT the MCP session id; measured, a registry id and an MCP id for one session shared no characters. Branch does not join them either -- the two rosters reported different branches for the same checkout in 2 of 6 cases. Only cwd joins, and it must be matched EXACTLY: every worktree cwd is an extension of the primary's, so a prefix match resolves a peer in the primary to an arbitrary worktree session. A registry id passed to send_message fails SILENTLY, which reads as the peer ignoring you. EVERY DECISION LEAVES A RECEIPT, because the bug being fixed was a hook that was wired, fired, resolved nothing and exited 0 for weeks -- byte-identical to a healthy hook with no peers. For the same reason the shim carries its OWN missing-script notice: every receipt the hook writes lives INSIDE the script, strictly downstream of the resolution failure that IS the bug, so the shim is the one surface that still reports when the script does not resolve. It is gated on presence.ps1 so the entry stays silent in every unrelated repo on the machine. It always exits 0 -- a UserPromptSubmit hook that fails can block the user's prompt. It consumes presence.ps1 and therefore the single liveness fence; it does not invent a second notion of live. A separate 'mefor-announce' marker keeps it outside install-coordination's mefor-coord strip and outside the website repo's mefor-web-announce entry in the same settings file, so no installer can delete another's hook, and -Only UserPromptSubmit -Uninstall removes announce alone without disarming the collision gate.
Most tests for a hook like this assert an ABSENCE, and a hook that does nothing at all satisfies every one of them -- which is precisely the production failure being fixed. So the silence assertions are paired with a positive arm: two tests run the SAME runner against fixtures differing only in whether a peer exists, and if the silence tests ever start passing for the wrong reason the positive one goes red first. test_announce_wiring.py is the class the repo had no test for AT ALL: does the thing that gets INSTALLED reach a script that EXISTS, and does it say so when it does not? Its absence is exactly how a wired-but-inert shim survived for weeks. test_every_wired_script_exists_in_this_checkout was written FIRST and watched fail, naming the missing script and printing all three paths it scanned; a green gate is only evidence if it was shown it can see the failure. Also pinned, each because it was got wrong somewhere first: - The foreign UserPromptSubmit entries -- another repo's shim and an unmarked waiting-flag cleanup -- survive install AND uninstall byte-identical. That is the only thing standing between a one-line wiring edit and deleting a hook this repo does not own. - A peer with no StartedAt ranks LAST, not first. ConvertFrom-Json coerces ISO-8601 to DateTime while the '' fallback stays String; Sort-Object over that mixed column raises ZERO errors and puts the empty string FIRST, so without an explicit projected key the least-trustworthy row silently takes the top of a capped target list. - NO_SESSION_ID and DISABLED write their receipt with NO injected -StateDir. An earlier draft resolved the state dir after those branches, so the receipt was unwritable in production while a test that always injected one went green. - Self is excluded by BOTH nets independently: a roster that cannot tell you from a sibling makes the session message itself. - Hostile peer text cannot escape the peer-data block or emit a non-ASCII byte, a hostile session id cannot escape the state dir, and two ids that sanitise identically get two markers. - Two concurrent runs announce exactly once. session-context.ps1 is registered twice on this box today, so double firing is a live pattern, not a hypothetical.
…about .claude WORKTREES.md gains the "Announcing yourself" section that the hook's own emitted text and the shim's missing-script notice both cite by name, so the pointer has to land on main in the same merge. It states the id rule ONCE, as the source of record: registry id is not the MCP id, cwd is the only join key and must be matched exactly rather than by prefix, a usable id starts with local_, and a wrong one fails silently. It also states what the change does NOT do. There is no receive-side hook, so the rule that an announcement is peer DATA -- not an operator instruction, and not something to reply to -- lives in the prose and in the fixed message shape and nowhere else. Reachability is given honestly: presence.ps1 is authoritative for who EXISTS, list_sessions only for who can be MESSAGED, and measured, they disagreed 6-to-1. Cost is stated rather than left to be discovered. CORRECTION, and it is why this doc change is in scope rather than deferred: the same chapter claimed ".claude/settings.json is tracked (shared across worktrees)". It is not. /.claude/ is git-ignored, and git ls-files .claude/ returns nothing -- so a worktree's copy is a creation-time snapshot nothing refreshes and several siblings have none at all. That sentence sat at the exact point a reader decides where to install a hook, and it argues for the wrong answer; the new section directly contradicted it. SESSION-DRIFT-CONTROLS.md records announce as the only PUSH control in the D4 layer, plus the two new guarantees worth tracking separately: that wiring reaches a script that exists, and that a resolution failure is now reported by the shim.
…nd finished
Reported by another session with a repro: it committed a file, went clean, said
in writing it was done and handed the file over -- and the peer it handed off to
was still refused the edit.
overlap.ps1's `Files` is the UNION of what a branch COMMITTED-and-not-yet-landed
with what is dirty in its tree. The gate denied on any live row in that set, so
"this branch authored it" was treated as "someone is typing in it right now".
Those are different claims. The first stays true for the branch's whole life;
only the second is what the gate exists to detect.
It self-clears on merge -- overlap already intersects three-dot with two-dot so a
LANDED branch stops claiming its files. But nothing clears it before landing, and
with PRs currently unable to merge, "until it lands" is indefinite: the blocked
set grows monotonically and is never released. Two sessions that coordinated
correctly and explicitly still cannot hand a file over. That is precisely the
failure this gate's own docstring names -- a gate that cries wolf gets
uninstalled.
overlap.ps1 already told callers to treat its signals differently ("block on
live, mention dormant"), but no caller COULD: the row unioned the two signals
away. So the row now carries `Dirty`, and the single-file query sets
`MatchedDirty` saying which signal actually matched.
The gate now DENIES only on an uncommitted edit in a live worktree, and REPORTS
committed-and-clean as context instead -- the peer may already have done what you
are about to do, which is worth knowing and not worth refusing over.
Fails SAFE across the upgrade: a cached row predating `MatchedDirty` has no such
property and is treated as dirty, so the gate degrades to its previous
over-blocking rather than silently permitting a real collision.
Also, while in the file: `git status` now runs with --no-optional-locks. A plain
status REWRITES the index of the repo it inspects, and this walks every peer
worktree -- so merely asking "what is in flight" was mutating other sessions'
checkouts.
Verified against the live repro and both directions: the reported file now
allows with context; a file with uncommitted changes in a live worktree still
denies; an untouched file stays silent.
…rktree name Reported by the session it happened to: its worktree is named inter-session-communication-*, auto-generated at creation from a task that session has never worked on -- it has been doing ASVS scorecard work for its entire life. The directory name is the most visible identifier in presence.ps1, overlap.ps1 and this hook's output, and it had already misled TWO sessions (including this one) into guessing that session was building the announce hook. A worktree name is a creation-time label, not a statement of current work, and nothing keeps the two in sync. The claim note is the only field written DELIBERATELY to say what a session is doing, so the roster now prints it, and the legend tells the reader to prefer it over the name. Joined on the claim's `worktree` path, normalised the same way as every other cwd key here. Fail-open throughout: no claims directory, an unreadable claim, or a peer with no claim all just mean the name is the only thing we have -- which is exactly the status quo, never an error. Same session also flagged that the branch I read for it from list_sessions was stale (a spent, merged branch). The announce text already refuses to join on branch and says why; this is a second, independent reason not to trust it.
A control that cannot distinguish 'ran and resolved' from 'ran and found nothing' is not installed, however it looks. The announce shim outlived every other silent-control defect found the same day BECAUSE it printed a status message -- which is more convincing than silence. The structural cause is the reusable part: every receipt that hook would have written lived inside the script the shim failed to find, so every check sat strictly downstream of the failure it existed to detect. Looking was not neglected, it was impossible. The question to ask of a new control is which surface still reports when the control itself fails to load. Formulation owed to a peer session that hit four instances of this class in one day and named it more sharply than I had.
…e hard way Announce-on-join introduces a session; it does not let an established one push an operational notice. That increment is deferred, and on 2026-08-01 six sessions rehearsed it by hand for four hours. Three constraints fell out, recorded so the next attempt does not rediscover them: - A broadcast needs an EXPIRY or a predicate the RECIPIENT can evaluate, never a promise from the sender. A merge freeze shipped with 'lift when #119 merges'; #119 died on an unrelated CI timeout, so five sessions held on a condition that could not arrive and a second round was needed to retract it. - 'Don't do X' is the wrong primitive when automation already has X armed. The freeze asked for restraint while six PRs had auto-merge ARMED and would have landed with nobody clicking anything. The right ask was an action: disarm. - Coordination a tool cannot read does not count. Two sessions agreed IN WRITING to hand over a file and the gate still refused, because the agreement was prose and the gate reads git. Field data from the sessions that lived it, not speculation.
Nothing drove overlap.ps1's row computation against a real repository, so the question "does MatchedDirty hold when a file is dirty AND committed at once" was unanswerable by the suite. Raised by the session that spent an evening in exactly that state. THAT CASE IS THE ONE THAT FAILS SILENT, which is why it gets a real fixture rather than a stub row. A peer with uncommitted edits in one region and landed work in another is a genuine collision. Had MatchedDirty been derived from the committed diff instead of the working tree it would read FALSE there, the gate would allow, and two sessions would write one file with nothing reported. The over-block this replaced was loud and annoying; that would be quiet and cost someone their work. Verified the tests can SEE it rather than assuming: sabotaged the row to publish an empty Dirty set -- the precise mis-implementation warned about -- and both MatchedDirty assertions went red; restored, all five green. A test written after the code, never observed failing, is a test of nothing. Also pins that overlap does not rewrite a peer worktree's git index, by comparing the index mtime across two queries. An observer must not perturb what it observes, and this one was doing so on every PreToolUse before f55d6c6. Stub rows would only have asserted that the plumbing carries a value someone else computed; the whole question here is what git actually reports.
…at exists Raised by the session that traced the shim: the coordination hooks are not installed copies, they are inline commands that locate their script in a working tree at every invocation. If neither base yields the file, Test-Path fails, the loop ends, nothing runs, and the tool call proceeds with no hook and no signal. "The hook is uninstalled" and "the hook ran and permitted this" are indistinguishable from outside, and nothing was watching. Not hypothetical: a foreign UserPromptSubmit entry sat in this same settings file for weeks probing a script that exists only in another repo. The risk composes badly for collision_gate.ps1 specifically, which now (a) fails OPEN on any error, (b) denies less by design after the dirty-vs-committed split, and (c) silently no-ops when unresolvable. Individually defensible; together the realistic bad day is "the gate was never running and nobody noticed". This closes (c) -- the observation is not mine, and it is a good one. Found immediately on writing it: FIVE user settings files across account directories, not the one I knew about. The informational test also prints the original defect as output rather than leaving it invisible: FOREIGN UserPromptSubmit [mefor-web-announce] -> scripts/hooks/announce.ps1: RESOLVES NOTHING HERE It is another repo's entry, so this reports it and does not touch it. Carries a NEGATIVE CONTROL, because the assertion passed on the first run and a green that has never been shown to fail is not evidence. The real hooks cannot be unwired to prove the predicate works -- the primary checkout is shared with live sessions -- so it is exercised against a path known not to exist. Local-machine only: CI has no user settings and these skip there, which means CI does NOT guard this property. Said plainly, and every test prints what it scanned BEFORE it can skip, per test_gate_installed_parity.py -- the pytest config has no -rs, so a skip would otherwise render as a bare dot with no reason.
…ommunication-hooks-a52335
wshallwshall
enabled auto-merge (squash)
August 2, 2026 00:29
…ommunication-hooks-a52335
…ommunication-hooks-a52335
…ommunication-hooks-a52335
wshallwshall
added a commit
that referenced
this pull request
Aug 2, 2026
…strument it used Two additions to #329, neither mine originally. THE FRAMING, from the ADR 0156 ASVS-sweep session. I had filed #329 as five leaks to plug. It is better than that: while the five remain, "no unclamped escape survives on an enforcing PHI posture" is five per-site facts, each checkable only by opening the site, and each silently falsified by a sixth cell added later. Convert them all and it collapses into ONE repo-wide invariant -- the raw insecure_tls_allowed() unreachable outside settings.py's own clamp, so the absence is checkable everywhere at once with weakened_tls_escape_permitted_here as the positive control. Today a convention enforced by review; afterwards an invariant enforced by a grep. That is not decoration. The scorecard's absence-claim mechanism runs regexes over the whole *.py corpus and CANNOT scope a grep to one file, so a per-connector claim is not expressible and has to ride as stated-but-unchecked prose. A repo-wide claim is machine-verified on every commit. The item is therefore the difference between a property re-audited by hand and one a gate can hold -- a stronger argument than "five leaks". THE CENSUS, corrected twice before it was right, which is why it now names its instrument. I reported direct.py=0 (measuring my own unlanded branch as though it were repo state) and mllp.py=1 (a regex excluding '#' comments but NOT docstrings, counting prose as a call). Both wrong. Recounted at main by ast.Call nodes: six real sites outside settings.py -- auth/ldap.py, pipeline/alert_sinks.py, transports/{ai_broker,database,direct,remotefile}.py. database.py is the documented unstamped fallback and stays excluded; mllp.py's hit is a docstring and is not a call at all. The scope note states that a census on the #323 branch disagrees with one on main and neither is wrong, and ends on the line that is the actually durable part: a line-based census reports mllp.py as a further site, an AST-based one does not. That tells the next person which instrument to use, which no count on its own can. Gate advisory honoured rather than bypassed: #133 changed collision_gate from a hard deny to an advisory for a peer whose tree is clean, and its message says to check the overlapping commits before editing. Did that -- adr-0154's hunks are at 398/881, the sandbox session's is an EOF append at 8308, mine are 5261/7397/8178/7772. Disjoint. (My own check of that gate was wrong first time, in the same class as everything above: I tested "is there output?" as a proxy for "was it denied?", and #133 changed the output from a deny decision to an advisory. The instrument was written against the old contract.) banner invariant OK (264 items); leak gate exit 0 under the real token set.
wshallwshall
added a commit
that referenced
this pull request
Aug 2, 2026
…strument it used Two additions to #329, neither mine originally. THE FRAMING, from the ADR 0156 ASVS-sweep session. I had filed #329 as five leaks to plug. It is better than that: while the five remain, "no unclamped escape survives on an enforcing PHI posture" is five per-site facts, each checkable only by opening the site, and each silently falsified by a sixth cell added later. Convert them all and it collapses into ONE repo-wide invariant -- the raw insecure_tls_allowed() unreachable outside settings.py's own clamp, so the absence is checkable everywhere at once with weakened_tls_escape_permitted_here as the positive control. Today a convention enforced by review; afterwards an invariant enforced by a grep. That is not decoration. The scorecard's absence-claim mechanism runs regexes over the whole *.py corpus and CANNOT scope a grep to one file, so a per-connector claim is not expressible and has to ride as stated-but-unchecked prose. A repo-wide claim is machine-verified on every commit. The item is therefore the difference between a property re-audited by hand and one a gate can hold -- a stronger argument than "five leaks". THE CENSUS, corrected twice before it was right, which is why it now names its instrument. I reported direct.py=0 (measuring my own unlanded branch as though it were repo state) and mllp.py=1 (a regex excluding '#' comments but NOT docstrings, counting prose as a call). Both wrong. Recounted at main by ast.Call nodes: six real sites outside settings.py -- auth/ldap.py, pipeline/alert_sinks.py, transports/{ai_broker,database,direct,remotefile}.py. database.py is the documented unstamped fallback and stays excluded; mllp.py's hit is a docstring and is not a call at all. The scope note states that a census on the #323 branch disagrees with one on main and neither is wrong, and ends on the line that is the actually durable part: a line-based census reports mllp.py as a further site, an AST-based one does not. That tells the next person which instrument to use, which no count on its own can. Gate advisory honoured rather than bypassed: #133 changed collision_gate from a hard deny to an advisory for a peer whose tree is clean, and its message says to check the overlapping commits before editing. Did that -- adr-0154's hunks are at 398/881, the sandbox session's is an EOF append at 8308, mine are 5261/7397/8178/7772. Disjoint. (My own check of that gate was wrong first time, in the same class as everything above: I tested "is there output?" as a proxy for "was it denied?", and #133 changed the output from a deny decision to an advisory. The instrument was written against the old contract.) banner invariant OK (264 items); leak gate exit 0 under the real token set.
wshallwshall
added a commit
that referenced
this pull request
Aug 2, 2026
…icated (#323, layers 1-2) (#132) * fix(smtp): the EMAIL and DIRECT TLS hops were encrypted but unauthenticated (#323, layers 1-2) smtplib takes no context by default and falls back to ssl._create_stdlib_context, which IS ssl._create_unverified_context -- measured on this project's required interpreter (CPython 3.14.6): verify_mode=CERT_NONE, check_hostname=False. So use_tls=true bought encryption without authentication on every SMTP send, and any certificate was accepted. That is worse than a plain gap because three shipped controls asserted the opposite: * transports/email.py registered a RevocationHopGuard on the hop, whose own definition in tls_policy.py says "the caller has already built a verifying context". An enforcing production-PHI instance therefore REFUSED TO START over a possibly-REVOKED certificate, on a hop that never validated a certificate at all. * the same file's comment claimed STARTTLS/SMTP_SSL "verifies the server cert". * the AUTH refusal keyed only on use_tls=false, so with TLS "on" the password went over the unauthenticated hop. WHAT LANDS (2 of the 3 cells): config/tls_policy.py build_smtp_tls_context() -- the shared verifying-context factory, mirroring remotefile.py's _ftps_ssl_context step for step (TLS 1.2 floor, harden_kex_groups, harden_cipher_suites, harden_verify_flags on the verify path). It lives in config/ rather than transports/ because pipeline/alert_sinks.py is the third caller and a transport must not import pipeline/ (ADR 0029's one-way rule). transports/email.py, transports/direct.py a three-arm branch (cleartext / verify-off / verifying) and context= on both smtplib arms. The verify-off arm refuses unless the CLAMPED weakened_tls_escape_permitted_here() allows it, and refuses AUTH outright. config/wiring.py tls_verify / tls_ca_file / tls_check_hostname on Email() and Direct(). Trust config, not verification-off, is the escape: [tls].internal_ca_file is ALREADY threaded onto every Destination and was simply never read here, so an estate that pinned its internal CA for MLLP/FTPS needs no change at all. SEPARABLE FIX, called out rather than folded in silently: direct.py's cleartext arm read the UNCLAMPED insecure_tls_allowed() while its sibling one branch away read the clamped form. It now reads the clamped one -- strictly ADDS refusals (ADR 0092 decision 5). Partially closes #329. VERIFICATION -- the part that matters. The pre-existing tests asserted "STARTTLS was issued", which was true the whole time it was insecure; that assertion could never have caught this. The eight new tests assert the CONTEXT (CERT_REQUIRED, check_hostname, TLS1.2 floor, CERT_NONE only under the escape, the clamp under enforcing PHI, and that a per-connection CA pins to ONLY that CA). Negative control run: with the code change stashed and the tests kept, all eight go RED. ruff + format clean; mypy unchanged at its 21-error pre-existing baseline (missing pynetdicom / webauthn extras, none in touched files); 437 targeted tests green. DELIBERATELY NOT DONE -- the alerts cell (pipeline/alert_sinks.py:384) still calls starttls() bare. It needs an acknowledgment switch rather than the clamp, because the contextvar hop posture is never stamped for that cell. Tracked as the residual on #323. #139's "verifying context by design" claim therefore remains FALSE and is not corrected here. BLOCKED, needs one follow-up commit: adding `ssl` to transports/{email,direct}.py reds the required crypto-inventory gate until scripts/security/crypto_inventory_check.py documents it. That file is checked out live in another session; the collision gate refused the edit and I asked that session for the two lines rather than clobbering their work. docs/BACKLOG.md (#323's banner, #139) is held by two other sessions for the same reason. * docs+gate(smtp): document the ssl usage #323 added, and correct two false premises it exposed Completes PR #132's blocked tail. Four edits in three files the collision gate refused because live sibling sessions carry diffs to them; applied outside the Edit tool with explicit written consent from both holders, quoted below. 1. scripts/security/crypto_inventory_check.py -- record `ssl` for transports/email.py and transports/direct.py. Without this the REQUIRED crypto-inventory context is red. The Sandbox Fixes session held this file and I offered to let them add the entries in their PR. Their answer was better than my question: find_violations() checks BOTH directions (undocumented AND stale, :378-399, verified at HEAD), and on their branch these two files contain zero ssl imports -- so documenting the usage there would have traded my `undocumented` failure for their `stale` failure on the same required context. Usage and its documentation must move in the SAME commit. That is the invariant, and it is why these lines belong here. 2. docs/ASVS-L2-PHASE0-CHANGES.md section 5 -- the EMAIL and DIRECT communications-inventory rows said "STARTTLS on by default" and stopped, which now understates the control. Both state verification, its trust anchors, and that tls_verify=false needs the clamped escape. The crypto_inventory_check.py header requires these kept in sync. 3. docs/BACKLOG.md #139 -- CORRECTS A FALSE COMPENSATING-CONTROL PREMISE. The item asserted "The engine's EmailAlertSink uses STARTTLS with a verifying context by design." It does not, and did not: starttls() with no context falls back to ssl._create_stdlib_context, which IS _create_unverified_context. A reader would have concluded alert email was TLS-verified when it was not -- the exact shape CLAUDE.md section 11 names as worst. It stays false AFTER #132: I fixed the two connectors, NOT the alert sink, and the item now says so rather than leaving the residual implied. 4. docs/BACKLOG.md #337 -- rationale amended, severity unchanged at LOW. Flagged by the ADR 0087 sandbox session and verified here at HEAD: DEFAULT_FORBIDDEN_MODULES (pipeline/sandbox.py:84-95) blocks socket/ssl/asyncio/multiprocessing/the I/O-bearing messagefoundry.* subpackages/ cryptography -- but NOT `os` or `subprocess`. So #337's justification, "the author already has in-process execution", is true at the default mode=off and FALSE under mode=subprocess, where the whole premise is that the author is not trusted with it. The number lands right for a different reason; the amended rationale holds in both postures and says to re-score when ADR 0147 (OS confinement, Proposed with no code) lands. Same defect class as #139: a claim stated independently of the configuration that makes it true. 5. docs/BACKLOG.md #323 -- banner to PARTIALLY SHIPPED (2 of 3 cells), with the alerts-cell residual, the direct.py clamp fix, and a correction to this item's own "Migration risk" framing (it presumed deployments; the owner confirmed there are none). CONSENT RECORDED, quoted verbatim. Sandbox Fixes (holds crypto_inventory_check.py): "So: take the file, it's yours. My change to it is committed, final, and a single entry (pipeline/sandbox.py -> {secrets}). I will not touch it again -- commitment, not estimate." Stuck CIs (holds docs/BACKLOG.md): "I have no further BACKLOG.md edits; my #340/#344 are committed and pushed on #131; your hunks at ~5264 (#139) and ~7398 (#323) are disjoint from my EOF appends after #338." WHY A BYPASS RATHER THAN WAITING -- AND WHY THIS IS NOT A PRECEDENT. The block was real: both holders' branches carry genuinely UNMERGED diffs to these files, so the gate was correct to fire. Waiting was viable -- their PRs merging would have cleared it -- and I chose consent-plus-verified- disjointness instead, because the gate keys on branch diffs and has no way to read a consent both holders had already given in writing. That is the actual limitation, and docs/WORKTREES.md states the rule from the other side: "coordination a tool cannot read does not count." READ THAT AS A CASE-BY-CASE CALL, NOT A GENERAL RULE. "The gate over-blocks in this specific way" and "therefore overriding it is warranted" are two separate claims; only the first is established, and the sessions that documented the over-blocking did not draw the second conclusion. The ADR 0087 sandbox session had the same clearance from both holders, verified disjointness, and knowledge that the pending fix would allow its edit -- and still WAITED, because its case was one stale sentence in its own item. Mine was a blocked REQUIRED CI context with the fix already written, which is a different weight of reason, not a stronger entitlement. The real remedy is f55d6c6 ("stop the collision gate blocking files a peer committed and finished"), which is written but NOT yet on main; until it lands, sessions are choosing individually whether to wait or override with disclosure. Two of us overrode and disclosed, one waited. All three are defensible. None is the rule. CORRECTION -- an earlier draft of this message justified the bypass with a claimed defect: that under squash merges a merged branch keeps reporting a three-dot diff forever, so a merged-and- forgotten worktree blocks its files permanently. THAT IS FALSE and the claim is withdrawn. The announce session refuted it, the Stuck CIs session retracted it, and I measured it here rather than take either on trust: MessageFoundry-prunefix (merged via #74, branch deleted, worktree still checked out) git diff --name-only origin/main...HEAD -> 7 files git diff --name-only origin/main..HEAD -> 9 files intersection -> 0 overlap.ps1 -File docs/SESSION-DRIFT-CONTROLS.md -Json -> does NOT name prunefix overlap.ps1 intersects the two diff forms deliberately (:138-155, with the reasoning in its own comment), and collision_gate.ps1 delegates to it (:70) rather than re-implementing the rule -- so the gate inherits that handling. `git diff A..B` compares TREES, not commit lists, so once a branch's content is in main the two-dot set empties and the intersection self-clears. Squash merges were already handled. The block set does not only grow. Recording the withdrawal rather than quietly dropping it, because a bypass justified by a real limitation is a decision, while one justified by a defect that does not exist is a hole -- and a false mechanism in the ledger would be cited as precedent. Three sessions got the two-dot/three-dot distinction wrong in different directions tonight, on a repo where the answer decides whether a guard fires; that is the durable lesson, and it is being routed to ADR 0157. Verification: backlog_status_check OK (262 items, each exactly one status) -- the invariant that guards precisely this banner edit; crypto-inventory gate clean; the three previously-failing tests (test_crypto_inventory_scanner, test_security_static x2) now pass; 79 green across the affected suites; ruff + format clean. * test(smtp): prove the #323 context REFUSES a bad certificate, not just that it is configured to The tests shipped with the fix assert `ctx.verify_mode is CERT_REQUIRED` and `ctx.check_hostname is True` -- ATTRIBUTES. That is a weaker claim than "it refuses an untrusted peer", and the gap matters here more than usual: the defect being fixed was a context whose attributes nobody had ever inspected. Asserting the attributes proves the code sets them; it does not prove the resulting handshake behaves. So these drive a REAL TLS handshake. A module-scoped fixture mints a self-signed `localhost` cert and runs a local TLS listener on 127.0.0.1 (ephemeral port, daemon threads). It speaks no SMTP by design -- the property under test is the TLS layer, and adding a protocol would only add ways for the test to fail for reasons unrelated to what it asserts. Five arms, measured: verify=True, no CA -> REFUSED (self-signed certificate) <- the fix, observed verify=True, ca_file=<CA> -> handshake OK <- the private-CA route works verify=True, wrong hostname -> REFUSED (hostname mismatch) check_hostname=False -> handshake OK, chain still validated verify=False (the escape) -> handshake OK, warning logged NEGATIVE CONTROL, run before committing: the same two refusal cases were replayed against `ssl._create_stdlib_context()` -- EXACTLY what smtplib used before #323 -- and both returned **ok**. So both tests genuinely fail against the pre-fix code path and are load-bearing rather than tautological. Without that check they would have been indistinguishable from tests that pass because the assertion is trivially true, which is the failure mode this suite already documents elsewhere ("a test that cannot fail is not a check"). The verify=False arm is asserted deliberately too: an escape that silently stopped connecting would leave operators unable to tell a policy refusal from a broken escape. ruff + format clean; 74 tests in this file, 132 across the three affected suites. * docs(smtp): stop #323 creating false statements in the other direction A fix that closes a defect can make previously-true prose false, and can make a previously-safe grep misleading. Two such cases, both raised by peer sessions rather than found by me. 1. docs/PHI.md:916 -- the [alerts] SMTP row. STILL ACCURATE (that cell is the deferred residual and genuinely does call starttls() with no context), but a reader could reasonably generalise "the SMTP hop is encrypted but unauthenticated" to the message connectors, which as of #323 is FALSE for both EMAIL and DIRECT. The row now says explicitly: do not generalise this to the connectors, they verify; this cell is the deferred residual, not an oversight, and not evidence that SMTP is unverified engine-wide. Raised by the ASVS session, who is sweeping these cells. 2. transports/direct.py -- a FALSE ABSENCE trap. Replacing the raw insecure_tls_allowed() with the clamped weakened_tls_escape_permitted_here() removed this file's last CALL to the raw escape, so a future assessor grepping for it here finds no call site and could conclude the connector has no escape. It has one; it is clamped. The comment now states that, and scopes the absence claim to this file rather than the repo. I got that comment wrong on the first attempt in an instructive way: I wrote "grepping this file returns zero hits" and the grep returned three -- my own comment, twice. I had asserted the result of a measurement while writing the thing that changed it. Corrected to the true and narrower claim (no CALL remains; the comments mention it), and every file named as still having a live call was verified by grep rather than recalled: auth/ldap.py 1 | pipeline/alert_sinks.py 1 | transports/ai_broker.py 1 transports/database.py 1 | transports/mllp.py 1 | config/settings.py 4 transports/direct.py 0 | transports/email.py 0 That is the same defect this whole change set has been about -- a claim stated independently of the measurement that would make it true -- committed inside the comment written to prevent it. Left in the record rather than quietly fixed, because the near-miss is the useful part: the comment would have read as authoritative and been wrong within one line of itself. ruff + format clean; 132 tests green across the affected suites. * backlog(#329): the invariant framing, and a census that says which instrument it used Two additions to #329, neither mine originally. THE FRAMING, from the ADR 0156 ASVS-sweep session. I had filed #329 as five leaks to plug. It is better than that: while the five remain, "no unclamped escape survives on an enforcing PHI posture" is five per-site facts, each checkable only by opening the site, and each silently falsified by a sixth cell added later. Convert them all and it collapses into ONE repo-wide invariant -- the raw insecure_tls_allowed() unreachable outside settings.py's own clamp, so the absence is checkable everywhere at once with weakened_tls_escape_permitted_here as the positive control. Today a convention enforced by review; afterwards an invariant enforced by a grep. That is not decoration. The scorecard's absence-claim mechanism runs regexes over the whole *.py corpus and CANNOT scope a grep to one file, so a per-connector claim is not expressible and has to ride as stated-but-unchecked prose. A repo-wide claim is machine-verified on every commit. The item is therefore the difference between a property re-audited by hand and one a gate can hold -- a stronger argument than "five leaks". THE CENSUS, corrected twice before it was right, which is why it now names its instrument. I reported direct.py=0 (measuring my own unlanded branch as though it were repo state) and mllp.py=1 (a regex excluding '#' comments but NOT docstrings, counting prose as a call). Both wrong. Recounted at main by ast.Call nodes: six real sites outside settings.py -- auth/ldap.py, pipeline/alert_sinks.py, transports/{ai_broker,database,direct,remotefile}.py. database.py is the documented unstamped fallback and stays excluded; mllp.py's hit is a docstring and is not a call at all. The scope note states that a census on the #323 branch disagrees with one on main and neither is wrong, and ends on the line that is the actually durable part: a line-based census reports mllp.py as a further site, an AST-based one does not. That tells the next person which instrument to use, which no count on its own can. Gate advisory honoured rather than bypassed: #133 changed collision_gate from a hard deny to an advisory for a peer whose tree is clean, and its message says to check the overlapping commits before editing. Did that -- adr-0154's hunks are at 398/881, the sandbox session's is an EOF append at 8308, mine are 5261/7397/8178/7772. Disjoint. (My own check of that gate was wrong first time, in the same class as everything above: I tested "is there output?" as a proxy for "was it denied?", and #133 changed the output from a deny decision to an advisory. The instrument was written against the old contract.) banner invariant OK (264 items); leak gate exit 0 under the real token set.
wshallwshall
added a commit
that referenced
this pull request
Aug 2, 2026
Found while checking a peer session's report, not by looking for it. That session announced itself by hand on 2026-08-02 and gave the reason as "the hook is on an unmerged branch". It had merged (#133, 3389aa2) hours earlier, so the observation was right and the diagnosis was not, and nothing would have corrected it. Measured across all five config roots: - no `mefor-announce` UserPromptSubmit entry anywhere - the one UserPromptSubmit entry installed is `# mefor-web-announce`, which resolves scripts/hooks/announce.ps1 -- a different script in a different repo, and one the installer's own comment already warns is easy to confuse with this marker - <git-common-dir>/mefor-coord/announce/ does not exist, so there is not a single receipt: it has never executed install-coordination.ps1 was last run before the announce row existed, and merging a hook does not install one. Its two other entries -- the SessionStart banner and the collision gate -- were wired then and are present, which is precisely why nothing looked wrong. The part worth carrying: the missing-script notice was built so this class could not hide, and it CANNOT FIRE when the hook is not wired at all, because it lives inside the shim. Same shape as the defect this document already records one level down -- the detector sat downstream of the failure it existed to detect. So the status table now distinguishes rule 4's inert-BY-DESIGN from this one's inert-BY-ACCIDENT, and the confirmation step is a receipt on disk rather than a reading of the settings file. Not installed here: that writes ~/.claude/settings.json, which is shared with every session on this machine. Owner's call, from a plain terminal.
wshallwshall
added a commit
that referenced
this pull request
Aug 2, 2026
#140) * fix(coord): overlap gave two different answers the same bytes, twice Two defects in one script, and they are the same defect: a signal that cannot distinguish the state it reports from a different state. 1. A -Json query answered "nobody else is in this file" by printing NOTHING. `@() | ConvertTo-Json -AsArray` sends zero objects down the pipeline, so ConvertTo-Json never runs -- -AsArray only shapes output that already exists. On stdout an all-clear was therefore byte-for-byte identical to the script dying before it answered, and no consumer could tell them apart. Every -Json exit now goes through one emitter that always produces an array. (-InputObject is not the fix: with -AsArray it double-wraps to [[]].) Found by running the real script against the real collision gate rather than the test stubs, which had been written to a shape the real script never produced. 2. A live session was attributed to a worktree by FIRST prefix hit. Linked worktrees live under the primary checkout, so every linked path is also a prefix match for the primary's row: the primary was handed whichever nested session the hash table enumerated first, and reported LIVE on main, "building" a peer's task list. Hash order is not stable, so it was a different wrong answer each run -- which is why it read as noise rather than as a bug. Longest prefix wins is the only rule that survives nesting, and it is resolved once against every worktree instead of per row. docs/WORKTREES.md already named this exact trap for the announce hook's id rule, where the cure was "never match by prefix". Here a prefix match is genuinely required -- a session may sit in any subdirectory -- so the cure has to be longest-prefix instead. Both are pinned against a real nested-worktree git fixture; a sibling layout would pass under the old rule and prove nothing. Each new assertion was checked against the unfixed script first: the attribution test reports the primary as Live/main/<peer session id>, and the array test sees ''. * fix(coord): the collision gate reported an all-clear when it had checked nothing Every fail-open path in this hook -- overlap script missing, throwing, or printing garbage -- exited 0 with EMPTY STDOUT. On a PreToolUse hook whose stdout is parsed as a decision, empty stdout means "allow", which is byte-for-byte what "checked, nobody else is in this file" looks like. So a gate that had consulted nothing was indistinguishable from a gate reporting all-clear, and its own failure reached the session as reassurance. That is the silent-control class this repo has now hit five times, and it is the same shape as the wired-but-inert announce shim: the surface that was supposed to report sat downstream of the failure it existed to detect. The posture does not change -- every one of these paths still ALLOWS. Only the silence does. It now emits a hookSpecificOutput.additionalContext notice naming which reason (overlap-missing / overlap-failed / overlap-empty / overlap-unparseable / payload-unreadable). It must be that JSON shape and never a bare line: this hook's stdout is a decision, so a stray line risks a misparse on every Edit and Write -- a diagnostic that would be a worse fault than the one it reports. There is deliberately no permissionDecision key: a notice that blocked would invert the fail-open posture that is the whole point of this gate. Rate-limited per reason (30 min, -NoticeCooldownMinutes) so a persistently broken overlap cannot narrate itself into every edit -- this gate's own docstring records where a gate that cries wolf ends up. The stamp lives under -StateDir, defaulting to the repo's coordination dir and resolved ONLY when about to report, so nothing new runs on the hot path. If the stamp cannot be read or written the notice is emitted anyway: the failure mode of a noise-suppressor must be noise, never quiet, or an unwritable directory silently restores exactly the behaviour this removes. Distinguishing overlap-empty from a resolved "nobody" required fixing the producer first (previous commit) -- you cannot detect a difference the producer never encoded. Verified against the real overlap script, not only the stubs: an ordinary edit to an untouched file is silent. Tests: -StateDir isolates the throttle per test, or the first notice would silence the next test's and the suite would pass on run order. * fix(coord): claim.ps1 accepted a new note, reported success, and discarded it -Take documented itself as idempotent -- "re-taking your own claim just refreshes the note" -- and did not refresh anything. A new -Note was taken, acknowledged and dropped. That is worse than an outright failure, because of what the note is for. It is the only field written deliberately to say what a session is doing, and announce-session.ps1 broadcasts it to every session joining the repo while telling them to prefer it over the worktree name. So the one field elevated to authoritative was the one field that could not be corrected. Measured 2026-08-02: a claim note was still announcing "NO PR OPENED -- honouring the #119 merge freeze" to every joining session hours after both that PR and the one it gated had merged. The workaround people reached for -- -Release then -Take -- drops the claim in between, re-opening the race the claim exists to close. Re-taking a key you hold now rewrites the file in place: note, branch (a worktree can have switched branches, and a claim naming a branch nobody is on is another confidently-wrong coordination fact) and a new `refreshed` stamp, leaving `claimed` untouched -- which is what proves the claim was never let go. Write-then-rename, not a truncating write: claim_check.py swallows a JSON parse error into "not claimed", so a torn file is a silently disabled gate, and a crash mid-refresh must leave the old note. Mutual exclusion is unchanged and pinned: a peer's key is still refused. One trap found by the test rather than by reading. ConvertFrom-Json silently coerces an ISO-8601 string to [datetime], so [string]$c.claimed returns the local short form -- sub-second precision and UTC offset gone. Writing that back would have downgraded the stamp on every refresh, and it would still have parsed, so nothing would ever have complained. Stamps now round-trip through "o", and the test asserts byte equality rather than "still parses". The same coercion is handled where announce reads it, with an invariant-culture parse for the string case. announce-session.ps1 now prints each claim note's AGE (from `refreshed` else `claimed`, "age unknown" when it cannot be determined -- an unknown age must not render as a fresh one). Elevating a note to authoritative makes a stale one strictly more dangerous than none, and age is the cheap signal that lets a reader discount it. Not taken here: claim -List's staleness-vs-liveness rendering, which is already open as its own change. * docs(coord): record the three fixes, and correct a claim that has expired SESSION-DRIFT-CONTROLS.md: a fifth instance of the silent-control class, in the collision gate itself, added to the callout that names the class. It carries the part worth reusing -- the fix was not "check harder", it was giving two states different bytes, and the first attempt failed because the PRODUCER had never encoded the difference. Status-table rows for the three controls, and the claim-refresh behaviour beside claim.ps1's entry. WORKTREES.md: the announce id rule already warned that a prefix match resolves a peer in the primary to an arbitrary worktree session, because every worktree cwd extends the primary's. overlap.ps1 had that same trap live at the same time. Noted there, with the distinction that matters: overlap genuinely needs a prefix match, so the cure is longest-prefix rather than exact-match. And a correction. The broadcast-constraints list said of last week's merge freeze that "#119 never merged (it died on an unrelated CI timeout)". It merged the following day, 2026-08-02 01:45Z. Verified against the API rather than restated. The lesson is unchanged and in fact sharper: the recipients could not evaluate the predicate, so the freeze outlived its own condition in both directions -- five sessions held while it had not arrived, and a claim note was still announcing it hours after it had. * docs(coord): announce-on-join merged and was never installed Found while checking a peer session's report, not by looking for it. That session announced itself by hand on 2026-08-02 and gave the reason as "the hook is on an unmerged branch". It had merged (#133, 3389aa2) hours earlier, so the observation was right and the diagnosis was not, and nothing would have corrected it. Measured across all five config roots: - no `mefor-announce` UserPromptSubmit entry anywhere - the one UserPromptSubmit entry installed is `# mefor-web-announce`, which resolves scripts/hooks/announce.ps1 -- a different script in a different repo, and one the installer's own comment already warns is easy to confuse with this marker - <git-common-dir>/mefor-coord/announce/ does not exist, so there is not a single receipt: it has never executed install-coordination.ps1 was last run before the announce row existed, and merging a hook does not install one. Its two other entries -- the SessionStart banner and the collision gate -- were wired then and are present, which is precisely why nothing looked wrong. The part worth carrying: the missing-script notice was built so this class could not hide, and it CANNOT FIRE when the hook is not wired at all, because it lives inside the shim. Same shape as the defect this document already records one level down -- the detector sat downstream of the failure it existed to detect. So the status table now distinguishes rule 4's inert-BY-DESIGN from this one's inert-BY-ACCIDENT, and the confirmation step is a receipt on disk rather than a reading of the settings file. Not installed here: that writes ~/.claude/settings.json, which is shared with every session on this machine. Owner's call, from a plain terminal. * fix(coord): five defects this PR's own first pass introduced or left Found by an adversarial review of the preceding commits, then each one reproduced by execution before being touched. Two were regressions I had introduced; three were gaps. 1. THE CLAIM FILE'S EXISTENCE IS THE LOCK, and the refresh unlinked it. `Move-Item -Force` is delete-then-rename. The take path is an exclusive CreateNew, so any instant the name does not exist is an instant another worktree can claim a key we hold -- i.e. the note refresh could hand a claim away. Measured on this box: 400 moves left the destination absent on 2,559 of 154,506 polls. [IO.File]::Move with overwrite is MoveFileEx(MOVEFILE_REPLACE_EXISTING), and the same harness never once saw the name missing across 134,581 polls. It fails transiently instead (13.5% under back-to-back churn, nothing like one refresh per run), so it retries five times and then reports; failing is the safe direction -- the old note survives and the claim stays ours. The catch around it is deliberately UNTYPED: PowerShell wraps a .NET method's exception in a MethodInvocationException, so the typed catch I wrote first never matched, the failure escaped to ErrorActionPreference = Stop, and the temp file was orphaned in the claim registry. The orphaned-temp assertion is what caught it. 2. `overlap.ps1 -Json` emitted `[null]` for an empty map. Build-Map returns AutomationNull, which PARAMETER BINDING converts to a real $null at the call -- and `@($null).Count` is 1, so the zero-rows guard was dead in exactly the case it was added for and the whole-map query printed a phantom row. Strictly worse than the nothing it replaced. The -File path I had verified by hand was fine; the two call sites do not fail alike. 3. The unresolved-notice throttle was repo-wide. The stamp lives in the SHARED git-common-dir and production invokes the gate with no arguments, so the first session to hit a broken gate silenced it for every other session -- and those sessions read that silence as "checked, nobody is here", which is the precise defect the notice exists to remove. One session's diagnostic must never become another's false all-clear. Keyed per worktree now. 4. An empty payload or a literal `null` on stdin does not throw, so that was the one unreadable-input path still exiting silently. 5. A ghost session could outrank a live one. UNVERIFIED is the shape a crashed session's record takes once its pid is recycled; last-write-wins had no opinion about which record it kept for a directory, so a ghost could supply the id and branch reported for a worktree somebody is really sitting in. Fenced records now win, then sorted cwd. Each fix is pinned, and the two regressions were checked against the unfixed code: the phantom-row test sees `[null]`, and the claim test asserts the file name never disappears while a refresh is failing. * docs(worktrees): "is it live yet" has two answers, and they are different I broadcast a merged claim.ps1 improvement to seven sessions as something they could use immediately. A peer tried it, got the old behaviour, and measured why: claim.ps1 is invoked BY HAND from the session's own worktree, so it runs that worktree's copy, and their branch predated the change. The in-force check I had given them was for the hook-run path and returned 0 for them. Both halves of what I said were individually true. The combination was wrong, because there are two rules and I collapsed them into one: hook-run (collision_gate.ps1, and overlap.ps1 as its callee) -- the installed shim resolves the PRIMARY first, so it is live when the primary advances, whatever any branch contains hand-run (claim.ps1, overlap.ps1, presence.ps1) -- resolved from the session's OWN tree, so it is live when that branch has it, and the primary is irrelevant Tabulated, with the check spelled out per path. The point generalises past this PR: test the property where the script will actually run from, because a token that resolves in the primary says nothing about a hand-run script. Also surfaces `collision_gate.ps1 -PathOverride <path>` as the read-only "who holds this file right now" query. It is documented in-script only as a test affordance, and the peer above found it by reading the source after it answered a question nothing else would. Both points are theirs, not mine. * docs(worktrees): the freeze bullet had the right lesson and the wrong reason Routed here by the ADR 0154 session because I was the one live in this file. I had already corrected the false half -- "#119 never merged" -- but only to "it merged the following day", and their framing is better, so this takes theirs. The failure was never that the condition could not arrive. #119 merged (2026-08-02 01:45:00Z, 002be18). It is that THE WORLD MOVED WHILE EVERYONE WAITED: main advanced four times first -- #74 20:27:03Z, #120 23:59:43Z, #131 00:35:29Z, #130 01:01:35Z. So the freeze did not hold main still even while nominally in force. It held only the sessions honouring it, which is the worst of both, and it is a sharper argument for the same bullet without resting on a false fact. Every timestamp re-verified against the API here rather than restated; the measurements are theirs. The same framing was independently corrected in ci.yml (07b6e55) and in BACKLOG #340, making this the third document to carry it and the last one live. Also names what the bullet had become: a compensating control resting on a false premise, which is the failure CLAUDE.md §11 lists -- occurring inside the document that argues for the rule. That is worth one sentence, because the next stale premise will look just as settled as this one did. * docs(worktrees): put the two numbers behind the freeze bullet, with their sources I omitted both for want of a source; the ADR 0154 session found both and I re-ran each before taking it. 12h15m #119's auto-merge armed 2026-08-01 13:29:37Z, merged 01:45:00Z. The timeline event is `auto_squash_enabled` -- a filter on `auto_merge_enabled` returns nothing, which is why the wait looked unmeasurable. Recorded in the doc, since the next person to look will reach for the wrong event name too. 8m26s the claim declaring the freeze is stamped 2026-08-01 23:51:17Z; #120 merged 23:59:43Z. The second is hedged in the doc, and their caveat was the right one: `claimed` records when the KEY was taken, not when the NOTE was written. What tightens it is that `refreshed` is ABSENT on that claim -- and on the code of the day there was no way to edit a note in place at all, so the two coincide unless someone hand-edited the JSON. Stated as "the claim was taken at", which is what the argument needs and no more. That claim is still on the board, still announcing the freeze, which is why it is cited in the present tense. * docs(ledger): the CI backstop does not re-check ownership, and said it did Found while unblocking another session that could not commit a rescued ADR: its number is allocated to a worktree that is not theirs. LEDGER-GATE.md §3 said "CI re-runs the same rules with --ci", and Limits said the --ci leg "is the backstop, and it cannot be bypassed from a branch". Both are true of every rule except the one a reader is most likely to be relying on. ledger_check.py:196 and :241 are each guarded by `not self.ci`, so "was this number allocated to you" runs LOCALLY AND NEVER IN CI. It has to be that way, and the reason is worth keeping: owns() reads the allocation store from <git-common-dir>/mefor-coord/alloc, and a CI runner clones fresh with no store, so the check would return False for every ADR and no ADR could ever merge. This is not a bug to fix. It is a limit that was documented as its own opposite. The consequence is now stated rather than left as an inference: a green CI on an ADR or BACKLOG PR is NOT evidence the number was allocated to anyone. And the residual is bounded in both directions -- after --no-verify a number belonging to another session's unmerged branch can be committed with nothing objecting, but the collision rule still blocks whichever of the two merges second. Late, loud and recoverable, rather than silent, which is the property the gate was actually built for. Same defect class as the freeze bullet corrected two commits ago, and as the collision gate this PR started with: a compensating control resting on a false premise -- CLAUDE.md §11 -- this time inside the document describing the control.
wshallwshall
added a commit
that referenced
this pull request
Aug 2, 2026
…ore it is cut off (#152) * fix(coord): overlap gave two different answers the same bytes, twice Two defects in one script, and they are the same defect: a signal that cannot distinguish the state it reports from a different state. 1. A -Json query answered "nobody else is in this file" by printing NOTHING. `@() | ConvertTo-Json -AsArray` sends zero objects down the pipeline, so ConvertTo-Json never runs -- -AsArray only shapes output that already exists. On stdout an all-clear was therefore byte-for-byte identical to the script dying before it answered, and no consumer could tell them apart. Every -Json exit now goes through one emitter that always produces an array. (-InputObject is not the fix: with -AsArray it double-wraps to [[]].) Found by running the real script against the real collision gate rather than the test stubs, which had been written to a shape the real script never produced. 2. A live session was attributed to a worktree by FIRST prefix hit. Linked worktrees live under the primary checkout, so every linked path is also a prefix match for the primary's row: the primary was handed whichever nested session the hash table enumerated first, and reported LIVE on main, "building" a peer's task list. Hash order is not stable, so it was a different wrong answer each run -- which is why it read as noise rather than as a bug. Longest prefix wins is the only rule that survives nesting, and it is resolved once against every worktree instead of per row. docs/WORKTREES.md already named this exact trap for the announce hook's id rule, where the cure was "never match by prefix". Here a prefix match is genuinely required -- a session may sit in any subdirectory -- so the cure has to be longest-prefix instead. Both are pinned against a real nested-worktree git fixture; a sibling layout would pass under the old rule and prove nothing. Each new assertion was checked against the unfixed script first: the attribution test reports the primary as Live/main/<peer session id>, and the array test sees ''. * fix(coord): the collision gate reported an all-clear when it had checked nothing Every fail-open path in this hook -- overlap script missing, throwing, or printing garbage -- exited 0 with EMPTY STDOUT. On a PreToolUse hook whose stdout is parsed as a decision, empty stdout means "allow", which is byte-for-byte what "checked, nobody else is in this file" looks like. So a gate that had consulted nothing was indistinguishable from a gate reporting all-clear, and its own failure reached the session as reassurance. That is the silent-control class this repo has now hit five times, and it is the same shape as the wired-but-inert announce shim: the surface that was supposed to report sat downstream of the failure it existed to detect. The posture does not change -- every one of these paths still ALLOWS. Only the silence does. It now emits a hookSpecificOutput.additionalContext notice naming which reason (overlap-missing / overlap-failed / overlap-empty / overlap-unparseable / payload-unreadable). It must be that JSON shape and never a bare line: this hook's stdout is a decision, so a stray line risks a misparse on every Edit and Write -- a diagnostic that would be a worse fault than the one it reports. There is deliberately no permissionDecision key: a notice that blocked would invert the fail-open posture that is the whole point of this gate. Rate-limited per reason (30 min, -NoticeCooldownMinutes) so a persistently broken overlap cannot narrate itself into every edit -- this gate's own docstring records where a gate that cries wolf ends up. The stamp lives under -StateDir, defaulting to the repo's coordination dir and resolved ONLY when about to report, so nothing new runs on the hot path. If the stamp cannot be read or written the notice is emitted anyway: the failure mode of a noise-suppressor must be noise, never quiet, or an unwritable directory silently restores exactly the behaviour this removes. Distinguishing overlap-empty from a resolved "nobody" required fixing the producer first (previous commit) -- you cannot detect a difference the producer never encoded. Verified against the real overlap script, not only the stubs: an ordinary edit to an untouched file is silent. Tests: -StateDir isolates the throttle per test, or the first notice would silence the next test's and the suite would pass on run order. * fix(coord): claim.ps1 accepted a new note, reported success, and discarded it -Take documented itself as idempotent -- "re-taking your own claim just refreshes the note" -- and did not refresh anything. A new -Note was taken, acknowledged and dropped. That is worse than an outright failure, because of what the note is for. It is the only field written deliberately to say what a session is doing, and announce-session.ps1 broadcasts it to every session joining the repo while telling them to prefer it over the worktree name. So the one field elevated to authoritative was the one field that could not be corrected. Measured 2026-08-02: a claim note was still announcing "NO PR OPENED -- honouring the #119 merge freeze" to every joining session hours after both that PR and the one it gated had merged. The workaround people reached for -- -Release then -Take -- drops the claim in between, re-opening the race the claim exists to close. Re-taking a key you hold now rewrites the file in place: note, branch (a worktree can have switched branches, and a claim naming a branch nobody is on is another confidently-wrong coordination fact) and a new `refreshed` stamp, leaving `claimed` untouched -- which is what proves the claim was never let go. Write-then-rename, not a truncating write: claim_check.py swallows a JSON parse error into "not claimed", so a torn file is a silently disabled gate, and a crash mid-refresh must leave the old note. Mutual exclusion is unchanged and pinned: a peer's key is still refused. One trap found by the test rather than by reading. ConvertFrom-Json silently coerces an ISO-8601 string to [datetime], so [string]$c.claimed returns the local short form -- sub-second precision and UTC offset gone. Writing that back would have downgraded the stamp on every refresh, and it would still have parsed, so nothing would ever have complained. Stamps now round-trip through "o", and the test asserts byte equality rather than "still parses". The same coercion is handled where announce reads it, with an invariant-culture parse for the string case. announce-session.ps1 now prints each claim note's AGE (from `refreshed` else `claimed`, "age unknown" when it cannot be determined -- an unknown age must not render as a fresh one). Elevating a note to authoritative makes a stale one strictly more dangerous than none, and age is the cheap signal that lets a reader discount it. Not taken here: claim -List's staleness-vs-liveness rendering, which is already open as its own change. * docs(coord): record the three fixes, and correct a claim that has expired SESSION-DRIFT-CONTROLS.md: a fifth instance of the silent-control class, in the collision gate itself, added to the callout that names the class. It carries the part worth reusing -- the fix was not "check harder", it was giving two states different bytes, and the first attempt failed because the PRODUCER had never encoded the difference. Status-table rows for the three controls, and the claim-refresh behaviour beside claim.ps1's entry. WORKTREES.md: the announce id rule already warned that a prefix match resolves a peer in the primary to an arbitrary worktree session, because every worktree cwd extends the primary's. overlap.ps1 had that same trap live at the same time. Noted there, with the distinction that matters: overlap genuinely needs a prefix match, so the cure is longest-prefix rather than exact-match. And a correction. The broadcast-constraints list said of last week's merge freeze that "#119 never merged (it died on an unrelated CI timeout)". It merged the following day, 2026-08-02 01:45Z. Verified against the API rather than restated. The lesson is unchanged and in fact sharper: the recipients could not evaluate the predicate, so the freeze outlived its own condition in both directions -- five sessions held while it had not arrived, and a claim note was still announcing it hours after it had. * docs(coord): announce-on-join merged and was never installed Found while checking a peer session's report, not by looking for it. That session announced itself by hand on 2026-08-02 and gave the reason as "the hook is on an unmerged branch". It had merged (#133, 3389aa2) hours earlier, so the observation was right and the diagnosis was not, and nothing would have corrected it. Measured across all five config roots: - no `mefor-announce` UserPromptSubmit entry anywhere - the one UserPromptSubmit entry installed is `# mefor-web-announce`, which resolves scripts/hooks/announce.ps1 -- a different script in a different repo, and one the installer's own comment already warns is easy to confuse with this marker - <git-common-dir>/mefor-coord/announce/ does not exist, so there is not a single receipt: it has never executed install-coordination.ps1 was last run before the announce row existed, and merging a hook does not install one. Its two other entries -- the SessionStart banner and the collision gate -- were wired then and are present, which is precisely why nothing looked wrong. The part worth carrying: the missing-script notice was built so this class could not hide, and it CANNOT FIRE when the hook is not wired at all, because it lives inside the shim. Same shape as the defect this document already records one level down -- the detector sat downstream of the failure it existed to detect. So the status table now distinguishes rule 4's inert-BY-DESIGN from this one's inert-BY-ACCIDENT, and the confirmation step is a receipt on disk rather than a reading of the settings file. Not installed here: that writes ~/.claude/settings.json, which is shared with every session on this machine. Owner's call, from a plain terminal. * fix(coord): five defects this PR's own first pass introduced or left Found by an adversarial review of the preceding commits, then each one reproduced by execution before being touched. Two were regressions I had introduced; three were gaps. 1. THE CLAIM FILE'S EXISTENCE IS THE LOCK, and the refresh unlinked it. `Move-Item -Force` is delete-then-rename. The take path is an exclusive CreateNew, so any instant the name does not exist is an instant another worktree can claim a key we hold -- i.e. the note refresh could hand a claim away. Measured on this box: 400 moves left the destination absent on 2,559 of 154,506 polls. [IO.File]::Move with overwrite is MoveFileEx(MOVEFILE_REPLACE_EXISTING), and the same harness never once saw the name missing across 134,581 polls. It fails transiently instead (13.5% under back-to-back churn, nothing like one refresh per run), so it retries five times and then reports; failing is the safe direction -- the old note survives and the claim stays ours. The catch around it is deliberately UNTYPED: PowerShell wraps a .NET method's exception in a MethodInvocationException, so the typed catch I wrote first never matched, the failure escaped to ErrorActionPreference = Stop, and the temp file was orphaned in the claim registry. The orphaned-temp assertion is what caught it. 2. `overlap.ps1 -Json` emitted `[null]` for an empty map. Build-Map returns AutomationNull, which PARAMETER BINDING converts to a real $null at the call -- and `@($null).Count` is 1, so the zero-rows guard was dead in exactly the case it was added for and the whole-map query printed a phantom row. Strictly worse than the nothing it replaced. The -File path I had verified by hand was fine; the two call sites do not fail alike. 3. The unresolved-notice throttle was repo-wide. The stamp lives in the SHARED git-common-dir and production invokes the gate with no arguments, so the first session to hit a broken gate silenced it for every other session -- and those sessions read that silence as "checked, nobody is here", which is the precise defect the notice exists to remove. One session's diagnostic must never become another's false all-clear. Keyed per worktree now. 4. An empty payload or a literal `null` on stdin does not throw, so that was the one unreadable-input path still exiting silently. 5. A ghost session could outrank a live one. UNVERIFIED is the shape a crashed session's record takes once its pid is recycled; last-write-wins had no opinion about which record it kept for a directory, so a ghost could supply the id and branch reported for a worktree somebody is really sitting in. Fenced records now win, then sorted cwd. Each fix is pinned, and the two regressions were checked against the unfixed code: the phantom-row test sees `[null]`, and the claim test asserts the file name never disappears while a refresh is failing. * docs(worktrees): "is it live yet" has two answers, and they are different I broadcast a merged claim.ps1 improvement to seven sessions as something they could use immediately. A peer tried it, got the old behaviour, and measured why: claim.ps1 is invoked BY HAND from the session's own worktree, so it runs that worktree's copy, and their branch predated the change. The in-force check I had given them was for the hook-run path and returned 0 for them. Both halves of what I said were individually true. The combination was wrong, because there are two rules and I collapsed them into one: hook-run (collision_gate.ps1, and overlap.ps1 as its callee) -- the installed shim resolves the PRIMARY first, so it is live when the primary advances, whatever any branch contains hand-run (claim.ps1, overlap.ps1, presence.ps1) -- resolved from the session's OWN tree, so it is live when that branch has it, and the primary is irrelevant Tabulated, with the check spelled out per path. The point generalises past this PR: test the property where the script will actually run from, because a token that resolves in the primary says nothing about a hand-run script. Also surfaces `collision_gate.ps1 -PathOverride <path>` as the read-only "who holds this file right now" query. It is documented in-script only as a test affordance, and the peer above found it by reading the source after it answered a question nothing else would. Both points are theirs, not mine. * docs(worktrees): the freeze bullet had the right lesson and the wrong reason Routed here by the ADR 0154 session because I was the one live in this file. I had already corrected the false half -- "#119 never merged" -- but only to "it merged the following day", and their framing is better, so this takes theirs. The failure was never that the condition could not arrive. #119 merged (2026-08-02 01:45:00Z, 002be18). It is that THE WORLD MOVED WHILE EVERYONE WAITED: main advanced four times first -- #74 20:27:03Z, #120 23:59:43Z, #131 00:35:29Z, #130 01:01:35Z. So the freeze did not hold main still even while nominally in force. It held only the sessions honouring it, which is the worst of both, and it is a sharper argument for the same bullet without resting on a false fact. Every timestamp re-verified against the API here rather than restated; the measurements are theirs. The same framing was independently corrected in ci.yml (07b6e55) and in BACKLOG #340, making this the third document to carry it and the last one live. Also names what the bullet had become: a compensating control resting on a false premise, which is the failure CLAUDE.md §11 lists -- occurring inside the document that argues for the rule. That is worth one sentence, because the next stale premise will look just as settled as this one did. * docs(worktrees): put the two numbers behind the freeze bullet, with their sources I omitted both for want of a source; the ADR 0154 session found both and I re-ran each before taking it. 12h15m #119's auto-merge armed 2026-08-01 13:29:37Z, merged 01:45:00Z. The timeline event is `auto_squash_enabled` -- a filter on `auto_merge_enabled` returns nothing, which is why the wait looked unmeasurable. Recorded in the doc, since the next person to look will reach for the wrong event name too. 8m26s the claim declaring the freeze is stamped 2026-08-01 23:51:17Z; #120 merged 23:59:43Z. The second is hedged in the doc, and their caveat was the right one: `claimed` records when the KEY was taken, not when the NOTE was written. What tightens it is that `refreshed` is ABSENT on that claim -- and on the code of the day there was no way to edit a note in place at all, so the two coincide unless someone hand-edited the JSON. Stated as "the claim was taken at", which is what the argument needs and no more. That claim is still on the board, still announcing the freeze, which is why it is cited in the present tense. * docs(ledger): the CI backstop does not re-check ownership, and said it did Found while unblocking another session that could not commit a rescued ADR: its number is allocated to a worktree that is not theirs. LEDGER-GATE.md §3 said "CI re-runs the same rules with --ci", and Limits said the --ci leg "is the backstop, and it cannot be bypassed from a branch". Both are true of every rule except the one a reader is most likely to be relying on. ledger_check.py:196 and :241 are each guarded by `not self.ci`, so "was this number allocated to you" runs LOCALLY AND NEVER IN CI. It has to be that way, and the reason is worth keeping: owns() reads the allocation store from <git-common-dir>/mefor-coord/alloc, and a CI runner clones fresh with no store, so the check would return False for every ADR and no ADR could ever merge. This is not a bug to fix. It is a limit that was documented as its own opposite. The consequence is now stated rather than left as an inference: a green CI on an ADR or BACKLOG PR is NOT evidence the number was allocated to anyone. And the residual is bounded in both directions -- after --no-verify a number belonging to another session's unmerged branch can be committed with nothing objecting, but the collision rule still blocks whichever of the two merges second. Late, loud and recoverable, rather than silent, which is the property the gate was actually built for. Same defect class as the freeze bullet corrected two commits ago, and as the collision gate this PR started with: a compensating control resting on a false premise -- CLAUDE.md §11 -- this time inside the document describing the control. * feat(coord): publish the account's plan limits so a session knows before it is cut off Sessions were hitting the plan limit mid-task and losing work. The real quota state exists -- Settings > Usage shows it -- but nothing inside a session could see it. WHERE THE NUMBERS COME FROM, because it determines the whole shape. Claude Code hands `rate_limits` to a statusLine command's stdin and NOWHERE ELSE; the hook payloads were enumerated in the shipped binary and it appears in exactly one of them. Quota state therefore cannot be subscribed to. It has to be collected by a statusLine and published somewhere shared, which is why this is scripts/coord/usage-collect.ps1 and not a hook. ONE PUBLISHER, N READERS. The quota is account-wide, so any one session's reading is true for all of them. The publish path is user-level because the data is a property of the ACCOUNT, not of a checkout. Summing across sessions would double-count one shared pool. Three defects found by testing rather than by reading, each now pinned: - AN EMPTY READING CLOBBERED A GOOD ONE. Every session runs the statusLine, so every session is a publisher; one that has not yet had its first API response carries no rate_limits and blanked the account's only reading for all of them. Windows are absent INDEPENDENTLY per the docs, so the carry-forward is per window and keeps each window's own captured_at -- a stale number must not wear a fresh timestamp. - HISTORY MUST RECORD ONLY FRESH OBSERVATIONS. A carried-forward percentage against a new timestamp tells the burn rate that consumption stopped, which is the one lie that matters here. - RATE MUST NOT SPAN A WINDOW RESET. The percentage legitimately collapses at the boundary; a rate across it is large and NEGATIVE. Mutation-checked: removing the epoch filter yields -101.63 %/hr at the exact moment a fresh window starts being spent. And a fourth, which is the same ConvertFrom-Json date coercion that downgraded the stamp in claim.ps1: captured_at arrives already typed as a [datetime]. Stringifying it drops the 'Z', re-parsing assumes local, and a reading taken 90 seconds earlier reported as 299 minutes IN THE FUTURE -- exactly this machine's UTC offset. The sign is what made it dangerous: a negative age passes an `age -gt max` test unconditionally, so the staleness guard would have been disarmed on every non-UTC machine while still looking present. Bounded both ways now. WHAT IT CANNOT SEE, printed on every run rather than buried: the per-model weekly buckets (Fable/Opus/Sonnet) and the plan tier are not in the payload at all, and the request to expose them was closed as not-planned. If Opus is burned hard across many sessions, the bucket most likely to stop you is the one this cannot report. Two green bars and an invisible third is worse than no tool. Exit codes 0/10/11/20 so a coordinator branches without parsing prose. UNKNOWN is a real answer and is returned for stale, undateable or future-dated readings; nothing is ever extrapolated from a dead publisher, and the statusLine does not run headless, so a dead publisher is the expected steady state for the coordinator itself. Not built on ccusage: it measures tokens and dollars, not plan limits, despite being the tool everyone recommends and several summaries claiming otherwise. Its own docs contradict them. Not installed here -- it writes user-level settings shared by every session on the machine, so that stays the owner's call from a plain terminal.
wshallwshall
added a commit
that referenced
this pull request
Aug 2, 2026
…ready pointed at (#145) * feat(coord): announce yourself to the other sessions in this repo Every coordination control in this repo is PULL-based: a new session discovers its peers from the SessionStart banner and the peers learn nothing until someone trips the collision gate. That is too late for the collision that costs the most -- two sessions building the same THING in different files, where nothing file-shaped can catch it. This closes the push direction. It ASKS, it cannot send. Hooks are shell commands and session messaging is MCP, so the hook prints the instruction, the live peer roster and the id-resolution rule at the first prompt that has intent to report; the model does the sending. UserPromptSubmit, not SessionStart: at SessionStart a session knows it exists and nothing else, so it can only say hello -- the interrupt without the information. THE ID RULE IS THE PAYLOAD, and it is counter-intuitive enough that the text states it with its evidence. The registry id in this repo's banners is NOT the MCP session id; measured, a registry id and an MCP id for one session shared no characters. Branch does not join them either -- the two rosters reported different branches for the same checkout in 2 of 6 cases. Only cwd joins, and it must be matched EXACTLY: every worktree cwd is an extension of the primary's, so a prefix match resolves a peer in the primary to an arbitrary worktree session. A registry id passed to send_message fails SILENTLY, which reads as the peer ignoring you. EVERY DECISION LEAVES A RECEIPT, because the bug being fixed was a hook that was wired, fired, resolved nothing and exited 0 for weeks -- byte-identical to a healthy hook with no peers. For the same reason the shim carries its OWN missing-script notice: every receipt the hook writes lives INSIDE the script, strictly downstream of the resolution failure that IS the bug, so the shim is the one surface that still reports when the script does not resolve. It is gated on presence.ps1 so the entry stays silent in every unrelated repo on the machine. It always exits 0 -- a UserPromptSubmit hook that fails can block the user's prompt. It consumes presence.ps1 and therefore the single liveness fence; it does not invent a second notion of live. A separate 'mefor-announce' marker keeps it outside install-coordination's mefor-coord strip and outside the website repo's mefor-web-announce entry in the same settings file, so no installer can delete another's hook, and -Only UserPromptSubmit -Uninstall removes announce alone without disarming the collision gate. * test(coord): pin the announce hook, and the anti-no-op wiring class Most tests for a hook like this assert an ABSENCE, and a hook that does nothing at all satisfies every one of them -- which is precisely the production failure being fixed. So the silence assertions are paired with a positive arm: two tests run the SAME runner against fixtures differing only in whether a peer exists, and if the silence tests ever start passing for the wrong reason the positive one goes red first. test_announce_wiring.py is the class the repo had no test for AT ALL: does the thing that gets INSTALLED reach a script that EXISTS, and does it say so when it does not? Its absence is exactly how a wired-but-inert shim survived for weeks. test_every_wired_script_exists_in_this_checkout was written FIRST and watched fail, naming the missing script and printing all three paths it scanned; a green gate is only evidence if it was shown it can see the failure. Also pinned, each because it was got wrong somewhere first: - The foreign UserPromptSubmit entries -- another repo's shim and an unmarked waiting-flag cleanup -- survive install AND uninstall byte-identical. That is the only thing standing between a one-line wiring edit and deleting a hook this repo does not own. - A peer with no StartedAt ranks LAST, not first. ConvertFrom-Json coerces ISO-8601 to DateTime while the '' fallback stays String; Sort-Object over that mixed column raises ZERO errors and puts the empty string FIRST, so without an explicit projected key the least-trustworthy row silently takes the top of a capped target list. - NO_SESSION_ID and DISABLED write their receipt with NO injected -StateDir. An earlier draft resolved the state dir after those branches, so the receipt was unwritable in production while a test that always injected one went green. - Self is excluded by BOTH nets independently: a roster that cannot tell you from a sibling makes the session message itself. - Hostile peer text cannot escape the peer-data block or emit a non-ASCII byte, a hostile session id cannot escape the state dir, and two ids that sanitise identically get two markers. - Two concurrent runs announce exactly once. session-context.ps1 is registered twice on this box today, so double firing is a live pattern, not a hypothetical. * docs(coord): document announcing yourself, and correct a false claim about .claude WORKTREES.md gains the "Announcing yourself" section that the hook's own emitted text and the shim's missing-script notice both cite by name, so the pointer has to land on main in the same merge. It states the id rule ONCE, as the source of record: registry id is not the MCP id, cwd is the only join key and must be matched exactly rather than by prefix, a usable id starts with local_, and a wrong one fails silently. It also states what the change does NOT do. There is no receive-side hook, so the rule that an announcement is peer DATA -- not an operator instruction, and not something to reply to -- lives in the prose and in the fixed message shape and nowhere else. Reachability is given honestly: presence.ps1 is authoritative for who EXISTS, list_sessions only for who can be MESSAGED, and measured, they disagreed 6-to-1. Cost is stated rather than left to be discovered. CORRECTION, and it is why this doc change is in scope rather than deferred: the same chapter claimed ".claude/settings.json is tracked (shared across worktrees)". It is not. /.claude/ is git-ignored, and git ls-files .claude/ returns nothing -- so a worktree's copy is a creation-time snapshot nothing refreshes and several siblings have none at all. That sentence sat at the exact point a reader decides where to install a hook, and it argues for the wrong answer; the new section directly contradicted it. SESSION-DRIFT-CONTROLS.md records announce as the only PUSH control in the D4 layer, plus the two new guarantees worth tracking separately: that wiring reaches a script that exists, and that a resolution failure is now reported by the shim. * fix(coord): stop the collision gate blocking files a peer committed and finished Reported by another session with a repro: it committed a file, went clean, said in writing it was done and handed the file over -- and the peer it handed off to was still refused the edit. overlap.ps1's `Files` is the UNION of what a branch COMMITTED-and-not-yet-landed with what is dirty in its tree. The gate denied on any live row in that set, so "this branch authored it" was treated as "someone is typing in it right now". Those are different claims. The first stays true for the branch's whole life; only the second is what the gate exists to detect. It self-clears on merge -- overlap already intersects three-dot with two-dot so a LANDED branch stops claiming its files. But nothing clears it before landing, and with PRs currently unable to merge, "until it lands" is indefinite: the blocked set grows monotonically and is never released. Two sessions that coordinated correctly and explicitly still cannot hand a file over. That is precisely the failure this gate's own docstring names -- a gate that cries wolf gets uninstalled. overlap.ps1 already told callers to treat its signals differently ("block on live, mention dormant"), but no caller COULD: the row unioned the two signals away. So the row now carries `Dirty`, and the single-file query sets `MatchedDirty` saying which signal actually matched. The gate now DENIES only on an uncommitted edit in a live worktree, and REPORTS committed-and-clean as context instead -- the peer may already have done what you are about to do, which is worth knowing and not worth refusing over. Fails SAFE across the upgrade: a cached row predating `MatchedDirty` has no such property and is treated as dirty, so the gate degrades to its previous over-blocking rather than silently permitting a real collision. Also, while in the file: `git status` now runs with --no-optional-locks. A plain status REWRITES the index of the repo it inspects, and this walks every peer worktree -- so merely asking "what is in flight" was mutating other sessions' checkouts. Verified against the live repro and both directions: the reported file now allows with context; a file with uncommitted changes in a live worktree still denies; an untouched file stays silent. * feat(coord): lead the announce roster with the claim note, not the worktree name Reported by the session it happened to: its worktree is named inter-session-communication-*, auto-generated at creation from a task that session has never worked on -- it has been doing ASVS scorecard work for its entire life. The directory name is the most visible identifier in presence.ps1, overlap.ps1 and this hook's output, and it had already misled TWO sessions (including this one) into guessing that session was building the announce hook. A worktree name is a creation-time label, not a statement of current work, and nothing keeps the two in sync. The claim note is the only field written DELIBERATELY to say what a session is doing, so the roster now prints it, and the legend tells the reader to prefer it over the name. Joined on the claim's `worktree` path, normalised the same way as every other cwd key here. Fail-open throughout: no claims directory, an unreadable claim, or a peer with no claim all just mean the name is the only thing we have -- which is exactly the status quo, never an error. Same session also flagged that the branch I read for it from list_sessions was stale (a spent, merged branch). The announce text already refuses to join on branch and says why; this is a second, independent reason not to trust it. * docs(coord): name the silent-control defect class in the drift inventory A control that cannot distinguish 'ran and resolved' from 'ran and found nothing' is not installed, however it looks. The announce shim outlived every other silent-control defect found the same day BECAUSE it printed a status message -- which is more convincing than silence. The structural cause is the reusable part: every receipt that hook would have written lived inside the script the shim failed to find, so every check sat strictly downstream of the failure it existed to detect. Looking was not neglected, it was impossible. The question to ask of a new control is which surface still reports when the control itself fails to load. Formulation owed to a peer session that hit four instances of this class in one day and named it more sharply than I had. * docs(coord): record the broadcast constraints six sessions learned the hard way Announce-on-join introduces a session; it does not let an established one push an operational notice. That increment is deferred, and on 2026-08-01 six sessions rehearsed it by hand for four hours. Three constraints fell out, recorded so the next attempt does not rediscover them: - A broadcast needs an EXPIRY or a predicate the RECIPIENT can evaluate, never a promise from the sender. A merge freeze shipped with 'lift when #119 merges'; #119 died on an unrelated CI timeout, so five sessions held on a condition that could not arrive and a second round was needed to retract it. - 'Don't do X' is the wrong primitive when automation already has X armed. The freeze asked for restraint while six PRs had auto-merge ARMED and would have landed with nobody clicking anything. The right ask was an action: disarm. - Coordination a tool cannot read does not count. Two sessions agreed IN WRITING to hand over a file and the gate still refused, because the agreement was prose and the gate reads git. Field data from the sessions that lived it, not speculation. * test(coord): pin overlap's dirty-vs-committed signals against real git Nothing drove overlap.ps1's row computation against a real repository, so the question "does MatchedDirty hold when a file is dirty AND committed at once" was unanswerable by the suite. Raised by the session that spent an evening in exactly that state. THAT CASE IS THE ONE THAT FAILS SILENT, which is why it gets a real fixture rather than a stub row. A peer with uncommitted edits in one region and landed work in another is a genuine collision. Had MatchedDirty been derived from the committed diff instead of the working tree it would read FALSE there, the gate would allow, and two sessions would write one file with nothing reported. The over-block this replaced was loud and annoying; that would be quiet and cost someone their work. Verified the tests can SEE it rather than assuming: sabotaged the row to publish an empty Dirty set -- the precise mis-implementation warned about -- and both MatchedDirty assertions went red; restored, all five green. A test written after the code, never observed failing, is a test of nothing. Also pins that overlap does not rewrite a peer worktree's git index, by comparing the index mtime across two queries. An observer must not perturb what it observes, and this one was doing so on every PreToolUse before f55d6c6. Stub rows would only have asserted that the plumbing carries a value someone else computed; the whole question here is what git actually reports. * test(coord): assert a wired coordination hook resolves to a script that exists Raised by the session that traced the shim: the coordination hooks are not installed copies, they are inline commands that locate their script in a working tree at every invocation. If neither base yields the file, Test-Path fails, the loop ends, nothing runs, and the tool call proceeds with no hook and no signal. "The hook is uninstalled" and "the hook ran and permitted this" are indistinguishable from outside, and nothing was watching. Not hypothetical: a foreign UserPromptSubmit entry sat in this same settings file for weeks probing a script that exists only in another repo. The risk composes badly for collision_gate.ps1 specifically, which now (a) fails OPEN on any error, (b) denies less by design after the dirty-vs-committed split, and (c) silently no-ops when unresolvable. Individually defensible; together the realistic bad day is "the gate was never running and nobody noticed". This closes (c) -- the observation is not mine, and it is a good one. Found immediately on writing it: FIVE user settings files across account directories, not the one I knew about. The informational test also prints the original defect as output rather than leaving it invisible: FOREIGN UserPromptSubmit [mefor-web-announce] -> scripts/hooks/announce.ps1: RESOLVES NOTHING HERE It is another repo's entry, so this reports it and does not touch it. Carries a NEGATIVE CONTROL, because the assertion passed on the first run and a green that has never been shown to fail is not evidence. The real hooks cannot be unwired to prove the predicate works -- the primary checkout is shared with live sessions -- so it is exercised against a path known not to exist. Local-machine only: CI has no user settings and these skip there, which means CI does NOT guard this property. Said plainly, and every test prints what it scanned BEFORE it can skip, per test_gate_installed_parity.py -- the pytest config has no -rs, so a skip would otherwise render as a bare dot with no reason. * docs(adr): ADR 0158 silent controls, plus a session handoff Session ended on an owner stop-work instruction at 96% weekly account usage, so this lands the two things that would otherwise have existed only in a transcript. ADR 0158 records a defect class that recurred at least a dozen times across independent surfaces in one working day, in at least two sub-classes: a bound stated independently of the thing it bounds, and a control that cannot observe or act on its own failure. Its spine is that a signal carrying too little information to act on makes every reader re-derive significance by hand until one of them derives it wrong -- so a correct-but-useless RED costs what a silent green costs. EVERY FIGURE IN IT WAS RE-DERIVED BY SOMEONE WHO DID NOT PRODUCE IT, against the repository and the GitHub API. That pass refuted six claims, including four CI numbers that were already merged, and including corrections this session had itself issued hours earlier. Seven retractions are recorded INSIDE the document, each carrying a found-by tag -- because the central empirical finding is that no retraction was made by the author of the claim it retracts, and that is invisible if attribution is smoothed into one voice. Shape over detection is reported as a ratio rather than flattered: three fixes are covered by tests in required CI legs, two by tests that always skip in CI, one by a workflow change with a live residual, and the rest are corrected prose or still open. The Decision separates ENFORCED rules, each naming its gate, from CONVENTION that is knowingly re-breakable. The handoff records what is pushed, what is filed-not-built, and the traps -- a linked worktree's .git being a FILE, a Windows Python unable to read MSYS paths, a raw hasher giving a false mismatch against a git blob on CRLF, and claim.ps1 silently discarding a note refresh. Each is stated as a fact plus its measurement. It also records, first, the five claims this session got wrong -- including retracting a CORRECT estimate on the strength of an incorrect measurement, and sending that false claim to four sessions and the correction to only three. One more arrived while committing this: the leak gate rejected the handoff for a branch slug, on a line a standalone run of the same scanner had passed. The hook scans STAGED files; the standalone run scanned tracked ones. Two scopes, one tool, and only the fail-closed gate could see it. Recorded in the handoff. No engine behaviour changes. * docs(adr): land ADR 0158 -- silent controls, green signals that mean nothing ADR 0158 was authored and committed in 994bfb1 on claude/intersession-communication-hooks-a52335, a trailing commit pushed about an hour and a half AFTER that branch's PR (#133) had already squash-merged. It therefore never reached main and no PR carried it, while the coordination ledger had already allocated the number: docs/adr/README.md stopped at 0156 and 0158 was taken, so the index pointed at a document that did not exist. That gap had a cost. At least four sessions cited this silent-controls taxonomy as "ADR 0157" -- an unrelated HA demotion-safety document allocated to another worktree and still in flight on PR #139. The document that settles the citation was the one sitting unmerged. This branch is cut from 994bfb1 itself, so the original commit stays in history and authorship is exact. The prose, voice and ASCII-only convention are its author's. This commit drops the session handoff and makes three factual corrections where main moved underneath the branch after it was written, each tagged inline in the ADR's own update convention rather than silently rewritten: * 0fdc326 is unreachable from main (this repo squash-merges). It is now given as "merged as 851c849 (#130)", matching the mapping the ADR already uses for 7ebb2ff/2a6649fb. * transports/email.py and transports/direct.py were cited as carrying the same bare starttls() call. 093db33 (#132) gave both an explicit verifying context; pipeline/alert_sinks.py:384 is now the only remaining instance. * The "the false sentence is still there" claim (five sites, one of them numbered Decision rule 13) is closed out: on main the clause survives only inside its own CORRECTED block at :5270 and as a quotation at :7476. The interval is recorded; the rule it produced is unchanged. HANDOFF-announce-hook.md from 994bfb1 is deliberately not landed: it is session state rather than project documentation, no root HANDOFF-*.md has ever existed on main, and it would publish local shim mechanics into a public repo. It stays on its own branch. Verified: exactly one commit in the repository ever added a 0158 ADR and exactly one 0158 filename exists across all refs, so nothing competes for the number. The index row is unchanged from 994bfb1 and appears exactly once. No engine behaviour changes. * docs(adr): make the 0158 TLS update non-perishable The correction I added said 093db33 (#132) left alert_sinks.py as "the only remaining instance on main". That is a checklist-shaped claim with an expiry date: BACKLOG #323 layer 3 (PR #142) closes the alerts call site, and the sentence goes false the moment it lands. Dating the observation does not help a reader who greps for it in a month and finds nothing. Restated as what happened rather than what is currently true -- #132 closed the two connectors, the alerts call site is tracked as #323 layer 3 -- so it holds whether or not #142 merges, and it says outright that the current state must be grepped rather than cited from here. Deliberately does NOT assert that #142 closed the cell: #142 is open at time of writing, and asserting a merge that has not happened is the same defect pointing the other way. found by: the repo-security-review session, which owns #142 and re-derived all three call sites against origin/main before raising it. * docs(adr-0158): replace rotting line-number citations with greppable strings The document's own rule, applied to itself: a quoted string survives a file edit, a line number does not. Ten citations replaced. WHY NOW. All three ci.yml citations (:229, :233, :254) resolve to unrelated text the moment #138 lands, and six docs/BACKLOG.md citations had ALREADY rotted on main before that -- +14 to +40 lines of drift from #345/#346/#347 being appended, with every cited claim surviving verbatim at a new address. Measured fresh against origin/main and against #138's branch, not reused from the report that found them. TENSE, not just addresses. Two of the quoted strings do not survive #138 -- "Measured over the 11 PASSING windows-2025 runs" and "1.46x" are both deleted by it, because #138 ADOPTS this ADR's retractions 1-3 wholesale (12:31, 21:34, 25:51, 1.006x, 1.206x, pools 42/39/36). Left in the present tense those two sentences would ship knowingly false the hour #138 merges, so they now say what ci.yml stated when this was written. The retractions themselves are unchanged and are vindicated by #138, not contradicted. ANCHORS ARE SINGLE-LINE ON PURPOSE. A first pass rewrapped two quotes across a newline, which makes them ungreppable and would have swapped one rot for another. Every anchor is now verified to grep as one line AND to resolve in the tree it points at -- "ZERO tests failing" resolves in ci.yml both on main and after #138. pyproject.toml:266 was simply wrong: the zizmor pin is at :271, in the group opening at :268. Replaced with the group name, which is what the sentence needed and cannot rot. The residual it reports -- that the pin's home is outside zizmor's paths filter -- is verified TRUE and unchanged. OUT OF SCOPE, deliberately: line numbers into less volatile files remain (test_stage_dispatcher.py, claim.ps1, zizmor.yml, install-coordination.ps1, freethread-smoke.yml, collision_gate.ps1). So the ADR does not yet "state no line numbers" outright -- see the handoff note.
wshallwshall
added a commit
that referenced
this pull request
Aug 4, 2026
The 2026-08-03 re-score flagged 24 open items as misdescribing what the code does. Re-verified each against the tree as it stands -- after the archive move and after PR #162, both of which post-date the findings -- and 10 survived. The other 14 did not, and are recorded here rather than silently dropped: #84 #95 #99 #105 #114 #124 #125 #127 #133 #137 #167 #169 #214 #228 Most of those already carry an amendment that covers the stale sentence (#95, #99, #105, #114, #124, #125, #127, #133, #228), and stacking a second ruling saying the same thing is noise. The rest did not survive verification: the finding was itself wrong or overstated, and a wrong correction in a ledger is worse than a stale one. CORRECTIONS ARE ADDED AS DATED AMENDMENTS, NOT PROSE REWRITES. This file's convention is to leave the original claim standing and rule against it, so the record shows what was believed and what replaced it. Silently editing the stale sentence would destroy the evidence that makes the correction checkable. Applied to #62 #64 #131 #166 #179 #182 #237 #321 #329 #336. Representative: - #329 "Five MEFOR_ALLOW_INSECURE_TLS cells": the census is FOUR. #323 landed and routed transports/direct.py through the clamp; it now holds no call to the raw predicate at all (:63, :197, :215). - #321 "no test asserts the detectors can see a site code": false -- tests/test_scan_forbidden.py has per-class hit tests for at least the site code (:126), a customer name (:83), a case-sensitive code (:91) and a routable IP (:107). The detector-coverage half of its Proposed 2 is already in the tree. - #62 plans a dual-read over "existing mfenc:v1 rows", but cell-bound mfenc:v2 is the default writer (settings.py:383 -> base.py:1841; crypto.py:36), and v2 folds (table, column, pk) into the GCM tag -- so a body landing under a different column must be RE-ENCRYPTED, not merely re-encoded. That tightens the catch. - #64's ordered plan still reads live ("Nothing builds before it"), but the measure-first phase completed 2026-07-12 (ADR 0051) and its step-2 lever is refused outright (ADR 0055 withdrawn; ADR 0107 "Do not build F2 or F3"). The refuters removed two overclaims before they landed: #62's draft asserted a live store holds both mfenc markers (a fresh store under the shipped default holds only v2 -- the defensible claim is that a MIGRATION must expect both), and #64's asserted the multi-DB log split still remains, which could not be verified against ADR 0098 and would have been a fresh false claim. No item closes here: in every case the correction narrows the remainder rather than discharging it, and the 2026-08-03 scores already price the remainder. backlog_status_check.py --min-items 277: OK, 277 items, one status banner each.
wshallwshall
added a commit
that referenced
this pull request
Aug 4, 2026
…ones (#163) * docs(backlog): close BACKLOG #226 — the estate Hybrid-layout sweep is done, off-repo The per-feed Hybrid split (connections.toml / <INBOUND>_router.py / <INBOUND>_handler.py / _<feed>_transforms.py) landed across the ported estate in the maintainer-internal migration repository. Owner-attested; nothing in this repository changes, which is also why leaving the item open could never have closed it. Both "Also" clauses are recorded as NOT delivered, with the reason each is not a residual of this item: - "align the IDE Corepoint-import / scaffold path to emit the Hybrid layout" — there is no Corepoint-import path in ide/ to align. That tooling is #105, still open, so the clause is a constraint on #105's design rather than work #226 can perform. The scaffold half is misaddressed too: Insert Element (#48) drops per-file idioms into the current buffer (ide/src/insertElement.ts:1-5) and emits no multi-file feed layout. - "consider a recursive-glob / folder-per-feed loader enhancement" — filed as a consider, and not taken: load_config still globs *.py non-recursively (config/wiring.py:4162), the flat-merge behaviour the Hybrid layout is built around. Follows the #227 precedent: close the primary, state the off-repo/misaddressed residuals explicitly so the item is not re-opened for them. backlog_status_check.py: OK — 277 items, each declaring exactly one status. * fix(ledger): teach the number-space gates to span an archive, and fix two holes found proving it Prerequisite for moving the 185 closed BACKLOG items into docs/archive/backlog/. No item has moved yet; this only makes the guards able to see one when it does. The item namespace will span two paths, so every guard now reads their UNION: - backlog_status_check.py: scan() takes (label, text) pairs and parses them as ONE namespace. A number re-used across BACKLOG.md and the archive was structurally undetectable before -- `seen` was per-parse -- which is the erratum's own shape. - ledger_check.py: triggers on any backlog-bearing path, not the one literal, and builds head/base as the union. Reading the union on both sides also removes a false positive: the move relocates 185 items, so head-union == base-union and `head - base` stays empty, where a per-file view would report 185 vanished numbers with a remedy that renumbers cited items. - alloc.ps1: sweeps both paths in the all-refs term and the working-tree term. - backlog-hygiene.yml: accepts a banner updated in either location. Two pre-existing defects surfaced only because the gates were made to fail on purpose first, neither of which is about the archive: 1. alloc.ps1's working-tree term has NEVER worked. `[regex]'^...'` anchors at the start of the STRING; the term feeds it `Get-Content -Raw`, one string starting "# Backlog". Measured: 0 of 277 headings matched without Multiline, 277 with. The all-refs term hid it by covering every number committed somewhere -- i.e. every case except the uncommitted one this term exists for. 2. backlog-hygiene.yml diffed BASE_SHA..HEAD_SHA (two-dot), which credits a PR for main-side changes to paths it never touched. One main-side edit to BACKLOG.md -- the move being a large one -- would let every PR with an older base pass the "must update BACKLOG.md" required check while enforcing nothing. Now three-dot, matching ci.yml's form for the same question. Anti-narrowing, because a green gate over a shrunken corpus is the failure mode: - `--min-items N` fails when fewer items are found than required, and CI pins 277. Without it, 277 -> 92 fails nothing. - The scanned files are always printed with the count; a bare integer cannot distinguish "items closed" from "a file stopped being read". - A liveness receipt in the test suite asserts the same floor. - An explicitly-named --backlog path that does not exist is an error, not a skip. alloc.ps1 gains `-ShowFloor`: print the floor and the swept paths, allocate nothing. Allocation is a one-way door, so before this the only way to ask what the floor could see was to spend a number on the question -- which is how it ran a whole release reading two refs while its header promised all of them. Get-Floor takes -Peek so the inspection cannot advance the high-water ratchet; the first -ShowFloor run against a planted number moved this clone's watermark 316 -> 990 before that was fixed. Proofs run, each observed failing BEFORE the fix: - archive-only unallocated #1007 staged: old gate rc=0, new gate BLOCKED. - #990 planted in the archive: old sweep floor 353 (blind), new sweep 990. - cross-file duplicate #118: detected, naming the other file. - banner violations inside the archive only: detected. - --min-items over a narrowed corpus: rc=1 with the scanned-file list. - -ShowFloor twice against a plant: watermark unchanged at 316. ruff + mypy --strict clean; 43 gate tests pass. * docs(backlog): move the 185 closed items into docs/archive/backlog/BACKLOG-CLOSED.md docs/BACKLOG.md becomes the ~92 items someone can act on: 8,742 -> 3,648 lines. The closed items are not deleted, summarised, or rewritten -- they are relocated verbatim, so the file that gets opened, grepped and edited daily is the open set. MOVED, NOT REWRITTEN. Every relocated block is byte-identical to the one that left BACKLOG.md, headings included. Verified mechanically against a pre-move copy: - 277 items before = 92 after + 185 archived, no overlap, union identical - every OPEN block byte-identical to its source - every ARCHIVED block byte-identical to its source - all non-item prose in BACKLOG.md preserved verbatim Byte-identical headings are load-bearing, not tidiness: GitHub derives anchor slugs from heading text, so all 64 archived->archived cross-references keep resolving with no edit at all. That is the whole argument for one archive file rather than a split by status, year, or cluster -- #52 alone receives 99 of the 110 in-file anchors, and its citers span #65 to #184, so no cut isolates them. Cutting item blocks at the next '## ' heading of EITHER kind, not the next numbered item: 4 blocks in this file are followed by a section header, which a naive cut would have dragged into the archive along with the prose beneath it. Anchors, all 127 re-resolved against real headings after the edit: - 44 rewritten in BACKLOG.md -> archive/backlog/BACKLOG-CLOSED.md#<same-slug> - 1 rewritten in the archive -> ../../BACKLOG.md#<same-slug> (#226 -> #105) - 3 cross-file links repointed: AOAG-DEPLOYMENT.md (#100, #101), ADR 0026 (#30) - 64 archived->archived untouched, by design 13 anchors still do not resolve, and ALL 13 WERE ALREADY DEAD BEFORE THIS COMMIT -- confirmed by running the same check over the pre-move file, which returns the identical multiset (11 bare-number self-anchors: #40 x4, #323 x3, #28, #29, #329, #333; plus 2 links to #13 in COUNSEL-ENGAGEMENT-BRIEF.md, a number this sequence never had). They are left dead and documented in the archive header rather than repointed at a plausible neighbour: a citation resolving to the WRONG item is the erratum's failure mode, and unlike a dead link it looks like success. The archive carries its retirement banner inline rather than in a sibling README -- docs/archive/throughput/ needs a README because it indexes five documents; one file does not, and two documents that must agree is a drift surface. It states the rules that keep the namespace honest: never renumber, re-open by moving the block back (never by copying, which creates the cross-file duplicate the status check now fails), and add any future archive file to alloc.ps1's $backlogPaths AND backlog_status_check.py's DEFAULT_SOURCES in the same commit -- a file named in neither is policed by nothing. Gates verified post-move: - backlog_status_check.py --min-items 277: OK, 277 items, and it now PRINTS "scanned: docs/BACKLOG.md (92), docs/archive/backlog/BACKLOG-CLOSED.md (185)" - ledger_check.py on the staged move: rc=0 (relocation adds no numbers, because head-union == base-union -- the exact false positive the union view removes) - alloc.ps1 -ShowFloor: floor 353 across both paths, next 1000 - 43 gate tests pass Note the floor is unchanged at 353 because the highest item (#353) is open and stays in BACKLOG.md. The archive-sweep fix is therefore PROSPECTIVE, not a save: it starts mattering the first time a top-of-range item closes and moves. * docs(backlog): re-score all 92 open items on the ten-level scale (2026-08-03) Every open item now carries a current value x difficulty score. Before this, 23 had none at all and the other 69 were from the frozen 2026-07-10 pass, which predates the 2026-07-28 reconcile that closed 31 items -- and a stale score reads exactly like a fresh one. Method, unchanged from the pass it supersedes: scored from each item's own Scope / Why / Trigger / Nearest-existing-mechanism text rather than rescaled from the old number, then adversarially verified against the code -- a second reader per batch attacking build state first, then verdict/tier, then value and difficulty. 26 of 92 scores were overturned by that pass and carry the refuter's number. The banner is the live record and the table is a view of it; both are written here and a mechanical check confirms 92 banners and 92 rows agree on every triple. THE RATIONALE IS REPLACED, NOT JUST THE NUMBERS. Carrying an old justification under a new score is how a banner comes to argue against itself: - #114's surviving "clean workaround via the on-demand test probe" is a claim PR #162 explicitly retracted -- both destinations' test_connection CREATE the target dir, so the probe cannot answer the question the toggle asks. That is what lifts it off the parity-with-a-workaround band to 6/3. Its replacement rationale was ALSO stale (it described the silent-ignore #162 had just fixed) and is hand-corrected. - #105's "large greenfield 71-action mapper needing its own ADR" describes an importer that has since shipped under ADR 0086. Scheduling barely moved, which is the reassuring result: only TWO tiers changed -- #64 DEMAND-GATE -> P3 (an index over levers that live in #62/#63/#47/#34, so it ships nothing runnable of its own) and #105 P3 -> DEMAND-GATE. Neither contradicts an explicit demand-gate/on-trigger ruling in its own body; that was checked for all 51 items carrying a prior tier. Distribution is RECOMPUTED with the table rather than carried forward, and all four lines sum to 92. The superseded table keeps its own frozen lines and now says so. Tiers: P1 4, P2 19, P3 17, DEMAND-GATE 52 Quadrants: quick win 22, big bet 5, fill-in 56, money pit 9 The four P1s: #341 (9/3, a handler returning a tuple/set of Sends delivers nothing silently -- an accept-and-drop CLAUDE.md §12 forbids), #324 (7/2), #325 (6/2), #327 (6/2). NOT in this commit: 24 items were found to misdescribe their own build state -- prose asserting a gap that has since shipped, or citing messagefoundry/console/, a package retired with #103. Those are banner corrections and land separately; the scores here already price the remainder rather than the original scope. Two mechanical faults were caught by reading the output rather than trusting the run: the quadrant regex omitted the hyphen in "fill-in", so 57 of 69 items took the fallback branch and got a SECOND score inserted beside the first; and the synthesizer's own distribution lines did not follow from its own table (11 quadrant mismatches, 8 ordering violations, difficulty summing to 95 of 92). The script now refuses to write when any line carries two score spans or the scored count is not 92. backlog_status_check.py --min-items 277: OK, 277 items across both files. * docs(backlog): correct 10 items whose own prose misdescribed build state The 2026-08-03 re-score flagged 24 open items as misdescribing what the code does. Re-verified each against the tree as it stands -- after the archive move and after PR #162, both of which post-date the findings -- and 10 survived. The other 14 did not, and are recorded here rather than silently dropped: #84 #95 #99 #105 #114 #124 #125 #127 #133 #137 #167 #169 #214 #228 Most of those already carry an amendment that covers the stale sentence (#95, #99, #105, #114, #124, #125, #127, #133, #228), and stacking a second ruling saying the same thing is noise. The rest did not survive verification: the finding was itself wrong or overstated, and a wrong correction in a ledger is worse than a stale one. CORRECTIONS ARE ADDED AS DATED AMENDMENTS, NOT PROSE REWRITES. This file's convention is to leave the original claim standing and rule against it, so the record shows what was believed and what replaced it. Silently editing the stale sentence would destroy the evidence that makes the correction checkable. Applied to #62 #64 #131 #166 #179 #182 #237 #321 #329 #336. Representative: - #329 "Five MEFOR_ALLOW_INSECURE_TLS cells": the census is FOUR. #323 landed and routed transports/direct.py through the clamp; it now holds no call to the raw predicate at all (:63, :197, :215). - #321 "no test asserts the detectors can see a site code": false -- tests/test_scan_forbidden.py has per-class hit tests for at least the site code (:126), a customer name (:83), a case-sensitive code (:91) and a routable IP (:107). The detector-coverage half of its Proposed 2 is already in the tree. - #62 plans a dual-read over "existing mfenc:v1 rows", but cell-bound mfenc:v2 is the default writer (settings.py:383 -> base.py:1841; crypto.py:36), and v2 folds (table, column, pk) into the GCM tag -- so a body landing under a different column must be RE-ENCRYPTED, not merely re-encoded. That tightens the catch. - #64's ordered plan still reads live ("Nothing builds before it"), but the measure-first phase completed 2026-07-12 (ADR 0051) and its step-2 lever is refused outright (ADR 0055 withdrawn; ADR 0107 "Do not build F2 or F3"). The refuters removed two overclaims before they landed: #62's draft asserted a live store holds both mfenc markers (a fresh store under the shipped default holds only v2 -- the defensible claim is that a MIGRATION must expect both), and #64's asserted the multi-DB log split still remains, which could not be verified against ADR 0098 and would have been a fresh false claim. No item closes here: in every case the correction narrows the remainder rather than discharging it, and the 2026-08-03 scores already price the remainder. backlog_status_check.py --min-items 277: OK, 277 items, one status banner each. * docs(backlog): file BACKLOG #1000 — prove each required merge context can fail Escalated by the coordinator on the ground that it outlives the PR that fixed it. Deliberately NOT filed as "fix the two-dot diff": that instance already landed in 39b62bf, and filing shipped work is the rot the hygiene gate exists to prevent. The item is the CLASS. `.github/required-contexts.txt` names 13 contexts that block merge, and not one of them is proven able to go red. The deliverable is a negative control per context -- a fixture carrying the exact violation that context exists to catch -- plus a CI job that fails when a required context has none, so the coverage cannot silently decay as contexts are added. Scoped narrower than "test the gates" on purpose: it does not re-test what each gate checks, since the gates' own suites do that. It asserts one property per context -- this gate is capable of failing. The argument is that the class has now fired at least four times here, each found by hand and none by CI: #334 semgrep, required and blocking, scans a two-directory allow-list #327 six .gitignore rules are the sole control over maintainer-internal docs, and nothing asserts they still match anything #321 the forbidden-content gate exited 0 on a real site code and partner product #325 the same gate's home-path detector misses 1 of 4 spellings of a Windows path Each is correctly filed as its own defect. None of them establishes the property that would have caught all four before they shipped, and that property is a different artifact from any of the individual fixes. Value 7 / Difficulty 3, quick win, P1 -- not demand-gated; the trigger fired four times. Ranked table and all four distribution lines recomputed to 93 open items; a mechanical check confirms 93 banners and 93 rows agree on every triple. Number allocated atomically via scripts/coord/alloc.ps1 (#1000 -- the first in the post-partition public sequence, clamped to >= PUBLIC_BACKLOG_FLOOR), never grepped. backlog_status_check.py --min-items 277: OK, 278 items across both files. The floor is a floor, so growth passes it; it is there to catch shrinkage.
wshallwshall
added a commit
that referenced
this pull request
Aug 4, 2026
…ng) + coordination-fact corrections (#164) * test(ui): use a reserved IdP host in the interstitial tests, not a real vendor endpoint `login.microsoftonline.com` was the only real IdP hostname anywhere in the repo -- no accounts.google.com, no *.okta.com, no auth0.com, no onelogin, no pingidentity. That singling-out is what makes it read as a hint about the deployment's actual identity provider rather than as generic test data, and no customer or site operational detail belongs in a public repo. `idp.example.com` is already this repo's OIDC placeholder for this feature (tests/test_settings.py configures the whole block with it), so this makes the interstitial tests consistent with the settings tests rather than inventing a convention. Behaviour-preserving, and measured rather than assumed: the external-link classifier special-cases no host at all -- no allowlist, no example.com handling, no loopback or same-origin shortcut -- so an RFC 2606 reserved name classifies exactly as the real hostname did. Both files: 27 passed. Also drops the vendor name from one docstring; the sentence's point was that the IdP is trusted and is not the hospital, which does not need the vendor to make it. * chore(coord): drop the Claude plan-usage tooling — internal to how we work, not product This repo is a healthcare integration engine. Tooling and documentation about the maintainer's own Anthropic plan consumption (5-hour / weekly pools, per-model weekly buckets, a statusLine collector) is internal development process, not something an open-source integration engine should carry. Removed set is exactly the inverse of one commit: 41a8c49 added these same five paths (three scripts, one test, one 52-line doc section) and nothing else, so this is a clean excision rather than untangling. The subsystem was also dead. The statusLine executor is only reached from the terminal TUI's render tree, so it never runs under the desktop app -- its heartbeat has never been written once and latest.json has never existed, which means the reader could only ever return UNKNOWN. What shipped was a tool that does not run, documented with a boundary claim ("never runs under claude -p or the SDK") that is false: the boundary is the terminal TUI, and the desktop app is a third category the docs never named. tests/test_coord_usage.py is deleted in THIS commit, not a later one. Its skip gate is `pwsh missing or os.name != "nt"`, so it skips on ubuntu but RUNS on both Windows legs, where it hard-asserts returncode == 0 with no existence check on the script paths. Deleting the scripts without the test would red two required contexts -- and with auto-merge armed and strict branch protection, a red required leg blocks every open PR in the repo, not just this one. docs/WORKTREES.md keeps everything else: the section is excised in place rather than the file moved or ignored, because ~12 files point readers at it and its PR-triage material is the best contributor documentation here. 728 -> 676 lines. Nothing that other sessions depend on is touched: alloc, claim, presence, occupancy, lock, overlap, session-registry, install-coordination and install-git-hooks all stay. install-coordination.ps1 wires three hook scripts and never the statusLine key, so the coordination bundle is unaffected. Verified: no residual reference to any removed path anywhere in the tree. The material is not lost -- `git show 41a8c49:<path>` retrieves any of it. * fix(coord): two load-bearing coordination facts were wrong — correct them at every site Both were inferences recorded as measurements in the doc other sessions treat as the source of record, and both taught the wrong lesson. 1. "Hooks cannot call MCP" is FALSE. `type: "mcp_tool"` is a documented hook handler on every hook event, with the tool's output treated like command-hook stdout. Probed 2026-08-03: a command control in the same UserPromptSubmit array fired and reached the model, while an mcp_tool naming ccd_session_mgmt AND one naming a deliberately nonexistent server both produced nothing — and with no MCP server connected on this box, "not surfaced", "not addressable" and "errored invisibly" are the same bytes. Recorded as UNTESTED, not impossible, with the re-run condition named. If it works, announce collapses to one hook entry. 2. "A registry id passed to send_message fails silently" is FALSE — it errors loudly. Measured by calling send_message with a valid id belonging to no session: `Session <id> not found.`, nothing delivered. The namespaces carry different UUIDs by construction, so that is the path a registry id takes. Getting the id wrong is self-announcing; the doc had every session braced for a failure mode that does not occur. Fixed at all four sites, including the instruction text the hook prints into every session — the one that was actively teaching it. tests/test_announce_hook.py + test_announce_wiring.py: 59 passed. * docs(drift): announce-on-join is LIVE — the status table was a claim about the past The row said "MERGED, NOT INSTALLED — inert by accident. It has never run." Re-established 2026-08-03 by both methods the section itself prescribes: `install-coordination.ps1 -Status` reports UserPromptSubmit -> announce-session.ps1 INSTALLED, and the state dir holds 47 markers / 28 receipt files / 23 delivery logs, newest line `out=ANNOUNCED peers=3 reach=3 sent=3 checks=2 ms=616`. Rewritten as a two-column table — the defect on 2026-08-02, the re-verification on 2026-08-03 — so the incident and its lesson survive instead of being deleted along with the stale status. The lesson is the valuable half: merging a hook does not install one, and the missing-script notice built to catch that class lives inside the shim, so it cannot fire when the shim is not wired at all. Also corrects the interval: ~13 hours after #133 merged, not "weeks", and adds the closing rule that a row not re-established by receipt is a claim about the past, not a status. * test(console): retire the CodeQL substring sink — and make the open-redirect assertion actually assert it CodeQL flagged `assert "login.microsoftonline.com" in body` as incomplete URL substring sanitization. It is a false positive on the SINK — that line asserts the interstitial RENDERS a host, it is not a redirect gate — and the coordinator confirmed the real question independently: production has no `X in url` check anywhere in routes/oidc.py, so no open-redirect exists. Restructured rather than suppressed, because the rule was pointing at something true about the TEST even though it was wrong about the risk: `host in page` passes if the host appears ANYWHERE — a stray comment, an unrelated attribute — which is the same laxness the rule exists to warn about. Now anchored to the element that renders it: `<code>host</code>`. Stronger assertion, no sink. Also strengthened the one that matters most in this file. The open-redirect test used to assert a string was ABSENT ("the IdP URL does not appear") plus a substring match on the form action. A string-absence check passes for the wrong reason the moment the markup changes shape. It now PARSES the Continue form's action and asserts the actual property: no scheme, no netloc, exact path, empty query — there is nothing in it for an attacker to steer. Proved it can go red in both directions before trusting it: an absolute action and a destination smuggled into the query are each rejected, current markup passes. ⛔ Not suppressed deliberately. A suppression is a claim someone reads later and takes on trust; a standing ignored red is the "green gate that lies" pattern this session has spent itself unpicking. Restructuring cost one line more and leaves nothing to trust. Cherry-picked here to land it at last, and to clear the CodeQL alert this PR's own hostname substitution surfaced. The substring shape at line 67 has been in main all along; changing that line re-attributed the pre-existing "Incomplete URL substring sanitization" alert to this PR as a new high-severity finding. The real fix already existed on this commit, so applying it resolves both. Conflict resolution: took this commit's structure (element-anchored assertion and the parsed form action) and kept the reserved `idp.example.com` host from the earlier commit in this PR, so the vendor endpoint does not come back. Recovered work: authored ~9 hours before PR #156 merged but dropped by a push boundary, not a review decision -- #156's merged head 9f85b46 is this commit's parent. When found it was reachable from a single local branch ref with no remote copy.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was broken
The
mefor-web-announceUserPromptSubmithook in~/.claude/settings.jsonis user-level, so it fires in every repo — but it probesscripts/hooks/announce.ps1, which only ever existed in the separatemessagefoundry-websiterepo. In MessageFoundry it resolved nothing and exited 0. Wired, firing, inert, silent — for weeks.Cross-session coordination here was entirely pull-based: a session discovers its peers at startup and the peers learn nothing. Nobody finds out about anybody until someone trips the collision gate, which is too late for the collision that costs most — two sessions building the same thing in different files, where nothing file-shaped catches it.
What this adds
scripts/hooks/announce-session.ps1— aUserPromptSubmithook that hands the model its live peer roster plus the id-resolution rule at the first prompt that has intent to report, and asks it to introduce itself.It asks; it cannot send. Hooks are shell commands and session messaging is MCP. It fires on the first prompt at which a messageable peer exists (not simply the first prompt), again when a new peer appears, under a lifetime budget. It always exits 0 — a
UserPromptSubmithook that fails can block the user's prompt. It consumespresence.ps1and therefore the single liveness fence; it does not invent a second notion of "live".The id rule is the payload, and it is counter-intuitive enough that the emitted text states it with evidence. The registry id in this repo's banners is not the MCP session id — measured, a registry id and an MCP id for one session shared no characters. Branch doesn't join them either (the two rosters disagreed on branch for 2 of 6 checkouts). Only cwd joins, matched exactly — every worktree cwd is an extension of the primary's, so a prefix match resolves a peer in the primary to an arbitrary worktree session. A registry id passed to
send_messagefails silently, which reads as the peer ignoring you.The collision gate over-block
Reported with a repro: a session committed a file, went clean, said in writing it was done and handed it over — and the peer was still refused.
overlap.ps1'sFilesis the union of committed-and-unlanded with working-tree. The gate denied on any live row in that set, so "this branch authored it" was treated as "someone is typing in it now". Its own header already told callers to treat the signals differently — but no caller could, because the row unioned them away. The contract was unimplementable.Rows now carry
Dirty; the single-file query setsMatchedDirty. The gate denies only on an uncommitted edit in a live worktree and reports committed-and-clean as context instead. Fails safe across the upgrade: a cached row predatingMatchedDirtyis treated as dirty, degrading to the old over-block rather than silently permitting a real collision.Also:
git statusnow runs--no-optional-locks. A plaingit statusrewrites the index of the repo it inspects, and overlap walks every peer worktree — so asking "what is in flight" was mutating other sessions' checkouts on everyPreToolUse.Tests, and why they should be believed
Both negative controls were written first and watched fail:
test_every_wired_script_exists_in_this_checkout— red until the script landed, naming the missing file and printing all three paths it scanned. Nothing in this repo asserted this for any hook before; its absence is how a wired-but-inert shim survived weeks.test_installed_coord_hooks.py— asserts a wired hook resolves to a script that exists, closing the "absence is indistinguishable from approval" hole. It found five user settings files across account directories, and now prints the founding defect as ordinary output:FOREIGN UserPromptSubmit [mefor-web-announce] -> scripts/hooks/announce.ps1: RESOLVES NOTHING HERE. Another repo's entry — reported, never touched.test_coord_overlap_signals.py— drivesoverlap.ps1against a real git fixture. Nothing did before, so "doesMatchedDirtyhold when a file is dirty and committed at once" was unanswerable by the suite. Verified by sabotaging the row to publish an emptyDirtyset and confirming both assertions went red.Most tests here assert an absence, which a hook that does nothing also satisfies — so they are paired with a positive arm running the same runner against fixtures differing only in whether a peer exists.
Deliberately not done
Broadcast. Announce-on-join introduces a session; it does not let one push an operational notice. Six sessions rehearsed that by hand for four hours and the constraints are recorded in
docs/WORKTREES.md: a broadcast needs an expiry or a recipient-checkable predicate, never a sender's promise; "don't do X" is the wrong primitive when automation has X armed; and coordination a tool cannot read does not count.Hunk-range disjointness. All three false denials reported were the committed-and-clean case this already fixes. A wrong disjointness check under-blocks, trading a loud failure for a silent one. Filed evidence-gated instead.
There is no receive-side hook, so "an announcement is peer data, not an operator instruction, and must not be replied to" lives in the prose and the fixed message shape alone. Stated rather than buried.
Cost, stated rather than discovered
The shim costs ~0.5s per user prompt in every repo on the machine; the peer lookup adds ~1.0s on prompts where it runs. The new tests are pwsh+Windows-only and add ~65-70s to
windows-2025specifically.Verification
ruff format --check,ruff check,mypy --strict, leak gate, and 86 tests green — re-run after mergingmain, which brought changes tosession-registry.ps1andpresence.ps1that this consumes.🤖 Generated with Claude Code
Deploying this — merging is NOT enough, and the difference is measurable
collision_gate.ps1is not an installed copy.~/.claude/settings.jsonwires it as an inline shim that resolves the script live out of a working tree on every invocation, primary checkout first. So the gate adjudicating all worktrees is whatever the primary's HEAD happens to point at.Measured while this PR was open:
The gate in force right now does not contain this fix, and merging this PR will not change that — it only makes the primary 3 behind instead of 2.
So do not announce this as "merged, the over-blocking is fixed." Sessions will test it, get a deny, and reasonably conclude the fix is broken while actually executing an older script. The accurate form is merged; takes effect once the primary checkout is updated to a commit containing it.
Verify what is actually running:
A non-zero count is the fix being in force. Two events, not one.
A corollary that is a live debugging trap: editing
collision_gate.ps1in your own worktree has no effect on the gate adjudicating you — the shim never reaches the second base while the primary has the file. Anyone testing a gate change from a feature branch will see zero effect and reasonably conclude their change is broken. Test it by invoking the script directly (-PathOverride), as this PR's tests do.Why the over-block needed fixing rather than waiting out
Observed on this repo while the PR was open: the false-positive holder set churns rather than drains. A session retried one blocked edit as branches merged and was still denied — but by a different set of holders, one having cleared and two new ones having arrived. On a busy repo a session can be blocked continuously by a succession of holders, none of whom is actually editing the file. "Wait for it to clear" is not a workable answer.
Separately, a clean positive control for
overlap.ps1's three-dot ∩ two-dot behaviour, measured across #131 merging:docs/BACKLOG.mdholders went 4 → 2, and one holder disappeared while still live with the same worktree checked out — only its content had reachedmain. The intersection self-clears on merge exactly as documented, which is why this PR narrows the predicate rather than adding an expiry.A known weakness in the claim note this leans on
This PR makes the announce roster print each peer's claim note and tells the model to prefer it over the worktree name, because a worktree name is a creation-time label that nothing keeps current — one in this repo has advertised work it never did for its whole life.
The claim note is the better signal, but it is not as strong as that framing implies, and the PR should not pretend otherwise:
scripts/coord/claim.ps1silently discards a note update. Re-taking a key you already hold prints a greenYou already hold '<key>'and drops the-Note, so a session that re-takes to refresh its note believes it has communicated and has not. Its parameter doc atclaim.ps1:37promises the opposite of whatclaim.ps1:129implements — the two contradict each other ninety lines apart, in one file.So until that is fixed, this hook can broadcast a stale note as current intent, which is a real cost of elevating the note to authoritative. Leading with the claim is still better than leading with the directory name — a deliberate-but-stale statement beats an auto-generated one that was never true — but the gap is worth naming rather than discovering.
Filed separately; deliberately not fixed here, since it is independent of this change and deserves its own test (take with note A, re-take with note B, assert
-Listshows B — the current code passes anything that only checks the exit code, which is how it survived).