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
|
||||
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
|
||||
|
||||
```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/,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user