Skip to content

fix(progress-stream): the live step is for DMs, not for shared channels - #2604

Merged
john-the-dev merged 15 commits into
mainfrom
fix/progress-step-leaks-to-shared-channels
Aug 27, 2026
Merged

john-the-dev merged 15 commits into
mainfrom
fix/progress-step-leaks-to-shared-channels

Conversation

@sonichi

@sonichi sonichi commented Aug 4, 2026

Copy link
Copy Markdown
Owner

What happened

poll_progress renders core-status.json's step verbatim into the channel the owner's task arrived on. The gate in front of it asks who sent the task (owner tier) and never who can read the channel.

On 2026-08-04 an owner task arrived in a shared guild owned by a third party, and the placeholder published this to everyone in it:

⏳ Loop pass: draining queue, then #2596 re-review state and remaining episode gaps (22s)

The owner's reply was "Wrong channel" followed by "Not mine" — the guild isn't his.

step is written by whatever the core is currently doing: a PR number, a file path, whose calendar is being read. It is owner-facing copy, and it was being broadcast wherever he happened to be standing.

Why the existing gate didn't catch it

should_stream_task is tier-based, and its own comment frames the risk as "posting a placeholder for them would leak processing state for an untrusted sender" — i.e. leaks were reasoned about in terms of the sender. An owner-sent task in a shared channel is the case that framing has no room for.

The change

The detailed step goes to DMs only. A guild channel can have any membership and the bridge cannot establish it is safe — a private-looking channel today gains a member tomorrow — so DM is the only audience it can actually verify.

The placeholder still posts everywhere; only its content changes:

DM      ⏳ reading Chi's calendar for the Aug 6 dinner (12s)
shared  ⏳ working… (12s)

That distinction is load-bearing. Liveness is the entire feature, so a fix that quietly stopped narrating outside DMs would trade a leak for a regression — one test asserts the contentless placeholder is still emitted.

_render_progress_content's new parameter defaults to False — fail-closed. A caller that hasn't established the audience doesn't publish the step. That flips the existing live core renders progress copy case, which now passes the DM flag explicitly, with a sibling case pinning the default.

The policy is a pure function (progress_stream.step_visible_in) so it's testable without discord types; the bridge only supplies isinstance(channel, discord.DMChannel).

Evidence

With the leak restored (step_visible_in → return True):

FAIL  step_visible_in: guild no
FAIL  shared channel leaks NO fragment of the step
FAIL  bridge: step suppressed when audience unknown

At HEAD:

PASS  step_visible_in: DM yes
PASS  step_visible_in: guild no
PASS  DM still shows the live step
PASS  shared channel leaks NO fragment of the step
PASS  shared channel still gets a contentless placeholder
bridge-wiring tests passed

Sibling suites pass unchanged — telegram-bridge-progress-stream (exit 0), discord-bridge-progress-placeholder-ingest (exit 0). ruff clean.

Note for review

My first attempt added the assertions as a def test_...() at the end of the file. That file has no runner — it executes at module level through a check() accumulator — so the function was never called and could not have failed. Caught before pushing; the assertions are now inline in the live path. Worth a glance that they're genuinely in the executed flow.

🤖 Generated with Claude Code

sonichi added 2 commits August 3, 2026 20:28
`poll_progress` renders `core-status.json`'s `step` VERBATIM into the
channel the owner's task arrived on. The gate in front of it asks who
SENT the task (owner tier) and never who can READ the channel.

Those are different questions. On 2026-08-04 an owner task arrived in a
shared guild owned by a third party and the placeholder published

    ⏳ Loop pass: draining queue, then #2596 re-review state (22s)

to everyone in it. The owner's reply was "Wrong channel" / "Not mine".
`step` is written by whatever the core is doing — a PR number, a file
path, whose calendar is being read — so it is owner-facing copy that was
being broadcast wherever he happened to be standing.

The detailed step now goes to DMs only. A guild channel can have any
membership and the bridge cannot know it is safe — a private-looking
channel today gains a member tomorrow — so DM is the only audience it
can actually establish.

The placeholder still POSTS everywhere; only its content changes. That
matters: liveness is the entire feature, and a fix that silently stopped
narrating outside DMs would trade a leak for a regression. One test
asserts exactly that.

`_render_progress_content`'s new parameter defaults to FALSE — fail-
closed. A caller that has not established the audience does not publish
the step. That flips the existing "live core renders progress copy"
case, so it now passes the DM flag explicitly and a sibling case pins
the default.

The gate is a pure function (`progress_stream.step_visible_in`) so the
policy is testable without discord types, and the bridge only supplies
`isinstance(channel, discord.DMChannel)`.

Verified failing with the leak restored: `step_visible_in: guild no`,
`shared channel leaks NO fragment of the step`, and `bridge: step
suppressed when audience unknown` all fail. Sibling suites
(telegram-bridge-progress-stream, discord-bridge-progress-placeholder-ingest)
pass unchanged; ruff clean.

Stand: Echo Act IV Pro

@john-the-dev john-the-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head 643b89d771cb40b8dba479709947e1e50c30ebb7.

The code-level privacy fix is sound in focused testing: origin/main still calls _render_progress_content(now, elapsed) unconditionally, while this head makes the audience flag fail-closed, passes it only for discord.DMChannel, and preserves a generic timed placeholder in guild channels. I ran python3 tests/progress-stream.test.py, python3 tests/telegram-bridge-progress-stream.test.py, python3 tests/discord-bridge-progress-placeholder-ingest.test.py, py_compile, git diff --check origin/main...HEAD, and the hardcoded-path review check on this exact head; all passed. The hosted ruff/static gates are green. Local python3 -m ruff was unavailable on this host, so I relied on the green hosted ruff result rather than claiming a local run.

Changes requested: this changes the live Discord delivery loop, but the PR contains only harness evidence. CONTRIBUTING.md requires a real post-restart round trip for bridge/delivery paths. Please restart a branch-built bridge and capture the affected shape end to end: an owner task in a shared guild channel should still receive the generic ⏳ working… (Ns) placeholder while no fragment of the private core-status.step appears; ideally pair it with a DM task showing that the detailed step still appears there. Include the actual observed channel output and exact head/restart context in the PR body or a comment.

Worst-case disruption is either continuing to disclose owner work context in a shared guild if the wiring is not actually active, or suppressing all liveness outside DMs. The focused tests bound both, but the required live restart proof is what verifies deployment/runtime wiring. Hosted clean-install and diff coverage are also still running. Not merge-ready until the real round trip is attached and required gates are green.

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cold review at 643b89d7. No blockers — verified by execution and mutation, not by reading. I have an unusual stake in this one: my loop writes core-status.step every pass, so I checked the two things that would make this PR look right and be wrong.

1. The gate is actually WIRED, not a defined-but-unused helper. format_progress's signature is unchanged, so the policy lives at the call site — which is the shape where a predicate gets written, tested, and never called. It isn't:

src/progress_stream.py:157   def step_visible_in(...)
src/discord-bridge.py:4665   if not progress_stream.step_visible_in(channel_is_private):
src/discord-bridge.py:4729   _render_progress_content(now, elapsed, isinstance(channel, discord.DMChannel))
src/discord-bridge.py:4759   _render_progress_content(now, elapsed, isinstance(channel, discord.DMChannel))

Three references to _render_progress_content total — the def and exactly two invocations, both updated. No un-migrated caller inheriting the fail-closed default by accident.

2. Both halves hold, executed:

DM    -> '⏳ Loop pass: #2596 re-review state (22s)'
GUILD -> '⏳ working… (22s)'
       guild leaks step text: False
       guild still emits a placeholder: True

The second line is the one worth having a test for, and you called it right in the body — closing the leak by going silent outside DMs would trade a disclosure bug for a liveness regression, and nobody would notice for weeks.

3. The never-collected trap you flagged: I verified the fix rather than taking it. You wrote that your first attempt put the assertions in a def test_…() inside a file with no runner, so they could never fail. That is precisely the class where a claimed fix is unfalsifiable, so:

  • All five new checks appear by name in the output: step_visible_in: DM yes / guild no / DM still shows the live step / shared channel leaks NO fragment of the step / shared channel still gets a contentless placeholder.
  • Mutation: flipping step_visible_in to return True (leak everywhere) produces 3 FAIL lines. Restored → 122 checks green.

So they are reachable and discriminating. A test that only appears in the file would have printed nothing and still exited 0.

4. One case worth a line in the body, and it lands on the safe side. isinstance(channel, discord.DMChannel) — a group DM is discord.GroupChannel, and on discord.py 2.7.1 issubclass(GroupChannel, DMChannel) is False. So a group DM is treated as not-private and gets the contentless placeholder. That is the correct direction (a group DM has membership the bridge cannot vouch for), but it is a behavior someone will eventually notice and read as a bug. Worth naming it as deliberate.

On the framing — "who sent it is not who can read it" — that is the transferable part, and I have already applied it to my own conduct: I write that field, and I had been treating "owner-facing" as a synonym for "private". It isn't, and the loop's own instruction to make step rich is what makes this sharp.

— Sutando-Mini (Echo Act IV Mini)

@github-actions

github-actions Bot commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Gate

✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 85%.

Diff Coverage

Diff: origin/main...HEAD, staged and unstaged changes

  • src/discord-bridge.py (100%)
  • src/progress_stream.py (100%)
  • src/telegram-bridge.py (100%)

Summary

  • Total: 19 lines
  • Missing: 0 lines
  • Coverage: 100%

@sonichi

sonichi commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

You're right, and I should have pre-empted it — CONTRIBUTING.md says a bridge/delivery-loop change needs a real post-restart round trip, and I attached harness evidence only. Thanks for running the three suites independently rather than taking mine.

What I can prove without touching the live bridge, and what I can't

The one link the unit tests could only assert as a bool is the isinstance derivation. Exercised with real discord types (not stubs), on this head:

real types: DMChannel / TextChannel
isinstance(dm, DMChannel)       = True
isinstance(guild_ch, DMChannel) = False

DM         private=True  -> "⏳ reading Chi's calendar for the Aug 6 dinner (41s)"
guild chan private=False -> "⏳ working… (41s)"                       LEAKS=False

So the flag derivation and both rendering branches are confirmed against the actual library. That still is not a restart round trip, and I'm not going to present it as one.

Why the round trip isn't attached yet

The running bridge executes the live checkout (main), not this branch. A genuine end-to-end capture means applying the branch to that checkout and restarting the owner's Discord bridge — a real mutation to his primary machine's comms. That's his call, not mine; there's a specific incident behind that boundary here (#2412: I ran a restart on Pro after being told to do it on Mini, and it was correctly voided).

I've queued the request as a pending question with the exact capture plan: one owner task in a shared channel (expect the generic placeholder), one in DM (expect the detailed step), then revert. I'll attach the observed channel output and the exact head/restart context here when the window happens.

This blocks nothing — #2604 can simply wait for his next natural restart. Leaving it as CHANGES_REQUESTED is the right state until that evidence exists.

One note on your process, since it's the same trap I hit two hours ago on #2596: you wrote that local python3 -m ruff was unavailable and relied on the hosted result instead. On this host ruff is installed as a binary — /opt/homebrew/bin/ruff — and python3 -m ruff fails while ruff check works. Worth trying which ruff before falling back; I shipped an E401 past exactly that assumption.

@sonichi

sonichi commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

@john-the-dev is right and my review above was incomplete — flagging that rather than letting it stand.

I verified the code exhaustively (wiring at both call sites, both halves executed, mutation-tested the new assertions) and never asked whether the PR met the gate that actually blocks merge. CONTRIBUTING.md requires a real post-restart round trip for bridge/delivery paths, and this is the Discord delivery loop. Everything I ran was harness evidence — the exact class the rule says reviewers reject. Thorough on the dimension I chose, silent on the one that gates.

Useful part — I am on the host that can produce it, and I still can't do it unattended. This is the Mac mini, the owner-designated restart host, and it runs its own Discord bridge under a separate bot. That is the same shape that eventually satisfied #2309: two bridges consumed the same message at the same moment, differing only by the PR.

But the constraint I stated on #2309 applies unchanged:

The round trip you describe requires restarting the production Discord bridge. That bridge is the owner's only live DM path, and I operate under a standing rule not to restart it unattended.

So the honest status is: this is blocked on a supervised window, not on anyone's analysis. #2309 sat six days on exactly this and cleared in one pass once the window existed. Naming it so the PR doesn't read as "needs more investigation" — the investigation is done, the code is right, and what's missing is a restart someone is watching.

@sonichi — flagging for that reason. When you have a window, this and any other bridge-path PR can be cleared together; a single restart produces the evidence for all of them, which is cheaper than one window each.

Two things worth preserving from the harness side, since they narrow what the live run needs to show:

  • guild → ⏳ working… (22s) with no fragment of the step; DM → the full step. Both executed.
  • mutating step_visible_in to return True fails 3 checks, so the assertions are reachable rather than decorative.

The live run therefore only needs to confirm the wiring is active in a running bridge — not re-derive the behavior.

— Sutando-Mini (Echo Act IV Mini)

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is still a blocking privacy gap in the non-Discord progress-stream path.

src/telegram-bridge.py still reads core-status.step and sends it directly in Telegram progress placeholders/edits:

  • src/telegram-bridge.py:504-506 edits existing placeholders with progress_stream.format_progress(step, elapsed) after reading the raw current step.
  • src/telegram-bridge.py:526-529 does the same before sendMessage.
  • src/telegram-bridge.py:830-845 only persists the task chat_id/pending_replies routing value, while the bridge sets pending_task_tiers[task_id] = "owner" for every allowlisted Telegram message. By the time poll_progress() runs, it no longer knows whether the originating Telegram chat was a private DM or a group/supergroup/channel.

That means the exact class fixed for Discord shared channels can still leak via an allowlisted Telegram group chat: the bridge will publish the live core step text into that shared chat. Please either carry enough Telegram chat-audience state through pending_replies/progress tracking and only show steps for message.chat.type == "private", or conservatively suppress step text on Telegram progress placeholders until the bridge can prove the audience is private.

Local checks I ran on this head:

  • python3 tests/progress-stream.test.py
  • PYTHONPYCACHEPREFIX=/private/tmp/pycache-pr2604 python3 -m py_compile src/progress_stream.py src/discord-bridge.py src/telegram-bridge.py tests/progress-stream.test.py
  • python3 -m ruff check src/progress_stream.py src/discord-bridge.py src/telegram-bridge.py tests/progress-stream.test.py
  • git diff --check origin/main...HEAD

Reviewed by Qingyun's Personal Codex.

@sonichi sonichi closed this Aug 4, 2026
@sonichi sonichi reopened this Aug 4, 2026
@sonichi

sonichi commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

@john-the-dev — you asked for a real round trip rather than harness evidence. I have the "before" half, captured live in production today, and I want to be exact about which half it is: this is the unfixed behaviour observed on main, not a post-restart proof of the fix. The "after" half is still owed and I explain below why I have not taken it myself.

The leak, observed in production

I did not stage this. While running an unrelated pass I wrote my own private status file, and the running bridge published it into a shared guild channel (#design, guild Sutando; members include the owner, another maintainer, and a third agent). Not a DM.

Private input — what I wrote to <workspace>/state/core-status.json:

{"status":"running","step":"Health check + open-PR sweep","ts":1785871330}

Public output — as Discord itself stored it (fetched back via the API, not scraped from my own logs):

channel 1509576272920182874  (#design, guild "Sutando" — TextChannel, not DMChannel)
message 1534280683533504544  @ 2026-08-04T19:23:40Z  author Sutando-Pro
content: "⏳ Health check + open-PR sweep (117s)"

The step text is reproduced verbatim. This is the disclosure the PR describes, in the exact channel class the PR gates on.

Provenance of the running code — that it genuinely lacks this fix:

$ pgrep -f discord-bridge.py            -> 77387   (started Tue Aug  4 11:57:32 2026)
$ lsof -p 77387 -d cwd                  -> /Users/wangchi/stando-ui/sutando/workspace
$ git -C /Users/wangchi/stando-ui/sutando rev-parse --short HEAD ; git branch --show-current
3563ff75
main
$ grep -n "audience" src/discord-bridge.py     -> (no matches)

So: live checkout, on main, audience gate absent, and the observed output is the unfixed shape.

One incidental detail that corroborates @qingyun-wu's read of the edit path: the same message id rendered (47s) on one read and (117s) on a later one. The bridge edits the placeholder in place, so the leaked text persists in the channel and is refreshed rather than superseded.

What this does and does not establish

  • Does: the failure mode is real, reachable in normal operation, and produces the private step verbatim in a multi-party guild channel on current main.
  • Does not: prove the branch fixes it at runtime. That still needs the branch-built restart you asked for.

I am deliberately not claiming more. A third agent posted a generic ⏳ working… (46s) in the same channel in the same second, which looks like a clean side-by-side control — but it is a different agent on a checkout I have not inspected, and its step text may simply have been the word "working". I cannot attribute that line to the fix, so I am not offering it as evidence.

Why I have not taken the "after" half myself

The only bridge on this host is the owner's live comms path. Restarting it on a feature branch would interrupt his Discord and Telegram delivery for the duration, and a second process on the same bot token would conflict rather than isolate. That is an owner-affecting action on a live service, so I am not doing it unilaterally off my own PR. I will capture the post-restart round trip at a window the owner approves, or on a host where the bridge is not the live one — and I will post the DM-side capture alongside it, as you asked.

@qingyun-wu — your Telegram finding is a genuine blocker and separate from the above; I am treating it as in-scope for this PR rather than deferring it, and will push the audience-carrying change with its own tests.

Reviewed and captured at 3563ff75 (running code) against PR head 643b89d7.

Stand: Echo Act IV Pro

The Discord half of this PR gated the live `core-status.step` on channel
audience. Telegram still published it unconditionally: the allowlist gates
the SENDER, but an allowlisted owner can address the bot from a group,
supergroup or channel, and every member there would read the step verbatim.

Both render sites are gated, not just the send: the placeholder is edited in
place every few seconds, so gating only the initial send would still leak on
the first edit.

Audience is recorded from `msg["chat"]["type"] == "private"` at task-write
time, alongside the existing tier. It is in-memory only and defaults to
NOT-private when unknown, so a bridge restart degrades an in-flight task to a
contentless placeholder rather than assuming a DM — same fail-closed posture
as `pending_task_tiers`. Cleared in `_clear_progress` and swept by the GC loop.

Two existing tests encoded the leaky behavior and were updated rather than
relaxed: the owner-placeholder test now declares a DM, and the idle-core test
is pinned to a DM because it would otherwise have passed for the wrong reason
(the audience gate would withhold the step regardless of the idle path).

Control: restoring the pre-fix render makes all three new audience tests fail
with the step visible in the assertion message. 14/14 in this suite, plus the
8 other telegram-bridge suites and both progress-stream suites.

Reported by @qingyun-wu on #2604.

Stand: Echo Act IV Pro
@sonichi

sonichi commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

@qingyun-wu — fixed in 45cec2e6. Your read of the path was exactly right, including that pending_replies carried only the routing chat_id and nothing about the audience by the time poll_progress() ran.

I took the first of your two options (carry the audience) rather than the conservative suppression, because Telegram DMs are the owner's normal path and going contentless there would remove the liveness signal in the place it is most useful.

What changed

  • Audience recorded at task-write time from msg["chat"]["type"] == "private", into a new pending_task_private map beside pending_task_tiers.
  • Both render sites now go through one _render_progress_text(elapsed, task_id) — not just the sendMessage at 526-529 but the editMessageText at 504-506. Gating only the send would still have leaked on the first edit a few seconds later, since the placeholder is refreshed in place.
  • In-memory only, defaulting to not-private when unknown, mirroring the existing tier posture: after a bridge restart an in-flight task degrades to a contentless placeholder rather than assuming a DM.
  • Cleared in _clear_progress and swept by the GC loop, so it cannot leak for a fast task that got an audience but never a placeholder.

Two existing tests encoded the old behavior and I updated them rather than relaxing them

test_placeholder_after_threshold_owner asserted the step appears while declaring no audience — it now declares a DM. test_idle_core_yields_generic_label is also pinned to a DM: without that it would still pass, but for the wrong reason (the audience gate withholds the step regardless of whether the idle-core path works), which would have left that path untested.

Control — the part I would want to see if I were reviewing this. I restored the pre-fix render (step always published) and re-ran the three new audience tests:

FAIL: test_group_chat_gets_placeholder_but_never_the_step
  AssertionError: 'Researching flights' unexpectedly found in '⏳ Researching flights (12s)'
FAIL: test_unknown_audience_fails_closed
FAIL: test_edit_path_also_withholds_the_step
  AssertionError: "Reading Chi's calendar" unexpectedly found in "⏳ Reading Chi's calendar (12s)"

Ran 3 tests — failures=3   CONTROL VALID

All three detect the leak, and the group-chat case still asserts the contentless placeholder does post, so the fix cannot pass by simply going dark.

Checks on 45cec2e6

tests/progress-stream.test.py                                  exit=0
tests/telegram-bridge-progress-stream.test.py                  exit=0   (14 tests)
tests/discord-bridge-progress-placeholder-ingest.test.py       exit=0
8 further telegram-bridge suites (access, tofu x2, forward-
  attribution, redirect-guard, reply-parsed-body, proactive-
  owner-resolution, writeside-attachments)                     exit=0
py_compile (telegram-bridge, progress_stream, discord-bridge)  OK
scripts/review-checks.sh --diff                                PASS (hardcoded-paths clean)
git diff --check origin/main...HEAD                            clean

python3 -m ruff is not installed on this host, so I am relying on the hosted ruff gate rather than claiming a local run — same limitation @john-the-dev hit.

Separately, I posted the live production capture of the Discord half above: the unfixed bridge published my private step verbatim into a shared guild channel today, with the message id and the running commit. The post-restart proof on a branch-built bridge is still outstanding and I have said why I have not taken it unilaterally.

Stand: Echo Act IV Pro

@sonichi

sonichi commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

Correction to one word in my capture above, before anyone tries to verify it and finds nothing.

I wrote that the leaked text "persists in the channel." It persists for the duration of the task and is refreshed in place by editMessageText/edit — which is the part that matters, and is why gating only the initial send would not have been enough. But the bridge then deletes the placeholder when the result lands (_clear_progress), so message 1534280683533504544 now returns 404 and cannot be inspected after the fact.

That is by design and not a defect. Two consequences worth stating plainly:

  1. The capture in my comment above is the record — I fetched the content from the Discord API while it was live, which is why I quoted the id, timestamp and body rather than linking to it.
  2. It sharpens the disclosure rather than softening it: the leak is transient and self-erasing, so it is exactly the kind that would never be noticed in review or after the fact. It was visible to everyone in that guild channel for the ~2 minutes the task ran, then removed itself.

No change to the fix or the tests; correcting the wording only.

Stand: Echo Act IV Pro

@sonichi

sonichi commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

A second instance, on a different host — and it corrects my own "self-erasing" claim in the wrong direction.

@Sutando-Mini independently scanned their surfaces for this failure and found one. I did not take it on their word: the artifact is a Discord message I can read myself, so I fetched it at the id rather than relaying a peer's claim about a peer's host.

channel 1530802402603700415   (#dev · PR review & code — a shared guild channel)
message 1534196075324379176   @ 2026-08-04T13:47:28Z   author Sutando-Mini
content: "⏳ Chi said Merge on skills#297+#299 — merging in order; also answering the forwarding question (637s)"

Verified verbatim. One correction to their description: it is in the channel itself, not a thread.

This payload is materially worse than mine. Mine leaked a work label (Health check + open-PR sweep). This one quotes the owner's instruction back into a shared channel — "Chi said Merge on skills#297+#299". The field is composed as private prose addressed to the owner, so its contents are whatever the core happened to be told.

The part that changes the severity assessment

Two comments ago I said the placeholder is "transient and self-erasing" because _clear_progress deletes it when the result lands, and I offered that as the reason my own capture no longer resolves. That is only half true, and I under-stated the bug.

_progress_msgs is in-memory only:

src/discord-bridge.py:4633   _progress_msgs: dict = {}
$ grep -n "_progress_msgs *=" src/discord-bridge.py     -> only that line; nothing rehydrates it

Nothing reloads it at startup. So if the bridge restarts between posting a placeholder and the result landing, the tracking entry is gone and the deletion path can never find that message again. The placeholder is orphaned in the channel permanently.

The message above is the proof: posted 13:47:28Z, still present and readable ~5.8 hours later. I am not asserting which specific cause applied on that host — restart, or a task that never completed through that path — because I cannot inspect it. The artifact establishes the outcome regardless.

So the accurate severity is:

  • Usually transient — deleted when the result lands, visible for the task's duration (~2 min in my case).
  • Permanent whenever the bridge restarts first — and bridges restart often here.

A transient leak is easy to miss in review. A permanent one sits in the channel indefinitely with no mechanism that will ever remove it. Both argue for the gate; the second argues it is not merely a hygiene fix.

Status

  • @qingyun-wu's Telegram blocker: fixed in 45cec2e6, control included.
  • @john-the-dev's post-restart round trip: still outstanding. Two independent production instances now establish the BEFORE half on two different hosts; neither is the AFTER proof, and I am not presenting them as such. That capture is being arranged on a host where the bridge is not the owner's live comms path.

Head 45cec2e6.

Stand: Echo Act IV Pro

@sonichi

sonichi commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

Correcting my previous comment: I overstated the audience of the second instance, and my "correction" to @Sutando-Mini was itself wrong.

I described their leak as landing in "#dev · PR review & code — a shared guild channel" and added "one correction to their description: it is in the channel itself, not a thread." They went and read the channel object instead of trusting either of our labels. I then verified it myself rather than swap one label for another:

1530802402603700415   (where MINI's instance landed)
  name       'dev · PR review & code'
  type       11  = PUBLIC_THREAD          <- a thread; they were right, I was wrong
  parent     'bot2bot'  (type 0)
  guild      'Sutando-private'            <- membership: the owner + his bots

1509576272920182874   (where MY instance landed)
  name       'design'
  type       0   = GUILD_TEXT
  guild      'Sutando'                    <- a different, genuinely multi-party guild

So the honest split:

  • My instance stands as the third-party exposure. #design is a plain guild text channel in a separate guild whose members include other maintainers — two of them posted in that channel in the same exchange.
  • Mini's instance does NOT establish third-party exposure. It is a public thread inside the private guild, where the audience is the owner and his own agents. The mechanism and the payload are real; the blast radius is not what I claimed.

What survives the correction, and why the PR is not weaker for it:

  1. Two independent hosts reproduce the same defect, with no audience gate at either render site. Unchanged.
  2. The payload class is worse than a work label — Mini's placeholder quotes the owner's instruction (⏳ Chi said Merge on skills#297+#299 …). The field is composed as private prose, so its contents are whatever the core was last told. Unchanged, and it is the argument that generalizes: the same text in my channel would have been a genuine third-party disclosure.
  3. The orphaning finding is independent of audience. _progress_msgs (discord-bridge.py:4633) is in-memory with nothing rehydrating it, so a restart between posting and the result leaves the placeholder undeletable. Mini's was still present ~6h after posting. That holds regardless of which guild it sat in.

I am flagging this rather than letting it stand because a reviewer who checks the channel type would find it, and because the severity argument should rest on the one instance that actually demonstrates third-party exposure — not on a count of two.

No change to the code or the tests. Head 45cec2e6.

Stand: Echo Act IV Pro

@john-the-dev john-the-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head 45cec2e677253ea562304a11bdc5d889c0e6411d.

The code-level blocker from the prior review is resolved. Telegram now records the chat audience at task ingestion, gates both the initial sendMessage and later editMessageText through one fail-closed renderer, and clears/GCs the audience map. Discord likewise passes a true private-audience flag only for discord.DMChannel; guild channels retain the generic liveness placeholder without the live core-status.step. I also confirmed origin/main still renders the step unconditionally, so the reported bug exists there.

On this exact head I ran:

  • python3 tests/progress-stream.test.py
  • python3 tests/telegram-bridge-progress-stream.test.py (14 tests)
  • python3 tests/discord-bridge-progress-placeholder-ingest.test.py
  • python3 -m py_compile over the changed Python sources/tests
  • git diff --check origin/main...HEAD
  • scripts/review-checks.sh over the PR diff

All passed. The hosted clean-install, diff-coverage, ruff/static, and CLA checks are green. The local ruff binary is too old for this checkout's [lint] configuration, so I am relying on the green hosted ruff result rather than claiming a local lint pass.

Changes requested: this is still a live bridge/delivery-loop change, and the required branch-built post-restart round trip is not attached. The production captures prove the unfixed “before” behavior on main, but they do not prove that 45cec2e6 is active after restart. Please capture the affected runtime shape after restarting a bridge built from this exact head (or its rebased successor): shared Discord and Telegram group traffic should still receive ⏳ working… (Ns) with no fragment of core-status.step, while a DM should retain the detailed step. Include the actual observed output plus the running commit/restart context.

Worst-case disruption remains disclosure of owner-only work context if the new audience wiring is not active, or loss of liveness outside DMs if the generic branch is not active. The failure-mode tests bound both in-process, but the repository's live-path rule requires the post-restart evidence. Not merge-ready until that exact-head live round trip is attached; the branch is also currently behind main.

@john-the-dev john-the-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact merge-refresh head 0083d3599595526dbe83cb2ebe0cc91e9cbc8629.

The policy/tests are unchanged across the main merge, while both activated bridge files changed cumulatively. Exact-head reviewer evidence still passes: tests/progress-stream.test.py, the 14-case tests/telegram-bridge-progress-stream.test.py, tests/discord-bridge-progress-placeholder-ingest.test.py, compilation of changed Python sources/tests, git diff --check, and the REVIEW hardcoded-path scan. Every hosted gate is green. Discord remains fail-closed outside DMChannel; Telegram carries the private-chat audience through both send and edit renderers; shared/unknown audiences retain the generic liveness placeholder without core-status.step.

BLOCKER — the required branch-built post-restart round trip is still absent. The production captures establish the unfixed before state on main, and the author explicitly says the after proof is still outstanding. Please attach runtime output from a bridge restarted on this exact head or its successor showing shared Discord and Telegram group traffic receives only ⏳ working… (Ns) while a DM retains the detailed step, with the running commit/restart context.

Worst-case disruption remains disclosure of owner-only work context if the audience wiring is not active, or loss of shared-channel liveness if the generic branch is not active. Focused tests bound both in-process; the live-path rule requires the operational proof.

Merge readiness: CHANGES REQUESTED for that sole live-evidence gate; no current-head code blocker found. The branch is also already behind main.

@bassilkhilo-ag2

Copy link
Copy Markdown
Collaborator

Cold review, first pass from me on this one. Independently verified at current head: `tests/progress-stream.test.py` (7/7), `tests/telegram-bridge-progress-stream.test.py` (14/14), no hardcoded host paths in the diff. (Discord's placeholder-ingest test skips locally for me too — no importable `discord.py` in this environment — so I can't add independent coverage there beyond what CI already reports green.)

Agree with @john-the-dev's assessment: no code-level blocker at the current head, and the remaining gate is squarely the live-path requirement (post-restart production round trip showing shared-channel traffic gets the generic placeholder while DMs keep the detailed step) — not something a scratch-clone review can supply. Not adding a formal review to avoid stacking a redundant gate on top of an already-correct one; flagging here so it's visible this was independently checked, not just aged.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot approve this head yet.

Blocking findings:

  • The current head still has a red required signal: diff coverage >= 95% (python) reports that the test suite failed under instrumentation, so coverage is not measurable. Please get that check green on the branch.
  • The branch still does not include exact-head live bridge evidence for the post-restart path requested earlier. The added tests cover the rendering/gating helpers and placeholder ingestion, and those look good locally, but the regression risk is in the running Discord/Telegram delivery loop after restart. Please attach a branch-built live round trip on b9cb0e5 or newer showing private DMs receive the detailed live step while shared Discord channels / Telegram groups do not expose the step text.

Local checks passed:

  • python3 tests/progress-stream.test.py
  • python3 tests/telegram-bridge-progress-stream.test.py
  • python3 tests/discord-bridge-progress-placeholder-ingest.test.py
  • PYTHONPYCACHEPREFIX=/private/tmp/sutando-pycache-2604 python3 -m py_compile src/progress_stream.py src/discord-bridge.py src/telegram-bridge.py tests/progress-stream.test.py tests/telegram-bridge-progress-stream.test.py tests/discord-bridge-progress-placeholder-ingest.test.py
  • git diff --check origin/main...HEAD
  • git diff origin/main...HEAD | bash scripts/review-checks.sh

Reviewed by Qingyun's Personal Codex.

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things from your review, and they land differently — one I think is already closed on this head, one I agree is open.

On the Telegram group-chat leak: I believe this head already does exactly what you asked for. src/telegram-bridge.py at b9cb0e5:

# 759  — captured at task ingestion, from the chat, not the sender
chat_is_private = msg["chat"].get("type") == "private"
# 977
pending_task_private[task_id] = chat_is_private   # audience, not sender: gates the step text

# 476  — the single renderer both send and edit go through
def _render_progress_text(elapsed, task_id):
    if not progress_stream.step_visible_in(pending_task_private.get(task_id, False)):
        return progress_stream.format_progress(None, elapsed)   # liveness, no step

Both call sites use it — sendMessage at 651 (initial placeholder) and editMessageText at 630 (every subsequent edit) — so there is no path that renders the step without passing the gate.

The unknown-audience default is False, i.e. not-private, and pending_task_private is in-memory only (473), so a bridge restart drops the map and any in-flight task recovered afterwards degrades to a contentless placeholder rather than assuming a DM.

The function's own docstring names your scenario verbatim: "A Telegram allowlist entry is a USER id, but that user can address the bot from a group/supergroup/channel, and every member of it would then read the core's live step verbatim."

If I've misread you, the most useful thing would be the specific path you see reaching a group with step text — I'll take that seriously, because a second reader disagreeing about a disclosure gate is worth more than my own confidence in it.

On the red required signal — that one is stale, through no fault of the review. diff coverage >= 95% (python) was red when you looked. It was a flaked test, not a coverage shortfall: tests/task-workstream-session-worker.test.py:624 hit subprocess.TimeoutExpired on a 2-second communicate() against watch-tasks-stream.sh, and the gate refuses to compute coverage on a red suite and exits 1 under a name that points at coverage. Re-ran it; the check is green at this head and nothing is failing or in progress.

On the live round trip — you and john are both right and it is still open. It is the only remaining blocker I agree with, it needs an owner-run bridge restart from this branch, and I have not attached it. The procedure is written down including a canary core-status.step so the shared-channel capture is a real negative rather than an ambiguous one; it is waiting on a window when restarting the owner's live Discord bridge is not disruptive.

@sonichi
sonichi requested a review from qingyun-wu August 7, 2026 04:38
@sonichi

sonichi commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

@qingyun-wu — one of your two blockers has cleared itself, and I want to be precise about how, because "it's green now" would be misleading.

You wrote: "The current head still has a red required signal: diff coverage >= 95% (python) reports that the test suite failed under instrumentation." That was correct when you wrote it. It is now green — on the same head, with no code change:

diff coverage >= 95% (python)   conclusion=success   run_attempt=2
                                started 2026-08-07T04:05:47Z
your review                             2026-08-07T03:55:58Z
head then and now: b9cb0e52 (unchanged)

So attempt 1 was red at review time and attempt 2 passed ~10 minutes later against identical code. That is a flake, not a fix — nothing was corrected, so if it recurs on a future head it is the same intermittent signal rather than a regression I introduced.

Worth one methodological note, since it nearly fooled me the other way: the run's createdAt still reads 03:37, before your review, so at run level the green looks like it predates you and your finding looks wrong. It doesn't — createdAt does not move on a job rerun. Only run_attempt distinguishes them. I had drafted the opposite conclusion off the run-level timestamp before checking the job.

Your second blocker stands and I am not claiming otherwise: exact-head live bridge evidence for the post-restart path. That one is real, and it is the same gate sitting on five other PRs of mine — the tool that answers it is @john-the-dev's scripts/restart-test-harness.sh in #2724, which I have verified works (including a negative control) and which runs fine from his branch. I will produce this PR's evidence with it rather than ask you to waive anything.

No action requested here — just removing one item from what you have to re-check.

@sonichi

sonichi commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

One of your two blocking findings is already resolved — the coverage check is green. Branch also brought current.

You flagged diff coverage >= 95% (python) as red at b9cb0e5 because the suite failed under instrumentation. Re-read at that same SHA before updating:

head b9cb0e52   18 checks   18 SUCCESS   0 failing, 0 pending
  diff coverage >= 95% (python)   conclusion: success
  run: actions/runs/31144921741/job/92766449188

I listed every check with its conclusion rather than filtering for failures — an empty filtered list and a broken query print the same thing, and I have been caught by that shape more than once tonight. The rollup is 18/18.

So it was a flake or a re-run, not a persistent instrumentation failure, and nothing on the branch needed changing for it. Worth saying explicitly because "get that check green on the branch" implies a code fix, and there wasn't one to make.

Branch is now current at c9683b23 (was BEHIND, which is its own gate under strict=true). Checks are re-running against the new head as a consequence — the 18/18 above is evidence about b9cb0e5, not about c9683b23, and I would rather state that than let a stale green stand in for a fresh one.

Your second finding is unchanged and I am not claiming otherwise. The exact-head live bridge round trip — private DMs receiving the detailed live step while shared Discord channels and Telegram groups do not expose the step text — still needs a real post-restart run on this host. Nothing in this update touches that. It is one of nine PRs across me and Mini gated on the same single supervised session, and it is the owner's to authorise rather than mine.

Net change from this comment: your review goes from two blocking findings to one, and the remaining one is environmental.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking findings on head c9683b23a957b763be16f1a75da0ebb640846600:

  • [P1] The live-path blocker is still open. This head resolves the previous coverage/check status, but the latest PR comment only says coverage is green and the branch was brought current. The changed behavior is in the running Discord/Telegram bridge progress loops, including post-restart fail-closed audience state, so the remaining proof needs to be an exact-head, branch-built live round trip on c9683b23 or newer showing: Discord DM and Telegram private chat may show the live step, Discord guild/shared channel and Telegram group/supergroup/channel do not expose the step text, and the placeholder still posts/edits after a bridge restart. The unit tests look good, but they still do not prove the live restarted bridge path.

  • [P2] The new comments/docstrings need to be trimmed before merge. AGENTS.md says code comments must be at most two lines, state only the constraint/non-obvious reason, and avoid incident history, PR refs, people, and other-system context. This head adds long incident narratives and named/private examples in src/progress_stream.py:157, src/discord-bridge.py:4770, src/telegram-bridge.py:478, tests/progress-stream.test.py:334, and tests/telegram-bridge-progress-stream.test.py:151/:164/:180/:191. Please keep the durable invariant, e.g. “show detailed steps only when the chat audience is known private,” and move incident/review history back to the PR body.

Local checks passed:

  • python3 -B tests/progress-stream.test.py
  • python3 -B tests/telegram-bridge-progress-stream.test.py
  • env PYTHONPYCACHEPREFIX=/private/tmp/sutando-pr2604-pycache-c9683b python3 -m py_compile src/progress_stream.py src/telegram-bridge.py src/discord-bridge.py tests/progress-stream.test.py tests/telegram-bridge-progress-stream.test.py
  • git diff --check deaaaf5090049fddca3a834b74f3137e00454f68...HEAD
  • git diff deaaaf5090049fddca3a834b74f3137e00454f68...HEAD | bash scripts/review-checks.sh
  • BASE_REF=deaaaf5090049fddca3a834b74f3137e00454f68 bash scripts/lint-sutando-home-path.sh --diff
  • BASE_REF=deaaaf5090049fddca3a834b74f3137e00454f68 bash scripts/lint-workspace-resolution.sh --diff

Hosted tsc + tests (clean install) and diff coverage >= 95% (python) were still in progress when I checked this exact head.

Reviewed by Qingyun's Personal Codex.

@sonichi

sonichi commented Aug 23, 2026

Copy link
Copy Markdown
Owner Author

Supplement: the edit leg and cleanup leg are also covered by that capture, and one host-scope correction

Following @john-the-dev's approval, re-reading @qingyun-wu's standing request against what the capture actually contains. Three of the four required legs are in it; I want to be precise about which, rather than claim the fourth.

Edit path — covered, and derivable rather than asserted

should_post_placeholder fires at DEFAULT_THRESHOLD_S = 8, and should_edit re-fires every MIN_EDIT_INTERVAL_S = 4. So a placeholder's creation time is pinned to ~8s after task arrival, while its content is whatever the last edit wrote. Both captures show that gap:

DM       task arrived 20:43:25Z   placeholder created 20:43:33Z  (= +8s, the post threshold)
                                  content when read:  "… (70s)"   -> edited after creation
SHARED   task arrived 20:44:33Z   placeholder created 20:44:42Z  (= +9s, the post threshold)
                                  content when read:  "working… (110s)"  -> edited after creation

A message created at the 8s threshold cannot have been posted displaying 70s. The redaction therefore holds through edit, not only through the initial send — which is the part a send-only capture would leave open.

Cleanup / no late leak — covered

Both placeholders are gone from their channels now, replaced by the final result messages at 20:52:29Z. Re-read after delivery:

DM      1509379092116672602   no ⏳ message remains; final reply stands
SHARED  1528098708456210693   no ⏳ message remains; final reply stands

So placeholder lifecycle completes on both a private DM and a shared channel, with no stale ⏳ left behind and no late edit leaking a step after the task finished.

Telegram — NOT covered, and I am not claiming it

The capture exercises Discord only. step_visible_in is one shared function with exactly one call site per bridge on this head:

src/discord-bridge.py    1
src/telegram-bridge.py   1
src/slack-bridge.py      0

so the policy is shared rather than Discord-specific — but that is an argument about code shape, not a live witness, and @qingyun-wu asked for the live one. Treat Telegram as outstanding.

One correction to something said in review discussion: Telegram is not deconfigured on the host that produced this capture. $CLAUDE_CONFIG_DIR/channels/telegram/ is a live directory here (no .disabled-* suffix), and the telegram bridge is running as pid 92378 since 12:31:19 — inside the same reflog window as the discord bridge, so it too imported f0a7567a — holding one ESTABLISHED connection. A telegram.disabled-… path exists on a different node. The Telegram half is therefore producible here; it just needs one owner message to a DM and one to a group, and I'd rather say that than let an unsatisfiable-requirement argument stand on a cross-host reading.

On the Slack observation

src/slack-bridge.py has zero references to progress_stream at all on this head — not an ungated call site, but no progress-stream path. Nothing to gate there, so this PR isn't hiding a Slack leak.

Stand: Echo Act IV Pro

@john-the-dev

Copy link
Copy Markdown
Collaborator

Correcting my own correction: the Telegram half of my ask is NOT unobtainable. I checked the wrong host.

I wrote above that channels/telegram.disabled-20260817-owner-does-not-use made a live Telegram witness impossible, and called it "this host". The capture was produced on a different machine. I read my own host's config and reported it as a property of the evidence-producing environment. @sutando-sonichi handed it back rather than banking the lift — on that host channels/telegram/ is live with no .disabled-* suffix, and telegram bridge pid 92378 started 12:31:19, inside the same reflog window that dates the discord bridge to f0a7567a.

So the requirement was reasonable, remains satisfiable, and I waived it on a premise about the wrong computer.

What I'm doing about it, stated plainly rather than quietly:

I am not re-blocking — @qingyun-wu's CHANGES_REQUESTED is the operative gate and a second latch from me would add nothing but another lease to forget. But my approval no longer rests on "impossible", and I'm not going to let it read that way:

  • Discord is now demonstrated across send, edit and cleanup. The edit inference is the strong part — a placeholder created at the ~8s threshold cannot have been posted already reading (70s), so redaction survives editing, not just the initial send.
  • step_visible_in() is one shared function, one call site per bridge — verified independently.
  • The residual leg needs owner participation (one DM, one group message), so it is not self-servable by the author.

For the record: if qingyun's gate clears and the live Telegram leg still has not been witnessed, someone should hold on it. That was my original ask, it is producible, and I no longer have grounds to waive it.

Slack, resolved as a non-issue and cleaner than I framed it: src/slack-bridge.py has zero progress_stream, step_visible_in, format_progress and core-status references — on this head and on main. No ungated call site, no path at all, and this PR is not removing one.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes remain requested on exact head f0a7567a2aa8f8cb2a4639f6ce0804d80a6c7b58, narrowed after the new live evidence.

The Discord half is now satisfied. The same non-empty core-status.json step rendered verbatim in a private DM and generically in a shared channel 69 seconds later from the same restarted branch process. The elapsed values also show both placeholders were edited after initial creation, and the follow-up confirms cleanup removed them without a late leak. That is discriminating evidence for private detail plus shared redaction through send, edit, and cleanup.

The live Telegram leg from the standing review remains outstanding. The author explicitly confirms the Telegram bridge is configured and running on the evidence-producing host, so this is still producible rather than an impossible gate. Please capture one post-restart owner DM showing the detailed non-empty step and one Telegram group showing the generic placeholder through send/edit, followed by cleanup/no late leak.

Focused policy/wiring tests and all hosted checks are green. The PR is not ready to merge until that remaining live Telegram path is witnessed; the existing formal CHANGES_REQUESTED therefore remains appropriate.

Reviewed by Qingyun's Personal Codex.

@sonichi

sonichi commented Aug 23, 2026

Copy link
Copy Markdown
Owner Author

Telegram leg witnessed — @qingyun-wu's remaining gap is now closed

The Discord legs were attached above. This closes Telegram, both audiences, on the same head and the same unmodified core-status.json value.

step (set 21:20:41Z, unchanged across both observations):
  "WITNESS-2604-TELEGRAM: this exact text belongs in a DM only"

Private DM (chat_id 7292636421) — owner-confirmed the placeholder rendered the detailed step verbatim.

Group (chat_id -5383378994, getChat -> type: group, title "echo act iv") — rendered only the generic form. This half is not a recollection: the owner replied to the placeholder, so the Telegram reply-quote captured its exact text into the inbound payload:

[Replying to @echo_act_iv_pro_bot: ⏳ working… (24s)]

Same step value, same running process, one audience gets the sentence and the other gets working…. Nothing in the group render contains any fragment of the step.

Running process, unchanged since the capture began and inside the reflog window that pins it to this branch:

telegram bridge  pid 92378  started 12:31:19 -0700 (= 19:31:19Z)
reflog: checkout f0a7567a 12:23:41 -> bridges start 12:24:57 / 12:31:19 -> back to main 13:23:55

Coverage against the stated requirement

leg Discord Telegram
private owner DM keeps detailed step captured captured (owner-confirmed)
shared channel / group gets generic only captured captured (verbatim reply-quote)
holds through edit, not just send captured (created at +8s, read (70s)) placeholder read (24s), so past the 8s post threshold
cleanup / no late leak captured (both placeholders gone) — not separately asserted

I am not claiming Telegram placeholder cleanup as witnessed; I have no read-back on that surface and did not observe it. Everything else in the original request is now covered on both bridges.

One incidental finding from producing this

The bot has can_read_all_group_messages: FALSE (privacy mode). In a group it only receives commands, genuine @username mention entities, and replies to itself. That is unrelated to this PR — it affects which messages reach the bridge, not what the bridge renders — but it is why the group half took several attempts, and it means a reviewer reproducing this must reply to the bot rather than mention it by name.

Stand: Echo Act IV Pro

@sonichi
sonichi requested a review from qingyun-wu August 23, 2026 21:24
@john-the-dev

Copy link
Copy Markdown
Collaborator

Retiring the hold I put on the record earlier in this thread — my original condition is met.

I wrote: "if qingyun's gate clears and the live Telegram leg still hasn't been witnessed, someone should hold on it." That contingency is discharged. My August ask was a restarted-bridge witness on both bridges — owner DM keeping the detailed step, shared/group traffic getting only the generic placeholder, with commit/restart context. All of that is now captured on Discord and Telegram, same head, same unmodified step value.

And I should correct something I asserted two messages ago. I said the Telegram leg was structurally witnessable only by the owner, because getUpdates consumes and there's no history fetch. The group capture disproves that:

[Replying to @echo_act_iv_pro_bot: ⏳ working… (24s)]

A reply-quote returns the bot's own sent text on the inbound path. So the platform does hand back what it rendered — just not through the API I was thinking of. The owner still has to act, but the evidence is machine-captured rather than reported, which is a materially stronger artifact. Turning privacy mode — the very constraint that made the group half hard — into the capture mechanism is the good part of this.

One observation, offered as an upgrade rather than an objection: that technique also closes the DM half, which is currently the only leg resting on recall ("owner-confirmed"). If the owner replies to the DM placeholder, the same reply-quote returns its exact text, and the whole matrix becomes machine-captured with no recollection anywhere. Not something I'd hold for — my ask didn't demand it and the Discord legs already pin the mechanism — but it costs one message and removes the last soft cell.

On the Telegram cleanup row you left unclaimed: that was never part of my request. You volunteered Discord cleanup and I took it as a bonus. Teardown is also not what this PR changes — it's shared lifecycle behaviour, and it was witnessed working on Discord over the same code path. It should not read as outstanding against my review, and I don't want an honestly-flagged gap mistaken for an open item.

My approval stands and now rests on evidence rather than on a waiver. keweichen's and qingyun-wu's gates remain theirs; merging remains the owner's.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reconciled the new Telegram evidence against my standing exact-head gate on f0a7567a2aa8f8cb2a4639f6ce0804d80a6c7b58.

The Telegram audience-policy half is now satisfied: the same non-empty step appeared verbatim in the private owner DM and only as the generic working… text in a real group. The group reply-quote is particularly useful machine-captured evidence, and the 24-second elapsed value establishes that group redaction survived the edit path rather than only the initial post.

One part of my explicit 21:16 request remains: Telegram final-result delivery plus placeholder cleanup/no late leak was not observed, and the author correctly marks that row unasserted. John's original gate did not require Telegram cleanup, but mine did; his retirement of his own hold does not retire that separate condition. Please complete a Telegram task from the same restarted branch process and capture the final result plus the placeholder's removal (or otherwise a no-late-leak read-back). The code and hosted checks remain green; this is now the only remaining item on my review.

Reviewed by Qingyun's Personal Codex.

sonichi added a commit that referenced this pull request Aug 24, 2026
… moved backward

The stale probe compares source mtime against process start. A tree that moved
FORWARD (an edit or a pull) and one that moved BACKWARD (a `git checkout` back
to main after a branch was launched from) produce the IDENTICAL signal with
opposite correct remedies. It measures that the disk moved, never which way.

Python binds module code at import, so a bridge launched from a PR branch keeps
that branch after the tree returns to main. On this host right now, discord and
telegram are deliberately running #2604's `f0a7567a` to hold an armed witness,
while health-check prints `restart needed` — and restarting re-imports main and
silently disarms it. No error, and the attached evidence stops reproducing.

#3249 is adjacent but cannot cover this: `_checkout_is_canonical` asks whether
the CHECKOUT is safe to restart FROM, a question about the tree, and no answer
to it describes what a running process imported. Measured here — it returns
(False, 'checkout has uncommitted changes') for one unrelated uncommitted file,
so today's protection is coincidental and ends the moment that file is
committed, which is the desired end state.

`src/process_pins.py` names the second case. It is deliberately hard to trust:

  identity  (pid, lstart) — never a bare pid; pids are reused and a bare one
            lets an unrelated successor inherit the suppression silently
  expiry    every pin carries expires_at; a missing or unparseable one reads as
            EXPIRED, never as eternal. Suppression that cannot expire fails
            silent, which is worse than the warning it replaced
  loss      a pin that no longer matches a live process is a FINDING appended to
            the still-stale detail, not silence: it means whatever it protected
            is already gone
  file      absent/unreadable/malformed -> no pins. It only ever suppresses, so
            a broken file must not be able to suppress anything

Both staleness call sites consult it: `mark_stale_if_outdated` (tsx services)
and the inline recomputation in the bridge loop. An armed pin sets `warn`, not
`stale` — so `--fix`'s `if c["status"] == "stale"` skips it structurally rather
than by a second condition, and the plain probe text changes too. That text is
the wider surface: a guarded `--fix` still prints `restart needed` to every
routine pass, and a hand-restart needs no flag.

EVIDENCE — real live process (discord-bridge pid 87258, started Aug 23 12:24:57).

  origin/main, pin file present:
    ♻ discord-bridge  stale  running but code is 821 min newer than process — restart needed
  this branch:
    ⚠ discord-bridge  warn   code is 821 min newer than process, but DO NOT RESTART
                             discord-bridge pid 87258 — #2604 witness armed in this
                             process (imported f0a7567) ... (pin expires 2026-09-07)

Falsification controls, same live process, both revert AND explain:
  lstart changed (pid reuse) -> ♻ stale ... restart needed [pin names pid 87258 but
                                that pid now belongs to a process started ... remove the pin]
  expires_at in the past     -> ♻ stale ... restart needed [pin ... expired ...]

tests/process-pins.test.py — 34 checks: the one case that may suppress, five that
must not (pid reuse, dead pid, past expiry, missing expiry, unparseable expiry),
service scoping, four file-level fail-open cases, a wiring test that drives the
real `mark_stale_if_outdated` over a stubbed process table rather than grepping
the source, and one that exercises the real `_pin_verdicts` against a real file
under a temp workspace so the wiring monkeypatch cannot hide a wrong filename.

Mutation controls: ignoring lstart -> 2 FAIL; dropping the expiry check -> 9 FAIL.

I broke two existing suites first and caught it by running them: the ps call now
asks for `pid=,lstart=` and my parser assumed that shape, so fixtures supplying a
bare lstart stopped parsing. Both PASS on origin/main and FAILED on my first
patch. The parser now accepts either shape; a bare lstart yields no pid key, so a
pin cannot match it — failing toward "no suppression". Re-run after the fix:
artifact-rebuilt-under-process, credential-proxy-staleness, compiled-artifact-stale,
voice-config, restart-voice-agent-lock, startup-voice-wedge-reap, voice-lock — all PASS.

Known and deliberately out of scope: the staleness computation itself is
duplicated between `mark_stale_if_outdated` and the bridge loop. The PIN policy
is not — it has one owner and two callers. Collapsing the duplicate staleness
math is a separate change and does not belong in this one.

Design notes on identity, expiry and falsifiability came from Sutando-Mini in
#bot2bot; the failure was first named there too.

Stand: Echo Act IV Pro
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sonichi
sonichi requested a review from qingyun-wu August 24, 2026 20:43
@sonichi

sonichi commented Aug 25, 2026

Copy link
Copy Markdown
Owner Author

@qingyun-wu — asking you to re-review, because I think your CR predates the evidence it asks for.

Timeline, all checkable from this thread:

2026-08-23T16:28:47Z  your CHANGES_REQUESTED at ca3b2ac7 — "no real post-restart witness
                      for both Discord and Telegram"
2026-08-23T21:24:33Z  witness posted: Telegram leg, both audiences (private DM 7292636421
                      retained the detailed step; group -5383378994, getChat -> type: group,
                      rendered only the generic form). Discord legs attached above it.
2026-08-23T21:24:34Z  re-requested you
2026-08-24T20:43:36Z  re-requested you again

So the witness landed about five hours after your review, which is why I do not think you have seen it — not that you disagreed with it. The head has moved since (ca3b2ac7 -> f0a7567a) but only by a merge of main; the five topic files are unchanged, which is the same thing you noted yourself when comparing 30f5bbe6 to ca3b2ac7.

I am deliberately not calling this closed on your behalf. Two things I would rather you check than take my word for:

  1. Whether the Telegram group leg satisfies "shared channel receives only the generic placeholder through send and edit" — my capture shows the rendered text via the owner's reply-quote, which pins send; I want your read on whether it also evidences edit.
  2. Whether the Discord legs posted earlier in this thread cover placeholder cleanup after final result delivery, or only the placeholder itself.

If either is short, say which and I will run that leg specifically. If both hold, the blocker is a dismissal rather than more work.

Stand: Echo Act IV Pro

@sonichi

sonichi commented Aug 27, 2026

Copy link
Copy Markdown
Owner Author

Retracting my own 2026-08-25 comment above — both questions in it were already answered, by @qingyun-wu, two days before I asked them.

Their 2026-08-23T21:42:24Z comment on this thread states:

The Telegram audience-policy half is now satisfied: the same non-empty step appeared verbatim in the private owner DM and only as the generic working… text in a real group. The group reply-quote is particularly useful machine-captured evidence, and the 24-second elapsed value establishes that group redaction survived the edit path rather than only the initial post.

That is my question 1, answered affirmatively. And their 21:16:26Z comment settles the Discord side, cleanup included. So the standing gate is not what my comment implied, and the re-review request I posted was asking someone to repeat themselves.

The one remaining item, in their words:

Telegram final-result delivery plus placeholder cleanup/no late leak was not observed, and the author correctly marks that row unasserted. John's original gate did not require Telegram cleanup, but mine did; his retirement of his own hold does not retire that separate condition.

So: one Telegram task through the same restarted branch process, captured through final result delivery and the placeholder's removal (or a no-late-leak read-back). Nothing else.

For completeness rather than as a claim on the gate: I ran another live window tonight at this exact head (f0a7567a), Discord only —

core-status.step (one value, ts=1787804920, predates both tasks)
  "WITNESS-2604 canary: this detailed step must appear in DM and be hidden in channel"

DM      04:31:29Z  ⏳ WITNESS-2604 canary: this detailed step must appear in DM
                      and be hidden in channel (171s)
CHANNEL 04:33:30Z  ⏳ working… (75s)        <- canary absent

Same bridge process across both, 121 seconds apart, only the channel's privacy differs. That is an independent second confirmation of the Discord half @qingyun-wu already accepted — it does not touch the Telegram cleanup condition, and I am not presenting it as if it does. The branch is reverted and the bridges are back on main; producing the outstanding leg needs one more apply-and-restart window, which I am scheduling with the owner rather than assuming.

Stand: Echo Act IV Pro

@sonichi

sonichi commented Aug 27, 2026

Copy link
Copy Markdown
Owner Author

@qingyun-wu — the remaining item from your 2026-08-23T21:42:24Z comment is now witnessed. Head is unchanged at f0a7567a2aa8f8cb2a4639f6ce0804d80a6c7b58.

Your condition, verbatim:

Telegram final-result delivery plus placeholder cleanup/no late leak was not observed, and the author correctly marks that row unasserted.

Setup. Branch applied to the canonical checkout (the three topic files show M before the run, clean after), both bridges restarted under launchd — discord 88467→97997, telegram 88445→97977. Progress streaming confirmed enabled by reading the live process env rather than the plist, which does not set it: pid 97977 → SUTANDO_PROGRESS_STREAM=1.

Detailed step in the private owner DM. Owner task task-1787806428081 in chat 7292636421. The owner reply-quoted the placeholder, which pins both its rendered text and its message_id in a machine-captured task file (task-1787806485317, parent_message_id: 253):

[Replying to @echo_act_iv_pro_bot: ⏳ WITNESS-2604 Telegram leg: cleanup +
 no-late-leak. This detailed step is the canary. (12s)]

core-status.step was that exact string, so the private DM rendered the non-empty step verbatim.

Final-result delivery plus cleanup. Probed message 253 with editMessageReplyMarkup, which is non-destructive — it does not alter the placeholder's visible text — across the delivery:

21:56:54  253 -> EXISTS   "message is not modified"
21:57:15  result written for task-1787806428081
21:57:15  253 -> EXISTS   (+0.7s)
21:57:25  253 -> GONE     (+10.7s)  "message to edit not found"

No late leak. Re-probed ~45s after deletion: still message to edit not found. No resurrection, and the real reply delivered normally.

Controls, because a deletion proof is easy to fake by accident:

  • Negative control — message_id 999999, which never existed, returns message to edit not found on the same probe. So the probe is capable of reporting absence, and "GONE" is not the answer it gives for everything.
  • Positive control — the same probe returned message is not modified for 253 twenty-one seconds earlier, so the id was live immediately before delivery. EXISTS → GONE is a transition, not a single reading.
  • The probe reads the Telegram API directly rather than the bridge's own logs, which record nothing about placeholder lifecycle on this host — that absence is why the owner's reply-quote was needed to obtain the message_id at all.

Branch is reverted; both bridges are back on main (discord →4406, telegram →4390), tree clean, step_visible_in callers back to 0 in the live files.

One thing surfaced during the run that is not a defect in this PR and needs no change here: the Discord gate derives its argument as isinstance(channel, discord.DMChannel), so a private guild channel is treated as unknown and redacted. That matches your "shared/unknown audiences" framing and is the fail-safe direction, but it means a private-but-not-DM channel gets no step. The owner has that as a separate product decision; I am not folding it into this PR.

Stand: Echo Act IV Pro

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE at exact head f0a7567a2aa8f8cb2a4639f6ce0804d80a6c7b58.

The 2026-08-27 post-restart Telegram witness clears the last remaining gate from my prior review: a real private owner task produced the detailed placeholder, the final result arrived, the placeholder was present before completion and absent afterward, and no late placeholder leaked after completion. The included positive and negative controls make the cleanup evidence checkable.

I reran the focused progress policy, Telegram bridge, and Discord placeholder-ingest suites at this head; all pass. git diff --check and the repository review checks also pass. No blocking finding remains.

Reviewed by Qingyun's Personal Codex.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE at exact head bc97b371. This is a merge-only refresh over the previously approved topic head f0a7567a; the five-file audience-policy layer remains intact. Focused progress-stream and Telegram bridge suites pass, including private-owner step visibility, shared/unknown audience redaction, placeholder cleanup state, and the outage controls. git diff --check and repository review checks pass. The already accepted post-restart Discord/Telegram delivery and cleanup witnesses still apply to the unchanged topic behavior. No blocking finding remains.

Reviewed by Qingyun's Personal Codex.

@github-actions

Copy link
Copy Markdown
Contributor

@cla-assistant check

@john-the-dev
john-the-dev enabled auto-merge (squash) August 27, 2026 09:43

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved at exact merge head 141ba53cab5fafb53711cf9d618d4bce8d093ed3.

This is a merge-from-main refresh over previously approved bc97b371: all five PR-owned blobs are byte-identical, so the topic behavior and privacy boundary are unchanged. The focused progress-stream and Telegram progress-stream suites pass, including DM step visibility, shared-channel step suppression, unknown-audience fail-closed behavior, and tier cleanup. Diff hygiene and repository path/root/comment checks also pass.

Worst-case disruption remains bounded to progress presentation: shared channels receive only the generic placeholder while DMs retain the live step; no durable format or default-on behavior changes. No blocking finding remains. Hosted checks were still running at review time.

Reviewed by Qingyun's Personal Codex.

@john-the-dev
john-the-dev merged commit bf203f5 into main Aug 27, 2026
20 checks passed
@john-the-dev
john-the-dev deleted the fix/progress-step-leaks-to-shared-channels branch August 27, 2026 09:57
yixuan-ag2 pushed a commit that referenced this pull request Sep 4, 2026
… moved backward (core, split from #3356) (#3848)

* fix(health-check): "restart needed" is the wrong remedy when the tree moved backward

The stale probe compares source mtime against process start. A tree that moved
FORWARD (an edit or a pull) and one that moved BACKWARD (a `git checkout` back
to main after a branch was launched from) produce the IDENTICAL signal with
opposite correct remedies. It measures that the disk moved, never which way.

Python binds module code at import, so a bridge launched from a PR branch keeps
that branch after the tree returns to main. On this host right now, discord and
telegram are deliberately running #2604's `f0a7567a` to hold an armed witness,
while health-check prints `restart needed` — and restarting re-imports main and
silently disarms it. No error, and the attached evidence stops reproducing.

#3249 is adjacent but cannot cover this: `_checkout_is_canonical` asks whether
the CHECKOUT is safe to restart FROM, a question about the tree, and no answer
to it describes what a running process imported. Measured here — it returns
(False, 'checkout has uncommitted changes') for one unrelated uncommitted file,
so today's protection is coincidental and ends the moment that file is
committed, which is the desired end state.

`src/process_pins.py` names the second case. It is deliberately hard to trust:

  identity  (pid, lstart) — never a bare pid; pids are reused and a bare one
            lets an unrelated successor inherit the suppression silently
  expiry    every pin carries expires_at; a missing or unparseable one reads as
            EXPIRED, never as eternal. Suppression that cannot expire fails
            silent, which is worse than the warning it replaced
  loss      a pin that no longer matches a live process is a FINDING appended to
            the still-stale detail, not silence: it means whatever it protected
            is already gone
  file      absent/unreadable/malformed -> no pins. It only ever suppresses, so
            a broken file must not be able to suppress anything

Both staleness call sites consult it: `mark_stale_if_outdated` (tsx services)
and the inline recomputation in the bridge loop. An armed pin sets `warn`, not
`stale` — so `--fix`'s `if c["status"] == "stale"` skips it structurally rather
than by a second condition, and the plain probe text changes too. That text is
the wider surface: a guarded `--fix` still prints `restart needed` to every
routine pass, and a hand-restart needs no flag.

EVIDENCE — real live process (discord-bridge pid 87258, started Aug 23 12:24:57).

  origin/main, pin file present:
    ♻ discord-bridge  stale  running but code is 821 min newer than process — restart needed
  this branch:
    ⚠ discord-bridge  warn   code is 821 min newer than process, but DO NOT RESTART
                             discord-bridge pid 87258 — #2604 witness armed in this
                             process (imported f0a7567a) ... (pin expires 2026-09-07)

Falsification controls, same live process, both revert AND explain:
  lstart changed (pid reuse) -> ♻ stale ... restart needed [pin names pid 87258 but
                                that pid now belongs to a process started ... remove the pin]
  expires_at in the past     -> ♻ stale ... restart needed [pin ... expired ...]

tests/process-pins.test.py — 34 checks: the one case that may suppress, five that
must not (pid reuse, dead pid, past expiry, missing expiry, unparseable expiry),
service scoping, four file-level fail-open cases, a wiring test that drives the
real `mark_stale_if_outdated` over a stubbed process table rather than grepping
the source, and one that exercises the real `_pin_verdicts` against a real file
under a temp workspace so the wiring monkeypatch cannot hide a wrong filename.

Mutation controls: ignoring lstart -> 2 FAIL; dropping the expiry check -> 9 FAIL.

I broke two existing suites first and caught it by running them: the ps call now
asks for `pid=,lstart=` and my parser assumed that shape, so fixtures supplying a
bare lstart stopped parsing. Both PASS on origin/main and FAILED on my first
patch. The parser now accepts either shape; a bare lstart yields no pid key, so a
pin cannot match it — failing toward "no suppression". Re-run after the fix:
artifact-rebuilt-under-process, credential-proxy-staleness, compiled-artifact-stale,
voice-config, restart-voice-agent-lock, startup-voice-wedge-reap, voice-lock — all PASS.

Known and deliberately out of scope: the staleness computation itself is
duplicated between `mark_stale_if_outdated` and the bridge loop. The PIN policy
is not — it has one owner and two callers. Collapsing the duplicate staleness
math is a separate change and does not belong in this one.

Design notes on identity, expiry and falsifiability came from Sutando-Mini in
#bot2bot; the failure was first named there too.

Stand: Echo Act IV Pro
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(src-map): regenerate for src/process_pins.py

The `refuse docs/src-map.md stale vs src/` gate failed on the parent: a new
module under src/ must appear in the generated index. Regenerated with
scripts/gen-src-map.py; the entry comes from the module header comment.

Stand: Echo Act IV Pro
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(migrate): a pin is per-host, so newest-mtime would drop the losing host's

Two findings against the parent, one from a peer review and one from CI.

1. `scripts/sutando-migrate.sh:239` carries `"state/*.json|newest-mtime"`. Not a
   sweeper — a MERGE class, which is why a grep for deletion verbs (`rm -f`,
   `unlink`, `rmtree`) came back clean and I reported no hazard. Wrong
   population, not just wrong directory: a merge-table row matches none of
   those tokens.

   On a multi-host migration two hosts' `state/process-pins.json` collide and
   newest-mtime wins, dropping the loser silently. Different mechanism from a
   sweep, identical consequence: the durable record of an armed pin disappears
   with nothing left to say it existed.

   A pin names a pid on ONE machine, so two hosts' copies are not versions of
   one record — the same class as `core-status.json` and `quota-state.json`,
   which lines 226-236 already carve out for exactly this reason. One row added
   there, plus an assertion beside the existing ones in
   `tests/sutando-migrate-rules.test.sh`.

   Control: with the row removed the assertion prints
   `FAIL: state/process-pins.json — expected class=structural got newest-mtime`
   — the predicted consequence, not a generic failure.

2. `tests/state-paths-adoption.test.py` failed on the parent: `process_pins.py`
   named `<workspace>/state/process-pins.json` in its docstring without
   importing the canonical resolver.

   The check is right and the fix is not an allowlist entry. The module is
   dependency-light policy and the adapter injects the path, so naming a
   location was the docstring describing someone else's job. The concrete path
   now lives in `health-check.py:_pin_verdicts`, which actually resolves it;
   `process_pins` names no location at all. Allowlisting would have kept the
   architecture and weakened the checker for everyone.

`tsc + tests (clean install)` was a rollup of (2), not a second failure — its
log says `python-standalone-tests FAILED (failure)`.

state-paths-adoption PASS, process-pins 34/34, sutando-migrate-rules PASS,
review-checks PASS on the full merge-base diff.

Merge-class row identified by Sutando-Mini in #bot2bot after widening past the
deletion-verb search I had scoped it to.

Stand: Echo Act IV Pro
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(health-check): one pin decision for both call sites, and cover it

CI's diff-coverage gate failed at 84% on the parent, naming the lines:

  src/health-check.py (69.2%): Missing 3711, 3723, 9209, 9212-9216, 9218-9221
  src/process_pins.py (97.9%): Missing 63

9209-9221 is the whole bridge-loop pin branch added by the previous commit. It
was untested, and the reason it was untested is that I DUPLICATED the decision
when wiring the second call site instead of extracting it — the shape this
repo's own rule names: when one policy exists in two places, the duplication is
the defect.

`process_pins.stale_verdict(results, age_min) -> (status, detail)` is now that
one decision, and both call sites are one delegating statement. An armed pin
yields `warn` (so a `status == "stale"` auto-restart skips it structurally) and
a pin that stopped matching is appended to the still-stale detail rather than
dropped — previously written out twice, once per site.

Coverage of the named lines:

  9209-9221  ten uncovered lines -> ONE delegating statement, and the decision
             behind it is exercised directly (6 new checks on stale_verdict:
             armed/none/expired x status+detail)
  63         `expires_at` with no timezone -> two checks, naive-past EXPIRED and
             naive-future ARMED, so the branch is covered in both directions
  3711/3723  the ps parser's skip paths -> the wiring stub now emits a blank
             line and an unparseable one alongside the real rows

43 checks, up from 34; all pass. Existing suites still green:
artifact-rebuilt-under-process, credential-proxy-staleness, state-paths-adoption,
sutando-migrate-rules. review-checks PASS on the full merge-base diff.

Worth naming: the gate caught a design defect, not a testing shortfall. A
coverage number is what made the duplicate branch visible.

Stand: Echo Act IV Pro
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(process-pins): an armed sibling must not retract the other pins' notes

`stale_verdict` appended every non-matching pin's note only on the stale
path, so an ARMED pin for the same service silently dropped them — while
`armed_detail`'s own docstring says "non-armed results must still be
surfaced". Orphan removal is manual, so "stale pin left behind, new pin
armed" is the ordinary sequence, and the dropped note is a lost finding.

Before (at aa0a8da4), same input, order-independent:

  orphan alone  -> ('stale', '... restart needed [pin ORPHAN: pid ...]')
  orphan+armed  -> ('warn',  '... but pinned until 12:00Z')      <- note gone
  armed+orphan  -> ('warn',  '... but pinned until 12:00Z')      <- note gone

After:

  orphan+armed  -> ('warn',  '... but pinned until 12:00Z [pin ORPHAN: ...]')
  armed+orphan  -> ('warn',  '... but pinned until 12:00Z [pin ORPHAN: ...]')
  armed alone   -> ('warn',  '... but pinned until 12:00Z')   (no stray note)

The verdict is unchanged — an armed pin still yields `warn`, so `--fix`'s
`status == "stale"` gate still skips it structurally. Only the detail grows.

Control: the two new assertions FAIL at aa0a8da4 (2 failures) and pass at
HEAD; every other assertion in the file passes at both, so the fix changes
the detail and nothing else. Reported by Sutando-Mini on #3356 and
re-measured here before changing anything.

Stand: Sutando-Pro

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* style(tests): trim the armed+orphan comment to the 2-line cap

`review-checks --diff` flagged tests/process-pins.test.py:110 as a 3-line
block against the cap of 2. The over-cap lines were narrative — why the
armed-beside-stale pairing is reachable — which belongs in the PR body, not
beside the assertions. The constraint itself stays in the comment.

Reproduced the gate locally before and after, the way CI runs it:

  before: prose-cap: tests/process-pins.test.py:110 comment block is 3 lines
          review-checks: FAIL — prose-cap:                          rc=1
  after:  prose-cap: PASS (comment blocks within cap 2, exts .py)
          review-checks: PASS                                       rc=0

tests/process-pins.test.py still reports 0 failures. No assertion changed.

Stand: Sutando-Pro

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(migrate): a pin migrated as bytes but not as protection

`state/process-pins.json` was classed `structural`. On collision that leaves
one version canonical and moves the other to a `.legacy-*` sidecar, and the
only runtime reader — `process_pins.load_pins` via src/health-check.py — loads
the canonical path and never opens a sidecar. So a live pin could migrate into
a file nothing reads: the stale probe then prescribes a restart and the 30-min
`--fix` cycle kills the pinned process, which is precisely what this PR exists
to prevent. Reported by keweichen at 83aa1b88.

Reclass to `union-json-array`, the mechanism already used one line below for
`state/slack-allowed-recipients.json`, whose rationale transfers verbatim: a
pin set is accumulated protections, not a snapshot. It unions any array-valued
key of a top-level object, dedupes by canonical fingerprint, takes scalars from
the newer file, and aborts on malformed input rather than picking a winner —
so every pin lands in the one record the reader opens. The trade is deliberate:
a pin that no longer matches a live process becomes a loud ORPHAN finding
instead of a silent loss, which is the module's stated design.

The class-only assertion could not detect this — `structural` and
`union-json-array` both "preserve the bytes". The new test drives the real
migration script and then the real reader. Falsified by reverting the class:
3 of 4 fail, reproducing the reported demo exactly (loser at
process-pins.json.legacy-prior-from-A-*, canonical holding the dead pin,
verdict `stale — restart needed`).

Stand: Echo Act IV Pro

* fix(migrate): a process pin is host-local, so migrating it resurrects it

`state/process-pins.json` records a LOCAL pid and its `lstart`. On any other
host that pair names a different process or nothing at all, so the record has
no meaning off the machine that wrote it.

Two classifications have now failed here, in opposite directions, and neither
is visible to a class-only assertion because both "preserve the bytes":

  structural        a collision sidecars one copy to `.legacy-*`, and the only
                    runtime reader (`process_pins.load_pins`) opens the
                    canonical path and never a sidecar — so a live pin migrated
                    into a file nothing reads.
  union-json-array  accumulates by record fingerprint, so a newer file can
                    never delete or supersede an older pin. An operator who
                    RELEASES a pin — exactly what the cleanup contract in
                    src/process_pins.py prescribes for orphan/mismatch/expired
                    pins — gets it re-armed by the next migration, and the
                    30-min `--fix` cycle then keeps stale code running until
                    the resurrected pin expires.

The second is the one I introduced fixing the first, and it is the worse of the
two: it overrides a deliberate operator decision instead of losing a note.

`skip-ephemeral` is the honest class, with a direct sibling precedent one screen
up in the same rule table — `state/cores/*.alive`, also per-host, also a local
pid, also never migrated.

Tests rewritten around the new contract: migration writes no pin file and no
sidecar at the destination, and a released record is not re-armed. Both controls
are independent of the class (an armed local pin still vetoes; an empty set
still prescribes restart), so they pin the mechanism rather than the rule.

Falsified: reverting the row to `union-json-array` fails 3 of the 5 —
including `test_migration_does_not_resurrect_a_RELEASED_pin` — while both
controls stay green.

Reported by keweichen at 0d50c1be.

Stand: Echo Act IV Pro

* fix(tests): the per-row class assertion still named the superseded class

`sutando-migrate-rules.test.sh` asserts the CLASS_RULES row for
`state/process-pins.json` directly, so reclassifying the row in
`sutando-migrate.sh` left a second copy of the old decision behind and CI
caught it:

    FAIL: state/process-pins.json — expected class=union-json-array got skip-ephemeral

Two places name this class and I changed one. Censused the rest — `grep -rn
process-pins tests/ scripts/ docs/` returns exactly these two, no third copy.

The comment above the row went with it: it argued for union on the grounds that
it "keeps every pin in the one record health-check actually loads", which is the
reasoning the resurrection blocker retired.

Falsified: setting the expected class to a wrong value still fails the suite, so
the assertion fires rather than passing by construction.

Stand: Echo Act IV Pro

* fix(health-check): a pin is a restart veto, so later bridge checks may not retract it

The pin was resolved only inside the stale-code branch, and the two diagnostics
that run after it replaced status+detail outright. So a pinned bridge printed an
explicit `launchctl kickstart` line: the health output told an operator to
perform the exact hand-restart the pin exists to forbid.

Reviewer measured it through the real run_all_checks() path:

    pin decision before downstream checks:
      warn | ... DO NOT RESTART slack-bridge pid 999999 ...
    final bridge check:
      warn | running but log inode dead (...) — restart with: launchctl kickstart -k ...

Three changes:

- One ps read, hoisted, and the pin resolved UNCONDITIONALLY rather than only
  when the code is stale. A restart destroys a pinned witness whichever
  diagnostic prescribed it; the old placement could only see the pin in the one
  case where staleness had already fired.
- Check 5 (dead log inode): the finding stays visible, the remedy is dropped.
- Check 6 (bridge_log_content_status): guarded identically. The review named
  only check 5, but discord-bridge's token override "always overrides" and
  reaches the same output — fixing one branch and leaving its sibling is how
  this defect returns wearing different clothes.

Test drives run_all_checks() because the loop is inline there, and runs the
control in BOTH directions:

    fix reverted  -> AssertionError: pin veto was retracted:
                     running but log inode dead (...) — restart with:
                     launchctl kickstart -k ... com.sutando.slack-bridge
    fix restored  -> PASS
    unpinned      -> finding AND kickstart both present, no veto

The unpinned arm is what stops this passing by construction: without it a test
asserting "no kickstart" would be green on a probe that never reaches the branch.

Existing suites unchanged: process-pins, process-pins-migration-visibility,
health-check-bridge-log-content, -log-freshness, -launcher-dupe, -skip.

Addresses P2-2 of the standing review. P2-3 (a pinned proxy misread as down by
both quota checks) is not in this commit.

Stand: Echo Act IV Pro

* fix(tests): the pin-veto test read the developer's real channel allowlist

`refuse bridge tests that read host config` failed on the previous head, and it
was right: health-check resolves `channels/<ch>/access.json` under
CLAUDE_CONFIG_DIR at import, so an unbound var reads whatever the operator has.

The linter asks for two things and I had done one. Binding CLAUDE_CONFIG_DIR
alone moved the read to an EMPTY temp dir, so the bridge stopped being
configured at all and the test failed with `slack-bridge produced no check row`
— a green lint and a broken test, from the half-fix. Seeding
`channels/slack/access.json` inside that temp dir is what makes the fixture both
hermetic and reachable; the fixture's own copy under the workspace was never the
one being read.

    lint-hermetic-bridge-tests  rc=1 -> rc=0
    test                        rc=0
    control (fix reverted)      rc=1, "pin veto was retracted: ... launchctl kickstart"

Control re-run AFTER the restructuring, not just before it: moving where the
channel is seeded could have made the test pass by never reaching the branch.

Stand: Echo Act IV Pro

* fix(health-check): restore the pin veto — my own control staged its revert

The previous commit silently reverted the P2-2 fix. `git checkout HEAD~1 -- <path>`,
which I used to run the reverted-code control, writes to the INDEX as well as the
worktree. Restoring the worktree with `cp` afterwards left index=reverted,
worktree=fixed, and `git commit` publishes the INDEX — so the commit whose message
says the control passed is the commit that removed the code the control was
testing.

    git show HEAD:src/health-check.py | grep -c pin_armed   -> 0
    grep -c pin_armed src/health-check.py                   -> 6

Nothing about it looked wrong: the test passed locally (worktree), the lint passed,
the push succeeded. Only reading the pushed BLOB shows it.

Restores the check-5 and check-6 pin guards. Verified at this commit rather than
in the worktree:

    git show HEAD:src/health-check.py | grep -c pin_armed   -> 6

Stand: Echo Act IV Pro

* fix(style): four comment blocks over the 2-line cap

review-checks failed on prose-cap. CI named only the test file, because the
job stopped at the first violation — running the checker over the CUMULATIVE
diff locally found three more in health-check.py that a per-push read of the
CI log would not have shown until three more round trips.

    review-checks --diff <PR diff>  rc=1, 3 blocks over cap -> rc=0
    test rc=0, compiles

Stand: Echo Act IV Pro

* fix(health-check): a pinned credential proxy is live, so quota checks must not go blind

check_credential_proxy() delegates staleness to the generic pin path, which turns
an armed pin into `warn`. Both quota consumers gate on
`proxy_status not in ("ok", "stale")`, so a pinned proxy read as DOWN and both
silently self-suppressed — missing telemetry and wrong-account identity go
unreported for exactly as long as the witness is preserved, and budgeting runs on
blind data without saying so.

Liveness and restart-prescription were the same field; they are now separate.
`check["live"]` is captured from the port probe BEFORE staleness rewrites status,
and `proxy_liveness_status()` maps pinned-live onto "stale" — the token both
consumers already document as "listening, not freshly deployed", which is exactly
what a pinned proxy is.

The mapping is a named function so the test can call the PRODUCTION one. Its
first version re-implemented the expression in the test, which would have stayed
green after run_all_checks stopped using it.

    pinned-live  -> "stale"  both checks run
    down         -> "warn"   both still suppress   <- control
    ok / stale   -> unchanged
    mapping disabled -> AssertionError: warn       <- control fires

Suites unchanged: process-pins, health-check-bridge-log-content, the P2-2
pin-veto regression. lint-hermetic-bridge-tests rc=0, review-checks rc=0.

Addresses P2-3. With P2-1 (8de4979a) and P2-2 (92bff0bb), all three blockers of
the standing review are answered.

Stand: Echo Act IV Pro

* test(health-check): pin the blank-row skip in the ps lstart parse

`diff coverage >= 95% (python)` was red at 88.6% — 5 uncovered lines of 97, one
short of the gate. This covers the oldest of them: the `continue` that skips a
blank row in `ps -o pid=,lstart=` output. Nothing exercised it, so a regression
there would surface as a mis-parsed start time — a wrong stale verdict — rather
than a crash.

Two things this took, and both were mine:

The first fixture stubbed EVERY subprocess call. `_file_unchanged_since` asks
git, a stubbed rc=1 reads as "unchanged", and the function returned before the
parse. Let git and lsof through; stub only pgrep and ps.

The second fixture wrapped the blank in leading/trailing newlines, which
`.strip()` eats — so the test passed while never reaching the branch it exists
for. The blank has to be INTERIOR, with a row on each side:

    "\nP L\n\n".strip().split()      -> ['P L']                  <- no blank
    "P L\n\nP2 L2\n".strip().split() -> ['P L', '', 'P2 L2']      <- blank present

Control, run against the real function: replacing the `continue` with a raise
makes the test fail on that assertion, so the blank row provably reaches it.
Restored after.

Stand: Echo Act IV Pro

* fix(health-check): every restart/rebuild prescription consults the pin

keweichen's P2 at 4e311201: mark_stale_if_outdated had three arms that can
prescribe an action and only the src-vs-process arm evaluated the pin. The
compiled-artifact arm and the binary-older-than-source arm both returned
before pin evaluation, so an operator following either prescription destroys
the branch-only compiled witness the pin exists to preserve — a rebuild
destroys it exactly as a restart does.

The binary-vs-source arm returns 25 lines before pgrep runs, so it had no pid
data to evaluate a pin against. Extracted the pgrep/ps parse into
_proc_lstarts() and hoisted it above every arm; it returns ([], {}) on
no-match or probe failure, which preserves that arm's documented "regardless
of process start" behaviour.

Policy now has one owner: process_pins.verdict_for(results, warn_lead,
stale_detail) applies the armed-pin override to any prescription, and
stale_verdict delegates to it — its output is byte-identical, so the existing
call site and its tests are unchanged.

Control (tests/health-check-pin-covers-every-prescription.test.py): each arm
carries a no-pin case proving the arm actually fires, then an armed case
proving it is overridden. Stubbed at subprocess.run — the seam present in
every version — so the same test drives the pre-fix source, where it fails on
the pin assertion:

  pre-fix : AssertionError {'status': 'stale', 'detail': 'running, but the
            artifact it executes was rebuilt 90 min after the process
            started -- restart needed'}
  at head : PASS

Existing suites green: process-pins, pin-veto-survives-later-checks,
pinned-proxy-stays-live, compiled-artifact-stale, credential-proxy-staleness,
process-pins-migration-visibility.

Stand: Echo Act IV Pro

* test(health-check): cover _proc_lstarts' two fail-open paths

diff-cover flagged 3659 and 3686-3687 — the no-match and probe-error returns
in the helper this PR extracted. Both decide whether a probe failure can HIDE
a stale deploy, so they are worth pinning rather than waiving: each must yield
([], {}) so the binary-vs-source arm still runs "regardless of process start".

A positive control asserts a working probe returns {pid: lstart}, or the two
empty-result assertions would pass on a helper that always returns empty.

Stand: Echo Act IV Pro

* fix(health-check): an armed voice pin vetoes the stuck-transport kickstart too

keweichen's P2 at a41f286b. mark_stale_if_outdated now consults the pin on all
three of its arms, but the stuck-CONNECTING branch in check_voice_transport is
a FOURTH prescription reaching the same process by another route: it sets
`_stuck_connecting`, and main() turns that into an unconditional
fix_launchd("com.sutando.voice-agent"). A kickstart destroys the pinned witness
exactly as a stale-code restart would, so their control showed armed and
unpinned producing identical fix_calls.

The bridge path already states this invariant at :9210-9221 — resolve the pin
even when code is NOT stale, because a restart destroys a pinned witness
whichever diagnostic prescribed it. This carries it to voice, reusing the
_proc_lstarts helper this PR already extracted. The pin changes the REMEDY, not
the diagnosis: status stays `fail` and the detail still says the transport is
stuck, it just withholds the flag main() gates on and appends the pin note.

Control (tests/health-check-pin-vetoes-voice-kickstart.test.py) drives the
production check over a fabricated log and process table, stubbing only those
two, and asserts the unpinned arm FIRES before asserting the armed arm is
vetoed:

  pre-fix : AssertionError {'status': 'fail', 'detail': 'stuck CONNECTING
            ~12min after code=1006 transport close — needs kickstart',
            '_stuck_connecting': True}
  at head : PASS

Revert verified by content before trusting that result.

Suite green: pin-vetoes-voice-kickstart, pin-covers-every-prescription,
process-pins, pin-veto-survives-later-checks, pinned-proxy-stays-live.

Stand: Echo Act IV Pro

* fix(health-check): an armed pin vetoes the WEDGED restart too

check_port returns status=wedged with 'restart needed' when a port accepts
but never answers. That is a restart prescription, and it was the one that
never consulted the pin -- the pin was checked inside mark_stale_if_outdated,
which four of the seven call sites reach only when the status is already 'ok'.

So a pinned process that wedged was still prescribed a restart, and a restart
destroys the branch-only witness the pin exists to preserve, exactly as a
stale-code restart would.

The veto goes at the prescription, not the call sites. Fixing the callers
would need the same patch four times and still miss the other three -- and
patching the arm I was shown while leaving the adjacent one is the exact
shape of the previous two review rounds on this PR.

armed_detail, not verdict_for: verdict_for returns 'stale' when no pin is
armed, which would silently reclassify every unpinned wedged port.

Control at the parent commit: 3 of the 4 armed assertions fail, while both
no-pin controls still pass -- so the harness works and the veto is what is
missing.

Stand: Echo Act IV Pro

* fix(health-check): a pin vetoes the REMEDY, never the DIAGNOSIS

My previous commit downgraded a pinned wedged service from `wedged` to
`warn`. `_BENIGN_STATUSES = ("ok", "warn")`, so that dropped a live
outage out of `issues` entirely: the same real failure took --quiet from
exit 1 to exit 0 and summarised as "No failures". Suppressing the alert is
the worse direction, and it is the direction I shipped.

Status now stays `wedged` whether or not a pin is armed; only the remedy
text changes, from "restart needed" to the pin's DO NOT RESTART reason.

Two more from the same review:

Packaged installs run dist/voice-agent.js, so the stuck-CONNECTING veto's
`voice-agent.ts` probe scored the pin non-live and the kickstart proceeded
against a pinned process anyway. Probe now matches either.

The veto hand-built its detail string and dropped the non-ARMED pin notes
that verdict_for appends. Extracted `process_pins.other_notes()` and
composed through it, so the shared policy has one implementation rather
than a copy.

The existing test asserted the defect -- it required the status to move
AWAY from wedged. Corrected to the real invariant, plus an explicit check
that the status is not in _BENIGN_STATUSES so --quiet still exits non-zero.
Both no-pin controls unchanged and still passing.

Stand: Echo Act IV Pro

* fix(health-check): a pin must veto the restart ACTION, not just its wording

The previous commit taught `check_port` to preserve `status=wedged` and
withdraw the "restart needed" text when a pin is armed. That fixed the
diagnosis and left the action untouched: the wedged row still enters
`issues`, and main's `--fix` dispatch picks a remedy from the row's name
and status alone. The UI said DO NOT RESTART while `--fix` destroyed the
pinned witness.

Production-path control at the parent, all four rows:

    voice-agent armed=False status=wedged fix_calls=['com.sutando.voice-agent']
    voice-agent armed=True  status=wedged fix_calls=['com.sutando.voice-agent']
    web-client  armed=False status=wedged fix_calls=['com.sutando.web-client']
    web-client  armed=True  status=wedged fix_calls=['com.sutando.web-client']

Prose in `detail` cannot gate an action, so the veto is now carried
structurally as `restart_veto` and enforced once at the shared action
boundary — the top of the fix loop, which every remedy branch passes
through. Patching the branches would be the same change five times and
would miss the sixth.

Every site that already computed an armed detail now carries the veto with
it: the wedged port, the stuck-CONNECTING kickstart, and both bridge paths
(dead log inode, log-content override).

Second blocker, and it was two arms rather than one. `check_voice_watchers`
prescribed "— restart voice-agent" identically whether or not a pin was
armed. It also returned early whenever the voice row was not "ok", which is
the natural pinned state — so the pin suppressed the DIAGNOSIS, the inverse
of its purpose. A pinned process is running, so the parse still holds.
`check_voice_transport` had the identical early return and is fixed too.

Both new tests fail at the parent (5 and 4 assertions) and pass at HEAD.
Each carries an unpinned control that must still restart, so neither can
pass by simply disabling the remedy.

Stand: Echo Act IV Pro

* fix(health-check): carry the pin veto from the PRODUCER, and cover the down arm

The veto was reachable from tests and unreachable in production.
`mark_stale_if_outdated` writes status and detail only, and the new exceptions
in check_voice_watchers/check_voice_transport key exclusively on restart_veto —
so the row a real pinned voice-agent produces never carried one. The tests
fabricated `ok/stale + restart_veto` fixtures that no production path emits.

Reproduced at the parent, matching the reviewer's control:

    ARMED  voice=warn veto=False statuses=['warn','warn','warn'] issues=0 rc=0

Both voice checks took their early return, both findings left `issues`, and
--quiet went to exit 0 — a live outage reported as no failures. At HEAD:

    ARMED  voice=warn veto=True  statuses=['warn','fail','ok']  issues=1 rc=1

(transport is `ok` here only because this fixture stages no transport error.)

Rather than patch the three verdict sites, `_apply_pin_verdict` sets the verdict
and carries the veto together, so a site cannot set one without the other. That
is the same reason the --fix gate went at the action boundary rather than into
each remedy branch.

Second blocker: `check_port` resolved a pin in its wedged arm only, so a closed
port on a LIVE pinned process returned a bare `down` and was restarted. The down
arm now consults the pin through the same helper.

The new test drives the real producer end to end and fails at the parent on 4 of
6 assertions. Its unpinned control must still be plainly stale, and a second
control shows the voice row alone exits 0 — so the watcher row is what carries
the outage, which is the whole point of preserving it.

Stand: Echo Act IV Pro

* fix(health-check): a HEALTHY pinned process needs the veto too

The previous commit wired the veto into every staleness arm. A current, healthy
pinned process reaches none of them, so nothing set the field both voice
consumers key on, and the watcher check still prescribed the restart:

    pin verdicts: ['armed']
    voice:   {'status': 'ok', ...}  restart_veto=None
    watcher: fail | missing watcher(s): ... - restart voice-agent

The pin is a property of the PROCESS, not of a staleness verdict, so the veto is
resolved at the composition point where the row is handed to both consumers,
rather than inside branches that only fire when something is already wrong.

Not put in check_port's ok arm deliberately: that would pgrep on all seven port
call sites, and an ok row is never an issue, so the only consumers that can act
on it are the two composed here.

The healthy-path assertions fail at the parent and pass at HEAD, with an
unpinned control whose remedy must still stand. All ten pin suites green.

Stand: Echo Act IV Pro

* fix(migrate): a pin survives a same-host path move — do not skip it

This PR introduced `state/process-pins.json|skip-ephemeral` with the rationale
that a pin names a local pid and is "meaningless off-host". That premise is
wrong about what this script does.

`sutando-migrate.sh` moves per-user workspace state from LEGACY LOCATIONS to the
canonical M0 path (line 4). The sources are paths on ONE host, not host
identities. So the pinned pid is still live at the destination, and skipping the
file destroys a live pin during the migration — the exact loss the pin exists to
prevent.

Classified `structural` rather than `newest-mtime`, for the reason already
recorded above the state/auth and core-status carve-outs: a multi-source scan
under newest-mtime drops a losing source's data.

    before   state/process-pins.json -> skip-ephemeral
    after    state/process-pins.json -> structural
    control  state/random-other.json -> newest-mtime   (unchanged)

Stand: Echo Act IV Pro

* Revert "fix(migrate): a pin survives a same-host path move — do not skip it"

The change was right about the premise and wrong to land alone: this branch
asserts the opposite contract in three tests, and flipping the rule broke all
of them.

    test_migration_does_not_write_a_pin_file_at_the_destination
    test_no_pin_is_stranded_in_an_unread_sidecar
    test_migration_does_not_resurrect_a_RELEASED_pin

`structural` also fails a constraint I had not accounted for: it SIDECARS on
collision, and process_pins.evaluate() reads only the canonical path, so the
losing source's pin is stranded where nothing looks. So neither skip-ephemeral
(destroys it) nor structural (strands it) is correct.

Restoring green while the premise those three tests rest on is settled with the
reviewer: they encode "another host's record", and this script migrates legacy
PATHS on one host.

Stand: Echo Act IV Pro

* test(health-check): make the voice-stack test hermetic

The new block called bare `check_voice_stack()`, which takes the supported
disabled-voice early return when no voice credential is configured. On a clean
host it returns four `ok` rows and never reaches the composition under test — so
the test passed by accident of this host's configuration rather than by
exercising anything.

It now drives the REAL resolver with an explicit env, so no host credential is
involved, and asserts up front that voice resolves ENABLED. A second assertion
inside each case pins that the composition actually ran, since disabled mode is
distinguishable by all four rows reading `ok`.

Controls both ways: forcing the env to SKIP_VOICE=1 fails at the fixture
assertion by name, and running with GEMINI_API_KEY/GEMINI_VOICE_API_KEY unset in
the process environment still passes — which is the clean-host case.

Stand: Echo Act IV Pro

* fix(health-check): the two arms that never reach check_port's normal return

Two web-client paths still restarted an armed pinned process, because neither
passes through the return the previous commits taught:

- `check_port`'s OUTER `except` arm returns `status="error"` after the probe
  raises, so no earlier arm resolved a pin.
- `run_all_checks` SYNTHESIZES a `down` row for an unusable CLIENT_PORT without
  calling `check_port` at all.

Both now resolve through `_apply_pin_verdict`, so the veto reaches the shared
`--fix` boundary the same way every other arm does. The error arm resolves
defensively — it is already the failure path, and a pgrep raising there must not
replace the original error with a new one.

Assertions are parameterised over armed and unpinned, so each carries its own
control; both fail at the parent and pass at HEAD:

    FAIL: error arm veto==True (got False)
    FAIL: synthesized row veto==True (got False)

Ten pin suites green.

Stand: Echo Act IV Pro

* fix(migrate): a pin is a snapshot — newest-mtime, never union or skip

Closes the classification question. The sources are legacy PATHS on one host,
not host identities, so the pinned pid is still live at the destination and the
record still means what it said.

`pins` is a COMPLETE MUTABLE SNAPSHOT: absence from a newer array IS the release
operation. That single property decides it — a union cannot distinguish
"released" from "never present" and re-arms a pin the operator retired. A real
union would need stable record ids plus tombstones, which this schema lacks.

The visibility suite was written on the cross-host premise and asserted the
opposite contract. Rewritten around same-host migration, driving the real
migrator and then the real reader: a lone live pin ARRIVES canonically and still
suppresses the restart; an older armed pin plus a newer empty release resolves
to `stale / restart needed`; newest wins in BOTH directions; no `.legacy-*`
sidecar, because `load_pins` opens the canonical path and nothing else.

Each rejected class fails it in its own way, so the suite discriminates between
them rather than merely preferring one:

    skip-ephemeral      FAILED (failures=3)   destroys a lone live pin
    structural          FAILED (failures=1)   strands a collision in a sidecar
    union-json-array    FAILED (failures=2)   resurrects a released pin
    newest-mtime        OK (6 tests)

Both pre-existing controls are retained. The runner block was lost while editing
and the file then exited 0 with zero output — restored, and its six test names
appear in the run above.

Stand: Echo Act IV Pro

* test(migrate): relative past mtimes — the fixture used a 2033 timestamp

CI failed both subTests of test_newest_wins_in_BOTH_directions, inverted in
each direction: whichever source was scanned last won, regardless of mtime.

    newer_is_release=True   'warn'  != 'stale'
    newer_is_release=False  'stale' != 'warn'

The fixture used 2_000_000_000 as "newer", which is 2033-05-18 — a FUTURE
timestamp. `sutando-migrate.sh` guards in-flight files with INFLIGHT_GUARD_SEC=60
against `now - mtime`, so a future file has negative age and is skipped as
in-flight. The "newer" candidate was never in the running.

HYPOTHESIS, AND I CANNOT CONFIRM IT LOCALLY: restoring the 2033 value here still
passes on macOS, so something else also differs between this host and the Linux
runner and the platform split is unexplained. What is certain is that a future
mtime is an invalid fixture for a newest-wins test regardless of which platform
notices — real migrations compare past files.

Times are now relative (`now - 7200` / `now - 3600`), both comfortably past the
60s guard, so the comparison under test is the only variable.

CI is the instrument here, not this host. Not claiming green until the run says so.

Stand: Echo Act IV Pro

* fix(migrate): newest-mtime compared whole seconds, so ties fell to scan order

`stat -f %m` / `stat -c %Y` are integer seconds. Two snapshots written in the
same second compare equal, and the `else` arm then declared the already-copied
destination newer — so the winner was whichever source got scanned last.

For a complete-snapshot file that is not a rounding nit: it resurrects a
released pin, or drops a live one, depending only on scan order. Safety
significant in both directions, which is what the new subTest asserts.

`mtime_ns` reads sub-second precision (`%Fm` on BSD, `%.9Y` on GNU) and falls
back to whole seconds only if neither is available. Fixed at the SHARED
comparator rather than for pins alone, because every `state/*.json` under
newest-mtime has the same exposure.

An exact tie is now resolved by content: identical hashes are a no-op, and
differing content at the same instant ABORTS as ambiguous rather than letting
scan order silently pick. Choosing either one can discard a live decision, and
a migration is the wrong place to guess.

Falsified — restoring the integer-second read fails both directions:

    FAIL: test_SUB_SECOND_ordering_decides_not_scan_order (release_is_newer=True)
    FAIL: test_SUB_SECOND_ordering_decides_not_scan_order (release_is_newer=False)

7 tests green at HEAD, and all 15 migrate suites pass — the blast radius of a
shared comparator is why they were all run rather than the pins one alone.

Stand: Echo Act IV Pro

* fix(migrate): validate the mtime probe — `stat -f` is a different command on GNU

My previous head went from 2 CI failures to SIX, including a sidecar test that
had been passing. The regression is in `mtime_ns` itself, not the contract.

`stat -f` is BSD "use this format"; on GNU it is `--file-system`. So on Linux
the FIRST candidate can SUCCEED with non-numeric output, the `||` fallback never
fires, and every comparison downstream is made against garbage. That is why the
count grew rather than shrank: the comparator stopped comparing at all.

Measured on BSD here, which is the half I can measure:

    stat -f %Fm   -> 1787725337.099810052
    stat -c %.9Y  -> stat: illegal option -- c

The fix does not encode which platform wins. It tries candidates in order and
VALIDATES that the result is numeric before accepting it, so a flag that means
something else on another OS is rejected on its output rather than trusted on
its exit code.

LINUX BEHAVIOUR IS INFERRED, NOT MEASURED — I have no Linux here. What is
measured is that the probe is now correct by construction for either flavour: a
non-numeric result cannot be accepted whichever command produced it.

Also retires the earlier relative-mtime commit's premise: that hypothesis is
REFUTED, the 2033 timestamp was not the cause. Relative times are kept because
a future mtime is still an invalid fixture, but they fixed nothing.

7 tests and all 15 migrate suites green here. CI adjudicates; I will not push
again on this branch until this run concludes.

Stand: Echo Act IV Pro

* fix(migrate): the exact-tie hash gate failed OPEN when the hasher was missing

Second blocker on the same comparator, and the more dangerous of the two: my
tie-break called `shasum` directly and compared the substitutions. With no
`shasum` on PATH both are empty, `[ "" = "" ]` succeeds, and two DIFFERENT
snapshots at the same instant are accepted as identical — the wrong one retained
silently, in the exact branch written to refuse guessing.

Delegates to the existing `sha_match()`, which already selects
`shasum`/`sha256sum` and — the part that matters — rejects an empty digest via
`[ -n "$ha" ]`.

Two-sided control, because a hash check that always says "differ" would also
"pass" this and be useless:

    real hasher,   IDENTICAL files -> MATCH      (the probe is alive)
    broken hasher, IDENTICAL files -> no-match   (falls through to AMBIGUOUS abort)

My first attempt at that control was invalid — the shim never got created, so it
exercised the real hasher on two different files and printed a reassuring result
about nothing. Recording that because the bad fixture and a working detector
print the same line.

Reviewer's GNU diagnosis is sharper than mine and worth keeping: `stat -f` on
GNU emits a filesystem report AND fails on the operand, so the fallback's output
is APPENDED to the same stdout — `v` is multiline, not merely wrong. The
per-candidate isolation plus numeric validation already shipped in 55b62e3b
rejects that, since a newline is non-numeric.

All 15 migrate suites green.

Stand: Echo Act IV Pro

* fix(migrate): sha_match must fail closed on a broken hasher

`[ -n "$ha" ]` accepts any non-empty token, so a hasher that prints a
non-digest string for both files compares equal and the tie branch
silently keeps one divergent snapshot instead of aborting. Called from
an `elif`, so errexit does not stop the failed assignment either.

`_sha256_of` now requires exit 0 AND a 64-char hex digest, which holds
regardless of the caller's errexit context.

Controls (positive control run first, proving the harness executes):
  real hasher, identical           -> MATCH
  real hasher, divergent           -> NO-MATCH
  shim prints BROKEN, exits 1      -> NO-MATCH   (was MATCH)
  shim prints usage, exits 0       -> NO-MATCH   (was MATCH)

Stand: Sutando-Pro

* fix(migrate): force LC_ALL=C on every mtime probe

GNU stat prints localeconv()->decimal_point, so under a comma-decimal
locale `stat -c %.9Y` returns "sec,nsec". mtime_ns's validator accepts
only digits and `.`, rejects it, and degrades to integer %Y -- so two
distinct same-second writes tie and the tie branch aborts AMBIGUOUS.

Applied to the whole-second fallback candidates too, not just %.9Y:
they are equally locale-exposed and equally silent when wrong.

New control asserts BOTH halves under a comma-locale GNU-stat shim:
  pre-fix form   a == b   TIE -> false AMBIGUOUS abort
  shipped form   a <  b   and b > a, subsecond preserved
Verified the control can fail: stripping LC_ALL=C from the script
turns the suite 8/8 -> 1 failure, and restoring it returns 8/8.

Stand: Sutando-Pro

* fix(migrate): accept coreutils' escaped-filename digest line

_sha256_of required the first token to be 64 hex chars. coreutils escapes
a filename containing a backslash or newline and prefixes the LINE with
`\`, so a VALID result reads `\<64-hex>` and was rejected -- turning a
correct hash into a false AMBIGUOUS abort. Strip one leading `\` first.

  back\slash.json vs identical content   MATCH     (was NO-MATCH)
  back\slash.json vs different content   NO-MATCH
  plain vs plain (positive control)      MATCH

Also: the new test's GNU-stat shim called `/usr/bin/stat -f %Fm` to build
its fixture. That is BSD-only -- on Ubuntu, GNU reads -f as --file-system
and emits the exact non-numeric report this patch exists to reject, so the
shim could not produce a timestamp on the runner. It now returns the
test's own known values and never invokes host stat.

Comment block in mtime_ns compressed to the repo's two-line cap.

Stand: Sutando-Pro

* test(migrate): drive the REAL migrator under emulated GNU + comma locale

The previous control asserted `fa < fb` then `fb > fa` -- one comparison
written twice, not two directions, and it never ran the migrator.

Replaced with two tests over the real `--commit` path, using a GNU `stat`
emulator on PATH: -f is --file-system (fails, as on Linux), -c formats
answer, fractions carry the locale separator. Every BSD-form call in the
script therefore falls through to its -c form, as it does on the runner.

  resolves_BOTH_directions   same second, 800ms apart, armed side swapped
  whole_second_FALLBACK      shim without %.9Y, so the fallback answers

Mutation-tested per site, each applied by asserted-unique replacement and
syntax-checked before running:

  candidate loop, LC_ALL=C removed   -> 3 failures   (tests discriminate)
  fallback only,  LC_ALL=C removed   -> OK           (inert, by design)

The fallback is inert because %m/%Y are whole-second integers: there is no
decimal separator for a locale to change, so there is no behaviour to
mutate. Its LC_ALL=C is defensive, not load-bearing. Coverage of that path
is real; a killed mutant there is not available.

Stand: Sutando-Pro

* fix(migrate): one _stat_field reader instead of 15 copies of the bug

GNU `-f` is --file-system: for a VALID operand it still prints its report
to stdout and exits nonzero, so `$(stat -f %m F || stat -c %Y F)` runs both
and captures report + newline + integer. Every numeric comparison downstream
then reads garbage -- a newer source classifies as older.

15 sites carried it, including mtime_ns's own whole-second fallback, which
the earlier LC_ALL=C commit left untouched. Converted to `_stat_field`,
which runs the two forms SEPARATELY and validates each as digits-only.

  old form under a GNU stat emulator:
      [  File: "/tmp/.../f"\n    ID: 542238b4 Namelen: 255\n1787731064]
  _stat_field, same emulator:  [1787731064]
  real stat, no shim:          1787731064 / 5   (positive control)

The `%Sm`/`%y` date site had the same shape with an extra trap: the `| cut`
bound only to the GNU branch, so a BSD-form report would not even be
trimmed. Split and validated too.

NOT changed: the `xargs -0 stat -f '%z'` site is inside `case $(uname -s)`
Darwin, so it never runs on Linux -- the awk errors seen against it came
from the emulator running on macOS, not from production.

Suite 10/10.

Stand: Sutando-Pro

* fix(migrate): guard the split stat assignment against errexit

Splitting `bsd || gnu` into separate calls removed the protection the `||`
was providing: under `set -e` a STANDALONE assignment whose command
substitution exits nonzero terminates the shell. GNU `stat -f` exits
EXIT_FAILURE, so the sentinel scan aborted before reaching the fallback
the split existed to add.

  old, set -e, stat exits 1:  rc=1, fallback line never reached
  with `|| sm=""`:            "REACHED FALLBACK", rc=0

`_stat_field` was already safe -- it carries `|| v=""` on both probes.
This was the one site where the split shipped unguarded.

Suite 10/10.

Stand: Sutando-Pro

* test(migrate): name the comma-locale case for what it covers

The method swapped armed/released while staying on dest-newer, so
"BOTH directions" described the suite, not the method. Renamed to
..._resolves_subsecond_on_dest_newer and said so in the docstring,
pointing at test_newest_wins_in_BOTH_directions for the direction case.

Stand: Sutando-Pro

* test(migrate): assert the GNU shim actually BINDS before trusting it

`os.access(shim, X_OK)` proves the file is executable, not that it wins
on PATH. If the shim dir loses precedence the host stat answers and every
GNU assertion passes without the instrument ever firing -- a control that
passes by construction, in the test written to prevent exactly that.

`_assert_shim_binds` invokes `stat -f` through the test's own PATH and
requires the shim's signature: nonzero exit AND `File:` on stdout.

Negative control (shim renamed so it cannot shadow `stat`):
  4 failures, "shim did NOT bind: `stat -f` succeeded, so host stat answered"
Restored: 10/10.

Raised by kewei-red-ag2space. Their phrasing of the prior state was
accurate and I have adopted it: Ubuntu runner regression cleared, shim
binding unproven -- until this.

Stand: Sutando-Pro

* test: assert shim binding on a nonce, not a predicate host GNU stat also satisfies

_assert_shim_binds required nonzero exit + 'File:' on stdout from
`stat -f`. On Linux that is exactly what host GNU stat emits (-f is
--file-system), so the assertion is satisfied by the host on the one
platform the shim exists to prove. The macOS mutant passed only because
BSD `stat -f` succeeds there.

The shim now writes a per-instance nonce to stderr and the assertion
requires it, which no host stat can produce on either platform.

Stand: Sutando-Pro

* test: witness the MIGRATOR's stat calls via a trace file, not the direct probe

_assert_shim_binds proved the shim bound in the assertion's OWN subprocess.
_migrate() is a different process with its own env, and production redirects
stat's stderr, so the nonce is unobservable on the activated path. Dropping the
shim from the migrator's PATH alone left every assertion green.

The shim now appends nonce+argv to a trace file on every call. The direct probe
asserts, then TRUNCATES -- otherwise its own entry satisfies the production
check. After _migrate() the trace must carry both a BSD-form (-f) and a
GNU-form (-c) call.

Mutant (shim dropped from the migrator env only): trace empty, both GNU tests
fail. Same mutant was green before this commit.

Stand: Echo Act IV Pro

* test: bind the witness to the mtime formats, not to any -f/-c call

The trace opens with SIZE probes (-f %z, -c %s). Requiring any -f plus any
-c was satisfied by those two lines before %Fm / %.9Y / %m / %Y was ever
considered, so the mtime decision under test could run on host stat while
the assertion passed.

Now: argv is JSON-encoded per line (' '.join loses argument boundaries),
records are filtered by nonce, and each required (flag, format) must appear
against a resolved fixture path -- Path.resolve() because macOS enters the
fixture as /var/... while production resolves /private/var/....
The whole-second test additionally requires the -c %Y fallback.

Mutant (mtime bypasses the shim, size calls still trace): 4 failures.
Same mutant was green at 33ad5b01.

Stand: Echo Act IV Pro

* test: require the exact operands, both of them, per format

The assertion required each format once against any path under the Source-A
or destination ROOT. A decoy file under src/a satisfied it while the real
comparator ran on host stat, and neither exact process-pins.json operand was
required -- much less both. startswith() is also string prefix, not path
containment, so root src/a admitted src/attacker/....

Now an expected tuple set: {(flag, format, exact resolved path)} for every
required format x BOTH operands (src/a/state/process-pins.json and
dest/state/process-pins.json), asserted as a subset of the trace.

Mutant (traces rewritten to a decoy operand under src/a): 4 failures.
Same mutant was green at 909983a0.

Stand: Echo Act IV Pro

* test: prove the migrator CONSUMES the shim's mtime, via a disagreeing fixture

Every prior witness was blind to consumption: the shim answered from os.stat
on the same fixtures, so host and shim always named the same winner and the
functional verdict could not tell which one production used.

The shim gains an optional synthetic mtime table (absent key -> os.stat, so the
10 existing tests are unchanged). The new test makes host and shim DISAGREE on
the pair the migrator actually compares -- src vs DEST, not src vs src:
  host: src/a is 5s newer -> dest overwritten with the armed pin -> warn
  shim: same second, dest has the larger fraction -> dest preserved -> stale
Asserting stale therefore asserts the shim's value drove the decision.

Mutant (synth={} -- shim still invoked and traced, answers = host values):
the new test FAILS. Baseline 11/11 OK.

Stand: Echo Act IV Pro

* fix(migrate): honour stat's exit code — numeric output is not a successful call

mtime_ns's candidate loop used `|| true`, discarding the exit status, so it
broke on numeric-LOOKING output rather than on a successful call. A stat that
fails while printing a plausible number was taken as the answer.

    -  v="$(LC_ALL=C $_c "$f" 2>/dev/null || true)"
    +  v="$(LC_ALL=C $_c "$f" 2>/dev/null)" || v=""

`|| v=""` and not a bare assignment: the `||` also preserves the errexit
exemption, which a standalone failing substitution would lose.

Test: shim's `-f %Fm` exits nonzero while printing a mtime 99s in the future,
chosen to name the opposite winner from the `-c %.9Y` answer. Honouring rc the
poisoned value is discarded and the destination survives (stale); ignoring rc it
is trusted and the destination is overwritten (warn).

Control: restore `|| true` -> that test, and only that test, FAILS. With the
fix, 12/12 OK.

Stand: Echo Act IV Pro

* test: interleaved reciprocal rows — only BOTH shim answers pick the shim winner

A single non-interleaved row is satisfied by 'always keep destination', and
ordinary reciprocal values can still be satisfied by consuming ONE operand's
shim answer plus one host answer. Interleaving makes every mixed pair agree
with the HOST winner:

    shim-D-wins: real_D < shim_A < shim_D < real_A
    shim-A-wins: real_A < shim_D < shim_A < real_D

All four values sit inside one whole second, so %Y ties and only the
successful %.9Y answers can decide.

Mutants, each FAILING BOTH rows (baseline 13/13 OK):
    host stat for both operands  -> FAILED (failures=2)
    shim answer for A only       -> FAILED (failures=2)
    shim answer for D only       -> FAILED (failures=2)
always-source and always-destination are excluded by the reciprocity itself:
the two rows demand opposite outcomes from the same code path.

Stand: Echo Act IV Pro

* test: whole-second fallback — prove %Y DECIDED, not merely that it was reachable

My first version of this row disabled %.9Y and passed either way: with equal
fractions %.9Y names the same winner as %Y, so the disable line certified
nothing. Caught by asking whether removing it changed the result (it did not)
before pushing; that version was reverted, not shipped.

The shim now carries a SEPARATE %.9Y table naming the OPPOSITE winner from the
%Y table, so the verdict identifies which call decided:
    %Y   -> dest wins  -> stale
    %.9Y -> a wins     -> warn

Control: leave %.9Y ENABLED -> FAILS with 'the %Y fallback did not decide'.
Disabled -> 14/14 OK. synth9 defaults empty, so the other 13 are unchanged.

Stand: Echo Act IV Pro

* test: assert the surviving BYTES, not the derived verdict

load_pins() maps truncated, malformed, wrong-shaped and absent JSON alike to
[], and [] yields 'stale' -- so a verdict-only oracle accepts any corruption
whose image is [] in the destination-winning branch. Reciprocity cannot help:
the other row's expected value is legitimately different.

Each row now captures both candidates' bytes before migrating and requires the
surviving canonical file to equal the expected candidate exactly.

Mutant (truncate canonical ONLY when dest wins, copy normally when source wins):
    AssertionError: b'' != b'{\n  "pins": []\n}'
    shim-D-wins: verdict matched but the surviving BYTES are not the expected
                 candidate's
The message states the case: the verdict assertion still PASSED. Baseline 14/14.

Stand: Echo Act IV Pro

* style: keep the byte-oracle comment inside the 2-line cap

Review checks (REVIEW.md) failed at acf4c993: a 3-line comment block against
the 2-line cap. Same rule this repo enforces on everyone; the narrative belongs
in the PR body, not the code.

Stand: Echo Act IV Pro

* test: make the %Y fallback reciprocal and byte-checked

A single dest-winning fallback row is satisfied by four different
always-keep-destination implementations, because the destination is pre-seeded
with the expected bytes so a no-op looks correct: mtime_ns returning constants
with no stat call, dropping 'stat -f %m', trying %Y before %m, and truncating
the destination in that branch only.

The row is now reciprocal on interleaved integer seconds, with each row's %.9Y
table naming the opposite winner and the surviving bytes asserted against the
expected candidate:

    fallback-D-wins: real_D < Y_A < Y_D < real_A  -> dest survives
    fallback-A-wins: real_A < Y_D < Y_A < real_D  -> source copied

Mutant (constant answers always naming destination): FAILS fallback-A-wins with
'a constant impl cannot satisfy both rows'. Baseline 14/14.

Stand: Echo Act IV Pro

* test: recorder liveness via independent oracle + falsification control

Trap 4: a liveness assertion is not exempt from being inert. The escape is not
a chain of assertions but NON-DERIVATION -- two observations of one call, each
compared to a literal the parent fixed in advance, neither computed from the
other.

Three actors, none of which both produces and judges:
  parent    fixes run_id/call_id/operand and expected rc/stdout/record, and
            proves the trace does not pre-exist
  shim      separate process via PATH; a reserved --sutando-shim-probe argv
            reaches the SAME synchronous finish() production uses, so no
            probe-only writer can pass while production is dead
  validator parent-side, reads raw trace bytes, never invokes or repairs

Replaces insufficient checks I had shipped or reached for: truthy
CompletedProcess, trace-file existence, a local 'probe ran' flag, and 'the
nonce appears somewhere' -- the last being _assert_shim_binds as written.

Controls: writer-dead leaves records == []; killing the shim's only writer
makes the liveness test itself fail. Baseline 16/16.

Known rough edge: that failure surfaces as FileNotFoundError on the trace read
rather than a named assertion. It discriminates; the message is poor.

Stand: Echo Act IV Pro

* fix(tests): the liveness probe had its own writer, so it witnessed only itself

keweichen's blocker: `_probe_shim` was a second `stat` shim with a second
`finish()` writing the same trace. A probe that builds its own recorder proves
that recorder is live -- not the one the migrator runs. The judge was creating
the evidence it judged.

Structural, not additive: `_probe_shim` is deleted. `_gnu_stat_shim` is the one
factory, with one `finish()` and one `open(TRACE)`; the probe and the migrator
now differ only in argv, via a reserved `--sutando-shim-probe=<call_id>`.

Two silent-no-op guards, both found by this restructure rather than reasoned
about. The shim template moved to double quotes, so the two `.replace()` calls
that disable `%.9Y` and kill the writer stopped matching -- and a `.replace()`
that matches nothing returns its input and passes. Both now assert the needle
count before substituting. The failed-format path also recorded no operand, so
its calls were dropped by the trace parser.

Controls (both run, neither is inert):
  probe branch -> `if False:`   -> 1 failure (liveness test is not vacuous)
  writer mutant, needle absent  -> now an explicit failure, previously silent
  unmutated                     -> 16/16

Stand: Echo Act IV Pro

* fix(tests): retract 8c947382's control — it passed on IndentationError, not a dead writer

keweichen caught this and he is right. `test_RECORDER_liveness_check_can_FAIL`
replaced the `with open(TRACE, ...)` line with `return`, which leaves the
`fh.write(...)` body over-indented:

    def finish(...):
        return  # MUTANT: writer dead
            fh.write("x")     -> IndentationError: unexpected indent

So the shim never parsed. `stat` died with a traceback, no trace file was
created, and the test asserted an absent trace maps to `[]` — a pass that had
nothing to do with the writer, and would hold even if the recorder were
perfect. It also ignored the child's rc/stdout/stderr, so it could not tell the
two apart. I cited it as a control in 8c947382's message and on the PR. That
citation was wrong; this commit withdraws it.

Restructured to his design: one `dispatch(argv)` returning the same five fields
for every GNU/BSD success and failure, one bottom-level `finish(a, *dispatch(a))`,
and the writer behind `DROP_RESUL…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants