@cohortapp/agent-sdk 2.4.0 → 2.5.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/bin/maestro.mjs +9 -0
- package/lib/backlog.mjs +35 -0
- package/lib/backlog.test.mjs +36 -0
- package/lib/channels/contract.mjs +1 -0
- package/lib/channels/contract.test.mjs +2 -1
- package/lib/channels/inbox-item.mjs +54 -0
- package/lib/comms/send-gate.mjs +56 -1
- package/lib/comms/send-gate.test.mjs +56 -0
- package/lib/execution/disposition.mjs +62 -2
- package/lib/execution/disposition.test.mjs +54 -0
- package/lib/execution/drive.mjs +1 -1
- package/lib/execution/effects.mjs +282 -24
- package/lib/execution/effects.test.mjs +112 -0
- package/lib/execution/index.mjs +1 -0
- package/lib/execution/intake.mjs +43 -9
- package/lib/execution/intake.test.mjs +46 -0
- package/lib/execution/pipeline.mjs +5 -0
- package/lib/execution/surface-policy.mjs +80 -30
- package/lib/goals/classify.mjs +49 -5
- package/lib/goals/classify.test.mjs +58 -0
- package/lib/goals/collaborate.mjs +131 -17
- package/lib/goals/collaborate.test.mjs +16 -4
- package/lib/goals/loop.mjs +160 -9
- package/lib/goals/loop.test.mjs +129 -3
- package/lib/kpi-sensors.mjs +666 -0
- package/lib/kpi-sensors.test.mjs +275 -0
- package/lib/kpi.mjs +23 -0
- package/lib/mandate/audit.mjs +3 -0
- package/lib/mandate/contract.mjs +277 -0
- package/lib/mandate/contract.test.mjs +185 -0
- package/lib/mandate/derive.mjs +49 -5
- package/lib/mandate/derive.test.mjs +7 -1
- package/lib/mandate/model.mjs +10 -1
- package/lib/mandate/model.test.mjs +22 -3
- package/lib/mandate/refresh.mjs +53 -5
- package/lib/mandate/refresh.test.mjs +83 -1
- package/lib/org/doctor.mjs +66 -0
- package/lib/org/doctor.test.mjs +73 -1
- package/lib/org/inbound/directedness.mjs +119 -1
- package/lib/org/inbound/directedness.test.mjs +67 -0
- package/lib/org/inbound/facts.mjs +132 -9
- package/lib/org/inbound/facts.test.mjs +96 -0
- package/lib/org/inbound/hydrate.mjs +40 -0
- package/lib/org/inbound/index.test.mjs +83 -0
- package/lib/org/inbound/project.mjs +8 -0
- package/lib/org/inbound/surfaces.mjs +20 -0
- package/lib/org/param-contract.mjs +16 -2
- package/lib/org/protocol.checksum +1 -1
- package/lib/org/protocol.mjs +214 -2
- package/lib/org/protocol.test.mjs +11 -2
- package/lib/org/push.mjs +213 -49
- package/lib/org/push.test.mjs +112 -10
- package/lib/plan/compile.mjs +85 -8
- package/lib/plan/compile.test.mjs +82 -0
- package/lib/plan/emit.test.mjs +6 -1
- package/lib/setup/enroll-from-cohort.mjs +22 -2
- package/lib/setup/enroll-from-cohort.test.mjs +25 -0
- package/lib/setup/sections/mandate.mjs +43 -1
- package/lib/subagents/schema.mjs +14 -2
- package/lib/subagents/schema.test.mjs +22 -0
- package/package.json +1 -1
- package/scripts/ci/check-subagent-frontmatter.mjs +139 -0
- package/scripts/ci/check-subagent-frontmatter.test.mjs +124 -0
- package/scripts/ci/check.mjs +3 -0
- package/scripts/ci/conformance-org-api.mjs +16 -0
- package/scripts/ci/journey-approval-escalation.mjs +341 -0
- package/scripts/daemon/agent-daemon.mjs +582 -28
- package/scripts/daemon/cadence-handlers.mjs +273 -17
- package/scripts/daemon/cadence-handlers.test.mjs +101 -0
- package/scripts/daemon/execution-ladder.test.mjs +430 -0
- package/scripts/daemon/goal-steward-cadence.test.mjs +69 -0
- package/scripts/daemon/maestro-daemon.mjs +53 -0
- package/scripts/daemon/prompt-builder.mjs +47 -0
- package/scripts/daemon/responder.mjs +70 -3
- package/scripts/poller/imap-client.mjs +20 -1
- package/scripts/poller/inbox-scan-poller.mjs +15 -0
- package/scripts/poller/utils.mjs +51 -0
- package/scripts/setup/generate-capability.mjs +120 -11
- package/scripts/setup/generate-capability.test.mjs +134 -0
- package/scripts/setup/generate-plan.mjs +6 -1
- package/scripts/setup/repair-subagent-frontmatter.mjs +231 -0
|
@@ -114,6 +114,8 @@ export async function hydrate(o = {}) {
|
|
|
114
114
|
return hydrateApproval(c, facts);
|
|
115
115
|
case "handoff":
|
|
116
116
|
return hydrateHandoff(c, facts);
|
|
117
|
+
case "calendar":
|
|
118
|
+
return hydrateCalendar(c, facts);
|
|
117
119
|
case "email":
|
|
118
120
|
return await hydrateEmail(c, facts, io, cache);
|
|
119
121
|
default:
|
|
@@ -324,6 +326,44 @@ function hydrateEscalation(c, facts) {
|
|
|
324
326
|
};
|
|
325
327
|
}
|
|
326
328
|
|
|
329
|
+
/**
|
|
330
|
+
* A calendar event. The body came back on the ACL'd `calendar.list` read in the
|
|
331
|
+
* facts pass — that read IS the probe, so there is no second request here and
|
|
332
|
+
* nothing is rendered that hq did not already hand this seat.
|
|
333
|
+
*/
|
|
334
|
+
function hydrateCalendar(c, facts) {
|
|
335
|
+
const id = s(c.ids.eventId);
|
|
336
|
+
const e = facts.myEvents instanceof Map ? facts.myEvents.get(id) : null;
|
|
337
|
+
const title = s((e && e.title) || c.ids.title || id);
|
|
338
|
+
const lines = [`${describeCalendarKind(c.kind)} — "${title}"`];
|
|
339
|
+
const startsAt = s((e && e.startsAt) || c.ids.startsAt);
|
|
340
|
+
if (startsAt) lines.push(`Starts: ${startsAt}${e && e.endsAt ? ` · ends ${s(e.endsAt)}` : ""}`);
|
|
341
|
+
if (c.kind === "event.rsvp" && c.ids.rsvp) lines.push(`RSVP: ${s(c.ids.rsvp)}`);
|
|
342
|
+
if (e && e.location) lines.push(`Location: ${s(e.location)}`);
|
|
343
|
+
if (e && e.description) lines.push(clip(e.description, 1200));
|
|
344
|
+
return {
|
|
345
|
+
ok: true,
|
|
346
|
+
text: clip(lines.join("\n")),
|
|
347
|
+
subjectDetail: title,
|
|
348
|
+
threadId: id,
|
|
349
|
+
from: { id: s(c.actor), name: s(c.actor) },
|
|
350
|
+
channelId: s(c.ids.channelId || (e && e.channelId) || ""),
|
|
351
|
+
channelLabel: `calendar/${title}`,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Human phrasing for a `calendar.*` event kind. */
|
|
356
|
+
export function describeCalendarKind(kind) {
|
|
357
|
+
switch (kind) {
|
|
358
|
+
case "event.created": return "Calendar event created";
|
|
359
|
+
case "event.updated": return "Calendar event updated";
|
|
360
|
+
case "event.rescheduled": return "Calendar event rescheduled";
|
|
361
|
+
case "event.rsvp": return "Calendar RSVP";
|
|
362
|
+
case "event.deleted": return "Calendar event cancelled";
|
|
363
|
+
default: return "Calendar update";
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
327
367
|
/** An approval — already read back by the facts pass (that IS the probe). */
|
|
328
368
|
function hydrateApproval(c, facts) {
|
|
329
369
|
const id = s(c.ids.approvalId);
|
|
@@ -322,3 +322,86 @@ test("an empty ledger window is a no-op that still advances nothing", async () =
|
|
|
322
322
|
assert.deepEqual(out.events, []);
|
|
323
323
|
assert.equal(out.nextCursor, 5);
|
|
324
324
|
});
|
|
325
|
+
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
// Calendar (JOINT 3) — the surface hq's own agent.wait lanes do not carry
|
|
328
|
+
// ---------------------------------------------------------------------------
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* The calendar payload is redacted like every other family: `event.created`
|
|
332
|
+
* carries `attendeeCount` and the CALENDAR OWNER, never the attendee list. So
|
|
333
|
+
* "am I in this meeting?" is answered by `calendar.list` — hq's seat-scoped
|
|
334
|
+
* query (owner OR attendeeRows, under the PERSONAL_RESTRICTED lens floor) — and
|
|
335
|
+
* NEVER by widening the chain payload.
|
|
336
|
+
*/
|
|
337
|
+
test("calendar: an event I attend arrives; a colleague's event never does", async () => {
|
|
338
|
+
const io = fakeIo({
|
|
339
|
+
"read:events": okRead({
|
|
340
|
+
events: [
|
|
341
|
+
// Mine — I am an attendee (owner is someone else).
|
|
342
|
+
{ seq: 900, family: "calendar", kind: "event.created", entity_id: "EV-mine", actor: BOSS,
|
|
343
|
+
at: "2026-08-11T10:00:00.000Z",
|
|
344
|
+
payload: { title: "Transport review", startsAt: "2026-08-12T10:00:00.000Z", endsAt: "2026-08-12T11:00:00.000Z",
|
|
345
|
+
eventKind: "MEETING", attendeeCount: 4, calendarMemberId: BOSS } },
|
|
346
|
+
// NOT mine — a meeting between two colleagues. `calendar.list` will not
|
|
347
|
+
// return it, so it must never be delivered or hydrated.
|
|
348
|
+
{ seq: 901, family: "calendar", kind: "event.created", entity_id: "EV-theirs", actor: THEM,
|
|
349
|
+
at: "2026-08-11T10:05:00.000Z",
|
|
350
|
+
payload: { title: "Their 1:1", startsAt: "2026-08-12T14:00:00.000Z",
|
|
351
|
+
eventKind: "MEETING", attendeeCount: 2, calendarMemberId: THEM } },
|
|
352
|
+
],
|
|
353
|
+
nextCursor: 901,
|
|
354
|
+
}),
|
|
355
|
+
"calendar.list": okFrame({
|
|
356
|
+
events: [
|
|
357
|
+
{ id: "EV-mine", title: "Transport review", startsAt: "2026-08-12T10:00:00.000Z",
|
|
358
|
+
endsAt: "2026-08-12T11:00:00.000Z", location: "Huddle 2", description: "Walk the five hops",
|
|
359
|
+
calendarMemberId: BOSS, channelId: "C-pub" },
|
|
360
|
+
],
|
|
361
|
+
}),
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const res = await pullWideInbound({ cfg: CFG, agentId: ME, cursor: 899, io });
|
|
365
|
+
|
|
366
|
+
assert.equal(res.events.length, 1, "exactly one calendar item — the one I am in");
|
|
367
|
+
const ev = res.events[0];
|
|
368
|
+
assert.equal(ev.kind, "calendar");
|
|
369
|
+
assert.equal(ev.cohort.surface, "calendar");
|
|
370
|
+
assert.equal(ev.cohort.reason, "attendee", "attendance was proven by an ACL'd read, not by a payload");
|
|
371
|
+
assert.match(ev.text, /Calendar event created — "Transport review"/);
|
|
372
|
+
assert.match(ev.text, /Walk the five hops/);
|
|
373
|
+
assert.equal(res.stats.bySurface.calendar, 1);
|
|
374
|
+
assert.equal(res.stats.dropped.calendar_not_mine, 1, "the colleague's meeting was dropped with a named reason");
|
|
375
|
+
|
|
376
|
+
// ACL by call log: hq was asked ONE seat-scoped question, and we never probed
|
|
377
|
+
// the event we were not entitled to.
|
|
378
|
+
const calendarCalls = io.calls.filter((c) => c.method === "calendar.list");
|
|
379
|
+
assert.equal(calendarCalls.length, 1, "one list read per pull, not one probe per event");
|
|
380
|
+
assert.ok(
|
|
381
|
+
calendarCalls[0].params.calendarMemberId === undefined,
|
|
382
|
+
"no calendarMemberId → hq uses the ACTING SEAT's viewpoint; naming one would be asking about someone else's calendar",
|
|
383
|
+
);
|
|
384
|
+
assert.equal(io.calls.filter((c) => c.method === "calendar.get").length, 0, "no per-event probe at all");
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("calendar: an unreadable calendar fails CLOSED and says so", async () => {
|
|
388
|
+
const io = fakeIo({
|
|
389
|
+
"read:events": okRead({
|
|
390
|
+
events: [
|
|
391
|
+
{ seq: 910, family: "calendar", kind: "event.rescheduled", entity_id: "EV-mine", actor: BOSS,
|
|
392
|
+
at: "2026-08-11T10:00:00.000Z", payload: { title: "Transport review", to: "2026-08-13T10:00:00.000Z" } },
|
|
393
|
+
],
|
|
394
|
+
nextCursor: 910,
|
|
395
|
+
}),
|
|
396
|
+
// calendar.list not routed → the fake returns NOT_FOUND, i.e. a failed read.
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
const res = await pullWideInbound({ cfg: CFG, agentId: ME, cursor: 909, io });
|
|
400
|
+
assert.equal(res.events.length, 0, "a private surface guesses CLOSED, never open");
|
|
401
|
+
assert.equal(res.stats.dropped.calendar_unknown, 1);
|
|
402
|
+
assert.ok(res.stats.degraded.includes("calendar"), "and the degradation is reported, not swallowed");
|
|
403
|
+
assert.ok(
|
|
404
|
+
io.logs.some((l) => l.level === "warn" && /calendar\.list unreadable/.test(l.message)),
|
|
405
|
+
`fail-open is fine, silent is not; got ${JSON.stringify(io.logs)}`,
|
|
406
|
+
);
|
|
407
|
+
});
|
|
@@ -144,6 +144,14 @@ export function toMessageEvent(o = {}) {
|
|
|
144
144
|
family: c.family,
|
|
145
145
|
event_kind: c.kind,
|
|
146
146
|
entity_id: entityId || null,
|
|
147
|
+
// The subject this surface hangs off, when it is NOT a channel: the task
|
|
148
|
+
// an approval blocks or an escalation flags, the decision a comment sits
|
|
149
|
+
// on, the file a comment is against. `channel_id` above deliberately only
|
|
150
|
+
// ever carries a real Cohort channel — a reply routes with it — so
|
|
151
|
+
// without this the downstream ladder has the event and no handle on the
|
|
152
|
+
// thing the event is about. `effects.escalate` needs exactly this to
|
|
153
|
+
// satisfy hq's "an escalation must reference a task or a channel".
|
|
154
|
+
scope_id: s(c.ids.taskId || c.ids.decisionId || c.ids.fileId || "") || null,
|
|
147
155
|
seq: c.seq ?? null,
|
|
148
156
|
me: s(me),
|
|
149
157
|
},
|
|
@@ -127,6 +127,26 @@ export const SURFACES = Object.freeze({
|
|
|
127
127
|
scope: "channel",
|
|
128
128
|
default: true,
|
|
129
129
|
}),
|
|
130
|
+
/**
|
|
131
|
+
* A calendar event on MY calendar, or one I am an attendee of: created,
|
|
132
|
+
* updated, rescheduled, RSVP'd, deleted.
|
|
133
|
+
*
|
|
134
|
+
* ADDRESSING. The `calendar.*` payload is redacted like every other family —
|
|
135
|
+
* `event.created` carries `attendeeCount`, `invitesQueued` and the CALENDAR
|
|
136
|
+
* OWNER (`calendarMemberId`), never the attendee list. So ownership is the
|
|
137
|
+
* only thing the frame can prove, and attendance has to come from
|
|
138
|
+
* `calendar.list`, which is ACL'd to the acting seat: it returns events where
|
|
139
|
+
* `calendarMemberId = viewpoint` OR `attendeeRows.some(memberId = viewpoint)`,
|
|
140
|
+
* under hq's PERSONAL_RESTRICTED lens floor (a colleague's restricted event is
|
|
141
|
+
* ABSENT, never redacted). We never widen the payload to carry attendees.
|
|
142
|
+
*/
|
|
143
|
+
calendar: Object.freeze({
|
|
144
|
+
topic: "calendar",
|
|
145
|
+
kind: "calendar",
|
|
146
|
+
subject: "Cohort calendar",
|
|
147
|
+
scope: "channel",
|
|
148
|
+
default: true,
|
|
149
|
+
}),
|
|
130
150
|
/** A delegation offered to me, or one I offered being accepted/declined. */
|
|
131
151
|
handoff: Object.freeze({
|
|
132
152
|
topic: "handoff",
|
|
@@ -422,11 +422,25 @@ export const PARAM_CONTRACT = {
|
|
|
422
422
|
note: "hq resolves a member by SLUG, not id.",
|
|
423
423
|
},
|
|
424
424
|
"escalation.create": {
|
|
425
|
-
alias: { subject: "title", body: "detail", description: "detail" },
|
|
425
|
+
alias: { subject: "title", body: "detail", description: "detail", summary: "title" },
|
|
426
426
|
enums: { severity: ESCALATION_SEVERITIES },
|
|
427
427
|
required: ["title"],
|
|
428
428
|
oneOf: [["taskId", "channelId"]],
|
|
429
|
-
|
|
429
|
+
unsupported: ["waitingOnMemberId", "options", "context"],
|
|
430
|
+
note:
|
|
431
|
+
"hq requires a title AND a target (taskId or channelId). `summary` is aliased " +
|
|
432
|
+
"because lib/execution/effects.mjs sent exactly that and 400'd on every call; " +
|
|
433
|
+
"`waitingOnMemberId`/`options`/`context` are NOT fields of this method — an " +
|
|
434
|
+
"escalation carrying OPTIONS is an OpenQuestion and belongs at escalation.ask.",
|
|
435
|
+
},
|
|
436
|
+
"escalation.ask": {
|
|
437
|
+
// hq methods/escalation/ask.ts: `question` (<=2000) + >=2 DISTINCT options,
|
|
438
|
+
// `context` is a STRING (<=4000) — not an object — and `trigger` <=100.
|
|
439
|
+
// The two-option floor is enforced server-side and is the whole point: an
|
|
440
|
+
// escalation with one option is a notification, with none it is prose.
|
|
441
|
+
alias: { text: "question", prompt: "question", choices: "options" },
|
|
442
|
+
required: ["question", "options"],
|
|
443
|
+
note: "at least two distinct options, or hq 400s; `context` is a string, not an object.",
|
|
430
444
|
},
|
|
431
445
|
|
|
432
446
|
// ── leases ───────────────────────────────────────────────────────────────
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
1946d98145f4c1e56d80e58c37d9b54d5ea8d88c7d30251a694484418327dddf
|
package/lib/org/protocol.mjs
CHANGED
|
@@ -55,6 +55,27 @@ export const FAMILIES = Object.freeze([
|
|
|
55
55
|
// the Directory (2026-07): the canonical registry of external people and
|
|
56
56
|
// organisations every other desk resolves its counterparties through.
|
|
57
57
|
"directory",
|
|
58
|
+
// the sub-agent registry (2026-08): layer 1 of the SDK / workspace /
|
|
59
|
+
// agent-local resolution model — org-scoped, versioned sub-agent definitions
|
|
60
|
+
// pinned per seat. Governs the DISTRIBUTION and provenance of agent prose;
|
|
61
|
+
// resolution itself runs locally on the agent, never here.
|
|
62
|
+
"subagent",
|
|
63
|
+
// the agent's own control lane (2026-08): agent.wait, the bounded long-poll a
|
|
64
|
+
// daemon parks on instead of polling every 45s. Read-only — nothing in this
|
|
65
|
+
// family appends to the chain; it only READS the chain by seq.
|
|
66
|
+
"agent",
|
|
67
|
+
// the MANDATE spine (2026-08, SPEC "Three-Dimensional Agent Autonomy"): the
|
|
68
|
+
// objective tree, its KPI samples, the published per-seat mandate snapshot,
|
|
69
|
+
// and the agent's own capability/plan/drift reports. hq owns the MANDATE
|
|
70
|
+
// (outcomes, owners, targets, budgets, kill-switch); the agent owns the PLAN
|
|
71
|
+
// (obligations, schedule, rung) and compiles it locally. This family is the
|
|
72
|
+
// wire between the two halves.
|
|
73
|
+
"mandate",
|
|
74
|
+
// member-scoped PREFERENCES (2026-08, SPEC §9 reverse parity). hq has always
|
|
75
|
+
// injected these into responder context automatically; the family exists so a
|
|
76
|
+
// seat can PERSIST one it learned rather than re-deriving it every session.
|
|
77
|
+
// The chain rows carry subject + domain + key only — never the value.
|
|
78
|
+
"preference",
|
|
58
79
|
]);
|
|
59
80
|
|
|
60
81
|
/**
|
|
@@ -131,6 +152,17 @@ export const SCOPES = Object.freeze([
|
|
|
131
152
|
"directory.write", // Directory writes: create/update parties, channels, captures, links, lists. Structural verbs (merge execution, awareness change, archive) refuse an agent actor in-domain and create a review item instead
|
|
132
153
|
"design.read", // Design reads: the SAVED brand foundation snapshot (never a draft), voice, visible templates. Renders and voice rewrites are AUDITED side-effecting reads under this scope (files.exportRequest precedent)
|
|
133
154
|
"design.write", // Design writes: generate imagery, propose a foundation change. Foundation mutation itself is admin-only and human-gated -- a proposal creates a reviewable diff, it does not write
|
|
155
|
+
// mandate spine (2026-08). The read/propose/write split is the anti-Goodhart
|
|
156
|
+
// law expressed as scopes, and it deliberately mirrors approval.request vs
|
|
157
|
+
// approval.decide: an agent may READ its mandate and PROPOSE against it, but
|
|
158
|
+
// ADOPTING an objective (making it live, work-creating and graded) is a
|
|
159
|
+
// human/manager act the seat's own credential cannot perform.
|
|
160
|
+
"mandate.read", // read own mandate snapshot + version + KPI series
|
|
161
|
+
"mandate.propose", // propose/update objectives — `state:'proposed'` ONLY, never an adopted row
|
|
162
|
+
"mandate.write", // adopt / retire an objective. NOT in DEFAULT_AGENT_SCOPES — human/manager only
|
|
163
|
+
"kpi.write", // report a KPI sample against an objective. Honesty is enforced by the
|
|
164
|
+
// server-set `source` field, never by scope: a seat may always report,
|
|
165
|
+
// but an `llm`-sourced number is advisory and cannot create work.
|
|
134
166
|
"admin", // pairing approval, deactivate (kill switch), governance, policy, credential put/revoke
|
|
135
167
|
]);
|
|
136
168
|
|
|
@@ -148,6 +180,11 @@ export const DEFAULT_AGENT_SCOPES = Object.freeze([
|
|
|
148
180
|
"files.read", "files.write", "calendar.read", "calendar.write",
|
|
149
181
|
"crm.read", "crm.write", "books.read", "books.write",
|
|
150
182
|
"directory.read", "directory.write", "design.read", "design.write",
|
|
183
|
+
// mandate spine (2026-08): a seat reads its own mandate, proposes against it,
|
|
184
|
+
// and reports its own samples. `mandate.write` (adopt/retire) is POINTEDLY
|
|
185
|
+
// ABSENT — that is the whole anti-Goodhart law. A seat cannot define, measure
|
|
186
|
+
// AND be graded on the same number.
|
|
187
|
+
"mandate.read", "mandate.propose", "kpi.write",
|
|
151
188
|
]);
|
|
152
189
|
|
|
153
190
|
/** Knowledge group scopes: org-wide, per-unit, or an information-barrier cell (deny-override). */
|
|
@@ -240,6 +277,13 @@ export const METHODS = Object.freeze({
|
|
|
240
277
|
"messaging.react": { family: "messaging", scope: "messaging.write", sideEffecting: true },
|
|
241
278
|
"messaging.edit": { family: "messaging", scope: "messaging.write", sideEffecting: true },
|
|
242
279
|
"messaging.history": { family: "messaging", scope: "messaging.read", sideEffecting: false },
|
|
280
|
+
// --- messaging.search (2026-08 REVERSE PARITY, SPEC §9): the family carried
|
|
281
|
+
// thirteen methods and no search, so an SDK-driven seat could not answer
|
|
282
|
+
// "what did we decide about X anywhere?" while the hq responder in the
|
|
283
|
+
// same workspace could (its `search_messages` tool). Postgres FTS across
|
|
284
|
+
// every channel the paired member can read; the ACL floor is computed
|
|
285
|
+
// server-side, so `channelId`/`channelSlug` narrow and can never widen. ---
|
|
286
|
+
"messaging.search": { family: "messaging", scope: "messaging.read", sideEffecting: false },
|
|
243
287
|
// --- messaging.typing (2026-08): the "is composing…" indicator an agent raises
|
|
244
288
|
// while it works. The ONE write-less method in the family — it appends NO
|
|
245
289
|
// chain event and writes NO row, it publishes an EPHEMERAL frame that
|
|
@@ -357,6 +401,14 @@ export const METHODS = Object.freeze({
|
|
|
357
401
|
"escalation.create": { family: "escalation", scope: "org.write", sideEffecting: true, idempotent: true },
|
|
358
402
|
"escalation.resolve": { family: "escalation", scope: "org.write", sideEffecting: true },
|
|
359
403
|
"escalation.list": { family: "escalation", scope: "org.read", sideEffecting: false },
|
|
404
|
+
// `OpenQuestion`'s FIRST WRITERS. The model has had readers and a UI for
|
|
405
|
+
// months with no producer, so a charter `escalationTrigger` firing could only
|
|
406
|
+
// ever emit prose. `ask` requires >= 2 concrete options (a one-option question
|
|
407
|
+
// is a notification, not a decision); `answer` is deliberately NOT idempotent
|
|
408
|
+
// — a second answer is a CONFLICT so the first decision stands — and refuses
|
|
409
|
+
// the asker, the same law as `Approval.requester ≠ approver`.
|
|
410
|
+
"escalation.ask": { family: "escalation", scope: "org.write", sideEffecting: true },
|
|
411
|
+
"escalation.answer": { family: "escalation", scope: "org.write", sideEffecting: true },
|
|
360
412
|
// --- annotation ---
|
|
361
413
|
"annotation.create": { family: "annotation", scope: "org.write", sideEffecting: true, idempotent: true },
|
|
362
414
|
"annotation.comment": { family: "annotation", scope: "org.write", sideEffecting: true },
|
|
@@ -380,6 +432,26 @@ export const METHODS = Object.freeze({
|
|
|
380
432
|
"memory.proposeAmendment": { family: "memory", scope: "knowledge.write", sideEffecting: true },
|
|
381
433
|
"memory.mergeAmendment": { family: "memory", scope: "knowledge.write", sideEffecting: true },
|
|
382
434
|
"memory.list": { family: "memory", scope: "knowledge.read", sideEffecting: false },
|
|
435
|
+
// --- memory.recall (2026-08 REVERSE PARITY, SPEC §9): layered semantic recall
|
|
436
|
+
// over the unified agent memory, with the FULL contract the hq responder's
|
|
437
|
+
// `recall_memory` tool has had since the agent-memory program shipped —
|
|
438
|
+
// scope (org|self|channel) / refType+refId / time range / depth /
|
|
439
|
+
// drill-down pointers. `knowledge.search` is a narrowed projection of this
|
|
440
|
+
// (shared facts only, no controls), so until now an SDK seat reasoned off
|
|
441
|
+
// a strictly smaller memory than the chat lane in the same workspace.
|
|
442
|
+
// ACL is server-side off the paired member — fail-closed, never a param. ---
|
|
443
|
+
"memory.recall": { family: "memory", scope: "knowledge.read", sideEffecting: false },
|
|
444
|
+
// --- preference (2026-08 REVERSE PARITY, SPEC §9): there was NO preference.*
|
|
445
|
+
// family at all. An SDK agent could LEARN a durable member-scoped fact and
|
|
446
|
+
// had nowhere to put it, while the hq responder's `remember_preference`
|
|
447
|
+
// wrote straight into PreferenceMemory — which hq then injects into every
|
|
448
|
+
// future conversation. Same workspace, two memories. `upsert` converges on
|
|
449
|
+
// one row per (subject, domain, key) and REQUIRES evidence; the chain
|
|
450
|
+
// append carries whose/which-key and never the value (a preference value
|
|
451
|
+
// is content). `list` ships with it because a write-only family is not
|
|
452
|
+
// parity: hq SEES injected preferences for free, a seat has to ask. ---
|
|
453
|
+
"preference.upsert": { family: "preference", scope: "org.write", sideEffecting: true, idempotent: true },
|
|
454
|
+
"preference.list": { family: "preference", scope: "org.read", sideEffecting: false },
|
|
383
455
|
// --- compact ---
|
|
384
456
|
"compact.upsert": { family: "compact", scope: "org.write", sideEffecting: true, idempotent: true },
|
|
385
457
|
// --- notification ---
|
|
@@ -775,8 +847,124 @@ export const METHODS = Object.freeze({
|
|
|
775
847
|
"files.comments": { family: "files", scope: "files.read", sideEffecting: false },
|
|
776
848
|
"files.sheetFedRange": { family: "files", scope: "files.write", sideEffecting: true, idempotent: true },
|
|
777
849
|
"files.ask": { family: "files", scope: "files.read", sideEffecting: false },
|
|
850
|
+
// --- subagent (2026-08): the org-scoped sub-agent registry. NO NEW SCOPES —
|
|
851
|
+
// a sub-agent definition is org-structural authored content exactly like
|
|
852
|
+
// a persona, an SOP or an annotation, which is what `org.write` covers,
|
|
853
|
+
// and both org.read/org.write are already in DEFAULT_AGENT_SCOPES.
|
|
854
|
+
// Curation (`yank`) is admin, like every other destructive verb.
|
|
855
|
+
//
|
|
856
|
+
// `resolve` is the EXPENSIVE read (bodies + provenance + rewire hints);
|
|
857
|
+
// the cheap poll is the `subagent.roster` GET in READS, which returns
|
|
858
|
+
// {slug, definitionId, version, contentHash} and no bodies. Naming a
|
|
859
|
+
// memberId other than the caller's own seat requires `admin` — enforced
|
|
860
|
+
// in-domain (methods/subagent/_shared.ts), the invokeTool doctrine. ---
|
|
861
|
+
"subagent.list": { family: "subagent", scope: "org.read", sideEffecting: false },
|
|
862
|
+
"subagent.get": { family: "subagent", scope: "org.read", sideEffecting: false },
|
|
863
|
+
"subagent.resolve": { family: "subagent", scope: "org.read", sideEffecting: false },
|
|
864
|
+
"subagent.create": { family: "subagent", scope: "org.write", sideEffecting: true, idempotent: true },
|
|
865
|
+
"subagent.publishVersion": { family: "subagent", scope: "org.write", sideEffecting: true, idempotent: true },
|
|
866
|
+
"subagent.fork": { family: "subagent", scope: "org.write", sideEffecting: true, idempotent: true },
|
|
867
|
+
"subagent.pin": { family: "subagent", scope: "org.write", sideEffecting: true, idempotent: true },
|
|
868
|
+
"subagent.unpin": { family: "subagent", scope: "org.write", sideEffecting: true },
|
|
869
|
+
"subagent.yank": { family: "subagent", scope: "admin", sideEffecting: true },
|
|
870
|
+
// --- agent control lane (2026-08): the bounded long-poll a daemon parks on so
|
|
871
|
+
// it learns about its own work in ~0s instead of on a 45s poll cadence.
|
|
872
|
+
// Modelled on the `approval.wait` precedent (READS, a held request), but
|
|
873
|
+
// registered here as a POST method because it takes a CURSOR + a page of
|
|
874
|
+
// structured results, which is the METHODS shape — and because a held GET
|
|
875
|
+
// is far more likely to be cached/coalesced by an intermediary.
|
|
876
|
+
//
|
|
877
|
+
// NO NEW SCOPE. `org.read` is the floor to call it at all (it is in
|
|
878
|
+
// DEFAULT_AGENT_SCOPES, so every paired agent can park), and each LANE it
|
|
879
|
+
// reports on is gated separately in-domain against the key's own scopes
|
|
880
|
+
// (messaging.read / board.read / email.read) — a key minted without
|
|
881
|
+
// email.read is never told that mail arrived. sideEffecting:false: it
|
|
882
|
+
// appends NOTHING to the chain, it only reads the chain by seq. ---
|
|
883
|
+
"agent.wait": { family: "agent", scope: "org.read", sideEffecting: false },
|
|
884
|
+
// --- mandate (2026-08): the MANDATE half of the mandate/plan split. hq is
|
|
885
|
+
// authoritative for WHAT the outcomes are; the agent compiles the plan.
|
|
886
|
+
//
|
|
887
|
+
// The scope split is the design: `mandate.read`/`mandate.propose`/
|
|
888
|
+
// `kpi.write` are in DEFAULT_AGENT_SCOPES, `mandate.write` is not.
|
|
889
|
+
// `adopt`/`retire` are ALSO governanceGated — the decision-rights matrix
|
|
890
|
+
// must exist before a seat can be handed a live, graded objective, the
|
|
891
|
+
// same ordering rule that gates board.assign and handoff.*.
|
|
892
|
+
//
|
|
893
|
+
// `declareCapability`/`publishPlan`/`reportDrift` reuse `registry.write`:
|
|
894
|
+
// they are a seat reporting facts about ITSELF, exactly like
|
|
895
|
+
// registry.register, and every paired agent already holds it. ---
|
|
896
|
+
"mandate.get": { family: "mandate", scope: "mandate.read", sideEffecting: false },
|
|
897
|
+
"mandate.version": { family: "mandate", scope: "mandate.read", sideEffecting: false },
|
|
898
|
+
"mandate.tree": { family: "mandate", scope: "org.read", sideEffecting: false },
|
|
899
|
+
"mandate.series": { family: "mandate", scope: "mandate.read", sideEffecting: false },
|
|
900
|
+
"mandate.propose": { family: "mandate", scope: "mandate.propose", sideEffecting: true, idempotent: true },
|
|
901
|
+
"mandate.adopt": { family: "mandate", scope: "mandate.write", sideEffecting: true, governanceGated: true },
|
|
902
|
+
"mandate.retire": { family: "mandate", scope: "mandate.write", sideEffecting: true, governanceGated: true },
|
|
903
|
+
"mandate.sample": { family: "mandate", scope: "kpi.write", sideEffecting: true, idempotent: true },
|
|
904
|
+
"mandate.declareCapability": { family: "mandate", scope: "registry.write", sideEffecting: true, idempotent: true },
|
|
905
|
+
"mandate.publishPlan": { family: "mandate", scope: "registry.write", sideEffecting: true, idempotent: true },
|
|
906
|
+
"mandate.reportDrift": { family: "mandate", scope: "registry.write", sideEffecting: true },
|
|
907
|
+
// --- board review lane (2026-08 consensus fix): `Task.reviewerId` has existed
|
|
908
|
+
// since T19 and had NO writer on the agent plane, so "send it for review"
|
|
909
|
+
// was unreachable from a daemon and every review round-trip degraded to a
|
|
910
|
+
// comment. These two methods are the writer. ---
|
|
911
|
+
"board.requestReview": { family: "board", scope: "board.write", sideEffecting: true },
|
|
912
|
+
"board.resolveReview": { family: "board", scope: "board.write", sideEffecting: true },
|
|
913
|
+
// --- escalation as a QUESTION (2026-08): `OpenQuestion` shipped with readers
|
|
914
|
+
// and no writer at all. `escalation.ask` is its first one. The difference
|
|
915
|
+
// from `escalation.create` matters: an Escalation is "a human is needed
|
|
916
|
+
// here"; an OpenQuestion is "here are 2-3 concrete options, pick one",
|
|
917
|
+
// which is what a Charter.escalationTriggers hit should actually produce. ---
|
|
918
|
+
// NOTE: `escalation.ask` / `escalation.answer` (raise an OpenQuestion with
|
|
919
|
+
// concrete options, and the human picking one) are deliberately NOT registered
|
|
920
|
+
// yet — `ask` needs a `_question` projector that is not written. A METHODS
|
|
921
|
+
// entry with no working handler is not a placeholder, it is a live promise the
|
|
922
|
+
// dispatcher cannot keep: the protocol advertises the method, the caller gets
|
|
923
|
+
// through scope resolution, and the call 500s. Register each in the same
|
|
924
|
+
// change that lands its handler.
|
|
925
|
+
// NOTE: `escalation.answer` (the human picking one of the offered options) is
|
|
926
|
+
// deliberately NOT registered yet — its handler is not written. A METHODS
|
|
927
|
+
// entry with no handler file is not a placeholder, it is a live promise the
|
|
928
|
+
// dispatcher cannot keep: the protocol advertises the method, callers get
|
|
929
|
+
// through scope resolution, and the call 500s. Register it in the same change
|
|
930
|
+
// that adds src/server/methods/escalation/answer.ts.
|
|
778
931
|
});
|
|
779
932
|
|
|
933
|
+
/**
|
|
934
|
+
* The ONE action-class table (SPEC §8 "Approvals"). Replaces maestro's local
|
|
935
|
+
* `GATED_CLASSES` and hq's free-form `Approval.actionClass` strings with a
|
|
936
|
+
* shared vocabulary both sides validate against.
|
|
937
|
+
*
|
|
938
|
+
* `GATED_ACTION_CLASSES` is the blast-radius gate: an action in one of these
|
|
939
|
+
* classes forces an approval BEFORE any execution rung runs, at every rung.
|
|
940
|
+
* @type {readonly string[]}
|
|
941
|
+
*/
|
|
942
|
+
export const ACTION_CLASSES = Object.freeze([
|
|
943
|
+
"internal", // stays inside the workspace; reversible; no spend
|
|
944
|
+
"external", // leaves the org (email to a third party, a public post)
|
|
945
|
+
"irreversible", // cannot be undone by a subsequent method call
|
|
946
|
+
"financial", // moves money or commits spend
|
|
947
|
+
]);
|
|
948
|
+
|
|
949
|
+
/** Action classes that force a human approval before execution. */
|
|
950
|
+
export const GATED_ACTION_CLASSES = Object.freeze(["external", "irreversible", "financial"]);
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* PlanDrift kinds — the reconciler's vocabulary for "the plan and the world
|
|
954
|
+
* disagree". Shared so hq can index/report on them without string drift.
|
|
955
|
+
* @type {readonly string[]}
|
|
956
|
+
*/
|
|
957
|
+
export const DRIFT_KINDS = Object.freeze([
|
|
958
|
+
"uncovered_event", // a directed event matched no REACT obligation
|
|
959
|
+
"unreachable_capability", // an obligation cites a capability that failed its probe
|
|
960
|
+
"stale_sensor", // no sample within 2x the objective's cadence
|
|
961
|
+
"schedule_missing", // a SCHEDULE obligation has no launchd trigger on disk
|
|
962
|
+
"kpi_gap", // measured value is outside tolerance of target
|
|
963
|
+
"budget_breach", // an obligation exhausted its envelope
|
|
964
|
+
"orphan_work", // open work with no objective behind it
|
|
965
|
+
"mandate_stale", // the cached mandate aged past the staleness ladder
|
|
966
|
+
]);
|
|
967
|
+
|
|
780
968
|
/**
|
|
781
969
|
* Is a method refused until the org server is `governance_ready` (decision_rights
|
|
782
970
|
* populated AND signing live)? A partially deployed control plane is worse than
|
|
@@ -806,6 +994,12 @@ export const READS = Object.freeze({
|
|
|
806
994
|
// phase 4 reads
|
|
807
995
|
"contacts.list": "knowledge.read", // org contacts registry (ACL-filtered)
|
|
808
996
|
"meetings.list": "knowledge.read", // org meetings registry (ACL-filtered)
|
|
997
|
+
// sub-agent registry (2026-08): the CHEAP roster poll. Returns the bare
|
|
998
|
+
// {rosterVersion, entries:[{slug, definitionId, version, contentHash}]} — no
|
|
999
|
+
// bodies — so a seat can diff its `.maestro/subagents.lock.json` on every
|
|
1000
|
+
// beat without paying for prose. `subagent.resolve` (METHODS) is the
|
|
1001
|
+
// expensive twin that carries bodies.
|
|
1002
|
+
"subagent.roster": "org.read",
|
|
809
1003
|
});
|
|
810
1004
|
|
|
811
1005
|
/** Error codes (the error contract; HTTP status hints are advisory). */
|
|
@@ -822,8 +1016,26 @@ export const ERROR_CODES = Object.freeze({
|
|
|
822
1016
|
INTERNAL: { http: 500 },
|
|
823
1017
|
});
|
|
824
1018
|
|
|
825
|
-
/**
|
|
826
|
-
|
|
1019
|
+
/**
|
|
1020
|
+
* Directive kinds the presence.beat response may carry (kill-switch + nudges).
|
|
1021
|
+
*
|
|
1022
|
+
* Levels 1-3 of the four-level kill-switch (SPEC §8) live here and are
|
|
1023
|
+
* COOPERATIVE: they only work while the daemon is healthy enough to beat and
|
|
1024
|
+
* honest enough to obey. Level 4 — `credential.revoke` + API-key disable — is
|
|
1025
|
+
* the only control that works against a wedged or compromised laptop. Say that
|
|
1026
|
+
* out loud rather than implying the beat is a hard stop.
|
|
1027
|
+
*/
|
|
1028
|
+
export const DIRECTIVES = Object.freeze([
|
|
1029
|
+
"halt", // stop entirely. hq ALSO closes the SSE lane (`event: bye`) so a
|
|
1030
|
+
// daemon that stopped beating stops receiving work to do.
|
|
1031
|
+
"rotate_token",
|
|
1032
|
+
"policy_stale",
|
|
1033
|
+
"resync",
|
|
1034
|
+
// --- mandate spine (2026-08) ---
|
|
1035
|
+
"pause_schedules", // stop INITIATING (Loop B + Loop C); keep answering when addressed
|
|
1036
|
+
"plan_stale", // the seat's mandate moved — refetch `mandate.get` and recompile
|
|
1037
|
+
"revoke_scope", // a scope was withdrawn; drop it locally and re-probe capabilities
|
|
1038
|
+
]);
|
|
827
1039
|
|
|
828
1040
|
// ---------------------------------------------------------------------------
|
|
829
1041
|
// Helpers (shared, pure)
|
|
@@ -131,7 +131,10 @@ test("protocol: frozen family + method counts (additive evolution guard)", () =>
|
|
|
131
131
|
// from the UI-parity "file" pins/comments family), and directory (external
|
|
132
132
|
// people + organisations; the Design half of that program rides the existing
|
|
133
133
|
// `branding` family — the section is renamed, the wire name is not).
|
|
134
|
-
|
|
134
|
+
// The 2026-08 protocol-convergence pass adds 4: subagent (the org-scoped
|
|
135
|
+
// sub-agent registry), agent (the agent.wait control lane), mandate (the
|
|
136
|
+
// MANDATE spine) and preference (member-scoped preferences) → 51.
|
|
137
|
+
assert.equal(FAMILIES.length, 51, "family count");
|
|
135
138
|
// 357 = the mesh-protocol + agent UI-parity + calling + branding + email
|
|
136
139
|
// methods, plus the Live Integrations surface: integration.toolsetVersion +
|
|
137
140
|
// integration.listAgentTools (SP1) and integration.invokeTool (SP5, execute a
|
|
@@ -160,8 +163,14 @@ test("protocol: frozen family + method counts (additive evolution guard)", () =>
|
|
|
160
163
|
// "is composing…" indicator an SDK-driven agent raises while it works —
|
|
161
164
|
// sideEffecting:false, no chain event, expires client-side) → 431.
|
|
162
165
|
// Bumped deliberately alongside
|
|
166
|
+
// The 2026-08 protocol-convergence pass closes the 29-method gap against hq's
|
|
167
|
+
// vendored table (the client rejected every one of them BEFORE the network,
|
|
168
|
+
// so mandate.* et al were unreachable from a daemon): messaging.search,
|
|
169
|
+
// escalation.ask/answer, memory.recall, preference.upsert/list (6 reverse-
|
|
170
|
+
// parity methods), the 9 subagent.* registry methods, agent.wait, the 11
|
|
171
|
+
// mandate.* spine methods, and board.requestReview/resolveReview → 460.
|
|
163
172
|
// the descriptors; the checksum (read at runtime) is the primary drift guard.
|
|
164
|
-
assert.equal(Object.keys(METHODS).length,
|
|
173
|
+
assert.equal(Object.keys(METHODS).length, 460, "method count");
|
|
165
174
|
});
|
|
166
175
|
|
|
167
176
|
test("protocol SP3: messaging + calling families/methods/scopes", async () => {
|