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