ux(steering): make every hook deny / tool refusal terminal or forwarding — no guard cycles, no dead ends, explicit "nothing executed" + "do not retry raw git" text #15
Labels
No labels
breaking
bug
documentation
enhancement
epic
good first issue
help wanted
refactoring
resolution/duplicate
resolution/invalid
resolution/wontfix
security
severity/critical
severity/high
severity/low
severity/medium
status/abandoned
status/blocked
status/needs-decision
status/needs-info
status/needs-verification
testing
upstream
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
sixfold-space/madtea#15
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem — agents hammer the guards because the texts are context-blind and can point at each other
Live case study (2026-07-10, a foreign-repo worktree flow on another repo; companion to #14 which covers the reconcile-mode gap itself):
git merge origin/mainin its linked worktree (allowed) → conflict → worktree now has merge-in-progress state.cd <wt> && git add <file> && git commit→ PreToolUse hook (hooks/scripts/check-git-local.sh) blocks the WHOLE invocation and steers tomadt_commit(dir=…). The text does not say that no part of the command executed — the agent later assumed thegit addhad run.madt_commit dir=<wt>→ refused (internal/mcp/tools_localgit.go): foreign repo has a merge in progress, "resolve or abort that operation directly in the target repo". That text reads as "use raw git here" — the exact thing the hook in step 2 steers away from. The two guards point at each other.git merge --continue→ fails with "unmerged files" — not a guard at all, but the agent can't tell: the staging from step 2 never happened (see step 2's missing "nothing executed"). From the agent's seat this is a third consecutive wall, indistinguishable from more steering.madt_add dir=<wt>+git merge --continue+madt_push dir= branch=+madt_prs action=merge owner_repo=. Nothing in any guard text names this path.Net effect: a well-resourced caller bounced 4 times; a lower-capability caller would still be hammering. The failure mode is not too little text — it's non-terminal text: rails that don't end in an executable next step for the state the repo is actually in.
Design rules for every deny/steer/refusal (the ask)
git adddid not execute)." One sentence kills the step-4 class of confusion.check-git-local.shcan detectMERGE_HEADin the target repo and emit the merge-in-progress rail directly (madt_add the resolutions +git merge --continueare the sanctioned pair — same rails madt_orchestrate reconcile already prints), instead of the generic madt_commit redirect that will bounce.git commitvariant; every form is steered, every time." Agents probe variants when the boundary is implied rather than stated.tools_localgit.go's foreign merge-in-progress refusal names the working exit verbatim (madt_add dir= + git merge --continue, or reconcile once #14 landsdir=), not "directly in the target repo".Acceptance
ADR \d+,#\d+-style issue citations, internal repo/host names); existing violations swept in the same change. Dynamic user-facing numbers (the caller's own PR/issue in a rail likereconcile=29) are exempt — the lint targets hardcoded literals only. Note: tool DESCRIPTIONS ship with the binary too and currently carry the same internal issue citations — decide in this issue whether the sweep covers them or that splits out.Evidence handoff from the #39 sweep (adversarial-review finding, deliberately deferred here because #39's text assigns the steering/refusal string class to this issue):
hooks/ships to plugin users viascripts/assemble-plugin-artifact.sh(hooks/hooks.json+hooks/scripts/*.sh) and carries 208 dangling 3+-digit tracker refs, including user-facing refusal text — e.g.hooks/scripts/check-foreign-primary.sh:49and:57inside thereason=denial strings ("#2032, ADR 0019" / "Launch-directory scope (#2063)"). Those numbers don't resolve on this tracker, so every hook denial a plugin user sees cites a ghost issue.#39 landed the shared static rule for exactly this:
internal/danglingrefexportsFindings/Pattern/allowedNumbers, and its package doc notes this issue should call the same rule. Design rule 5's lint over hook scripts and steer/refusal constants can extend that scan set (hooks/hooks.json,hooks/scripts/*.sh) and sweep the refusal strings to carry the rule, not the reference.