Repository navigation
Expand troubleshooting in README - #14
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 of 2 tasks
sonichi
added a commit
that referenced
this pull request
Apr 25, 2026
Pulls in the bodhi #14 silent-context-injection fix on Gemini reconnect. Combined with PR #515 (merged into main, now in this branch via merge), this covers both reconnect paths: - Gemini-internal reconnect: silent injection drops "Say I'm back" prompt - Client reconnect <60s: getSecondsSinceLastTurn suppresses "Welcome back" Voice-agent verified: skill-loader still fires 3 tools on boot (PID 23502). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2 of 5 tasks
sonichi
added a commit
that referenced
this pull request
Apr 25, 2026
…ged)" This reverts commit f9a4be4.
sonichi
added a commit
that referenced
this pull request
Apr 25, 2026
Pairs with the system_instruction presenter marker (374cebf): with the marker now authoritative + sent on every session.start, the new bodhi's silent context injection should be safe — the per-cue gating in voice- context.txt is enforced from the system_instruction, not relying on the flat history replay to anchor model behavior. Trade-off in case of regression: revert this commit, voice-agent runs on old bodhi 4d1592eb (welcome-back fires, but no concatenation risk). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sonichi
added a commit
that referenced
this pull request
Apr 26, 2026
* feat(mode-indicator): full 3-mode UI recovered from Apr 21 session jsonl
Restores the 3-mode (active / meeting / presenter) UI that was lost
when `git reset --hard HEAD~2` on dev/apr-21-local-ships at 2026-04-23
06:22 PT discarded uncommitted working-tree edits alongside two
sync-memory commits.
Recovered by replaying 15 Edit calls from session 6eb8f10e jsonl
(2026-04-21 23:18-23:54 PT) on top of dev/apr-21-local-ships's
presenter-dot indicator base. Replay applied 15/15 cleanly.
Sutando.app (`src/Sutando/main.swift`):
- New properties: `voiceMode`, `presenterModeActive`, three menu-item
weak refs (`modeActiveMenuItem`, `modeMeetingMenuItem`,
`modePresenterMenuItem`).
- Three-mode radio in menu bar dropdown: "Mode: Active" /
"Mode: Meeting" / "Mode: Presenter". Exactly one has ● at a time.
Clicking any item switches: Active/Meeting write
`state/voice-mode.request` (voice-agent picks up on 1s poll);
Presenter POSTs to `:7877/presenter/on`.
- Avatar badge: `avatarImage(presenterActive:meetingActive:)` paints
a small purple dot when presenter is active, or amber dot for
meeting — matches the web UI mode-pill colors.
- New polls: `pollPresenterMode()` + `pollVoiceMode()` every 1s on
the same timer. Both silent-fail if their backend is down.
- `updateModeMenuItem()` recomposes the radio whenever either signal
flips. Priority: presenter > meeting > active.
Voice-agent (`src/voice-agent.ts`):
- `switchModeTool.execute()` writes `state/voice-mode.txt` on
switch_mode("active"|"meeting") so cross-process readers (web-client,
Sutando.app) can resolve the unified mode.
- `applyModeRequest()` polls `state/voice-mode.request` every 1s,
applies the mode flip, deletes the request file. Lets Sutando.app
send mode requests without an HTTP server in voice-agent.
Web client (`src/web-client.ts`):
- `mode` field added to /sse-status response — built from sync read of
`state/voice-mode.txt` + cached presenter-active boolean (refreshed
every 1s in background from :7877/presenter).
- Mode pill in top-bar UI: 3 CSS variants (mode-active dim /
mode-meeting amber / mode-presenter purple-glow). Polls /sse-status
every 1.5s.
Recovery details: notes/3-mode-recovered-edits-2026-04-21.md.
Earlier #518 (simpler text-only version) is closed in favor of this
fuller recovery.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(voice-agent): sync voice-mode sentinel on switch_mode + after Zoom detect
Two related bugs in the recovered 3-mode work surfaced when Chi tested
the menu radio: clicking "Mode: Meeting" silently no-op'd because the
in-memory `meetingActive` flag and the on-disk `state/voice-mode.txt`
sentinel had diverged.
(1) `switchModeTool.execute` flipped `meetingActive` but never called
`writeVoiceModeSentinel()`. So a voice-triggered switch_mode kept
the sentinel stale, and a subsequent menu click that wrote
`voice-mode.request="meeting"` hit the `meetingActive === want`
early-return in `applyModeRequest` — request consumed without
writing the sentinel. Sutando.app's pollVoiceMode kept reading
"active" → menu radio stuck.
(2) The startup `writeVoiceModeSentinel()` call ran BEFORE the Zoom
auto-detect, so even when Zoom was detected as in-meeting and
`meetingActive` was set to true, the sentinel had already been
written as "active" with no second write.
Fix: call `writeVoiceModeSentinel()` inside `switchModeTool.execute`,
and move the startup call to after the Zoom auto-detect block.
Verified live: kickstart → sentinel "meeting" matches in-memory state
(Zoom-running case); write voice-mode.request="meeting" → sentinel
flips, log line "External request applied: mode=meeting" fires.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(voice-agent): elevate presenter_mode to direct tool trigger
Mirrors the switch_mode pattern. Without this, "presenter mode on" / "the talk starts" routes to the work tool, producing "working on it" instead of silently flipping the mode.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(inline-tools): restore skill manifest loader stripped by PR #505
PR #505 (dup-name guard) inadvertently removed loadSkillManifestTools()
along with the personalTools spread into inlineTools. Voice-agent had no
way to register highlight_slide / presenter_mode tools from
skills/personal-iclr-highlight/, so the autonav cue silently no-op'd
during the ICLR talk rehearsal.
Restored verbatim from 9b545c2 (the original local-ships commit). Smoke-
tested: voice-agent boots with "[skill-loader] loaded 3 tool(s) from
iclr-highlight" — highlightSlideTool, presenterModeTool, fullscreenTool
(renamed to fullscreen_presenter to dodge the dup-name guard).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(voice-agent): reject filler words; disambig screen_record from fullscreen
Two pre-talk hardenings observed during ICLR rehearsal:
1. recording-tools.ts: screen_record description now explicitly rejects
"fullscreen" / "full screen" / "play fullscreen" cues. Chi asked to play
the cross-owner video fullscreen and Sutando self-fired screen_record
(06:34:46) right after fullscreen_presenter + play_video — STT or model
association from the word "screen" was matching screen_record.
2. voice-agent.ts: restored FILLERS ARE NOT REQUESTS rule (originally on
the unmerged 9b545c2 local-ships branch). Short utterances "hmm" / "um" /
"ok" / "[BLANK_AUDIO]" are not instructions — Sutando should stay silent
or ack, not call work and say "queued up".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(deps): bump bodhi-realtime-agent to ee58489 (PR #14 merged)
Pulls in the bodhi #14 silent-context-injection fix on Gemini reconnect.
Combined with PR #515 (merged into main, now in this branch via merge),
this covers both reconnect paths:
- Gemini-internal reconnect: silent injection drops "Say I'm back" prompt
- Client reconnect <60s: getSecondsSinceLastTurn suppresses "Welcome back"
Voice-agent verified: skill-loader still fires 3 tools on boot (PID 23502).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Revert "chore(deps): bump bodhi-realtime-agent to ee58489 (PR #14 merged)"
This reverts commit f9a4be4.
* feat(voice-agent): inject presenter-state into greeting + reconnect prompts
Closes the gap that surfaced during talk rehearsal: voice-agent restart
loses co-presenter mode anchor. Even though :7877/presenter still says
active=true, the model gets the default Sutando system prompt and
defaults to "Echo Act IV" generic greeting, routing slide-topic phrases
to work instead of highlight_slide+narrate.
Adds getPresenterStateMarker() that synchronously curls the highlight
server and returns "[System: PRESENTER MODE IS CURRENTLY ACTIVE — apply
the CO-PRESENTER protocol...]" when active. Appended to both greeting
paths:
- Fresh connect (line 484): generic greeting + presenter marker
- Reconnect with history (line 453): reconnect prompt + presenter marker
Failure-silent — if curl fails the marker is empty string, so non-talk
sessions and missing iclr-highlight skill are unaffected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(voice-agent): suppress "Welcome back" when presenter mode is active
Reconnect path was still emitting "Welcome back" when presenter is on,
which breaks the co-presenter flow mid-talk. Reuses the existing
quick-reconnect silent-reconnect hint when presenterActive is true,
keyed off getPresenterStateMarker() returning non-empty.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(voice-agent): make instructions a per-session factory + inject presenter marker
Previous a284190 + 5eb09af put the presenter-state marker in the GREETING
string. Gemini Live treats the greeting as a user-style turn — the model
called get_core_status to verify the "claim" instead of trusting it,
and answered "presenter mode is not currently active" despite
:7877/presenter being active.
System instructions are authoritative. This commit:
1. Converts mainAgent.instructions from a static joined string to a
factory function `() => [...].join('\n')`. Each session.start() now
re-evaluates the array, picking up live state.
2. Adds getPresenterStateMarker() as the FIRST slot in the array — so
the system prompt opens with "[System: PRESENTER MODE IS CURRENTLY
ACTIVE...]" when the iclr-highlight server says active=true.
Failure-silent — if the curl fails, the marker is empty string and the
slot drops out of the join cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(deps): re-bump bodhi-realtime-agent to ee58489 (PR #14)
Pairs with the system_instruction presenter marker (374cebf): with the
marker now authoritative + sent on every session.start, the new bodhi's
silent context injection should be safe — the per-cue gating in voice-
context.txt is enforced from the system_instruction, not relying on the
flat history replay to anchor model behavior.
Trade-off in case of regression: revert this commit, voice-agent runs
on old bodhi 4d1592eb (welcome-back fires, but no concatenation risk).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(open-file): QuickTime fullscreen survives Zoom screen-share
The 'present' Apple event succeeds but the visible window doesn't
come forward when Zoom is screen-sharing — Zoom's floating control bar
holds the foreground z-order. Switch to Ctrl+Cmd+F routed through
'tell process "QuickTime Player"' so System Events targets QT directly
instead of going through global keystroke focus that Zoom is grabbing.
Same pattern as the Chrome fullscreen fix in
skills/personal-iclr-highlight/tools.ts. Validated locally — TS clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
qingyun-wu
pushed a commit
that referenced
this pull request
May 21, 2026
…ndo-skills (#995) The main-repo `skills/info-radar/` was a SKILL.md-only stub: no scripts, no manifest, and no cron driving it. The functional info-radar — adaptive-x-feed recommender + x-radar-daily + append-signal + tests — lives in the private `sonichi/sutando-skills` repo (added #14, fixed #15), whose SKILL.md is a strict superset of this stub (keeps the static topic-monitor as a Legacy section, adds the adaptive recommender). Keeping both created a dual-home: `install.sh` skipped re-linking info-radar ("symlink points elsewhere"), pinning `~/.claude/skills/info-radar` to this non-functional stub while the real version stayed dormant. Removing the stub lets the next skills-sync install re-link to the functional copy. info-radar is intentionally staged private (sutando-skills) while its scripts are not yet owner-validated; it returns to the public repo once tested. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
liususan091219
pushed a commit
to liususan091219/sutando-fork
that referenced
this pull request
May 28, 2026
…ichi#14) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
liususan091219
pushed a commit
to liususan091219/sutando-fork
that referenced
this pull request
May 28, 2026
…chi#520) * feat(mode-indicator): full 3-mode UI recovered from Apr 21 session jsonl Restores the 3-mode (active / meeting / presenter) UI that was lost when `git reset --hard HEAD~2` on dev/apr-21-local-ships at 2026-04-23 06:22 PT discarded uncommitted working-tree edits alongside two sync-memory commits. Recovered by replaying 15 Edit calls from session 6eb8f10e jsonl (2026-04-21 23:18-23:54 PT) on top of dev/apr-21-local-ships's presenter-dot indicator base. Replay applied 15/15 cleanly. Sutando.app (`src/Sutando/main.swift`): - New properties: `voiceMode`, `presenterModeActive`, three menu-item weak refs (`modeActiveMenuItem`, `modeMeetingMenuItem`, `modePresenterMenuItem`). - Three-mode radio in menu bar dropdown: "Mode: Active" / "Mode: Meeting" / "Mode: Presenter". Exactly one has ● at a time. Clicking any item switches: Active/Meeting write `state/voice-mode.request` (voice-agent picks up on 1s poll); Presenter POSTs to `:7877/presenter/on`. - Avatar badge: `avatarImage(presenterActive:meetingActive:)` paints a small purple dot when presenter is active, or amber dot for meeting — matches the web UI mode-pill colors. - New polls: `pollPresenterMode()` + `pollVoiceMode()` every 1s on the same timer. Both silent-fail if their backend is down. - `updateModeMenuItem()` recomposes the radio whenever either signal flips. Priority: presenter > meeting > active. Voice-agent (`src/voice-agent.ts`): - `switchModeTool.execute()` writes `state/voice-mode.txt` on switch_mode("active"|"meeting") so cross-process readers (web-client, Sutando.app) can resolve the unified mode. - `applyModeRequest()` polls `state/voice-mode.request` every 1s, applies the mode flip, deletes the request file. Lets Sutando.app send mode requests without an HTTP server in voice-agent. Web client (`src/web-client.ts`): - `mode` field added to /sse-status response — built from sync read of `state/voice-mode.txt` + cached presenter-active boolean (refreshed every 1s in background from :7877/presenter). - Mode pill in top-bar UI: 3 CSS variants (mode-active dim / mode-meeting amber / mode-presenter purple-glow). Polls /sse-status every 1.5s. Recovery details: notes/3-mode-recovered-edits-2026-04-21.md. Earlier sonichi#518 (simpler text-only version) is closed in favor of this fuller recovery. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(voice-agent): sync voice-mode sentinel on switch_mode + after Zoom detect Two related bugs in the recovered 3-mode work surfaced when Chi tested the menu radio: clicking "Mode: Meeting" silently no-op'd because the in-memory `meetingActive` flag and the on-disk `state/voice-mode.txt` sentinel had diverged. (1) `switchModeTool.execute` flipped `meetingActive` but never called `writeVoiceModeSentinel()`. So a voice-triggered switch_mode kept the sentinel stale, and a subsequent menu click that wrote `voice-mode.request="meeting"` hit the `meetingActive === want` early-return in `applyModeRequest` — request consumed without writing the sentinel. Sutando.app's pollVoiceMode kept reading "active" → menu radio stuck. (2) The startup `writeVoiceModeSentinel()` call ran BEFORE the Zoom auto-detect, so even when Zoom was detected as in-meeting and `meetingActive` was set to true, the sentinel had already been written as "active" with no second write. Fix: call `writeVoiceModeSentinel()` inside `switchModeTool.execute`, and move the startup call to after the Zoom auto-detect block. Verified live: kickstart → sentinel "meeting" matches in-memory state (Zoom-running case); write voice-mode.request="meeting" → sentinel flips, log line "External request applied: mode=meeting" fires. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(voice-agent): elevate presenter_mode to direct tool trigger Mirrors the switch_mode pattern. Without this, "presenter mode on" / "the talk starts" routes to the work tool, producing "working on it" instead of silently flipping the mode. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(inline-tools): restore skill manifest loader stripped by PR sonichi#505 PR sonichi#505 (dup-name guard) inadvertently removed loadSkillManifestTools() along with the personalTools spread into inlineTools. Voice-agent had no way to register highlight_slide / presenter_mode tools from skills/personal-iclr-highlight/, so the autonav cue silently no-op'd during the ICLR talk rehearsal. Restored verbatim from 9b545c2 (the original local-ships commit). Smoke- tested: voice-agent boots with "[skill-loader] loaded 3 tool(s) from iclr-highlight" — highlightSlideTool, presenterModeTool, fullscreenTool (renamed to fullscreen_presenter to dodge the dup-name guard). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(voice-agent): reject filler words; disambig screen_record from fullscreen Two pre-talk hardenings observed during ICLR rehearsal: 1. recording-tools.ts: screen_record description now explicitly rejects "fullscreen" / "full screen" / "play fullscreen" cues. Chi asked to play the cross-owner video fullscreen and Sutando self-fired screen_record (06:34:46) right after fullscreen_presenter + play_video — STT or model association from the word "screen" was matching screen_record. 2. voice-agent.ts: restored FILLERS ARE NOT REQUESTS rule (originally on the unmerged 9b545c2 local-ships branch). Short utterances "hmm" / "um" / "ok" / "[BLANK_AUDIO]" are not instructions — Sutando should stay silent or ack, not call work and say "queued up". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(deps): bump bodhi-realtime-agent to ee58489 (PR sonichi#14 merged) Pulls in the bodhi sonichi#14 silent-context-injection fix on Gemini reconnect. Combined with PR sonichi#515 (merged into main, now in this branch via merge), this covers both reconnect paths: - Gemini-internal reconnect: silent injection drops "Say I'm back" prompt - Client reconnect <60s: getSecondsSinceLastTurn suppresses "Welcome back" Voice-agent verified: skill-loader still fires 3 tools on boot (PID 23502). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Revert "chore(deps): bump bodhi-realtime-agent to ee58489 (PR sonichi#14 merged)" This reverts commit f9a4be4. * feat(voice-agent): inject presenter-state into greeting + reconnect prompts Closes the gap that surfaced during talk rehearsal: voice-agent restart loses co-presenter mode anchor. Even though :7877/presenter still says active=true, the model gets the default Sutando system prompt and defaults to "Echo Act IV" generic greeting, routing slide-topic phrases to work instead of highlight_slide+narrate. Adds getPresenterStateMarker() that synchronously curls the highlight server and returns "[System: PRESENTER MODE IS CURRENTLY ACTIVE — apply the CO-PRESENTER protocol...]" when active. Appended to both greeting paths: - Fresh connect (line 484): generic greeting + presenter marker - Reconnect with history (line 453): reconnect prompt + presenter marker Failure-silent — if curl fails the marker is empty string, so non-talk sessions and missing iclr-highlight skill are unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(voice-agent): suppress "Welcome back" when presenter mode is active Reconnect path was still emitting "Welcome back" when presenter is on, which breaks the co-presenter flow mid-talk. Reuses the existing quick-reconnect silent-reconnect hint when presenterActive is true, keyed off getPresenterStateMarker() returning non-empty. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(voice-agent): make instructions a per-session factory + inject presenter marker Previous a284190 + 5eb09af put the presenter-state marker in the GREETING string. Gemini Live treats the greeting as a user-style turn — the model called get_core_status to verify the "claim" instead of trusting it, and answered "presenter mode is not currently active" despite :7877/presenter being active. System instructions are authoritative. This commit: 1. Converts mainAgent.instructions from a static joined string to a factory function `() => [...].join('\n')`. Each session.start() now re-evaluates the array, picking up live state. 2. Adds getPresenterStateMarker() as the FIRST slot in the array — so the system prompt opens with "[System: PRESENTER MODE IS CURRENTLY ACTIVE...]" when the iclr-highlight server says active=true. Failure-silent — if the curl fails, the marker is empty string and the slot drops out of the join cleanly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(deps): re-bump bodhi-realtime-agent to ee58489 (PR sonichi#14) Pairs with the system_instruction presenter marker (374cebf): with the marker now authoritative + sent on every session.start, the new bodhi's silent context injection should be safe — the per-cue gating in voice- context.txt is enforced from the system_instruction, not relying on the flat history replay to anchor model behavior. Trade-off in case of regression: revert this commit, voice-agent runs on old bodhi 4d1592eb (welcome-back fires, but no concatenation risk). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(open-file): QuickTime fullscreen survives Zoom screen-share The 'present' Apple event succeeds but the visible window doesn't come forward when Zoom is screen-sharing — Zoom's floating control bar holds the foreground z-order. Switch to Ctrl+Cmd+F routed through 'tell process "QuickTime Player"' so System Events targets QT directly instead of going through global keystroke focus that Zoom is grabbing. Same pattern as the Chrome fullscreen fix in skills/personal-iclr-highlight/tools.ts. Validated locally — TS clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
liususan091219
pushed a commit
to liususan091219/sutando-fork
that referenced
this pull request
May 28, 2026
…ndo-skills (sonichi#995) The main-repo `skills/info-radar/` was a SKILL.md-only stub: no scripts, no manifest, and no cron driving it. The functional info-radar — adaptive-x-feed recommender + x-radar-daily + append-signal + tests — lives in the private `sonichi/sutando-skills` repo (added sonichi#14, fixed sonichi#15), whose SKILL.md is a strict superset of this stub (keeps the static topic-monitor as a Legacy section, adds the adaptive recommender). Keeping both created a dual-home: `install.sh` skipped re-linking info-radar ("symlink points elsewhere"), pinning `~/.claude/skills/info-radar` to this non-functional stub while the real version stayed dormant. Removing the stub lets the next skills-sync install re-link to the functional copy. info-radar is intentionally staged private (sutando-skills) while its scripts are not yet owner-validated; it returns to the public repo once tested. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sonichi
added a commit
that referenced
this pull request
Aug 23, 2026
… regex qingyun-wu's second review round on #3323 cited REVIEW.md rule #14 ("never assert on source text as a stand-in for a behavioral claim") against the guard-based structural test — even anchored to the skip branch (88f3839), it still only regexes discord-bridge.py's source rather than exercising the real decision. Extract the skip check into an importable, dependency-light helper — has_skip_action(actions) in src/result_markers.py — and update poll_proactive/poll_dm_fallback in discord-bridge.py to call it instead of an inline `any(a.kind == "skip" for a in actions)`. A third site (the [deduped:] cross-channel-rejection branch) needs the full Action object, not a bool, and is left untouched — different shape, out of scope per CLAUDE.md's "fix the policy that duplicates, not every shape that happens to look similar." The test now exercises has_skip_action() behaviorally with real parse_markers() output (no-send/REPLIED/deduped all skip; ordinary text does not), plus narrowly-scoped structural pins (delegation to the shared helper, no inline reimplementation, discard-before-continue ordering) — legitimate under REVIEW.md's structural exception because each is paired with the behavioral test above, never a substitute. Verified: py_compile clean, review-checks.sh clean, 8 sibling tests unaffected (bridge-marker-no-leak, result-markers 48 tests, proactive-channel-redirect-loud-failure, health-check-proactive- quarantine 8 tests, bridge-dedup-poll-loop 4 tests, bridge-dedup- wrappers 4 tests, dedup-cross-channel 15 tests, dedup-holder-delivered 18 tests). Live post-restart round trip on the production host: restarted discord-bridge, a [no-send]-marked proactive file was cleanly dropped (no send attempt), and a new ordinary proactive file delivered normally — zero new results/undelivered/ items since restart.
sonichi
added a commit
that referenced
this pull request
Aug 23, 2026
…onor suppression markers qingyun-wu's second CHANGES_REQUESTED on this PR, verbatim: Blocking: the new head narrows the string search, but the regression test still does not prove that the suppression predicate controls the terminal drop. For example, changing the production branch to `if False and any(a.kind == "skip" for a in _pp.actions):` leaves parse_markers, the `kind == "skip"` text, `.drop(...)`, `continue`, and their ordering intact, so every guard in this test still passes while `[no-send]` files again fall through to Discord delivery. This is the same mutation called out on the previous head. Please exercise `poll_proactive` behavior (or extract the parse/drop decision into a small callable and test it) so that disabling the condition makes the regression test fail. f669570's extraction of has_skip_action() into src/result_markers.py was still insufficient: applying qingyun's exact mutation, `if False and has_skip_action(_pp.actions):`, inside poll_proactive and rerunning tests/proactive-suppression-marker-honored.test.py still PASSED (should have failed) — that test only greps poll_proactive's source text for `has_skip_action(`, `.drop(`, `continue`, never executes the function. Rather than extract poll_proactive/poll_dm_fallback into separate `_route_*_file` callables (the larger refactor originally proposed), this adds tests/proactive-suppression-marker-behavioral.test.py, which directly executes the real, UNMODIFIED poll_proactive() and poll_dm_fallback() coroutines — the alternative qingyun's own comment explicitly offered ("or exercise poll_proactive behavior") — using the same asyncio.run()+sleep-sentinel stub-and-redirect pattern already established by tests/proactive-dm-failure-keeps-file-behaviour.test.py. A large code-motion extraction was rejected: it would need many continue->return conversions in deeply-nested try/except blocks (real control-flow risk for zero behavior change), and since plain `git diff` treats moved code as delete+add, it would trip this repo's prose_cap:2 comment-line cap on every relocated multi-line comment (e.g. the ~13-line "Claim-by-rename" and "#1147 follow-up" comments) for no functional benefit. Both existing structural tests (proactive-suppression-marker-honored, proactive-channel-redirect-loud-failure) are left untouched, per the task constraint not to weaken them. For poll_proactive, the mutation-resistance signal is the ProactiveClaimFence terminal call (.drop() vs .confirm()), not send content: parse_markers() already blanks the body for ANY skip-matched marker independent of has_skip_action, so a [no-send] file's clean_text is "" whether or not the predicate is disabled, and a content-only check (e.g. "was deliver() called with real text") cannot see the mutation. The fence call can: disabling the predicate flips poll_proactive from .drop() (suppressed) to .confirm() (treated as sent) for that file, which is exactly the "does the suppression predicate control the terminal drop" property under review. A _FenceProxy wraps the real ProactiveClaimFence and records only calls poll_proactive itself makes through _proactive_fence() (drop()'s own internal self.confirm() call stays invisible to the wrapper), giving an unambiguous, uncontaminated signal. For poll_dm_fallback, the signal is whether the dm-result.py subprocess is invoked at all (its argv), not whether a DM was ultimately sent — dm-result.py has its own independent, redundant skip-marker check (tests/dm-result-skip-markers.test.py) that would otherwise mask a mutation of poll_dm_fallback's own predicate. check_mutation_makes_it_fail() is a PERMANENT guard, not a one-off manual step: it reads the real discord-bridge.py source, applies qingyun's exact mutation via a verified-unique string substitution to a COPY, writes it to a temp file, loads it as a genuinely separate module via importlib (the same hyphenated-filename pattern used by ~80 other tests in this repo), and asserts the real behavioral signal flips. This executes the mutated code rather than matching its text, directly answering rule #14 in REVIEW.md ("never assert on source text as a stand-in for a behavioral claim"). Manual verification (mutation applied directly to the real, tracked src/discord-bridge.py in this checkout, then reverted — not just the test's internal copy-based mechanism): $ python3 tests/proactive-suppression-marker-behavioral.test.py; echo $? PASS: poll_proactive/poll_dm_fallback behaviorally honor suppression markers, and disabling the check (mutation) makes this test fail 0 $ python3 - <<'EOF' # apply qingyun's exact mutation to both call sites ... if has_skip_action(_pp.actions): -> if False and has_skip_action(_pp.actions): ... if has_skip_action(_parsed_fb.actions): -> if False and has_skip_action(_parsed_fb.actions): EOF $ python3 tests/proactive-suppression-marker-behavioral.test.py; echo $? FAIL: poll_proactive: [no-send] file ... did not take the fence .drop() (suppressed) path — fence calls were ['confirm'], expected ['drop'] FAIL: poll_dm_fallback: [no-send] file ... reached the dm-result.py subprocess 1 $ git checkout -- src/discord-bridge.py $ python3 tests/proactive-suppression-marker-behavioral.test.py; echo $? PASS: ... 0 Both functions' predicates are proven to control the observed outcome in both directions (mutation makes it fail, revert makes it pass again). Also fixes a separate drift found mid-review: src/result_markers.py's has_skip_action() (added in f669570) was never synced to its vendored mirror at packages/ag2-sparrow/ag2_sparrow/result_markers.py, so packages/ag2-sparrow/tools/test_no_drift.py was failing. Regenerated via `python3 packages/ag2-sparrow/tools/sync_from_src.py` — the diff is the 6-line has_skip_action() addition, verbatim from src/, with no other mirrored module touched. Regression (all green): python3 -m py_compile src/discord-bridge.py src/result_markers.py tests/proactive-suppression-marker-honored.test.py tests/proactive-suppression-marker-behavioral.test.py (new) tests/bridge-marker-no-leak.test.py tests/result-markers.test.py tests/proactive-channel-redirect-loud-failure.test.py tests/health-check-proactive-quarantine.test.py tests/bridge-dedup-poll-loop.test.py tests/bridge-dedup-wrappers.test.py tests/dedup-cross-channel.test.py tests/dedup-holder-delivered.test.py tests/proactive-dm-failure-keeps-file-behaviour.test.py packages/ag2-sparrow/tools/test_no_drift.py git diff | bash scripts/review-checks.sh -> PASS No production code (src/discord-bridge.py) was modified. This commit adds one test file and syncs the ag2-sparrow mirror. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
qingyun-wu
pushed a commit
that referenced
this pull request
Aug 23, 2026
* fix(discord-bridge): honor suppression markers in poll_proactive poll_dm_fallback() checks for skip-kind markers ([no-send]/[REPLIED]/ [deduped:]) before attempting DM delivery, and its own docstring claims poll_proactive() already does the same. It didn't: a suppressed proactive-*.txt still got DM-attempted, the attempt failed (no valid send target), and fail() parked it permanently in results/undelivered/ instead of the clean discard the marker asked for. 77 files had accumulated this way. Fix mirrors poll_dm_fallback's own check: parse the body once (already done for the redirect-marker parse just below), and if any action is skip-kind, drop the claim via the existing ProactiveClaimFence.drop() path (already used elsewhere for unresolvable recipients) instead of proceeding to delivery. Regression test: tests/proactive-suppression-marker-honored.test.py FAILs on the pre-fix loop body, PASSes with the fix. * fix(tests): anchor suppression-marker guards to the skip branch, not the whole function Guard 3 (`.drop(` presence) searched the entire poll_proactive body, but the function has a second, unrelated `.drop(f, "no human user in allowFrom")` call — so the guard passed even when the skip branch's own drop() was swapped for release() (verified: a mutated .release() build passed all 5 guards before this fix). Anchor it to the same skip_idx..skip_idx+400 window Guard 5 already uses. Guard 4 (send-ordering) only checked the literal `.send(` token, which never appears on the primary text-DM path — that goes through the `deliver_text(` helper. `.send(` only occurs on file-attachment sub-paths, so the guard gave no real signal about the path that actually matters. Now checks both tokens and orders against whichever occurs first. Verified fail-before (pre-fix commit)/pass-after and against a synthetic release()-regression build; sibling tests (proactive-channel-redirect-loud-failure, health-check-proactive-quarantine) still pass. Reported by kewei-red-ag2space in review of PR #3323. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(tests): make suppression-marker check behavioral, not source-text regex qingyun-wu's second review round on #3323 cited REVIEW.md rule #14 ("never assert on source text as a stand-in for a behavioral claim") against the guard-based structural test — even anchored to the skip branch (88f3839), it still only regexes discord-bridge.py's source rather than exercising the real decision. Extract the skip check into an importable, dependency-light helper — has_skip_action(actions) in src/result_markers.py — and update poll_proactive/poll_dm_fallback in discord-bridge.py to call it instead of an inline `any(a.kind == "skip" for a in actions)`. A third site (the [deduped:] cross-channel-rejection branch) needs the full Action object, not a bool, and is left untouched — different shape, out of scope per CLAUDE.md's "fix the policy that duplicates, not every shape that happens to look similar." The test now exercises has_skip_action() behaviorally with real parse_markers() output (no-send/REPLIED/deduped all skip; ordinary text does not), plus narrowly-scoped structural pins (delegation to the shared helper, no inline reimplementation, discard-before-continue ordering) — legitimate under REVIEW.md's structural exception because each is paired with the behavioral test above, never a substitute. Verified: py_compile clean, review-checks.sh clean, 8 sibling tests unaffected (bridge-marker-no-leak, result-markers 48 tests, proactive-channel-redirect-loud-failure, health-check-proactive- quarantine 8 tests, bridge-dedup-poll-loop 4 tests, bridge-dedup- wrappers 4 tests, dedup-cross-channel 15 tests, dedup-holder-delivered 18 tests). Live post-restart round trip on the production host: restarted discord-bridge, a [no-send]-marked proactive file was cleanly dropped (no send attempt), and a new ordinary proactive file delivered normally — zero new results/undelivered/ items since restart. * test(proactive): behaviorally prove poll_proactive/poll_dm_fallback honor suppression markers qingyun-wu's second CHANGES_REQUESTED on this PR, verbatim: Blocking: the new head narrows the string search, but the regression test still does not prove that the suppression predicate controls the terminal drop. For example, changing the production branch to `if False and any(a.kind == "skip" for a in _pp.actions):` leaves parse_markers, the `kind == "skip"` text, `.drop(...)`, `continue`, and their ordering intact, so every guard in this test still passes while `[no-send]` files again fall through to Discord delivery. This is the same mutation called out on the previous head. Please exercise `poll_proactive` behavior (or extract the parse/drop decision into a small callable and test it) so that disabling the condition makes the regression test fail. f669570's extraction of has_skip_action() into src/result_markers.py was still insufficient: applying qingyun's exact mutation, `if False and has_skip_action(_pp.actions):`, inside poll_proactive and rerunning tests/proactive-suppression-marker-honored.test.py still PASSED (should have failed) — that test only greps poll_proactive's source text for `has_skip_action(`, `.drop(`, `continue`, never executes the function. Rather than extract poll_proactive/poll_dm_fallback into separate `_route_*_file` callables (the larger refactor originally proposed), this adds tests/proactive-suppression-marker-behavioral.test.py, which directly executes the real, UNMODIFIED poll_proactive() and poll_dm_fallback() coroutines — the alternative qingyun's own comment explicitly offered ("or exercise poll_proactive behavior") — using the same asyncio.run()+sleep-sentinel stub-and-redirect pattern already established by tests/proactive-dm-failure-keeps-file-behaviour.test.py. A large code-motion extraction was rejected: it would need many continue->return conversions in deeply-nested try/except blocks (real control-flow risk for zero behavior change), and since plain `git diff` treats moved code as delete+add, it would trip this repo's prose_cap:2 comment-line cap on every relocated multi-line comment (e.g. the ~13-line "Claim-by-rename" and "#1147 follow-up" comments) for no functional benefit. Both existing structural tests (proactive-suppression-marker-honored, proactive-channel-redirect-loud-failure) are left untouched, per the task constraint not to weaken them. For poll_proactive, the mutation-resistance signal is the ProactiveClaimFence terminal call (.drop() vs .confirm()), not send content: parse_markers() already blanks the body for ANY skip-matched marker independent of has_skip_action, so a [no-send] file's clean_text is "" whether or not the predicate is disabled, and a content-only check (e.g. "was deliver() called with real text") cannot see the mutation. The fence call can: disabling the predicate flips poll_proactive from .drop() (suppressed) to .confirm() (treated as sent) for that file, which is exactly the "does the suppression predicate control the terminal drop" property under review. A _FenceProxy wraps the real ProactiveClaimFence and records only calls poll_proactive itself makes through _proactive_fence() (drop()'s own internal self.confirm() call stays invisible to the wrapper), giving an unambiguous, uncontaminated signal. For poll_dm_fallback, the signal is whether the dm-result.py subprocess is invoked at all (its argv), not whether a DM was ultimately sent — dm-result.py has its own independent, redundant skip-marker check (tests/dm-result-skip-markers.test.py) that would otherwise mask a mutation of poll_dm_fallback's own predicate. check_mutation_makes_it_fail() is a PERMANENT guard, not a one-off manual step: it reads the real discord-bridge.py source, applies qingyun's exact mutation via a verified-unique string substitution to a COPY, writes it to a temp file, loads it as a genuinely separate module via importlib (the same hyphenated-filename pattern used by ~80 other tests in this repo), and asserts the real behavioral signal flips. This executes the mutated code rather than matching its text, directly answering rule #14 in REVIEW.md ("never assert on source text as a stand-in for a behavioral claim"). Manual verification (mutation applied directly to the real, tracked src/discord-bridge.py in this checkout, then reverted — not just the test's internal copy-based mechanism): $ python3 tests/proactive-suppression-marker-behavioral.test.py; echo $? PASS: poll_proactive/poll_dm_fallback behaviorally honor suppression markers, and disabling the check (mutation) makes this test fail 0 $ python3 - <<'EOF' # apply qingyun's exact mutation to both call sites ... if has_skip_action(_pp.actions): -> if False and has_skip_action(_pp.actions): ... if has_skip_action(_parsed_fb.actions): -> if False and has_skip_action(_parsed_fb.actions): EOF $ python3 tests/proactive-suppression-marker-behavioral.test.py; echo $? FAIL: poll_proactive: [no-send] file ... did not take the fence .drop() (suppressed) path — fence calls were ['confirm'], expected ['drop'] FAIL: poll_dm_fallback: [no-send] file ... reached the dm-result.py subprocess 1 $ git checkout -- src/discord-bridge.py $ python3 tests/proactive-suppression-marker-behavioral.test.py; echo $? PASS: ... 0 Both functions' predicates are proven to control the observed outcome in both directions (mutation makes it fail, revert makes it pass again). Also fixes a separate drift found mid-review: src/result_markers.py's has_skip_action() (added in f669570) was never synced to its vendored mirror at packages/ag2-sparrow/ag2_sparrow/result_markers.py, so packages/ag2-sparrow/tools/test_no_drift.py was failing. Regenerated via `python3 packages/ag2-sparrow/tools/sync_from_src.py` — the diff is the 6-line has_skip_action() addition, verbatim from src/, with no other mirrored module touched. Regression (all green): python3 -m py_compile src/discord-bridge.py src/result_markers.py tests/proactive-suppression-marker-honored.test.py tests/proactive-suppression-marker-behavioral.test.py (new) tests/bridge-marker-no-leak.test.py tests/result-markers.test.py tests/proactive-channel-redirect-loud-failure.test.py tests/health-check-proactive-quarantine.test.py tests/bridge-dedup-poll-loop.test.py tests/bridge-dedup-wrappers.test.py tests/dedup-cross-channel.test.py tests/dedup-holder-delivered.test.py tests/proactive-dm-failure-keeps-file-behaviour.test.py packages/ag2-sparrow/tools/test_no_drift.py git diff | bash scripts/review-checks.sh -> PASS No production code (src/discord-bridge.py) was modified. This commit adds one test file and syncs the ag2-sparrow mirror. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
sonichi
added a commit
that referenced
this pull request
Sep 16, 2026
Round-9 review findings:
1. The readiness-check guard from round 7 contradicted the readiness owner's
own contract: src/delivery/readiness.py treats existence as NOT readiness
(empty/unreadable/partial -> None), but the guard skip-continued on ANY
existing file when PYBIN was unavailable -- silently reading a genuinely
empty, undelivered result as done. Reproduced: PYBIN empty + a 0-byte
result -> {} with no warning, exactly the false-negative readiness.py
exists to prevent. Fixed by routing readiness-unknown into UNPROCESSED
(preserving unprocessed status) so the existing no-interpreter warning/
fail-open path handles it honestly instead.
2. Both source-text scans from rounds 7-8 were behavioral surrogates
(REVIEW.md #14) that pass on a dead comment or miss execution through a
different variable ($_cand instead of $_stub). Replaced the PYBIN scan
with a genuine behavioral witness: run the hook under `bash -x` and grep
the trace for `+ '' ...` (bash's own rendering of an empty command about
to execute) -- verified it catches a dead-comment false-pass the old regex
missed. Replaced the stub-execution scan with the seam the review named:
override `_sutando_git_is_system_stub` to classify a REAL RECORDING script
as the stub (decoupling "is-stub" from "resolves to /usr/bin/git", which
is what made a genuine witness possible at all), force xcode-select to
fail, and assert the recording script's log stays empty. Verified against
both named mutations (`"$_stub" --version` and execution through $_cand).
3. Trimmed the remaining cited comment blocks (one production, six test) to
the 2-line cap.
22/22 + 15/15 local checks pass; review-checks.sh and shellcheck clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
sonichi
added a commit
that referenced
this pull request
Sep 19, 2026
Round-9 review findings:
1. The readiness-check guard from round 7 contradicted the readiness owner's
own contract: src/delivery/readiness.py treats existence as NOT readiness
(empty/unreadable/partial -> None), but the guard skip-continued on ANY
existing file when PYBIN was unavailable -- silently reading a genuinely
empty, undelivered result as done. Reproduced: PYBIN empty + a 0-byte
result -> {} with no warning, exactly the false-negative readiness.py
exists to prevent. Fixed by routing readiness-unknown into UNPROCESSED
(preserving unprocessed status) so the existing no-interpreter warning/
fail-open path handles it honestly instead.
2. Both source-text scans from rounds 7-8 were behavioral surrogates
(REVIEW.md #14) that pass on a dead comment or miss execution through a
different variable ($_cand instead of $_stub). Replaced the PYBIN scan
with a genuine behavioral witness: run the hook under `bash -x` and grep
the trace for `+ '' ...` (bash's own rendering of an empty command about
to execute) -- verified it catches a dead-comment false-pass the old regex
missed. Replaced the stub-execution scan with the seam the review named:
override `_sutando_git_is_system_stub` to classify a REAL RECORDING script
as the stub (decoupling "is-stub" from "resolves to /usr/bin/git", which
is what made a genuine witness possible at all), force xcode-select to
fail, and assert the recording script's log stays empty. Verified against
both named mutations (`"$_stub" --version` and execution through $_cand).
3. Trimmed the remaining cited comment blocks (one production, six test) to
the 2-line cap.
22/22 + 15/15 local checks pass; review-checks.sh and shellcheck clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
sonichi
added a commit
that referenced
this pull request
Sep 23, 2026
Round-9 review findings:
1. The readiness-check guard from round 7 contradicted the readiness owner's
own contract: src/delivery/readiness.py treats existence as NOT readiness
(empty/unreadable/partial -> None), but the guard skip-continued on ANY
existing file when PYBIN was unavailable -- silently reading a genuinely
empty, undelivered result as done. Reproduced: PYBIN empty + a 0-byte
result -> {} with no warning, exactly the false-negative readiness.py
exists to prevent. Fixed by routing readiness-unknown into UNPROCESSED
(preserving unprocessed status) so the existing no-interpreter warning/
fail-open path handles it honestly instead.
2. Both source-text scans from rounds 7-8 were behavioral surrogates
(REVIEW.md #14) that pass on a dead comment or miss execution through a
different variable ($_cand instead of $_stub). Replaced the PYBIN scan
with a genuine behavioral witness: run the hook under `bash -x` and grep
the trace for `+ '' ...` (bash's own rendering of an empty command about
to execute) -- verified it catches a dead-comment false-pass the old regex
missed. Replaced the stub-execution scan with the seam the review named:
override `_sutando_git_is_system_stub` to classify a REAL RECORDING script
as the stub (decoupling "is-stub" from "resolves to /usr/bin/git", which
is what made a genuine witness possible at all), force xcode-select to
fail, and assert the recording script's log stays empty. Verified against
both named mutations (`"$_stub" --version` and execution through $_cand).
3. Trimmed the remaining cited comment blocks (one production, six test) to
the 2-line cap.
22/22 + 15/15 local checks pass; review-checks.sh and shellcheck clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
sonichi
added a commit
that referenced
this pull request
Sep 23, 2026
…lated repo's worktree (#4323) * fix(hooks): Stop-hook queue/turn-gate skips guest sessions in an unrelated repo's worktree check-pending-tasks.sh is inherited by every Claude Code session with this repo as an ancestor via CLAUDE.md, including one whose actual cwd is a worktree of a genuinely different repo (e.g. a skill-repo fix branch) that merely picked up this file. Per CLAUDE.md's "Chat-path task tracking" guest carve-out, such a session is a guest, not the core: Sutando's own task/result queue and its per-turn message discipline are the core's operating rules, not a universal Claude Code requirement, so a guest's Stop must not be held hostage to either. Detect this by comparing `git rev-parse --git-common-dir` for the CWD against the same for this hook's own repo (resolved from $0, not cwd) — worktrees of the SAME repo share one common-dir, so an own-repo worktree still counts as the core and still gets the gate; only a different repo's common-dir exits early with `{}` before the task-queue loop AND before turn_ledger.py's stop-gate. Tests: two new cases in check-pending-tasks-workspace.test.sh. Case 7 pins the defect (a pending core task must not block a session cwd'd into an unrelated repo's worktree); case 8 is the control proving the carve-out is keyed on "different repo", not "any worktree" (a worktree of THIS repo must still block). Mutation-tested: reverting the fix fails case 7 and leaves case 8 green, confirming each case measures what it claims. Stand: Echo Act IV Pro * fix(hooks): resolve the repo's git-common-dir with --path-format=absolute CI caught what this host's git couldn't reproduce: plain `git -C <dir> rev-parse --git-common-dir`, run from a DIFFERENT cwd, can print a path relative to <dir> (e.g. plain ".git") rather than to the caller. The previous fix's normalization then resolved that relative path against the CALLER's cwd, not <dir>'s — silently computing the wrong absolute path. Reproduced directly: a plain (non-submodule) repo, queried via -C from an unrelated cwd, returns ".git"; resolving that the old way computes "<caller-cwd>/.git" instead of "<dir>/.git". That collapses both directions of the guest-vs-core comparison: - case 7 (unrelated repo): the caller's own real common-dir and the miscomputed "repo" common-dir both landed on the caller's cwd -> read as SAME repo -> guest carve-out did not fire -> task wrongly blocked. - case 8 (this repo's own worktree): the worktree's real (absolute) common-dir did not match the miscomputed value -> read as DIFFERENT repo -> carve-out wrongly fired -> block wrongly skipped. `--path-format=absolute` (git >= 2.31; CI runs 2.55.0, this host 2.50.1) always prints an absolute path regardless of -C or cwd, removing the ambiguity outright -- and drops the dirname/cd/basename normalization that was reconstructing it by hand. Verified against the exact failing shape: a plain repo queried via -C from an unrelated cwd now returns an absolute path directly. Full suite (9 checks) passes locally; CI will confirm on this push. Stand: Echo Act IV Pro * fix(hooks): canonicalize the git-common-dir comparison past symlinks too Per Mini's review of this PR (#bot2bot, sonichi/sutando-skills#784 revival thread): --path-format=absolute fixes relative-vs-cwd, but two paths to the same directory can still differ as STRINGS if either crosses a symlink (macOS /tmp -> /private/tmp being the standing example in this codebase's own test helpers). Resolve both sides with `cd <dir> && pwd -P` after the git call, matching the `_real()` idiom this test file already uses for the same reason. Verified empirically that --path-format=absolute alone already handles every scenario Mini's review named (subdir-of-primary-checkout, a same-repo worktree, and a genuinely unrelated repo, tested directly against a plain non-submodule checkout) -- no live bug reproduced beyond the one already fixed. Added the pwd -P pass anyway as a costless, requested safety margin. Full suite still 9/9. Stand: Echo Act IV Pro * fix(hooks): route the Stop hook's git probes through safe git resolution keweichen (round 2, #4323): the guest-worktree carve-out's two git-common-dir probes called bare `git`, which resolves to the macOS CLT stub when the developer tools are absent -- invoking it raises a modal system dialog on every Stop, exactly what src/git_binary.py exists to prevent (REVIEW.md lesson 7). Added scripts/git-binary.sh, the shell twin of src/git_binary.py (no such bash-side resolver existed yet): PATH-first non-stub git, walking every PATH entry rather than trusting the first hit, falling back to the system git only when `xcode-select -p` confirms the developer tools are installed, or nothing. check-pending-tasks.sh now sources it and gates both probes on a resolved binary; when none is runnable it fails closed the same way the pre-fix hook always ran (proceeds as core -- cannot prove a different repo). Verified: tests/git-binary-sh.test.sh (8 new checks, pinning the same never-execute-to-probe contract as tests/python-binary-sh.test.sh) and the existing tests/check-pending-tasks-workspace.test.sh (9 checks) both pass. Mutation-tested by reverting check-pending-tasks.sh's hunk alone: the new "no bare git" / "sources git-binary.sh" checks fail exactly as expected, confirming they exercise the fix rather than passing regardless. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(hooks): non-Git bundle carve-out + symlink-to-stub in git resolver keweichen (round 2, #4323): two real blockers on the round-2 fix. 1. src/check-pending-tasks.sh's guest carve-out required BOTH CWD_COMMON_DIR and REPO_COMMON_DIR non-empty before comparing. A shipped app bundle has no .git at all, so REPO_COMMON_DIR is empty by design -- which made a genuinely foreign Git cwd read as "cannot tell" and fall through to blocking on the bundle's own queue anyway. Fixed: skip whenever CWD has a Git identity that isn't REPO_DIR's, including REPO_DIR having none; a CWD with no Git identity at all still fails closed (proceeds as core), matching the existing "no runnable git" fail-closed direction. 2. scripts/git-binary.sh's stub check compared only the candidate's DIRECTORY, not its resolved target -- so $HOME/bin/git symlinked to /usr/bin/git read as an ordinary PATH git and was returned even with no developer tools installed, and the hook then executed the actual CLT stub through the symlink. Added a hand-rolled realpath (no GNU-only `readlink -f`, no shelling to python3 -- that would re-enter the same landmine) that resolves the full candidate before comparing, and fails toward refusing (not toward "must be fine") when a symlink can't be resolved -- e.g. `readlink` itself missing from a minimal PATH. Also require a regular file, not just `-x` (a directory literally named `git` passes `-x` too). Verified: two direct reproductions before fixing (non-Git bundle + foreign Git cwd wrongly blocked; a stub-symlink resolved as non-empty), both fixed after. New regression cases: tests/git-binary-sh.test.sh gets a symlink-to-stub case (plus the same case with readlink unavailable) and a directory-named-git case (11 checks, was 8); tests/check-pending-tasks- workspace.test.sh gets the packaged-bundle deployment matrix keweichen named -- non-Git bundle + foreign Git cwd (skip) and non-Git bundle + non-Git cwd (still gates, the control) (11 checks, was 9). Mutation-tested both fixes independently: reverting each one's hunk alone fails exactly its own new check(s) and none of the others; restoring passes all 22. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(hooks): fail-open symlink bound, ambiguous-probe carve-out, CI portability keweichen (round 3, #4323): three real blockers. 1. scripts/git-binary.sh's realpath resolver stopped at a 20-hop bound without checking whether the target was STILL a symlink -- a chain longer than that (macOS permits up to 32) fell through as "resolved" to a mid-chain link that trivially wasn't the literal stub path, so a sufficiently long symlink chain to the real stub bypassed detection. Raised the bound to 40 and added the missing check: if the loop exits while `_target` is still a symlink, that's an unresolved chain, not a resolved one -- return failure (fails toward refusing, same direction as an unresolvable single-hop symlink already does). 2. src/check-pending-tasks.sh's round-2 fix (drop the "both sides non-empty" requirement) conflated two things that produce the same empty REPO_COMMON_DIR: a packaged bundle with no .git AT ALL (intentional), and a real checkout whose git probe on REPO_DIR merely FAILED (unresolved/ambiguous). Only the first should skip the guest gate. Now checks for the `.git` marker itself before deciding which branch applies: marker present -> only skip when REPO_COMMON_DIR is ALSO resolved and differs; marker absent -> any CWD Git identity is foreign. An ambiguous probe on a real checkout now fails closed (gates) instead of being read as license to skip. 3. tests/git-binary-sh.test.sh's new directory-named-git case omitted the `OSTYPE=darwin25` pin every other Darwin-specific case in this file uses -- on non-Darwin CI it silently took the non-Darwin branch (a bare `command -v git` with zero directory-rejection logic), so it could pass without exercising the code it names. Pinned, and added the missing positive control: a symlink to a REAL non-system git must still be accepted, so a fix that over-corrects into "reject every symlink" cannot pass unnoticed. Verified: finding #1's bug reproduced directly against the internal _sutando_git_realpath/_sutando_git_is_system_stub functions (a 45-hop chain), bypassing this host's own (lower-than-32) native ELOOP limit, which would otherwise mask the defect through resolve_git's outer `-f` gate. Finding #2 reproduced via a real checkout with a present-but- unparseable `.git`. New tests/git-binary-sh.test.sh cases 7c, 8b (14 checks, was 11); new check-pending-tasks-workspace.test.sh case 11 (12 checks, was 11). Mutation-tested findings #1 and #2 independently: reverting either fix's hunk alone fails exactly its own new check(s); restored, both suites fully green. Finding #3 is a CI-portability defect this host's own platform can't locally exercise as a failure (it's already Darwin) -- fixed by direct code-path reasoning and mirrored against every other Darwin-pinned case already in the file. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(hooks): identity-first carve-out, dangling-symlink marker, test activation keweichen (round 5, #4323): round 4's three fixes confirmed correct and verified independently (bounded symlink resolution, its controls, case 8 reaching Darwin logic, present-.git + failed-probe gating). Two new blockers. 1. check-pending-tasks.sh's marker-first branch discarded a KNOWN, matching identity: a REPO_DIR that is a subdirectory of a real checkout with no .git of its own still resolves correctly (git walks up to the ancestor), but the old code checked the marker BEFORE the identity and treated any non-empty CWD as foreign regardless. Reordered: a known REPO_COMMON_DIR now wins outright, and the marker only breaks the tie when it's empty. Also: `-e` alone reads a DANGLING `.git` symlink as absent (marker-absent, "intentional bundle") when it should read as present (marker-present, ambiguous, fail closed) -- added the `-L` check. 2. The activated-no-CLT proof had two real gaps: the source-scan regex (`\bgit -C`) only covered the -C'd probe, so reverting the OTHER call to bare `git rev-parse` (no -C) passed it clean -- widened to catch any `$(git ...)`. And workspace test case 6 never actually copied scripts/git-binary.sh into its fixture, so the whole git-identity block silently no-op'd on an undefined `resolve_git` rather than being exercised at all -- added the file, a recording `git` stub proving activation, a non-Git cwd so the guest branch can't short-circuit before reaching the code under test, and a stderr assertion. Also rebased onto origin/main (one commit behind) and trimmed comments introduced across this PR's rounds to the repo's 2-line cap, moving reviewer/round history out of code and into commit messages where it stays checkable. Verified: both new findings reproduced directly before fixing (a nested subdir losing same-repo identity; the source-scan gap, confirmed by reproducing the exact mutation it should have caught and didn't). New cases 12/13 in check-pending-tasks-workspace.test.sh (16 checks, was 14); case 6 reworked to prove activation, not just output shape. All checks green; git-binary-sh.test.sh unchanged at 14/14 (source-scan fix verified separately, not via a new numbered case). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(hooks): genuine no-own-.git fixture, resolver-empty integration case, comment trim Round-6 review findings: 1. Case 12 (no-own-.git subdir) never constructed its claimed scenario -- git init ran directly in the hook's own REPO_DIR, giving it a real .git. Rebuilt with a genuine outer-repo/inner-child (no .git of its own) structure; verified the OLD marker-first code fails it, current code passes. 2. Added a resolver-empty/poison-stub integration case: a symlink-to-system- git on PATH is refused by resolve_git (xcode-select forced to fail), so GIT_BIN stays empty and the hook fails closed -- while a GIT_BIN=git bypass would find that same PATH entry usable and wrongly skip. Verified the case fails against exactly that bypass mutation. Also routed the test fixtures' own bare git setup calls through the same resolver the hook uses, so the suite isn't exposed to the failure mode it tests for. 3. Trimmed remaining comment blocks over the 2-line cap in git-binary.sh, check-pending-tasks.sh, and removed a stale reviewer/issue reference from git-binary-sh.test.sh. 18/18 + 16/16 local checks pass; review-checks.sh and shellcheck clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(hooks): readiness-check PYBIN guard, checked fixture init, exact-count witnesses Round-7 review findings: 1. Fixture setup could fail vacuously under set -u (which doesn't catch command failures). Added a checked _git_fixture_repo() helper that aborts the whole suite loudly on a failed init/commit, replacing every ad-hoc "init && commit" call site. Added explicit identity assertions (both common-dir probes nonempty, and equal where intended) to cases 11-14 so a silently-broken fixture can no longer pass for the wrong reason. 2. Case 6 and case 14's activation witnesses were loose (>=2 / >=1 calls, unchecked stderr). Tightened to exact expected counts, exact argv records, and the exact rejection message. "Never executes a candidate to decide" for the poison-stub case is structurally unwitnessable without touching the real /usr/bin/git it symlinks to; that property is proven once, generically, by git-binary-sh.test.sh's existing stub-ran witness (same code path, any candidate) -- noted in a comment at the call site. Also found and fixed a real gap while verifying this: the readiness-check loop invoked "" unconditionally, so an unavailable interpreter plus an EXISTING result file produced a stray "command not found" and silently misclassified a real, non-empty result as EMPTY (masked today by the outer no-PYBIN short-circuit, but not something a future refactor could rely on). Reproduced before fixing; guarded and pinned with a new case. 3. Trimmed the two comment blocks over the cap (case 12's version-history narration moved out entirely, case 14's header condensed) plus git-binary-sh.test.sh's file header and cases 7c/8 headers. 20/20 + 14/14 local checks pass; review-checks.sh and shellcheck clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(hooks): guard the turn-ledger PYBIN call, exact-match witnesses, stub-exec test Round-8 review findings: 1. The readiness-check PYBIN guard from round 7 was incomplete: the turn-ledger call at line ~105 still invoked "$PYBIN" unconditionally, accidentally safe only because an empty-command failure's rc (127) happens not to equal the gate's explicit-refusal rc (1) -- fragile, and stderr- hidden by its own 2>/dev/null so no black-box test can see the difference. Added an explicit guard; since old and new code are byte-identical in observable output, pinned the guard's PRESENCE structurally instead (both call sites checked via source grep) rather than claim a behavioral test that cannot exist. Fixed case 6b's own gap in the same pass: the earlier no-result task was never removed, so the truly-result-only/empty-queue scenario this case claims to test never actually ran. 2. Case 6/14's assertions were tightened from substring (case ... *pattern*) to exact whole-value equality (stderr, and the whole git-calls.log against its full expected content) -- a substring match lets extra noise or extra argv on the same line slide through. Added a genuine system-stub execution witness at the git-binary.sh level: black-box execution can't be witnessed without touching the real /usr/bin/git a symlink points at, so this is a structural scan (splitting on ; && || the way the shell would) asserting $_stub is never used as a command, only tested/printed -- verified it catches the exact "$_stub" --version mutation described in the review. Also replaced the test file's own "|| echo python3" fallback (6 sites) with a safely-resolved $TEST_PY, same fix as $TEST_GIT two rounds ago. 3. Trimmed 8 more comment blocks over the cap across both test files. 21/21 + 15/15 local checks pass; review-checks.sh and shellcheck clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(hooks): readiness-owner contract, real xtrace/override witnesses Round-9 review findings: 1. The readiness-check guard from round 7 contradicted the readiness owner's own contract: src/delivery/readiness.py treats existence as NOT readiness (empty/unreadable/partial -> None), but the guard skip-continued on ANY existing file when PYBIN was unavailable -- silently reading a genuinely empty, undelivered result as done. Reproduced: PYBIN empty + a 0-byte result -> {} with no warning, exactly the false-negative readiness.py exists to prevent. Fixed by routing readiness-unknown into UNPROCESSED (preserving unprocessed status) so the existing no-interpreter warning/ fail-open path handles it honestly instead. 2. Both source-text scans from rounds 7-8 were behavioral surrogates (REVIEW.md #14) that pass on a dead comment or miss execution through a different variable ($_cand instead of $_stub). Replaced the PYBIN scan with a genuine behavioral witness: run the hook under `bash -x` and grep the trace for `+ '' ...` (bash's own rendering of an empty command about to execute) -- verified it catches a dead-comment false-pass the old regex missed. Replaced the stub-execution scan with the seam the review named: override `_sutando_git_is_system_stub` to classify a REAL RECORDING script as the stub (decoupling "is-stub" from "resolves to /usr/bin/git", which is what made a genuine witness possible at all), force xcode-select to fail, and assert the recording script's log stays empty. Verified against both named mutations (`"$_stub" --version` and execution through $_cand). 3. Trimmed the remaining cited comment blocks (one production, six test) to the 2-line cap. 22/22 + 15/15 local checks pass; review-checks.sh and shellcheck clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(hooks): split PYBIN witness by call site, fix nested-trace-depth blind spot Round-10 review finding: the round-9 xtrace witness was false-green for the turn-ledger (second) PYBIN site, in two independent ways. 1. Its fixture (a task with a result present) exits at the readiness-check's own UNPROCESSED/no-interpreter branch and never reaches the turn-ledger call at all -- confirmed by tracing: zero occurrences of turn_ledger in that fixture's trace. Deleting the turn-ledger guard entirely still left the case green. Split into two fixtures: a result-present queue isolates the readiness-check site, and only a genuinely EMPTY task queue reaches the turn-ledger site (any task, with or without a matching result, exits earlier when PYBIN is empty). 2. The regex `^\+ '' ` anchors on exactly one `+`, but bash traces a command SUBSTITUTION (both call sites use one) one level deeper -- `++ '' ...`, confirmed by direct reproduction. Widened to `^\+{1,} '' `, forced PS4 so the prefix character can't drift with the environment, and added a positive-control assertion that the regex matches a known nested-depth line before trusting either site's negative result. Verified: with the fix, both sites pass; reverting either guard makes its OWN dedicated case fail (confirmed independently for both sites this round). 24/24 + 15/15 local checks pass; review-checks.sh and shellcheck clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(hooks): argv-boundary + real xtrace-capture witnesses for #4323 round 11 keweichen's round-11 review (at head 34c8ce4) confirmed the reachability/ nested-trace fix from round 10 and found two more real gaps in the tests: 1. The recording git stub logged argv via "$*", which joins with a space -- three separate args (-C, /a, b) and one quoted arg ("/a b") serialize IDENTICALLY, so the exact-equality check proved character content, not argv boundaries. Reproduced directly: both cases produced the same log line. Fixed by recording argc + one line per argument, and added an isolated positive control proving the technique actually distinguishes the two cases (this fixture's own args never contain spaces, so the main check alone can't prove the general property). 2. The xtrace "positive control" fed a hand-built "++ '' ..." string straight to grep -- fully decoupled from the real bash -x/PS4/redirect pipeline the two site checks depend on. Reproduced directly: dropping -x from either site's real capture line left every check (including the positive control) reporting ok, on a genuinely empty trace file. Fixed two ways: the positive control now drives a known-bad script through the real PS4+bash -x+redirect mechanism at both nesting depths, and each real site now asserts a stable trace marker (TASKS_DIR=) is present before trusting an absence of the bad pattern -- both confirmed to turn the exact -x-dropped mutation into a caught failure. 26/26 local (was 24), 15/15 sibling suite unchanged, shellcheck clean (pre-existing SC2016 info-notes only, same pattern as existing lines), review-checks.sh clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(hooks): sed-slicing false-green + stub-activation witness for #4323 round 12 keweichen's round-12 review (at head 59f1afb) found two more real gaps: 1. Round-11's isolated argv positive control used sed line-range slices ('1,4p' / '5,7p') over ONE shared log file, assuming each invocation writes a fixed number of lines. Reproduced exactly as described: a regression back to "$*" recording writes one line per call (two total); the first slice silently swallows BOTH lines, the second is empty, the slices trivially differ, and the control reports ok without ever comparing the two calls to each other. Fixed by giving each invocation its own log file and doing exact whole-file equality on the complete record. 2. git-binary-sh.test.sh case 10 (the stub-is-never-executed witness) only asserted ran.log's ABSENCE. If the classifier override seam were bypassed -- resolve_git never calling it, or calling a differently-named function -- ran.log would also stay absent, for the wrong reason, and the case would pass without exercising the intended branch. Fixed by asserting the classifier override WAS invoked (its own marker), the dev-tools probe WAS invoked (its own marker), resolve_git's stdout is captured and asserted empty (not discarded), and a genuine positive control that the same recording mechanism does write when called directly. Both mutation-tested: reverting the argv fix to sed-slicing reproduces the exact false-green; renaming the classifier override function reproduces the exact bypassed-seam false-pass keweichen described, and each fix turns its mutation into a caught failure. 26/26 + 16/16 local (was 26/24... i.e. unchanged counts, content fixed; git-binary-sh grew 15->16), shellcheck clean (pre-existing SC2016 info-notes only), review-checks.sh clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(hooks): trim 7 comment-cap violations reintroduced across rounds 9-12 CLAUDE.md caps code comments at 2 physical lines with no incident/review narration. Rounds 9-12's fixes each added an explanation of the reviewer finding they addressed directly into the test-file comments, and by round 12 that had grown to 7 oversized runs (up to 17 consecutive lines) carrying review-history narration this repo's own rule puts in the commit/PR body, not the source. Trimmed all 7 to the constraint each exists to state, in at most 2 lines each; the mutation-tested behavior is unchanged -- this is comment-only. 26/26 + 16/16 unchanged, shellcheck clean, review-checks.sh clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(hooks): collapse 4 adjacent-2-line comment runs to single <=2-line blocks keweichen's round-13 review: two back-to-back 2-line explanations with no blank/code separator between them are still one contiguous 4-line run under CLAUDE.md's cap, not two compliant 2-line comments. Round-12's trim reduced line count but kept this shape in 4 places. Collapsed each entire run to a single tight <=2-line comment instead of two stacked ones. Mechanically verified: 0 comment runs over 2 lines remain in the diff (down from 4). 26/26 + 16/16 unchanged (comment-only), shellcheck clean, review-checks.sh clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(hooks): isolate the PYBIN-empty comment from the baseline block above it keweichen's round-14: their mechanical audit had only grouped diff-added comment lines; a full-source scan that intersects each physical comment run with this PR's added lines found one more -- src/check-pending-tasks.sh: the PR-added 2-line PYBIN-empty comment sat immediately after a pre-existing 6-line baseline comment with no separator, forming one 9-line run under the cap. Inserted a blank line before the new comment to isolate it, without touching the baseline block's own content. Verified with the same intersection method across all 3 touched files: 0 violations. 26/26 + 16/16 unchanged, shellcheck clean, review-checks.sh clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(hooks): stub classification uses filesystem identity, not spelling git-binary.sh's -ef fix mirrors src/git_binary.py's new same_file() check: os.path.realpath / the shell realpath twin do not case-fold on a case-insensitive volume, so a case-variant spelling of the system stub (e.g. /USR/BIN/GIT) shared the real stub's inode but compared unequal by `=`/`==` and slipped past the guard. Reproduced directly on this checkout's case-insensitive volume: /usr/bin/git, /USR/bin/git and /USR/BIN/GIT all report the same st_ino. The added shell test (case 11) fails against the old `=` comparison ("expected [], got [/USR/BIN/git]") and passes with -ef; the added Python tests (SameFileHelper + test_case_variant_alias_of_the_stub_is_still_recognized) cover the same defect at the select_git() level. Addresses keweichen's review at b281d71. Stand: Echo Act IV Pro Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(hooks): identity-probe failure fails toward the stub, not away from it An unreadable candidate is unknown, not proven non-stub. _same_file caught either stat() OSError and returned False, so select_git handed back an unverified binary with no CLT check on a race/permission failure -- the exact modal-dialog bug this module exists to prevent. Split the two stats: the candidate failing is now treated as the stub (fails toward caution); SYSTEM_GIT itself being absent still means nothing can be it. Fixed two comment-cap violations the automated review-checks.sh gate cannot see (it scans .py only, not .sh): scripts/git-binary.sh's -ef comment and the new shell test's header, both trimmed to <=2 lines, the latter also stripped of a PR/reviewer reference per CLAUDE.md's comment rule. Added tests: SameFileHelper::test_unreadable_candidate_fails_closed_ toward_the_stub exercises the fixed policy directly; SelectGitOrdering:: test_default_wiring_recognizes_a_real_hardlinked_alias exercises the production composition (default realpath AND default same_file together, no mocks) via a real hardlinked file. Replaced 5 uses of a bare, never- created "fixture-bin/git" path string with a real temp file -- the fix makes an unstatable candidate fail toward "is the stub," so a synthetic nonexistent path silently started depending on the host's actual CLT install state instead of testing the intended logic. Addresses keweichen's round-17 review at b0b02dd. Stand: Echo Act IV Pro Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(hooks): a reference-side stat error is unknown, not proof of absence _same_file's SYSTEM_GIT-side stat caught every OSError uniformly and returned False ("nothing can be the stub"), but only FileNotFoundError actually proves absence -- a PermissionError/EIO/sandbox denial on the reference path leaves identity unknown, and reopens the modal-dialog bug for a candidate whose own stat happens to succeed. Split that except by type: FileNotFoundError still returns False (safe); every other OSError now returns True (unknown -> fail toward "could be the stub"), matching the candidate side's existing direction. Addresses keweichen's round-18 review at 73d230c. Stand: Echo Act IV Pro Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(hooks): the shell resolver gets genuine same/distinct/unknown states Python's twin already distinguished a candidate-side stat failure (always unknown) from a reference-side one (ENOENT = absent/safe, anything else unknown) -- the shell resolver still only had `-ef`, a single boolean that cannot represent "unknown" at all, so any stat failure on either side silently read as "distinct" and returned the candidate with no CLT check. Added `_sutando_git_stat_state()`: a real `/usr/bin/stat` call (its own inode, never in the CLT-stub hardlink group, so always safe to invoke), distinguishing ENOENT (genuinely absent) from any other failure (unknown) by its stderr text -- exit 0/1/2 for exists/absent/unknown. Rewired `_sutando_git_is_system_stub` around it: candidate-side failure of any kind fails toward "could be the stub" (matching the Python side); on the reference side, only confirmed absence clears a candidate, everything else stays cautious. Added the two controls this needed: a candidate that fails its own stat (a TOCTOU-shaped vanish since resolve_git's own -f/-x check), and a reference-side failure that is NOT absence (permission/IO), with a positive control proving genuine absence still resolves correctly. Addresses keweichen's round-19 review at 3786977. Stand: Echo Act IV Pro Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(hooks): the final identity compare is captured values, never a fresh -ef Two real gaps. First, /usr/bin/stat -f is BSD-only syntax; on Linux (the shell suite's own OSTYPE=darwin25 cases run on real Linux CI hardware) GNU stat reads -f as filesystem mode with no format flag, so every probe failed and every candidate misclassified. Split flag selection into a pure function keyed on the REAL kernel (/usr/bin/uname -s, absolute -- several existing tests deliberately run under a PATH with no /usr/bin, where a bare `uname` does not resolve either) -- OSTYPE stays the test-only knob it always was, never consulted for the flag itself. Second, even with per-side unknown/absent handling, the classifier still finished with a fresh `[ "$_resolved" -ef "$_sb" ]` -- a stat racing between the earlier checks and that line collapses to the same "false" a genuinely distinct file produces, reopening the exact bug one line later. Replaced it: the new stat helper prints dev+ino on success, so both sides' identities are already-captured values by the time they are compared -- the compare itself touches no filesystem, so nothing can race it. Addresses keweichen's round-20 review at e0837a0. Stand: Echo Act IV Pro Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(hooks): force C locale on the identity probe, tighten the -ef pin Non-blocking hardening from round-21 review, implemented because it's real and cheap: the ENOENT-text match in _sutando_git_stat_id depends on stat's error string being English, which a non-C locale on a real GNU host could localize -- LC_ALL=C forces it. Not observable on this host (BSD stat's error text stays English regardless of locale here, confirmed by testing under de_DE.UTF-8), so this is a defensive fix for the Linux path, not a demonstrated local bug. Also tightened the shell test's own structural -ef pin: it grepped a relative path and collapsed "no match" (rc=1, the real pass) with "grep itself failed" (rc=2) into the same "ok" branch. Now absolute-pathed and rc-distinguished, plus a new pin asserting LC_ALL=C precedes the stat call. Stand: Echo Act IV Pro Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(hooks): anchor the LC_ALL=C pin to the command, not the comment test 15b's `grep -q 'LC_ALL=C.*stat'` matched the comment explaining the fix as readily as the fix itself -- reconstructed the exact mutation (strip LC_ALL=C from the executable _out= line, leave the comment) and confirmed the old pattern stayed green against it. Anchored the pattern to a line starting with `_out="$(LC_ALL=C /usr/bin/stat`, which only the executable assignment can match, and added the mutation as a negative control run every test pass rather than a one-off manual check: strip LC_ALL=C from a temp copy's command line, assert the anchored pattern now refuses. Addresses keweichen's round-22 review at 4827fe3. Stand: Echo Act IV Pro Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(tests): case 15b's negative control could false-green on a broken setup An unchecked mktemp/sed left the mutation-file grep able to return rc=2 (missing file), which the plain if/else read the same as rc=1 (refusal) -- the exact rc-collapse case 15 above it already guards against. Check the lab dir and mutated file were actually built, then classify grep's rc with the same 0/1/* case case 15 uses. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(hooks): a failed repo-side git probe must fail closed, not guest The guest-worktree carveout treated an empty REPO_COMMON_DIR the same whether it meant "genuinely not a git checkout" or "the -C probe (or its canonicalization) failed for an unrelated reason" -- and in the markerless same-repo case, the second reading wrongly took the guest exit. Both enforcement gates (task queue + turn-ledger) were then skipped on a core session for a transient/path-specific failure. Two fixes: retry the repo-side probe via a plain cd'd rev-parse when the -C form comes back empty, and track the pre-canonicalization result separately so a value that resolves but then fails to cd is treated as ambiguous (fall through to the gate) rather than as no identity at all. 2 new pinned cases reproducing the exact cross-product with a real git wrapper (fails only a bare -C invocation; a genuinely distinct binary, not a symlink, so resolve_git() doesn't mistake it for the CLT stub) and a wrapper that hands back a path it then deletes. Both fail against the parent commit, pass here. Existing 27 cases + git-binary-sh (29/29) + git-binary-resolution (36/36) unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(hooks): confirmed-absent gates the guest exit, not any probe failure qingyun-wu (round 25, at 81f2432): the retry only closes the case where -C fails but a cd'd retry succeeds. If BOTH forms fail, the markerless-child layout still fell through to the guest exit even though nothing confirmed the repo was actually unrelated. Now the guest exit requires git's own stderr to say "not a git repository" on both attempts; any other failure (permission error, transient I/O) falls through to the gate as ambiguous instead. Added case 12d (mutation-tested: fails at the parent commit, passes here) covering a both-probes-fail error that never claims "not a git repository". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(hooks): the identity probes force a stable locale and ignore an inherited discovery ceiling keweichen's and qingyun-wu's round-25 findings, confirmed by direct execution. Both repo-identity probes in check-pending-tasks.sh inherited whatever the caller's environment happened to set, and either inheritance alone could turn a real, still-ours directory into a false "confirmed absent": - GIT_CEILING_DIRECTORIES stops git's ancestor walk at a caller-chosen boundary. A no-own-.git child whose real repo sits just past that boundary gets the exact same "not a git repository" text a genuinely different repo would produce -- confirmed on real git 2.50.1: setting the ceiling to the child's own parent makes the identical fixture from round 12 (a no-own-.git child of a real repo) fail both probes and silently guest-exit, losing a real pending queue. - The classification's only signal is an English substring match (line 52/now line 57). Git localizes its diagnostics; this repo's git build has no NLS support to prove it live, so the reviewers used a translating wrapper (this fix's tests do too) -- but the code's own logic is locale-fragile regardless of what any one build can currently produce: a genuinely non-Git bundle in a foreign Git cwd, under any locale that DOES translate the message, is no longer recognized as absent and the core's queue wrongly gates an unrelated session. Fixed by forcing `env -u GIT_CEILING_DIRECTORIES LC_ALL=C LANGUAGE=C` on both probes (and the `-C`-vs-`cd` fallback retry) -- the probes' whole purpose is a full, unambiguous identity answer, so neither a caller's ceiling nor its locale should ever be allowed to narrow that answer. 2 new cases (12e: real GIT_CEILING_DIRECTORIES against round 12's own fixture; 12f: case 9's non-Git-bundle-plus-foreign-cwd fixture behind a translating git wrapper), mutation-tested against the parent (e9fe2e1) by swapping the hook file in place and restoring it via `cp` -- exactly these 2 fail there, nothing else. 29/29 (git-binary-sh) + 36/36 (git-binary-resolution) + all cases in check-pending-tasks-workspace unchanged and green. Still outstanding, named not attempted: the live post-restart witness (a foreign-repo session completing while a same-repo session with the same pending queue blocks) both reviewers have asked for across several rounds now -- restarting the core is owner-gated, not mine to run. Stand: Echo Act IV Pro Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(hooks): clear inherited GIT_DIR/GIT_COMMON_DIR from the identity probes keweichen and qingyun-wu both found the same real bug: the guest carve-out in src/check-pending-tasks.sh only unset an inherited GIT_CEILING_DIRECTORIES before probing, but left GIT_DIR and GIT_COMMON_DIR intact. A foreign repository whose caller inherits either variable pointed at this repo's own git-common-dir makes the CWD probe report OUR identity instead of its own, so the guest is misclassified as core and its unrelated Stop turn gets held by this repo's task queue. Reproduced directly on real git 2.50.1: $ cd /tmp/foreign-repo && GIT_DIR=<sutando-common-dir> \ env -u GIT_CEILING_DIRECTORIES LC_ALL=C git rev-parse --git-common-dir <sutando-common-dir> # should be the foreign repo's own .git and end-to-end through the actual hook with a pending task in the queue: before the fix it returned a block decision for an unrelated foreign worktree; after, '{}'. Fixed by adding both variables to the same env -u list the ceiling override already uses. Also fixed, both named by keweichen on this round's own diff: - tests/check-pending-tasks-workspace.test.sh's locale case (12f) merged stderr into the captured output and asserted exact equality with '{}' -- a real hosted Bash emitting its own "cannot change locale" warning to stderr (glibc-specific; reproduced here via a wrapped bash that injects the exact reported line) made a correct production result fail the test. Split stdout/stderr; assert '{}' on stdout only. - 3 comment blocks over the repo's 2-line cap (this file's prose-cap check only covers .py per REVIEW.md's checks.prose_exts, so .sh slipped through uncaught) trimmed to 2 lines each; the stale "line 52" reference in one of them (already wrong -- the real match had drifted to line 58) removed rather than updated, since a line-number citation goes stale on the next edit regardless. 2 new regression cases (12g/12h), mutation-tested against the parent (3f30c3f): swapped in the parent's version of the one changed line (via git show, never git checkout), confirmed exactly those 2 fail against it, restored byte-identical. Full check-pending-tasks-workspace suite (34 cases) + git-binary-sh (29/29) + git-binary-resolution (36/36) green. Stand: Echo Act IV Pro Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(hooks): claimed_by_a_worker refuses when task_dispatch.py itself is missing A minimal packaged bundle (the deployment-matrix test fixtures ship only check-pending-tasks.sh + git-binary.sh, no src/delivery/) has no worker-pool capability at all. Python's exit code for "script file not found" is 2, the same code claimed_by_a_worker() already treats as "deliveries root unreadable -> hold, don't report to the core". The two causes collapsed onto one branch, so a bundle-mode task got silently treated as claimed-by-a-worker instead of gating -- reproduced live via bash -x, root-caused to this exact conflation. Fix: check the script file exists before invoking it. Absent -> no worker-pool capability -> not claimed (return 1), never "hold". Present-but-erroring -> unchanged (rc 2 still holds, per its own contract). Found while rebasing #4323 onto a moved main; confirmed pre-existing (byte- identical region on the pre-rebase head) and now confirmed CI-visible on this exact push (typecheck + node & shell tests). All 9 previously-failing cases in tests/check-pending-tasks-workspace.test.sh now pass; full suite green. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(hooks): the guest exit no longer bypasses an enrolled worker's own gate The guest-cwd carve-out (a foreign repo's Stop must not gate on Sutando's queue) fired unconditionally, before the worker-mode branch further down ever ran. A worker-pool worker legitimately runs with a foreign cwd (spawn_worker.py, start-cli.sh both support a --cwd outside this repo), so an enrolled worker (SUTANDO_INSTANCE_ID set) could exit as a guest and skip its own delivery/turn gate, ending a turn with delegated work unanswered while the core correctly assumed the worker had it. Both guest-exit branches now require SUTANDO_INSTANCE_ID to be unset. An ordinary guest (no instance id) is unaffected. Also trims three comment blocks that exceeded the 2-line cap (review-checks.sh's prose-cap only covers .py, so it missed them in this .sh file). Adds a foreign-repo enrolled-worker regression (pending + ready-result cases) alongside the existing ordinary-guest control, so the distinction this fix depends on stays pinned. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * test(hooks): trim two comment runs past the 2-line cap CLAUDE.md's comment rule (at most 2 physical lines) missed these two in the new worker-carve-out test cases; also drops the PR-number reference, which belongs in the commit/PR history, not the code. * fix(hooks): a marked core session in a foreign cwd is the core, not a guest Both guest exits exempted only an enrolled worker, so the launcher's own core session, which carries SUTANDO_CORE_SESSION=1 and may run with a foreign cwd, took the guest exit and skipped the queue and turn-ledger gates. An identified session is now either a worker or the marked core, and both exits key on that; the foreign-repo and bundle suites pin the marked-core case beside the guest and worker controls. Stand: Echo Act IV Pro Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This was referenced Oct 9, 2026
Merged
This was referenced Oct 17, 2026
Open
This was referenced Oct 19, 2026
Merged
Open
sonichi
added a commit
that referenced
this pull request
Oct 24, 2026
…ish last Round 6 of #4399 (Susan's and Kewei's CHANGES_REQUESTED at de30fbe). Blocker: the move re-checked the generation and then unlinked the live NAME, so a writer replacing tasks/<name> in that window lost its new task with errors=[]. No code unlinks a live name any more. The contract lives in core: - `task_archive.claim_generation` takes the name by one atomic rename into `<name>.claim-<pid>-<token>` in the same directory (same filesystem, never a copy); `place_claim` judges the claim through its own descriptor (`file_generation`: dev:ino:size:mtime_ns) and only a match is placed by `move_without_clobbering` (now the exported owner of `.N` naming, taking directory handles so no path is re-resolved). A mismatch links the claim back to its live name without replacing; a name a writer already retook leaves the claimed bytes quarantined under the claim name, recorded and reported. `move_generation` composes the two. `_move_without_clobbering` keeps its callers' behaviour: they move files the bridge itself already processed, for which no planned generation exists. - `result_publish.publish_text(..., exclusive=True)` commits by link and refuses an existing name; `task_transition` no longer carries its own link + fsync. Both vendored copies synced (drift guard green). - `task_transition.transition()` is a staged, journaled transaction with `tasks/` and `tasks/archive/` opened once as O_DIRECTORY|O_NOFOLLOW handles that stay open through the pass and its recovery; identity is re-checked before each claim, so a directory swapped for a symlink after pre-flight is refused, never followed. The journal (in the archive dir, beside a flock'd lock the owner holds while alive) records `planned`, each claim, each landing or quarantine, then `publishing` with the reserved notice name; it is removed when the notice commits. The notice is rendered from the FINAL landed names (`task-a.txt.1` after a collision) and published last. Recovery continues a dead owner's journal from its stage: an unplaced claim is placed, a placed-but-unrecorded one is found in the archive by its generation, a reserved notice is published unless it already exists, then the journal goes. Each generation ends with one copy, live or archived; one notice iff publication committed; a second recovery pass finds nothing. - classify.py: `recovery_plan()` keeps the planned body (within BODY_MARGIN_B of the ceiling) and lists `pending_notices`; `apply_plan()` renders the notice from `landed` and reports `quarantined`/`recovered`. The bullet strips a `.N` tail when comparing file and id; the requeue line tells the owner to drop the tail. Nits: error lines say where a file is (back live / quarantined as claim / archived by another pass); the ceiling pin is labelled a REVIEW.md #14 data pin; the bare-name docstring no longer mentions os.replace. Tests: task-transition 15 -> 31 (replaced between the generation check and the claim, name retaken, writer re-creating the name mid-move, archive dir swapped after pre-flight, `.N` named in the notice, crash after each stage recovered once, live pass's journal never taken, handles held across recovery); task-archive 39 -> 48 (the contract on task_archive itself, handles only, cross-device through handles); classify 132 -> 134; result-publish 22 -> 26. Mutation-checked: fstat verify removed (both suites), give-back by replace, directory re-check removed, lock ignored, publish-before-moves, reserved name not reused — each fails its test. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This was referenced Oct 26, 2026
This was referenced Nov 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds common voice agent errors (API key, port conflict) and restart.sh.