feat(roles): coder drops the task.reassign grant the broker always refuses (#1567, row 73)

roles/coder.json crossRole goes from ["task.reassign", "task.scope.change"]
to ["task.scope.change"]. The broker refuses a non-pm task.reassign at the
writer check before it looks at authority, so the grant could never be
used. A coder's task.reassign now classifies as gated and goes to the
human. Backed by lead decision 90.

Tests: business role and resolve (the narrowing test moves to the pm,
which still holds two cross-role actions), a bus test on the shipped role
files, a harness test that a coder with a tracker gets no task_reassign,
the tasks fixture, and a verbs line showing a coder's task.reassign
refuses field-writer with no action.allowed. mutants.sh is Rocko's packet
rerun script, already committed.

Author: Rocko (packet agents/rocko/work/queue-73/, commit 2137dd7f).
Candidate manifest sha256 bf4e5ed6, base 324c064b, no drift on the
candidate paths through 8a42e819.
Approval: Dewey, round 1, comment 27506 on #1567.
Gate (Sage, /tmp/sage-g73 on 8a42e819, 23:42-23:48Z): tasks 71, bus 81,
business 60, harness 59, cli 113, webui 61, seat 27 node tests, 0 failed;
every scripts/test-*.sh exit 0, 0 failed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-11 18:49:09 -05:00
co-authored by Claude Opus 5.5
parent fb619befd0
commit 0453da7e10
7 changed files with 50 additions and 9 deletions
+9 -7
View File
@@ -122,21 +122,23 @@ test("limits.authority without role.launch leaves the launcher with no launch bl
});
test("limits.authority narrows cross-role actions too", () => {
// pm, the role with two cross-role actions: the limit keeps one and drops the other.
const s = setup((d) => {
d.roles.coder.vars["limits.authority"] = ["task.update.assigned", "git.push.working", "task.scope.change"];
d.roles.pm.vars["limits.authority"] = ["task.create", "task.close", "task.priority.change"];
});
const coder = resolve(s, "coder");
assert.deepEqual(coder.limits.authority, { withinRole: ["task.update.assigned", "git.push.working"], crossRole: ["task.scope.change"] });
assert.equal(classify(coder, "task.reassign"), "gated");
assert.equal(classify(coder, "task.scope.change"), "cross");
assert.equal(classify(coder, "review.request"), "gated");
const pm = resolve(s, "pm");
assert.deepEqual(pm.limits.authority, { withinRole: ["task.create", "task.close"], crossRole: ["task.priority.change"] });
assert.equal(classify(pm, "task.priority.change"), "cross");
assert.equal(classify(pm, "task.scope.change"), "gated");
assert.equal(classify(pm, "task.assign"), "gated");
});
test("classify", () => {
const s = setup();
const coder = resolve(s, "coder");
assert.equal(classify(coder, "git.push.working"), "within");
assert.equal(classify(coder, "task.reassign"), "cross");
assert.equal(classify(coder, "task.scope.change"), "cross");
assert.equal(classify(coder, "task.reassign"), "gated");
assert.equal(classify(coder, "task.close"), "gated");
assert.equal(classify(coder, "deploy"), "gated");
assert.equal(classify(resolve(s, "pm"), "task.priority.change"), "cross");
+2
View File
@@ -61,6 +61,8 @@ test("shipped role scopes match addendum B section 2 and the SR runbook", () =>
test("shipped authority follows the note's table", () => {
const auth = (name) => loadRole(REPO_ROLES, name).authority;
assert.ok(auth("coder").withinRole.includes("git.push.working"));
// Assignee is pm-only (decision 90): a coder grant of task.reassign could never succeed.
assert.deepEqual(auth("coder").crossRole, ["task.scope.change"]);
assert.ok(!auth("reviewer").withinRole.includes("git.push.working"));
assert.deepEqual(auth("reviewer").crossRole, []);
assert.ok(auth("pm").withinRole.includes("role.launch"));