@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,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* inbox-wake.test.mjs — the daemon's "something landed" wake signal.
|
|
3
|
+
*
|
|
4
|
+
* Exercised against a REAL temp filesystem (fs.watch semantics are the thing
|
|
5
|
+
* under test), plus one injected-watch case that proves the scan floor carries
|
|
6
|
+
* the load when every watcher is dead — the property that keeps a missed event
|
|
7
|
+
* from turning into a silently late agent.
|
|
8
|
+
*
|
|
9
|
+
* Run: node --test scripts/daemon/inbox-wake.test.mjs
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { test } from "node:test";
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import { mkdtempSync, mkdirSync, writeFileSync, renameSync, rmSync } from "node:fs";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
waitForInboxWake,
|
|
20
|
+
signalInbox,
|
|
21
|
+
findLiveInboxService,
|
|
22
|
+
isLiveItemName,
|
|
23
|
+
resetInboxWake,
|
|
24
|
+
DEFAULT_SERVICES,
|
|
25
|
+
} from "./inbox-wake.mjs";
|
|
26
|
+
|
|
27
|
+
function makeRoot() {
|
|
28
|
+
const root = mkdtempSync(join(tmpdir(), "inbox-wake-"));
|
|
29
|
+
mkdirSync(join(root, "state", "inbox", "cohort"), { recursive: true });
|
|
30
|
+
return root;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function writeItem(root, service, name, body = "id: x\n") {
|
|
34
|
+
writeFileSync(join(root, "state", "inbox", service, name), body);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// --- the predicate ----------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
test("isLiveItemName: only un-drained, non-dot files count as work", () => {
|
|
40
|
+
assert.equal(isLiveItemName("2026-08-11-abc.yaml"), true);
|
|
41
|
+
assert.equal(isLiveItemName("2026-08-11-abc.yaml.processed"), false);
|
|
42
|
+
assert.equal(isLiveItemName("2026-08-11-abc.yaml.deferred"), false);
|
|
43
|
+
assert.equal(isLiveItemName("2026-08-11-abc.yaml.processed-bundled"), false);
|
|
44
|
+
assert.equal(isLiveItemName(".DS_Store"), false);
|
|
45
|
+
assert.equal(isLiveItemName(""), false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("findLiveInboxService: names the service holding work, else null", () => {
|
|
49
|
+
const root = makeRoot();
|
|
50
|
+
try {
|
|
51
|
+
assert.equal(findLiveInboxService({ agentRoot: root }), null);
|
|
52
|
+
writeItem(root, "cohort", "a.yaml.processed");
|
|
53
|
+
assert.equal(findLiveInboxService({ agentRoot: root }), null, "a drained item is not work");
|
|
54
|
+
writeItem(root, "cohort", "b.yaml");
|
|
55
|
+
assert.equal(findLiveInboxService({ agentRoot: root }), "cohort");
|
|
56
|
+
} finally {
|
|
57
|
+
rmSync(root, { recursive: true, force: true });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("the cohort + orgmail dirs are in the default service set (the ones that were missed)", () => {
|
|
62
|
+
assert.ok(DEFAULT_SERVICES.includes("cohort"));
|
|
63
|
+
assert.ok(DEFAULT_SERVICES.includes("orgmail"));
|
|
64
|
+
assert.ok(DEFAULT_SERVICES.includes("telegram"));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// --- the wake ---------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
test("returns immediately when work is already waiting (never sleep on a full inbox)", async () => {
|
|
70
|
+
const root = makeRoot();
|
|
71
|
+
try {
|
|
72
|
+
writeItem(root, "cohort", "a.yaml");
|
|
73
|
+
const r = await waitForInboxWake({ agentRoot: root, timeoutMs: 5_000 });
|
|
74
|
+
assert.equal(r.woke, true);
|
|
75
|
+
assert.equal(r.reason, "pending");
|
|
76
|
+
assert.equal(r.service, "cohort");
|
|
77
|
+
assert.equal(r.waitedMs, 0);
|
|
78
|
+
} finally {
|
|
79
|
+
rmSync(root, { recursive: true, force: true });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("wakes on a file written by another writer, far inside the 60s poll interval", async () => {
|
|
84
|
+
const root = makeRoot();
|
|
85
|
+
try {
|
|
86
|
+
const started = Date.now();
|
|
87
|
+
const waiting = waitForInboxWake({ agentRoot: root, timeoutMs: 8_000 });
|
|
88
|
+
setTimeout(() => writeItem(root, "cohort", "late.yaml"), 50);
|
|
89
|
+
const r = await waiting;
|
|
90
|
+
assert.equal(r.woke, true);
|
|
91
|
+
assert.ok(["watch", "scan"].includes(r.reason), `unexpected reason ${r.reason}`);
|
|
92
|
+
assert.equal(r.service, "cohort");
|
|
93
|
+
assert.ok(Date.now() - started < 3_000, "a wake must not take seconds");
|
|
94
|
+
} finally {
|
|
95
|
+
rmSync(root, { recursive: true, force: true });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("does NOT wake when the only change is an item being drained", async () => {
|
|
100
|
+
const root = makeRoot();
|
|
101
|
+
try {
|
|
102
|
+
writeItem(root, "cohort", "a.yaml");
|
|
103
|
+
// Drain it, then wait: renaming to `.processed` raises watch events for BOTH
|
|
104
|
+
// names, so an event-driven wake would spin here.
|
|
105
|
+
renameSync(
|
|
106
|
+
join(root, "state", "inbox", "cohort", "a.yaml"),
|
|
107
|
+
join(root, "state", "inbox", "cohort", "a.yaml.processed")
|
|
108
|
+
);
|
|
109
|
+
const r = await waitForInboxWake({ agentRoot: root, timeoutMs: 400 });
|
|
110
|
+
assert.equal(r.woke, false);
|
|
111
|
+
assert.equal(r.reason, "timeout");
|
|
112
|
+
} finally {
|
|
113
|
+
rmSync(root, { recursive: true, force: true });
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("times out cleanly with an empty inbox (the loop shape is unchanged)", async () => {
|
|
118
|
+
const root = makeRoot();
|
|
119
|
+
try {
|
|
120
|
+
const r = await waitForInboxWake({ agentRoot: root, timeoutMs: 200 });
|
|
121
|
+
assert.deepEqual(
|
|
122
|
+
{ woke: r.woke, reason: r.reason, service: r.service },
|
|
123
|
+
{ woke: false, reason: "timeout", service: null }
|
|
124
|
+
);
|
|
125
|
+
assert.ok(r.waitedMs >= 150);
|
|
126
|
+
} finally {
|
|
127
|
+
rmSync(root, { recursive: true, force: true });
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("signalInbox wakes an in-process waiter (the dispatcher's promote path)", async () => {
|
|
132
|
+
const root = makeRoot();
|
|
133
|
+
resetInboxWake();
|
|
134
|
+
try {
|
|
135
|
+
const waiting = waitForInboxWake({ agentRoot: root, timeoutMs: 5_000, scanMs: 60_000 });
|
|
136
|
+
setTimeout(() => {
|
|
137
|
+
// Simulate promoteDeferred: the file becomes live, then we nudge.
|
|
138
|
+
writeItem(root, "cohort", "promoted.yaml");
|
|
139
|
+
signalInbox("promote");
|
|
140
|
+
}, 20);
|
|
141
|
+
const r = await waiting;
|
|
142
|
+
assert.equal(r.woke, true);
|
|
143
|
+
assert.equal(r.service, "cohort");
|
|
144
|
+
} finally {
|
|
145
|
+
resetInboxWake();
|
|
146
|
+
rmSync(root, { recursive: true, force: true });
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("signalInbox with no work does NOT wake (a spurious nudge is free)", async () => {
|
|
151
|
+
const root = makeRoot();
|
|
152
|
+
resetInboxWake();
|
|
153
|
+
try {
|
|
154
|
+
const waiting = waitForInboxWake({ agentRoot: root, timeoutMs: 400, scanMs: 60_000 });
|
|
155
|
+
setTimeout(() => signalInbox("noise"), 20);
|
|
156
|
+
const r = await waiting;
|
|
157
|
+
assert.equal(r.woke, false);
|
|
158
|
+
} finally {
|
|
159
|
+
resetInboxWake();
|
|
160
|
+
rmSync(root, { recursive: true, force: true });
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("with EVERY watcher dead the scan floor still wakes it — and says so once", async () => {
|
|
165
|
+
const root = makeRoot();
|
|
166
|
+
const logs = [];
|
|
167
|
+
try {
|
|
168
|
+
const watchImpl = () => {
|
|
169
|
+
throw new Error("EMFILE: watcher limit reached");
|
|
170
|
+
};
|
|
171
|
+
const waiting = waitForInboxWake({
|
|
172
|
+
agentRoot: root,
|
|
173
|
+
timeoutMs: 6_000,
|
|
174
|
+
scanMs: 100,
|
|
175
|
+
watchImpl,
|
|
176
|
+
log: (m) => logs.push(m),
|
|
177
|
+
});
|
|
178
|
+
setTimeout(() => writeItem(root, "cohort", "floor.yaml"), 30);
|
|
179
|
+
const r = await waiting;
|
|
180
|
+
assert.equal(r.woke, true);
|
|
181
|
+
assert.equal(r.reason, "scan", "correctness must not depend on fs.watch");
|
|
182
|
+
assert.equal(logs.length, 1, "a dead watcher is reported — fail-open, never silent");
|
|
183
|
+
assert.match(logs[0], /falling back to the 100ms rescan/);
|
|
184
|
+
} finally {
|
|
185
|
+
resetInboxWake();
|
|
186
|
+
rmSync(root, { recursive: true, force: true });
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("a missing service dir is not an error (an un-enrolled agent just times out)", async () => {
|
|
191
|
+
const root = mkdtempSync(join(tmpdir(), "inbox-wake-bare-"));
|
|
192
|
+
try {
|
|
193
|
+
const r = await waitForInboxWake({ agentRoot: root, timeoutMs: 200 });
|
|
194
|
+
assert.equal(r.woke, false);
|
|
195
|
+
assert.equal(r.reason, "timeout");
|
|
196
|
+
} finally {
|
|
197
|
+
rmSync(root, { recursive: true, force: true });
|
|
198
|
+
}
|
|
199
|
+
});
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { readFileSync, readdirSync } from "fs";
|
|
7
7
|
import { join } from "path";
|
|
8
8
|
import { compileContext } from "./context-compiler.mjs";
|
|
9
|
+
import { loadPersonaBlock } from "../../lib/identity/persona.mjs";
|
|
9
10
|
import { wrapExternalContent } from "../../lib/security/external-content.mjs";
|
|
10
11
|
import { isEnabled as orgEnabled } from "../../lib/org/client.mjs";
|
|
11
12
|
import { recall as orgRecall } from "../../lib/org/knowledge.mjs";
|
|
@@ -118,6 +119,27 @@ async function buildOrgKnowledgeBlock(item, classResult, queueItem) {
|
|
|
118
119
|
// Cached identity preamble — extracted from CLAUDE.md on first call
|
|
119
120
|
let cachedPreamble = null;
|
|
120
121
|
|
|
122
|
+
// Cached persona block — rendered from config/agent.json (the SoT, kept in sync
|
|
123
|
+
// with the Cohort server) on first call. This leads every prompt and is
|
|
124
|
+
// AUTHORITATIVE over the CLAUDE.md-scraped preamble that follows it: a scaffolded
|
|
125
|
+
// repo still carries "your identity has not yet been configured" prose in
|
|
126
|
+
// CLAUDE.md long after the org record is populated, and without an explicit
|
|
127
|
+
// precedence rule the model would take that placeholder at face value and fall
|
|
128
|
+
// back to a generic-assistant register.
|
|
129
|
+
let cachedPersona = null;
|
|
130
|
+
|
|
131
|
+
/** Render (once per process) the persona block from config/agent.json. */
|
|
132
|
+
function loadPersona() {
|
|
133
|
+
if (cachedPersona !== null) return cachedPersona;
|
|
134
|
+
cachedPersona = loadPersonaBlock(AGENT_REPO_DIR) || "";
|
|
135
|
+
return cachedPersona;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** For tests: drop the cached persona so a fresh config/agent.json is re-read. */
|
|
139
|
+
export function _resetPersonaCache() {
|
|
140
|
+
cachedPersona = null;
|
|
141
|
+
}
|
|
142
|
+
|
|
121
143
|
function loadPreamble() {
|
|
122
144
|
if (cachedPreamble) return cachedPreamble;
|
|
123
145
|
|
|
@@ -185,7 +207,10 @@ function buildFallbackPreamble() {
|
|
|
185
207
|
const principal = a.principal || {};
|
|
186
208
|
const principalName = principal.fullName || "the principal";
|
|
187
209
|
const principalTitle = principal.title || "principal";
|
|
188
|
-
|
|
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}.
|
|
189
214
|
You operate as the autonomous executive command layer for the company.
|
|
190
215
|
|
|
191
216
|
${a.companyDescription || a.company}.
|
|
@@ -552,6 +577,21 @@ export async function buildPrompt(item, classResult, options = {}) {
|
|
|
552
577
|
|
|
553
578
|
const parts = [];
|
|
554
579
|
|
|
580
|
+
// 1. Persona block (cached) — rendered from config/agent.json, which the
|
|
581
|
+
// Cohort enrollment pull keeps current. Leads the prompt and outranks the
|
|
582
|
+
// CLAUDE.md preamble below, because a scaffolded CLAUDE.md carries
|
|
583
|
+
// "identity not yet configured" placeholder prose that would otherwise
|
|
584
|
+
// contradict a perfectly good org record.
|
|
585
|
+
const persona = loadPersona();
|
|
586
|
+
if (persona) {
|
|
587
|
+
parts.push(persona);
|
|
588
|
+
parts.push("");
|
|
589
|
+
parts.push(
|
|
590
|
+
"The section above is your identity, resolved from your organisation's record of you. Where anything below conflicts with it, the section above wins.",
|
|
591
|
+
);
|
|
592
|
+
parts.push("");
|
|
593
|
+
}
|
|
594
|
+
|
|
555
595
|
// 1. Identity preamble (cached)
|
|
556
596
|
parts.push(preamble);
|
|
557
597
|
parts.push("");
|
|
@@ -19,9 +19,31 @@
|
|
|
19
19
|
|
|
20
20
|
"use strict";
|
|
21
21
|
|
|
22
|
+
import { cohortTypingAdapter } from "../../lib/org/typing.mjs";
|
|
23
|
+
|
|
22
24
|
/** @type {Map<string, { startTypingHeartbeat:Function, stopTypingHeartbeat:Function }>} */
|
|
23
25
|
const ADAPTERS = new Map();
|
|
24
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Services whose adapter is BUILT IN rather than created by the daemon's
|
|
29
|
+
* channel-bus loop, resolved on first use.
|
|
30
|
+
*
|
|
31
|
+
* `cohort` is the org substrate itself: there is no channel adapter object for
|
|
32
|
+
* it anywhere (the messaging-inbound cadence writes inbox YAML directly), so
|
|
33
|
+
* nothing would ever have called `registerTypingAdapter("cohort", …)` — and an
|
|
34
|
+
* SDK-driven agent, which is now the ONLY responder for its own rooms
|
|
35
|
+
* (hq `llm-responder/sdk-driven.ts` stands hq down for a live daemon), would
|
|
36
|
+
* show no "composing" indicator at all while it worked. The adapter is a plain
|
|
37
|
+
* `messaging.typing` client (`lib/org/typing.mjs`) that no-ops when the agent is
|
|
38
|
+
* not enrolled, so resolving it lazily here costs nothing for an agent that has
|
|
39
|
+
* no org.
|
|
40
|
+
*
|
|
41
|
+
* @type {Record<string, () => object>}
|
|
42
|
+
*/
|
|
43
|
+
const BUILTIN_ADAPTERS = {
|
|
44
|
+
cohort: () => cohortTypingAdapter(),
|
|
45
|
+
};
|
|
46
|
+
|
|
25
47
|
/** Register a channel adapter under its service name (e.g. "telegram"). */
|
|
26
48
|
export function registerTypingAdapter(service, adapter) {
|
|
27
49
|
if (!service || !adapter) return;
|
|
@@ -29,6 +51,37 @@ export function registerTypingAdapter(service, adapter) {
|
|
|
29
51
|
ADAPTERS.set(service, adapter);
|
|
30
52
|
}
|
|
31
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Resolve the adapter for a service: an explicitly registered one wins (so a
|
|
56
|
+
* test or a future real cohort adapter can override), else a built-in.
|
|
57
|
+
* Fail-open: a built-in that cannot be constructed yields null and the caller
|
|
58
|
+
* no-ops, but the reason is logged — a silently missing indicator is exactly
|
|
59
|
+
* the class of bug this module exists to fix.
|
|
60
|
+
*/
|
|
61
|
+
function adapterFor(service) {
|
|
62
|
+
if (!service) return null;
|
|
63
|
+
const registered = ADAPTERS.get(service);
|
|
64
|
+
if (registered) return registered;
|
|
65
|
+
const builtin = BUILTIN_ADAPTERS[service];
|
|
66
|
+
if (!builtin) return null;
|
|
67
|
+
try {
|
|
68
|
+
const made = builtin();
|
|
69
|
+
if (!made || typeof made.startTypingHeartbeat !== "function") return null;
|
|
70
|
+
ADAPTERS.set(service, made);
|
|
71
|
+
return made;
|
|
72
|
+
} catch (err) {
|
|
73
|
+
try {
|
|
74
|
+
console.warn(
|
|
75
|
+
`[typing-registry] built-in typing adapter for "${service}" failed to load ` +
|
|
76
|
+
`(${err && err.message}) — sessions will run without a typing indicator`
|
|
77
|
+
);
|
|
78
|
+
} catch {
|
|
79
|
+
/* never throw from logging */
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
32
85
|
export function clearTypingAdapters() {
|
|
33
86
|
ADAPTERS.clear();
|
|
34
87
|
}
|
|
@@ -57,7 +110,7 @@ export function sourceFromItem(item) {
|
|
|
57
110
|
export function startTyping(item) {
|
|
58
111
|
try {
|
|
59
112
|
const service = item?.service;
|
|
60
|
-
const adapter =
|
|
113
|
+
const adapter = adapterFor(service);
|
|
61
114
|
if (!adapter) return null;
|
|
62
115
|
const source = sourceFromItem(item);
|
|
63
116
|
if (!source || !source.chatId) return null;
|
|
@@ -71,7 +124,7 @@ export function startTyping(item) {
|
|
|
71
124
|
export function stopTyping(item) {
|
|
72
125
|
try {
|
|
73
126
|
const service = item?.service;
|
|
74
|
-
const adapter =
|
|
127
|
+
const adapter = adapterFor(service);
|
|
75
128
|
if (!adapter) return;
|
|
76
129
|
const source = sourceFromItem(item);
|
|
77
130
|
if (!source || !source.chatId) return;
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
stopTyping,
|
|
17
17
|
sourceFromItem,
|
|
18
18
|
} from "./typing-registry.mjs";
|
|
19
|
+
import { resetCohortTypingAdapter } from "../../lib/org/typing.mjs";
|
|
19
20
|
|
|
20
21
|
function fakeAdapter() {
|
|
21
22
|
return {
|
|
@@ -68,6 +69,30 @@ test("sourceFromItem prefers a structured source, else reconstructs from item",
|
|
|
68
69
|
assert.equal(group.threadRef, "77");
|
|
69
70
|
});
|
|
70
71
|
|
|
72
|
+
test("cohort resolves the BUILT-IN adapter with nothing registered (the SDK parity gap)", () => {
|
|
73
|
+
clearTypingAdapters();
|
|
74
|
+
resetCohortTypingAdapter();
|
|
75
|
+
// No registerTypingAdapter("cohort", …) anywhere — nothing constructs a
|
|
76
|
+
// cohort channel adapter, which is why the indicator was missing entirely.
|
|
77
|
+
const item = { service: "cohort", channel_id: "ch_1", channel: "#build", thread_id: "" };
|
|
78
|
+
assert.doesNotThrow(() => startTyping(item));
|
|
79
|
+
assert.doesNotThrow(() => stopTyping(item));
|
|
80
|
+
clearTypingAdapters();
|
|
81
|
+
resetCohortTypingAdapter();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("an explicitly registered cohort adapter still wins over the built-in", () => {
|
|
85
|
+
clearTypingAdapters();
|
|
86
|
+
resetCohortTypingAdapter();
|
|
87
|
+
const fake = fakeAdapter();
|
|
88
|
+
registerTypingAdapter("cohort", fake);
|
|
89
|
+
startTyping({ service: "cohort", channel_id: "ch_1", channel: "#build" });
|
|
90
|
+
assert.equal(fake.started.length, 1);
|
|
91
|
+
assert.equal(fake.started[0].chatId, "ch_1");
|
|
92
|
+
clearTypingAdapters();
|
|
93
|
+
resetCohortTypingAdapter();
|
|
94
|
+
});
|
|
95
|
+
|
|
71
96
|
test("registerTypingAdapter ignores adapters lacking startTypingHeartbeat", () => {
|
|
72
97
|
clearTypingAdapters();
|
|
73
98
|
registerTypingAdapter("bad", { foo: 1 });
|
|
@@ -107,7 +107,7 @@ function listPlists(agentRoot) {
|
|
|
107
107
|
// Tests
|
|
108
108
|
// ---------------------------------------------------------------------------
|
|
109
109
|
|
|
110
|
-
test("generator emits
|
|
110
|
+
test("generator emits 22 standard plists with the agent's first name", async () => {
|
|
111
111
|
// Standard inventory (cadence-bus v1 + slack-socket-mode v1), with NO
|
|
112
112
|
// config/.cadence-plists.tsv present (no archetype cadences):
|
|
113
113
|
// daemon, poll-relay, slack-socket (3 infra), PLUS one trigger plist per
|
|
@@ -118,8 +118,8 @@ test("generator emits 21 standard plists with the agent's first name", async ()
|
|
|
118
118
|
// economics/scheduling cadences nightly-cost-reconcile, fleet-cost-digest,
|
|
119
119
|
// org-cost-sync, monitoring-alerts, dynamic-jobs (5), PLUS the SP10
|
|
120
120
|
// messaging-inbound cadence (1), PLUS the 2026-07 Directory & Design
|
|
121
|
-
// stewards directory-hygiene + brand-steward (2)
|
|
122
|
-
// =
|
|
121
|
+
// stewards directory-hygiene + brand-steward (2), PLUS the D3 self-directed
|
|
122
|
+
// loop goal-steward (1). = 3 infra + 19 trigger = 22 total.
|
|
123
123
|
// org-pulse used to be declared standard but was OMITTED by the old hardcoded
|
|
124
124
|
// list (it never got a plist); deriving from the SoT fixes that drift.
|
|
125
125
|
// The former weekly-* cadences are still ARCHETYPE-DRIVEN (function × altitude),
|
|
@@ -129,7 +129,7 @@ test("generator emits 21 standard plists with the agent's first name", async ()
|
|
|
129
129
|
const r = runGenerator(root);
|
|
130
130
|
assert.equal(r.status, 0, r.stderr);
|
|
131
131
|
const plists = listPlists(root);
|
|
132
|
-
assert.equal(plists.length,
|
|
132
|
+
assert.equal(plists.length, 22, `expected 22 standard plists; got ${plists.join(",")}`);
|
|
133
133
|
assert.ok(plists.includes("ai.maestro.alice-messaging-inbound.plist"),
|
|
134
134
|
"messaging-inbound (SP10 standard cadence) must get a SoT-derived plist");
|
|
135
135
|
for (const id of ["directory-hygiene", "brand-steward"]) {
|
|
@@ -200,7 +200,7 @@ test("archetype cadences from config/.cadence-plists.tsv emit extra trigger plis
|
|
|
200
200
|
const r = runGenerator(root);
|
|
201
201
|
assert.equal(r.status, 0, r.stderr);
|
|
202
202
|
const plists = listPlists(root);
|
|
203
|
-
assert.equal(plists.length,
|
|
203
|
+
assert.equal(plists.length, 24, `expected 22 standard + 2 archetype; got ${plists.join(",")}`);
|
|
204
204
|
const dir = join(root, "scripts/local-triggers/plists");
|
|
205
205
|
const eng = readFileSync(join(dir, "ai.maestro.erin-engineering-health.plist"), "utf-8");
|
|
206
206
|
assert.match(eng, /<key>Weekday<\/key>\s*<integer>3<\/integer>/); // base64 schedule decoded
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* gen-subagent-manifest.mjs — generate `agents/manifest.json`, layer 0's index
|
|
4
|
+
* (Sub-Agent Registry v1 §2.2).
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS SCRIPT EXISTS. `agents/` ships in the npm package and is copied
|
|
7
|
+
* verbatim by `maestro create`, then MERGED (never overwritten) by
|
|
8
|
+
* `UPGRADE_PATHS`. That means an SDK fix to a shipped agent.md can never reach a
|
|
9
|
+
* deployed agent — an upgraded file and a hand-edited one look identical on disk.
|
|
10
|
+
* The manifest's per-agent sha256 breaks the tie: a file whose hash matches a
|
|
11
|
+
* manifest sha is a pristine copy and may be re-materialised; anything else is a
|
|
12
|
+
* human's edit and is sacred. That single fact is what lets `create`'s copy list
|
|
13
|
+
* and `UPGRADE_PATHS` stay EXACTLY as they are — no migration, no breaking change.
|
|
14
|
+
*
|
|
15
|
+
* Run at prepublish and commit the result:
|
|
16
|
+
* node scripts/setup/gen-subagent-manifest.mjs
|
|
17
|
+
* node scripts/setup/gen-subagent-manifest.mjs --check # CI: fail on drift
|
|
18
|
+
*
|
|
19
|
+
* `--check` is the CI gate: it rebuilds the manifest in memory and fails when it
|
|
20
|
+
* differs from the committed one, so a shipped agent.md can never change without
|
|
21
|
+
* its sha changing with it (which would silently reclassify every deployed copy
|
|
22
|
+
* as a local edit and freeze it forever).
|
|
23
|
+
*
|
|
24
|
+
* Exit codes: 0 ok · 1 drift (with --check) · 2 an agent.md failed validation.
|
|
25
|
+
*
|
|
26
|
+
* Node builtins only. ESM.
|
|
27
|
+
*
|
|
28
|
+
* @module scripts/setup/gen-subagent-manifest
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
"use strict";
|
|
32
|
+
|
|
33
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
34
|
+
import { dirname, join, resolve } from "node:path";
|
|
35
|
+
import { fileURLToPath } from "node:url";
|
|
36
|
+
import { buildManifest, manifestPath, readManifest, writeManifest } from "../../lib/subagents/manifest.mjs";
|
|
37
|
+
|
|
38
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
39
|
+
const MAESTRO_ROOT =
|
|
40
|
+
process.env.MAESTRO_ROOT && existsSync(join(process.env.MAESTRO_ROOT, "lib", "subagents", "manifest.mjs"))
|
|
41
|
+
? process.env.MAESTRO_ROOT
|
|
42
|
+
: resolve(HERE, "..", "..");
|
|
43
|
+
|
|
44
|
+
/** Read the package version so the manifest can name the release it describes. */
|
|
45
|
+
function sdkVersion(root) {
|
|
46
|
+
try { return String(JSON.parse(readFileSync(join(root, "package.json"), "utf8")).version || ""); }
|
|
47
|
+
catch { return ""; }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Compare two manifests ignoring `generatedAt` — a timestamp differing on every
|
|
52
|
+
* run would make `--check` permanently red and train everyone to ignore it.
|
|
53
|
+
*/
|
|
54
|
+
function sameManifest(a, b) {
|
|
55
|
+
const strip = (m) => JSON.stringify({ sdkVersion: m.sdkVersion || "", agents: m.agents || {}, emptyDirs: m.emptyDirs || [] });
|
|
56
|
+
return strip(a || {}) === strip(b || {});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function main(argv = process.argv.slice(2), root = MAESTRO_ROOT) {
|
|
60
|
+
const check = argv.includes("--check");
|
|
61
|
+
const strict = argv.includes("--strict");
|
|
62
|
+
const previous = readManifest(root);
|
|
63
|
+
const { manifest, invalid } = buildManifest({ maestroRoot: root, sdkVersion: sdkVersion(root), previous });
|
|
64
|
+
|
|
65
|
+
for (const bad of invalid) {
|
|
66
|
+
console.error(`[gen-subagent-manifest] agents/${bad.slug}/agent.md is invalid:`);
|
|
67
|
+
for (const e of bad.errors) console.error(` - ${e}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (check) {
|
|
71
|
+
if (!existsSync(manifestPath(root))) {
|
|
72
|
+
console.error(`[gen-subagent-manifest] ${manifestPath(root)} does not exist — run this script and commit it.`);
|
|
73
|
+
return 1;
|
|
74
|
+
}
|
|
75
|
+
if (!sameManifest(previous, manifest)) {
|
|
76
|
+
console.error("[gen-subagent-manifest] agents/manifest.json is STALE — an agent.md changed without regenerating it.");
|
|
77
|
+
console.error(" fix: node scripts/setup/gen-subagent-manifest.mjs && git add agents/manifest.json");
|
|
78
|
+
return 1;
|
|
79
|
+
}
|
|
80
|
+
console.log(`[gen-subagent-manifest] up to date (${Object.keys(manifest.agents).length} agents).`);
|
|
81
|
+
return invalid.length && strict ? 2 : 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const p = writeManifest(root, manifest);
|
|
85
|
+
console.log(`[gen-subagent-manifest] wrote ${p} (${Object.keys(manifest.agents).length} agents, sdk ${manifest.sdkVersion || "?"}).`);
|
|
86
|
+
if (manifest.emptyDirs && manifest.emptyDirs.length) {
|
|
87
|
+
console.warn(`[gen-subagent-manifest] ${manifest.emptyDirs.length} agents/ directory(ies) have no agent.md: ${manifest.emptyDirs.join(", ")}`);
|
|
88
|
+
}
|
|
89
|
+
return invalid.length && strict ? 2 : 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Only run when invoked directly (importable for tests).
|
|
93
|
+
if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) {
|
|
94
|
+
process.exit(main());
|
|
95
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gen-subagent-manifest.test.mjs — the layer-0 manifest generator + its CI gate.
|
|
3
|
+
* Run: node --test scripts/setup/gen-subagent-manifest.test.mjs
|
|
4
|
+
*/
|
|
5
|
+
"use strict";
|
|
6
|
+
|
|
7
|
+
import { test } from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
import { main } from "./gen-subagent-manifest.mjs";
|
|
15
|
+
import { buildManifest, manifestPath, readManifest } from "../../lib/subagents/manifest.mjs";
|
|
16
|
+
import { hashFile, stringifyAgentMd } from "../../lib/subagents/schema.mjs";
|
|
17
|
+
|
|
18
|
+
const MAESTRO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
19
|
+
|
|
20
|
+
function put(root, slug, body = `# ${slug}\n\nprose\n`, over = {}) {
|
|
21
|
+
const text = stringifyAgentMd({ name: slug, description: `${slug} does things`, model: "sonnet", tools: ["Read"], ...over }, body);
|
|
22
|
+
mkdirSync(join(root, "agents", slug), { recursive: true });
|
|
23
|
+
writeFileSync(join(root, "agents", slug, "agent.md"), text);
|
|
24
|
+
return text;
|
|
25
|
+
}
|
|
26
|
+
function fixture(version = "1.2.3") {
|
|
27
|
+
const root = mkdtempSync(join(tmpdir(), "gen-manifest-"));
|
|
28
|
+
writeFileSync(join(root, "package.json"), JSON.stringify({ version }));
|
|
29
|
+
return root;
|
|
30
|
+
}
|
|
31
|
+
/** Run main() with its console noise captured. */
|
|
32
|
+
function runMain(argv, root) {
|
|
33
|
+
const log = console.log, warn = console.warn, error = console.error;
|
|
34
|
+
const out = [];
|
|
35
|
+
console.log = console.warn = console.error = (...a) => out.push(a.join(" "));
|
|
36
|
+
try { return { code: main(argv, root), out: out.join("\n") }; }
|
|
37
|
+
finally { console.log = log; console.warn = warn; console.error = error; }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
test("writes agents/manifest.json with the package version and per-agent shas", () => {
|
|
41
|
+
const root = fixture("1.2.3");
|
|
42
|
+
const text = put(root, "alpha");
|
|
43
|
+
const { code } = runMain([], root);
|
|
44
|
+
assert.equal(code, 0);
|
|
45
|
+
assert.equal(existsSync(manifestPath(root)), true);
|
|
46
|
+
const m = readManifest(root);
|
|
47
|
+
assert.equal(m.sdkVersion, "1.2.3");
|
|
48
|
+
assert.equal(m.agents.alpha.sha256, hashFile(text));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("--check passes when the committed manifest is current", () => {
|
|
52
|
+
const root = fixture();
|
|
53
|
+
put(root, "alpha");
|
|
54
|
+
runMain([], root);
|
|
55
|
+
const { code, out } = runMain(["--check"], root);
|
|
56
|
+
assert.equal(code, 0);
|
|
57
|
+
assert.match(out, /up to date \(1 agents\)/);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("--check FAILS when an agent.md changed without regenerating the manifest", () => {
|
|
61
|
+
// Without this gate a shipped agent.md could change while its sha did not,
|
|
62
|
+
// which would silently reclassify every deployed copy as a local edit and
|
|
63
|
+
// freeze it forever.
|
|
64
|
+
const root = fixture();
|
|
65
|
+
put(root, "alpha");
|
|
66
|
+
runMain([], root);
|
|
67
|
+
put(root, "alpha", "# alpha\n\nEDITED UPSTREAM\n");
|
|
68
|
+
const { code, out } = runMain(["--check"], root);
|
|
69
|
+
assert.equal(code, 1);
|
|
70
|
+
assert.match(out, /STALE/);
|
|
71
|
+
assert.match(out, /gen-subagent-manifest\.mjs && git add/);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("--check fails when the manifest has never been generated", () => {
|
|
75
|
+
const root = fixture();
|
|
76
|
+
put(root, "alpha");
|
|
77
|
+
const { code, out } = runMain(["--check"], root);
|
|
78
|
+
assert.equal(code, 1);
|
|
79
|
+
assert.match(out, /does not exist/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("--check ignores generatedAt (a per-run timestamp must not make CI permanently red)", () => {
|
|
83
|
+
const root = fixture();
|
|
84
|
+
put(root, "alpha");
|
|
85
|
+
runMain([], root);
|
|
86
|
+
const doc = JSON.parse(readFileSync(manifestPath(root), "utf8"));
|
|
87
|
+
doc.generatedAt = "1999-01-01T00:00:00.000Z";
|
|
88
|
+
writeFileSync(manifestPath(root), JSON.stringify(doc, null, 2));
|
|
89
|
+
assert.equal(runMain(["--check"], root).code, 0);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("an invalid agent.md is REPORTED, and only --strict makes it an exit code", () => {
|
|
93
|
+
const root = fixture();
|
|
94
|
+
put(root, "alpha", "# alpha\n\nprose\n", { name: "wrong-name" });
|
|
95
|
+
const loose = runMain([], root);
|
|
96
|
+
assert.equal(loose.code, 0);
|
|
97
|
+
assert.match(loose.out, /agents\/alpha\/agent\.md is invalid/);
|
|
98
|
+
assert.match(loose.out, /must equal the directory name/);
|
|
99
|
+
assert.equal(runMain(["--strict"], root).code, 2);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("empty agents/<slug>/ directories are warned about", () => {
|
|
103
|
+
const root = fixture();
|
|
104
|
+
put(root, "alpha");
|
|
105
|
+
mkdirSync(join(root, "agents", "hollow"), { recursive: true });
|
|
106
|
+
const { out } = runMain([], root);
|
|
107
|
+
assert.match(out, /have no agent\.md: hollow/);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("regenerating carries the previous sha into history", () => {
|
|
111
|
+
const root = fixture();
|
|
112
|
+
const v1 = put(root, "alpha", "v1\n");
|
|
113
|
+
runMain([], root);
|
|
114
|
+
put(root, "alpha", "v2\n");
|
|
115
|
+
runMain([], root);
|
|
116
|
+
assert.deepEqual(readManifest(root).agents.alpha.history, [hashFile(v1)]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("the REAL SDK tree would pass the gate (no invalid shipped agents)", () => {
|
|
120
|
+
// Nothing is written to disk — buildManifest is pure. This only proves the
|
|
121
|
+
// shipped corpus is generatable, so `--check` can be wired into CI safely.
|
|
122
|
+
const { invalid } = buildManifest({ maestroRoot: MAESTRO_ROOT, sdkVersion: "test" });
|
|
123
|
+
assert.deepEqual(invalid.map((i) => i.slug), []);
|
|
124
|
+
});
|