@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,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
|
+
}
|