# Row 73 (#1567): drop the coder's task.reassign grant, candidate packet, round 1 Author: Rocko. Reviewer: Dewey. Brief: `docs/plans/2026-10-11_claude-pin-and-coder-grant.md`, section "Coder role: drop the task.reassign grant the broker always refuses". Base: `324c064b` (`base.txt`). At 23:16Z the checkout HEAD `f6b9ea4e` has no change since the base to any candidate path or to `packages/{bus,business,tasks}/src` or `packages/harness/src/tools.mjs`. `build.patch` applies to it cleanly. The packet is uncommitted. I made no commits, pushes or Gitea calls and read no token or private binding. ## Files (`files.txt`, `candidate-manifest.sha256`) | File | Change | |---|---| | `roles/coder.json` | `crossRole`: `["task.reassign", "task.scope.change"]` → `["task.scope.change"]`. Nothing else in the file changes. | | `packages/business/tests/role.test.mjs` | "shipped authority follows the note's table": asserts that the coder's `crossRole` is exactly `["task.scope.change"]` | | `packages/business/tests/resolve.test.mjs` | "classify": the coder's `task.reassign` is now `gated` and its `task.scope.change` is `cross`. "limits.authority narrows cross-role actions too" moves from the coder to the pm (see below). | | `packages/bus/tests/broker.test.mjs` | +1 test built on the shipped role files: "the shipped coder role raises task.reassign as gated to the human; task.scope.change stays cross-role" | | `packages/harness/tests/tools.test.mjs` | +1 test: "a coder with a tracker gets its own task tools and no task_reassign" | | `packages/tasks/tests/world.mjs` | the fixture's coder `crossRole` now matches the shipped role | | `packages/tasks/tests/verbs.test.mjs` | in "assign and reassign …": a coder's `task.reassign` refuses `field-writer` and records no `action.allowed` | | `agents/rocko/work/queue-73/mutants.sh` | the mutant rerun: `mutants.sh ` | No change to `broker.mjs`, `PM_VERBS`, the slice 1 field table or any other role's grants. ### Why the narrowing test moved to the pm Before this change, the test limited the coder to `task.scope.change` and watched `task.reassign` drop out of `crossRole`. With one cross-role action left, that limit drops nothing from `crossRole`. I checked this: with the new role file and the old test, the mutant `resolve-cross-unnarrowed` (crossRole ignores `limits.authority`) passes the test (`out/old-narrow-test-under-mutant.txt`). The pm still holds two cross-role actions. The test now limits the pm to `task.create`, `task.close` and `task.priority.change`. It checks that `crossRole` keeps `task.priority.change` and drops `task.scope.change`, and that `withinRole` narrows too. ## Every place that read the old grant, and what it says now Probe (`out/probe-base.txt`, `out/probe-candidate.txt`): the shipped roles through `validateBusinessDocument`, `resolveInstance`, `busBusiness` and a real `Broker`, as `bootConfig` assembles them. | Reader | Before (base) | After (candidate) | |---|---|---| | `business/src/resolve.mjs` `resolveInstance` | coder `crossRole` `[task.reassign, task.scope.change]` | `[task.scope.change]`. The resolved digest of every coder instance changes too. Only the bundle manifest records it, and nothing compares it. | | `business/src/resolve.mjs` `classify` | `cross` | `gated` | | `bus/src/broker.mjs` `#classification` (via `cli/src/config.mjs` `bootConfig` → `busBusiness`) | `cross-role`; `decision.raise` routes to the domain arbiter, `pm` by default (`cto` for technical) | `gated`; routes to `human` | | `harness/src/tools.mjs` `typedTools`, with a tracker | the coder gets a `task_reassign` tool | no `task_reassign` tool; `task_scope_change` and `task_update_assigned` remain | | `harness/src/tools.mjs` `untypedActions` → `bundle.mjs` prompt, without a tracker | the prompt lists `task.reassign` among "Actions you hold that have no bus tool yet" | not listed | | `tasks/src/verbs.mjs` `PM_VERBS` | `field-writer` before authority | unchanged: `field-writer` before authority | | `tasks/tests/world.mjs` fixture | copied the old grant | matches the shipped role | | business, bus, harness and tasks tests | see Files | see Files | Readers I did not change (outside the brief's files; Sage's call): - `packages/business/README.md`, lines 30-43: the version 2 example is the shipped coder file and still shows `"crossRole": ["task.reassign", "task.scope.change"]`. It goes stale with this change. Suggested fix: drop `"task.reassign", ` from the example. - `packages/tasks/README.md`, lines 193-196: "A coder role's `crossRole` grant of `task.reassign` can't be used. The writer check refuses it before authority is looked at. …" This will be stale too. Suggested text: "The coder role holds no `task.reassign` grant (row 73). The writer check refuses a non-pm `task.reassign` before authority is looked at anyway. If a coder should hand work on, that's a change to the field table, not to the role file." - `packages/tasks/live/run.mjs`, line 32: the live run's own copy of the slice 1 authority still gives the coder `task.reassign`. Nothing runs it in the suites. It never has the coder call `task.reassign`, so its result doesn't change. It should match `world.mjs`. - Unaffected: `scripts/test-task.sh` line 279 and `business/tests/role.test.mjs` line 115 are role-validation fixtures, not the coder's grant. Historical notes are left as they are: the slice 1 plan, the Darkwing and Filbert work notes, and lead decision 90. ## What a coder's task.reassign does end to end after the change 1. A pi coder session has no `task_reassign` tool, so it can't make the call through the harness. 2. It can still `raise_decision` with `action: task.reassign`, because the tool's action enum is every action. The broker classes it `gated` and routes it to the human, where before it went to the pm as cross-role. The human is then asked a question that can only end in `field-writer`. 3. If the human approves and the coder calls the verb directly on the bus, the broker's `authorize` passes as `gated`; the bus test shows that. The task verbs refuse first with `field-writer`, before `authorize`, so nothing is consumed and nothing is written (tasks test). The brief names the follow-up but doesn't make it, and nor does this row. To refuse a non-pm `task.reassign` (or any `PM_VERBS` action) at `decision.raise`, before a decision opens, is a broker change and would be its own row. A bus that is already running keeps the authority it loaded at boot. The change reaches it on its next restart. ## Mutants (`out/mutants-candidate.txt`, `out/mutants-base.txt`) | Mutant | Edit | Base `324c064b` | Candidate | |---|---|---|---| | `role-keeps-reassign` | `roles/coder.json` puts `task.reassign` back | n/a (the base is this mutant) | killed by role, classify, bus and harness tests | | `broker-unlisted-cross` | `#classification`: an action in neither list → `cross-role` | **survived** | killed by the new bus test | | `resolve-cross-unnarrowed` | `resolveInstance`: `crossRole` ignores `limits.authority` | killed | killed (by the narrowing test and the harness test "an action outside the instance's authority has no tool") | | `classify-unlisted-cross` | `classify`: an unlisted action → `cross` | killed | killed | | `tools-all-task-verbs` | `typedTools`: a task tool for every verb, held or not | killed | killed (now also by the new harness test) | The script saves each file, applies the sed and checks it changed something. It then runs the five test files and restores the file from the saved copy, so it works on an uncommitted tree. `out/mutants-*/` holds each diff and log. I ran the candidate twice. The run recorded here uses the final script; the first run used the same script before a comment-only reflow, with the same results. ## Gate (`out/gate/`, sequential, scratch worktree at 324c064b with the candidate, `node_modules` linked, 23:09:50Z-23:14:34Z) | Suite | Result | |---|---| | tasks `node --test 'packages/tasks/tests/*.test.mjs'` | 71 pass, 0 fail | | bus | 81 pass, 0 fail | | business | 60 pass, 0 fail | | harness | 58 pass, 0 fail, 0 skipped | | cli | 112 pass, 0 fail | | webui | 50 pass, 0 fail | | seat | 27 pass, 0 fail | | test-auth | 15 passed, 0 failed | | test-conductor | 17 passed, 0 failed | | test-config | 24 passed, 0 failed | | test-discord | 66 passed, 0 failed | | test-extension-package | 18 passed, 0 failed | | test-foundation | 44 passed, 0 failed | | test-queue | 27 passed, 0 failed | | test-release | 14 passed, 0 failed | | test-task | 98 passed, 0 failed (Docker) | cli, webui and seat go beyond the gate the brief names. I ran them because they read the shipped roles. After the gate I removed the `rocko-r73_default` network.