@cohortapp/agent-sdk 2.10.0 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/init-maestro.md +16 -9
- package/docs/guides/mac-mini.md +11 -1
- package/docs/runbooks/cohort-cutover.md +16 -0
- package/lib/mcp/server.test.mjs +16 -4
- package/lib/org/client.mjs +58 -1
- package/lib/org/protocol.checksum +1 -1
- package/lib/org/protocol.mjs +98 -0
- package/lib/org/protocol.test.mjs +19 -2
- package/lib/org/resource-tools.mjs +317 -0
- package/lib/org/resource-tools.test.mjs +361 -0
- package/lib/org/tool-access.mjs +176 -0
- package/lib/org/tool-access.test.mjs +144 -0
- package/lib/org/tool-surface.mjs +431 -5
- package/lib/org/tool-surface.test.mjs +385 -8
- package/lib/org/ui-parity.mjs +196 -3
- package/lib/org/ui-parity.test.mjs +126 -7
- package/lib/tool-definitions.js +23 -2
- package/package.json +2 -2
- package/plugins/maestro-skills/.claude-plugin/marketplace.json +1 -1
- package/plugins/maestro-skills/plugin.json +4 -0
- package/plugins/maestro-skills/skills/venture-deliverables.md +176 -0
- package/policies/information-barriers.yaml +34 -7
- package/scripts/ci/check-no-residual-identity.mjs +281 -9
- package/scripts/ci/check-no-residual-identity.test.mjs +115 -2
- package/scripts/cloud-relay/voice/relay-identity.test.mjs +96 -0
- package/scripts/cloud-relay/voice/server.mjs +42 -2
- package/scripts/cost/track-claude-usage-pricing.test.mjs +183 -0
- package/scripts/cost/track-claude-usage.mjs +113 -4
- package/scripts/daemon/agent-daemon.mjs +150 -3
- package/scripts/daemon/agent-daemon.test.mjs +190 -0
- package/scripts/daemon/assurance.mjs +38 -15
- package/scripts/daemon/assurance.test.mjs +39 -1
- package/scripts/daemon/classifier-identity.test.mjs +137 -0
- package/scripts/daemon/classifier.mjs +98 -17
- package/scripts/daemon/prompt-builder-preamble.test.mjs +210 -0
- package/scripts/daemon/prompt-builder.mjs +264 -41
- package/scripts/daemon/prompt-builder.test.mjs +5 -5
- package/scripts/disclosure_boundaries.py +56 -5
- package/scripts/huddle/huddle-prompt.test.mjs +176 -0
- package/scripts/huddle/huddle-server.mjs +128 -13
- package/scripts/local-triggers/autoupdate.sh +83 -0
- package/scripts/local-triggers/generate-plists.sh +9 -0
- package/scripts/local-triggers/generate-plists.test.mjs +12 -10
- package/scripts/media-generation/brand-clause.test.mjs +135 -0
- package/scripts/media-generation/gemini-image-client.mjs +27 -9
- package/scripts/media-generation/generate-assets.mjs +102 -7
- package/scripts/pre-draft-context.py +91 -15
- package/scripts/spawn-session.sh +36 -6
- package/scripts/test-employer-grounding.py +348 -0
- package/scripts/validate_outbound.py +190 -26
|
@@ -71,23 +71,68 @@ try {
|
|
|
71
71
|
// peers list. Prevents cross-agent message interception in shared channels
|
|
72
72
|
// (ib-20260418-cross-agent-routing).
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
// Resolved lazily so a test (or a re-exec that re-points AGENT_DIR) sees the
|
|
75
|
+
// current value rather than whatever env the module happened to load under.
|
|
76
|
+
function agentDir() {
|
|
77
|
+
return process.env.AGENT_DIR || process.env.AGENT_ROOT || resolve(__dirname, "../..");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A config value the operator has not filled in yet. config/company.json ships
|
|
82
|
+
* `"name": "UNCONFIGURED"` in the scaffold and empty strings elsewhere, so BOTH
|
|
83
|
+
* shapes mean absent.
|
|
84
|
+
* @param {*} v @returns {string}
|
|
85
|
+
*/
|
|
86
|
+
function configuredStr(v) {
|
|
87
|
+
const s = typeof v === "string" ? v.trim() : "";
|
|
88
|
+
// The scaffold ships SENTINELS, not blanks: config/company.json has
|
|
89
|
+
// "UNCONFIGURED", config/agent.json has "UNCONFIGURED" / "Unconfigured Agent"
|
|
90
|
+
// / "Unconfigured Role". A sentinel is an absence and must never be rendered.
|
|
91
|
+
if (!s || /^unconfigured\b/i.test(s)) return "";
|
|
92
|
+
return s;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Read config/company.json (the company SOT). Never throws. */
|
|
96
|
+
function loadCompanyConfig(dir) {
|
|
97
|
+
try {
|
|
98
|
+
return JSON.parse(readFileSync(resolve(dir, "config/company.json"), "utf-8"));
|
|
99
|
+
} catch {
|
|
100
|
+
return {};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
75
103
|
|
|
76
|
-
let _agentIdentity = null; // { name, slackId, role, fullName, principal: {…} }
|
|
104
|
+
let _agentIdentity = null; // { name, slackId, role, fullName, company, principal: {…} }
|
|
77
105
|
let _peerAgents = []; // [{ name, slackId, role }] — all agents EXCEPT me
|
|
78
106
|
|
|
107
|
+
/** For tests: drop the cached registry so a fresh AGENT_DIR is re-read. */
|
|
108
|
+
export function _resetAgentRegistry() {
|
|
109
|
+
_agentIdentity = null;
|
|
110
|
+
_peerAgents = [];
|
|
111
|
+
}
|
|
112
|
+
|
|
79
113
|
function loadAgentRegistry() {
|
|
80
114
|
if (_agentIdentity) return; // already loaded
|
|
115
|
+
const dir = agentDir();
|
|
81
116
|
|
|
82
117
|
// 1. Primary: load THIS agent's identity from config/agent.json (SOT).
|
|
83
118
|
try {
|
|
84
|
-
const me = JSON.parse(readFileSync(resolve(
|
|
119
|
+
const me = JSON.parse(readFileSync(resolve(dir, "config/agent.json"), "utf-8"));
|
|
85
120
|
_agentIdentity = {
|
|
86
|
-
|
|
87
|
-
|
|
121
|
+
// Scrubbed to "" when unset — each consumer then picks its own honest
|
|
122
|
+
// default (a generic noun in prose; NO match at all in the @-mention test,
|
|
123
|
+
// which must never widen because identity is missing).
|
|
124
|
+
name: configuredStr(me.firstName),
|
|
125
|
+
fullName: configuredStr(me.fullName),
|
|
88
126
|
slackId: me.slackMemberId || "",
|
|
89
|
-
role: me.title
|
|
127
|
+
role: configuredStr(me.title),
|
|
90
128
|
repo: me.repoSlug,
|
|
129
|
+
// The employer, from the SAME object the name and role come from — and
|
|
130
|
+
// config/company.json when agent.json leaves it blank. It used to be
|
|
131
|
+
// a hardcoded example-company literal in the prompt below while
|
|
132
|
+
// everything around it was interpolated. INTERNAL classification comes
|
|
133
|
+
// from CONFIG, never a hardcoded company (the same doctrine as
|
|
134
|
+
// scripts/poller/gmail-poller.mjs#internalDomains).
|
|
135
|
+
company: configuredStr(me.company) || configuredStr(loadCompanyConfig(dir).name),
|
|
91
136
|
principal: me.principal
|
|
92
137
|
? {
|
|
93
138
|
firstName: me.principal.firstName || "the principal",
|
|
@@ -99,13 +144,14 @@ function loadAgentRegistry() {
|
|
|
99
144
|
} catch {
|
|
100
145
|
// Backwards compatibility for repos that haven't migrated yet.
|
|
101
146
|
try {
|
|
102
|
-
const agentTs = readFileSync(resolve(
|
|
147
|
+
const agentTs = readFileSync(resolve(dir, "config/agent.ts"), "utf-8");
|
|
103
148
|
const nameMatch = agentTs.match(/firstName:\s*['"](\w+)['"]/);
|
|
104
149
|
const slackMatch = agentTs.match(/slackMemberId:\s*['"]([^'"]+)['"]/);
|
|
105
150
|
_agentIdentity = {
|
|
106
151
|
name: nameMatch ? nameMatch[1] : "Agent",
|
|
107
152
|
slackId: slackMatch ? slackMatch[1] : "",
|
|
108
153
|
role: "agent",
|
|
154
|
+
company: configuredStr(loadCompanyConfig(dir).name),
|
|
109
155
|
principal: { firstName: "the principal", fullName: "the principal", title: "principal" },
|
|
110
156
|
};
|
|
111
157
|
} catch {
|
|
@@ -113,6 +159,7 @@ function loadAgentRegistry() {
|
|
|
113
159
|
name: "Agent",
|
|
114
160
|
slackId: "",
|
|
115
161
|
role: "agent",
|
|
162
|
+
company: configuredStr(loadCompanyConfig(dir).name),
|
|
116
163
|
principal: { firstName: "the principal", fullName: "the principal", title: "principal" },
|
|
117
164
|
};
|
|
118
165
|
}
|
|
@@ -122,7 +169,7 @@ function loadAgentRegistry() {
|
|
|
122
169
|
// maintains its own peer registry (shipped via `npx @cohortapp/agent-sdk upgrade`).
|
|
123
170
|
let agents = [];
|
|
124
171
|
try {
|
|
125
|
-
const raw = readFileSync(resolve(
|
|
172
|
+
const raw = readFileSync(resolve(dir, "config/known-agents.json"), "utf-8");
|
|
126
173
|
agents = JSON.parse(raw).agents || [];
|
|
127
174
|
} catch { /* not present — proceed without peer filtering */ }
|
|
128
175
|
|
|
@@ -131,7 +178,7 @@ function loadAgentRegistry() {
|
|
|
131
178
|
? agents.filter(a => a.slackId !== mySlackId)
|
|
132
179
|
: agents;
|
|
133
180
|
|
|
134
|
-
console.log(`[classifier] Agent identity: ${_agentIdentity.name} (${_agentIdentity.slackId || "no slack ID"}), ${_peerAgents.length} peer agents`);
|
|
181
|
+
console.log(`[classifier] Agent identity: ${_agentIdentity.name || "(unconfigured)"} (${_agentIdentity.slackId || "no slack ID"}), ${_peerAgents.length} peer agents`);
|
|
135
182
|
}
|
|
136
183
|
|
|
137
184
|
const ANTHROPIC_MODEL = "claude-haiku-4-5-20251001";
|
|
@@ -142,10 +189,41 @@ const CLAUDE_CLI_TIMEOUT_MS = 30_000;
|
|
|
142
189
|
|
|
143
190
|
// ── System prompt shared by both LLM classifiers ────────────────────────────
|
|
144
191
|
|
|
192
|
+
/**
|
|
193
|
+
* The classifier's identity sentence. Pure, so the omission rule is testable.
|
|
194
|
+
*
|
|
195
|
+
* The employer is INTERPOLATED FROM CONFIG or the clause is dropped entirely —
|
|
196
|
+
* it was a hardcoded example-company literal sitting between two interpolated
|
|
197
|
+
* fields. This
|
|
198
|
+
* is not cosmetic: the classifier's `summary` is carried verbatim into
|
|
199
|
+
* assurance.composeAck (text a human reads) and board-mirror's board row title,
|
|
200
|
+
* so a fabricated employer in its frame of reference leaks into the org record.
|
|
201
|
+
*
|
|
202
|
+
* @param {{name?:string, role?:string, company?:string, principal?:object}} id
|
|
203
|
+
* @returns {string}
|
|
204
|
+
*/
|
|
205
|
+
export function renderClassifierIdentityLine(id) {
|
|
206
|
+
const agentName = configuredStr(id && id.name) || "the agent";
|
|
207
|
+
const agentRole = configuredStr(id && id.role) || "agent";
|
|
208
|
+
const company = configuredStr(id && id.company);
|
|
209
|
+
const p = (id && id.principal && typeof id.principal === "object") ? id.principal : {};
|
|
210
|
+
const pFull = configuredStr(p.fullName);
|
|
211
|
+
const pTitle = configuredStr(p.title);
|
|
212
|
+
|
|
213
|
+
const at = company ? ` at ${company}` : "";
|
|
214
|
+
const lines = [`You are a message classifier for ${agentName}, ${agentRole} (AI-operated)${at}.`];
|
|
215
|
+
// Only assert a principal when one is actually on record — the loader's
|
|
216
|
+
// "the principal" placeholder is an absence, not a person.
|
|
217
|
+
if (pFull && pFull !== "the principal") {
|
|
218
|
+
const who = pTitle && pTitle !== "principal" ? `${pTitle} ${pFull}` : pFull;
|
|
219
|
+
lines.push(`${agentName} is the autonomous executive command layer for ${who}.`);
|
|
220
|
+
}
|
|
221
|
+
return lines.join(" ");
|
|
222
|
+
}
|
|
223
|
+
|
|
145
224
|
function buildSystemPrompt() {
|
|
146
225
|
loadAgentRegistry();
|
|
147
|
-
const agentName = _agentIdentity.name;
|
|
148
|
-
const agentRole = _agentIdentity.role || "agent";
|
|
226
|
+
const agentName = configuredStr(_agentIdentity.name) || "the agent";
|
|
149
227
|
const principalName = _agentIdentity.principal.firstName;
|
|
150
228
|
const principalFullName = _agentIdentity.principal.fullName;
|
|
151
229
|
const principalTitle = _agentIdentity.principal.title;
|
|
@@ -155,7 +233,7 @@ function buildSystemPrompt() {
|
|
|
155
233
|
? `\nKNOWN PEER AGENTS (other AI agents in the organisation — if a message @-mentions one of these by name or Slack ID, it is NOT directed at ${agentName}):\n${_peerAgents.map(a => `- ${a.name} (${a.role}, Slack: <@${a.slackId}>)`).join("\n")}\n`
|
|
156
234
|
: "";
|
|
157
235
|
|
|
158
|
-
return
|
|
236
|
+
return `${renderClassifierIdentityLine(_agentIdentity)}
|
|
159
237
|
|
|
160
238
|
Your job: classify each incoming message and return a JSON object with exactly these fields:
|
|
161
239
|
|
|
@@ -212,7 +290,7 @@ DIRECTED_AT_AGENT (CRITICAL — determines whether ${agentName} should respond.
|
|
|
212
290
|
|
|
213
291
|
Context:
|
|
214
292
|
- sender_privilege "ceo" = ${principalFullName} (always critical, always opus)
|
|
215
|
-
- sender_privilege "leadership" =
|
|
293
|
+
- sender_privilege "leadership" = executive-team senders (CFO, CIO, General Counsel, and equivalent function heads) — usually high priority
|
|
216
294
|
- sender_privilege "team" = internal team members
|
|
217
295
|
- sender_privilege "external" = partners, candidates, advisors, vendors
|
|
218
296
|
- sender_privilege "unknown" = unrecognised sender
|
|
@@ -273,7 +351,7 @@ function formatItemPrompt(item) {
|
|
|
273
351
|
];
|
|
274
352
|
// Indicate channel type so the classifier can assess directed-ness
|
|
275
353
|
loadAgentRegistry();
|
|
276
|
-
const agentName = _agentIdentity.name;
|
|
354
|
+
const agentName = configuredStr(_agentIdentity.name) || "the agent";
|
|
277
355
|
if (item.is_dm) {
|
|
278
356
|
parts.push(`Channel type: Direct message (1:1 with ${agentName})`);
|
|
279
357
|
} else if (item.is_group) {
|
|
@@ -530,10 +608,13 @@ function isDirectedAtAgent(item) {
|
|
|
530
608
|
|
|
531
609
|
// Explicit @mention or name mention in the message content
|
|
532
610
|
const content = (item.content || "").toLowerCase();
|
|
533
|
-
|
|
611
|
+
// FAIL CLOSED on an unconfigured name. The old fallback was the bare word
|
|
612
|
+
// "agent", so an unnamed seat matched any message containing "agent" and
|
|
613
|
+
// treated it as addressed to itself. No name on record ⇒ no name match.
|
|
614
|
+
const myName = configuredStr(_agentIdentity.name).toLowerCase();
|
|
534
615
|
const mySlackId = (_agentIdentity.slackId || "").toLowerCase();
|
|
535
616
|
|
|
536
|
-
if (content.includes(myName)) return true;
|
|
617
|
+
if (myName && content.includes(myName)) return true;
|
|
537
618
|
if (mySlackId && content.includes(mySlackId.toLowerCase())) return true;
|
|
538
619
|
|
|
539
620
|
// Gmail/calendar messages are directed at the current agent (they're in its inbox)
|
|
@@ -630,7 +711,7 @@ function classifyWithRules(item) {
|
|
|
630
711
|
}
|
|
631
712
|
|
|
632
713
|
// Export for use in daemon's pre-classification gate
|
|
633
|
-
export { isDirectedAtAgent, mentionsOtherAgent, loadAgentRegistry };
|
|
714
|
+
export { isDirectedAtAgent, mentionsOtherAgent, loadAgentRegistry, buildSystemPrompt };
|
|
634
715
|
|
|
635
716
|
// ── Main export ─────────────────────────────────────────────────────────────
|
|
636
717
|
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prompt-builder-preamble.test.mjs — the fallback preamble must not FABRICATE
|
|
3
|
+
* authority.
|
|
4
|
+
*
|
|
5
|
+
* WHAT BROKE
|
|
6
|
+
* `buildFallbackPreamble()` used to hardcode an autonomy grant:
|
|
7
|
+
* "<Agent> sends autonomously: all internal messages, external operational
|
|
8
|
+
* messages, candidate comms, follow-ups, calendar coordination"
|
|
9
|
+
* read from nowhere. It fires when CLAUDE.md is unreadable OR its extracted
|
|
10
|
+
* sections total <= 200 chars — i.e. on a freshly scaffolded repo that has
|
|
11
|
+
* never had autonomy bands set — while driving a real `claude --print` session
|
|
12
|
+
* that can send real messages. It also stated "You are <name>, <title> at
|
|
13
|
+
* <company>" with `company` interpolated unconditionally.
|
|
14
|
+
*
|
|
15
|
+
* WHAT THESE TESTS PIN
|
|
16
|
+
* 1. GROUNDED PATH — with a real config/agent.json + config/company.json + a
|
|
17
|
+
* GENERATED policies/action-classification.yaml, every band in the preamble
|
|
18
|
+
* comes from those files.
|
|
19
|
+
* 2. EMPTY PATH — with the scaffold's UNCONFIGURED config and no generated
|
|
20
|
+
* policy, the preamble asserts NO employer, NO autonomy grant, and says
|
|
21
|
+
* draft-not-send. Nothing is invented.
|
|
22
|
+
* 3. The hand-written TEMPLATE action-classification.yaml (which grants "fully
|
|
23
|
+
* autonomous executive operator" to every seat) is treated as ABSENT.
|
|
24
|
+
*
|
|
25
|
+
* Run: node --test scripts/daemon/prompt-builder-preamble.test.mjs
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
"use strict";
|
|
29
|
+
|
|
30
|
+
import { test } from "node:test";
|
|
31
|
+
import assert from "node:assert/strict";
|
|
32
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
33
|
+
import { tmpdir } from "node:os";
|
|
34
|
+
import { join } from "node:path";
|
|
35
|
+
|
|
36
|
+
import {
|
|
37
|
+
buildFallbackPreamble,
|
|
38
|
+
loadSeatAutonomyPolicy,
|
|
39
|
+
renderAutonomySection,
|
|
40
|
+
} from "./prompt-builder.mjs";
|
|
41
|
+
|
|
42
|
+
/** A repo root with whatever files the case needs. */
|
|
43
|
+
function seat(files) {
|
|
44
|
+
const root = mkdtempSync(join(tmpdir(), "seat-"));
|
|
45
|
+
for (const [rel, body] of Object.entries(files)) {
|
|
46
|
+
const abs = join(root, rel);
|
|
47
|
+
mkdirSync(join(abs, ".."), { recursive: true });
|
|
48
|
+
writeFileSync(abs, body);
|
|
49
|
+
}
|
|
50
|
+
return root;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const SCAFFOLD_AGENT = JSON.stringify({
|
|
54
|
+
firstName: "UNCONFIGURED",
|
|
55
|
+
lastName: "AGENT",
|
|
56
|
+
fullName: "Unconfigured Agent",
|
|
57
|
+
title: "Unconfigured Role",
|
|
58
|
+
company: "",
|
|
59
|
+
principal: { firstName: "", fullName: "", title: "" },
|
|
60
|
+
responsibilities: [],
|
|
61
|
+
operatingPrinciples: [],
|
|
62
|
+
});
|
|
63
|
+
const SCAFFOLD_COMPANY = JSON.stringify({ name: "UNCONFIGURED", description: "", products: [] });
|
|
64
|
+
|
|
65
|
+
const REAL_AGENT = JSON.stringify({
|
|
66
|
+
firstName: "Robin",
|
|
67
|
+
lastName: "Okafor",
|
|
68
|
+
fullName: "Robin Okafor",
|
|
69
|
+
title: "VP Operations",
|
|
70
|
+
altitude: "vp",
|
|
71
|
+
company: "",
|
|
72
|
+
principal: { firstName: "Dana", fullName: "Dana Lee", title: "CEO" },
|
|
73
|
+
responsibilities: ["Run the ops cadence"],
|
|
74
|
+
operatingPrinciples: ["Close the loop on every commitment"],
|
|
75
|
+
});
|
|
76
|
+
const REAL_COMPANY = JSON.stringify({ name: "Meridian Freight", description: "Regional logistics." });
|
|
77
|
+
|
|
78
|
+
/** What scripts/setup/generate-autonomy.mjs writes — note archetype + delegation_breadth. */
|
|
79
|
+
const GENERATED_POLICY = `
|
|
80
|
+
archetype: operations@vp
|
|
81
|
+
autonomy_default: execute
|
|
82
|
+
delegation_breadth: moderate
|
|
83
|
+
action_levels:
|
|
84
|
+
execute_autonomous:
|
|
85
|
+
examples:
|
|
86
|
+
- Reschedule internal meetings
|
|
87
|
+
approval_preferred:
|
|
88
|
+
examples:
|
|
89
|
+
- Send a quote to a customer
|
|
90
|
+
escalate:
|
|
91
|
+
examples:
|
|
92
|
+
- Sign a supplier contract
|
|
93
|
+
escalate_to:
|
|
94
|
+
- principal
|
|
95
|
+
decision_rights:
|
|
96
|
+
- id: dr-1
|
|
97
|
+
scope: Carrier selection
|
|
98
|
+
authority: decide
|
|
99
|
+
`;
|
|
100
|
+
|
|
101
|
+
/** The hand-written framework TEMPLATE — no archetype, no delegation_breadth. */
|
|
102
|
+
const TEMPLATE_POLICY = `
|
|
103
|
+
action_levels:
|
|
104
|
+
execute_autonomous:
|
|
105
|
+
description: Operational actions the agent executes on its own authority.
|
|
106
|
+
examples:
|
|
107
|
+
- Send emails in the agent's voice to any recipient
|
|
108
|
+
- Send candidate communications (outreach, scheduling, offers, rejections)
|
|
109
|
+
`;
|
|
110
|
+
|
|
111
|
+
// ── 1. GROUNDED PATH ────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
test("grounded: identity, employer and every autonomy band come from the real files", () => {
|
|
114
|
+
const root = seat({
|
|
115
|
+
"config/agent.json": REAL_AGENT,
|
|
116
|
+
"config/company.json": REAL_COMPANY,
|
|
117
|
+
"policies/action-classification.yaml": GENERATED_POLICY,
|
|
118
|
+
});
|
|
119
|
+
const out = buildFallbackPreamble(root);
|
|
120
|
+
|
|
121
|
+
assert.match(out, /You are Robin Okafor, VP Operations at Meridian Freight\./);
|
|
122
|
+
assert.match(out, /You report to Dana Lee, CEO\./);
|
|
123
|
+
assert.match(out, /Close the loop on every commitment/, "operating principles come from config");
|
|
124
|
+
|
|
125
|
+
assert.match(out, /generated for archetype operations@vp/);
|
|
126
|
+
assert.match(out, /You act on your own authority: Reschedule internal meetings/);
|
|
127
|
+
assert.match(out, /You get agreement first: Send a quote to a customer/);
|
|
128
|
+
assert.match(out, /You escalate and do NOT act alone: Sign a supplier contract/);
|
|
129
|
+
assert.match(out, /Escalate to: principal/);
|
|
130
|
+
assert.match(out, /Decision right — Carrier selection \(decide\)/);
|
|
131
|
+
|
|
132
|
+
// The fabricated grant is gone, verbatim.
|
|
133
|
+
assert.doesNotMatch(out, /sends autonomously/i);
|
|
134
|
+
assert.doesNotMatch(out, /all internal messages, external operational messages/i);
|
|
135
|
+
assert.doesNotMatch(out, /Full autonomy, full accountability/i);
|
|
136
|
+
|
|
137
|
+
rmSync(root, { recursive: true, force: true });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ── 2. EMPTY PATH — nothing configured means nothing asserted ────────────────
|
|
141
|
+
|
|
142
|
+
test("empty: an unconfigured seat gets NO employer and NO autonomy grant", () => {
|
|
143
|
+
const root = seat({
|
|
144
|
+
"config/agent.json": SCAFFOLD_AGENT,
|
|
145
|
+
"config/company.json": SCAFFOLD_COMPANY,
|
|
146
|
+
});
|
|
147
|
+
const out = buildFallbackPreamble(root);
|
|
148
|
+
|
|
149
|
+
// No invented employer anywhere — including the scaffold's own sentinel.
|
|
150
|
+
assert.doesNotMatch(out, /UNCONFIGURED/);
|
|
151
|
+
assert.doesNotMatch(out, / at \./, "no dangling 'at <blank>' clause");
|
|
152
|
+
|
|
153
|
+
// No grant of any kind.
|
|
154
|
+
assert.doesNotMatch(out, /sends autonomously/i);
|
|
155
|
+
assert.doesNotMatch(out, /You act on your own authority/);
|
|
156
|
+
assert.doesNotMatch(out, /escalates only/i);
|
|
157
|
+
|
|
158
|
+
// Fail closed, and say why.
|
|
159
|
+
assert.match(out, /NO AUTONOMY POLICY IS CONFIGURED FOR THIS SEAT/);
|
|
160
|
+
assert.match(out, /Draft, do not send/);
|
|
161
|
+
assert.match(out, /Nothing here grants that\./);
|
|
162
|
+
|
|
163
|
+
rmSync(root, { recursive: true, force: true });
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("empty: a repo with NO config at all still produces a safe, non-fabricated preamble", () => {
|
|
167
|
+
const root = seat({});
|
|
168
|
+
const out = buildFallbackPreamble(root);
|
|
169
|
+
assert.doesNotMatch(out, /You are .* at /, "no identity is asserted");
|
|
170
|
+
assert.match(out, /NO AUTONOMY POLICY IS CONFIGURED/);
|
|
171
|
+
assert.match(out, /Draft, do not send/);
|
|
172
|
+
rmSync(root, { recursive: true, force: true });
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// ── 3. The template is NOT ground truth ─────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
test("the framework TEMPLATE action-classification.yaml is treated as ABSENT", () => {
|
|
178
|
+
const root = seat({
|
|
179
|
+
"config/agent.json": REAL_AGENT,
|
|
180
|
+
"config/company.json": REAL_COMPANY,
|
|
181
|
+
"policies/action-classification.yaml": TEMPLATE_POLICY,
|
|
182
|
+
});
|
|
183
|
+
assert.equal(loadSeatAutonomyPolicy(root), null, "no archetype/delegation_breadth ⇒ not this seat's policy");
|
|
184
|
+
|
|
185
|
+
const out = buildFallbackPreamble(root);
|
|
186
|
+
assert.match(out, /NO AUTONOMY POLICY IS CONFIGURED FOR THIS SEAT/);
|
|
187
|
+
assert.doesNotMatch(
|
|
188
|
+
out,
|
|
189
|
+
/Send candidate communications/,
|
|
190
|
+
"the template's maximal grant must never reach a prompt",
|
|
191
|
+
);
|
|
192
|
+
rmSync(root, { recursive: true, force: true });
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("a GENERATED policy whose bands are all empty grants nothing", () => {
|
|
196
|
+
const section = renderAutonomySection({
|
|
197
|
+
archetype: "ops@vp",
|
|
198
|
+
delegation_breadth: "narrow",
|
|
199
|
+
action_levels: { execute_autonomous: { examples: [] }, approval_preferred: {}, escalate: {} },
|
|
200
|
+
decision_rights: [],
|
|
201
|
+
});
|
|
202
|
+
assert.match(section, /declares no bands/);
|
|
203
|
+
assert.match(section, /draft, do not send/i);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("loadSeatAutonomyPolicy never throws on malformed YAML", () => {
|
|
207
|
+
const root = seat({ "policies/action-classification.yaml": "::: not: [valid" });
|
|
208
|
+
assert.equal(loadSeatAutonomyPolicy(root), null);
|
|
209
|
+
rmSync(root, { recursive: true, force: true });
|
|
210
|
+
});
|