New roles: - roles/security-reviewer.json and .md: read tools only, review.verdict within its role, no cross-role grant and no credentials, one verdict per request. - roles/merge-gate.json and .md: read tools only, cross-role git.merge.protected only. It raises with domain technical and the task ref, and lands with land_work once the decision resolves, the verdicts approve and the gate passes. The chief of staff messages the relaunched instance with the task ref and decision id (round 3 finding 28913). Contracts: - All 13 roles/*.md carry the proactive line in their Authority section. - pm and chief-of-staff carry the REQ-DEC-2 routing paragraph. Its eight hard waits are pinned to GATED_ONLY by a test. Harness: raise_decision takes an optional domain, technical or delivery, defaulting to delivery. The tool description says where each kind of raise goes. end_run is unchanged (row 155). Tests are new files only: business and bus workflow-roles tests and harness raise-domain. Builder Dewey; Filbert approved round 4 (comment 28929) on manifest 6b4af9c2685b, base0f715a054plus build.patch df0fec543dba. Gate on806ab89dfplus the candidate, 16:34:57Z to 16:39:43Z: 2027 package tests and 422 script selftests (plus the queue suite's 156 node tests), 0 failed. Doc-only follow-ups: business README:194-196 and the launch.instances doc line. Co-Authored-By: Claude Opus 5.5 <[email protected]>
54 lines
3.3 KiB
JavaScript
54 lines
3.3 KiB
JavaScript
// Row 149 (#1643): raise_decision takes an optional domain, technical or delivery, so a raise can
|
|
// reach arbiters.technical. The tool sends what it is given; the broker defaults to delivery.
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { typedTools, callTool } from '../src/tools.mjs';
|
|
import { resolvedFor, fakeToolSocket } from './helpers.mjs';
|
|
import { fixture } from '../../bus/tests/spec-fixtures.mjs';
|
|
|
|
const options = [{ key: 'yes', text: 'Yes' }, { key: 'no', text: 'No' }];
|
|
|
|
test('raise_decision offers domain as technical or delivery only, optional, and says where a raise goes', t => {
|
|
for (const role of ['coder', 'reviewer', 'pm', 'chief']) {
|
|
const raise = typedTools({ resolved: resolvedFor(t, role) }).find(x => x.name === 'raise_decision');
|
|
const { domain } = raise.parameters.properties;
|
|
assert.equal(domain.type, 'string');
|
|
assert.deepEqual(domain.enum, ['technical', 'delivery']);
|
|
for (const other of ['Technical', 'DELIVERY', 'human', 'security', '']) assert.ok(!domain.enum.includes(other), other);
|
|
assert.match(domain.description, /Leaving it out means delivery/);
|
|
assert.ok(!raise.parameters.required.includes('domain'));
|
|
assert.equal(raise.parameters.additionalProperties, false);
|
|
assert.match(raise.description, /A cross-role raise, or a gated raise citing the project's current spec commit, goes to the business's arbiter for its domain/);
|
|
assert.match(raise.description, /an uncited gated raise or a hard wait goes to the human/);
|
|
}
|
|
});
|
|
|
|
test('raise_decision forwards domain to decision.raise, and a call without it sends none', async t => {
|
|
const f = await fixture(t, {
|
|
arbiters: { technical: 'cto', delivery: 'pm' },
|
|
roles: {
|
|
coder: { definition: 'coder', authority: { withinRole: ['message.send'], crossRole: ['task.reassign'] } },
|
|
cto: { definition: 'cto', authority: { withinRole: ['message.send'], crossRole: [] } },
|
|
pm: { definition: 'pm', authority: { withinRole: ['message.send'], crossRole: [] } },
|
|
},
|
|
});
|
|
const byName = new Map(typedTools({ resolved: { definition: 'coder', limits: { tools: [], authority: {
|
|
withinRole: ['message.send'], crossRole: ['task.reassign'],
|
|
} } } }).map(tool => [tool.name, tool]));
|
|
const server = await fakeToolSocket(t, f.root, (name, args) =>
|
|
f.broker.request(f.coder, { verb: byName.get(name).verb, args }));
|
|
const base = { action: 'task.reassign', question: 'Hand T-1 to another coder?', options, recommendation: 'yes', blocking: false, target: 'T-1' };
|
|
|
|
const technical = await callTool(server.path, 'raise_decision', { ...base, domain: 'technical' });
|
|
const delivery = await callTool(server.path, 'raise_decision', { ...base, domain: 'delivery' });
|
|
const omitted = await callTool(server.path, 'raise_decision', base);
|
|
assert.deepEqual([technical.route_to, delivery.route_to, omitted.route_to], ['cto', 'pm', 'pm']);
|
|
assert.equal(server.calls[0].args.domain, 'technical');
|
|
assert.equal(server.calls[1].args.domain, 'delivery');
|
|
assert.equal(Object.hasOwn(server.calls[2].args, 'domain'), false);
|
|
|
|
// The harness doesn't check its own schema, so a value outside it reaches the broker, which refuses it.
|
|
await assert.rejects(callTool(server.path, 'raise_decision', { ...base, domain: 'security' }), /invalid-domain/);
|
|
assert.equal(f.store.get('SELECT count(*) n FROM decisions').n, 3);
|
|
});
|