@cohortapp/agent-sdk 2.3.1 → 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.
Files changed (162) hide show
  1. package/bin/maestro.mjs +37 -50
  2. package/framework-features.json +30 -0
  3. package/lib/backlog.mjs +136 -0
  4. package/lib/cadences.mjs +63 -2
  5. package/lib/cadences.test.mjs +105 -0
  6. package/lib/capability/inventory.mjs +542 -0
  7. package/lib/capability/inventory.test.mjs +232 -0
  8. package/lib/capability/probe.mjs +255 -0
  9. package/lib/channels/contract.mjs +37 -1
  10. package/lib/channels/contract.test.mjs +25 -1
  11. package/lib/channels/inbox-item.mjs +20 -0
  12. package/lib/claude-bin.mjs +37 -3
  13. package/lib/claude-bin.test.mjs +42 -8
  14. package/lib/execution/disposition.mjs +501 -0
  15. package/lib/execution/disposition.test.mjs +482 -0
  16. package/lib/execution/drive.mjs +352 -0
  17. package/lib/execution/drive.test.mjs +270 -0
  18. package/lib/execution/effects.mjs +340 -0
  19. package/lib/execution/effects.test.mjs +193 -0
  20. package/lib/execution/index.mjs +152 -0
  21. package/lib/execution/intake.mjs +581 -0
  22. package/lib/execution/intake.test.mjs +343 -0
  23. package/lib/execution/journal.mjs +374 -0
  24. package/lib/execution/journal.test.mjs +261 -0
  25. package/lib/execution/match.mjs +331 -0
  26. package/lib/execution/match.test.mjs +235 -0
  27. package/lib/execution/pipeline.mjs +341 -0
  28. package/lib/execution/pipeline.test.mjs +389 -0
  29. package/lib/execution/route.mjs +332 -0
  30. package/lib/execution/route.test.mjs +186 -0
  31. package/lib/execution/surface-policy.mjs +446 -0
  32. package/lib/execution/surface-policy.test.mjs +162 -0
  33. package/lib/goals/admission.mjs +209 -0
  34. package/lib/goals/admission.test.mjs +139 -0
  35. package/lib/goals/classify.mjs +206 -0
  36. package/lib/goals/classify.test.mjs +109 -0
  37. package/lib/goals/collaborate.mjs +415 -0
  38. package/lib/goals/collaborate.test.mjs +324 -0
  39. package/lib/goals/gaps.mjs +111 -0
  40. package/lib/goals/gaps.test.mjs +284 -0
  41. package/lib/goals/loop.mjs +537 -0
  42. package/lib/goals/loop.test.mjs +719 -0
  43. package/lib/identity/persona.mjs +247 -0
  44. package/lib/identity/persona.test.mjs +117 -0
  45. package/lib/kpi.mjs +469 -0
  46. package/lib/kpi.test.mjs +244 -0
  47. package/lib/mandate/audit.mjs +168 -0
  48. package/lib/mandate/audit.test.mjs +195 -0
  49. package/lib/mandate/cache.mjs +162 -0
  50. package/lib/mandate/derive.mjs +317 -0
  51. package/lib/mandate/derive.test.mjs +224 -0
  52. package/lib/mandate/model.mjs +352 -0
  53. package/lib/mandate/model.test.mjs +145 -0
  54. package/lib/mandate/refresh.mjs +187 -0
  55. package/lib/mandate/refresh.test.mjs +293 -0
  56. package/lib/mcp/server.test.mjs +4 -4
  57. package/lib/org/approvals.mjs +14 -2
  58. package/lib/org/client.mjs +79 -25
  59. package/lib/org/client.test.mjs +54 -1
  60. package/lib/org/doctor.mjs +64 -0
  61. package/lib/org/doctor.test.mjs +31 -2
  62. package/lib/org/inbound/directedness.mjs +720 -0
  63. package/lib/org/inbound/directedness.test.mjs +543 -0
  64. package/lib/org/inbound/facts.mjs +501 -0
  65. package/lib/org/inbound/facts.test.mjs +375 -0
  66. package/lib/org/inbound/hydrate.mjs +535 -0
  67. package/lib/org/inbound/hydrate.test.mjs +326 -0
  68. package/lib/org/inbound/index.mjs +233 -0
  69. package/lib/org/inbound/index.test.mjs +324 -0
  70. package/lib/org/inbound/io.mjs +141 -0
  71. package/lib/org/inbound/project.mjs +201 -0
  72. package/lib/org/inbound/project.test.mjs +287 -0
  73. package/lib/org/inbound/surfaces.mjs +257 -0
  74. package/lib/org/knowledge.mjs +10 -1
  75. package/lib/org/knowledge.test.mjs +8 -1
  76. package/lib/org/leases.mjs +5 -0
  77. package/lib/org/mesh.mjs +45 -2
  78. package/lib/org/mesh.test.mjs +55 -0
  79. package/lib/org/messaging.mjs +180 -15
  80. package/lib/org/messaging.test.mjs +117 -0
  81. package/lib/org/param-contract.mjs +694 -0
  82. package/lib/org/param-contract.test.mjs +451 -0
  83. package/lib/org/protocol.checksum +1 -1
  84. package/lib/org/protocol.mjs +8 -0
  85. package/lib/org/protocol.test.mjs +5 -1
  86. package/lib/org/push.mjs +1025 -0
  87. package/lib/org/push.test.mjs +690 -0
  88. package/lib/org/tool-surface.mjs +138 -38
  89. package/lib/org/tool-surface.test.mjs +13 -8
  90. package/lib/org/typing.mjs +341 -0
  91. package/lib/org/typing.test.mjs +291 -0
  92. package/lib/plan/compile.mjs +510 -0
  93. package/lib/plan/compile.test.mjs +286 -0
  94. package/lib/plan/emit.mjs +256 -0
  95. package/lib/plan/emit.test.mjs +246 -0
  96. package/lib/plan/explain.mjs +226 -0
  97. package/lib/plan/explain.test.mjs +188 -0
  98. package/lib/plan/schema.mjs +140 -0
  99. package/lib/resource-governor.mjs +47 -1
  100. package/lib/resource-governor.test.mjs +21 -1
  101. package/lib/setup/enroll-from-cohort.mjs +84 -16
  102. package/lib/setup/enroll-from-cohort.test.mjs +43 -1
  103. package/lib/setup/sections/identity.mjs +15 -4
  104. package/lib/setup/sections/identity.test.mjs +94 -0
  105. package/lib/setup/sections/inventory.mjs +178 -0
  106. package/lib/setup/sections/inventory.test.mjs +198 -0
  107. package/lib/setup/sections/mandate.mjs +392 -0
  108. package/lib/setup/sections/mandate.test.mjs +373 -0
  109. package/lib/setup/sections/subagents.mjs +427 -0
  110. package/lib/setup/sections/subagents.test.mjs +429 -0
  111. package/lib/setup/sections/verify.mjs +121 -0
  112. package/lib/setup/sections/verify.test.mjs +175 -0
  113. package/lib/setup/sot.mjs +2 -0
  114. package/lib/subagents/cli.mjs +463 -0
  115. package/lib/subagents/cli.test.mjs +389 -0
  116. package/lib/subagents/client.mjs +373 -0
  117. package/lib/subagents/client.test.mjs +309 -0
  118. package/lib/subagents/gap.mjs +268 -0
  119. package/lib/subagents/gap.test.mjs +234 -0
  120. package/lib/subagents/lock.mjs +296 -0
  121. package/lib/subagents/lock.test.mjs +248 -0
  122. package/lib/subagents/manifest.mjs +224 -0
  123. package/lib/subagents/manifest.test.mjs +175 -0
  124. package/lib/subagents/refs.mjs +274 -0
  125. package/lib/subagents/refs.test.mjs +204 -0
  126. package/lib/subagents/resolve.mjs +455 -0
  127. package/lib/subagents/resolve.test.mjs +422 -0
  128. package/lib/subagents/schema.mjs +467 -0
  129. package/lib/subagents/schema.test.mjs +306 -0
  130. package/package.json +9 -4
  131. package/plugins/maestro-skills/.claude-plugin/marketplace.json +16 -0
  132. package/policies/ai-disclosure.yaml +42 -2
  133. package/scaffold/CLAUDE.md +16 -2
  134. package/schedules/triggers/goal-steward.md +79 -0
  135. package/scripts/ci/conformance-org-api.mjs +792 -0
  136. package/scripts/ci/conformance-org-api.test.mjs +417 -0
  137. package/scripts/daemon/agent-daemon.mjs +70 -11
  138. package/scripts/daemon/cadence-handlers.mjs +187 -5
  139. package/scripts/daemon/goal-steward-cadence.test.mjs +243 -0
  140. package/scripts/daemon/inbox-deferral.mjs +45 -2
  141. package/scripts/daemon/inbox-deferral.test.mjs +56 -0
  142. package/scripts/daemon/inbox-wake.mjs +282 -0
  143. package/scripts/daemon/inbox-wake.test.mjs +199 -0
  144. package/scripts/daemon/maestro-daemon.mjs +23 -0
  145. package/scripts/daemon/prompt-builder.mjs +41 -1
  146. package/scripts/daemon/responder.mjs +56 -0
  147. package/scripts/daemon/typing-registry.mjs +55 -2
  148. package/scripts/daemon/typing-registry.test.mjs +25 -0
  149. package/scripts/local-triggers/generate-plists.test.mjs +5 -5
  150. package/scripts/poller/inbox-scan-poller.mjs +26 -1
  151. package/scripts/poller/inbox-scan-poller.test.mjs +64 -0
  152. package/scripts/poller/slack-cloud-relay-client.mjs +5 -0
  153. package/scripts/poller/slack-poller.mjs +32 -0
  154. package/scripts/poller/slack-socket-mode.mjs +27 -1
  155. package/scripts/poller/slack-socket-mode.test.mjs +52 -0
  156. package/scripts/poller/utils.mjs +47 -0
  157. package/scripts/setup/gen-subagent-manifest.mjs +95 -0
  158. package/scripts/setup/gen-subagent-manifest.test.mjs +124 -0
  159. package/scripts/setup/generate-plan.mjs +108 -0
  160. package/scripts/setup/init-capability-manifest.mjs +70 -0
  161. package/scripts/setup/init-skill-marketplace.mjs +155 -0
  162. package/scripts/setup/init-skill-marketplace.test.mjs +193 -0
@@ -0,0 +1,373 @@
1
+ /**
2
+ * lib/subagents/client.mjs — the hq half of the registry (§2.1, §4).
3
+ *
4
+ * Nine verbs (`subagent.list|get|resolve|create|publishVersion|fork|pin|unpin|
5
+ * yank`) plus one cheap GET read (`subagent.roster`). The split matters and is
6
+ * the whole reason there are two read paths: `roster` returns
7
+ * `{rosterVersion, entries:[{slug, definitionId, version, contentHash}]}` with NO
8
+ * bodies and is what `doctor` polls; `resolve` returns bodies and is the
9
+ * expensive one `apply`/`pull` call.
10
+ *
11
+ * WHY THIS MODULE HAS ITS OWN TRANSPORT RATHER THAN lib/org/client.mjs `call()`.
12
+ * `call()` looks the method up in the frozen `METHODS` table and returns
13
+ * `NOT_FOUND` for anything it does not know. The `subagent` family is a protocol
14
+ * change (canonical source `lib/org/protocol.mjs`, then `sync-protocol.mjs` into
15
+ * hq, then a checksum bump in BOTH repos) and that dual-repo ritual is landed
16
+ * separately, once, complete — see §6's protocol-drift risk. Wiring this module
17
+ * to `call()` before the family exists would make every verb fail-open into
18
+ * silence. The transport below is a deliberate 60-line mirror of `httpRequest` +
19
+ * `v1Url` + `baseHeaders`; once the family lands, `postRpc`/`getRead` are the
20
+ * only two functions that need to be swapped for `call`/`read`, and every verb
21
+ * above them is unchanged.
22
+ *
23
+ * FAIL-OPEN IS NOT OPTIONAL HERE. `apply` throws are FATAL to the whole wizard
24
+ * (lib/setup/runner.mjs:220-227), so every function in this file returns a
25
+ * result object and never throws — a timeout, a 500, a missing key and an
26
+ * unparseable body all come back as `{ok:false, ...}` with `degraded:true`. The
27
+ * caller degrades to cache-or-local and records `degraded:["workspace"]`.
28
+ *
29
+ * Writes made with no reachable server QUEUE to `state/subagents/outbox/` and
30
+ * replay on `maestro sync`. They are idempotent server-side by `contentHash` +
31
+ * `(orgId, slug)`, so a replay of a write that actually landed is a no-op.
32
+ *
33
+ * Timeout is 5 s (not the org client's 8 s): this runs inside an interactive
34
+ * wizard step, and a human waiting on a dead server is a worse outcome than a
35
+ * degraded roster.
36
+ *
37
+ * Node builtins only (global fetch on Node 20). ESM.
38
+ *
39
+ * @module lib/subagents/client
40
+ */
41
+
42
+ "use strict";
43
+
44
+ import { loadOrgConfig, configFromAgent } from "../org/client.mjs";
45
+ import { queueOutbox, writeCache } from "./lock.mjs";
46
+ import { hashBody, stringifyAgentMd, stripProvenance } from "./schema.mjs";
47
+
48
+ /** 5 s — an interactive wizard must not hang on a dead server. */
49
+ export const TIMEOUT_MS = 5000;
50
+
51
+ /** The protocol version header every /v1 request carries. */
52
+ const PROTOCOL_VERSION = 1;
53
+
54
+ /**
55
+ * Resolve `{base, token, orgId, memberId, enabled}` for the registry.
56
+ *
57
+ * Precedence mirrors `cohortPullEnv` + `configFromAgent` exactly, because an
58
+ * operator who set `COHORT_API_KEY` for enrolment must not have to set a second
59
+ * variable for sub-agents. `enabled` false is a legitimate steady state, not an
60
+ * error: an un-enrolled agent simply has no layer 1 (§4 — the section reports
61
+ * `status:"skipped"`).
62
+ *
63
+ * @param {{agentRoot?:string, env?:NodeJS.ProcessEnv, cfg?:object}} o
64
+ * @returns {{enabled:boolean, base:string, token:string, orgId:string, memberId:string}}
65
+ */
66
+ export function resolveConfig(o = {}) {
67
+ const env = o.env || process.env;
68
+ const cfg = o.cfg || (o.agentRoot ? loadOrgConfig(o.agentRoot) : {});
69
+ const c = configFromAgent(cfg);
70
+ const base = String(env.COHORT_BASE || env.COHORT_API_URL || c.base || "").replace(/\/+$/, "");
71
+ const token = String(c.token || env.COHORT_API_KEY || env.COHORT_API_TOKEN || env.COHORT_TOKEN || "");
72
+ const orgId = String(c.orgId || env.COHORT_ORG_ID || "");
73
+ const memberId = String(env.COHORT_MEMBER_ID || env.COHORT_AGENT_ID || "");
74
+ return { enabled: !!(base && token), base, token, orgId, memberId };
75
+ }
76
+
77
+ /** Bearer + protocol + tenant-pin headers. Mirrors lib/org/client baseHeaders. */
78
+ function headers(cfg, withBody) {
79
+ const h = {
80
+ authorization: `Bearer ${cfg.token}`,
81
+ "x-org-protocol": String(PROTOCOL_VERSION),
82
+ };
83
+ if (cfg.orgId) h["x-org-id"] = cfg.orgId;
84
+ if (withBody) h["content-type"] = "application/json";
85
+ return h;
86
+ }
87
+
88
+ function v1Url(base, path) {
89
+ return `${String(base).replace(/\/+$/, "")}/v1/${String(path).replace(/^\/+/, "")}`;
90
+ }
91
+
92
+ function pickFetch(fetchImpl) {
93
+ if (typeof fetchImpl === "function") return fetchImpl;
94
+ return typeof fetch === "function" ? fetch : null;
95
+ }
96
+
97
+ /**
98
+ * One HTTP round-trip with a hard timeout. Returns a normalised
99
+ * `{ok, status, body}` — never throws, never rejects.
100
+ * @returns {Promise<{ok:boolean, status:number, body:any, error?:string}>}
101
+ */
102
+ async function http(url, init, fetchImpl, timeoutMs) {
103
+ const f = pickFetch(fetchImpl);
104
+ if (!f) return { ok: false, status: 0, body: null, error: "no fetch available" };
105
+ const ctrl = typeof AbortController === "function" ? new AbortController() : null;
106
+ const ms = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : TIMEOUT_MS;
107
+ const timer = ctrl ? setTimeout(() => ctrl.abort(), ms) : null;
108
+ try {
109
+ const res = await f(url, { ...init, signal: ctrl ? ctrl.signal : undefined });
110
+ const status = typeof res.status === "number" ? res.status : 0;
111
+ let body = null;
112
+ try { body = await res.json(); } catch { body = null; }
113
+ const ok = typeof res.ok === "boolean" ? res.ok : status >= 200 && status < 300;
114
+ return { ok, status, body };
115
+ } catch (e) {
116
+ return { ok: false, status: 0, body: null, error: e && e.name === "AbortError" ? `timeout after ${ms}ms` : String((e && e.message) || e) };
117
+ } finally {
118
+ if (timer) clearTimeout(timer);
119
+ }
120
+ }
121
+
122
+ /**
123
+ * POST an RPC method. Returns `{ok, result, error, degraded}`.
124
+ * `degraded:true` means "the server did not answer" (as opposed to "the server
125
+ * said no") — the caller uses that to decide between outbox-queueing and
126
+ * surfacing a real error to the operator.
127
+ * @param {string} method @param {object} params
128
+ * @param {{cfg:object, fetchImpl?:Function, timeoutMs?:number, idempotencyKey?:string}} o
129
+ */
130
+ export async function postRpc(method, params, o = {}) {
131
+ const cfg = o.cfg || {};
132
+ if (!cfg.base || !cfg.token) return { ok: false, degraded: true, error: { code: "UNAUTHORIZED", message: "no base/token — agent is not enrolled" } };
133
+ const h = headers(cfg, true);
134
+ if (o.idempotencyKey) h["x-idempotency-key"] = String(o.idempotencyKey);
135
+ const r = await http(v1Url(cfg.base, method), { method: "POST", headers: h, body: JSON.stringify(params || {}) }, o.fetchImpl, o.timeoutMs);
136
+ if (!r.ok && r.status === 0) return { ok: false, degraded: true, error: { code: "INTERNAL", message: r.error || "transport error" } };
137
+ const frame = r.body && typeof r.body === "object" ? r.body : null;
138
+ if (frame && frame.ok === true) return { ok: true, result: frame.result === undefined ? null : frame.result };
139
+ const error = (frame && frame.error) || { code: `HTTP_${r.status}`, message: `http ${r.status}` };
140
+ return { ok: false, degraded: false, status: r.status, error };
141
+ }
142
+
143
+ /**
144
+ * GET a read endpoint. Reads return the BARE payload on 200 (not a frame) —
145
+ * same convention as lib/org/client.read.
146
+ */
147
+ export async function getRead(path, o = {}) {
148
+ const cfg = o.cfg || {};
149
+ if (!cfg.base || !cfg.token) return { ok: false, degraded: true, error: { code: "UNAUTHORIZED", message: "no base/token — agent is not enrolled" } };
150
+ const r = await http(v1Url(cfg.base, path), { method: "GET", headers: headers(cfg, false) }, o.fetchImpl, o.timeoutMs);
151
+ if (!r.ok && r.status === 0) return { ok: false, degraded: true, error: { code: "INTERNAL", message: r.error || "transport error" } };
152
+ if (r.ok) return { ok: true, payload: r.body };
153
+ const error = (r.body && r.body.error) || { code: `HTTP_${r.status}`, message: `http ${r.status}` };
154
+ return { ok: false, degraded: false, status: r.status, error };
155
+ }
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // Reads
159
+ // ---------------------------------------------------------------------------
160
+
161
+ /**
162
+ * The cheap poll: `GET /v1/subagent.roster`. No bodies. This is what `doctor`
163
+ * compares against the lock's `rosterVersion` to say "N sub-agents behind
164
+ * workspace" — the ONLY tell that the offline cache has gone stale (§6).
165
+ * @param {{cfg:object, fetchImpl?:Function, timeoutMs?:number}} o
166
+ * @returns {Promise<{ok:boolean, rosterVersion:number, entries:Array, degraded:boolean, error?:object}>}
167
+ */
168
+ export async function fetchRoster(o = {}) {
169
+ const r = await getRead("subagent.roster", o);
170
+ if (!r.ok) return { ok: false, rosterVersion: 0, entries: [], degraded: !!r.degraded, error: r.error };
171
+ const p = r.payload && typeof r.payload === "object" ? r.payload : {};
172
+ return {
173
+ ok: true,
174
+ degraded: false,
175
+ rosterVersion: Number.isFinite(p.rosterVersion) ? p.rosterVersion : 0,
176
+ entries: Array.isArray(p.entries) ? p.entries : [],
177
+ };
178
+ }
179
+
180
+ /**
181
+ * The expensive read: `subagent.resolve` → bodies + rewire advice.
182
+ *
183
+ * Every returned body is written to `state/subagents/cache/<slug>@<v>.md` when an
184
+ * `agentRoot` is supplied, so the NEXT run resolves layer 1 with no network.
185
+ * A `memberId` other than the actor's own is FORBIDDEN server-side unless the
186
+ * actor holds `admin` — we send it verbatim and let hq decide (the dispatcher
187
+ * holds the coarse scope; the real gate is in-domain).
188
+ *
189
+ * @param {{cfg:object, agentRoot?:string, memberId?:string, function?:string, altitude?:string, refs?:string[], fetchImpl?:Function, timeoutMs?:number}} o
190
+ */
191
+ export async function fetchResolve(o = {}) {
192
+ const params = {};
193
+ if (o.memberId) params.memberId = o.memberId;
194
+ if (o.function) params.function = o.function;
195
+ if (o.altitude) params.altitude = o.altitude;
196
+ if (Array.isArray(o.refs) && o.refs.length) params.refs = o.refs.map(String);
197
+
198
+ const r = await postRpc("subagent.resolve", params, o);
199
+ if (!r.ok) return { ok: false, degraded: !!r.degraded, rosterVersion: 0, entries: [], rewire: [], unmatched: [], error: r.error };
200
+
201
+ const res = r.result && typeof r.result === "object" ? r.result : {};
202
+ const entries = Array.isArray(res.entries) ? res.entries : [];
203
+ if (o.agentRoot) {
204
+ for (const e of entries) {
205
+ if (!e || !e.slug || typeof e.body !== "string") continue;
206
+ // Cache the raw body with the frontmatter the server sent, so an offline
207
+ // resolve reconstructs the identical entry (not a re-derived one). Emitted
208
+ // through the SAME serializer the resolver parses with, so a cache round
209
+ // trip is lossless.
210
+ const fm = e.frontmatter && typeof e.frontmatter === "object" ? e.frontmatter : {};
211
+ writeCache(o.agentRoot, e.slug, e.version, stringifyAgentMd(fm, e.body));
212
+ }
213
+ }
214
+ return {
215
+ ok: true,
216
+ degraded: false,
217
+ rosterVersion: Number.isFinite(res.rosterVersion) ? res.rosterVersion : 0,
218
+ entries,
219
+ rewire: Array.isArray(res.rewire) ? res.rewire : [],
220
+ unmatched: Array.isArray(res.unmatched) ? res.unmatched : [],
221
+ };
222
+ }
223
+
224
+ /** `subagent.list` — the catalogue, no bodies. */
225
+ export async function listDefinitions(o = {}) {
226
+ const r = await postRpc("subagent.list", { ...(o.params || {}) }, o);
227
+ return r.ok ? { ok: true, items: Array.isArray(r.result && r.result.items) ? r.result.items : [] } : { ok: false, items: [], degraded: !!r.degraded, error: r.error };
228
+ }
229
+
230
+ /** `subagent.get` — one definition with its head version body. */
231
+ export async function getDefinition(slug, o = {}) {
232
+ const r = await postRpc("subagent.get", { slug: String(slug) }, o);
233
+ return r.ok ? { ok: true, definition: r.result } : { ok: false, definition: null, degraded: !!r.degraded, error: r.error };
234
+ }
235
+
236
+ // ---------------------------------------------------------------------------
237
+ // Writes — each queues to the outbox when the server is unreachable
238
+ // ---------------------------------------------------------------------------
239
+
240
+ /**
241
+ * Run a write, and on a DEGRADED failure (server unreachable, not a rejection)
242
+ * queue it for `maestro sync` to replay.
243
+ *
244
+ * The distinction is load-bearing: a 403 must NOT be queued (it will be rejected
245
+ * forever and the operator would never see why), while a timeout must be, or an
246
+ * offline `maestro subagents publish` silently loses the operator's work.
247
+ *
248
+ * @param {string} method @param {object} params
249
+ * @param {{cfg:object, agentRoot?:string, idempotencyKey?:string, fetchImpl?:Function, timeoutMs?:number, queue?:boolean}} o
250
+ * @returns {Promise<{ok:boolean, result?:object, queued?:string|null, degraded?:boolean, error?:object}>}
251
+ */
252
+ export async function writeVerb(method, params, o = {}) {
253
+ const r = await postRpc(method, params, { ...o, idempotencyKey: o.idempotencyKey });
254
+ if (r.ok) return { ok: true, result: r.result };
255
+ if (r.degraded && o.queue !== false && o.agentRoot) {
256
+ const queued = queueOutbox(o.agentRoot, method, params);
257
+ return { ok: false, degraded: true, queued, error: r.error };
258
+ }
259
+ return { ok: false, degraded: !!r.degraded, error: r.error };
260
+ }
261
+
262
+ /**
263
+ * `subagent.create` — a NEW org-scoped definition + its version 1.
264
+ * `body` is stored with tokens UNRENDERED and with provenance stripped: a
265
+ * definition describes prose, not where a copy of it happened to live.
266
+ */
267
+ export function createDefinition(params, o = {}) {
268
+ const body = String(params.body || "");
269
+ const frontmatter = stripProvenance(params.frontmatter || {});
270
+ return writeVerb("subagent.create", {
271
+ slug: String(params.slug),
272
+ title: String(params.title || params.slug),
273
+ summary: String(params.summary || ""),
274
+ tags: Array.isArray(params.tags) ? params.tags.map(String) : [],
275
+ functions: Array.isArray(params.functions) ? params.functions.map(String) : [],
276
+ altitudes: Array.isArray(params.altitudes) ? params.altitudes.map(String) : [],
277
+ baseLayer: String(params.baseLayer || "none"),
278
+ baseSlug: params.baseSlug ? String(params.baseSlug) : null,
279
+ frontmatter,
280
+ body,
281
+ contentHash: hashBody(body),
282
+ generator: params.generator || { source: "human" },
283
+ }, { ...o, idempotencyKey: `subagent.create:${o.cfg && o.cfg.orgId}:${params.slug}` });
284
+ }
285
+
286
+ /** `subagent.publishVersion` — the "I edited it locally, promote it" path. */
287
+ export function publishVersion(params, o = {}) {
288
+ const body = String(params.body || "");
289
+ return writeVerb("subagent.publishVersion", {
290
+ slug: String(params.slug),
291
+ definitionId: params.definitionId ? String(params.definitionId) : undefined,
292
+ frontmatter: stripProvenance(params.frontmatter || {}),
293
+ body,
294
+ contentHash: hashBody(body),
295
+ generator: params.generator || { source: "human" },
296
+ }, { ...o, idempotencyKey: `subagent.publishVersion:${params.slug}:${hashBody(body)}` });
297
+ }
298
+
299
+ /** `subagent.fork` — copy a resolved body into a NEW org definition (§5.2). */
300
+ export function forkDefinition(params, o = {}) {
301
+ const body = String(params.body || "");
302
+ return writeVerb("subagent.fork", {
303
+ slug: String(params.slug),
304
+ baseLayer: String(params.baseLayer || "sdk"),
305
+ baseSlug: String(params.baseSlug || params.slug),
306
+ title: String(params.title || params.slug),
307
+ summary: String(params.summary || ""),
308
+ frontmatter: stripProvenance(params.frontmatter || {}),
309
+ body,
310
+ contentHash: hashBody(body),
311
+ generator: params.generator || { source: "fork", forkedFrom: params.baseSlug || params.slug },
312
+ }, { ...o, idempotencyKey: `subagent.fork:${o.cfg && o.cfg.orgId}:${params.slug}` });
313
+ }
314
+
315
+ /** `subagent.pin` — give THIS member this definition at this version. */
316
+ export function pinDefinition(params, o = {}) {
317
+ return writeVerb("subagent.pin", {
318
+ memberId: params.memberId ? String(params.memberId) : undefined,
319
+ definitionId: params.definitionId ? String(params.definitionId) : undefined,
320
+ slug: params.slug ? String(params.slug) : undefined,
321
+ versionId: params.versionId ? String(params.versionId) : undefined,
322
+ version: Number.isFinite(params.version) ? params.version : undefined,
323
+ }, { ...o, idempotencyKey: `subagent.pin:${params.memberId || "self"}:${params.definitionId || params.slug}` });
324
+ }
325
+
326
+ /** `subagent.unpin` — remove the seat's pin. NEVER a row delete (§2.4). */
327
+ export function unpinDefinition(params, o = {}) {
328
+ return writeVerb("subagent.unpin", {
329
+ memberId: params.memberId ? String(params.memberId) : undefined,
330
+ definitionId: params.definitionId ? String(params.definitionId) : undefined,
331
+ slug: params.slug ? String(params.slug) : undefined,
332
+ }, o);
333
+ }
334
+
335
+ /** `subagent.yank` — admin-scoped curation. Status flip, never a delete. */
336
+ export function yankDefinition(params, o = {}) {
337
+ return writeVerb("subagent.yank", {
338
+ definitionId: params.definitionId ? String(params.definitionId) : undefined,
339
+ slug: params.slug ? String(params.slug) : undefined,
340
+ versionId: params.versionId ? String(params.versionId) : undefined,
341
+ reason: params.reason ? String(params.reason) : "",
342
+ }, o);
343
+ }
344
+
345
+ /**
346
+ * Replay queued offline writes, oldest first. Stops at the FIRST degraded
347
+ * failure (the server is down again — draining the rest would just re-queue
348
+ * them) but continues past a rejection (that entry is dead; dropping it is the
349
+ * only way the outbox ever empties, and the error is returned to the caller).
350
+ *
351
+ * @param {{agentRoot:string, cfg:object, fetchImpl?:Function, timeoutMs?:number, entries?:Array}} o
352
+ * @returns {Promise<{replayed:number, dropped:Array<object>, remaining:number, stopped:boolean}>}
353
+ */
354
+ export async function flushOutbox(o = {}) {
355
+ const { listOutbox, removeOutbox } = await import("./lock.mjs");
356
+ const entries = Array.isArray(o.entries) ? o.entries : listOutbox(o.agentRoot);
357
+ let replayed = 0;
358
+ const dropped = [];
359
+ let stopped = false;
360
+ let i = 0;
361
+ for (; i < entries.length; i++) {
362
+ const e = entries[i];
363
+ const r = await postRpc(e.op, e.payload, o);
364
+ if (r.ok) { removeOutbox(e.path); replayed++; continue; }
365
+ if (r.degraded) { stopped = true; break; }
366
+ // A REJECTION (403/409/…) is dead on arrival: retrying forever would make the
367
+ // outbox unemptiable and hide the real error. Drop it and report it.
368
+ removeOutbox(e.path);
369
+ dropped.push({ op: e.op, error: r.error });
370
+ }
371
+ // Everything from the stop point onward is still queued.
372
+ return { replayed, dropped, remaining: entries.length - i, stopped };
373
+ }