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, baseac6b56ee). 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 on11bf4cb0plus 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:
@@ -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/,
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -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": []
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user