Skip to content

orchestrate: scope the hooks to the owning session; narrow the Stop check - #114

Merged
gilesknap merged 3 commits into
mainfrom
fix/orchestrate-hook-scope
Oct 14, 2026
Merged

gilesknap merged 3 commits into
mainfrom
fix/orchestrate-hook-scope

Conversation

@gilesknap

Copy link
Copy Markdown
Contributor

Closes #107
Closes #109

What changes

Hooks act only in the session that owns the goal (#107). The pointer
~/.claude/orchestrate/active gains a session=<session_id> line. Every hook
still requires the pointer and a cwd at or below the launch directory, and now
also requires the hook input's session_id to equal the owner. Any other
session in the same directory gets no SessionStart block and no Stop nudge,
and its SessionEnd writes nothing.

How a session becomes the owner:

  • goal.sh runs in the model's Bash tool and cannot see the session id, so
    goal.sh start and goal.sh resume write the pointer with no owner (no
    session= line). While a goal has no owner, every hook is silent.
  • The Stop hook of the turn that ran goal.sh start or goal.sh resume
    records its own session_id. Only the command word counts
    (bash <path>/goal.sh resume, "$CLAUDE_PLUGIN_ROOT/scripts/goal.sh" resume);
    a grep or echo that mentions it does not. Running /orchestrate resume
    in another session is therefore an explicit takeover, and the old owner goes quiet.
  • A session that takes the goal over this way never had a SessionStart block,
    so on that turn the Stop hook gives it the same checks as additionalContext:
    the crash check against the previous transcript, Now entries with no
    report, and the scratch report. The SessionStart block and this one share
    one function in lib.sh. goal.sh resume of the goal that is already
    active no longer rewrites state.md, so this crash check still compares
    against the last real write.
  • /clear starts a new session id. The owner's SessionEnd with
    reason=clear writes session=cleared:<old id>, and the SessionStart with
    source=clear that follows claims it. A fresh session (source=startup),
    a fork, or another session's Stop never claims a cleared goal.
  • /compact and claude --resume keep the session id. /fork and
    --fork-session leave the goal with the original session. Switching away
    with /resume leaves it with the conversation you left. The how-to says so.

The scheme uses only the documented session_id hook input and needs no
new files. A pointer written by 0.2.0 has no session= line: after
upgrading, run /orchestrate resume once in the orchestrating session
(documented in the how-to).

Stop hook: reads are not changes (#107). Bash commands are split into
simple commands (&&, ||, ;, |, newline, and the ( or { that opens
$(...), a subshell or a group). A verb counts only as the command word of
its simple command, after any VAR=value, a shell keyword (do, then,
if, ...) or a wrapper (env, command, exec, nohup, time,
timeout N). So grep "git commit" and echo "git checkout" are not writes,
and url=$(gh pr create --fill) is one.

  • git (also with -C <dir> or --no-pager): commit, push, rebase, reset,
    checkout, switch, merge, cherry-pick, revert, restore, pull, stash,
    worktree add|remove|move|prune, and branch with a delete, move, copy or
    force flag. Not push --dry-run / push -n, stash list|show. A flag
    inside a quoted argument (commit -m "... --dry-run") is ignored.
  • gh: pr create|merge|edit|close|reopen|comment|review|ready|checkout,
    issue create|edit|close|reopen|comment|transfer|delete|lock|unlock|pin|unpin,
    release create|edit|delete|upload. gh api counts only with a non-GET
    -X/--method or with -f/-F/--field/--raw-field/--input.
    gh api graphql counts only for a mutation anywhere in the command,
    including a multi-line -f query=, or a -F query=@file.

Stop hook: only a real write to state.md counts (#109). The turn counts
as recorded only for an Edit, Write or NotebookEdit of <goal>/state.md, or a
goal.sh verb that writes it (start, resume, and touch, now and
land from the sibling PR). A command that only names state.md (cat,
grep, sed -n) is a read and no longer counts. Writing a brief, the log, a
map, scratch or the status file does not silence the nudge either.

Also: SKILL.md, README and docs/how-to/use-orchestrate-mode.md describe
ownership. Plugin version 0.3.0.

Tested

bash tests/orchestrate_plugin.sh: 224 passed, 0 failed (112 before this change).
New cases:

  • a goal with no owner: SessionStart and Stop are silent. goal.sh list and
    commands that only mention goal.sh (grep, echo, sed) claim nothing.
  • goal.sh resume and goal.sh start each claim the goal. The claiming turn
    is not nudged and gets the context block.
  • a second session in the launch directory: SessionStart, Stop (after an
    Edit plus an Agent call), SessionEnd(logout) and SessionEnd(clear) are all
    silent. The log and the owner are unchanged. The owner still gets the nudge.
  • a takeover in another session with the quoted $CLAUDE_PLUGIN_ROOT form
    reports both the lost Now agent and the crash line (previous transcript
    newer than the state file). It does not inject the head. Then the goal is
    taken back.
  • /clear hand-off: the cleared:<id> marker is ignored by a startup
    session and by another session's Stop, and claimed by the clear
    SessionStart, which injects the head. An exit (logout) does not hand the
    goal on.
  • 24 read-only commands give no nudge, including git merge-base main HEAD (and inside $(...)), gh api repos/o/r/pulls/102/files,
    gh api -X GET ... -f, single- and multi-line graphql queries, git push --dry-run,
    git push -n, git stash show -p, grep -n "git commit", git worktree list
    and git branch -a.
  • 41 write commands nudge, including url=$(gh pr create --fill), (git commit),
    { git commit; }, for ...; do git commit; done, timeout 60 git push, a multi-line graphql mutation, -F query=@file,
    gh api -XPATCH, --input, gh pr checkout, git -C dir commit,
    GIT_X=1 git commit, git --no-pager commit, git commit -m "... --dry-run",
    git worktree add, git revert, git branch -D, git push --dry-run && git push
    and a two-line command whose second line commits.
  • an Agent call plus a Write to briefs/, log.md, scratch/ or status.md
    nudges, and adding a state.md Edit silences it. cat, grep, sed -n and
    cat >> naming state.md all nudge. goal.sh resume|touch|now|land silence
    it, and goal.sh list does not.

shellcheck: no error-severity findings, and no new advisory findings.

Not tested in a live Claude Code session:

  • That SessionEnd(clear) runs before the SessionStart(clear) of the new
    session is assumed, not observed. If the order were reversed, the cleared
    session would be silent until /orchestrate resume reclaims the goal.
  • A claim needs the turn's goal.sh tool call to be in the transcript when
    Stop runs.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AJKFfYT7qdSfhkZGj6syCo

…heck

Orchestrate hooks now act only in the session that owns the active goal
(fixes #107), the Stop hook ignores read-only commands, and only a real
write to state.md counts as updating it (fixes #109).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AJKFfYT7qdSfhkZGj6syCo
…edits and the updated= marker

Adds a state.md lint run from the hooks (#108), rewrites the skill's
state-file rules to one row per item with dated Done lines (#110), has
goal.sh write the updated= marker (#111) and adds goal.sh now/land
helpers (#112). Stacked on #114.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AJKFfYT7qdSfhkZGj6syCo
…-quality

orchestrate: lint the state file from the hooks, script the Now/Done edits and the updated= marker
@gilesknap
gilesknap merged commit d8c00ae into main Oct 14, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants