Skip to content

Gate the publishing act, not the subagent prompt - #473

Merged
EdbertChan merged 1 commit into
mainfrom
stack/EdbertChan/fix/publish-act-guard/gate-publishing-act-subagent-prompt--53e191f4
Sep 12, 2026
Merged

Gate the publishing act, not the subagent prompt#473
EdbertChan merged 1 commit into
mainfrom
stack/EdbertChan/fix/publish-act-guard/gate-publishing-act-subagent-prompt--53e191f4

Conversation

@EdbertChan

@EdbertChan EdbertChan commented Sep 12, 2026

Copy link
Copy Markdown
Owner

Summary

A guard meant to keep publishing work on Invoker was deciding from the words in a subagent prompt.

It blocked three spawns in one session. One cleared when the word "commit" was deleted from a sentence that forbade committing. Nothing about the work changed.

It also could not see an approval given through a question prompt, so an answer the user had already chosen read as no answer at all.

The two reads behind that, at engine/hooks/agent-routing-guard/detect.py before this change:

77-81   NOUN_CONTEXT_RE = r"(?i)\b(?:the|a|an|this|that|...|our)\s+(?:\w+[\s-]+){0,2}\Z"
232     elif isinstance(block, dict) and block.get("type") in ("text", "output_text"):
242     return bool(text.strip()) and not text.lstrip().startswith("<")

The noun test needs a determiner, so "carrying commits" fell through to the action branch. _text_content collects only text blocks, so an answer arriving as a tool result was empty and never reached the override check.

This replaces it with a gate on the command. A subagent that actually runs git push, opens a PR, or pushes a stack is stopped while a live Invoker owner is reachable. When no owner answers, the subagent publishes, because Invoker cannot take the work.

Review Claim

Approve deciding this gate from the command being run instead of from prompt wording.

Review Lane

behavior

Review Unit

engine-runtime

Safety Invariant

Three facts must all hold before anything is refused: the caller is a subagent, the command performs a publishing act at command position, and the liveness probe finds a reachable owner. Any read that cannot be made keeps the command running and prints why.

Slice Rationale

The new detector and the retirement of the old one are one claim: the gate moves from prose to the command. Landing them apart would leave two gates for the same decision, one of them known wrong.

Non-goals

No change to what counts as publishing work, to Invoker routing itself, or to any other hook. The main session is untouched: it was never gated and still is not.

Test Plan

Test Plan
  • cd engine/hooks/publish-act-guard && python3 -m unittest discover -s tests
  • python3 -m unittest tests.test_install
  • python3 engine/skills/make-pr/scripts/preflight.py --base origin/main
$ cd engine/hooks/publish-act-guard && python3 -m unittest discover -s tests
Ran 13 tests in 0.002s

OK

$ python3 -m unittest tests.test_install
test_publish_act_guard_linked_and_bash_pretooluse_wired_for_claude ... ok
test_retired_agent_routing_guard_is_gone ... ok
(exit code 0)

$ python3 engine/skills/make-pr/scripts/preflight.py --base origin/main
unit    engine-runtime: 20 file(s)
neutral 2 file(s): docs/ecosystem.md, tests/test_install.py
gate    python3 scripts/check_hook_test_coverage.py engine/hooks/publish-act-guard
        check_hook_test_coverage: OK (1 hook(s) checked)
ok      preflight passed

Detector fixtures include test_prompt_wording_cannot_trigger_or_clear_the_gate, which pins that an echoed sentence about "carrying commits" is silent while a real git push is refused.

The fixtures caught two defects during the build: git push --dry-run and cat scripts/safe-stack-push.mjs both matched the first matcher and now do not.

Install suite: test_publish_act_guard_linked_and_bash_pretooluse_wired_for_claude ... ok and test_retired_agent_routing_guard_is_gone ... ok, exit code 0.

Preflight: unit engine-runtime: 20 file(s), neutral 2 file(s), check_hook_test_coverage: OK, ok preflight passed.

Revert Plan

Revert Plan
  • Safe to revert? Yes
  • Revert command: git revert d392aaf
  • Post-revert steps: Run ./install.sh to restore the old hook's settings entry, then restart the harness.
  • Data migration? No. The liveness cache under $TMPDIR is disposable.

Note

Medium Risk
Changes when subagents can push or open PRs during live Invoker sessions; misclassification of commands or liveness could block or miss routing, though UNCHECKED and non-subagent paths fail open.

Overview
Replaces prompt-based subagent blocking with command-based publishing gates. The retired agent-routing-guard PreToolUse hook on Agent is removed entirely; new publish-act-guard runs on Bash and only when a subagent is about to run a real publishing command (git push, gh pr create|merge|ready, mutating gh api on /pulls, stack push scripts, etc.) while invoker-cli query capacity reports a live owner.

The gate keys off parsed shell argv (dry runs and read-only neighbours stay silent), not subagent prompt prose—addressing false blocks from wording like “carrying commits” and override checks that missed non-text user answers. Main-session Bash is never gated; if Invoker is down or liveness is UNCHECKED, publishing is allowed (UNCHECKED is logged, not exit 2).

Install and docs: install.sh symlinks and merges the new hook; install_claude_hook.py strips stale agent-routing-guard settings entries. docs/ecosystem.md lists publish-act-guard; install tests assert Bash wiring and that the old hook path and settings are gone.

Reviewed by Cursor Bugbot for commit 00851e0. Bugbot is set up for automated code reviews on this repo. Configure here.

@cursor

cursor Bot commented Sep 12, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_c113759f-a149-475f-b505-a71238a4eba9)

agent-routing-guard decided from regexes over the spawn prompt. "carrying
commits" parsed as an action because the noun test required a determiner,
so deleting the word cleared the block without changing what the subagent
would do; and its override check read only text blocks, so an answer given
through AskUserQuestion never counted as approval.

publish-act-guard reads the command the tool is about to run, which is a
typed field, and blocks only when all three hold: the caller is a subagent,
the command performs a publishing act at command position, and a liveness
probe finds a reachable Invoker owner. No live owner means the subagent
publishes, which is what the user asked for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UbWT7AQ3sJY49m13QPcom4
Change-Id: I53e191f4a21c16d42a1a345741108ead187f7687
@EdbertChan
EdbertChan force-pushed the stack/EdbertChan/fix/publish-act-guard/gate-publishing-act-subagent-prompt--53e191f4 branch from d392aaf to 00851e0 Compare September 12, 2026 06:35
@cursor

cursor Bot commented Sep 12, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_98ae1dd3-9ecf-4a32-b48f-e66d0cfc61a1)

@EdbertChan
EdbertChan merged commit 64bdc40 into main Sep 12, 2026
4 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

Development

Successfully merging this pull request may close these issues.

1 participant