@cohortapp/agent-sdk 2.3.2 → 2.4.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/framework-features.json +30 -0
- package/lib/backlog.mjs +136 -0
- package/lib/cadences.mjs +63 -2
- package/lib/cadences.test.mjs +105 -0
- package/lib/capability/inventory.mjs +542 -0
- package/lib/capability/inventory.test.mjs +232 -0
- package/lib/capability/probe.mjs +255 -0
- package/lib/channels/contract.mjs +37 -1
- package/lib/channels/contract.test.mjs +25 -1
- package/lib/claude-bin.mjs +37 -3
- package/lib/claude-bin.test.mjs +42 -8
- package/lib/execution/disposition.mjs +501 -0
- package/lib/execution/disposition.test.mjs +482 -0
- package/lib/execution/drive.mjs +352 -0
- package/lib/execution/drive.test.mjs +270 -0
- package/lib/execution/effects.mjs +340 -0
- package/lib/execution/effects.test.mjs +193 -0
- package/lib/execution/index.mjs +152 -0
- package/lib/execution/intake.mjs +581 -0
- package/lib/execution/intake.test.mjs +343 -0
- package/lib/execution/journal.mjs +374 -0
- package/lib/execution/journal.test.mjs +261 -0
- package/lib/execution/match.mjs +331 -0
- package/lib/execution/match.test.mjs +235 -0
- package/lib/execution/pipeline.mjs +341 -0
- package/lib/execution/pipeline.test.mjs +389 -0
- package/lib/execution/route.mjs +332 -0
- package/lib/execution/route.test.mjs +186 -0
- package/lib/execution/surface-policy.mjs +446 -0
- package/lib/execution/surface-policy.test.mjs +162 -0
- package/lib/goals/admission.mjs +209 -0
- package/lib/goals/admission.test.mjs +139 -0
- package/lib/goals/classify.mjs +206 -0
- package/lib/goals/classify.test.mjs +109 -0
- package/lib/goals/collaborate.mjs +415 -0
- package/lib/goals/collaborate.test.mjs +324 -0
- package/lib/goals/gaps.mjs +111 -0
- package/lib/goals/gaps.test.mjs +284 -0
- package/lib/goals/loop.mjs +537 -0
- package/lib/goals/loop.test.mjs +719 -0
- package/lib/identity/persona.mjs +247 -0
- package/lib/identity/persona.test.mjs +117 -0
- package/lib/kpi.mjs +469 -0
- package/lib/kpi.test.mjs +244 -0
- package/lib/mandate/audit.mjs +168 -0
- package/lib/mandate/audit.test.mjs +195 -0
- package/lib/mandate/cache.mjs +162 -0
- package/lib/mandate/derive.mjs +317 -0
- package/lib/mandate/derive.test.mjs +224 -0
- package/lib/mandate/model.mjs +352 -0
- package/lib/mandate/model.test.mjs +145 -0
- package/lib/mandate/refresh.mjs +187 -0
- package/lib/mandate/refresh.test.mjs +293 -0
- package/lib/mcp/server.test.mjs +4 -4
- package/lib/org/approvals.mjs +14 -2
- package/lib/org/client.mjs +58 -22
- package/lib/org/client.test.mjs +3 -1
- package/lib/org/inbound/directedness.mjs +720 -0
- package/lib/org/inbound/directedness.test.mjs +543 -0
- package/lib/org/inbound/facts.mjs +501 -0
- package/lib/org/inbound/facts.test.mjs +375 -0
- package/lib/org/inbound/hydrate.mjs +535 -0
- package/lib/org/inbound/hydrate.test.mjs +326 -0
- package/lib/org/inbound/index.mjs +233 -0
- package/lib/org/inbound/index.test.mjs +324 -0
- package/lib/org/inbound/io.mjs +141 -0
- package/lib/org/inbound/project.mjs +201 -0
- package/lib/org/inbound/project.test.mjs +287 -0
- package/lib/org/inbound/surfaces.mjs +257 -0
- package/lib/org/knowledge.mjs +10 -1
- package/lib/org/knowledge.test.mjs +8 -1
- package/lib/org/leases.mjs +5 -0
- package/lib/org/mesh.mjs +17 -2
- package/lib/org/messaging.mjs +40 -4
- package/lib/org/messaging.test.mjs +40 -0
- package/lib/org/param-contract.mjs +694 -0
- package/lib/org/param-contract.test.mjs +451 -0
- package/lib/org/protocol.checksum +1 -1
- package/lib/org/protocol.mjs +8 -0
- package/lib/org/protocol.test.mjs +5 -1
- package/lib/org/push.mjs +1025 -0
- package/lib/org/push.test.mjs +690 -0
- package/lib/org/tool-surface.mjs +138 -38
- package/lib/org/tool-surface.test.mjs +13 -8
- package/lib/org/typing.mjs +341 -0
- package/lib/org/typing.test.mjs +291 -0
- package/lib/plan/compile.mjs +510 -0
- package/lib/plan/compile.test.mjs +286 -0
- package/lib/plan/emit.mjs +256 -0
- package/lib/plan/emit.test.mjs +246 -0
- package/lib/plan/explain.mjs +226 -0
- package/lib/plan/explain.test.mjs +188 -0
- package/lib/plan/schema.mjs +140 -0
- package/lib/resource-governor.mjs +47 -1
- package/lib/resource-governor.test.mjs +21 -1
- package/lib/setup/enroll-from-cohort.mjs +84 -16
- package/lib/setup/enroll-from-cohort.test.mjs +43 -1
- package/lib/setup/sections/identity.mjs +15 -4
- package/lib/setup/sections/identity.test.mjs +94 -0
- package/lib/setup/sections/inventory.mjs +178 -0
- package/lib/setup/sections/inventory.test.mjs +198 -0
- package/lib/setup/sections/mandate.mjs +392 -0
- package/lib/setup/sections/mandate.test.mjs +373 -0
- package/lib/setup/sections/subagents.mjs +427 -0
- package/lib/setup/sections/subagents.test.mjs +429 -0
- package/lib/setup/sections/verify.mjs +121 -0
- package/lib/setup/sections/verify.test.mjs +175 -0
- package/lib/setup/sot.mjs +2 -0
- package/lib/subagents/cli.mjs +463 -0
- package/lib/subagents/cli.test.mjs +389 -0
- package/lib/subagents/client.mjs +373 -0
- package/lib/subagents/client.test.mjs +309 -0
- package/lib/subagents/gap.mjs +268 -0
- package/lib/subagents/gap.test.mjs +234 -0
- package/lib/subagents/lock.mjs +296 -0
- package/lib/subagents/lock.test.mjs +248 -0
- package/lib/subagents/manifest.mjs +224 -0
- package/lib/subagents/manifest.test.mjs +175 -0
- package/lib/subagents/refs.mjs +274 -0
- package/lib/subagents/refs.test.mjs +204 -0
- package/lib/subagents/resolve.mjs +455 -0
- package/lib/subagents/resolve.test.mjs +422 -0
- package/lib/subagents/schema.mjs +467 -0
- package/lib/subagents/schema.test.mjs +306 -0
- package/package.json +8 -3
- package/plugins/maestro-skills/.claude-plugin/marketplace.json +16 -0
- package/policies/ai-disclosure.yaml +42 -2
- package/scaffold/CLAUDE.md +16 -2
- package/schedules/triggers/goal-steward.md +79 -0
- package/scripts/ci/conformance-org-api.mjs +792 -0
- package/scripts/ci/conformance-org-api.test.mjs +417 -0
- package/scripts/daemon/agent-daemon.mjs +36 -4
- package/scripts/daemon/cadence-handlers.mjs +145 -1
- package/scripts/daemon/goal-steward-cadence.test.mjs +243 -0
- package/scripts/daemon/inbox-deferral.mjs +45 -2
- package/scripts/daemon/inbox-deferral.test.mjs +56 -0
- package/scripts/daemon/inbox-wake.mjs +282 -0
- package/scripts/daemon/inbox-wake.test.mjs +199 -0
- package/scripts/daemon/prompt-builder.mjs +41 -1
- package/scripts/daemon/typing-registry.mjs +55 -2
- package/scripts/daemon/typing-registry.test.mjs +25 -0
- package/scripts/local-triggers/generate-plists.test.mjs +5 -5
- package/scripts/setup/gen-subagent-manifest.mjs +95 -0
- package/scripts/setup/gen-subagent-manifest.test.mjs +124 -0
- package/scripts/setup/generate-plan.mjs +108 -0
- package/scripts/setup/init-capability-manifest.mjs +70 -0
- package/scripts/setup/init-skill-marketplace.mjs +155 -0
- package/scripts/setup/init-skill-marketplace.test.mjs +193 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* persona.mjs — renders an agent's identity into the block that leads every
|
|
3
|
+
* LLM prompt the agent runs under.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS
|
|
6
|
+
* The daemon used to build its preamble by scraping four prose sections out of
|
|
7
|
+
* the agent's CLAUDE.md (`extractPreamble` in scripts/daemon/prompt-builder.mjs).
|
|
8
|
+
* That had two failure modes, both of which we hit in production:
|
|
9
|
+
*
|
|
10
|
+
* 1. A freshly-scaffolded repo has "*Configured by `maestro setup`*" sitting
|
|
11
|
+
* in those sections. The model was handed a near-empty identity and fell
|
|
12
|
+
* back to the generic-assistant register it ships with — introducing and
|
|
13
|
+
* signing itself as "an AI assistant working on behalf of <principal>",
|
|
14
|
+
* deferring on matters inside its own mandate, and writing to colleagues
|
|
15
|
+
* as a support function rather than as the person holding the role.
|
|
16
|
+
*
|
|
17
|
+
* 2. Everything the org actually knows about the member — title, seniority
|
|
18
|
+
* band, background, tone of voice, responsibilities, operating principles,
|
|
19
|
+
* charter — is pulled from the Cohort server into config/agent.json by
|
|
20
|
+
* lib/setup/enroll-from-cohort.mjs, and was then read by NOTHING on the
|
|
21
|
+
* prompt path. A fully-enrolled agent got no more persona than an empty one.
|
|
22
|
+
*
|
|
23
|
+
* This module closes both. It renders config/agent.json (+ config/company.json)
|
|
24
|
+
* — the source of truth, kept in sync with Cohort — into a persona block, and
|
|
25
|
+
* the prompt builder prepends it unconditionally. Identity now degrades
|
|
26
|
+
* gracefully instead of silently: an unconfigured field is omitted, not faked.
|
|
27
|
+
*
|
|
28
|
+
* SCOPE — what this module does NOT do
|
|
29
|
+
* It does not touch the AI-disclosure truthfulness invariant. An agent that is
|
|
30
|
+
* sincerely asked whether it is an AI still answers plainly, and
|
|
31
|
+
* policies/ai-disclosure.yaml remains the authority on that. What changes here
|
|
32
|
+
* is that the agent stops volunteering a standing disclaimer on work nobody
|
|
33
|
+
* asked to have disclaimed. Honesty on demand, not self-deprecation by default.
|
|
34
|
+
* See `voiceRules()` — the honesty bullet is deliberately part of the block,
|
|
35
|
+
* not an afterthought bolted on elsewhere.
|
|
36
|
+
*
|
|
37
|
+
* Pure-function core (`renderPersona`) so it is testable without a filesystem;
|
|
38
|
+
* `loadPersonaBlock` does the I/O and never throws.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { readFileSync } from "fs";
|
|
42
|
+
import { join } from "path";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Human-readable gloss for each archetype altitude band (archetypes/altitudes/).
|
|
46
|
+
* Used to tell the model what standing the seat carries, because a title string
|
|
47
|
+
* alone ("SVP AI Systems & Agent Platform") does not reliably convey it.
|
|
48
|
+
*/
|
|
49
|
+
const ALTITUDE_STANDING = Object.freeze({
|
|
50
|
+
founder:
|
|
51
|
+
"You are a founder. You set direction rather than wait for it, and you carry final say on most things short of the board.",
|
|
52
|
+
"c-suite":
|
|
53
|
+
"You sit on the executive team. You own your function end to end and are expected to take positions, not to relay options upward.",
|
|
54
|
+
svp:
|
|
55
|
+
"You are a senior vice president. You run your area with real independence — you decide within it and escalate only what genuinely crosses another owner's remit.",
|
|
56
|
+
vp:
|
|
57
|
+
"You are a vice president. You own your area's outcomes and make the calls inside it; you bring your principal decisions, not questions.",
|
|
58
|
+
"senior-manager":
|
|
59
|
+
"You are a senior manager. You own delivery in your area and decide the how; you agree the what with your principal.",
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/** Trim to a string, or "" for anything nullish/non-scalar. */
|
|
63
|
+
function s(v) {
|
|
64
|
+
if (v === null || v === undefined) return "";
|
|
65
|
+
if (typeof v === "string") return v.trim();
|
|
66
|
+
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
|
67
|
+
return "";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Normalise to a clean string[] — drops blanks, coerces scalars, ignores junk. */
|
|
71
|
+
function list(v) {
|
|
72
|
+
if (!Array.isArray(v)) return [];
|
|
73
|
+
return v.map(s).filter(Boolean);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Free-prose persona/background, wherever the org happens to have put it.
|
|
78
|
+
* Cohort profile sections land in `persona`; older configs used `bio`/`background`.
|
|
79
|
+
* @param {object} a config/agent.json
|
|
80
|
+
* @returns {string}
|
|
81
|
+
*/
|
|
82
|
+
function personaProse(a) {
|
|
83
|
+
return s(a.persona) || s(a.background) || s(a.bio) || "";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The reporting line, phrased as a peer relationship rather than a service one.
|
|
88
|
+
* Returns "" when no principal is configured — better silent than invented.
|
|
89
|
+
* @param {object} a config/agent.json
|
|
90
|
+
* @returns {string}
|
|
91
|
+
*/
|
|
92
|
+
function reportingLine(a) {
|
|
93
|
+
const p = a.principal && typeof a.principal === "object" ? a.principal : {};
|
|
94
|
+
const name = s(p.fullName) || s(p.firstName);
|
|
95
|
+
if (!name) return "";
|
|
96
|
+
const title = s(p.title);
|
|
97
|
+
return title ? `You report to ${name}, ${title}.` : `You report to ${name}.`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The voice rules — the behavioural half of the block, and the reason it exists.
|
|
102
|
+
*
|
|
103
|
+
* These are phrased as standing facts about who the agent is rather than as
|
|
104
|
+
* prohibitions, because a negative instruction ("don't call yourself an AI")
|
|
105
|
+
* tends to be obeyed literally and narrowly while the underlying deferential
|
|
106
|
+
* register survives. The final bullet preserves the non-overridable truthfulness
|
|
107
|
+
* invariant from policies/ai-disclosure.yaml in the same breath — the agent is
|
|
108
|
+
* senior AND honest, and neither is traded for the other.
|
|
109
|
+
*
|
|
110
|
+
* @param {object} a config/agent.json
|
|
111
|
+
* @returns {string[]} bullet lines
|
|
112
|
+
*/
|
|
113
|
+
function voiceRules(a) {
|
|
114
|
+
const principal = a.principal && typeof a.principal === "object" ? a.principal : {};
|
|
115
|
+
const pName = s(principal.firstName) || s(principal.fullName);
|
|
116
|
+
// Only claim a reporting line when one is actually configured. Asserting
|
|
117
|
+
// "you report to your principal" at an agent with no principal on record
|
|
118
|
+
// invents a relationship and re-introduces the deference we are removing.
|
|
119
|
+
const reportsClause = pName
|
|
120
|
+
? ` You report to ${pName} the way any senior colleague reports to their manager, and you do not preface your work with that fact.`
|
|
121
|
+
: "";
|
|
122
|
+
|
|
123
|
+
return [
|
|
124
|
+
"You hold this role. Write as the person who holds it — with the judgement, standing and directness the title carries. You are a full member of this team, not a service attached to it.",
|
|
125
|
+
`Do not introduce, describe or sign yourself as an assistant, a bot, an AI helper, or as "working on behalf of" someone.${reportsClause}`,
|
|
126
|
+
"Do not position yourself as junior to whoever you are writing to, including your principal or the CEO. State what you did, what you found, and what you recommend. Ask for a decision only when the decision is genuinely theirs to make.",
|
|
127
|
+
"Do not thank people for their patience, apologise for taking up their time, or hedge a finding you are confident in. Colleagues at your level do not do this, and it reads as a tell.",
|
|
128
|
+
"Disagree when you disagree, and say so first rather than burying it under agreement. A concern you soften into politeness is a concern you failed to raise.",
|
|
129
|
+
"Being straight about what you are is not negotiable and does not conflict with any of the above: if someone sincerely asks whether they are dealing with an AI, say so plainly and without drama. That is a direct answer to a direct question — not a standing disclaimer you attach to your own work.",
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Render the persona block from resolved config. Pure — no I/O, never throws.
|
|
135
|
+
*
|
|
136
|
+
* Every section is conditional: a field the org has not filled in is omitted
|
|
137
|
+
* rather than rendered as a placeholder, so a partially-enrolled agent gets a
|
|
138
|
+
* shorter block instead of a block full of "unconfigured".
|
|
139
|
+
*
|
|
140
|
+
* @param {object} agentCfg parsed config/agent.json
|
|
141
|
+
* @param {object} [companyCfg] parsed config/company.json
|
|
142
|
+
* @param {object} [opts]
|
|
143
|
+
* @param {boolean} [opts.includeVoiceRules=true] emit the "How you write" section
|
|
144
|
+
* @returns {string} the persona block, or "" when there is no identity at all
|
|
145
|
+
*/
|
|
146
|
+
export function renderPersona(agentCfg, companyCfg = {}, opts = {}) {
|
|
147
|
+
const a = agentCfg && typeof agentCfg === "object" ? agentCfg : {};
|
|
148
|
+
const co = companyCfg && typeof companyCfg === "object" ? companyCfg : {};
|
|
149
|
+
const { includeVoiceRules = true } = opts;
|
|
150
|
+
|
|
151
|
+
const name = s(a.fullName) || [s(a.firstName), s(a.lastName)].filter(Boolean).join(" ").trim();
|
|
152
|
+
const title = s(a.title);
|
|
153
|
+
const company = s(a.company) || s(co.name);
|
|
154
|
+
|
|
155
|
+
// No name and no title means nothing has been configured — emit nothing and
|
|
156
|
+
// let the caller fall back rather than render a hollow "You are ." line.
|
|
157
|
+
if (!name && !title) return "";
|
|
158
|
+
|
|
159
|
+
const out = [];
|
|
160
|
+
out.push("## Who you are");
|
|
161
|
+
out.push("");
|
|
162
|
+
|
|
163
|
+
const opener = [name, title && `${title}`].filter(Boolean).join(", ");
|
|
164
|
+
out.push(company ? `You are ${opener} at ${company}.` : `You are ${opener}.`);
|
|
165
|
+
|
|
166
|
+
const standing = ALTITUDE_STANDING[s(a.altitude)];
|
|
167
|
+
if (standing) out.push(standing);
|
|
168
|
+
|
|
169
|
+
const reports = reportingLine(a);
|
|
170
|
+
if (reports) out.push(reports);
|
|
171
|
+
|
|
172
|
+
const desc = s(a.companyDescription) || s(co.description);
|
|
173
|
+
if (desc) {
|
|
174
|
+
out.push("");
|
|
175
|
+
out.push(desc);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const prose = personaProse(a);
|
|
179
|
+
if (prose) {
|
|
180
|
+
out.push("");
|
|
181
|
+
out.push("### Background");
|
|
182
|
+
out.push("");
|
|
183
|
+
out.push(prose);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const responsibilities = list(a.responsibilities);
|
|
187
|
+
if (responsibilities.length) {
|
|
188
|
+
out.push("");
|
|
189
|
+
out.push("### What you own");
|
|
190
|
+
out.push("");
|
|
191
|
+
for (const r of responsibilities) out.push(`- ${r}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const principles = list(a.operatingPrinciples);
|
|
195
|
+
if (principles.length) {
|
|
196
|
+
out.push("");
|
|
197
|
+
out.push("### How you operate");
|
|
198
|
+
out.push("");
|
|
199
|
+
for (const p of principles) out.push(`- ${p}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const comms = a.communication && typeof a.communication === "object" ? a.communication : {};
|
|
203
|
+
const defaultTone = s(comms.defaultTone);
|
|
204
|
+
const externalTone = s(comms.externalTone);
|
|
205
|
+
if (defaultTone || externalTone) {
|
|
206
|
+
out.push("");
|
|
207
|
+
out.push("### Your register");
|
|
208
|
+
out.push("");
|
|
209
|
+
if (defaultTone) out.push(`- Default tone: ${defaultTone}`);
|
|
210
|
+
if (externalTone) out.push(`- With people outside ${company || "the company"}: ${externalTone}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (includeVoiceRules) {
|
|
214
|
+
out.push("");
|
|
215
|
+
out.push("### How you write and carry yourself");
|
|
216
|
+
out.push("");
|
|
217
|
+
for (const rule of voiceRules(a)) out.push(`- ${rule}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return out.join("\n").trim();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Read config/agent.json + config/company.json from an agent repo and render the
|
|
225
|
+
* persona block. Fail-open: any read/parse error yields "" so a malformed config
|
|
226
|
+
* degrades the prompt rather than taking the daemon down.
|
|
227
|
+
*
|
|
228
|
+
* @param {string} agentRoot absolute path to the agent repo
|
|
229
|
+
* @param {object} [opts] forwarded to renderPersona
|
|
230
|
+
* @returns {string}
|
|
231
|
+
*/
|
|
232
|
+
export function loadPersonaBlock(agentRoot, opts = {}) {
|
|
233
|
+
const read = (rel) => {
|
|
234
|
+
try {
|
|
235
|
+
return JSON.parse(readFileSync(join(agentRoot, rel), "utf-8"));
|
|
236
|
+
} catch {
|
|
237
|
+
return {};
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
try {
|
|
241
|
+
return renderPersona(read("config/agent.json"), read("config/company.json"), opts);
|
|
242
|
+
} catch {
|
|
243
|
+
return "";
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export const _test = { ALTITUDE_STANDING, personaProse, reportingLine, voiceRules, list };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
import { renderPersona, loadPersonaBlock } from "./persona.mjs";
|
|
8
|
+
|
|
9
|
+
const AGENT = {
|
|
10
|
+
firstName: "Jamie",
|
|
11
|
+
lastName: "Okafor",
|
|
12
|
+
fullName: "Jamie Okafor",
|
|
13
|
+
title: "SVP AI Systems & Agent Platform",
|
|
14
|
+
altitude: "svp",
|
|
15
|
+
company: "Northwind",
|
|
16
|
+
principal: { firstName: "Robin", fullName: "Robin Vance", title: "Chief Product & Technology Officer" },
|
|
17
|
+
responsibilities: ["Agent platform reliability", "Fleet enrolment integrity"],
|
|
18
|
+
operatingPrinciples: ["Evidence over opinion"],
|
|
19
|
+
communication: { defaultTone: "warm-professional", externalTone: "formal" },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
test("renders name, title, company and the seniority standing for the altitude band", () => {
|
|
23
|
+
const out = renderPersona(AGENT);
|
|
24
|
+
assert.match(out, /You are Jamie Okafor, SVP AI Systems & Agent Platform at Northwind\./);
|
|
25
|
+
assert.match(out, /You are a senior vice president\./);
|
|
26
|
+
assert.match(out, /You report to Robin Vance, Chief Product & Technology Officer\./);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("renders responsibilities, principles and register when present", () => {
|
|
30
|
+
const out = renderPersona(AGENT);
|
|
31
|
+
assert.match(out, /### What you own/);
|
|
32
|
+
assert.match(out, /- Agent platform reliability/);
|
|
33
|
+
assert.match(out, /### How you operate/);
|
|
34
|
+
assert.match(out, /- Evidence over opinion/);
|
|
35
|
+
assert.match(out, /Default tone: warm-professional/);
|
|
36
|
+
assert.match(out, /outside Northwind: formal/);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// The regression this module exists for: the agent signing off as an AI
|
|
40
|
+
// assistant working on behalf of its principal.
|
|
41
|
+
test("instructs against assistant framing and on-behalf-of signatures", () => {
|
|
42
|
+
const out = renderPersona(AGENT);
|
|
43
|
+
assert.match(out, /Do not introduce, describe or sign yourself as an assistant/);
|
|
44
|
+
assert.match(out, /working on behalf of/); // named as the thing NOT to do
|
|
45
|
+
assert.match(out, /You report to Robin the way any senior colleague reports to their manager/);
|
|
46
|
+
assert.match(out, /full member of this team, not a service attached to it/);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// The truthfulness invariant in policies/ai-disclosure.yaml is locked and must
|
|
50
|
+
// survive the seniority rewrite — senior AND honest, neither traded for the other.
|
|
51
|
+
test("preserves the answer-truthfully-if-asked invariant", () => {
|
|
52
|
+
const out = renderPersona(AGENT);
|
|
53
|
+
assert.match(out, /sincerely asks whether they are dealing with an AI, say so plainly/);
|
|
54
|
+
assert.match(out, /not a standing disclaimer/);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("omits unconfigured sections rather than rendering placeholders", () => {
|
|
58
|
+
const out = renderPersona({ fullName: "Ada Lovelace", title: "Head of Analysis" });
|
|
59
|
+
assert.match(out, /You are Ada Lovelace, Head of Analysis\./);
|
|
60
|
+
assert.doesNotMatch(out, /### What you own/);
|
|
61
|
+
assert.doesNotMatch(out, /### How you operate/);
|
|
62
|
+
assert.doesNotMatch(out, /### Your register/);
|
|
63
|
+
assert.doesNotMatch(out, /You report to/);
|
|
64
|
+
assert.doesNotMatch(out, /undefined|null|unconfigured/i);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("returns empty string when there is no identity at all", () => {
|
|
68
|
+
assert.equal(renderPersona({}), "");
|
|
69
|
+
assert.equal(renderPersona(null), "");
|
|
70
|
+
assert.equal(renderPersona({ company: "Northwind" }), "");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("persona prose falls back across persona → background → bio", () => {
|
|
74
|
+
assert.match(renderPersona({ ...AGENT, persona: "P-text" }), /### Background\n\nP-text/);
|
|
75
|
+
assert.match(renderPersona({ ...AGENT, background: "B-text" }), /### Background\n\nB-text/);
|
|
76
|
+
assert.match(renderPersona({ ...AGENT, bio: "Bio-text" }), /### Background\n\nBio-text/);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("an unknown altitude drops the standing line without breaking the block", () => {
|
|
80
|
+
const out = renderPersona({ ...AGENT, altitude: "wizard" });
|
|
81
|
+
assert.match(out, /You are Jamie Okafor/);
|
|
82
|
+
assert.doesNotMatch(out, /senior vice president/);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("company falls back to config/company.json when agent.json lacks it", () => {
|
|
86
|
+
const out = renderPersona({ fullName: "Ada Lovelace", title: "Head of Analysis" }, { name: "Northwind" });
|
|
87
|
+
assert.match(out, /Head of Analysis at Northwind\./);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("includeVoiceRules:false emits identity only", () => {
|
|
91
|
+
const out = renderPersona(AGENT, {}, { includeVoiceRules: false });
|
|
92
|
+
assert.match(out, /You are Jamie Okafor/);
|
|
93
|
+
assert.doesNotMatch(out, /How you write and carry yourself/);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("junk values in list fields are dropped, not rendered", () => {
|
|
97
|
+
const out = renderPersona({ ...AGENT, responsibilities: ["real", "", null, {}, 42] });
|
|
98
|
+
assert.match(out, /- real/);
|
|
99
|
+
assert.match(out, /- 42/);
|
|
100
|
+
assert.doesNotMatch(out, /\[object Object\]/);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("loadPersonaBlock reads an agent repo, and fails open on a broken config", () => {
|
|
104
|
+
const dir = mkdtempSync(join(tmpdir(), "persona-"));
|
|
105
|
+
try {
|
|
106
|
+
mkdirSync(join(dir, "config"));
|
|
107
|
+
writeFileSync(join(dir, "config/agent.json"), JSON.stringify(AGENT));
|
|
108
|
+
assert.match(loadPersonaBlock(dir), /You are Jamie Okafor/);
|
|
109
|
+
|
|
110
|
+
writeFileSync(join(dir, "config/agent.json"), "{ not json");
|
|
111
|
+
assert.equal(loadPersonaBlock(dir), "");
|
|
112
|
+
|
|
113
|
+
assert.equal(loadPersonaBlock(join(dir, "nope")), "");
|
|
114
|
+
} finally {
|
|
115
|
+
rmSync(dir, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
});
|