@cohortapp/agent-sdk 2.10.0 → 2.11.1
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/channels/inbox-item.mjs +12 -0
- package/lib/channels/inbox-item.test.mjs +33 -0
- package/lib/execution/disposition.mjs +13 -2
- package/lib/execution/disposition.test.mjs +19 -2
- package/lib/execution/pipeline.test.mjs +4 -1
- package/lib/mcp/server.test.mjs +16 -4
- package/lib/org/client.mjs +58 -1
- package/lib/org/messaging.mjs +5 -0
- package/lib/org/messaging.test.mjs +7 -0
- 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 +212 -5
- package/scripts/daemon/agent-daemon.test.mjs +307 -0
- package/scripts/daemon/assurance.mjs +38 -15
- package/scripts/daemon/assurance.test.mjs +39 -1
- package/scripts/daemon/cadence-handlers.mjs +48 -5
- package/scripts/daemon/cadence-handlers.test.mjs +57 -2
- package/scripts/daemon/classifier-identity.test.mjs +137 -0
- package/scripts/daemon/classifier.mjs +98 -17
- package/scripts/daemon/inbox-deferral.mjs +49 -24
- package/scripts/daemon/inbox-deferral.test.mjs +39 -1
- 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/daemon/responder.mjs +9 -0
- 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/poller/inbox-scan-poller.mjs +7 -0
- package/scripts/poller/utils.mjs +11 -0
- 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
|
@@ -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
|
+
});
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
import { readFileSync, readdirSync } from "fs";
|
|
7
7
|
import { join } from "path";
|
|
8
|
+
import { createRequire } from "node:module";
|
|
8
9
|
import { compileContext } from "./context-compiler.mjs";
|
|
9
|
-
import {
|
|
10
|
+
import { renderPersona } from "../../lib/identity/persona.mjs";
|
|
10
11
|
import { wrapExternalContent } from "../../lib/security/external-content.mjs";
|
|
11
12
|
import { isEnabled as orgEnabled } from "../../lib/org/client.mjs";
|
|
12
13
|
import { recall as orgRecall } from "../../lib/org/knowledge.mjs";
|
|
@@ -14,12 +15,22 @@ import { outcomeSourceShareable } from "./session-outcomes.mjs";
|
|
|
14
15
|
|
|
15
16
|
const AGENT_REPO_DIR = process.env.AGENT_DIR || join(new URL(".", import.meta.url).pathname, "../..");
|
|
16
17
|
|
|
17
|
-
//
|
|
18
|
+
// js-yaml, synchronously. The autonomy policy is read on the fallback-preamble
|
|
19
|
+
// path, which is sync by contract; the org-config reader below can afford the
|
|
20
|
+
// dynamic import, this cannot. js-yaml is a declared dependency of this package.
|
|
21
|
+
const _require = createRequire(import.meta.url);
|
|
22
|
+
const yaml = _require("js-yaml");
|
|
23
|
+
|
|
24
|
+
// Load agent identity from canonical SOT. Used for the backlog-item owner label.
|
|
25
|
+
// The fallback carries ONLY a generic first name — it used to also carry
|
|
26
|
+
// `company: "the company"` and a fabricated principal, which is a claim about
|
|
27
|
+
// the seat that nothing read. Identity for prompts goes through
|
|
28
|
+
// renderSeatPersona() below, which omits an unset field rather than filling it.
|
|
18
29
|
function loadAgent() {
|
|
19
30
|
try {
|
|
20
31
|
return JSON.parse(readFileSync(join(AGENT_REPO_DIR, "config/agent.json"), "utf-8"));
|
|
21
32
|
} catch {
|
|
22
|
-
return { firstName: "
|
|
33
|
+
return { firstName: "agent" };
|
|
23
34
|
}
|
|
24
35
|
}
|
|
25
36
|
|
|
@@ -128,10 +139,94 @@ let cachedPreamble = null;
|
|
|
128
139
|
// back to a generic-assistant register.
|
|
129
140
|
let cachedPersona = null;
|
|
130
141
|
|
|
142
|
+
/**
|
|
143
|
+
* A config value the operator has not filled in yet.
|
|
144
|
+
*
|
|
145
|
+
* The scaffold does not ship empty strings for everything — it ships SENTINELS:
|
|
146
|
+
* config/company.json has `"name": "UNCONFIGURED"`, config/agent.json has
|
|
147
|
+
* `"firstName": "UNCONFIGURED"`, `"fullName": "Unconfigured Agent"`,
|
|
148
|
+
* `"title": "Unconfigured Role"`. lib/identity/persona.mjs#renderPersona omits
|
|
149
|
+
* EMPTY fields, which is right, but a sentinel is not empty — so an untouched
|
|
150
|
+
* scaffold rendered "You are Unconfigured Agent, Unconfigured Role at
|
|
151
|
+
* UNCONFIGURED." into a live prompt. Treat the sentinel as absence.
|
|
152
|
+
*
|
|
153
|
+
* @param {*} v
|
|
154
|
+
* @returns {string} the trimmed value, or "" when absent/sentinel
|
|
155
|
+
*/
|
|
156
|
+
function configuredStr(v) {
|
|
157
|
+
const s = typeof v === "string" ? v.trim() : "";
|
|
158
|
+
if (!s) return "";
|
|
159
|
+
if (/^unconfigured\b/i.test(s)) return "";
|
|
160
|
+
return s;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Strip scaffold sentinels out of config/agent.json before it is rendered, so
|
|
165
|
+
* `renderPersona`'s omit-when-unset rule actually fires on a fresh repo.
|
|
166
|
+
* @param {object} a
|
|
167
|
+
* @returns {object}
|
|
168
|
+
*/
|
|
169
|
+
function scrubAgentConfig(a) {
|
|
170
|
+
const src = a && typeof a === "object" ? a : {};
|
|
171
|
+
const out = { ...src };
|
|
172
|
+
for (const k of ["firstName", "lastName", "fullName", "title", "company", "companyDescription", "persona", "background", "bio"]) {
|
|
173
|
+
if (k in out) out[k] = configuredStr(out[k]);
|
|
174
|
+
}
|
|
175
|
+
// A surname with no first name and no full name is not an identity — better
|
|
176
|
+
// to render no name at all than "You are AGENT." (the scaffold ships
|
|
177
|
+
// firstName "UNCONFIGURED" / lastName "AGENT").
|
|
178
|
+
if (!out.firstName && !out.fullName) out.lastName = "";
|
|
179
|
+
if (src.principal && typeof src.principal === "object") {
|
|
180
|
+
const p = { ...src.principal };
|
|
181
|
+
for (const k of ["firstName", "lastName", "fullName", "title"]) p[k] = configuredStr(p[k]);
|
|
182
|
+
out.principal = p;
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Same, for config/company.json. */
|
|
188
|
+
function scrubCompanyConfig(c) {
|
|
189
|
+
const src = c && typeof c === "object" ? c : {};
|
|
190
|
+
const out = { ...src };
|
|
191
|
+
for (const k of ["name", "legalName", "description", "tagline", "industry", "stage"]) {
|
|
192
|
+
if (k in out) out[k] = configuredStr(out[k]);
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Read + scrub both config files. Never throws. @returns {{agent:object, company:object}} */
|
|
198
|
+
function readSeatConfig(root) {
|
|
199
|
+
const read = (rel) => {
|
|
200
|
+
try { return JSON.parse(readFileSync(join(root, rel), "utf-8")); } catch { return {}; }
|
|
201
|
+
};
|
|
202
|
+
return {
|
|
203
|
+
agent: scrubAgentConfig(read("config/agent.json")),
|
|
204
|
+
company: scrubCompanyConfig(read("config/company.json")),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Render the persona block for a seat — config/agent.json + config/company.json
|
|
210
|
+
* through lib/identity/persona.mjs#renderPersona, with scaffold sentinels
|
|
211
|
+
* scrubbed first so an unconfigured field is OMITTED rather than asserted.
|
|
212
|
+
*
|
|
213
|
+
* @param {string} root agent repo root
|
|
214
|
+
* @param {object} [opts] forwarded to renderPersona
|
|
215
|
+
* @returns {string} "" when nothing is configured
|
|
216
|
+
*/
|
|
217
|
+
export function renderSeatPersona(root, opts = {}) {
|
|
218
|
+
try {
|
|
219
|
+
const { agent, company } = readSeatConfig(root);
|
|
220
|
+
return renderPersona(agent, company, opts) || "";
|
|
221
|
+
} catch {
|
|
222
|
+
return "";
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
131
226
|
/** Render (once per process) the persona block from config/agent.json. */
|
|
132
227
|
function loadPersona() {
|
|
133
228
|
if (cachedPersona !== null) return cachedPersona;
|
|
134
|
-
cachedPersona =
|
|
229
|
+
cachedPersona = renderSeatPersona(AGENT_REPO_DIR);
|
|
135
230
|
return cachedPersona;
|
|
136
231
|
}
|
|
137
232
|
|
|
@@ -148,7 +243,7 @@ function loadPreamble() {
|
|
|
148
243
|
cachedPreamble = extractPreamble(raw);
|
|
149
244
|
} catch (err) {
|
|
150
245
|
console.error(`[prompt-builder] Failed to read CLAUDE.md: ${err.message}`);
|
|
151
|
-
cachedPreamble =
|
|
246
|
+
cachedPreamble = fallbackPreamble();
|
|
152
247
|
}
|
|
153
248
|
return cachedPreamble;
|
|
154
249
|
}
|
|
@@ -196,44 +291,172 @@ function extractPreamble(raw) {
|
|
|
196
291
|
|
|
197
292
|
// If extraction got something reasonable, use it; otherwise fall back
|
|
198
293
|
if (extracted.length > 200) return extracted;
|
|
199
|
-
return
|
|
294
|
+
return fallbackPreamble();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Read the seat's GENERATED autonomy policy, or null.
|
|
299
|
+
*
|
|
300
|
+
* Provenance check, not just a file check. `policies/action-classification.yaml`
|
|
301
|
+
* exists in two very different shapes:
|
|
302
|
+
*
|
|
303
|
+
* - the hand-written TEMPLATE that ships with the framework, whose header says
|
|
304
|
+
* "the agent is a fully autonomous executive operator" for every seat; and
|
|
305
|
+
* - the per-seat artefact `scripts/setup/generate-autonomy.mjs` renders from
|
|
306
|
+
* `lib/autonomy.mjs#buildActionClassification` for THIS agent's archetype
|
|
307
|
+
* (function × altitude), which is the only one that describes a real grant.
|
|
308
|
+
*
|
|
309
|
+
* Only the generated artefact carries `archetype` + `delegation_breadth`, so we
|
|
310
|
+
* require both. A template copy is treated as ABSENT — inheriting another
|
|
311
|
+
* seat's maximal grant is precisely the fabrication this function exists to stop.
|
|
312
|
+
*
|
|
313
|
+
* Never throws; returns null on any read/parse failure.
|
|
314
|
+
*
|
|
315
|
+
* @param {string} [root=AGENT_REPO_DIR]
|
|
316
|
+
* @returns {object|null}
|
|
317
|
+
*/
|
|
318
|
+
export function loadSeatAutonomyPolicy(root = AGENT_REPO_DIR) {
|
|
319
|
+
try {
|
|
320
|
+
const raw = readFileSync(join(root, "policies/action-classification.yaml"), "utf-8");
|
|
321
|
+
const doc = yaml.load(raw);
|
|
322
|
+
if (!doc || typeof doc !== "object") return null;
|
|
323
|
+
if (!doc.archetype || !doc.delegation_breadth) return null;
|
|
324
|
+
return doc;
|
|
325
|
+
} catch {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Clean string[] from a config list; drops blanks. */
|
|
331
|
+
function strList(v, max = 12) {
|
|
332
|
+
if (!Array.isArray(v)) return [];
|
|
333
|
+
return v.map((x) => (typeof x === "string" ? x.trim() : "")).filter(Boolean).slice(0, max);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Render what this seat may actually do, from the generated policy — or, when
|
|
338
|
+
* no policy has been generated, a fail-closed restraint.
|
|
339
|
+
*
|
|
340
|
+
* WHY THIS REPLACED A LITERAL
|
|
341
|
+
* The previous text granted the seat authority to send, unreviewed, "all
|
|
342
|
+
* internal messages, external operational messages, candidate comms,
|
|
343
|
+
* follow-ups, calendar coordination" and to escalate only four narrow
|
|
344
|
+
* categories. NONE of that was read from anywhere. It is a GOVERNANCE claim,
|
|
345
|
+
* not a naming one, and it fired on exactly the population least entitled to
|
|
346
|
+
* it: this preamble is used when CLAUDE.md is unreadable OR its extracted
|
|
347
|
+
* sections total <= 200 chars, i.e. on a freshly scaffolded repo that has
|
|
348
|
+
* never had autonomy bands set — while driving a real `claude --print`
|
|
349
|
+
* session that can send real messages.
|
|
350
|
+
*
|
|
351
|
+
* So: bands come from the seat's generated policy or they are not asserted.
|
|
352
|
+
* With no policy the correct posture is LESS authority, not more — the
|
|
353
|
+
* preamble says draft-and-confirm and says why. That is a statement about the
|
|
354
|
+
* ABSENCE of a configured grant, not an invented grant.
|
|
355
|
+
*
|
|
356
|
+
* @param {object|null} policy result of loadSeatAutonomyPolicy()
|
|
357
|
+
* @returns {string}
|
|
358
|
+
*/
|
|
359
|
+
export function renderAutonomySection(policy) {
|
|
360
|
+
if (!policy) {
|
|
361
|
+
return [
|
|
362
|
+
"Authority (NO AUTONOMY POLICY IS CONFIGURED FOR THIS SEAT):",
|
|
363
|
+
"- `policies/action-classification.yaml` has not been generated for you (run `maestro upgrade`, which runs scripts/setup/generate-autonomy.mjs).",
|
|
364
|
+
"- Until it has been, assume NO standing authority to act outward. Draft, do not send. Prepare the message, document or calendar change and put it in front of your principal for a decision.",
|
|
365
|
+
"- Do not make commitments, send outbound communications, or take irreversible actions on your own authority on the strength of this prompt. Nothing here grants that.",
|
|
366
|
+
].join("\n");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const levels = (policy.action_levels && typeof policy.action_levels === "object") ? policy.action_levels : {};
|
|
370
|
+
const green = strList(levels.execute_autonomous && levels.execute_autonomous.examples);
|
|
371
|
+
const amber = strList(levels.approval_preferred && levels.approval_preferred.examples);
|
|
372
|
+
const red = strList(levels.escalate && levels.escalate.examples);
|
|
373
|
+
const escalateTo = strList(levels.escalate && levels.escalate.escalate_to, 4);
|
|
374
|
+
const rights = Array.isArray(policy.decision_rights) ? policy.decision_rights.slice(0, 8) : [];
|
|
375
|
+
|
|
376
|
+
const out = [
|
|
377
|
+
`Authority (from policies/action-classification.yaml, generated for archetype ${policy.archetype}):`,
|
|
378
|
+
];
|
|
379
|
+
if (policy.autonomy_default) out.push(`- Default posture: ${policy.autonomy_default}`);
|
|
380
|
+
if (green.length) out.push(`- You act on your own authority: ${green.join("; ")}`);
|
|
381
|
+
if (amber.length) out.push(`- You get agreement first: ${amber.join("; ")}`);
|
|
382
|
+
if (red.length) out.push(`- You escalate and do NOT act alone: ${red.join("; ")}`);
|
|
383
|
+
if (escalateTo.length) out.push(`- Escalate to: ${escalateTo.join(", ")}`);
|
|
384
|
+
for (const d of rights) {
|
|
385
|
+
if (!d || typeof d !== "object") continue;
|
|
386
|
+
const scope = typeof d.scope === "string" ? d.scope.trim() : "";
|
|
387
|
+
const authority = typeof d.authority === "string" ? d.authority.trim() : "";
|
|
388
|
+
if (!scope) continue;
|
|
389
|
+
out.push(`- Decision right — ${scope}${authority ? ` (${authority})` : ""}`);
|
|
390
|
+
}
|
|
391
|
+
// A generated policy with every band empty asserts nothing. Say so rather
|
|
392
|
+
// than implying the empty bands are a grant.
|
|
393
|
+
if (out.length === 1) {
|
|
394
|
+
out.push("- The generated policy declares no bands. Treat that as no standing authority: draft, do not send.");
|
|
395
|
+
}
|
|
396
|
+
return out.join("\n");
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Preamble used ONLY when CLAUDE.md is unreadable or effectively empty.
|
|
401
|
+
*
|
|
402
|
+
* Everything seat-specific is rendered from the source of truth:
|
|
403
|
+
* identity/company/responsibilities/operating principles → config/agent.json +
|
|
404
|
+
* config/company.json via lib/identity/persona.mjs#renderPersona (which omits
|
|
405
|
+
* an unset field rather than faking it);
|
|
406
|
+
* authority → the seat's generated action-classification policy.
|
|
407
|
+
*
|
|
408
|
+
* What remains hardcoded is framework craft doctrine that makes NO claim about
|
|
409
|
+
* this seat's employer or its authority, so it is honest on any deployment.
|
|
410
|
+
*
|
|
411
|
+
* @returns {string}
|
|
412
|
+
*/
|
|
413
|
+
export function buildFallbackPreamble(root = AGENT_REPO_DIR) {
|
|
414
|
+
const parts = [];
|
|
415
|
+
|
|
416
|
+
// Identity, company, responsibilities and operating principles — all from
|
|
417
|
+
// config. Voice rules are omitted here because buildPrompt() already leads
|
|
418
|
+
// with the full persona block; this is the degraded path, not a second copy.
|
|
419
|
+
const persona = renderSeatPersona(root, { includeVoiceRules: false });
|
|
420
|
+
if (persona) parts.push(persona);
|
|
421
|
+
|
|
422
|
+
parts.push(renderAutonomySection(loadSeatAutonomyPolicy(root)));
|
|
423
|
+
|
|
424
|
+
parts.push(
|
|
425
|
+
[
|
|
426
|
+
"How you work:",
|
|
427
|
+
"- Follow-through over brilliance — track every commitment until it closes",
|
|
428
|
+
"- Concise over comprehensive — sharp recommendations, not exhaustive reports",
|
|
429
|
+
"- Evidence over opinion — cite sources, and say when you do not know",
|
|
430
|
+
"- Audit everything — every action is logged",
|
|
431
|
+
].join("\n"),
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
parts.push(
|
|
435
|
+
[
|
|
436
|
+
"Document Sharing (CRITICAL):",
|
|
437
|
+
"- NEVER reference local file paths in outbound communications",
|
|
438
|
+
"- Upload to shared storage or generate a branded PDF for sharing",
|
|
439
|
+
"- Inline short content directly in messages",
|
|
440
|
+
"- Never attach raw .md or .yaml files",
|
|
441
|
+
].join("\n"),
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
return parts.join("\n\n");
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Computed lazily (and cached) rather than at module load: the autonomy policy
|
|
448
|
+
// and config/agent.json are read from disk, and a test that points AGENT_DIR at
|
|
449
|
+
// a fixture must not be beaten to it by import-time evaluation.
|
|
450
|
+
let _fallbackPreamble = null;
|
|
451
|
+
function fallbackPreamble() {
|
|
452
|
+
if (_fallbackPreamble === null) _fallbackPreamble = buildFallbackPreamble();
|
|
453
|
+
return _fallbackPreamble;
|
|
200
454
|
}
|
|
201
455
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
function buildFallbackPreamble() {
|
|
206
|
-
const a = loadAgent();
|
|
207
|
-
const principal = a.principal || {};
|
|
208
|
-
const principalName = principal.fullName || "the principal";
|
|
209
|
-
const principalTitle = principal.title || "principal";
|
|
210
|
-
// "<title> to <principal>" only reads correctly for assistant-shaped titles;
|
|
211
|
-
// for a functional seat ("SVP AI Systems & Agent Platform") it turns the role
|
|
212
|
-
// into a service relationship. State the seat, then the reporting line.
|
|
213
|
-
return `You are ${a.fullName}, ${a.title} at ${a.company}. You report to ${principalName}, ${principalTitle}.
|
|
214
|
-
You operate as the autonomous executive command layer for the company.
|
|
215
|
-
|
|
216
|
-
${a.companyDescription || a.company}.
|
|
217
|
-
|
|
218
|
-
Operating Principles:
|
|
219
|
-
1. Follow-through over brilliance — track every commitment until it closes
|
|
220
|
-
2. Concise over comprehensive — sharp recommendations, not exhaustive reports
|
|
221
|
-
3. Evidence over opinion — cite sources
|
|
222
|
-
4. Bias to action — act decisively on operational matters; escalate only strategic commitments
|
|
223
|
-
5. Audit everything — every action is logged
|
|
224
|
-
6. Full autonomy, full accountability
|
|
225
|
-
|
|
226
|
-
Autonomy Model:
|
|
227
|
-
- ${a.firstName} sends autonomously: all internal messages, external operational messages, candidate comms, follow-ups, calendar coordination
|
|
228
|
-
- ${a.firstName} escalates only: binding legal/financial obligations, regulatory submissions, acquisition deal terms, public statements
|
|
229
|
-
|
|
230
|
-
Document Sharing (CRITICAL):
|
|
231
|
-
- NEVER reference local file paths in outbound communications
|
|
232
|
-
- Upload to Google Drive or generate branded PDF for sharing
|
|
233
|
-
- Inline short content directly in messages
|
|
234
|
-
- Never attach raw .md or .yaml files`;
|
|
456
|
+
/** For tests: drop the cached fallback preamble. */
|
|
457
|
+
export function _resetFallbackPreamble() {
|
|
458
|
+
_fallbackPreamble = null;
|
|
235
459
|
}
|
|
236
|
-
const FALLBACK_PREAMBLE = buildFallbackPreamble();
|
|
237
460
|
|
|
238
461
|
/**
|
|
239
462
|
* Proactive self-learning habit (WS3). Injected near the top of every reactive /
|
|
@@ -531,7 +754,7 @@ function buildBacklogContext(queueItem) {
|
|
|
531
754
|
lines.push(`Title: ${queueItem.title || "untitled"}`);
|
|
532
755
|
lines.push(`Status: ${queueItem.status || "open"}`);
|
|
533
756
|
lines.push(`Priority: ${queueItem.priority || "normal"}`);
|
|
534
|
-
lines.push(`Owner: ${queueItem.owner || loadAgent().firstName.toLowerCase()}`);
|
|
757
|
+
lines.push(`Owner: ${queueItem.owner || configuredStr(loadAgent().firstName).toLowerCase() || "agent"}`);
|
|
535
758
|
if (queueItem.source) lines.push(`Source: ${queueItem.source}`);
|
|
536
759
|
if (queueItem.source_ref) lines.push(`Source ref: ${queueItem.source_ref}`);
|
|
537
760
|
// ── what to act ON, and with WHAT ────────────────────────────────────────
|
|
@@ -215,18 +215,18 @@ test("buildPrompt caps the number of injected org facts (bounded)", async () =>
|
|
|
215
215
|
test("buildPrompt states the acknowledgement was sent when it actually was", async () => {
|
|
216
216
|
const prompt = await buildPrompt(ITEM, CLASS, {
|
|
217
217
|
type: "inbox",
|
|
218
|
-
holdingMessage: "Understood —
|
|
218
|
+
holdingMessage: "Understood — I'm digging into this now (the noted issues). I want to get this right, so I'll come back to you here with a full answer — usually within 10-20 minutes — and you'll hear from me either way.",
|
|
219
219
|
holdingSent: true,
|
|
220
220
|
});
|
|
221
221
|
assert.match(prompt, /A HOLDING MESSAGE has ALREADY been sent/);
|
|
222
|
-
assert.ok(prompt.includes("Understood —
|
|
222
|
+
assert.ok(prompt.includes("Understood — I'm digging into this now (the noted issues). I want to get this right, so I'll come back to you here with a full answer — usually within 10-20 minutes — and you'll hear from me either way."));
|
|
223
223
|
assert.ok(!/DELIVERY FAILED/.test(prompt), "no failure framing on a delivered ack");
|
|
224
224
|
});
|
|
225
225
|
|
|
226
226
|
test("buildPrompt does NOT claim delivery when the acknowledgement failed to send", async () => {
|
|
227
227
|
const prompt = await buildPrompt(ITEM, CLASS, {
|
|
228
228
|
type: "inbox",
|
|
229
|
-
holdingMessage: "Understood —
|
|
229
|
+
holdingMessage: "Understood — I'm digging into this now (the noted issues). I want to get this right, so I'll come back to you here with a full answer — usually within 10-20 minutes — and you'll hear from me either way.",
|
|
230
230
|
holdingSent: false,
|
|
231
231
|
});
|
|
232
232
|
// The lie: telling the session a human already heard from us when they did not.
|
|
@@ -242,13 +242,13 @@ test("buildPrompt does NOT claim delivery when the acknowledgement failed to sen
|
|
|
242
242
|
assert.match(prompt, /DELIVERY FAILED/);
|
|
243
243
|
assert.match(prompt, /received NOTHING/);
|
|
244
244
|
// The composed text is still shown — it is what the sender would have seen.
|
|
245
|
-
assert.ok(prompt.includes("Understood —
|
|
245
|
+
assert.ok(prompt.includes("Understood — I'm digging into this now (the noted issues). I want to get this right, so I'll come back to you here with a full answer — usually within 10-20 minutes — and you'll hear from me either way."));
|
|
246
246
|
});
|
|
247
247
|
|
|
248
248
|
test("buildPrompt treats an unspecified holdingSent as delivered (back-compat)", async () => {
|
|
249
249
|
const prompt = await buildPrompt(ITEM, CLASS, {
|
|
250
250
|
type: "inbox",
|
|
251
|
-
holdingMessage: "Understood —
|
|
251
|
+
holdingMessage: "Understood — I'm digging into this now (the noted issues). I want to get this right, so I'll come back to you here with a full answer — usually within 10-20 minutes — and you'll hear from me either way.",
|
|
252
252
|
});
|
|
253
253
|
assert.match(prompt, /A HOLDING MESSAGE has ALREADY been sent/);
|
|
254
254
|
});
|
|
@@ -901,6 +901,15 @@ export async function sendQuickResponse(item, classResult, routed = null) {
|
|
|
901
901
|
via: delivered.via,
|
|
902
902
|
...(delivered.channel ? { channel: delivered.channel } : {}),
|
|
903
903
|
...(delivered.draft_path ? { draft_path: delivered.draft_path } : {}),
|
|
904
|
+
// PROPAGATE THE PERMANENCE. `deliver` marks a failure the send-gate refused
|
|
905
|
+
// (FORBIDDEN_SCOPE) or hq declared unroutable (NOT_FOUND/BAD_REQUEST) as
|
|
906
|
+
// `permanent` with the RPC `code`. Without carrying these up, the caller
|
|
907
|
+
// (agent-daemon.answerItem) could not tell a transient blip from an
|
|
908
|
+
// impossible send, so it fell through to a full autonomous session — which
|
|
909
|
+
// then posted UNRELATED work into a channel this seat was just told it may
|
|
910
|
+
// not write to. The daemon uses `permanent` to escalate instead of spawn.
|
|
911
|
+
...(delivered.permanent ? { permanent: true } : {}),
|
|
912
|
+
...(delivered.code ? { code: delivered.code } : {}),
|
|
904
913
|
...(delivered.error ? { error: delivered.error } : {}),
|
|
905
914
|
};
|
|
906
915
|
|