Files
stack/agents/researcher/work/queue-84/r1/build.patch
T
jason.woltjeandClaude Opus 5.5 7198d336cd researcher: row 84 round 1 packet on ac6b56ee, business 67/0, nine mutants killed (#1578)
Haiku subagent build under decision 129. Candidate manifest f72730de
names roles/chief-of-staff.json and .md, the business README and
packages/business/tests/chief-of-staff.test.mjs. Gate and mutants ran
in Sage's slot.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 09:56:33 -05:00

240 lines
9.5 KiB
Diff

diff --git a/packages/business/README.md b/packages/business/README.md
index ca372c5ed..8c7a43350 100644
--- a/packages/business/README.md
+++ b/packages/business/README.md
@@ -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
diff --git a/packages/business/tests/chief-of-staff.test.mjs b/packages/business/tests/chief-of-staff.test.mjs
new file mode 100644
index 000000000..08374d122
--- /dev/null
+++ b/packages/business/tests/chief-of-staff.test.mjs
@@ -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/,
+ );
+});
diff --git a/roles/chief-of-staff.json b/roles/chief-of-staff.json
new file mode 100644
index 000000000..031f4b7a6
--- /dev/null
+++ b/roles/chief-of-staff.json
@@ -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": []
+}
diff --git a/roles/chief-of-staff.md b/roles/chief-of-staff.md
new file mode 100644
index 000000000..977f50354
--- /dev/null
+++ b/roles/chief-of-staff.md
@@ -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.