Skip to content

Harness fixes: standing-goal Stop hook, Read-path guard, comment-guard fixups, Windows notifications, fleet status - #1

Merged
TAJD merged 4 commits into
mainfrom
wt/dev-tooling
Sep 6, 2026
Merged

TAJD merged 4 commits into
mainfrom
wt/dev-tooling

Conversation

@TAJD

@TAJD TAJD commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Summary

Closes out the DEV harness-bug standing goal, in priority order:

  • DEV-44/DEV-45 — spawned workers with a standing-goal prompt had no Stop hook enforcing it, so they parked at the next ordinary turn boundary with the goal unmet. zj-claude.sh --keep-going now installs a session-scoped Stop hook (keep-going-hook.sh) that blocks stop until .claude/goal-done exists (bounded by KEEP_GOING_MAX, default 40, with a .claude/goal-stop human abort).
  • DEV-46 — investigated as a Windows hang (no tool result ever appended after gh pr create). Verified directly against the transcript: 3 gh pr create calls (not 1), the PR link is present right after the 3rd, and the queue was fully drained at the last recorded timestamp — the original "stalled" diagnosis doesn't hold up against the actual transcript. Closed as a false stall diagnosis; no code change.
  • DEV-39 — Read/Edit/Glob/Grep paths resolving into ~/.claude/projects/<key>/ (the transcript store) instead of the repo root, 78 times across 53 sessions per the emergent-tools digest. A prose CLAUDE.md rule wasn't working; fix-project-path-hook.sh (PreToolUse) now blocks and rewrites the suggestion directly, fails open on malformed input.
  • DEV-28/41/43/49 — the stop-writing-comments hook (separate repo/PR, see below) had 4 false-positive/negative classes: regex literals misread as // comments, bare HTML placeholders flagged as prose, git-plumbing Bash commands falsely audited, and comment restores/moves permanently denied. Fixed as one cluster with one test suite.
  • DEV-11 — zellaude-hook.sh's notification path had Darwin/Linux branches and nothing for Windows. Replaced both with a single OSC9 escape (ESC ] 9 ; <text> ESC \) written to /dev/tty, which zellij passes through to the host terminal. Unfocused mode's proper fix needs zellij 0.45 (PR 5485, not yet released — confirmed via zellij --version / scoop info zellij, both 0.44.3); it now falls back to Always with a one-time stderr warning instead of silently never firing.
  • DEV-14 — zellaude-hook.sh already emitted a JSON payload per hook event but never aggregated it. It now upserts into a shared state file (~/.config/zellij/plugins/zellaude-fleet-state.json, mkdir-lock guarded); zj-fleet-status.sh renders one line per worker (pane, ticket slug, event, idle seconds, PR state via cached gh pr list).
  • DEV-15 — judged not worth building yet, not implemented. OSC133 shell integration needs zellij 0.45 (PR 5460) — same version gap as DEV-11, but harder: zellij itself must parse the marks, there's no passthrough substitute. Documented on the ticket, left in backlog for revisit once 0.45 ships.

The comment-guard fixes (DEV-28/41/43/49) shipped as a separate PR in the stop-writing-comments repo, now CI-green on all 6 platform/Node combinations: TAJD/stop-writing-comments#1

Test plan

  • keep-going-hook.test.sh — 4/4 (block-until-done, goal-done allows stop, goal-stop aborts, max-tries exhaustion allows stop)
  • fix-project-path-hook.test.sh — 5/5 (misresolved paths corrected, normal paths pass silently, Glob path variant, Bash-with-no-path passes, Windows-backslash variant), plus a fail-open fix for malformed JSON input verified before/after
  • zellaude-hook.sh OSC9 notification path exercised directly with crafted stdin: Always mode, Unfocused fallback + one-shot warning, 10s rate limit, non-PermissionRequest events, malformed JSON fail-open
  • zj-fleet-status.test.sh — 9/9 (state upsert, overwrite-in-place, multi-pane render, missing/malformed state file handling)
  • All hooks tested by direct crafted-stdin invocation, never by relying on live firing in this session, per the standing goal's own constraint

HUMAN CHECK

  1. DEV-11 (Windows OSC9 notification) — trigger a real PermissionRequest in a zellij pane on Windows Terminal (run Claude Code without --dangerously-skip-permissions, let it ask for a tool). Expect: bell + a "Claude Code — Permission requested" toast within ~30s, suppressed on a second request inside 10s. This is the one thing I could not visually verify from this session — I confirmed the OSC9 byte sequence is well-formed and zellij 0.44.3 doesn't error on it, but not that a toast actually renders.
  2. DEV-44/45 (--keep-going) — spawn a throwaway worker with bash ~/.claude/scripts/zj-claude.sh --keep-going test-tab <dir> "say hi then try to stop" and confirm it gets re-prompted instead of ending, then touch .claude/goal-done in its worktree and confirm it's allowed to stop.
  3. Revert — each ticket's fix is an isolated commit (dc6386d, d8608a3, 68c0309, 4d5fd70); git revert <sha> on any one of them cleanly undoes just that fix. No settings.json changes beyond the one new PreToolUse hook entry for DEV-39 (fix-project-path-hook.sh), which is additive and itself fails open.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EAvmyrEwiqgQhbaA3w4TsM

TAJD and others added 4 commits September 6, 2026 08:46
…awns

DEV-44/DEV-45: zj-claude.sh spawned autonomous-goal workers with no
mechanism to enforce the goal to completion, so they parked at the next
ordinary turn boundary with the goal unmet. --keep-going installs a
session-scoped Stop hook (keep-going-hook.sh) that blocks the session
from stopping until it creates .claude/goal-done, bounded by a max
continuation count (KEEP_GOING_MAX, default 40) so a broken done-when
signal can't loop forever. A human aborts early via .claude/goal-stop.
Opt-in per spawn so a one-shot ask still stops normally.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EAvmyrEwiqgQhbaA3w4TsM
zellaude-hook.sh's PermissionRequest notification path had a Darwin
branch (terminal-notifier/osascript) and a Linux branch (notify-send)
but no Windows branch at all - on Windows a blocking permission
request produced only a terminal bell and nothing else surfaced it.

Replaced both platform branches with a single OSC9 (ConEmu-style)
toast escape sequence written to /dev/tty. zellij passes unrecognised
OSC sequences through to the host terminal, and OSC9 is understood by
Windows Terminal, iTerm2, and others, so this covers all three
platforms with one code path instead of two platform-specific ones
that silently degrade on a third.

Unfocused mode previously used osascript/xdotool frontmost-window
checks with no Windows equivalent, so it silently never fired there.
The ticket's proposed fix (read zellij's forwarded host focus state)
needs zellij 0.45 (PR 5485), not yet released - installed/available
via scoop is still 0.44.3. Rather than ship a focus check that can't
work yet, Unfocused now falls back to notifying Always with a
once-per-pane stderr warning, so the degradation is visible instead
of silent (which was the original bug). Revisit when 0.45 ships.

Click-to-focus (the terminal-notifier -execute callback) is dropped:
OSC9 carries no callback. Tracked separately as DEV-12.

Tested by invoking the hook directly with crafted stdin (Always mode,
Unfocused mode incl. one-shot warning, rate-limit window, a
non-PermissionRequest event, and malformed JSON for fail-open) rather
than relying on live firing in this session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EAvmyrEwiqgQhbaA3w4TsM
…jects/<key>/ (DEV-39)

The existing CLAUDE.md prose rule ("confirm a path is relative to the
repo root, not ~/.claude/projects/<key>/") did not change behaviour:
the emergent-tools digest shows 78 occurrences across 53 sessions of
Read failing on a path resolved against the transcript-store directory
instead of the actual repo. A classification pass correlating
tool_use/tool_result pairs across all local transcripts (not just the
5 examples in the digest) found this class dominates at 44% of the 78
occurrences, ahead of genuinely-absent paths (35%), POSIX /tmp on
Windows (10%), and tool-artifact-assumed-to-exist (5%) - the latter
two are out of scope here and noted as follow-ups rather than folded
into this fix.

Since prose guidance can't compete with a wrong path already in
context, this adds a PreToolUse hook (Read|Edit|Write|Glob|Grep) that
detects a path routed through .claude/projects/<key>/, computes the
likely intended path relative to the real cwd, and denies with the
suggested correction instead of letting the tool fail with a bare
"File does not exist."

Tested by invoking the hook directly with crafted stdin (misresolved
path denied with correct suggestion, normal path passes, Glob's
`path` field denied, a Bash tool call with no path field passes, and
a real Windows-backslash-style payload denied) - see
fix-project-path-hook.test.sh, 5/5 passing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EAvmyrEwiqgQhbaA3w4TsM
zellaude-hook.sh already emits a JSON payload on every hook event but
only forwards it to the zellaude zellij plugin - nothing aggregates it
across workers. Sink each event into a shared JSON map (keyed by pane
id) at ~/.config/zellij/plugins/zellaude-fleet-state.json (or the
Windows AppData equivalent, matching the existing settings-file
lookup pattern), guarded by a short mkdir-based lock since many
worker panes write the same file concurrently.

zj-fleet-status.sh reads that map and prints pane id, ticket slug
(derived from the worktree dirname), last hook event, seconds idle,
and PR state via `gh pr list --head wt/<slug>` (cached 30s so a run
doesn't hammer the API once per pane). Plain script + on-demand
floating pane, per the ticket's own steer away from a third wasm
plugin when a script gets most of the value.

zj-fleet-status.test.sh drives both scripts directly with crafted
hook stdin (never live firing): upsert creates/overwrites per-pane
entries without disturbing others, the renderer picks them up, and
both a missing and a malformed state file degrade sanely. 9/9 passing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EAvmyrEwiqgQhbaA3w4TsM
@TAJD
TAJD merged commit 31e9c50 into main Sep 6, 2026
TAJD added a commit that referenced this pull request Sep 6, 2026
fix(zj-claude): port --keep-going onto the current script, restoring trust pre-accept

PR #1 was authored against an older zj-claude.sh. Merging it would have reverted
three fixes that were live in the chezmoi target: workspace-trust pre-acceptance,
the post-spawn transcript verification, and the @file prompt-mutation fix.
Losing trust pre-accept is the worst of these - the trust dialog swallows the
prompt auto-submit and the session hangs looking alive.

Re-added the target as the source of truth, then reapplied the --keep-going
feature on top of it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dYTG2kvngJ875otpDRVHm
@
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant