@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.
Files changed (148) hide show
  1. package/framework-features.json +30 -0
  2. package/lib/backlog.mjs +136 -0
  3. package/lib/cadences.mjs +63 -2
  4. package/lib/cadences.test.mjs +105 -0
  5. package/lib/capability/inventory.mjs +542 -0
  6. package/lib/capability/inventory.test.mjs +232 -0
  7. package/lib/capability/probe.mjs +255 -0
  8. package/lib/channels/contract.mjs +37 -1
  9. package/lib/channels/contract.test.mjs +25 -1
  10. package/lib/claude-bin.mjs +37 -3
  11. package/lib/claude-bin.test.mjs +42 -8
  12. package/lib/execution/disposition.mjs +501 -0
  13. package/lib/execution/disposition.test.mjs +482 -0
  14. package/lib/execution/drive.mjs +352 -0
  15. package/lib/execution/drive.test.mjs +270 -0
  16. package/lib/execution/effects.mjs +340 -0
  17. package/lib/execution/effects.test.mjs +193 -0
  18. package/lib/execution/index.mjs +152 -0
  19. package/lib/execution/intake.mjs +581 -0
  20. package/lib/execution/intake.test.mjs +343 -0
  21. package/lib/execution/journal.mjs +374 -0
  22. package/lib/execution/journal.test.mjs +261 -0
  23. package/lib/execution/match.mjs +331 -0
  24. package/lib/execution/match.test.mjs +235 -0
  25. package/lib/execution/pipeline.mjs +341 -0
  26. package/lib/execution/pipeline.test.mjs +389 -0
  27. package/lib/execution/route.mjs +332 -0
  28. package/lib/execution/route.test.mjs +186 -0
  29. package/lib/execution/surface-policy.mjs +446 -0
  30. package/lib/execution/surface-policy.test.mjs +162 -0
  31. package/lib/goals/admission.mjs +209 -0
  32. package/lib/goals/admission.test.mjs +139 -0
  33. package/lib/goals/classify.mjs +206 -0
  34. package/lib/goals/classify.test.mjs +109 -0
  35. package/lib/goals/collaborate.mjs +415 -0
  36. package/lib/goals/collaborate.test.mjs +324 -0
  37. package/lib/goals/gaps.mjs +111 -0
  38. package/lib/goals/gaps.test.mjs +284 -0
  39. package/lib/goals/loop.mjs +537 -0
  40. package/lib/goals/loop.test.mjs +719 -0
  41. package/lib/identity/persona.mjs +247 -0
  42. package/lib/identity/persona.test.mjs +117 -0
  43. package/lib/kpi.mjs +469 -0
  44. package/lib/kpi.test.mjs +244 -0
  45. package/lib/mandate/audit.mjs +168 -0
  46. package/lib/mandate/audit.test.mjs +195 -0
  47. package/lib/mandate/cache.mjs +162 -0
  48. package/lib/mandate/derive.mjs +317 -0
  49. package/lib/mandate/derive.test.mjs +224 -0
  50. package/lib/mandate/model.mjs +352 -0
  51. package/lib/mandate/model.test.mjs +145 -0
  52. package/lib/mandate/refresh.mjs +187 -0
  53. package/lib/mandate/refresh.test.mjs +293 -0
  54. package/lib/mcp/server.test.mjs +4 -4
  55. package/lib/org/approvals.mjs +14 -2
  56. package/lib/org/client.mjs +58 -22
  57. package/lib/org/client.test.mjs +3 -1
  58. package/lib/org/inbound/directedness.mjs +720 -0
  59. package/lib/org/inbound/directedness.test.mjs +543 -0
  60. package/lib/org/inbound/facts.mjs +501 -0
  61. package/lib/org/inbound/facts.test.mjs +375 -0
  62. package/lib/org/inbound/hydrate.mjs +535 -0
  63. package/lib/org/inbound/hydrate.test.mjs +326 -0
  64. package/lib/org/inbound/index.mjs +233 -0
  65. package/lib/org/inbound/index.test.mjs +324 -0
  66. package/lib/org/inbound/io.mjs +141 -0
  67. package/lib/org/inbound/project.mjs +201 -0
  68. package/lib/org/inbound/project.test.mjs +287 -0
  69. package/lib/org/inbound/surfaces.mjs +257 -0
  70. package/lib/org/knowledge.mjs +10 -1
  71. package/lib/org/knowledge.test.mjs +8 -1
  72. package/lib/org/leases.mjs +5 -0
  73. package/lib/org/mesh.mjs +17 -2
  74. package/lib/org/messaging.mjs +40 -4
  75. package/lib/org/messaging.test.mjs +40 -0
  76. package/lib/org/param-contract.mjs +694 -0
  77. package/lib/org/param-contract.test.mjs +451 -0
  78. package/lib/org/protocol.checksum +1 -1
  79. package/lib/org/protocol.mjs +8 -0
  80. package/lib/org/protocol.test.mjs +5 -1
  81. package/lib/org/push.mjs +1025 -0
  82. package/lib/org/push.test.mjs +690 -0
  83. package/lib/org/tool-surface.mjs +138 -38
  84. package/lib/org/tool-surface.test.mjs +13 -8
  85. package/lib/org/typing.mjs +341 -0
  86. package/lib/org/typing.test.mjs +291 -0
  87. package/lib/plan/compile.mjs +510 -0
  88. package/lib/plan/compile.test.mjs +286 -0
  89. package/lib/plan/emit.mjs +256 -0
  90. package/lib/plan/emit.test.mjs +246 -0
  91. package/lib/plan/explain.mjs +226 -0
  92. package/lib/plan/explain.test.mjs +188 -0
  93. package/lib/plan/schema.mjs +140 -0
  94. package/lib/resource-governor.mjs +47 -1
  95. package/lib/resource-governor.test.mjs +21 -1
  96. package/lib/setup/enroll-from-cohort.mjs +84 -16
  97. package/lib/setup/enroll-from-cohort.test.mjs +43 -1
  98. package/lib/setup/sections/identity.mjs +15 -4
  99. package/lib/setup/sections/identity.test.mjs +94 -0
  100. package/lib/setup/sections/inventory.mjs +178 -0
  101. package/lib/setup/sections/inventory.test.mjs +198 -0
  102. package/lib/setup/sections/mandate.mjs +392 -0
  103. package/lib/setup/sections/mandate.test.mjs +373 -0
  104. package/lib/setup/sections/subagents.mjs +427 -0
  105. package/lib/setup/sections/subagents.test.mjs +429 -0
  106. package/lib/setup/sections/verify.mjs +121 -0
  107. package/lib/setup/sections/verify.test.mjs +175 -0
  108. package/lib/setup/sot.mjs +2 -0
  109. package/lib/subagents/cli.mjs +463 -0
  110. package/lib/subagents/cli.test.mjs +389 -0
  111. package/lib/subagents/client.mjs +373 -0
  112. package/lib/subagents/client.test.mjs +309 -0
  113. package/lib/subagents/gap.mjs +268 -0
  114. package/lib/subagents/gap.test.mjs +234 -0
  115. package/lib/subagents/lock.mjs +296 -0
  116. package/lib/subagents/lock.test.mjs +248 -0
  117. package/lib/subagents/manifest.mjs +224 -0
  118. package/lib/subagents/manifest.test.mjs +175 -0
  119. package/lib/subagents/refs.mjs +274 -0
  120. package/lib/subagents/refs.test.mjs +204 -0
  121. package/lib/subagents/resolve.mjs +455 -0
  122. package/lib/subagents/resolve.test.mjs +422 -0
  123. package/lib/subagents/schema.mjs +467 -0
  124. package/lib/subagents/schema.test.mjs +306 -0
  125. package/package.json +8 -3
  126. package/plugins/maestro-skills/.claude-plugin/marketplace.json +16 -0
  127. package/policies/ai-disclosure.yaml +42 -2
  128. package/scaffold/CLAUDE.md +16 -2
  129. package/schedules/triggers/goal-steward.md +79 -0
  130. package/scripts/ci/conformance-org-api.mjs +792 -0
  131. package/scripts/ci/conformance-org-api.test.mjs +417 -0
  132. package/scripts/daemon/agent-daemon.mjs +36 -4
  133. package/scripts/daemon/cadence-handlers.mjs +145 -1
  134. package/scripts/daemon/goal-steward-cadence.test.mjs +243 -0
  135. package/scripts/daemon/inbox-deferral.mjs +45 -2
  136. package/scripts/daemon/inbox-deferral.test.mjs +56 -0
  137. package/scripts/daemon/inbox-wake.mjs +282 -0
  138. package/scripts/daemon/inbox-wake.test.mjs +199 -0
  139. package/scripts/daemon/prompt-builder.mjs +41 -1
  140. package/scripts/daemon/typing-registry.mjs +55 -2
  141. package/scripts/daemon/typing-registry.test.mjs +25 -0
  142. package/scripts/local-triggers/generate-plists.test.mjs +5 -5
  143. package/scripts/setup/gen-subagent-manifest.mjs +95 -0
  144. package/scripts/setup/gen-subagent-manifest.test.mjs +124 -0
  145. package/scripts/setup/generate-plan.mjs +108 -0
  146. package/scripts/setup/init-capability-manifest.mjs +70 -0
  147. package/scripts/setup/init-skill-marketplace.mjs +155 -0
  148. package/scripts/setup/init-skill-marketplace.test.mjs +193 -0
@@ -0,0 +1,415 @@
1
+ /**
2
+ * lib/goals/collaborate.mjs — route a classified candidate through the
3
+ * collaboration primitives that ALREADY EXIST.
4
+ *
5
+ * There is no new coordination mechanism anywhere in this file. Every
6
+ * disposition lands on machinery that is already wired end to end:
7
+ *
8
+ * SELF → a board work item (`board.create`) + the local queue.
9
+ * COLLABORATE → a `work-claim` LEASE (lib/org/leases.mjs, 3s fail-open) so
10
+ * two agents cannot both drive the same objective work, then
11
+ * the board item + a comment naming the contributor.
12
+ * HANDOFF → a handoff ENVELOPE (lib/org/handoff.sendHandoff →
13
+ * `handoff.offer`), which rides the human channels, so every
14
+ * delegation is visible to the responsible humans.
15
+ * NEEDS_REVIEW → `approval.request` (lib/org/approvals.mjs) when the action
16
+ * class is gated — the one human-decision path that is
17
+ * already well-wired end to end — otherwise a REVIEW handoff
18
+ * to the named reviewer.
19
+ * (escalate) → `escalation.create`, the existing method, when the decision
20
+ * table says "ask, don't pile on more tasks".
21
+ *
22
+ * Where the spec anticipates a method hq has not shipped yet
23
+ * (`board.requestReview`, `escalation.ask`), this module PREFERS it when the
24
+ * vendored protocol carries it and otherwise falls back to the existing
25
+ * primitive **with a logged degradation**. It never silently does nothing.
26
+ *
27
+ * Fail-open, never silent: an unreachable server produces
28
+ * `{ok:false, reason}` plus a log line, and the caller still writes the local
29
+ * backlog item — the work survives the partition, the collaboration is retried.
30
+ *
31
+ * @module lib/goals/collaborate
32
+ */
33
+
34
+ "use strict";
35
+
36
+ import { createHash } from "node:crypto";
37
+
38
+ import { methodDef } from "../org/protocol.mjs";
39
+
40
+ /** The lease scope self-directed work claims. One of the four LEASE_SCOPES. */
41
+ export const WORK_LEASE_SCOPE = "work-claim";
42
+
43
+ function logOf(deps) {
44
+ return deps && typeof deps.log === "function" ? deps.log : () => {};
45
+ }
46
+
47
+ /** Stable resource id for an objective's work lease. Pure + deterministic. */
48
+ export function workResourceId(objectiveKey, title) {
49
+ const h = createHash("sha1").update(`${objectiveKey}|${title}`).digest("hex").slice(0, 10);
50
+ return `objective:${objectiveKey}:${h}`;
51
+ }
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Lazily-resolved defaults (so a test can inject everything and touch no I/O)
55
+ // ---------------------------------------------------------------------------
56
+
57
+ async function defaultCreateItem(item, deps) {
58
+ const { createItem } = await import("../org/board.mjs");
59
+ return createItem(item, { cfg: deps.cfg, base: deps.base, token: deps.token, fetchImpl: deps.fetchImpl });
60
+ }
61
+
62
+ async function defaultSendHandoff(o, deps) {
63
+ const { sendHandoff } = await import("../org/handoff.mjs");
64
+ return sendHandoff({ ...o, cfg: deps.cfg, fetchImpl: deps.fetchImpl, agentRoot: deps.agentRoot, directory: deps.directory });
65
+ }
66
+
67
+ async function defaultRequestApproval(o, deps) {
68
+ const { requestApproval } = await import("../org/approvals.mjs");
69
+ return requestApproval({ ...o, cfg: deps.cfg, fetchImpl: deps.fetchImpl, agentRoot: deps.agentRoot });
70
+ }
71
+
72
+ async function defaultClaimLease(scope, resourceId, opts) {
73
+ const { claimLease } = await import("../org/leases.mjs");
74
+ return claimLease(scope, resourceId, opts);
75
+ }
76
+
77
+ async function defaultCall(method, params, deps) {
78
+ const { call } = await import("../org/client.mjs");
79
+ return call(method, params, { base: deps.base, token: deps.token, orgId: deps.orgId, fetchImpl: deps.fetchImpl });
80
+ }
81
+
82
+ function pick(deps, name, fallback) {
83
+ return deps && typeof deps[name] === "function" ? deps[name] : fallback;
84
+ }
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // The board item — every disposition creates one, so the work is visible
88
+ // ---------------------------------------------------------------------------
89
+
90
+ /**
91
+ * Create the org board item that carries the work, stamped with the full
92
+ * provenance chain (`why`). Fail-open: an unreachable org returns
93
+ * `{ok:false, itemId:null, reason}` and the caller still writes locally.
94
+ *
95
+ * @param {object} item - { id, title, objectiveKey, obligationKey, why, priority, assignee? }
96
+ * @param {object} deps
97
+ * @returns {Promise<{ok:boolean, itemId:string|null, reason:string, frame?:object}>}
98
+ */
99
+ export async function createWorkItem(item, deps = {}) {
100
+ const create = pick(deps, "createItem", (i) => defaultCreateItem(i, deps));
101
+ const payload = {
102
+ id: item.id,
103
+ title: item.title,
104
+ status: "open",
105
+ priority: item.priority || "normal",
106
+ assignee: item.assignee || deps.memberId || undefined,
107
+ // `why` is mandatory for machine-created work: {reason, objectiveKey,
108
+ // obligationKey, clause, expectedDelta}. A task a human cannot trace is a
109
+ // task a human cannot trust.
110
+ why: item.why || null,
111
+ objectiveKey: item.objectiveKey || null,
112
+ obligationKey: item.obligationKey || null,
113
+ };
114
+ let frame;
115
+ try {
116
+ frame = await create(payload);
117
+ } catch (err) {
118
+ logOf(deps)("warn", `[goals] board.create threw for "${item.title}": ${err && err.message ? err.message : err}`);
119
+ return { ok: false, itemId: null, reason: "board-create-threw" };
120
+ }
121
+ const ok = !!(frame && frame.ok);
122
+ if (!ok) {
123
+ logOf(deps)(
124
+ "warn",
125
+ `[goals] board.create failed for "${item.title}": ${(frame && frame.error && frame.error.code) || "unknown"} — the local backlog item still stands`
126
+ );
127
+ }
128
+ return {
129
+ ok,
130
+ itemId: ok ? (frame.result && (frame.result.id || frame.result.itemId)) || item.id : null,
131
+ reason: ok ? "created" : (frame && frame.error && frame.error.code) || "board-create-failed",
132
+ frame,
133
+ };
134
+ }
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // Per-disposition routers
138
+ // ---------------------------------------------------------------------------
139
+
140
+ /** COLLABORATE — claim the work lease before the item exists, so two agents racing the same objective resolve to one owner. */
141
+ export async function routeCollaborate(item, classification, deps = {}) {
142
+ const claim = pick(deps, "claimLease", (s, r, o) => defaultClaimLease(s, r, o));
143
+ const resourceId = workResourceId(item.objectiveKey, item.title);
144
+ let lease;
145
+ try {
146
+ lease = await claim(WORK_LEASE_SCOPE, resourceId, {
147
+ holder: deps.memberId || undefined,
148
+ base: deps.base,
149
+ token: deps.token,
150
+ fetchImpl: deps.fetchImpl,
151
+ });
152
+ } catch (err) {
153
+ logOf(deps)("warn", `[goals] work-claim lease threw for ${resourceId}: ${err && err.message ? err.message : err}`);
154
+ lease = { held: true, failedOpen: true, reason: "lease-threw-fail-open" };
155
+ }
156
+ if (!lease.held) {
157
+ logOf(deps)("info", `[goals] standing down on "${item.title}" — a peer holds the work-claim lease (${lease.reason})`);
158
+ return { routed: "COLLABORATE", primitive: "lease", ok: false, reason: `lease-lost:${lease.reason}`, resourceId, item: null };
159
+ }
160
+ if (lease.failedOpen) {
161
+ logOf(deps)("warn", `[goals] work-claim lease for ${resourceId} FAILED OPEN (${lease.reason}) — proceeding, duplicate work is possible`);
162
+ }
163
+ const created = await createWorkItem(item, deps);
164
+ // Tell the contributor, over the existing board comment method.
165
+ let notified = null;
166
+ if (created.ok && classification.contributors.length > 0) {
167
+ const callImpl = pick(deps, "callImpl", (m, p) => defaultCall(m, p, deps));
168
+ try {
169
+ notified = await callImpl("board.comment", {
170
+ itemId: created.itemId,
171
+ body: `Working "${item.title}" toward ${item.objectiveKey}. Contributors: ${classification.contributors.join(", ")}.`,
172
+ mentionedMemberIds: classification.contributors,
173
+ });
174
+ } catch (err) {
175
+ logOf(deps)("warn", `[goals] could not comment the collaborator note: ${err && err.message ? err.message : err}`);
176
+ }
177
+ }
178
+ return {
179
+ routed: "COLLABORATE",
180
+ primitive: "lease+board",
181
+ ok: created.ok,
182
+ reason: created.reason,
183
+ resourceId,
184
+ leaseToken: lease.leaseToken || null,
185
+ failedOpen: !!lease.failedOpen,
186
+ itemId: created.itemId,
187
+ notified: !!(notified && notified.ok),
188
+ };
189
+ }
190
+
191
+ /** HANDOFF — the item first (a Cohort handoff must reference a work item), then the envelope. */
192
+ export async function routeHandoff(item, classification, deps = {}) {
193
+ const created = await createWorkItem({ ...item, assignee: classification.target }, deps);
194
+ const send = pick(deps, "sendHandoff", (o) => defaultSendHandoff(o, deps));
195
+ let res;
196
+ try {
197
+ res = await send({
198
+ from: deps.memberId || "",
199
+ to: classification.target,
200
+ intent: item.title,
201
+ itemId: created.itemId || item.id,
202
+ payload: {
203
+ objectiveKey: item.objectiveKey,
204
+ obligationKey: item.obligationKey,
205
+ why: item.why,
206
+ expectedDelta: item.expectedDelta ?? null,
207
+ successCriteria: item.successCriteria || [],
208
+ },
209
+ dueBy: item.dueBy || undefined,
210
+ });
211
+ } catch (err) {
212
+ logOf(deps)("warn", `[goals] handoff to ${classification.target} threw: ${err && err.message ? err.message : err}`);
213
+ return { routed: "HANDOFF", primitive: "handoff.offer", ok: false, reason: "handoff-threw", itemId: created.itemId };
214
+ }
215
+ if (!res || !res.sent) {
216
+ logOf(deps)(
217
+ "warn",
218
+ `[goals] handoff to ${classification.target} was NOT sent (${(res && res.transport && res.transport.reason) || "unknown"}) — the work item exists and is unassigned in practice`
219
+ );
220
+ }
221
+ return {
222
+ routed: "HANDOFF",
223
+ primitive: "handoff.offer",
224
+ ok: !!(res && res.sent),
225
+ reason: (res && res.transport && res.transport.reason) || (res && res.sent ? "offered" : "not-sent"),
226
+ target: classification.target,
227
+ itemId: created.itemId,
228
+ runId: (res && res.runId) || null,
229
+ };
230
+ }
231
+
232
+ /**
233
+ * NEEDS_REVIEW — a gated action class goes through `approval.request` (the
234
+ * existing, well-wired human-decision path). A plain named reviewer goes
235
+ * through `board.requestReview` IF the vendored protocol carries it, else a
236
+ * REVIEW handoff envelope, with the fallback logged.
237
+ */
238
+ export async function routeNeedsReview(item, classification, deps = {}) {
239
+ const created = await createWorkItem(item, deps);
240
+
241
+ if (classification.gatedClasses.length > 0) {
242
+ const request = pick(deps, "requestApproval", (o) => defaultRequestApproval(o, deps));
243
+ let res;
244
+ try {
245
+ res = await request({
246
+ action: item.obligationKey || item.title,
247
+ classification: classification.gatedClasses,
248
+ payload: {
249
+ title: item.title,
250
+ objectiveKey: item.objectiveKey,
251
+ obligationKey: item.obligationKey,
252
+ why: item.why,
253
+ },
254
+ subject: `${item.title} (advances ${item.objectiveKey})`,
255
+ itemId: created.itemId || item.id,
256
+ approver: classification.target ? { id: classification.target } : undefined,
257
+ requester: deps.memberId || undefined,
258
+ });
259
+ } catch (err) {
260
+ logOf(deps)("warn", `[goals] approval.request threw for "${item.title}": ${err && err.message ? err.message : err}`);
261
+ return { routed: "NEEDS_REVIEW", primitive: "approval.request", ok: false, reason: "approval-threw", itemId: created.itemId };
262
+ }
263
+ if (!res.requested) {
264
+ logOf(deps)(
265
+ "warn",
266
+ `[goals] "${item.title}" is gated (${classification.gatedClasses.join("+")}) but no approval could be requested — the item stays BLOCKED, not executed`
267
+ );
268
+ }
269
+ return {
270
+ routed: "NEEDS_REVIEW",
271
+ primitive: "approval.request",
272
+ ok: !!res.requested,
273
+ reason: res.requested ? "approval-pending" : "approval-not-requested",
274
+ approvalId: res.id || null,
275
+ classes: classification.gatedClasses,
276
+ itemId: created.itemId,
277
+ blocked: !res.requested,
278
+ };
279
+ }
280
+
281
+ // Plain review. Prefer the first-class method once hq ships it.
282
+ if (methodDef("board.requestReview")) {
283
+ const callImpl = pick(deps, "callImpl", (m, p) => defaultCall(m, p, deps));
284
+ try {
285
+ const frame = await callImpl("board.requestReview", {
286
+ itemId: created.itemId || item.id,
287
+ reviewer: classification.target,
288
+ });
289
+ return {
290
+ routed: "NEEDS_REVIEW",
291
+ primitive: "board.requestReview",
292
+ ok: !!(frame && frame.ok),
293
+ reason: frame && frame.ok ? "review-requested" : (frame && frame.error && frame.error.code) || "review-request-failed",
294
+ reviewer: classification.target,
295
+ itemId: created.itemId,
296
+ };
297
+ } catch (err) {
298
+ logOf(deps)("warn", `[goals] board.requestReview threw: ${err && err.message ? err.message : err}`);
299
+ }
300
+ } else {
301
+ logOf(deps)(
302
+ "info",
303
+ "[goals] board.requestReview is not in the vendored protocol — falling back to a REVIEW handoff envelope (degradation, not a silent skip)"
304
+ );
305
+ }
306
+
307
+ const send = pick(deps, "sendHandoff", (o) => defaultSendHandoff(o, deps));
308
+ let res;
309
+ try {
310
+ res = await send({
311
+ from: deps.memberId || "",
312
+ to: classification.target,
313
+ intent: `Review: ${item.title}`,
314
+ itemId: created.itemId || item.id,
315
+ payload: { review: true, objectiveKey: item.objectiveKey, why: item.why },
316
+ });
317
+ } catch (err) {
318
+ logOf(deps)("warn", `[goals] review handoff threw: ${err && err.message ? err.message : err}`);
319
+ return { routed: "NEEDS_REVIEW", primitive: "handoff.offer(review)", ok: false, reason: "review-handoff-threw", itemId: created.itemId };
320
+ }
321
+ return {
322
+ routed: "NEEDS_REVIEW",
323
+ primitive: "handoff.offer(review)",
324
+ ok: !!(res && res.sent),
325
+ reason: (res && res.transport && res.transport.reason) || "not-sent",
326
+ reviewer: classification.target,
327
+ itemId: created.itemId,
328
+ degraded: true,
329
+ };
330
+ }
331
+
332
+ /** SELF — just the board item; the local backlog write is the caller's job. */
333
+ export async function routeSelf(item, classification, deps = {}) {
334
+ const created = await createWorkItem(item, deps);
335
+ return {
336
+ routed: "SELF",
337
+ primitive: "board.create",
338
+ ok: created.ok,
339
+ reason: created.reason,
340
+ itemId: created.itemId,
341
+ capabilityGap: classification.capabilityGap || null,
342
+ };
343
+ }
344
+
345
+ /**
346
+ * The decision table's "ask, don't pile on" branch: raise an escalation with
347
+ * concrete options rather than creating more tasks. Uses `escalation.create`,
348
+ * which the vendored protocol already carries.
349
+ *
350
+ * @param {object} o - { objectiveKey, question, options[], gap, why }
351
+ * @param {object} deps
352
+ * @returns {Promise<{ok:boolean, reason:string, id:string|null}>}
353
+ */
354
+ export async function raiseEscalation(o = {}, deps = {}) {
355
+ const callImpl = pick(deps, "callImpl", (m, p) => defaultCall(m, p, deps));
356
+ const method = methodDef("escalation.ask") ? "escalation.ask" : "escalation.create";
357
+ if (method === "escalation.create" && methodDef("escalation.ask") == null) {
358
+ logOf(deps)("info", "[goals] escalation.ask is not in the vendored protocol — using escalation.create with the options in the body");
359
+ }
360
+ let frame;
361
+ try {
362
+ frame = await callImpl(method, {
363
+ title: o.question,
364
+ question: o.question,
365
+ body: [
366
+ o.question,
367
+ "",
368
+ ...(o.options || []).map((opt, i) => `${i + 1}. ${typeof opt === "string" ? opt : opt.label}`),
369
+ "",
370
+ `Objective: ${o.objectiveKey}. Gap: ${o.gap && o.gap.gap} vs target ${o.gap && o.gap.target} (${o.gap && o.gap.trend}).`,
371
+ ].join("\n"),
372
+ options: (o.options || []).map((opt) => (typeof opt === "string" ? { label: opt } : opt)),
373
+ severity: o.severity || "normal",
374
+ objectiveKey: o.objectiveKey,
375
+ });
376
+ } catch (err) {
377
+ logOf(deps)("warn", `[goals] ${method} threw: ${err && err.message ? err.message : err}`);
378
+ return { ok: false, reason: "escalation-threw", id: null };
379
+ }
380
+ const ok = !!(frame && frame.ok);
381
+ if (!ok) {
382
+ logOf(deps)(
383
+ "warn",
384
+ `[goals] ${method} failed (${(frame && frame.error && frame.error.code) || "unknown"}) — the question is still recorded in the audit ledger`
385
+ );
386
+ }
387
+ return { ok, reason: ok ? "raised" : (frame && frame.error && frame.error.code) || "failed", id: ok && frame.result ? frame.result.id || null : null, method };
388
+ }
389
+
390
+ /**
391
+ * Dispatch one classified candidate to its primitive.
392
+ * @param {object} item @param {object} classification @param {object} deps
393
+ * @returns {Promise<object>} the route result
394
+ */
395
+ export async function routeWorkItem(item, classification, deps = {}) {
396
+ switch (classification.disposition) {
397
+ case "HANDOFF": return routeHandoff(item, classification, deps);
398
+ case "COLLABORATE": return routeCollaborate(item, classification, deps);
399
+ case "NEEDS_REVIEW": return routeNeedsReview(item, classification, deps);
400
+ case "SELF":
401
+ default: return routeSelf(item, classification, deps);
402
+ }
403
+ }
404
+
405
+ export default {
406
+ WORK_LEASE_SCOPE,
407
+ workResourceId,
408
+ createWorkItem,
409
+ routeWorkItem,
410
+ routeSelf,
411
+ routeCollaborate,
412
+ routeHandoff,
413
+ routeNeedsReview,
414
+ raiseEscalation,
415
+ };