roles: chief-of-staff role file, contract and tests, row 84 (#1578)

Lands the Haiku subagent build from agents/researcher/work/queue-84/r1/
(candidate manifest f72730de, base ac6b56ee). roles/chief-of-staff.json
and .md follow decision 104's list; packages/business/tests/
chief-of-staff.test.mjs covers the contract, and the business README
names the role.

Approved by Filbert (comment 28176) and Darkwing (comment 28183). Sage's
gate on 11bf4cb0 plus the patch: 14 node suites and all nine
scripts/test-*.sh green, 15:33:26-15:37:31Z.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-13 10:38:03 -05:00
co-authored by Claude Opus 5.5
parent 11bf4cb00a
commit 7be720fd72
4 changed files with 210 additions and 0 deletions
+21
View File
@@ -94,6 +94,27 @@ instance whose definition holds `role.launch` within-role; otherwise the
file refuses. Only that instance keeps `role.launch`. Every other instance file refuses. Only that instance keeps `role.launch`. Every other instance
loses it, so for them it's gated. loses it, so for them it's gated.
### Chief-of-staff instance
The `chief-of-staff` role (`roles/chief-of-staff.json`, contract
`roles/chief-of-staff.md`, lead decision 104) is the main agent that
carries Jason's requests to the PM. It holds `message.send` within role
and nothing else: no write tool, no credentials, no crossRole action, and
no `launch` block. Its instance is `mos` (decision 116). Jason adds this
entry under `roles` in his business file, next to the other instances. The
stack never writes that file.
```json
"mos": { "definition": "chief-of-staff", "holder": "mos" }
```
The entry has no `tracker`, because the role uses no Vikunja, and no
`credentials`, because it holds none. The file must not name `mos` as the
`launch.by` instance: the loader refuses that, since the role has no
`role.launch`. `packages/business/tests/chief-of-staff.test.mjs` checks the
role against decision 104's list and loads it through `loadBusiness` from a
fixture business under the name `chief`.
### Project file ### Project file
```json ```json
@@ -0,0 +1,133 @@
// The chief-of-staff role (lead decision 104, M1, row 84). The role file is
// policy: its tools, network, authority and credentials must equal the list
// Jason approved, so a widened file fails here. Fixtures live in a temp
// directory and nothing reads the real config.
import { test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { ACTIONS, BusinessError, classify, loadBusiness, loadRole, resolveInstance } from "../src/index.mjs";
import { businessDoc, REPO_ROLES, systemFor, tmp, writeJson } from "./helpers.mjs";
// Decision 104's list, as approved. A role file that differs from it fails.
const APPROVED = Object.freeze({
tools: ["read", "grep", "find", "ls"],
network: "api-only",
authority: Object.freeze({ withinRole: Object.freeze(["message.send"]), crossRole: Object.freeze([]) }),
credentials: Object.freeze([]),
});
// A fixture instance name. The shipped entry for Jason's business is `mos`.
const INSTANCE = "chief";
function refuses(fn, pattern) {
assert.throws(fn, (error) => {
assert.ok(error instanceof BusinessError, `expected BusinessError, got ${error}`);
assert.equal(error.exitCode, 2);
assert.match(error.message, pattern);
return true;
});
}
// Write a business file to a temp config directory and load it with
// loadBusiness. The file is the shipped four roles from businessDoc(), plus
// one chief-of-staff instance, with `mutate` applied after both.
function loadFixture(mutate = () => {}) {
const root = tmp();
const doc = businessDoc(root, "acme");
doc.roles[INSTANCE] = { definition: "chief-of-staff", holder: "mos" };
mutate(doc);
writeJson(join(root, "businesses", "acme.json"), doc);
return { root, business: loadBusiness("acme", { dir: root, rolesDir: REPO_ROLES }) };
}
function resolved({ root, business }) {
return resolveInstance({ system: systemFor(root), business, instance: INSTANCE });
}
test("the role file loads and resolves through loadBusiness", () => {
const fixture = loadFixture();
const role = loadRole(REPO_ROLES, "chief-of-staff");
assert.equal(role.roleVersion, 2);
assert.equal(role.name, "chief-of-staff");
assert.equal(role.contract, "chief-of-staff.md");
assert.equal(role.contractPath, join(REPO_ROLES, "chief-of-staff.md"));
const record = resolved(fixture);
assert.equal(record.definition, "chief-of-staff");
assert.equal(record.holder, "mos");
assert.equal(record.contract, join(REPO_ROLES, "chief-of-staff.md"));
assert.equal(record.launch, null);
assert.equal(record.tracker, null);
});
test("the shipped file is exactly decision 104's list", () => {
const raw = JSON.parse(readFileSync(join(REPO_ROLES, "chief-of-staff.json"), "utf8"));
const { title, ...rest } = raw;
assert.equal(typeof title, "string");
assert.deepEqual(rest, {
roleVersion: 2,
name: "chief-of-staff",
contract: "chief-of-staff.md",
...APPROVED,
});
});
test("the resolved record matches decision 104, so a widened file fails", () => {
const record = resolved(loadFixture());
assert.deepEqual(record.limits, {
tools: APPROVED.tools,
network: APPROVED.network,
authority: APPROVED.authority,
});
assert.deepEqual(record.credentials, {});
});
test("message.send is within role and every other action is gated", () => {
const record = resolved(loadFixture());
for (const action of ACTIONS) {
assert.equal(classify(record, action), action === "message.send" ? "within" : "gated", action);
}
assert.equal(classify(record, "message.send"), "within");
assert.equal(classify(record, "task.create"), "gated");
assert.equal(classify(record, "role.launch"), "gated");
});
test("a business grant of a gated action through limits does not widen the role", () => {
// limits.authority only narrows, so the grant has no effect and both
// actions stay gated. limits only narrow (resolve.mjs), so the grant has
// no effect; this asserts that, rather than a refusal.
const fixture = loadFixture((doc) => {
doc.roles[INSTANCE].vars = { "limits.authority": ["message.send", "task.create", "role.launch"] };
});
const record = resolved(fixture);
assert.deepEqual(record.limits.authority, { withinRole: ["message.send"], crossRole: [] });
assert.equal(classify(record, "task.create"), "gated");
assert.equal(classify(record, "role.launch"), "gated");
assert.equal(record.launch, null);
});
test("a launch block whose by is the chief-of-staff instance is refused", () => {
refuses(
() => loadFixture((doc) => {
doc.launch = { by: INSTANCE, instances: ["coder"], max: { sonnet: 1 } };
}),
/launch\.by names chief, whose role chief-of-staff doesn't hold role\.launch within-role/,
);
});
test("the instance takes no credential references and no tracker", () => {
refuses(
() => loadFixture((doc) => {
doc.roles[INSTANCE].credentials = { gitea: { file: "/x/token", rotateBy: "2099-01-01" } };
}),
/must reference exactly the services its role definition needs \(none; got gitea\)/,
);
refuses(
() => loadFixture((doc) => {
doc.roles[INSTANCE].tracker = { bot: "bot-acme-chief", botId: 7 };
}),
/has "tracker" but chief-of-staff uses no vikunja credential/,
);
});
+13
View File
@@ -0,0 +1,13 @@
{
"roleVersion": 2,
"name": "chief-of-staff",
"title": "Chief of staff",
"contract": "chief-of-staff.md",
"tools": ["read", "grep", "find", "ls"],
"network": "api-only",
"authority": {
"withinRole": ["message.send"],
"crossRole": []
},
"credentials": []
}
+43
View File
@@ -0,0 +1,43 @@
# Role contract: chief of staff (chief-of-staff)
You hold the `chief-of-staff` role for one business. You are the human's
main agent in that business. You carry Jason's requests to the product
manager, relay his rulings and ask the PM for work. You don't write the
product's code, you don't review it and you don't create tasks yourself.
## Duties
- Send each request Jason makes to the PM as a message, addressed to the
`pm` role. Keep his words as he gave them, and name the request each
message carries.
- Ask the PM for work by message. The PM decides which work becomes a
task, who holds it and when it runs.
- Relay each ruling Jason gives verbatim to the role it concerns. Don't
summarize it, paraphrase it or add to it. Say whose ruling it is.
- Report to Jason what the PM answers, and every refusal with its exact
text.
## Authority
Your role file lists what you do on your own: sending messages. Anything
the list doesn't name is gated. That includes creating or assigning
tasks, launching a role session, pushing to a repository, sending
messages outside the business, minting credentials, and any change to
policy or to a PRD. When an action is gated, raise it to Jason and wait
for his answer. Never route around a refusal.
You hold no human authority. Never resolve a decision for Jason, never
approve a request in his name, and never write words as if he had said
them. When a question needs his decision, pass it on as his question and
wait for his answer.
Only the human stops a run, with `mosaic stop`. If you think a coder or
reviewer run should stop, say so to Jason. Don't stop it yourself.
## Protocol
- You act only through the typed tools the broker gives you. Your tools
are read, grep, find and ls. You have no write tool, and you never hold
a token or a credential.
- Send messages through the bus, addressed to a role, not a person.
- Report a blocker as soon as you find it, with what you tried.