@cohortapp/agent-sdk 2.3.2 → 2.4.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.
Files changed (148) hide show
  1. package/framework-features.json +30 -0
  2. package/lib/backlog.mjs +136 -0
  3. package/lib/cadences.mjs +63 -2
  4. package/lib/cadences.test.mjs +105 -0
  5. package/lib/capability/inventory.mjs +542 -0
  6. package/lib/capability/inventory.test.mjs +232 -0
  7. package/lib/capability/probe.mjs +255 -0
  8. package/lib/channels/contract.mjs +37 -1
  9. package/lib/channels/contract.test.mjs +25 -1
  10. package/lib/claude-bin.mjs +37 -3
  11. package/lib/claude-bin.test.mjs +42 -8
  12. package/lib/execution/disposition.mjs +501 -0
  13. package/lib/execution/disposition.test.mjs +482 -0
  14. package/lib/execution/drive.mjs +352 -0
  15. package/lib/execution/drive.test.mjs +270 -0
  16. package/lib/execution/effects.mjs +340 -0
  17. package/lib/execution/effects.test.mjs +193 -0
  18. package/lib/execution/index.mjs +152 -0
  19. package/lib/execution/intake.mjs +581 -0
  20. package/lib/execution/intake.test.mjs +343 -0
  21. package/lib/execution/journal.mjs +374 -0
  22. package/lib/execution/journal.test.mjs +261 -0
  23. package/lib/execution/match.mjs +331 -0
  24. package/lib/execution/match.test.mjs +235 -0
  25. package/lib/execution/pipeline.mjs +341 -0
  26. package/lib/execution/pipeline.test.mjs +389 -0
  27. package/lib/execution/route.mjs +332 -0
  28. package/lib/execution/route.test.mjs +186 -0
  29. package/lib/execution/surface-policy.mjs +446 -0
  30. package/lib/execution/surface-policy.test.mjs +162 -0
  31. package/lib/goals/admission.mjs +209 -0
  32. package/lib/goals/admission.test.mjs +139 -0
  33. package/lib/goals/classify.mjs +206 -0
  34. package/lib/goals/classify.test.mjs +109 -0
  35. package/lib/goals/collaborate.mjs +415 -0
  36. package/lib/goals/collaborate.test.mjs +324 -0
  37. package/lib/goals/gaps.mjs +111 -0
  38. package/lib/goals/gaps.test.mjs +284 -0
  39. package/lib/goals/loop.mjs +537 -0
  40. package/lib/goals/loop.test.mjs +719 -0
  41. package/lib/identity/persona.mjs +247 -0
  42. package/lib/identity/persona.test.mjs +117 -0
  43. package/lib/kpi.mjs +469 -0
  44. package/lib/kpi.test.mjs +244 -0
  45. package/lib/mandate/audit.mjs +168 -0
  46. package/lib/mandate/audit.test.mjs +195 -0
  47. package/lib/mandate/cache.mjs +162 -0
  48. package/lib/mandate/derive.mjs +317 -0
  49. package/lib/mandate/derive.test.mjs +224 -0
  50. package/lib/mandate/model.mjs +352 -0
  51. package/lib/mandate/model.test.mjs +145 -0
  52. package/lib/mandate/refresh.mjs +187 -0
  53. package/lib/mandate/refresh.test.mjs +293 -0
  54. package/lib/mcp/server.test.mjs +4 -4
  55. package/lib/org/approvals.mjs +14 -2
  56. package/lib/org/client.mjs +58 -22
  57. package/lib/org/client.test.mjs +3 -1
  58. package/lib/org/inbound/directedness.mjs +720 -0
  59. package/lib/org/inbound/directedness.test.mjs +543 -0
  60. package/lib/org/inbound/facts.mjs +501 -0
  61. package/lib/org/inbound/facts.test.mjs +375 -0
  62. package/lib/org/inbound/hydrate.mjs +535 -0
  63. package/lib/org/inbound/hydrate.test.mjs +326 -0
  64. package/lib/org/inbound/index.mjs +233 -0
  65. package/lib/org/inbound/index.test.mjs +324 -0
  66. package/lib/org/inbound/io.mjs +141 -0
  67. package/lib/org/inbound/project.mjs +201 -0
  68. package/lib/org/inbound/project.test.mjs +287 -0
  69. package/lib/org/inbound/surfaces.mjs +257 -0
  70. package/lib/org/knowledge.mjs +10 -1
  71. package/lib/org/knowledge.test.mjs +8 -1
  72. package/lib/org/leases.mjs +5 -0
  73. package/lib/org/mesh.mjs +17 -2
  74. package/lib/org/messaging.mjs +40 -4
  75. package/lib/org/messaging.test.mjs +40 -0
  76. package/lib/org/param-contract.mjs +694 -0
  77. package/lib/org/param-contract.test.mjs +451 -0
  78. package/lib/org/protocol.checksum +1 -1
  79. package/lib/org/protocol.mjs +8 -0
  80. package/lib/org/protocol.test.mjs +5 -1
  81. package/lib/org/push.mjs +1025 -0
  82. package/lib/org/push.test.mjs +690 -0
  83. package/lib/org/tool-surface.mjs +138 -38
  84. package/lib/org/tool-surface.test.mjs +13 -8
  85. package/lib/org/typing.mjs +341 -0
  86. package/lib/org/typing.test.mjs +291 -0
  87. package/lib/plan/compile.mjs +510 -0
  88. package/lib/plan/compile.test.mjs +286 -0
  89. package/lib/plan/emit.mjs +256 -0
  90. package/lib/plan/emit.test.mjs +246 -0
  91. package/lib/plan/explain.mjs +226 -0
  92. package/lib/plan/explain.test.mjs +188 -0
  93. package/lib/plan/schema.mjs +140 -0
  94. package/lib/resource-governor.mjs +47 -1
  95. package/lib/resource-governor.test.mjs +21 -1
  96. package/lib/setup/enroll-from-cohort.mjs +105 -17
  97. package/lib/setup/enroll-from-cohort.test.mjs +68 -1
  98. package/lib/setup/sections/identity.mjs +15 -4
  99. package/lib/setup/sections/identity.test.mjs +94 -0
  100. package/lib/setup/sections/inventory.mjs +178 -0
  101. package/lib/setup/sections/inventory.test.mjs +198 -0
  102. package/lib/setup/sections/mandate.mjs +392 -0
  103. package/lib/setup/sections/mandate.test.mjs +373 -0
  104. package/lib/setup/sections/subagents.mjs +427 -0
  105. package/lib/setup/sections/subagents.test.mjs +429 -0
  106. package/lib/setup/sections/verify.mjs +121 -0
  107. package/lib/setup/sections/verify.test.mjs +175 -0
  108. package/lib/setup/sot.mjs +2 -0
  109. package/lib/subagents/cli.mjs +463 -0
  110. package/lib/subagents/cli.test.mjs +389 -0
  111. package/lib/subagents/client.mjs +373 -0
  112. package/lib/subagents/client.test.mjs +309 -0
  113. package/lib/subagents/gap.mjs +268 -0
  114. package/lib/subagents/gap.test.mjs +234 -0
  115. package/lib/subagents/lock.mjs +296 -0
  116. package/lib/subagents/lock.test.mjs +248 -0
  117. package/lib/subagents/manifest.mjs +224 -0
  118. package/lib/subagents/manifest.test.mjs +175 -0
  119. package/lib/subagents/refs.mjs +274 -0
  120. package/lib/subagents/refs.test.mjs +204 -0
  121. package/lib/subagents/resolve.mjs +455 -0
  122. package/lib/subagents/resolve.test.mjs +422 -0
  123. package/lib/subagents/schema.mjs +467 -0
  124. package/lib/subagents/schema.test.mjs +306 -0
  125. package/package.json +8 -3
  126. package/plugins/maestro-skills/.claude-plugin/marketplace.json +16 -0
  127. package/policies/ai-disclosure.yaml +42 -2
  128. package/scaffold/CLAUDE.md +16 -2
  129. package/schedules/triggers/goal-steward.md +79 -0
  130. package/scripts/ci/conformance-org-api.mjs +792 -0
  131. package/scripts/ci/conformance-org-api.test.mjs +417 -0
  132. package/scripts/daemon/agent-daemon.mjs +36 -4
  133. package/scripts/daemon/cadence-handlers.mjs +145 -1
  134. package/scripts/daemon/goal-steward-cadence.test.mjs +243 -0
  135. package/scripts/daemon/inbox-deferral.mjs +45 -2
  136. package/scripts/daemon/inbox-deferral.test.mjs +56 -0
  137. package/scripts/daemon/inbox-wake.mjs +282 -0
  138. package/scripts/daemon/inbox-wake.test.mjs +199 -0
  139. package/scripts/daemon/prompt-builder.mjs +41 -1
  140. package/scripts/daemon/typing-registry.mjs +55 -2
  141. package/scripts/daemon/typing-registry.test.mjs +25 -0
  142. package/scripts/local-triggers/generate-plists.test.mjs +5 -5
  143. package/scripts/setup/gen-subagent-manifest.mjs +95 -0
  144. package/scripts/setup/gen-subagent-manifest.test.mjs +124 -0
  145. package/scripts/setup/generate-plan.mjs +108 -0
  146. package/scripts/setup/init-capability-manifest.mjs +70 -0
  147. package/scripts/setup/init-skill-marketplace.mjs +155 -0
  148. package/scripts/setup/init-skill-marketplace.test.mjs +193 -0
@@ -0,0 +1,306 @@
1
+ /**
2
+ * schema.test.mjs — the frontmatter parser/validator (Sub-Agent Registry §2.1).
3
+ * Run: node --test lib/subagents/schema.test.mjs
4
+ */
5
+ "use strict";
6
+
7
+ import { test } from "node:test";
8
+ import assert from "node:assert/strict";
9
+ import { readFileSync } from "node:fs";
10
+ import { dirname, join } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+
13
+ import {
14
+ AUTHORED_KEYS,
15
+ MODEL_ALIASES,
16
+ PROVENANCE_KEYS,
17
+ bodyTokens,
18
+ checkAgentFile,
19
+ hashBody,
20
+ hashFile,
21
+ normaliseModel,
22
+ parseAgentMd,
23
+ parseFrontmatter,
24
+ splitFrontmatter,
25
+ stampProvenance,
26
+ stringifyAgentMd,
27
+ stringifyFrontmatter,
28
+ stripProvenance,
29
+ unsupportedTokens,
30
+ validateAgentMd,
31
+ } from "./schema.mjs";
32
+ import { listAgentDirs } from "./manifest.mjs";
33
+
34
+ const MAESTRO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
35
+
36
+ const GOOD = [
37
+ "---",
38
+ "name: alpha",
39
+ "description: Handles alpha work.",
40
+ "model: claude-sonnet-4-6",
41
+ 'tools: ["Read", "Write"]',
42
+ "---",
43
+ "# alpha",
44
+ "",
45
+ "Serve {{agent.firstName}}.",
46
+ "",
47
+ ].join("\n");
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // splitFrontmatter
51
+ // ---------------------------------------------------------------------------
52
+
53
+ test("splitFrontmatter: splits fences and keeps the body verbatim", () => {
54
+ const s = splitFrontmatter(GOOD);
55
+ assert.equal(s.ok, true);
56
+ assert.match(s.frontmatterText, /^name: alpha\n/);
57
+ assert.match(s.body, /^# alpha\n/);
58
+ });
59
+
60
+ test("splitFrontmatter: a prose-only file is an ERROR, not empty frontmatter", () => {
61
+ // The bug this closes: an agents/<slug>/ with no real frontmatter used to pass
62
+ // doctor because doctor only checked that the directory existed.
63
+ const s = splitFrontmatter("# alpha\n\njust prose\n");
64
+ assert.equal(s.ok, false);
65
+ assert.match(s.error, /missing opening/);
66
+ });
67
+
68
+ test("splitFrontmatter: unterminated frontmatter is an error", () => {
69
+ const s = splitFrontmatter("---\nname: alpha\n# no closing fence\n");
70
+ assert.equal(s.ok, false);
71
+ assert.match(s.error, /unterminated/);
72
+ });
73
+
74
+ test("splitFrontmatter: tolerates a BOM and CRLF line endings", () => {
75
+ const crlf = "" + GOOD.replace(/\n/g, "\r\n");
76
+ const s = splitFrontmatter(crlf);
77
+ assert.equal(s.ok, true);
78
+ assert.equal(s.body.includes("\r"), false);
79
+ });
80
+
81
+ test("splitFrontmatter: empty frontmatter block parses to an empty map", () => {
82
+ const s = splitFrontmatter("---\n---\nbody\n");
83
+ assert.equal(s.ok, true);
84
+ assert.equal(s.frontmatterText, "");
85
+ assert.equal(s.body, "body\n");
86
+ });
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // parseFrontmatter
90
+ // ---------------------------------------------------------------------------
91
+
92
+ test("parseFrontmatter: scalars, flow sequences and block sequences", () => {
93
+ const r = parseFrontmatter([
94
+ "name: alpha",
95
+ 'description: "Handles: alpha work"',
96
+ "model: sonnet # trailing comment",
97
+ 'tools: [Read, "Write", \'Edit\']',
98
+ "tokens:",
99
+ " - agent.firstName",
100
+ " - agent.company",
101
+ ].join("\n"));
102
+ assert.equal(r.ok, true, r.errors.join("; "));
103
+ assert.equal(r.value.name, "alpha");
104
+ assert.equal(r.value.description, "Handles: alpha work");
105
+ assert.equal(r.value.model, "sonnet");
106
+ assert.deepEqual(r.value.tools, ["Read", "Write", "Edit"]);
107
+ assert.deepEqual(r.value.tokens, ["agent.firstName", "agent.company"]);
108
+ });
109
+
110
+ test("parseFrontmatter: a `#` inside a quoted scalar is content, not a comment", () => {
111
+ const r = parseFrontmatter('description: "ships #1 priority"');
112
+ assert.equal(r.value.description, "ships #1 priority");
113
+ });
114
+
115
+ test("parseFrontmatter: a comma inside a quoted flow item is not a separator", () => {
116
+ // The registry round-trips these bytes into a database — "unlikely" is not a
117
+ // licence to corrupt.
118
+ const r = parseFrontmatter('tools: ["Read, maybe", Write]');
119
+ assert.deepEqual(r.value.tools, ["Read, maybe", "Write"]);
120
+ });
121
+
122
+ test("parseFrontmatter: anything outside the closed grammar is an ERROR, never a silent mis-parse", () => {
123
+ const r = parseFrontmatter("name: alpha\nnested:\n key: value\n");
124
+ assert.equal(r.ok, false);
125
+ assert.match(r.errors.join(" "), /nested maps are outside the frontmatter grammar/);
126
+ });
127
+
128
+ test("parseFrontmatter: an orphan list item is an error", () => {
129
+ const r = parseFrontmatter("name: alpha\n - stray\n");
130
+ assert.equal(r.ok, false);
131
+ assert.match(r.errors.join(" "), /no owning key/);
132
+ });
133
+
134
+ test("parseFrontmatter: an unterminated flow sequence is an error", () => {
135
+ const r = parseFrontmatter("tools: [Read, Write\n");
136
+ assert.equal(r.ok, false);
137
+ assert.match(r.errors.join(" "), /unterminated flow sequence/);
138
+ });
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // model normalisation — where the shipped-vs-generated drift dies
142
+ // ---------------------------------------------------------------------------
143
+
144
+ test("normaliseModel: aliases and full ids both normalise, idempotently", () => {
145
+ assert.equal(normaliseModel("sonnet"), "claude-sonnet-4-6");
146
+ assert.equal(normaliseModel("claude-sonnet-4-6"), "claude-sonnet-4-6");
147
+ assert.equal(normaliseModel(normaliseModel("opus")), "claude-opus-4-8");
148
+ assert.equal(normaliseModel("HAIKU"), "claude-haiku-4-5");
149
+ assert.equal(normaliseModel("inherit"), "inherit");
150
+ });
151
+
152
+ test("normaliseModel: unknown → null (an error for the caller), absent → \"\"", () => {
153
+ assert.equal(normaliseModel("gpt-4o"), null);
154
+ assert.equal(normaliseModel(""), "");
155
+ assert.equal(normaliseModel(undefined), "");
156
+ });
157
+
158
+ test("MODEL_ALIASES: every alias target is itself a key (normalisation is a fixpoint)", () => {
159
+ for (const target of Object.values(MODEL_ALIASES)) {
160
+ assert.equal(MODEL_ALIASES[target], target, `${target} must map to itself`);
161
+ }
162
+ });
163
+
164
+ // ---------------------------------------------------------------------------
165
+ // validate
166
+ // ---------------------------------------------------------------------------
167
+
168
+ function fm(over = {}) {
169
+ return { name: "alpha", description: "d", model: "sonnet", tools: ["Read"], ...over };
170
+ }
171
+
172
+ test("validateAgentMd: the happy path normalises the model and derives tokens", () => {
173
+ const v = validateAgentMd({ frontmatter: fm(), body: "x {{agent.firstName}}", slug: "alpha" });
174
+ assert.equal(v.ok, true, v.errors.join("; "));
175
+ assert.equal(v.normalised.frontmatter.model, "claude-sonnet-4-6");
176
+ assert.deepEqual(v.normalised.frontmatter.tokens, ["agent.firstName"]);
177
+ });
178
+
179
+ test("validateAgentMd: name must equal the directory name (identity IS the dirname)", () => {
180
+ const v = validateAgentMd({ frontmatter: fm({ name: "beta" }), body: "x", slug: "alpha" });
181
+ assert.equal(v.ok, false);
182
+ assert.match(v.errors.join(" "), /must equal the directory name/);
183
+ });
184
+
185
+ test("validateAgentMd: description, model, tools and body are all required", () => {
186
+ const cases = [
187
+ [fm({ description: "" }), "x", /description is required/],
188
+ [fm({ model: "gpt-4o" }), "x", /not a known model/],
189
+ [fm({ model: "" }), "x", /model is required/],
190
+ [fm({ tools: undefined }), "x", /tools is required/],
191
+ [fm({ tools: [] }), "x", /tools must be non-empty/],
192
+ [fm(), " ", /body is empty/],
193
+ ];
194
+ for (const [f, body, re] of cases) {
195
+ const v = validateAgentMd({ frontmatter: f, body, slug: "alpha" });
196
+ assert.equal(v.ok, false, `expected failure for ${re}`);
197
+ assert.match(v.errors.join(" "), re);
198
+ }
199
+ });
200
+
201
+ test("validateAgentMd: an ABSENT tokens key is derived, not an error", () => {
202
+ // Every file that shipped before the registry existed has no `tokens:` key.
203
+ // Demanding one would fail-closed the whole fleet on the first doctor run.
204
+ const v = validateAgentMd({ frontmatter: fm(), body: "{{agent.company}}", slug: "alpha" });
205
+ assert.equal(v.ok, true);
206
+ assert.deepEqual(v.normalised.frontmatter.tokens, ["agent.company"]);
207
+ });
208
+
209
+ test("validateAgentMd: a DECLARED tokens list must cover every token the body uses", () => {
210
+ const v = validateAgentMd({ frontmatter: fm({ tokens: [] }), body: "{{agent.company}}", slug: "alpha" });
211
+ assert.equal(v.ok, false);
212
+ assert.match(v.errors.join(" "), /undeclared token/);
213
+ });
214
+
215
+ test("validateAgentMd: declared-but-unused and renderer-unknown tokens WARN, never fail", () => {
216
+ const v = validateAgentMd({
217
+ frontmatter: fm({ tokens: ["agent.firstName", "agent.nope"] }),
218
+ body: "{{agent.firstName}}",
219
+ slug: "alpha",
220
+ });
221
+ assert.equal(v.ok, true, v.errors.join("; "));
222
+ assert.match(v.warnings.join(" "), /declared but unused/);
223
+ assert.match(v.warnings.join(" "), /unknown to this agent's renderer/);
224
+ });
225
+
226
+ test("unsupportedTokens: honours an injected KNOWN_TOKENS set", () => {
227
+ assert.deepEqual(unsupportedTokens(["a", "b"], new Set(["a"])), ["b"]);
228
+ assert.deepEqual(unsupportedTokens(null, new Set()), []);
229
+ });
230
+
231
+ test("bodyTokens: deduped and sorted", () => {
232
+ assert.deepEqual(bodyTokens("{{agent.z}} {{agent.a}} {{agent.z}}"), ["agent.a", "agent.z"]);
233
+ });
234
+
235
+ // ---------------------------------------------------------------------------
236
+ // emit / round-trip — what makes local-edit detection trustworthy
237
+ // ---------------------------------------------------------------------------
238
+
239
+ test("stringifyFrontmatter: canonical key order (authored, then provenance, then rest)", () => {
240
+ const text = stringifyFrontmatter({ contentHash: "h", extra: 1, tools: ["Read"], name: "alpha" });
241
+ assert.deepEqual(text.trim().split("\n"), ['name: alpha', 'tools: ["Read"]', "contentHash: h", "extra: 1"]);
242
+ });
243
+
244
+ test("round trip: parse(stringify(x)) === x, and a second pass is byte-identical", () => {
245
+ const f = { name: "alpha", description: "d", model: "claude-sonnet-4-6", tools: ["Read", "Write"], tokens: [] };
246
+ const once = stringifyAgentMd(f, "# alpha\n\nbody\n");
247
+ const parsed = parseAgentMd(once);
248
+ assert.equal(parsed.ok, true, parsed.errors.join("; "));
249
+ assert.deepEqual(parsed.frontmatter, f);
250
+ assert.equal(stringifyAgentMd(parsed.frontmatter, parsed.body), once);
251
+ });
252
+
253
+ test("stampProvenance: is IDEMPOTENT — re-stamping produces identical bytes", () => {
254
+ // Non-idempotent stamping would change the file hash on every materialisation
255
+ // and make local-edit detection permanently wrong.
256
+ const one = stampProvenance({
257
+ frontmatter: fm(), body: "b\n", layer: "workspace", source: "cohort://o/alpha", version: 7,
258
+ });
259
+ const re = parseAgentMd(one);
260
+ const two = stampProvenance({
261
+ frontmatter: re.frontmatter, body: re.body, layer: "workspace", source: "cohort://o/alpha", version: 7,
262
+ });
263
+ assert.equal(two, one);
264
+ assert.equal(hashFile(two), hashFile(one));
265
+ });
266
+
267
+ test("stampProvenance: never renders tokens — {{agent.*}} survives to disk", () => {
268
+ const text = stampProvenance({ frontmatter: fm(), body: "hi {{agent.firstName}}\n", layer: "sdk" });
269
+ assert.match(text, /\{\{agent\.firstName\}\}/);
270
+ });
271
+
272
+ test("stripProvenance: removes exactly the registry-owned keys", () => {
273
+ const out = stripProvenance({ name: "alpha", layer: "sdk", source: "s", version: "1", contentHash: "h" });
274
+ assert.deepEqual(Object.keys(out), ["name"]);
275
+ for (const k of PROVENANCE_KEYS) assert.equal(k in out, false);
276
+ assert.ok(AUTHORED_KEYS.includes("name"));
277
+ });
278
+
279
+ test("hashBody and hashFile have different preimages (body vs whole file)", () => {
280
+ const parsed = parseAgentMd(GOOD);
281
+ assert.notEqual(hashBody(parsed.body), hashFile(GOOD));
282
+ assert.equal(hashBody(parsed.body), hashBody(parsed.body));
283
+ assert.equal(hashBody(null), hashBody(""));
284
+ });
285
+
286
+ // ---------------------------------------------------------------------------
287
+ // the real corpus — the validator must not reject what already shipped
288
+ // ---------------------------------------------------------------------------
289
+
290
+ test("every shipped agents/*/agent.md validates (the fleet must not fail-closed)", () => {
291
+ const { slugs } = listAgentDirs(MAESTRO_ROOT);
292
+ assert.ok(slugs.length >= 1, "expected the SDK to ship at least one agent");
293
+ const bad = [];
294
+ for (const slug of slugs) {
295
+ const text = readFileSync(join(MAESTRO_ROOT, "agents", slug, "agent.md"), "utf8");
296
+ const c = checkAgentFile(text, slug);
297
+ if (!c.ok) bad.push(`${slug}: ${c.errors.join(", ")}`);
298
+ }
299
+ assert.deepEqual(bad, []);
300
+ });
301
+
302
+ test("checkAgentFile: reports the parse error rather than throwing", () => {
303
+ const c = checkAgentFile("no frontmatter here", "alpha");
304
+ assert.equal(c.ok, false);
305
+ assert.match(c.errors.join(" "), /missing opening/);
306
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cohortapp/agent-sdk",
3
- "version": "2.3.2",
3
+ "version": "2.4.1",
4
4
  "description": "Cohort Agent SDK \u2014 autonomous AI colleague runtime. Deploy senior AI colleagues on dedicated Mac minis, wired to the Cohort operating surface.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,6 +36,9 @@
36
36
  "./org/approvals": "./lib/org/approvals.mjs",
37
37
  "./org/mesh": "./lib/org/mesh.mjs",
38
38
  "./org/cohort-client": "./lib/org/cohort-client.mjs",
39
+ "./org/inbound": "./lib/org/inbound/index.mjs",
40
+ "./org/inbound/surfaces": "./lib/org/inbound/surfaces.mjs",
41
+ "./org/inbound/directedness": "./lib/org/inbound/directedness.mjs",
39
42
  "./secrets/broker": "./lib/secrets/broker.mjs",
40
43
  "./secrets/providers": "./lib/secrets/providers.mjs",
41
44
  "./diagnostics/trace": "./lib/diagnostics/trace.mjs",
@@ -118,9 +121,11 @@
118
121
  "scripts": {
119
122
  "test": "node scripts/ci/run-tests.mjs",
120
123
  "test:coverage": "node scripts/ci/run-coverage.mjs",
121
- "test:list": "node --test bin/maestro.test.mjs lib/action-executor.test.mjs lib/archetype.test.mjs lib/autonomy.test.mjs lib/backlog.test.mjs lib/budget-guard.test.mjs lib/cadence-bus-schedule.test.mjs lib/cadence-bus.test.mjs lib/cadences.test.mjs lib/capability.test.mjs lib/channels/base-adapter.test.mjs lib/channels/channels.test.mjs lib/channels/contract.test.mjs lib/channels/inbox-item.test.mjs lib/channels/pairing.test.mjs lib/channels/slack-adapter.test.mjs lib/channels/telegram-adapter.test.mjs lib/channels/whatsapp/baileys-typing.test.mjs lib/charter.test.mjs lib/claude-bin.test.mjs lib/collective/capture.test.mjs lib/collective/cards.test.mjs lib/collective/config.test.mjs lib/collective/global-config.test.mjs lib/collective/presence.test.mjs lib/collective/recall.test.mjs lib/comms.test.mjs lib/fs-atomic.test.mjs lib/fs-ownership.test.mjs lib/learning/config.test.mjs lib/learning/counters.test.mjs lib/learning/curator-consolidate.test.mjs lib/learning/curator.test.mjs lib/learning/reflect.test.mjs lib/learning/session-index.test.mjs lib/learning/skill-writer.test.mjs lib/model-router.test.mjs lib/model-router/catalog.test.mjs lib/model-router/economics.test.mjs lib/model-router/failover.test.mjs lib/model-router/health.test.mjs lib/model-router/integration.test.mjs lib/model-router/integration-coverage.test.mjs lib/model-router/ledger.test.mjs lib/model-router/llm-task.test.mjs lib/model-router/org-credentials.test.mjs lib/model-router/reconcile.test.mjs lib/model-router/repair.test.mjs lib/model-router/spawn.test.mjs lib/model-router/taxonomy.test.mjs lib/org/approvals.test.mjs lib/org/board.test.mjs lib/org/client.test.mjs lib/org/cost-sync.test.mjs lib/org/handoff.test.mjs lib/org/keys.test.mjs lib/org/knowledge.test.mjs lib/org/activity.test.mjs lib/org/leases.test.mjs lib/org/mesh.test.mjs lib/org/messaging.test.mjs lib/org/cohort-client.test.mjs lib/org/policy.test.mjs lib/org/registry.test.mjs lib/org/tool-surface.test.mjs lib/org/ui-parity.test.mjs lib/org/doctor.test.mjs lib/org/verify.test.mjs lib/mcp/server.test.mjs lib/channels/orgmail/adapter.test.mjs lib/rag/rag.test.mjs lib/rate-guard.test.mjs lib/render.test.mjs lib/resource-governor.test.mjs lib/security/coerce-args.test.mjs lib/security/dangerous-tools.test.mjs lib/security/external-content.test.mjs lib/security/redact.test.mjs lib/security/secret-equal.test.mjs lib/session-permissions.test.mjs lib/setup/completeness.test.mjs lib/setup/context-pack.test.mjs lib/setup/enrich.test.mjs lib/setup/enroll-from-cohort.test.mjs lib/setup/integration.test.mjs lib/setup/io.test.mjs lib/setup/runner.test.mjs lib/setup/sections/identity.test.mjs lib/setup/sections/learning.test.mjs lib/setup/sections/messaging.test.mjs lib/setup/sections/org.test.mjs lib/setup/sections/orgmail.test.mjs lib/setup/sections/recovery.test.mjs lib/setup/sot.test.mjs lib/setup/state.test.mjs lib/singleton.test.mjs lib/telemetry/alerts.test.mjs lib/telemetry/collect.test.mjs lib/tool-definitions.test.mjs lib/voice/outbound.test.mjs lib/voice/stt.test.mjs lib/voice/voice.test.mjs scripts/cadence/enqueue-cadence-tick.test.mjs scripts/ci/check-no-build-artifacts.test.mjs scripts/ci/check-no-residual-identity.test.mjs scripts/ci/check.test.mjs scripts/collective/hook-runner.test.mjs scripts/cost/fleet-digest.test.mjs scripts/daemon/cadence-consumer-governance.test.mjs scripts/daemon/cadence-consumer.test.mjs scripts/daemon/cadence-handlers.test.mjs scripts/daemon/classify-kind.test.mjs scripts/daemon/dispatcher-cooldown.test.mjs scripts/daemon/dispatcher-governance.test.mjs scripts/daemon/inbox-deferral.test.mjs scripts/daemon/lib/session-router.test.mjs scripts/daemon/prompt-builder.test.mjs scripts/daemon/typing-registry.test.mjs scripts/hooks/pre-send-audit.test.mjs scripts/local-triggers/generate-plists.test.mjs scripts/poller/inbox-scan-poller.test.mjs scripts/poller/lib/cloud-relay-dedup.test.mjs scripts/poller/slack-socket-mode.test.mjs scripts/setup/init-agent.test.mjs services/webhook-relay/server.test.mjs",
122
- "test:setup": "node --test lib/setup/io.test.mjs lib/setup/state.test.mjs lib/setup/sot.test.mjs lib/setup/completeness.test.mjs lib/setup/context-pack.test.mjs lib/setup/enrich.test.mjs lib/setup/runner.test.mjs lib/setup/integration.test.mjs",
124
+ "test:list": "node --test bin/maestro.test.mjs lib/action-executor.test.mjs lib/archetype.test.mjs lib/autonomy.test.mjs lib/backlog.test.mjs lib/budget-guard.test.mjs lib/cadence-bus-schedule.test.mjs lib/cadence-bus.test.mjs lib/cadences.test.mjs lib/capability.test.mjs lib/channels/base-adapter.test.mjs lib/channels/channels.test.mjs lib/channels/contract.test.mjs lib/channels/inbox-item.test.mjs lib/channels/pairing.test.mjs lib/channels/slack-adapter.test.mjs lib/channels/telegram-adapter.test.mjs lib/channels/whatsapp/baileys-typing.test.mjs lib/charter.test.mjs lib/claude-bin.test.mjs lib/collective/capture.test.mjs lib/collective/cards.test.mjs lib/collective/config.test.mjs lib/collective/global-config.test.mjs lib/collective/presence.test.mjs lib/collective/recall.test.mjs lib/comms.test.mjs lib/fs-atomic.test.mjs lib/fs-ownership.test.mjs lib/learning/config.test.mjs lib/learning/counters.test.mjs lib/learning/curator-consolidate.test.mjs lib/learning/curator.test.mjs lib/learning/reflect.test.mjs lib/learning/session-index.test.mjs lib/learning/skill-writer.test.mjs lib/model-router.test.mjs lib/model-router/catalog.test.mjs lib/model-router/economics.test.mjs lib/model-router/failover.test.mjs lib/model-router/health.test.mjs lib/model-router/integration.test.mjs lib/model-router/integration-coverage.test.mjs lib/model-router/ledger.test.mjs lib/model-router/llm-task.test.mjs lib/model-router/org-credentials.test.mjs lib/model-router/reconcile.test.mjs lib/model-router/repair.test.mjs lib/model-router/spawn.test.mjs lib/model-router/taxonomy.test.mjs lib/org/approvals.test.mjs lib/org/board.test.mjs lib/org/client.test.mjs lib/org/cost-sync.test.mjs lib/org/handoff.test.mjs lib/org/keys.test.mjs lib/org/knowledge.test.mjs lib/org/activity.test.mjs lib/org/leases.test.mjs lib/org/mesh.test.mjs lib/org/messaging.test.mjs lib/org/cohort-client.test.mjs lib/org/policy.test.mjs lib/org/registry.test.mjs lib/org/tool-surface.test.mjs lib/org/ui-parity.test.mjs lib/org/doctor.test.mjs lib/org/verify.test.mjs lib/mcp/server.test.mjs lib/channels/orgmail/adapter.test.mjs lib/rag/rag.test.mjs lib/rate-guard.test.mjs lib/render.test.mjs lib/resource-governor.test.mjs lib/security/coerce-args.test.mjs lib/security/dangerous-tools.test.mjs lib/security/external-content.test.mjs lib/security/redact.test.mjs lib/security/secret-equal.test.mjs lib/session-permissions.test.mjs lib/setup/completeness.test.mjs lib/setup/context-pack.test.mjs lib/setup/enrich.test.mjs lib/setup/enroll-from-cohort.test.mjs lib/setup/integration.test.mjs lib/setup/io.test.mjs lib/setup/runner.test.mjs lib/setup/sections/identity.test.mjs lib/setup/sections/learning.test.mjs lib/setup/sections/messaging.test.mjs lib/setup/sections/org.test.mjs lib/setup/sections/orgmail.test.mjs lib/setup/sections/recovery.test.mjs lib/setup/sections/subagents.test.mjs lib/setup/sot.test.mjs lib/setup/state.test.mjs lib/singleton.test.mjs lib/subagents/schema.test.mjs lib/subagents/refs.test.mjs lib/subagents/manifest.test.mjs lib/subagents/lock.test.mjs lib/subagents/resolve.test.mjs lib/subagents/gap.test.mjs lib/subagents/client.test.mjs lib/subagents/cli.test.mjs lib/telemetry/alerts.test.mjs lib/telemetry/collect.test.mjs lib/tool-definitions.test.mjs lib/voice/outbound.test.mjs lib/voice/stt.test.mjs lib/voice/voice.test.mjs scripts/cadence/enqueue-cadence-tick.test.mjs scripts/ci/check-no-build-artifacts.test.mjs scripts/ci/check-no-residual-identity.test.mjs scripts/ci/check.test.mjs scripts/collective/hook-runner.test.mjs scripts/cost/fleet-digest.test.mjs scripts/daemon/cadence-consumer-governance.test.mjs scripts/daemon/cadence-consumer.test.mjs scripts/daemon/cadence-handlers.test.mjs scripts/daemon/classify-kind.test.mjs scripts/daemon/dispatcher-cooldown.test.mjs scripts/daemon/dispatcher-governance.test.mjs scripts/daemon/inbox-deferral.test.mjs scripts/daemon/lib/session-router.test.mjs scripts/daemon/prompt-builder.test.mjs scripts/daemon/typing-registry.test.mjs scripts/hooks/pre-send-audit.test.mjs scripts/local-triggers/generate-plists.test.mjs scripts/poller/inbox-scan-poller.test.mjs scripts/poller/lib/cloud-relay-dedup.test.mjs scripts/poller/slack-socket-mode.test.mjs scripts/setup/gen-subagent-manifest.test.mjs scripts/setup/init-agent.test.mjs services/webhook-relay/server.test.mjs",
125
+ "test:setup": "node --test lib/setup/io.test.mjs lib/setup/state.test.mjs lib/setup/sot.test.mjs lib/setup/completeness.test.mjs lib/setup/context-pack.test.mjs lib/setup/enrich.test.mjs lib/setup/runner.test.mjs lib/setup/integration.test.mjs lib/setup/sections/subagents.test.mjs",
123
126
  "check": "node scripts/ci/check.mjs",
127
+ "conformance:org": "node scripts/ci/conformance-org-api.mjs",
128
+ "conformance:org:write": "node scripts/ci/conformance-org-api.mjs --write",
124
129
  "test:router": "node --test lib/model-router.test.mjs lib/model-router/catalog.test.mjs lib/model-router/economics.test.mjs lib/model-router/failover.test.mjs lib/model-router/health.test.mjs lib/model-router/integration.test.mjs lib/model-router/integration-coverage.test.mjs lib/model-router/ledger.test.mjs lib/model-router/llm-task.test.mjs lib/model-router/org-credentials.test.mjs lib/model-router/reconcile.test.mjs lib/model-router/repair.test.mjs lib/model-router/spawn.test.mjs lib/model-router/taxonomy.test.mjs scripts/cost/fleet-digest.test.mjs",
125
130
  "test:voice": "node --test lib/voice/voice.test.mjs lib/voice/outbound.test.mjs",
126
131
  "test:channels": "node --test lib/channels/channels.test.mjs",
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "maestro-skills",
3
+ "owner": {
4
+ "name": "maestro"
5
+ },
6
+ "metadata": {
7
+ "description": "The maestro framework skill pack. Registering this marketplace is what makes the skill files in plugins/maestro-skills/skills/ actually invocable — without it they are inert markdown and rung 1 of the execution ladder (\"a skill exists for this procedure\") is a lie. lib/capability/inventory.mjs probes for this file and reports every skill as reachable:false when it is absent."
8
+ },
9
+ "plugins": [
10
+ {
11
+ "name": "maestro-skills",
12
+ "source": "./",
13
+ "description": "Maestro operational skills — executive briefings, communications, hiring, strategy, org-desk workflows (mail, files, calendar, CRM, books, directory, design) and Cohort call working sessions."
14
+ }
15
+ ]
16
+ }
@@ -154,9 +154,30 @@ channels:
154
154
  footer: >
155
155
  Sent by {agent_name}, an AI assistant working on behalf of
156
156
  {principal_name}.
157
+ # WHEN THE HUMAN-READABLE FOOTER IS APPENDED.
158
+ # Previously unscoped, which meant it rendered on EVERY email — including
159
+ # replies deep in a thread and mail to colleagues inside the deploying
160
+ # organisation who provisioned the agent in the first place. That is not
161
+ # what Art. 50(1) asks for (it addresses informing a person that they are
162
+ # interacting with an AI system, which a continuing correspondent already
163
+ # knows), and the standing repetition reads as the agent disclaiming its own
164
+ # work rather than as a transparency control.
165
+ #
166
+ # The X- header is UNCONDITIONAL and carries the machine-readable duty under
167
+ # Art. 50(2) on every message. Only the human-readable footer is scoped.
168
+ footer_scope:
169
+ first_contact_only: true # not on replies within an established thread
170
+ external_recipients_only: true # not to members of the deploying org
171
+ # A recipient is "internal" when they resolve to a member of this agent's
172
+ # own organisation (Cohort directory / config/contacts.yaml). Anyone else
173
+ # is external and gets the footer on first contact.
174
+ internal_recipient_source: ["cohort_directory", "config/contacts.yaml"]
157
175
  notes: >
158
- The X- header is the machine-readable marking; the footer is the
159
- human-readable one. Keep the footer one line, no emoji.
176
+ The X- header is the machine-readable marking and always ships. The footer
177
+ is the human-readable one and is scoped by `footer_scope` — first contact,
178
+ external recipients. Keep it one line, no emoji. Scoping the footer does not
179
+ touch the truthfulness invariant: a sincere question about the agent's
180
+ nature is answered plainly on any message, first or thousandth.
160
181
 
161
182
  whatsapp:
162
183
  identity_line: >
@@ -268,6 +289,25 @@ jurisdictions:
268
289
  - "Disclose AI use in defined employment and biometric contexts."
269
290
  machine_readable_marking_required: false
270
291
 
292
+ AE:
293
+ law: "UAE / DIFC — no enumerated AI-transparency statute as at 2026-08."
294
+ status: unlegislated
295
+ # DELIBERATELY fail-safe, and NOT a considered position. Until counsel rules,
296
+ # a UAE-seated deployment inherits the strictest posture. Named explicitly
297
+ # rather than left to fall through to `default:` so the choice is visible to
298
+ # whoever reviews this file — a deployment headquartered here was silently
299
+ # being governed by the catch-all and nobody could see it.
300
+ #
301
+ # TO REVIEW (governance.owner_role): whether a UAE-seated agent corresponding
302
+ # with UAE and non-EU counterparties should sit at disclose-on-ask instead.
303
+ # Note this is about the PROACTIVE posture only; the truthfulness invariant is
304
+ # locked and unaffected either way.
305
+ posture: disclose-always
306
+ machine_readable_marking_required: false
307
+ note: >
308
+ Reaching an EU-based recipient pulls the EU entry in regardless of where the
309
+ agent is seated — apply the strictest of the two, per the header above.
310
+
271
311
  # Catch-all for any jurisdiction not enumerated above.
272
312
  default:
273
313
  law: "No enumerated statute; deployment default."
@@ -2,9 +2,9 @@
2
2
 
3
3
  ## Identity
4
4
 
5
- You are a Maestro-powered autonomous AI agent. Your identity has not yet been configured.
5
+ You are `{{agent.fullName}}`, `{{agent.title}}` at `{{agent.company}}`, reporting to `{{agent.principal.fullName}}`, `{{agent.principal.title}}`. Your background and remit are in `{{agent.persona}}`, `{{agent.responsibilities}}` and `{{agent.operatingPrinciples}}`; your register is `{{agent.communication.defaultTone}}` internally and `{{agent.communication.externalTone}}` outside the company. All of these resolve from `config/agent.json`, which the Cohort enrolment pull keeps in sync with your organisation's record of you.
6
6
 
7
- Run the deterministic setup wizard to set up your persona:
7
+ If those tokens are still unresolved, your identity has not been configured yet — run the deterministic setup wizard:
8
8
 
9
9
  ```
10
10
  maestro setup
@@ -12,6 +12,14 @@ maestro setup
12
12
 
13
13
  This configures your name, role, responsibilities, company context, model/provider, channels, the full operating model (charter, backlog, cadences, policies), and self-verifies. It is resumable (checkpointed) and keeps `config/agent.json` as the single source of truth. (`claude "/init-maestro"` remains as a manual LLM-driven fallback.)
14
14
 
15
+ ### How you write and carry yourself
16
+
17
+ 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.
18
+
19
+ Do not introduce, describe or sign yourself as an assistant, a bot, an AI helper, or as "working on behalf of" someone. You report to `{{agent.principal.firstName}}` the way any senior colleague reports to their manager, and you do not preface your work with that fact. 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, and ask for a decision only when the decision is genuinely theirs. Don't thank people for their patience, apologise for taking their time, or hedge a finding you're confident in.
20
+
21
+ Being straight about what you are is not negotiable and doesn't conflict with any of the above: if someone sincerely asks whether they're dealing with an AI, say so plainly and without drama. That's a direct answer to a direct question, not a standing disclaimer you attach to your own work. `policies/ai-disclosure.yaml` is the authority on where proactive disclosure is legally required; it is not a licence to narrate your own nature unprompted.
22
+
15
23
  ## Source of Truth & Identity Tokens
16
24
 
17
25
  Your canonical identity lives in `config/agent.json`. Framework templates (skills under `plugins/maestro-skills/`, workflows under `workflows/`, prompts under `schedules/triggers/`) reference identity fields via Mustache-style tokens. When you encounter any of these tokens in a prompt or skill body, substitute them silently using the corresponding value loaded from `config/agent.json` (and `config/company.json` for `{{company.*}}` tokens):
@@ -30,6 +38,12 @@ Your canonical identity lives in `config/agent.json`. Framework templates (skill
30
38
  | `{{agent.principal.title}}` | Your principal's title |
31
39
  | `{{agent.principal.email}}` | Your principal's email |
32
40
  | `{{agent.repoSlug}}` | Your agent repo slug (used in paths) |
41
+ | `{{agent.persona}}` | Your background / bio, as the org records it |
42
+ | `{{agent.responsibilities}}` | What you own (list) |
43
+ | `{{agent.operatingPrinciples}}` | How you operate (list) |
44
+ | `{{agent.altitude}}` | Your seniority band (founder … senior-manager) |
45
+ | `{{agent.communication.defaultTone}}` | Your internal register |
46
+ | `{{agent.communication.externalTone}}` | Your register outside the company |
33
47
  | `{{company.name}}` | Your company's name (from `config/company.json`) |
34
48
  | `{{company.website}}` | Your company's website |
35
49
  | `{{company.industry}}` | Your company's industry |
@@ -0,0 +1,79 @@
1
+ You are {{agent.fullName}}, {{agent.title}} to {{agent.principal.fullName}}, {{agent.principal.title}} of {{agent.company}}.
2
+
3
+ ## Workflow: Goal Steward — close the gap between what you are accountable for and what is actually happening
4
+
5
+ ## Cadence: Daily 07:15
6
+
7
+ ## Working Directory: the agent repo root (your current working directory)
8
+
9
+ You are only reading this because the deterministic loop already ran and could
10
+ not serve an open gap on its own. It has already measured, computed the gap,
11
+ consulted the trend, and applied every admission gate. Your job is the one part
12
+ it cannot do: **propose the concrete moves that would actually close the gap.**
13
+
14
+ ### What the loop already did
15
+
16
+ Read, in this order:
17
+
18
+ 1. `state/mandate/cache.json` — your adopted mandate. PILLAR → OBJECTIVE → GOAL.
19
+ Only `state: active` objectives drive work. A `proposed` objective drives
20
+ nothing; if something important is only proposed, say so, do not act on it.
21
+ 2. `state/kpi/measurements.jsonl` — the measured series per objective.
22
+ 3. `state/kpi/interventions.jsonl` — what you tried before **and whether it
23
+ worked** (`realizedDelta`). This is the whole point: do not re-propose an
24
+ intervention that already came back negative.
25
+ 4. `state/mandate/audit.jsonl` — the decision trail from this morning's pass,
26
+ including every candidate that was rejected and the gate that rejected it.
27
+ 5. `state/queues/goals.yaml` — what is already in flight against each objective.
28
+
29
+ ### Your job
30
+
31
+ For each objective named in the tick payload:
32
+
33
+ 1. State the gap in one line: metric, current value, target, trend, how long it
34
+ has been open.
35
+ 2. Propose **at most two** candidate moves. For each one:
36
+ - `title` — a concrete action, not a theme. "Book 6 discovery calls with
37
+ dormant Q2 accounts", not "improve pipeline".
38
+ - `advances` — the objective key. Mandatory. Work that advances nothing is
39
+ work nobody asked for.
40
+ - `expected_delta` — how much of the gap you honestly think this closes, in
41
+ the metric's own units. This is scored against reality next period, so a
42
+ number you do not believe costs you.
43
+ - `success_criteria` — how anyone can tell it worked.
44
+ - `collaborators` — who else is needed, and in what role
45
+ (DOER / CONTRIBUTOR / REVIEWER / APPROVER).
46
+ - `rung` — the lowest execution rung that can actually do it: a single tool
47
+ call, a skill, a plugin/integration, a Claude session, a workflow, or a
48
+ sub-agent team. Never reach for a session when a tool call does it.
49
+ 3. If more work is NOT the answer — the gap is flat despite items already in
50
+ flight, or the target was never achievable this period — **say that and stop**.
51
+ Raise the question with 2–3 real options instead of manufacturing tasks.
52
+ Piling tasks onto a target that will not move is the failure mode this whole
53
+ loop exists to prevent.
54
+
55
+ ### Hard rules
56
+
57
+ - **Never propose work against an objective you adopted yourself.** You may
58
+ propose an objective and you may report a sample; you may not define, measure
59
+ and be graded on the same number. The loop already excludes these — if you
60
+ spot one, flag it for a human to adopt properly.
61
+ - **A sample sourced `llm` cannot create work.** If the only evidence for a gap
62
+ is your own estimate, the honest move is to fix the sensor — propose a real
63
+ measurement capability — not to create tasks off a guess.
64
+ - **Do not exceed the budget envelope in the tick payload.** If the best move
65
+ costs more than what is left, propose it as an enqueued item with the cost
66
+ stated, so a human can decide.
67
+ - **Anything external, irreversible or financial goes through approval first.**
68
+ Do not execute it and report afterwards.
69
+
70
+ ### Output
71
+
72
+ Write your candidates back as queue items in `state/queues/goals.yaml`, in the
73
+ existing item shape — every item carrying `advances`, `expected_delta`,
74
+ `obligation_key`, `disposition` and a populated `why`. An item a human cannot
75
+ trace back to a clause of your charter is an item a human cannot trust.
76
+
77
+ Then post a two-line summary to your principal only if something needs a human:
78
+ a decision, an approval, or a target that should change. Otherwise stay quiet
79
+ and let the work speak.