0.46.0 — Holmes reviews work that never reached the board
⚡ After you update
Restart any session that should be governed. A running session holds the plugin it loaded at start. An open conversation keeps the old hooks until it restarts.
No setup re-run is needed. This release changes agent prompts, skills, and hooks. All three are read live. 0.44.0's requirement to run /workbench-dev-team:setup still stands if you have not run it.
✨ Holmes reviews work that never reached the board
Holmes accepted Item ID: <n> and nothing else. Every review therefore needed a board item behind it.
Watson has had no such coupling. His Direct mode takes a prose brief. It runs on any local repo.
That asymmetry had a cost. Watson's Direct mode hands work back as an uncommitted working tree. That tree had no review path. You reviewed it yourself, or it shipped unreviewed.
Local mode is now Holmes's default. Hand him a five-slot brief. He reviews the uncommitted working tree in its Workdir:. Index mode is entered only on an explicit item-ID token.
| Local mode | Index mode | |
|---|---|---|
| Entered by | a prose brief (the default) | Item ID: <n> |
| Reviews | the uncommitted working tree | the PR on the board |
| Rubric | the brief's Goal: and Done when: |
the issue's acceptance criteria |
| Tests | runs the repo's own suite | reads CI status |
| Verdict | prose, to the dispatching session | an App-signed review |
| Writes | one vault note | the board and the PR |
Ambiguous prose resolves to Local mode. It never resolves to Index mode. The two mistakes cost different amounts. A misread brief wastes a report. A guessed id posts a signed verdict onto somebody else's PR.
The rubric binds exactly as acceptance criteria bind. Goal: and Done when: go verbatim into the lens prompts. Holmes never amends them. A rubric that is itself wrong comes back as a dispute with three options.
Local mode makes no The Index call and no GitHub write. There is no board item to move. A local review carries no consent to post under your identity.
The lens fan-out, the adversarial verification, the memory pass, and the finding-routing matrix are unchanged.
🔒 A guard, because the prose did not hold
Local mode reads your live working directory. There the uncommitted change is the only copy of the work.
The rule protecting that tree started as prose at five sites. It sat verbatim in every sub-agent prompt Local mode dispatches.
On the mode's first real exercise, a lens sub-agent ran chmod against that directory. It changed a script from 755 to 644. It disclosed the breach itself. The prohibition had reached it and it acted anyway.
Prose in an agent prompt is advisory. A second PreToolUse hook now enforces the rule. It sits beside the commit gate rather than inside it.
| Event | Behaviour |
|---|---|
| A Holmes dispatch carrying a prose brief | arms a record for that session |
| A Bash call from a sub-agent of an armed session | denied when it mutates a tree |
| The dispatch returns | releases the hold |
Nothing asks the reviewed agent to arm anything. The hook reads the Agent tool's own subagent type and prompt. It applies Holmes's own mode detection. The prose that drifts is not in the loop.
The signal is the harness-supplied session_id plus a non-empty agent_id. Your own window keeps working. A concurrent session is untouched. A host-wide marker would be the watson.lock leak with the sign reversed.
What it refuses is a class, not a roster. Git's reading verbs are enumerated. Every other git verb is refused. That is how it sees git restore, which the old prose list never named. File metadata joins it, since that is what the breach used.
Reads and the repository's own suite stay legal. That is the binding constraint. A guard that stops either makes the mode useless.
What it costs you
While a review is armed, every sub-agent of that session is read-only. That includes ones unrelated to the review. It is bounded by the release and a two-hour expiry.
A review that was never armed fails open silently. The hook is a backstop. The prose stays.
🔍 The approval prompt now names the commit
The commit-approval gate denied a commit. It said nothing about the description on the approval command. Each session therefore worded that line itself. One wrote "Request human approval for this commit". That names the action and never the commit.
A prompt carrying no decision content gets cleared unread. That habit reaches the other rules on the same ask list. git reset --hard and gh pr merge are among them.
The denial now dictates the line rather than describing an intent. It names the parameter, and gives the literal shape Commit: <first line of the commit message>. It stops at the subject. Nothing about what the gate approves has changed.
🧪 Tests
| Suite | Cases |
|---|---|
hooks/scripts/test-local-review-guard.sh |
79, new |
hooks/scripts/test-commit-approval-gate.sh |
84, up from 83 |
agents/lint-holmes-local-mode.sh |
8 checks across four files, new |
The lint keeps no copy of the forbidden-verb rule. It feeds the reference's fenced blocks to the guard's own classifier. The documentation and the enforcement therefore cannot drift apart.