blun-king-cli 9.1.564 → 9.1.566

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 (88) hide show
  1. package/LIESMICH.txt +2 -2
  2. package/README.md +1 -1
  3. package/agent-spine-plugin/.claude-plugin/marketplace.json +20 -20
  4. package/agent-spine-plugin/.claude-plugin/plugin.json +14 -14
  5. package/agent-spine-plugin/.codex-plugin/plugin.json +34 -34
  6. package/agent-spine-plugin/.mcp.json +8 -8
  7. package/agent-spine-plugin/CONTRIBUTING.md +52 -0
  8. package/agent-spine-plugin/LICENSE +186 -186
  9. package/agent-spine-plugin/README.md +406 -0
  10. package/agent-spine-plugin/SECURITY.md +47 -0
  11. package/agent-spine-plugin/assets/agentspine-banner.svg +32 -32
  12. package/agent-spine-plugin/bin/agentspine-mcp.js +4 -4
  13. package/agent-spine-plugin/bin/agentspine.js +7 -7
  14. package/agent-spine-plugin/blun.plugin.json +33 -33
  15. package/agent-spine-plugin/hooks/codex.json +47 -47
  16. package/agent-spine-plugin/hooks/hooks.json +106 -106
  17. package/agent-spine-plugin/hooks/version.json +5 -5
  18. package/agent-spine-plugin/package.json +69 -69
  19. package/agent-spine-plugin/scripts/check-hosts.js +199 -199
  20. package/agent-spine-plugin/skill/SKILL.md +76 -76
  21. package/agent-spine-plugin/skills/agent-spine/SKILL.md +85 -85
  22. package/agent-spine-plugin/src/cli.js +1488 -1442
  23. package/agent-spine-plugin/src/hook.js +882 -861
  24. package/agent-spine-plugin/src/index.js +94 -93
  25. package/agent-spine-plugin/src/lib/acceptance.js +333 -333
  26. package/agent-spine-plugin/src/lib/attention.js +755 -755
  27. package/agent-spine-plugin/src/lib/audit.js +351 -342
  28. package/agent-spine-plugin/src/lib/authentication.js +515 -515
  29. package/agent-spine-plugin/src/lib/briefing.js +317 -317
  30. package/agent-spine-plugin/src/lib/catalog.js +167 -167
  31. package/agent-spine-plugin/src/lib/channel-runtime.js +665 -665
  32. package/agent-spine-plugin/src/lib/context.js +154 -154
  33. package/agent-spine-plugin/src/lib/continuity.js +338 -338
  34. package/agent-spine-plugin/src/lib/coordination.js +577 -577
  35. package/agent-spine-plugin/src/lib/documents.js +217 -217
  36. package/agent-spine-plugin/src/lib/feed-transport.js +499 -499
  37. package/agent-spine-plugin/src/lib/filesystem-retry.js +32 -32
  38. package/agent-spine-plugin/src/lib/gateway-runtime.js +1623 -760
  39. package/agent-spine-plugin/src/lib/graph.js +337 -337
  40. package/agent-spine-plugin/src/lib/hook-audit.js +29 -29
  41. package/agent-spine-plugin/src/lib/https-transport.js +392 -392
  42. package/agent-spine-plugin/src/lib/indexed-memory-offline.js +40 -40
  43. package/agent-spine-plugin/src/lib/indexed-memory.js +281 -281
  44. package/agent-spine-plugin/src/lib/learning.js +6923 -6466
  45. package/agent-spine-plugin/src/lib/object-transport.js +206 -206
  46. package/agent-spine-plugin/src/lib/owned-file-lock.js +143 -143
  47. package/agent-spine-plugin/src/lib/paths.js +109 -109
  48. package/agent-spine-plugin/src/lib/peer-transport.js +283 -283
  49. package/agent-spine-plugin/src/lib/persona-runtime.js +581 -581
  50. package/agent-spine-plugin/src/lib/preflight.js +702 -702
  51. package/agent-spine-plugin/src/lib/runtime.js +13 -13
  52. package/agent-spine-plugin/src/lib/selfstarter.js +911 -866
  53. package/agent-spine-plugin/src/lib/sharing.js +969 -969
  54. package/agent-spine-plugin/src/lib/source-roots.js +530 -498
  55. package/agent-spine-plugin/src/lib/sqlite-transport.js +501 -501
  56. package/agent-spine-plugin/src/lib/telegram-adapter.js +119 -119
  57. package/agent-spine-plugin/src/lib/voice-runtime.js +39 -39
  58. package/agent-spine-plugin/src/mcp.js +597 -572
  59. package/agent-spine-plugin/src/version.js +1 -1
  60. package/agent-spine-plugin/src/worker.js +210 -195
  61. package/bin/active-steer-priority-policy.cjs +24 -0
  62. package/bin/launcher-runtime.js +8 -1
  63. package/bin/mnemo-tool-agent-policy.cjs +22 -0
  64. package/bin/native-runtime-cache.cjs +76 -0
  65. package/bin/thinking-activity-status-policy.cjs +132 -0
  66. package/bin/thinking-only-guard.cjs +75 -0
  67. package/bin/tool-call-loop-policy.cjs +53 -0
  68. package/bin/turn-thinking-policy.cjs +25 -1
  69. package/blun.mjs +556 -63
  70. package/package.json +4 -1
  71. package/standard-skills/translate-native/LICENSE +21 -21
  72. package/standard-skills/translate-native/references/evaluation-protocol.md +95 -95
  73. package/standard-skills/translate-native/references/native-orthography.md +79 -79
  74. package/standard-skills/translate-native/references/native-translation-standard.md +94 -94
  75. package/standard-skills/translate-native/references/structured-content.md +72 -72
  76. package/standard-skills/translate-native/references/translationese-review.md +77 -77
  77. package/standard-skills/translate-native/scripts/blun_language_guard.py +697 -697
  78. package/standard-skills/translate-native/scripts/check_diacritics.py +353 -353
  79. package/standard-skills/translate-native/scripts/guard_service_client.py +264 -264
  80. package/standard-skills/translate-native/scripts/language_gateway.py +62 -62
  81. package/standard-skills/translate-native/scripts/language_quality.py +377 -377
  82. package/standard-skills/translate-native/scripts/pre_output_guard.py +64 -64
  83. package/standard-skills/translate-native/scripts/translation_guard.py +916 -916
  84. package/standard-tools/language-guard/blun_language_guard.py +697 -697
  85. package/standard-tools/language-guard/check_diacritics.py +353 -353
  86. package/standard-tools/language-guard/guard_service_client.py +264 -264
  87. package/standard-tools/language-guard/language_quality.py +377 -377
  88. package/standard-tools/language-guard/translation_guard.py +916 -916
@@ -1,760 +1,1623 @@
1
- import { createHash, randomUUID } from "node:crypto";
2
- import { open, readFile, stat, unlink, writeFile } from "node:fs/promises";
3
- import { join } from "node:path";
4
- import { buildCatalog } from "./catalog.js";
5
- import { isFileLockContention, replaceFileWithRetry } from "./filesystem-retry.js";
6
- import { attentionFindings, loadAttention } from "./attention.js";
7
- import {
8
- channelRuntimeFindings, loadChannelPolicy, loadChannelRuntime
9
- } from "./channel-runtime.js";
10
- import { loadGraph } from "./graph.js";
11
- import { projectStateDir } from "./paths.js";
12
- import { loadPersonaRuntime, personaRuntimeFindings } from "./persona-runtime.js";
13
- import { evaluateVoiceOutput } from "./voice-runtime.js";
14
-
15
- export const GATEWAY_POLICY_SCHEMA = "agentspine.gateway-policy/v1";
16
- export const GATEWAY_RUNTIME_SCHEMA = "agentspine.gateway-runtime/v1";
17
- export const GATEWAY_EVENT_SCHEMA = "agentspine.gateway-event/v1";
18
-
19
- const CONFIRMATION = "local-owner-confirmed";
20
- const MAX_BYTES = 8 * 1024 * 1024;
21
- const ID_RE = /^[A-Za-z0-9][A-Za-z0-9:_.@/+~-]{0,255}$/;
22
- const ROUTE_RE = /^-?[A-Za-z0-9][A-Za-z0-9:_.@/+~-]{0,255}$/;
23
- const GOAL_STATUSES = new Set(["active", "blocked", "completed", "cancelled"]);
24
- const QUEUE_STATUSES = new Set(["pending", "leased", "awaiting-delivery", "completed", "blocked", "dead-letter", "cancelled"]);
25
- const OUTBOX_STATUSES = new Set(["prepared", "sending", "delivered", "failed", "dead-letter", "delivery-unknown", "acknowledged"]);
26
- const WAKE_KINDS = new Set(["direct-message", "deadline", "promise", "resolved-blocker", "assignment", "follow-up", "relationship"]);
27
- const PRIORITY = { "direct-message": 100, deadline: 90, promise: 90, "resolved-blocker": 80, assignment: 70, "follow-up": 60, relationship: 50 };
28
- const SECRET_RE = /-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:sk|gh[opusu])_[A-Za-z0-9_-]{20,}\b|\bBearer\s+[A-Za-z0-9._~+/-]{20,}|\b(?:api[-_ ]?key|token|password|secret)\s*[:=]\s*\S{8,}/i;
29
- const SECRET_KEY_RE = /"(?:api[-_ ]?key|token|password|secret|credential)"\s*:/i;
30
- const AUTHORITY_RE = /\b(?:permission|rights?|roles?|owner|trusted|delegat|authorized|approval|production|payment|spending|tool capability|send capability)\b/i;
31
- const HEALTH_VALUES = new Set(["stopped", "running", "unknown", "healthy", "degraded", "failed"]);
32
-
33
- function emptyPolicy(root) {
34
- return { schema: GATEWAY_POLICY_SCHEMA, root, revision: 0, enabled: false, killSwitch: false, goals: [], history: [] };
35
- }
36
-
37
- function emptyRuntime(root) {
38
- return { schema: GATEWAY_RUNTIME_SCHEMA, root, revision: 0, queue: [], lanes: [], outbox: [], receipts: [], history: [],
39
- health: { gateway: "stopped", adapter: "unknown", scheduler: "unknown", queue: "healthy", worker: "unknown", host: "unknown", lastTickAt: null, lastReconciledAt: null } };
40
- }
41
-
42
- function exactId(value, field, nullable = false) {
43
- if ((value === null || value === undefined || value === "") && nullable) return null;
44
- if (typeof value !== "string" || !ID_RE.test(value) || value.includes("*")) throw new Error(field + " must be an exact stable ID without wildcards");
45
- return value;
46
- }
47
-
48
- function safeText(value, field, maximum = 1000) {
49
- if (typeof value !== "string" || !value.trim()) throw new Error(field + " is required");
50
- const text = value.trim().replace(/\s+/g, " ").slice(0, maximum);
51
- if (SECRET_RE.test(text)) throw new Error(field + " appears to contain a secret");
52
- return text;
53
- }
54
-
55
- function timestamp(value = new Date()) {
56
- const date = value instanceof Date ? value : new Date(value);
57
- if (!Number.isFinite(date.getTime())) throw new Error("timestamp is invalid");
58
- return date.toISOString();
59
- }
60
-
61
- function sha256(value) { return createHash("sha256").update(value).digest("hex"); }
62
-
63
- function safeCheckpoint(value) {
64
- if (value === null || value === undefined) return null;
65
- let content;
66
- try { content = JSON.stringify(value); } catch { throw new Error("checkpoint must be JSON serializable"); }
67
- if (!content || Buffer.byteLength(content) > 16384) throw new Error("checkpoint exceeds 16 KiB");
68
- if (SECRET_RE.test(content) || SECRET_KEY_RE.test(content) || AUTHORITY_RE.test(content)) {
69
- throw new Error("checkpoint contains secret- or authority-shaped content");
70
- }
71
- return JSON.parse(content);
72
- }
73
-
74
- function validGoal(goal) {
75
- return goal && ID_RE.test(goal.goalId || "") && ID_RE.test(goal.agentId || "")
76
- && ID_RE.test(goal.ownerSubjectId || "") && ID_RE.test(goal.projectId || "")
77
- && (goal.groupId === null || ID_RE.test(goal.groupId || "")) && GOAL_STATUSES.has(goal.status)
78
- && typeof goal.successCriterion === "string" && goal.successCriterion.length > 0
79
- && typeof goal.nextSafeStep === "string" && goal.nextSafeStep.length > 0
80
- && (goal.deadline === null || Number.isFinite(new Date(goal.deadline).getTime()))
81
- && Number.isInteger(goal.priority) && goal.priority >= 0 && goal.priority <= 100
82
- && (goal.checkpoint === null || (() => { try { safeCheckpoint(goal.checkpoint); return true; } catch { return false; } })())
83
- && (goal.blocker === null || (typeof goal.blocker === "string" && goal.blocker.length <= 500 && !SECRET_RE.test(goal.blocker)))
84
- && (goal.heartbeatAt === null || Number.isFinite(new Date(goal.heartbeatAt).getTime()))
85
- && goal.authority === "authenticated-goal-policy" && Number.isFinite(new Date(goal.createdAt).getTime())
86
- && Number.isFinite(new Date(goal.updatedAt).getTime());
87
- }
88
-
89
- function validPolicyHistory(item) {
90
- if (!item || item.authority !== "authenticated-goal-policy" || !Number.isFinite(new Date(item.at).getTime())) return false;
91
- if (item.kind === "goal") return validGoal(item.value);
92
- return item.kind === "control" && item.value && typeof item.value.enabled === "boolean" && typeof item.value.killSwitch === "boolean";
93
- }
94
-
95
- function validQueue(item) {
96
- return item && ID_RE.test(item.queueId || "") && WAKE_KINDS.has(item.kind)
97
- && ID_RE.test(item.dedupeKey || "") && item.queueId === "gateway-queue:" + sha256(item.dedupeKey).slice(0, 32)
98
- && ID_RE.test(item.agentId || "") && ID_RE.test(item.projectId || "")
99
- && (item.groupId === null || ID_RE.test(item.groupId || ""))
100
- && (item.goalId === null || ID_RE.test(item.goalId || ""))
101
- && (item.channelEventId === null || ID_RE.test(item.channelEventId || ""))
102
- && QUEUE_STATUSES.has(item.status) && Number.isInteger(item.priority) && item.priority >= 0 && item.priority <= 100
103
- && Number.isInteger(item.attempts) && item.attempts >= 0 && item.attempts <= 20
104
- && item.authority === "execution-state-only" && Number.isFinite(new Date(item.createdAt).getTime())
105
- && Number.isFinite(new Date(item.availableAt).getTime()) && Number.isFinite(new Date(item.updatedAt).getTime())
106
- && (item.completedAt === null || Number.isFinite(new Date(item.completedAt).getTime()))
107
- && (item.lastError === null || (typeof item.lastError === "string" && item.lastError.length <= 500 && !SECRET_RE.test(item.lastError)))
108
- && (item.lease === null || (item.status === "leased" && ID_RE.test(item.lease.workerId || "")
109
- && Number.isFinite(new Date(item.lease.expiresAt).getTime())));
110
- }
111
-
112
- function validOutbox(item) {
113
- return item && ID_RE.test(item.outboxId || "") && ID_RE.test(item.queueId || "")
114
- && ID_RE.test(item.idempotencyKey || "") && ID_RE.test(item.bindingId || "")
115
- && item.outboxId === "gateway-outbox:" + sha256(item.idempotencyKey).slice(0, 32)
116
- && ID_RE.test(item.eventId || "") && ROUTE_RE.test(item.provider || "") && ROUTE_RE.test(item.tenantId || "")
117
- && ROUTE_RE.test(item.accountId || "") && ROUTE_RE.test(item.chatId || "")
118
- && (item.threadId === null || ROUTE_RE.test(item.threadId || "")) && (item.replyTo === null || ROUTE_RE.test(item.replyTo || ""))
119
- && OUTBOX_STATUSES.has(item.status)
120
- && typeof item.text === "string" && item.text.length > 0 && item.text.length <= 16000 && !SECRET_RE.test(item.text)
121
- && Number.isInteger(item.attempts) && item.attempts >= 0 && item.attempts <= 20
122
- && item.authority === "delivery-state-only" && Number.isFinite(new Date(item.createdAt).getTime())
123
- && Number.isFinite(new Date(item.updatedAt).getTime()) && Number.isFinite(new Date(item.nextAttemptAt).getTime())
124
- && (item.deliveredAt === null || Number.isFinite(new Date(item.deliveredAt).getTime()))
125
- && (item.adapterReceipt === null || (typeof item.adapterReceipt === "string" && item.adapterReceipt.length <= 500))
126
- && (item.lastError === null || (typeof item.lastError === "string" && item.lastError.length <= 500 && !SECRET_RE.test(item.lastError)));
127
- }
128
-
129
- function validLane(item) {
130
- return item && ID_RE.test(item.agentId || "") && ID_RE.test(item.queueId || "") && ID_RE.test(item.workerId || "")
131
- && new Set(["leased", "completed", "expired"]).has(item.status)
132
- && Number.isFinite(new Date(item.claimedAt).getTime()) && Number.isFinite(new Date(item.expiresAt).getTime())
133
- && Number.isFinite(new Date(item.updatedAt).getTime()) && item.authority === "execution-state-only";
134
- }
135
-
136
- function validReceipt(item) {
137
- if (!(item && ID_RE.test(item.id || "") && ID_RE.test(item.kind || "") && ID_RE.test(item.objectId || "")
138
- && Number.isFinite(new Date(item.at).getTime()) && item.details && typeof item.details === "object"
139
- && !Array.isArray(item.details) && !SECRET_RE.test(JSON.stringify(item.details))
140
- && item.authority === "execution-state-only" && /^[a-f0-9]{64}$/.test(item.digest || ""))) return false;
141
- const material = { kind: item.kind, objectId: item.objectId, at: item.at, details: item.details, authority: "execution-state-only" };
142
- return item.digest === sha256(JSON.stringify(material)) && item.id === "gateway-receipt:" + item.digest.slice(0, 24);
143
- }
144
-
145
- function validHistory(item) {
146
- if (!(item && new Set(["queue", "outbox"]).has(item.kind) && ID_RE.test(item.objectId || "")
147
- && ID_RE.test(item.transition || "") && Number.isFinite(new Date(item.at).getTime())
148
- && item.value && item.authority === "execution-state-only")) return false;
149
- return item.kind === "queue" ? validQueue(item.value) && item.objectId === item.value.queueId
150
- : validOutbox(item.value) && item.objectId === item.value.outboxId;
151
- }
152
-
153
- function validHealth(health) {
154
- return health && ["gateway", "adapter", "scheduler", "queue", "worker", "host"].every((key) => HEALTH_VALUES.has(health[key]))
155
- && (health.lastTickAt === null || Number.isFinite(new Date(health.lastTickAt).getTime()))
156
- && (health.lastReconciledAt === null || Number.isFinite(new Date(health.lastReconciledAt).getTime()));
157
- }
158
-
159
- function normalizePolicy(value, root) {
160
- if (!value || value.schema !== GATEWAY_POLICY_SCHEMA || value.root !== root
161
- || !Number.isInteger(value.revision) || typeof value.enabled !== "boolean" || typeof value.killSwitch !== "boolean"
162
- || !Array.isArray(value.goals) || !Array.isArray(value.history) || value.goals.some((item) => !validGoal(item))
163
- || value.history.some((item) => !validPolicyHistory(item))) {
164
- throw new Error("gateway policy is invalid; autonomous runtime is disabled");
165
- }
166
- return value;
167
- }
168
-
169
- function normalizeRuntime(value, root) {
170
- const baseValid = value && value.schema === GATEWAY_RUNTIME_SCHEMA && value.root === root && Number.isInteger(value.revision)
171
- && Array.isArray(value.queue) && Array.isArray(value.lanes) && Array.isArray(value.outbox)
172
- && Array.isArray(value.receipts) && Array.isArray(value.history);
173
- if (!baseValid) throw new Error("gateway runtime structure is invalid; worker is disabled");
174
- const invalid = [];
175
- if (!validHealth(value.health)) invalid.push("health");
176
- const queueIndex = value.queue.findIndex((item) => !validQueue(item));
177
- const laneIndex = value.lanes.findIndex((item) => !validLane(item));
178
- const outboxIndex = value.outbox.findIndex((item) => !validOutbox(item));
179
- const receiptIndex = value.receipts.findIndex((item) => !validReceipt(item));
180
- const historyIndex = value.history.findIndex((item) => !validHistory(item));
181
- if (queueIndex >= 0) invalid.push("queue:" + queueIndex);
182
- if (laneIndex >= 0) invalid.push("lanes:" + laneIndex);
183
- if (outboxIndex >= 0) invalid.push("outbox:" + outboxIndex);
184
- if (receiptIndex >= 0) invalid.push("receipts:" + receiptIndex);
185
- if (historyIndex >= 0) invalid.push("history:" + historyIndex);
186
- if (invalid.length) throw new Error("gateway runtime is invalid (" + invalid.join(",") + "); worker is disabled");
187
- return value;
188
- }
189
-
190
- async function pathsFor(root, catalog = null) {
191
- catalog ||= await buildCatalog(root);
192
- const directory = await projectStateDir(catalog.root);
193
- return { catalog, directory, gatewayPolicyPath: join(directory, "gateway-policy.json"), gatewayRuntimePath: join(directory, "gateway-runtime.json") };
194
- }
195
-
196
- async function readJson(path, root, normalize, empty) {
197
- try {
198
- const metadata = await stat(path);
199
- if (metadata.size > MAX_BYTES) throw new Error("gateway state exceeds 8 MiB");
200
- return normalize(JSON.parse(await readFile(path, "utf8")), root);
201
- } catch (error) {
202
- if (error.code !== "ENOENT") throw error;
203
- return empty(root);
204
- }
205
- }
206
-
207
- async function writeJson(path, value) {
208
- const content = JSON.stringify(value, null, 2) + "\n";
209
- if (Buffer.byteLength(content) > MAX_BYTES) throw new Error("gateway state exceeds 8 MiB");
210
- const temporary = path + "." + process.pid + "." + randomUUID() + ".tmp";
211
- try { await writeFile(temporary, content, { mode: 0o600 }); await replaceFileWithRetry(temporary, path); }
212
- finally { await unlink(temporary).catch((error) => { if (error.code !== "ENOENT") throw error; }); }
213
- }
214
-
215
- async function withLock(paths, task) {
216
- const lockPath = join(paths.directory, "gateway-runtime.lock");
217
- let handle;
218
- for (let attempt = 0; attempt < 160; attempt += 1) {
219
- try { handle = await open(lockPath, "wx", 0o600); break; } catch (error) {
220
- if (!isFileLockContention(error)) throw error;
221
- try { const metadata = await stat(lockPath); if (Date.now() - metadata.mtimeMs > 120000) await unlink(lockPath); }
222
- catch (lockError) { if (lockError.code !== "ENOENT") throw lockError; }
223
- await new Promise((resolve) => setTimeout(resolve, 25));
224
- }
225
- }
226
- if (!handle) throw new Error("gateway state is busy; retry later");
227
- try { return await task(); } finally {
228
- await handle.close();
229
- await unlink(lockPath).catch((error) => { if (error.code !== "ENOENT") throw error; });
230
- }
231
- }
232
-
233
- function appendReceipt(runtime, kind, objectId, now, details = {}) {
234
- const material = { kind, objectId, at: now, details, authority: "execution-state-only" };
235
- const digest = sha256(JSON.stringify(material));
236
- const id = "gateway-receipt:" + digest.slice(0, 24);
237
- const previous = runtime.receipts.find((item) => item.id === id);
238
- if (previous) return previous;
239
- const receipt = { id, ...material, digest };
240
- runtime.receipts.push(receipt);
241
- return receipt;
242
- }
243
-
244
- function preserve(runtime, kind, value, transition, now) {
245
- runtime.history.push({ kind, objectId: kind === "outbox" ? value.outboxId : value.queueId, transition, at: now,
246
- value: structuredClone(value), authority: "execution-state-only" });
247
- }
248
-
249
- function currentLane(runtime, agentId) {
250
- return runtime.lanes.find((item) => item.agentId === agentId && item.status === "leased") || null;
251
- }
252
-
253
- function assertActivePersona(personaPolicy, personaRuntime, agentId, projectId, groupId) {
254
- const findings = personaRuntimeFindings(personaPolicy, personaRuntime);
255
- if (findings.length) throw new Error("persona runtime is unhealthy: " + findings.join(", "));
256
- const persona = personaRuntime.personas.find((item) => item.personaId === agentId && item.status === "active");
257
- if (!persona || !["agent", "bot"].includes(persona.kind)) throw new Error("gateway work requires an active authenticated agent or bot");
258
- if (groupId !== null && persona.groupId !== groupId) throw new Error("gateway work group does not match authenticated persona membership");
259
- const binding = personaPolicy.bindings.find((item) => item.id === persona.bindingId && item.active);
260
- if (!binding || !binding.tenantId || !binding.profileId || !projectId) throw new Error("gateway work lacks an active authenticated identity binding");
261
- return { persona, binding };
262
- }
263
-
264
- export async function setGatewayControl({ root = process.cwd(), enabled, killSwitch, confirmation, now = new Date() }) {
265
- if (confirmation !== CONFIRMATION) throw new Error("gateway control changes require explicit local owner confirmation");
266
- const paths = await pathsFor(root);
267
- return withLock(paths, async () => {
268
- const policy = await readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy);
269
- const previous = { enabled: policy.enabled, killSwitch: policy.killSwitch };
270
- if (enabled !== undefined) policy.enabled = Boolean(enabled);
271
- if (killSwitch !== undefined) policy.killSwitch = Boolean(killSwitch);
272
- policy.revision += 1;
273
- policy.history.push({ kind: "control", at: timestamp(now), value: previous, authority: "authenticated-goal-policy" });
274
- await writeJson(paths.gatewayPolicyPath, policy);
275
- return { enabled: policy.enabled, killSwitch: policy.killSwitch, revision: policy.revision };
276
- });
277
- }
278
-
279
- export async function assignGoal({ root = process.cwd(), goalId, agentId, ownerSubjectId, projectId, groupId = null,
280
- priority = 70, successCriterion, nextSafeStep, deadline = null, confirmation, now = new Date() }) {
281
- if (confirmation !== CONFIRMATION) throw new Error("goal assignment requires explicit local owner confirmation");
282
- const paths = await pathsFor(root);
283
- return withLock(paths, async () => {
284
- const [policy, runtime, personas] = await Promise.all([
285
- readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
286
- readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime),
287
- loadPersonaRuntime(paths.catalog.root, paths.catalog)
288
- ]);
289
- agentId = exactId(agentId, "agentId"); projectId = exactId(projectId, "projectId"); groupId = exactId(groupId, "groupId", true);
290
- assertActivePersona(personas.policy, personas.runtime, agentId, projectId, groupId);
291
- const createdAt = timestamp(now);
292
- const active = policy.goals.find((item) => item.agentId === agentId && item.status === "active" && item.goalId !== goalId);
293
- if (active) throw new Error("an agent may have only one active focused goal");
294
- const goal = { goalId: exactId(goalId, "goalId"), agentId, ownerSubjectId: exactId(ownerSubjectId, "ownerSubjectId"),
295
- projectId, groupId, priority: Number(priority), successCriterion: safeText(successCriterion, "successCriterion"),
296
- nextSafeStep: safeText(nextSafeStep, "nextSafeStep"), deadline: deadline === null ? null : timestamp(deadline),
297
- status: "active", checkpoint: null, heartbeatAt: null, blocker: null, createdAt, updatedAt: createdAt,
298
- authority: "authenticated-goal-policy" };
299
- if (!validGoal(goal)) throw new Error("goal assignment is invalid");
300
- const previous = policy.goals.find((item) => item.goalId === goal.goalId);
301
- if (previous && [previous.agentId, previous.ownerSubjectId, previous.projectId, previous.groupId].join("\0")
302
- !== [goal.agentId, goal.ownerSubjectId, goal.projectId, goal.groupId].join("\0")) throw new Error("goal scope is immutable");
303
- if (previous) {
304
- policy.history.push({ kind: "goal", at: createdAt, value: structuredClone(previous), authority: "authenticated-goal-policy" });
305
- goal.createdAt = previous.createdAt;
306
- }
307
- policy.goals = policy.goals.filter((item) => item.goalId !== goal.goalId);
308
- policy.goals.push(goal);
309
- policy.revision += 1;
310
- const key = "goal:" + goal.goalId + ":assignment";
311
- if (!runtime.queue.some((item) => item.dedupeKey === key)) {
312
- runtime.queue.push({ queueId: "gateway-queue:" + sha256(key).slice(0, 32), dedupeKey: key, kind: "assignment",
313
- agentId, projectId, groupId, goalId: goal.goalId, channelEventId: null, priority: PRIORITY.assignment,
314
- status: "pending", attempts: 0, lease: null, availableAt: createdAt, createdAt, updatedAt: createdAt,
315
- completedAt: null, lastError: null, authority: "execution-state-only" });
316
- runtime.revision += 1;
317
- appendReceipt(runtime, "queued", "gateway-queue:" + sha256(key).slice(0, 32), createdAt, { kind: "assignment", dedupeKey: key });
318
- }
319
- await Promise.all([writeJson(paths.gatewayPolicyPath, policy), writeJson(paths.gatewayRuntimePath, runtime)]);
320
- return { goal, gatewayPolicyPath: paths.gatewayPolicyPath };
321
- });
322
- }
323
-
324
- export async function enqueueGatewayWake({ root = process.cwd(), kind, agentId, projectId, groupId = null, goalId = null,
325
- channelEventId = null, dedupeKey, availableAt = null, now = new Date() }) {
326
- if (!WAKE_KINDS.has(kind)) throw new Error("unsupported gateway wake kind");
327
- const paths = await pathsFor(root);
328
- return withLock(paths, async () => {
329
- const [policy, runtime, personas] = await Promise.all([
330
- readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
331
- readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime),
332
- loadPersonaRuntime(paths.catalog.root, paths.catalog)
333
- ]);
334
- if (!policy.enabled || policy.killSwitch) throw new Error("gateway is disabled by local policy");
335
- agentId = exactId(agentId, "agentId"); projectId = exactId(projectId, "projectId"); groupId = exactId(groupId, "groupId", true);
336
- assertActivePersona(personas.policy, personas.runtime, agentId, projectId, groupId);
337
- goalId = exactId(goalId, "goalId", true); channelEventId = exactId(channelEventId, "channelEventId", true);
338
- const key = exactId(dedupeKey, "dedupeKey");
339
- const existing = runtime.queue.find((item) => item.dedupeKey === key);
340
- if (existing) return { item: existing, duplicate: true };
341
- const createdAt = timestamp(now);
342
- const item = { queueId: "gateway-queue:" + sha256(key).slice(0, 32), dedupeKey: key, kind, agentId, projectId, groupId,
343
- goalId, channelEventId, priority: PRIORITY[kind], status: "pending", attempts: 0, lease: null,
344
- availableAt: availableAt === null ? createdAt : timestamp(availableAt), createdAt, updatedAt: createdAt,
345
- completedAt: null, lastError: null, authority: "execution-state-only" };
346
- runtime.queue.push(item); runtime.revision += 1;
347
- appendReceipt(runtime, "queued", item.queueId, createdAt, { kind, dedupeKey: key });
348
- await writeJson(paths.gatewayRuntimePath, runtime);
349
- return { item, duplicate: false };
350
- });
351
- }
352
-
353
- export async function reconcileGateway({ root = process.cwd(), now = new Date() }) {
354
- const paths = await pathsFor(root);
355
- return withLock(paths, async () => {
356
- const [policy, runtime, channelPolicy, channel, attention, personas, { graph }] = await Promise.all([
357
- readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
358
- readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime),
359
- loadChannelPolicy(paths.catalog.root, paths.catalog), loadChannelRuntime(paths.catalog.root, paths.catalog),
360
- loadAttention(paths.catalog.root, paths.catalog), loadPersonaRuntime(paths.catalog.root, paths.catalog),
361
- loadGraph(paths.catalog.root, paths.catalog)
362
- ]);
363
- const current = timestamp(now);
364
- const findings = personaRuntimeFindings(personas.policy, personas.runtime, graph);
365
- if (findings.length) throw new Error("persona runtime is unhealthy: " + findings.join(", "));
366
- const channelFindings = channelRuntimeFindings(channel.runtime, channelPolicy.policy, graph);
367
- if (channelFindings.length) throw new Error("channel runtime is unhealthy: " + channelFindings.join(", "));
368
- const attentionIssues = attentionFindings(attention.attention);
369
- if (attentionIssues.length) throw new Error("attention runtime is unhealthy: " + attentionIssues.join(", "));
370
- for (const lane of runtime.lanes.filter((item) => item.status === "leased" && new Date(item.expiresAt) <= new Date(current))) {
371
- const item = runtime.queue.find((entry) => entry.queueId === lane.queueId && entry.status === "leased");
372
- if (item) { preserve(runtime, "queue", item, "lease-expired", current); item.status = item.attempts >= 3 ? "dead-letter" : "pending"; item.lease = null; item.updatedAt = current; }
373
- lane.status = "expired"; lane.updatedAt = current;
374
- }
375
- for (const outbox of runtime.outbox.filter((item) => item.status === "sending")) {
376
- preserve(runtime, "outbox", outbox, "ambiguous-send-recovery", current);
377
- outbox.status = "delivery-unknown"; outbox.updatedAt = current;
378
- appendReceipt(runtime, "delivery-unknown", outbox.outboxId, current, { reason: "crash-during-send" });
379
- }
380
- if (policy.enabled && !policy.killSwitch) {
381
- for (const goal of policy.goals.filter((item) => item.status === "active" && item.deadline
382
- && new Date(item.deadline) <= new Date(current))) {
383
- try { assertActivePersona(personas.policy, personas.runtime, goal.agentId, goal.projectId, goal.groupId); }
384
- catch { continue; }
385
- const key = "goal:" + goal.goalId + ":deadline:" + goal.deadline;
386
- if (!runtime.queue.some((item) => item.dedupeKey === key)) runtime.queue.push({
387
- queueId: "gateway-queue:" + sha256(key).slice(0, 32), dedupeKey: key, kind: "deadline",
388
- agentId: goal.agentId, projectId: goal.projectId, groupId: goal.groupId, goalId: goal.goalId,
389
- channelEventId: null, priority: PRIORITY.deadline, status: "pending", attempts: 0, lease: null,
390
- availableAt: current, createdAt: current, updatedAt: current, completedAt: null, lastError: null,
391
- authority: "execution-state-only"
392
- });
393
- }
394
- for (const event of channel.runtime.events.filter((item) => item.status === "pending")) {
395
- if (!personas.runtime.personas.some((persona) => persona.personaId === event.agentId && persona.status === "active")) continue;
396
- const key = "channel:" + event.eventId;
397
- if (!runtime.queue.some((item) => item.dedupeKey === key)) runtime.queue.push({ queueId: "gateway-queue:" + sha256(key).slice(0, 32),
398
- dedupeKey: key, kind: "direct-message", agentId: event.agentId, projectId: event.projectId, groupId: event.groupId,
399
- goalId: null, channelEventId: event.eventId, priority: PRIORITY["direct-message"], status: "pending", attempts: 0,
400
- lease: null, availableAt: current, createdAt: current, updatedAt: current, completedAt: null, lastError: null, authority: "execution-state-only" });
401
- }
402
- for (const event of attention.attention.events.filter((item) => item.entityId
403
- && ((item.kind === "promise" && item.status === "open") || (item.kind === "blocker" && item.status === "resolved")))) {
404
- try { assertActivePersona(personas.policy, personas.runtime, event.entityId, event.projectId, event.groupId); }
405
- catch { continue; }
406
- const kind = event.kind === "promise" ? "promise" : "resolved-blocker";
407
- const key = "attention:" + event.id + ":" + event.status;
408
- if (!runtime.queue.some((entry) => entry.dedupeKey === key)) runtime.queue.push({ queueId: "gateway-queue:" + sha256(key).slice(0, 32),
409
- dedupeKey: key, kind, agentId: event.entityId, projectId: event.projectId, groupId: event.groupId,
410
- goalId: null, channelEventId: null, priority: PRIORITY[kind], status: "pending", attempts: 0, lease: null,
411
- availableAt: event.dueAt || current, createdAt: current, updatedAt: current, completedAt: null, lastError: null, authority: "execution-state-only" });
412
- }
413
- }
414
- runtime.health.gateway = policy.enabled && !policy.killSwitch ? "running" : "stopped";
415
- runtime.health.scheduler = "healthy"; runtime.health.queue = "healthy"; runtime.health.lastReconciledAt = current;
416
- runtime.revision += 1;
417
- await writeJson(paths.gatewayRuntimePath, runtime);
418
- return { policy, runtime, recovered: true };
419
- });
420
- }
421
-
422
- export async function claimGatewayWork({ root = process.cwd(), workerId, leaseSeconds = 120, now = new Date() }) {
423
- const paths = await pathsFor(root);
424
- return withLock(paths, async () => {
425
- const [policy, runtime, personas] = await Promise.all([
426
- readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
427
- readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime),
428
- loadPersonaRuntime(paths.catalog.root, paths.catalog)
429
- ]);
430
- if (!policy.enabled || policy.killSwitch) return { item: null, reason: "disabled" };
431
- const current = timestamp(now); workerId = exactId(workerId, "workerId");
432
- const seconds = Number(leaseSeconds);
433
- if (!Number.isInteger(seconds) || seconds < 15 || seconds > 900) throw new Error("leaseSeconds must be 15-900");
434
- const items = runtime.queue.filter((item) => item.status === "pending" && new Date(item.availableAt) <= new Date(current)
435
- && !currentLane(runtime, item.agentId));
436
- items.sort((a, b) => b.priority - a.priority || a.createdAt.localeCompare(b.createdAt) || a.queueId.localeCompare(b.queueId));
437
- let item = null; let revoked = false;
438
- for (const candidate of items) {
439
- try {
440
- assertActivePersona(personas.policy, personas.runtime, candidate.agentId, candidate.projectId, candidate.groupId);
441
- item = candidate;
442
- break;
443
- } catch {
444
- preserve(runtime, "queue", candidate, "identity-revoked", current);
445
- candidate.status = "cancelled"; candidate.completedAt = current; candidate.updatedAt = current;
446
- appendReceipt(runtime, "identity-revoked", candidate.queueId, current, {});
447
- runtime.revision += 1; revoked = true;
448
- }
449
- }
450
- if (!item && revoked) await writeJson(paths.gatewayRuntimePath, runtime);
451
- if (!item) return { item: null, reason: runtime.queue.some((entry) => entry.status === "pending") ? "waiting" : "idle/needs-goal" };
452
- preserve(runtime, "queue", item, "leased", current);
453
- item.status = "leased"; item.attempts += 1; item.updatedAt = current;
454
- item.lease = { workerId, claimedAt: current, expiresAt: new Date(new Date(current).getTime() + seconds * 1000).toISOString() };
455
- runtime.lanes = runtime.lanes.filter((lane) => lane.agentId !== item.agentId || lane.status !== "leased");
456
- runtime.lanes.push({ agentId: item.agentId, queueId: item.queueId, workerId, status: "leased", claimedAt: current,
457
- expiresAt: item.lease.expiresAt, updatedAt: current, authority: "execution-state-only" });
458
- runtime.health.worker = "healthy"; runtime.health.lastTickAt = current; runtime.revision += 1;
459
- const receipt = appendReceipt(runtime, "leased", item.queueId, current, { workerId, attempt: item.attempts });
460
- await writeJson(paths.gatewayRuntimePath, runtime);
461
- return { item: structuredClone(item), receipt };
462
- });
463
- }
464
-
465
- function exactReplyBinding(channelPolicy, event) {
466
- const binding = channelPolicy.bindings.find((item) => item.id === event.bindingId && item.status === "active"
467
- && item.agentId === event.agentId && item.projectId === event.projectId && item.groupId === event.groupId
468
- && item.provider === event.provider && item.tenantId === event.tenantId && item.accountId === event.accountId
469
- && item.chatId === event.chatId && item.threadId === event.threadId && item.capabilities.includes("reply"));
470
- if (!binding) throw new Error("current exact channel reply capability is unavailable");
471
- return binding;
472
- }
473
-
474
- export async function completeGatewayRun({ root = process.cwd(), queueId, workerId, result, now = new Date() }) {
475
- const paths = await pathsFor(root);
476
- return withLock(paths, async () => {
477
- const [policy, runtime, channelPolicy, channelRuntime, personas] = await Promise.all([
478
- readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
479
- readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime),
480
- loadChannelPolicy(paths.catalog.root, paths.catalog), loadChannelRuntime(paths.catalog.root, paths.catalog),
481
- loadPersonaRuntime(paths.catalog.root, paths.catalog)
482
- ]);
483
- if (!policy.enabled || policy.killSwitch) throw new Error("gateway was disabled before run completion");
484
- const item = runtime.queue.find((entry) => entry.queueId === exactId(queueId, "queueId"));
485
- workerId = exactId(workerId, "workerId");
486
- if (!item || item.status !== "leased" || item.lease?.workerId !== workerId) throw new Error("run completion requires the exact active queue lease");
487
- assertActivePersona(personas.policy, personas.runtime, item.agentId, item.projectId, item.groupId);
488
- const current = timestamp(now);
489
- const lane = runtime.lanes.find((entry) => entry.queueId === item.queueId && entry.workerId === workerId && entry.status === "leased");
490
- if (!lane) throw new Error("agent lane lease is missing");
491
- const text = result?.text ? safeText(result.text, "result.text", 16000) : null;
492
- preserve(runtime, "queue", item, "run-completed", current);
493
- if (item.channelEventId) {
494
- if (!text) throw new Error("a channel obligation requires a non-empty response");
495
- const voice = evaluateVoiceOutput(text);
496
- if (!voice.ok) throw new Error("channel response contains a prohibited attachment or consciousness claim");
497
- const event = channelRuntime.runtime.events.find((entry) => entry.eventId === item.channelEventId);
498
- if (!event) throw new Error("channel event disappeared before delivery preparation");
499
- const binding = exactReplyBinding(channelPolicy.policy, event);
500
- const idempotencyKey = "delivery:" + sha256([event.eventId, binding.id, event.chatId, event.threadId || "", event.replyTo || ""].join("\0")).slice(0, 32);
501
- let outbox = runtime.outbox.find((entry) => entry.idempotencyKey === idempotencyKey);
502
- if (!outbox) {
503
- outbox = { outboxId: "gateway-outbox:" + sha256(idempotencyKey).slice(0, 32), queueId: item.queueId,
504
- idempotencyKey, bindingId: binding.id, eventId: event.eventId, provider: event.provider, tenantId: event.tenantId,
505
- accountId: event.accountId, chatId: event.chatId, threadId: event.threadId, replyTo: event.replyTo,
506
- text, status: "prepared", attempts: 0, nextAttemptAt: current, createdAt: current, updatedAt: current,
507
- deliveredAt: null, adapterReceipt: null, lastError: null, authority: "delivery-state-only" };
508
- runtime.outbox.push(outbox);
509
- }
510
- item.status = "awaiting-delivery";
511
- } else {
512
- item.status = result?.blocked ? "blocked" : "completed"; item.completedAt = current;
513
- const goal = item.goalId ? policy.goals.find((entry) => entry.goalId === item.goalId) : null;
514
- if (goal) {
515
- policy.history.push({ kind: "goal", at: current, value: structuredClone(goal), authority: "authenticated-goal-policy" });
516
- goal.checkpoint = result?.checkpoint === undefined ? goal.checkpoint : safeCheckpoint(result.checkpoint); goal.heartbeatAt = current;
517
- goal.blocker = result?.blocked ? safeText(result.blocker || "Run blocked.", "blocker", 500) : null;
518
- goal.status = result?.completed ? "completed" : result?.blocked ? "blocked" : "active"; goal.updatedAt = current;
519
- policy.revision += 1;
520
- if (goal.status === "active") {
521
- const checkpointDigest = sha256(JSON.stringify(goal.checkpoint || { heartbeatAt: current })).slice(0, 20);
522
- const key = "goal:" + goal.goalId + ":follow-up:" + checkpointDigest;
523
- if (!runtime.queue.some((entry) => entry.dedupeKey === key)) runtime.queue.push({
524
- queueId: "gateway-queue:" + sha256(key).slice(0, 32), dedupeKey: key, kind: "follow-up",
525
- agentId: goal.agentId, projectId: goal.projectId, groupId: goal.groupId, goalId: goal.goalId,
526
- channelEventId: null, priority: PRIORITY["follow-up"], status: "pending", attempts: 0, lease: null,
527
- availableAt: new Date(new Date(current).getTime() + 60000).toISOString(), createdAt: current, updatedAt: current,
528
- completedAt: null, lastError: null, authority: "execution-state-only"
529
- });
530
- }
531
- }
532
- }
533
- item.lease = null; item.updatedAt = current; lane.status = "completed"; lane.updatedAt = current;
534
- runtime.health.host = "healthy"; runtime.health.worker = "healthy"; runtime.health.lastTickAt = current;
535
- appendReceipt(runtime, "run-terminal", item.queueId, current, { status: item.status }); runtime.revision += 1;
536
- await Promise.all([writeJson(paths.gatewayPolicyPath, policy), writeJson(paths.gatewayRuntimePath, runtime)]);
537
- return { item, outbox: runtime.outbox.find((entry) => entry.queueId === item.queueId) || null };
538
- });
539
- }
540
-
541
- export async function failGatewayRun({ root = process.cwd(), queueId, workerId, error, retryAfterMs = 5000,
542
- now = new Date() }) {
543
- const paths = await pathsFor(root);
544
- return withLock(paths, async () => {
545
- const [policy, runtime] = await Promise.all([
546
- readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
547
- readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime)
548
- ]);
549
- const item = runtime.queue.find((entry) => entry.queueId === exactId(queueId, "queueId"));
550
- workerId = exactId(workerId, "workerId");
551
- if (!item || item.status !== "leased" || item.lease?.workerId !== workerId) {
552
- throw new Error("run failure requires the exact active queue lease");
553
- }
554
- const lane = runtime.lanes.find((entry) => entry.queueId === item.queueId && entry.workerId === workerId
555
- && entry.status === "leased");
556
- if (!lane) throw new Error("agent lane lease is missing");
557
- const current = timestamp(now);
558
- const message = safeText(String(error || "host runtime unavailable"), "runError", 500);
559
- const delay = Number(retryAfterMs);
560
- if (!Number.isFinite(delay) || delay < 250 || delay > 300000) throw new Error("retryAfterMs must be 250-300000");
561
- preserve(runtime, "queue", item, "run-failed", current);
562
- item.status = item.attempts >= 3 ? "dead-letter" : "pending";
563
- item.lease = null; item.lastError = message; item.updatedAt = current;
564
- item.availableAt = new Date(new Date(current).getTime() + delay).toISOString();
565
- if (item.status === "dead-letter") item.completedAt = current;
566
- lane.status = "completed"; lane.updatedAt = current;
567
- runtime.health.host = "failed"; runtime.health.worker = "degraded"; runtime.health.lastTickAt = current;
568
- runtime.revision += 1;
569
- const receipt = appendReceipt(runtime, item.status === "dead-letter" ? "run-dead-letter" : "run-retry",
570
- item.queueId, current, { attempt: item.attempts });
571
- await writeJson(paths.gatewayRuntimePath, runtime);
572
- return { item, receipt, policyEnabled: policy.enabled && !policy.killSwitch };
573
- });
574
- }
575
-
576
- export async function deliverPrepared({ root = process.cwd(), outboxId, adapter, now = new Date() }) {
577
- if (!adapter || typeof adapter.send !== "function") throw new Error("delivery adapter is unavailable");
578
- const paths = await pathsFor(root);
579
- let prepared;
580
- await withLock(paths, async () => {
581
- const [policy, runtime, channelPolicy, channelRuntime, personas] = await Promise.all([
582
- readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
583
- readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime),
584
- loadChannelPolicy(paths.catalog.root, paths.catalog), loadChannelRuntime(paths.catalog.root, paths.catalog),
585
- loadPersonaRuntime(paths.catalog.root, paths.catalog)
586
- ]);
587
- if (!policy.enabled || policy.killSwitch) throw new Error("gateway was disabled before delivery");
588
- const outbox = runtime.outbox.find((item) => item.outboxId === exactId(outboxId, "outboxId"));
589
- if (!outbox) throw new Error("unknown outbox item");
590
- if (["delivered", "acknowledged"].includes(outbox.status)) { prepared = { duplicate: true, outbox: structuredClone(outbox) }; return; }
591
- if (outbox.status !== "prepared" && outbox.status !== "failed") throw new Error("outbox item is not safely retryable");
592
- if (new Date(outbox.nextAttemptAt) > new Date(timestamp(now))) throw new Error("outbox retry is not due");
593
- const event = channelRuntime.runtime.events.find((item) => item.eventId === outbox.eventId);
594
- if (!event) throw new Error("outbox channel event is missing");
595
- try {
596
- assertActivePersona(personas.policy, personas.runtime, event.agentId, event.projectId, event.groupId);
597
- exactReplyBinding(channelPolicy.policy, event);
598
- }
599
- catch (error) {
600
- const current = timestamp(now);
601
- preserve(runtime, "outbox", outbox, "capability-revoked", current);
602
- outbox.status = "dead-letter"; outbox.lastError = safeText(error.message, "adapterError", 500);
603
- outbox.updatedAt = current;
604
- const queue = runtime.queue.find((item) => item.queueId === outbox.queueId);
605
- if (queue) { queue.status = "dead-letter"; queue.completedAt = current; queue.updatedAt = current; }
606
- runtime.health.adapter = "failed"; runtime.revision += 1;
607
- const receipt = appendReceipt(runtime, "dead-letter", outbox.outboxId, current, { reason: "identity-or-capability-revoked" });
608
- await writeJson(paths.gatewayRuntimePath, runtime);
609
- prepared = { duplicate: false, terminal: true, outbox: structuredClone(outbox), receipt };
610
- return;
611
- }
612
- preserve(runtime, "outbox", outbox, "sending", timestamp(now));
613
- outbox.status = "sending"; outbox.attempts += 1; outbox.updatedAt = timestamp(now); runtime.revision += 1;
614
- appendReceipt(runtime, "sending", outbox.outboxId, outbox.updatedAt, { attempt: outbox.attempts });
615
- await writeJson(paths.gatewayRuntimePath, runtime);
616
- prepared = { duplicate: false, outbox: structuredClone(outbox) };
617
- });
618
- if (prepared.duplicate || prepared.terminal) return prepared;
619
- let outcome;
620
- try { outcome = await adapter.send(structuredClone(prepared.outbox)); }
621
- catch (error) { outcome = { ok: false, effect: "unknown", error: error.message }; }
622
- return withLock(paths, async () => {
623
- const runtime = await readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime);
624
- const outbox = runtime.outbox.find((item) => item.outboxId === prepared.outbox.outboxId);
625
- if (!outbox || outbox.status !== "sending") throw new Error("outbox sending state changed unexpectedly");
626
- const current = timestamp(now);
627
- preserve(runtime, "outbox", outbox, outcome?.ok ? "delivered" : "send-failed", current);
628
- if (outcome?.ok) {
629
- outbox.status = "delivered"; outbox.deliveredAt = current; outbox.adapterReceipt = safeText(String(outcome.receipt || "delivered"), "adapterReceipt", 500);
630
- runtime.health.adapter = "healthy";
631
- const queue = runtime.queue.find((item) => item.queueId === outbox.queueId);
632
- if (queue) { queue.status = "completed"; queue.completedAt = current; queue.updatedAt = current; }
633
- } else if (outcome?.effect === "none") {
634
- outbox.status = outbox.attempts < 3 ? "failed" : "dead-letter";
635
- outbox.lastError = safeText(String(outcome.error || "adapter failure"), "adapterError", 500);
636
- runtime.health.adapter = outbox.status === "failed" ? "degraded" : "failed";
637
- if (outbox.status === "failed") {
638
- const delay = Math.min(300000, Number(outcome.retryAfterMs) || 1000 * (2 ** outbox.attempts));
639
- outbox.nextAttemptAt = new Date(new Date(current).getTime() + delay).toISOString();
640
- } else {
641
- const queue = runtime.queue.find((item) => item.queueId === outbox.queueId);
642
- if (queue) { queue.status = "dead-letter"; queue.completedAt = current; queue.updatedAt = current; }
643
- }
644
- } else {
645
- outbox.status = "delivery-unknown"; outbox.lastError = safeText(String(outcome?.error || "delivery outcome is ambiguous"), "adapterError", 500);
646
- runtime.health.adapter = "failed";
647
- }
648
- outbox.updatedAt = current; runtime.revision += 1;
649
- const receipt = appendReceipt(runtime, outbox.status, outbox.outboxId, current, { adapterReceipt: outbox.adapterReceipt });
650
- await writeJson(paths.gatewayRuntimePath, runtime);
651
- return { outbox, receipt, duplicate: false };
652
- });
653
- }
654
-
655
- export async function updateGatewayHealth({ root = process.cwd(), worker = null, adapter = null, host = null,
656
- now = new Date() } = {}) {
657
- const paths = await pathsFor(root);
658
- return withLock(paths, async () => {
659
- const runtime = await readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime);
660
- for (const [key, value] of Object.entries({ worker, adapter, host })) {
661
- if (value !== null) {
662
- if (!HEALTH_VALUES.has(value)) throw new Error("unsupported gateway health value");
663
- runtime.health[key] = value;
664
- }
665
- }
666
- runtime.health.lastTickAt = timestamp(now); runtime.revision += 1;
667
- await writeJson(paths.gatewayRuntimePath, runtime);
668
- return structuredClone(runtime.health);
669
- });
670
- }
671
-
672
- export async function loadGatewayRuntime(root = process.cwd(), catalog = null) {
673
- const paths = await pathsFor(root, catalog);
674
- const [policy, runtime] = await Promise.all([
675
- readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
676
- readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime)
677
- ]);
678
- return { policy, runtime, ...paths };
679
- }
680
-
681
- export async function inspectGatewayRuntime(root = process.cwd(), catalog = null) {
682
- const paths = await pathsFor(root, catalog); const errors = [];
683
- let policy = emptyPolicy(paths.catalog.root); let runtime = emptyRuntime(paths.catalog.root);
684
- try { policy = await readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy); } catch (error) { errors.push("policy:" + error.message); }
685
- try { runtime = await readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime); } catch (error) { errors.push("runtime:" + error.message); }
686
- return { policy, runtime, errors, ...paths };
687
- }
688
-
689
- export function gatewayRuntimeFindings(policy, runtime) {
690
- const findings = [];
691
- const active = new Set();
692
- const goalIds = new Set();
693
- for (const goal of policy.goals) {
694
- if (!validGoal(goal)) findings.push("invalid-goal:" + (goal?.goalId || "unknown"));
695
- if (goalIds.has(goal.goalId)) findings.push("duplicate-goal:" + goal.goalId);
696
- goalIds.add(goal.goalId);
697
- if (goal.status === "active" && active.has(goal.agentId)) findings.push("multiple-active-goals:" + goal.agentId);
698
- if (goal.status === "active") active.add(goal.agentId);
699
- }
700
- for (const item of policy.history) if (!validPolicyHistory(item)) findings.push("invalid-gateway-policy-history");
701
- const queueIds = new Set(); const dedupeKeys = new Set();
702
- for (const item of runtime.queue) {
703
- if (!validQueue(item)) findings.push("invalid-queue-item:" + (item?.queueId || "unknown"));
704
- if (queueIds.has(item.queueId)) findings.push("duplicate-queue-item:" + item.queueId);
705
- if (dedupeKeys.has(item.dedupeKey)) findings.push("duplicate-queue-dedupe:" + item.dedupeKey);
706
- queueIds.add(item.queueId); dedupeKeys.add(item.dedupeKey);
707
- }
708
- const outboxIds = new Set(); const idempotencyKeys = new Set();
709
- for (const item of runtime.outbox) {
710
- if (!validOutbox(item)) findings.push("invalid-outbox-item:" + (item?.outboxId || "unknown"));
711
- if (!queueIds.has(item.queueId)) findings.push("orphan-outbox-item:" + item.outboxId);
712
- if (outboxIds.has(item.outboxId)) findings.push("duplicate-outbox-item:" + item.outboxId);
713
- if (idempotencyKeys.has(item.idempotencyKey)) findings.push("duplicate-outbox-idempotency:" + item.idempotencyKey);
714
- outboxIds.add(item.outboxId); idempotencyKeys.add(item.idempotencyKey);
715
- }
716
- for (const item of runtime.receipts) if (!validReceipt(item)
717
- || (!queueIds.has(item.objectId) && !outboxIds.has(item.objectId))) findings.push("invalid-gateway-receipt:" + (item?.id || "unknown"));
718
- for (const item of runtime.history) if (!validHistory(item)) findings.push("invalid-gateway-history:" + (item?.objectId || "unknown"));
719
- const leased = runtime.lanes.filter((item) => item.status === "leased");
720
- for (const lane of runtime.lanes) {
721
- if (!validLane(lane)) findings.push("invalid-agent-lane:" + (lane?.queueId || "unknown"));
722
- const queue = runtime.queue.find((item) => item.queueId === lane.queueId);
723
- if (!queue || (lane.status === "leased" && (queue.status !== "leased" || queue.lease?.workerId !== lane.workerId))) {
724
- findings.push("agent-lane-queue-mismatch:" + (lane.queueId || "unknown"));
725
- }
726
- }
727
- if (new Set(leased.map((item) => item.agentId)).size !== leased.length) findings.push("duplicate-agent-lane");
728
- if (!validHealth(runtime.health)) findings.push("invalid-gateway-health");
729
- return findings;
730
- }
731
-
732
- export function gatewayHealthFindings(policy, runtime, { now = new Date(), staleAfterMs = 180000 } = {}) {
733
- if (!policy.enabled || policy.killSwitch) return [];
734
- const findings = [];
735
- if (runtime.health.gateway !== "running") findings.push("gateway-not-running");
736
- if (runtime.health.scheduler !== "healthy") findings.push("scheduler-not-healthy");
737
- if (runtime.health.queue !== "healthy") findings.push("queue-not-healthy");
738
- if (!new Set(["healthy", "degraded"]).has(runtime.health.worker)) findings.push("worker-not-healthy");
739
- if (!new Set(["healthy", "degraded"]).has(runtime.health.adapter)) findings.push("adapter-not-healthy");
740
- const current = now instanceof Date ? now : new Date(now);
741
- const tick = runtime.health.lastTickAt === null ? null : new Date(runtime.health.lastTickAt);
742
- const reconciliation = runtime.health.lastReconciledAt === null ? null : new Date(runtime.health.lastReconciledAt);
743
- if (!tick || current.getTime() - tick.getTime() > staleAfterMs) findings.push("worker-heartbeat-stale");
744
- if (!reconciliation || current.getTime() - reconciliation.getTime() > staleAfterMs) findings.push("scheduler-heartbeat-stale");
745
- return findings;
746
- }
747
-
748
- export async function gatewayContext({ root = process.cwd(), agentId = null } = {}) {
749
- const { policy, runtime } = await loadGatewayRuntime(root);
750
- const findings = gatewayRuntimeFindings(policy, runtime);
751
- if (findings.length) throw new Error("gateway runtime failed closed: " + findings.join(", "));
752
- const goals = policy.goals.filter((item) => agentId === null || item.agentId === exactId(agentId, "agentId"));
753
- const queue = runtime.queue.filter((item) => agentId === null || item.agentId === exactId(agentId, "agentId"));
754
- const queueIds = new Set(queue.map((item) => item.queueId));
755
- return { schema: "agentspine.gateway-context/v1", enabled: policy.enabled, killSwitch: policy.killSwitch,
756
- goals: structuredClone(goals), queue: structuredClone(queue),
757
- outbox: structuredClone(runtime.outbox.filter((item) => queueIds.has(item.queueId))),
758
- health: structuredClone(runtime.health), healthFindings: gatewayHealthFindings(policy, runtime),
759
- authority: "execution-state-only" };
760
- }
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { open, readFile, stat, unlink, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { buildCatalog } from "./catalog.js";
5
+ import { isFileLockContention, replaceFileWithRetry } from "./filesystem-retry.js";
6
+ import { attentionFindings, loadAttention } from "./attention.js";
7
+ import {
8
+ channelRuntimeFindings, loadChannelPolicy, loadChannelRuntime
9
+ } from "./channel-runtime.js";
10
+ import { loadGraph } from "./graph.js";
11
+ import { projectStateDir } from "./paths.js";
12
+ import { loadPersonaRuntime, personaRuntimeFindings } from "./persona-runtime.js";
13
+ import { evaluateVoiceOutput } from "./voice-runtime.js";
14
+
15
+ export const GATEWAY_POLICY_SCHEMA = "agentspine.gateway-policy/v1";
16
+ export const GATEWAY_RUNTIME_SCHEMA = "agentspine.gateway-runtime/v1";
17
+ export const GATEWAY_EVENT_SCHEMA = "agentspine.gateway-event/v1";
18
+ export const GOAL_PLAN_SCHEMA = "agentspine.goal-plan/v1";
19
+ export const KNOWLEDGE_GAP_SCHEMA = "agentspine.knowledge-gap/v1";
20
+ export const EXECUTION_OUTCOME_SCHEMA = "agentspine.execution-outcome/v1";
21
+ export const EXECUTION_ATTEMPT_SCHEMA = "agentspine.execution-attempt/v1";
22
+ export const STRATEGY_TRANSFER_PROOF_SCHEMA = "agentspine.strategy-transfer-proof/v1";
23
+
24
+ const CONFIRMATION = "local-owner-confirmed";
25
+ const MAX_BYTES = 8 * 1024 * 1024;
26
+ const ID_RE = /^[A-Za-z0-9][A-Za-z0-9:_.@/+~-]{0,255}$/;
27
+ const ROUTE_RE = /^-?[A-Za-z0-9][A-Za-z0-9:_.@/+~-]{0,255}$/;
28
+ const GOAL_STATUSES = new Set(["active", "blocked", "completed", "cancelled"]);
29
+ const PLAN_STEP_STATUSES = new Set(["pending", "active", "blocked", "completed", "cancelled"]);
30
+ const QUEUE_STATUSES = new Set(["pending", "leased", "awaiting-delivery", "completed", "blocked", "dead-letter", "cancelled"]);
31
+ const OUTBOX_STATUSES = new Set(["prepared", "sending", "delivered", "failed", "dead-letter", "delivery-unknown", "acknowledged"]);
32
+ const WAKE_KINDS = new Set(["direct-message", "deadline", "promise", "resolved-blocker", "assignment", "follow-up", "relationship"]);
33
+ const PRIORITY = { "direct-message": 100, deadline: 90, promise: 90, "resolved-blocker": 80, assignment: 70, "follow-up": 60, relationship: 50 };
34
+ const SECRET_RE = /-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:sk|gh[opusu])_[A-Za-z0-9_-]{20,}\b|\bBearer\s+[A-Za-z0-9._~+/-]{20,}|\b(?:api[-_ ]?key|token|password|secret)\s*[:=]\s*\S{8,}/i;
35
+ const SECRET_KEY_RE = /"(?:api[-_ ]?key|token|password|secret|credential)"\s*:/i;
36
+ const AUTHORITY_RE = /\b(?:permission|rights?|roles?|owner|trusted|delegat|authorized|approval|production|payment|spending|tool capability|send capability)\b/i;
37
+ const HEALTH_VALUES = new Set(["stopped", "running", "unknown", "healthy", "degraded", "failed"]);
38
+ const KNOWLEDGE_EVIDENCE = new Set(["owner-input", "objective-observation"]);
39
+ const METRIC_OPERATORS = new Set(["gte", "lte", "eq"]);
40
+
41
+ function emptyPolicy(root) {
42
+ return { schema: GATEWAY_POLICY_SCHEMA, root, revision: 0, enabled: false, killSwitch: false, goals: [], history: [] };
43
+ }
44
+
45
+ function emptyRuntime(root) {
46
+ return { schema: GATEWAY_RUNTIME_SCHEMA, root, revision: 0, queue: [], lanes: [], outbox: [], receipts: [], history: [],
47
+ health: { gateway: "stopped", adapter: "unknown", scheduler: "unknown", queue: "healthy", worker: "unknown", host: "unknown", lastTickAt: null, lastReconciledAt: null } };
48
+ }
49
+
50
+ function exactId(value, field, nullable = false) {
51
+ if ((value === null || value === undefined || value === "") && nullable) return null;
52
+ if (typeof value !== "string" || !ID_RE.test(value) || value.includes("*")) throw new Error(field + " must be an exact stable ID without wildcards");
53
+ return value;
54
+ }
55
+
56
+ function safeText(value, field, maximum = 1000) {
57
+ if (typeof value !== "string" || !value.trim()) throw new Error(field + " is required");
58
+ const text = value.trim().replace(/\s+/g, " ").slice(0, maximum);
59
+ if (SECRET_RE.test(text)) throw new Error(field + " appears to contain a secret");
60
+ return text;
61
+ }
62
+
63
+ function timestamp(value = new Date()) {
64
+ const date = value instanceof Date ? value : new Date(value);
65
+ if (!Number.isFinite(date.getTime())) throw new Error("timestamp is invalid");
66
+ return date.toISOString();
67
+ }
68
+
69
+ function sha256(value) { return createHash("sha256").update(value).digest("hex"); }
70
+
71
+ function safeCheckpoint(value) {
72
+ if (value === null || value === undefined) return null;
73
+ let content;
74
+ try { content = JSON.stringify(value); } catch { throw new Error("checkpoint must be JSON serializable"); }
75
+ if (!content || Buffer.byteLength(content) > 16384) throw new Error("checkpoint exceeds 16 KiB");
76
+ if (SECRET_RE.test(content) || SECRET_KEY_RE.test(content) || AUTHORITY_RE.test(content)) {
77
+ throw new Error("checkpoint contains secret- or authority-shaped content");
78
+ }
79
+ return JSON.parse(content);
80
+ }
81
+
82
+ function safeKnowledgeText(value, field, maximum) {
83
+ const text = safeText(value, field, maximum);
84
+ if (AUTHORITY_RE.test(text)) throw new Error(field + " cannot grant or describe authority");
85
+ return text;
86
+ }
87
+
88
+ function knowledgeGapSubjectMaterial(gap) {
89
+ return {
90
+ goalId: gap.goalId, goalStepId: gap.goalStepId, planDefinitionsDigest: gap.planDefinitionsDigest,
91
+ question: gap.question, reason: gap.reason, requiredEvidence: gap.requiredEvidence,
92
+ authority: "context-only"
93
+ };
94
+ }
95
+
96
+ function knowledgeGapRequestMaterial(gap) {
97
+ return {
98
+ subjectDigest: gap.subjectDigest, requestedAt: gap.requestedAt,
99
+ requestedByQueueId: gap.requestedByQueueId, authority: "context-only"
100
+ };
101
+ }
102
+
103
+ function knowledgeGapResolutionMaterial(gap) {
104
+ return {
105
+ requestDigest: gap.requestDigest, answer: gap.answer, answerSource: gap.answerSource,
106
+ sourceDigest: gap.sourceDigest, resolvedAt: gap.resolvedAt,
107
+ resolvedBySubjectId: gap.resolvedBySubjectId, authority: "context-only"
108
+ };
109
+ }
110
+
111
+ function validKnowledgeGap(gap) {
112
+ if (!(gap && gap.schema === KNOWLEDGE_GAP_SCHEMA && ID_RE.test(gap.gapId || "")
113
+ && ID_RE.test(gap.goalId || "") && ID_RE.test(gap.goalStepId || "")
114
+ && /^[a-f0-9]{64}$/.test(gap.planDefinitionsDigest || "")
115
+ && typeof gap.question === "string" && gap.question.length <= 500
116
+ && typeof gap.reason === "string" && gap.reason.length <= 500
117
+ && KNOWLEDGE_EVIDENCE.has(gap.requiredEvidence)
118
+ && ID_RE.test(gap.requestedByQueueId || "")
119
+ && Number.isFinite(new Date(gap.requestedAt).getTime())
120
+ && /^[a-f0-9]{64}$/.test(gap.subjectDigest || "")
121
+ && /^[a-f0-9]{64}$/.test(gap.requestDigest || "")
122
+ && ["open", "resolved"].includes(gap.status) && gap.authority === "context-only")) return false;
123
+ try {
124
+ if (safeKnowledgeText(gap.question, "knowledgeGap.question", 500) !== gap.question
125
+ || safeKnowledgeText(gap.reason, "knowledgeGap.reason", 500) !== gap.reason) return false;
126
+ } catch { return false; }
127
+ if (gap.subjectDigest !== sha256(JSON.stringify(knowledgeGapSubjectMaterial(gap)))
128
+ || gap.gapId !== "knowledge-gap:" + gap.subjectDigest.slice(0, 32)
129
+ || gap.requestDigest !== sha256(JSON.stringify(knowledgeGapRequestMaterial(gap)))) return false;
130
+ if (gap.status === "open") return gap.answer === null && gap.answerSource === null && gap.sourceDigest === null
131
+ && gap.resolvedAt === null && gap.resolvedBySubjectId === null && gap.resolutionDigest === null;
132
+ if (!(typeof gap.answer === "string" && gap.answer.length <= 2000
133
+ && gap.answerSource === gap.requiredEvidence
134
+ && (gap.sourceDigest === null || /^[a-f0-9]{64}$/.test(gap.sourceDigest))
135
+ && Number.isFinite(new Date(gap.resolvedAt).getTime()) && ID_RE.test(gap.resolvedBySubjectId || "")
136
+ && /^[a-f0-9]{64}$/.test(gap.resolutionDigest || ""))) return false;
137
+ if (new Date(gap.resolvedAt) < new Date(gap.requestedAt)) return false;
138
+ if ((gap.answerSource === "objective-observation") !== (gap.sourceDigest !== null)) return false;
139
+ try { if (safeKnowledgeText(gap.answer, "knowledgeGap.answer", 2000) !== gap.answer) return false; } catch { return false; }
140
+ return gap.resolutionDigest === sha256(JSON.stringify(knowledgeGapResolutionMaterial(gap)));
141
+ }
142
+
143
+ function executionDecisionMaterial(execution) {
144
+ return {
145
+ requiredCapabilities: execution.requiredCapabilities, strategies: execution.strategies,
146
+ verification: execution.verification, selectedStrategyId: execution.selectedStrategyId,
147
+ ...(execution.explorationMaxAttempts === undefined ? {} : {
148
+ explorationMaxAttempts: execution.explorationMaxAttempts,
149
+ explorationOrder: execution.explorationOrder
150
+ }),
151
+ ...(execution.transferKey === undefined ? {} : {
152
+ transferKey: execution.transferKey, transferMaxAgeDays: execution.transferMaxAgeDays,
153
+ transferProof: execution.transferProof
154
+ }),
155
+ authority: "context-only-decision"
156
+ };
157
+ }
158
+
159
+ function sufficientExecutionStrategies(execution) {
160
+ const required = new Set(execution.requiredCapabilities);
161
+ return execution.strategies.filter((strategy) => strategy.capabilities.every((capability) => ID_RE.test(capability))
162
+ && [...required].every((capability) => strategy.capabilities.includes(capability)))
163
+ .sort((left, right) => left.risk - right.risk || left.cost - right.cost
164
+ || left.strategyId.localeCompare(right.strategyId));
165
+ }
166
+
167
+ function selectedExecutionStrategy(execution) {
168
+ const sufficient = sufficientExecutionStrategies(execution);
169
+ const minimumRisk = sufficient[0]?.risk;
170
+ const transferred = execution.transferProof && sufficient.find((strategy) =>
171
+ strategy.strategyId === execution.transferProof.strategyId && strategy.risk === minimumRisk);
172
+ return transferred || sufficient[0] || null;
173
+ }
174
+
175
+ function expectedExplorationOrder(execution) {
176
+ const sufficient = sufficientExecutionStrategies(execution);
177
+ const selected = selectedExecutionStrategy(execution);
178
+ if (!selected) return [];
179
+ return [selected, ...sufficient.filter((strategy) => strategy.risk === selected.risk
180
+ && strategy.strategyId !== selected.strategyId)].map((strategy) => strategy.strategyId);
181
+ }
182
+
183
+ function strategyTransferProofMaterial(proof) {
184
+ return {
185
+ transferKey: proof.transferKey, strategyId: proof.strategyId, maxAgeDays: proof.maxAgeDays,
186
+ evidence: proof.evidence, authority: "context-only-transfer"
187
+ };
188
+ }
189
+
190
+ function validStrategyTransferProof(proof) {
191
+ if (!(proof && proof.schema === STRATEGY_TRANSFER_PROOF_SCHEMA
192
+ && ID_RE.test(proof.proofId || "") && ID_RE.test(proof.transferKey || "")
193
+ && ID_RE.test(proof.strategyId || "") && Number.isInteger(proof.maxAgeDays)
194
+ && proof.maxAgeDays >= 1 && proof.maxAgeDays <= 90 && Array.isArray(proof.evidence)
195
+ && proof.evidence.length >= 2 && proof.evidence.length <= 8
196
+ && new Set(proof.evidence.map((item) => item.sourceGoalId)).size === proof.evidence.length
197
+ && new Set(proof.evidence.map((item) => item.sourceDigest)).size === proof.evidence.length
198
+ && proof.evidence.every((item) => item && ID_RE.test(item.sourceGoalId || "")
199
+ && ID_RE.test(item.sourceStepId || "") && ID_RE.test(item.outcomeId || "")
200
+ && /^[a-f0-9]{64}$/.test(item.outcomeDigest || "")
201
+ && /^[a-f0-9]{64}$/.test(item.sourceDigest || "")
202
+ && Number.isFinite(new Date(item.completedAt).getTime()))
203
+ && /^[a-f0-9]{64}$/.test(proof.proofDigest || "")
204
+ && proof.authority === "context-only-transfer")) return false;
205
+ const digest = sha256(JSON.stringify(strategyTransferProofMaterial(proof)));
206
+ return proof.proofDigest === digest && proof.proofId === "strategy-transfer:" + digest.slice(0, 32);
207
+ }
208
+
209
+ function sameStrategy(left, right) {
210
+ return JSON.stringify(left) === JSON.stringify(right);
211
+ }
212
+
213
+ function sameVerification(left, right) {
214
+ return JSON.stringify(left) === JSON.stringify(right);
215
+ }
216
+
217
+ function collectStrategyTransferEvidence(goals, execution, strategy, scope) {
218
+ if (!execution.transferKey) return { evidence: [], regressed: false };
219
+ const before = new Date(scope.before);
220
+ const cutoff = new Date(before.getTime() - execution.transferMaxAgeDays * 86400000);
221
+ const evidence = []; let regressed = false;
222
+ for (const goal of goals) {
223
+ if (goal.goalId === scope.goalId || goal.projectId !== scope.projectId || goal.groupId !== scope.groupId || !goal.plan) continue;
224
+ for (const step of goal.plan.steps) {
225
+ const prior = step.execution;
226
+ const completedAt = step.completedAt && new Date(step.completedAt);
227
+ const updatedAt = new Date(step.updatedAt);
228
+ if (!prior || prior.transferKey !== execution.transferKey
229
+ || !sameVerification(prior.verification, execution.verification)
230
+ || !sameStrategy(prior.strategies.find((item) => item.strategyId === strategy.strategyId), strategy)
231
+ || updatedAt > before) continue;
232
+ for (const outcome of step.executionOutcomes || []) {
233
+ if (outcome.strategyId !== strategy.strategyId) continue;
234
+ const observedAt = new Date(outcome.observedAt);
235
+ if (observedAt < cutoff || observedAt > before || observedAt < new Date(goal.createdAt)) continue;
236
+ if (!outcome.passed || outcome.blockingDefect) { regressed = true; continue; }
237
+ if (step.status !== "completed" || !completedAt || completedAt < cutoff || completedAt > before
238
+ || observedAt > completedAt) continue;
239
+ evidence.push({ sourceGoalId: goal.goalId, sourceStepId: step.stepId, outcomeId: outcome.outcomeId,
240
+ outcomeDigest: outcome.digest, sourceDigest: outcome.sourceDigest, completedAt: step.completedAt });
241
+ }
242
+ }
243
+ }
244
+ const unique = [];
245
+ for (const item of evidence.sort((left, right) => left.completedAt.localeCompare(right.completedAt)
246
+ || left.outcomeId.localeCompare(right.outcomeId))) {
247
+ if (!unique.some((entry) => entry.sourceGoalId === item.sourceGoalId || entry.sourceDigest === item.sourceDigest)) unique.push(item);
248
+ }
249
+ return { evidence: unique.slice(-8), regressed };
250
+ }
251
+
252
+ function createStrategyTransferProof(goals, execution, scope) {
253
+ if (!execution.transferKey) return null;
254
+ const required = new Set(execution.requiredCapabilities);
255
+ const sufficient = execution.strategies.filter((strategy) =>
256
+ [...required].every((capability) => strategy.capabilities.includes(capability)));
257
+ const minimumRisk = Math.min(...sufficient.map((strategy) => strategy.risk));
258
+ const proven = sufficient.filter((strategy) => strategy.risk === minimumRisk).map((strategy) => ({
259
+ strategy, ...collectStrategyTransferEvidence(goals, execution, strategy, scope)
260
+ })).filter((item) => !item.regressed && item.evidence.length >= 2)
261
+ .sort((left, right) => right.evidence.length - left.evidence.length
262
+ || left.strategy.cost - right.strategy.cost || left.strategy.strategyId.localeCompare(right.strategy.strategyId));
263
+ if (!proven.length) return null;
264
+ const proof = { schema: STRATEGY_TRANSFER_PROOF_SCHEMA, proofId: null,
265
+ transferKey: execution.transferKey, strategyId: proven[0].strategy.strategyId,
266
+ maxAgeDays: execution.transferMaxAgeDays, evidence: proven[0].evidence,
267
+ proofDigest: null, authority: "context-only-transfer" };
268
+ proof.proofDigest = sha256(JSON.stringify(strategyTransferProofMaterial(proof)));
269
+ proof.proofId = "strategy-transfer:" + proof.proofDigest.slice(0, 32);
270
+ return proof;
271
+ }
272
+
273
+ function validGoalTransferProofs(goal, goals) {
274
+ if (!goal.plan) return true;
275
+ return goal.plan.steps.every((step) => {
276
+ if (!step.execution?.transferProof) return true;
277
+ const expected = createStrategyTransferProof(goals, step.execution, {
278
+ goalId: goal.goalId, projectId: goal.projectId, groupId: goal.groupId, before: goal.createdAt
279
+ });
280
+ return JSON.stringify(expected) === JSON.stringify(step.execution.transferProof);
281
+ });
282
+ }
283
+
284
+ function validExecutionDecision(execution) {
285
+ if (!(execution && execution.authority === "context-only-decision"
286
+ && Array.isArray(execution.requiredCapabilities) && execution.requiredCapabilities.length > 0
287
+ && execution.requiredCapabilities.length <= 16
288
+ && new Set(execution.requiredCapabilities).size === execution.requiredCapabilities.length
289
+ && execution.requiredCapabilities.every((capability) => ID_RE.test(capability || ""))
290
+ && Array.isArray(execution.strategies) && execution.strategies.length >= 2 && execution.strategies.length <= 8
291
+ && new Set(execution.strategies.map((strategy) => strategy?.strategyId)).size === execution.strategies.length
292
+ && execution.strategies.every((strategy) => strategy && ID_RE.test(strategy.strategyId || "")
293
+ && Array.isArray(strategy.capabilities) && strategy.capabilities.length > 0 && strategy.capabilities.length <= 16
294
+ && new Set(strategy.capabilities).size === strategy.capabilities.length
295
+ && strategy.capabilities.every((capability) => ID_RE.test(capability || ""))
296
+ && Number.isInteger(strategy.risk) && strategy.risk >= 0 && strategy.risk <= 100
297
+ && Number.isInteger(strategy.cost) && strategy.cost >= 0 && strategy.cost <= 100)
298
+ && execution.verification && ID_RE.test(execution.verification.evaluatorId || "")
299
+ && ID_RE.test(execution.verification.metric || "") && METRIC_OPERATORS.has(execution.verification.operator)
300
+ && Number.isFinite(execution.verification.threshold)
301
+ && Number.isInteger(execution.verification.minCases) && execution.verification.minCases >= 1
302
+ && execution.verification.minCases <= 100000 && ID_RE.test(execution.selectedStrategyId || "")
303
+ && (execution.explorationMaxAttempts === undefined || (Number.isInteger(execution.explorationMaxAttempts)
304
+ && execution.explorationMaxAttempts >= 2 && execution.explorationMaxAttempts <= 4
305
+ && Array.isArray(execution.explorationOrder)
306
+ && execution.explorationOrder.length === execution.explorationMaxAttempts
307
+ && new Set(execution.explorationOrder).size === execution.explorationOrder.length
308
+ && execution.explorationOrder.every((strategyId) => ID_RE.test(strategyId || ""))))
309
+ && (execution.transferKey === undefined || (ID_RE.test(execution.transferKey || "")
310
+ && Number.isInteger(execution.transferMaxAgeDays) && execution.transferMaxAgeDays >= 1
311
+ && execution.transferMaxAgeDays <= 90
312
+ && (execution.transferProof === null || (validStrategyTransferProof(execution.transferProof)
313
+ && execution.transferProof.transferKey === execution.transferKey
314
+ && execution.transferProof.maxAgeDays === execution.transferMaxAgeDays))))
315
+ && /^[a-f0-9]{64}$/.test(execution.decisionDigest || ""))) return false;
316
+ const selected = selectedExecutionStrategy(execution);
317
+ return selected?.strategyId === execution.selectedStrategyId
318
+ && (execution.explorationMaxAttempts === undefined
319
+ || JSON.stringify(execution.explorationOrder)
320
+ === JSON.stringify(expectedExplorationOrder(execution).slice(0, execution.explorationMaxAttempts)))
321
+ && execution.decisionDigest === sha256(JSON.stringify(executionDecisionMaterial(execution)));
322
+ }
323
+
324
+ function createExecutionDecision(value, field, transferContext) {
325
+ if (value === null || value === undefined) return null;
326
+ const execution = {
327
+ requiredCapabilities: Array.isArray(value.requiredCapabilities)
328
+ ? value.requiredCapabilities.map((capability) => exactId(capability, `${field}.requiredCapabilities`)) : [],
329
+ strategies: Array.isArray(value.strategies) ? value.strategies.map((strategy, index) => ({
330
+ strategyId: exactId(strategy?.strategyId, `${field}.strategies[${index}].strategyId`),
331
+ capabilities: Array.isArray(strategy?.capabilities)
332
+ ? strategy.capabilities.map((capability) => exactId(capability, `${field}.strategies[${index}].capabilities`)) : [],
333
+ risk: Number(strategy?.risk), cost: Number(strategy?.cost)
334
+ })) : [],
335
+ verification: {
336
+ evaluatorId: exactId(value.verification?.evaluatorId, `${field}.verification.evaluatorId`),
337
+ metric: exactId(value.verification?.metric, `${field}.verification.metric`),
338
+ operator: value.verification?.operator,
339
+ threshold: Number(value.verification?.threshold), minCases: Number(value.verification?.minCases)
340
+ },
341
+ selectedStrategyId: null, decisionDigest: null, authority: "context-only-decision"
342
+ };
343
+ if (value.transfer !== undefined && value.transfer !== null) {
344
+ execution.transferKey = exactId(value.transfer?.transferKey, `${field}.transfer.transferKey`);
345
+ execution.transferMaxAgeDays = Number(value.transfer?.maxAgeDays);
346
+ execution.transferProof = null;
347
+ execution.transferProof = createStrategyTransferProof(transferContext.goals, execution, transferContext.scope);
348
+ }
349
+ execution.selectedStrategyId = selectedExecutionStrategy(execution)?.strategyId || null;
350
+ if (value.exploration !== undefined && value.exploration !== null) {
351
+ execution.explorationMaxAttempts = Number(value.exploration?.maxAttempts);
352
+ execution.explorationOrder = expectedExplorationOrder(execution).slice(0, execution.explorationMaxAttempts);
353
+ }
354
+ execution.decisionDigest = sha256(JSON.stringify(executionDecisionMaterial(execution)));
355
+ if (!validExecutionDecision(execution)) {
356
+ throw new Error(`${field} requires 2-8 bounded strategies and one objective verification gate`);
357
+ }
358
+ return execution;
359
+ }
360
+
361
+ function executionOutcomeMaterial(outcome) {
362
+ return {
363
+ queueId: outcome.queueId, decisionDigest: outcome.decisionDigest, strategyId: outcome.strategyId,
364
+ ...(outcome.attempt === undefined ? {} : {
365
+ attempt: outcome.attempt, previousOutcomeDigest: outcome.previousOutcomeDigest
366
+ }),
367
+ capabilitiesUsed: outcome.capabilitiesUsed, evaluatorId: outcome.evaluatorId, metric: outcome.metric,
368
+ value: outcome.value, cases: outcome.cases, blockingDefect: outcome.blockingDefect,
369
+ sourceDigest: outcome.sourceDigest, observedAt: outcome.observedAt, passed: outcome.passed,
370
+ authority: "objective-evidence-only"
371
+ };
372
+ }
373
+
374
+ function currentExecutionAttempt(execution, outcomes = []) {
375
+ if (execution.explorationMaxAttempts === undefined) return null;
376
+ if (outcomes.length >= execution.explorationMaxAttempts || outcomes.some((outcome) => outcome.passed || outcome.blockingDefect)) {
377
+ return null;
378
+ }
379
+ return {
380
+ schema: EXECUTION_ATTEMPT_SCHEMA,
381
+ attempt: outcomes.length + 1, maxAttempts: execution.explorationMaxAttempts,
382
+ strategyId: execution.explorationOrder[outcomes.length],
383
+ previousOutcomeDigest: outcomes.at(-1)?.digest || null,
384
+ decisionDigest: execution.decisionDigest, authority: "context-only-attempt"
385
+ };
386
+ }
387
+
388
+ export function executionAttemptForStep(step) {
389
+ if (!step?.execution || !Array.isArray(step.executionOutcomes)) return null;
390
+ return currentExecutionAttempt(step.execution, step.executionOutcomes);
391
+ }
392
+
393
+ function metricPassed(operator, value, threshold) {
394
+ if (operator === "gte") return value >= threshold;
395
+ if (operator === "lte") return value <= threshold;
396
+ return value === threshold;
397
+ }
398
+
399
+ function validExecutionOutcome(outcome, execution, expectedStrategyId = execution.selectedStrategyId,
400
+ expectedAttempt = null, previousOutcomeDigest = null) {
401
+ if (!(outcome && outcome.schema === EXECUTION_OUTCOME_SCHEMA && ID_RE.test(outcome.outcomeId || "")
402
+ && ID_RE.test(outcome.queueId || "") && outcome.decisionDigest === execution.decisionDigest
403
+ && outcome.strategyId === expectedStrategyId
404
+ && (execution.explorationMaxAttempts === undefined
405
+ ? outcome.attempt === undefined && outcome.previousOutcomeDigest === undefined
406
+ : outcome.attempt === expectedAttempt && outcome.previousOutcomeDigest === previousOutcomeDigest)
407
+ && Array.isArray(outcome.capabilitiesUsed)
408
+ && outcome.capabilitiesUsed.length <= 16 && new Set(outcome.capabilitiesUsed).size === outcome.capabilitiesUsed.length
409
+ && outcome.capabilitiesUsed.every((capability) => ID_RE.test(capability || ""))
410
+ && outcome.evaluatorId === execution.verification.evaluatorId && outcome.metric === execution.verification.metric
411
+ && Number.isFinite(outcome.value) && Number.isInteger(outcome.cases) && outcome.cases >= 0
412
+ && typeof outcome.blockingDefect === "boolean" && /^[a-f0-9]{64}$/.test(outcome.sourceDigest || "")
413
+ && Number.isFinite(new Date(outcome.observedAt).getTime()) && typeof outcome.passed === "boolean"
414
+ && /^[a-f0-9]{64}$/.test(outcome.digest || "") && outcome.authority === "objective-evidence-only")) return false;
415
+ const strategy = execution.strategies.find((item) => item.strategyId === expectedStrategyId);
416
+ const used = new Set(outcome.capabilitiesUsed);
417
+ const expectedPass = execution.requiredCapabilities.every((capability) => used.has(capability))
418
+ && outcome.capabilitiesUsed.every((capability) => strategy.capabilities.includes(capability))
419
+ && outcome.cases >= execution.verification.minCases && !outcome.blockingDefect
420
+ && metricPassed(execution.verification.operator, outcome.value, execution.verification.threshold);
421
+ const digest = sha256(JSON.stringify(executionOutcomeMaterial(outcome)));
422
+ return outcome.passed === expectedPass && outcome.digest === digest
423
+ && outcome.outcomeId === "execution-outcome:" + digest.slice(0, 32);
424
+ }
425
+
426
+ function validExecutionOutcomeSequence(execution, outcomes) {
427
+ if (execution.explorationMaxAttempts === undefined) {
428
+ return outcomes.every((outcome) => validExecutionOutcome(outcome, execution));
429
+ }
430
+ if (outcomes.length > execution.explorationMaxAttempts
431
+ || new Set(outcomes.map((outcome) => outcome.sourceDigest)).size !== outcomes.length) return false;
432
+ let previous = null;
433
+ for (let index = 0; index < outcomes.length; index += 1) {
434
+ const outcome = outcomes[index];
435
+ if (!validExecutionOutcome(outcome, execution, execution.explorationOrder[index], index + 1, previous)) return false;
436
+ if (index < outcomes.length - 1 && (outcome.passed || outcome.blockingDefect)) return false;
437
+ previous = outcome.digest;
438
+ }
439
+ return true;
440
+ }
441
+
442
+ function reviewExecutionResult(execution, priorOutcomes, queueId, report, now) {
443
+ const attempt = currentExecutionAttempt(execution, priorOutcomes);
444
+ const expectedStrategyId = attempt?.strategyId || execution.selectedStrategyId;
445
+ if (!report || report.strategyId !== expectedStrategyId || !Array.isArray(report.capabilitiesUsed)
446
+ || !report.outcome || report.outcome.evaluatorId !== execution.verification.evaluatorId
447
+ || report.outcome.metric !== execution.verification.metric) {
448
+ return { passed: false, outcome: null, reason: "Objective execution evidence is missing or invalid." };
449
+ }
450
+ let outcome;
451
+ try {
452
+ outcome = {
453
+ schema: EXECUTION_OUTCOME_SCHEMA, outcomeId: null, queueId,
454
+ decisionDigest: execution.decisionDigest, strategyId: report.strategyId,
455
+ ...(attempt === null ? {} : { attempt: attempt.attempt, previousOutcomeDigest: attempt.previousOutcomeDigest }),
456
+ capabilitiesUsed: report.capabilitiesUsed.map((capability) => exactId(capability, "execution.capabilitiesUsed")),
457
+ evaluatorId: report.outcome.evaluatorId, metric: report.outcome.metric,
458
+ value: Number(report.outcome.value), cases: Number(report.outcome.cases),
459
+ blockingDefect: report.outcome.blockingDefect === true, sourceDigest: String(report.outcome.sourceDigest || ""),
460
+ observedAt: timestamp(report.outcome.observedAt || now), passed: false, digest: null,
461
+ authority: "objective-evidence-only"
462
+ };
463
+ if (new Date(outcome.observedAt) > new Date(now)
464
+ || (attempt && priorOutcomes.some((prior) => prior.sourceDigest === outcome.sourceDigest))) {
465
+ return { passed: false, outcome: null, reason: "Objective execution evidence is missing or invalid." };
466
+ }
467
+ const strategy = execution.strategies.find((item) => item.strategyId === expectedStrategyId);
468
+ const used = new Set(outcome.capabilitiesUsed);
469
+ outcome.passed = execution.requiredCapabilities.every((capability) => used.has(capability))
470
+ && outcome.capabilitiesUsed.every((capability) => strategy.capabilities.includes(capability))
471
+ && outcome.cases >= execution.verification.minCases && !outcome.blockingDefect
472
+ && metricPassed(execution.verification.operator, outcome.value, execution.verification.threshold);
473
+ outcome.digest = sha256(JSON.stringify(executionOutcomeMaterial(outcome)));
474
+ outcome.outcomeId = "execution-outcome:" + outcome.digest.slice(0, 32);
475
+ } catch {
476
+ return { passed: false, outcome: null, reason: "Objective execution evidence is missing or invalid." };
477
+ }
478
+ if (!validExecutionOutcome(outcome, execution, expectedStrategyId, attempt?.attempt ?? null,
479
+ attempt?.previousOutcomeDigest ?? null)
480
+ || !validExecutionOutcomeSequence(execution, [...priorOutcomes, outcome])) {
481
+ return { passed: false, outcome: null, reason: "Objective execution evidence is missing or invalid." };
482
+ }
483
+ const nextAttempt = !outcome.passed && !outcome.blockingDefect
484
+ ? currentExecutionAttempt(execution, [...priorOutcomes, outcome]) : null;
485
+ return { passed: outcome.passed, outcome, nextAttempt,
486
+ reason: outcome.passed ? null : "The objective execution gate did not pass." };
487
+ }
488
+
489
+ function planDefinitionMaterial(steps) {
490
+ return steps.map(({ stepId, agentId, resources, execution, title, successCriterion, dependsOn }) => ({
491
+ stepId, ...(agentId === undefined ? {} : { agentId }),
492
+ ...(resources === undefined ? {} : { resources }), ...(execution === undefined ? {} : { execution }),
493
+ title, successCriterion, dependsOn
494
+ }));
495
+ }
496
+
497
+ function validGoalPlan(plan) {
498
+ if (!(plan && plan.schema === GOAL_PLAN_SCHEMA && plan.authority === "context-only-plan"
499
+ && Number.isInteger(plan.revision) && plan.revision >= 0 && Array.isArray(plan.steps)
500
+ && plan.steps.length > 0 && plan.steps.length <= 32 && /^[a-f0-9]{64}$/.test(plan.definitionsDigest || "")
501
+ && (plan.currentStepId === null || ID_RE.test(plan.currentStepId || "")))) return false;
502
+ const ids = new Set(plan.steps.map((step) => step?.stepId));
503
+ if (ids.size !== plan.steps.length || ids.has(undefined)) return false;
504
+ for (const step of plan.steps) {
505
+ if (!(ID_RE.test(step.stepId || "") && typeof step.title === "string" && step.title.length > 0 && step.title.length <= 500
506
+ && (step.agentId === undefined || ID_RE.test(step.agentId || ""))
507
+ && (step.resources === undefined || (Array.isArray(step.resources) && step.resources.length <= 16
508
+ && new Set(step.resources).size === step.resources.length && step.resources.every((resource) => ID_RE.test(resource || ""))))
509
+ && (step.execution === undefined || step.execution === null || validExecutionDecision(step.execution))
510
+ && (step.executionOutcomes === undefined || (Array.isArray(step.executionOutcomes) && step.executionOutcomes.length <= 8
511
+ && new Set(step.executionOutcomes.map((outcome) => outcome.outcomeId)).size === step.executionOutcomes.length
512
+ && (step.executionOutcomes.length === 0 || (step.execution
513
+ && validExecutionOutcomeSequence(step.execution, step.executionOutcomes)))))
514
+ && typeof step.successCriterion === "string" && step.successCriterion.length > 0 && step.successCriterion.length <= 1000
515
+ && Array.isArray(step.dependsOn) && new Set(step.dependsOn).size === step.dependsOn.length
516
+ && step.dependsOn.every((dependency) => ids.has(dependency) && dependency !== step.stepId)
517
+ && PLAN_STEP_STATUSES.has(step.status)
518
+ && (step.checkpoint === null || (() => { try { safeCheckpoint(step.checkpoint); return true; } catch { return false; } })())
519
+ && (step.blocker === null || (typeof step.blocker === "string" && step.blocker.length <= 500 && !SECRET_RE.test(step.blocker)))
520
+ && (step.completedAt === null || Number.isFinite(new Date(step.completedAt).getTime()))
521
+ && (step.completedByQueueId === null || ID_RE.test(step.completedByQueueId || ""))
522
+ && (step.knowledgeGaps === undefined || (Array.isArray(step.knowledgeGaps) && step.knowledgeGaps.length <= 16
523
+ && new Set(step.knowledgeGaps.map((gap) => gap.gapId)).size === step.knowledgeGaps.length
524
+ && step.knowledgeGaps.every((gap) => validKnowledgeGap(gap)
525
+ && gap.goalStepId === step.stepId && gap.planDefinitionsDigest === plan.definitionsDigest)))
526
+ && Number.isFinite(new Date(step.updatedAt).getTime())
527
+ && (step.executionOutcomes || []).every((outcome) => new Date(outcome.observedAt) <= new Date(step.updatedAt)))) return false;
528
+ }
529
+ const visiting = new Set(); const visited = new Set();
530
+ const visit = (stepId) => {
531
+ if (visiting.has(stepId)) return false;
532
+ if (visited.has(stepId)) return true;
533
+ visiting.add(stepId);
534
+ const step = plan.steps.find((entry) => entry.stepId === stepId);
535
+ if (!step.dependsOn.every(visit)) return false;
536
+ visiting.delete(stepId); visited.add(stepId); return true;
537
+ };
538
+ if (!plan.steps.every((step) => visit(step.stepId))) return false;
539
+ const completed = new Set(plan.steps.filter((step) => step.status === "completed").map((step) => step.stepId));
540
+ if (plan.steps.some((step) => ["completed", "active", "blocked"].includes(step.status)
541
+ && !step.dependsOn.every((dependency) => completed.has(dependency)))) return false;
542
+ if (plan.steps.some((step) => (step.status === "completed") !== (step.completedAt !== null && step.completedByQueueId !== null))) return false;
543
+ if (plan.steps.some((step) => (step.status === "blocked") !== (step.blocker !== null))) return false;
544
+ if (plan.steps.some((step) => (step.knowledgeGaps || []).filter((gap) => gap.status === "open").length > 1)) return false;
545
+ if (plan.steps.some((step) => (step.knowledgeGaps || []).some((gap) => gap.status === "open")
546
+ && step.status !== "blocked")) return false;
547
+ if (plan.steps.some((step) => step.execution && step.status === "completed"
548
+ && !(step.executionOutcomes || []).some((outcome) => outcome.passed))) return false;
549
+ if (plan.definitionsDigest !== sha256(JSON.stringify(planDefinitionMaterial(plan.steps)))) return false;
550
+ const current = plan.steps.filter((step) => ["active", "blocked"].includes(step.status));
551
+ return current.length <= 1 && (plan.currentStepId === null
552
+ ? current.length === 0 : current.length === 1 && current[0].stepId === plan.currentStepId);
553
+ }
554
+
555
+ function activateNextPlanStep(plan, now) {
556
+ const completed = new Set(plan.steps.filter((step) => step.status === "completed").map((step) => step.stepId));
557
+ const next = plan.steps.find((step) => step.status === "pending" && step.dependsOn.every((dependency) => completed.has(dependency)));
558
+ if (!next) { plan.currentStepId = null; return null; }
559
+ next.status = "active"; next.updatedAt = now; plan.currentStepId = next.stepId; plan.revision += 1;
560
+ return next;
561
+ }
562
+
563
+ function createGoalPlan(steps, now, defaultAgentId, transferContext) {
564
+ if (!Array.isArray(steps) || steps.length === 0 || steps.length > 32) throw new Error("goal plan requires 1-32 steps");
565
+ const normalized = steps.map((step, index) => ({
566
+ stepId: exactId(step?.stepId ?? step?.id, `steps[${index}].stepId`),
567
+ agentId: exactId(step?.agentId ?? defaultAgentId, `steps[${index}].agentId`),
568
+ resources: Array.isArray(step?.resources)
569
+ ? step.resources.map((resource) => exactId(resource, `steps[${index}].resources`)) : [],
570
+ execution: createExecutionDecision(step?.execution, `steps[${index}].execution`, transferContext),
571
+ title: safeText(step?.title, `steps[${index}].title`, 500),
572
+ successCriterion: safeText(step?.successCriterion, `steps[${index}].successCriterion`),
573
+ dependsOn: Array.isArray(step?.dependsOn) ? step.dependsOn.map((dependency) => exactId(dependency, `steps[${index}].dependsOn`)) : [],
574
+ status: "pending", checkpoint: null, blocker: null, completedAt: null, completedByQueueId: null,
575
+ knowledgeGaps: [], executionOutcomes: [], updatedAt: now
576
+ }));
577
+ const plan = { schema: GOAL_PLAN_SCHEMA, revision: 0, currentStepId: null, steps: normalized,
578
+ definitionsDigest: sha256(JSON.stringify(planDefinitionMaterial(normalized))), authority: "context-only-plan" };
579
+ if (!validGoalPlan(plan)) throw new Error("goal plan must be an acyclic dependency graph with exact unique step IDs");
580
+ activateNextPlanStep(plan, now);
581
+ return plan;
582
+ }
583
+
584
+ function currentPlanStep(goal) {
585
+ return goal.plan?.steps.find((step) => step.stepId === goal.plan.currentStepId) || null;
586
+ }
587
+
588
+ function planStepAgentId(goal, step) {
589
+ return step?.agentId ?? goal.agentId;
590
+ }
591
+
592
+ function planStepResources(step) {
593
+ return Array.isArray(step?.resources) ? step.resources : [];
594
+ }
595
+
596
+ function queuePlanStep(policy, item) {
597
+ if (!item.goalStepId) return null;
598
+ return policy.goals.find((goal) => goal.goalId === item.goalId)?.plan?.steps
599
+ .find((step) => step.stepId === item.goalStepId) || null;
600
+ }
601
+
602
+ function conflictingResources(policy, candidate, leased) {
603
+ const wanted = new Set(planStepResources(queuePlanStep(policy, candidate)));
604
+ if (!wanted.size || candidate.projectId !== leased.projectId || candidate.groupId !== leased.groupId) return [];
605
+ return planStepResources(queuePlanStep(policy, leased)).filter((resource) => wanted.has(resource));
606
+ }
607
+
608
+ function effectiveQueuePriority(policy, item) {
609
+ if (!item.goalId) return item.priority;
610
+ return policy.goals.find((goal) => goal.goalId === item.goalId)?.priority ?? item.priority;
611
+ }
612
+
613
+ function createKnowledgeGap(goal, step, queueId, request, now) {
614
+ const gap = {
615
+ schema: KNOWLEDGE_GAP_SCHEMA, gapId: null, goalId: goal.goalId, goalStepId: step.stepId,
616
+ planDefinitionsDigest: goal.plan.definitionsDigest,
617
+ question: safeKnowledgeText(request?.question, "knowledgeGap.question", 500),
618
+ reason: safeKnowledgeText(request?.reason, "knowledgeGap.reason", 500),
619
+ requiredEvidence: request?.requiredEvidence,
620
+ requestedAt: now, requestedByQueueId: exactId(queueId, "requestedByQueueId"),
621
+ subjectDigest: null, requestDigest: null, status: "open", answer: null, answerSource: null,
622
+ sourceDigest: null, resolvedAt: null, resolvedBySubjectId: null, resolutionDigest: null,
623
+ authority: "context-only"
624
+ };
625
+ if (!KNOWLEDGE_EVIDENCE.has(gap.requiredEvidence)) {
626
+ throw new Error("knowledgeGap.requiredEvidence must be owner-input or objective-observation");
627
+ }
628
+ gap.subjectDigest = sha256(JSON.stringify(knowledgeGapSubjectMaterial(gap)));
629
+ gap.gapId = "knowledge-gap:" + gap.subjectDigest.slice(0, 32);
630
+ gap.requestDigest = sha256(JSON.stringify(knowledgeGapRequestMaterial(gap)));
631
+ if (!validKnowledgeGap(gap)) throw new Error("knowledge gap request is invalid");
632
+ return gap;
633
+ }
634
+
635
+ function planQueueKey(goalId, stepId, phase, suffix = "") {
636
+ return ["goal", goalId, "step", stepId, phase, suffix].filter(Boolean).join(":");
637
+ }
638
+
639
+ function newGoalQueue(goal, step, kind, key, current, availableAt = current) {
640
+ return { queueId: "gateway-queue:" + sha256(key).slice(0, 32), dedupeKey: key, kind,
641
+ agentId: planStepAgentId(goal, step), projectId: goal.projectId, groupId: goal.groupId, goalId: goal.goalId,
642
+ goalStepId: step?.stepId || null, channelEventId: null, priority: goal.priority, status: "pending",
643
+ attempts: 0, lease: null, availableAt, createdAt: current, updatedAt: current,
644
+ completedAt: null, lastError: null, authority: "execution-state-only" };
645
+ }
646
+
647
+ function validGoal(goal) {
648
+ return goal && ID_RE.test(goal.goalId || "") && ID_RE.test(goal.agentId || "")
649
+ && ID_RE.test(goal.ownerSubjectId || "") && ID_RE.test(goal.projectId || "")
650
+ && (goal.groupId === null || ID_RE.test(goal.groupId || "")) && GOAL_STATUSES.has(goal.status)
651
+ && typeof goal.successCriterion === "string" && goal.successCriterion.length > 0
652
+ && typeof goal.nextSafeStep === "string" && goal.nextSafeStep.length > 0
653
+ && (goal.deadline === null || Number.isFinite(new Date(goal.deadline).getTime()))
654
+ && Number.isInteger(goal.priority) && goal.priority >= 0 && goal.priority <= 100
655
+ && (goal.checkpoint === null || (() => { try { safeCheckpoint(goal.checkpoint); return true; } catch { return false; } })())
656
+ && (goal.blocker === null || (typeof goal.blocker === "string" && goal.blocker.length <= 500 && !SECRET_RE.test(goal.blocker)))
657
+ && (goal.heartbeatAt === null || Number.isFinite(new Date(goal.heartbeatAt).getTime()))
658
+ && goal.authority === "authenticated-goal-policy" && Number.isFinite(new Date(goal.createdAt).getTime())
659
+ && Number.isFinite(new Date(goal.updatedAt).getTime())
660
+ && (goal.plan === undefined || goal.plan === null || (validGoalPlan(goal.plan)
661
+ && goal.plan.steps.every((step) => (step.knowledgeGaps || []).every((gap) => gap.goalId === goal.goalId))
662
+ && (goal.status !== "active" || (currentPlanStep(goal)?.status === "active" && goal.nextSafeStep === currentPlanStep(goal).title))
663
+ && (goal.status !== "blocked" || currentPlanStep(goal)?.status === "blocked")
664
+ && (goal.status !== "completed" || goal.plan.steps.every((step) => step.status === "completed"))));
665
+ }
666
+
667
+ function validPolicyHistory(item) {
668
+ if (!item || item.authority !== "authenticated-goal-policy" || !Number.isFinite(new Date(item.at).getTime())) return false;
669
+ if (item.kind === "goal") return validGoal(item.value);
670
+ return item.kind === "control" && item.value && typeof item.value.enabled === "boolean" && typeof item.value.killSwitch === "boolean";
671
+ }
672
+
673
+ function validQueue(item) {
674
+ return item && ID_RE.test(item.queueId || "") && WAKE_KINDS.has(item.kind)
675
+ && ID_RE.test(item.dedupeKey || "") && item.queueId === "gateway-queue:" + sha256(item.dedupeKey).slice(0, 32)
676
+ && ID_RE.test(item.agentId || "") && ID_RE.test(item.projectId || "")
677
+ && (item.groupId === null || ID_RE.test(item.groupId || ""))
678
+ && (item.goalId === null || ID_RE.test(item.goalId || ""))
679
+ && (item.goalStepId === undefined || item.goalStepId === null || (item.goalId !== null && ID_RE.test(item.goalStepId || "")))
680
+ && (item.channelEventId === null || ID_RE.test(item.channelEventId || ""))
681
+ && QUEUE_STATUSES.has(item.status) && Number.isInteger(item.priority) && item.priority >= 0 && item.priority <= 100
682
+ && Number.isInteger(item.attempts) && item.attempts >= 0 && item.attempts <= 20
683
+ && item.authority === "execution-state-only" && Number.isFinite(new Date(item.createdAt).getTime())
684
+ && Number.isFinite(new Date(item.availableAt).getTime()) && Number.isFinite(new Date(item.updatedAt).getTime())
685
+ && (item.completedAt === null || Number.isFinite(new Date(item.completedAt).getTime()))
686
+ && (item.lastError === null || (typeof item.lastError === "string" && item.lastError.length <= 500 && !SECRET_RE.test(item.lastError)))
687
+ && (item.lease === null || (item.status === "leased" && ID_RE.test(item.lease.workerId || "")
688
+ && Number.isFinite(new Date(item.lease.expiresAt).getTime())));
689
+ }
690
+
691
+ function validOutbox(item) {
692
+ return item && ID_RE.test(item.outboxId || "") && ID_RE.test(item.queueId || "")
693
+ && ID_RE.test(item.idempotencyKey || "") && ID_RE.test(item.bindingId || "")
694
+ && item.outboxId === "gateway-outbox:" + sha256(item.idempotencyKey).slice(0, 32)
695
+ && ID_RE.test(item.eventId || "") && ROUTE_RE.test(item.provider || "") && ROUTE_RE.test(item.tenantId || "")
696
+ && ROUTE_RE.test(item.accountId || "") && ROUTE_RE.test(item.chatId || "")
697
+ && (item.threadId === null || ROUTE_RE.test(item.threadId || "")) && (item.replyTo === null || ROUTE_RE.test(item.replyTo || ""))
698
+ && OUTBOX_STATUSES.has(item.status)
699
+ && typeof item.text === "string" && item.text.length > 0 && item.text.length <= 16000 && !SECRET_RE.test(item.text)
700
+ && Number.isInteger(item.attempts) && item.attempts >= 0 && item.attempts <= 20
701
+ && item.authority === "delivery-state-only" && Number.isFinite(new Date(item.createdAt).getTime())
702
+ && Number.isFinite(new Date(item.updatedAt).getTime()) && Number.isFinite(new Date(item.nextAttemptAt).getTime())
703
+ && (item.deliveredAt === null || Number.isFinite(new Date(item.deliveredAt).getTime()))
704
+ && (item.adapterReceipt === null || (typeof item.adapterReceipt === "string" && item.adapterReceipt.length <= 500))
705
+ && (item.lastError === null || (typeof item.lastError === "string" && item.lastError.length <= 500 && !SECRET_RE.test(item.lastError)));
706
+ }
707
+
708
+ function validLane(item) {
709
+ return item && ID_RE.test(item.agentId || "") && ID_RE.test(item.queueId || "") && ID_RE.test(item.workerId || "")
710
+ && new Set(["leased", "completed", "expired"]).has(item.status)
711
+ && Number.isFinite(new Date(item.claimedAt).getTime()) && Number.isFinite(new Date(item.expiresAt).getTime())
712
+ && Number.isFinite(new Date(item.updatedAt).getTime()) && item.authority === "execution-state-only";
713
+ }
714
+
715
+ function validReceipt(item) {
716
+ if (!(item && ID_RE.test(item.id || "") && ID_RE.test(item.kind || "") && ID_RE.test(item.objectId || "")
717
+ && Number.isFinite(new Date(item.at).getTime()) && item.details && typeof item.details === "object"
718
+ && !Array.isArray(item.details) && !SECRET_RE.test(JSON.stringify(item.details))
719
+ && item.authority === "execution-state-only" && /^[a-f0-9]{64}$/.test(item.digest || ""))) return false;
720
+ const material = { kind: item.kind, objectId: item.objectId, at: item.at, details: item.details, authority: "execution-state-only" };
721
+ return item.digest === sha256(JSON.stringify(material)) && item.id === "gateway-receipt:" + item.digest.slice(0, 24);
722
+ }
723
+
724
+ function validHistory(item) {
725
+ if (!(item && new Set(["queue", "outbox"]).has(item.kind) && ID_RE.test(item.objectId || "")
726
+ && ID_RE.test(item.transition || "") && Number.isFinite(new Date(item.at).getTime())
727
+ && item.value && item.authority === "execution-state-only")) return false;
728
+ return item.kind === "queue" ? validQueue(item.value) && item.objectId === item.value.queueId
729
+ : validOutbox(item.value) && item.objectId === item.value.outboxId;
730
+ }
731
+
732
+ function validHealth(health) {
733
+ return health && ["gateway", "adapter", "scheduler", "queue", "worker", "host"].every((key) => HEALTH_VALUES.has(health[key]))
734
+ && (health.lastTickAt === null || Number.isFinite(new Date(health.lastTickAt).getTime()))
735
+ && (health.lastReconciledAt === null || Number.isFinite(new Date(health.lastReconciledAt).getTime()));
736
+ }
737
+
738
+ function normalizePolicy(value, root) {
739
+ if (!value || value.schema !== GATEWAY_POLICY_SCHEMA || value.root !== root
740
+ || !Number.isInteger(value.revision) || typeof value.enabled !== "boolean" || typeof value.killSwitch !== "boolean"
741
+ || !Array.isArray(value.goals) || !Array.isArray(value.history) || value.goals.some((item) => !validGoal(item))
742
+ || value.goals.some((item) => !validGoalTransferProofs(item, value.goals))
743
+ || value.history.some((item) => !validPolicyHistory(item))) {
744
+ throw new Error("gateway policy is invalid; autonomous runtime is disabled");
745
+ }
746
+ return value;
747
+ }
748
+
749
+ function normalizeRuntime(value, root) {
750
+ const baseValid = value && value.schema === GATEWAY_RUNTIME_SCHEMA && value.root === root && Number.isInteger(value.revision)
751
+ && Array.isArray(value.queue) && Array.isArray(value.lanes) && Array.isArray(value.outbox)
752
+ && Array.isArray(value.receipts) && Array.isArray(value.history);
753
+ if (!baseValid) throw new Error("gateway runtime structure is invalid; worker is disabled");
754
+ const invalid = [];
755
+ if (!validHealth(value.health)) invalid.push("health");
756
+ const queueIndex = value.queue.findIndex((item) => !validQueue(item));
757
+ const laneIndex = value.lanes.findIndex((item) => !validLane(item));
758
+ const outboxIndex = value.outbox.findIndex((item) => !validOutbox(item));
759
+ const receiptIndex = value.receipts.findIndex((item) => !validReceipt(item));
760
+ const historyIndex = value.history.findIndex((item) => !validHistory(item));
761
+ if (queueIndex >= 0) invalid.push("queue:" + queueIndex);
762
+ if (laneIndex >= 0) invalid.push("lanes:" + laneIndex);
763
+ if (outboxIndex >= 0) invalid.push("outbox:" + outboxIndex);
764
+ if (receiptIndex >= 0) invalid.push("receipts:" + receiptIndex);
765
+ if (historyIndex >= 0) invalid.push("history:" + historyIndex);
766
+ if (invalid.length) throw new Error("gateway runtime is invalid (" + invalid.join(",") + "); worker is disabled");
767
+ return value;
768
+ }
769
+
770
+ async function pathsFor(root, catalog = null) {
771
+ catalog ||= await buildCatalog(root);
772
+ const directory = await projectStateDir(catalog.root);
773
+ return { catalog, directory, gatewayPolicyPath: join(directory, "gateway-policy.json"), gatewayRuntimePath: join(directory, "gateway-runtime.json") };
774
+ }
775
+
776
+ async function readJson(path, root, normalize, empty) {
777
+ try {
778
+ const metadata = await stat(path);
779
+ if (metadata.size > MAX_BYTES) throw new Error("gateway state exceeds 8 MiB");
780
+ return normalize(JSON.parse(await readFile(path, "utf8")), root);
781
+ } catch (error) {
782
+ if (error.code !== "ENOENT") throw error;
783
+ return empty(root);
784
+ }
785
+ }
786
+
787
+ async function writeJson(path, value) {
788
+ const content = JSON.stringify(value, null, 2) + "\n";
789
+ if (Buffer.byteLength(content) > MAX_BYTES) throw new Error("gateway state exceeds 8 MiB");
790
+ const temporary = path + "." + process.pid + "." + randomUUID() + ".tmp";
791
+ try { await writeFile(temporary, content, { mode: 0o600 }); await replaceFileWithRetry(temporary, path); }
792
+ finally { await unlink(temporary).catch((error) => { if (error.code !== "ENOENT") throw error; }); }
793
+ }
794
+
795
+ async function withLock(paths, task) {
796
+ const lockPath = join(paths.directory, "gateway-runtime.lock");
797
+ let handle;
798
+ for (let attempt = 0; attempt < 160; attempt += 1) {
799
+ try { handle = await open(lockPath, "wx", 0o600); break; } catch (error) {
800
+ if (!isFileLockContention(error)) throw error;
801
+ try { const metadata = await stat(lockPath); if (Date.now() - metadata.mtimeMs > 120000) await unlink(lockPath); }
802
+ catch (lockError) { if (lockError.code !== "ENOENT") throw lockError; }
803
+ await new Promise((resolve) => setTimeout(resolve, 25));
804
+ }
805
+ }
806
+ if (!handle) throw new Error("gateway state is busy; retry later");
807
+ try { return await task(); } finally {
808
+ await handle.close();
809
+ await unlink(lockPath).catch((error) => { if (error.code !== "ENOENT") throw error; });
810
+ }
811
+ }
812
+
813
+ function appendReceipt(runtime, kind, objectId, now, details = {}) {
814
+ const material = { kind, objectId, at: now, details, authority: "execution-state-only" };
815
+ const digest = sha256(JSON.stringify(material));
816
+ const id = "gateway-receipt:" + digest.slice(0, 24);
817
+ const previous = runtime.receipts.find((item) => item.id === id);
818
+ if (previous) return previous;
819
+ const receipt = { id, ...material, digest };
820
+ runtime.receipts.push(receipt);
821
+ return receipt;
822
+ }
823
+
824
+ function preserve(runtime, kind, value, transition, now) {
825
+ runtime.history.push({ kind, objectId: kind === "outbox" ? value.outboxId : value.queueId, transition, at: now,
826
+ value: structuredClone(value), authority: "execution-state-only" });
827
+ }
828
+
829
+ function currentLane(runtime, agentId) {
830
+ return runtime.lanes.find((item) => item.agentId === agentId && item.status === "leased") || null;
831
+ }
832
+
833
+ function assertActivePersona(personaPolicy, personaRuntime, agentId, projectId, groupId) {
834
+ const findings = personaRuntimeFindings(personaPolicy, personaRuntime);
835
+ if (findings.length) throw new Error("persona runtime is unhealthy: " + findings.join(", "));
836
+ const persona = personaRuntime.personas.find((item) => item.personaId === agentId && item.status === "active");
837
+ if (!persona || !["agent", "bot"].includes(persona.kind)) throw new Error("gateway work requires an active authenticated agent or bot");
838
+ if (groupId !== null && persona.groupId !== groupId) throw new Error("gateway work group does not match authenticated persona membership");
839
+ const binding = personaPolicy.bindings.find((item) => item.id === persona.bindingId && item.active);
840
+ if (!binding || !binding.tenantId || !binding.profileId || !projectId) throw new Error("gateway work lacks an active authenticated identity binding");
841
+ return { persona, binding };
842
+ }
843
+
844
+ export async function setGatewayControl({ root = process.cwd(), enabled, killSwitch, confirmation, now = new Date() }) {
845
+ if (confirmation !== CONFIRMATION) throw new Error("gateway control changes require explicit local owner confirmation");
846
+ const paths = await pathsFor(root);
847
+ return withLock(paths, async () => {
848
+ const policy = await readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy);
849
+ const previous = { enabled: policy.enabled, killSwitch: policy.killSwitch };
850
+ if (enabled !== undefined) policy.enabled = Boolean(enabled);
851
+ if (killSwitch !== undefined) policy.killSwitch = Boolean(killSwitch);
852
+ policy.revision += 1;
853
+ policy.history.push({ kind: "control", at: timestamp(now), value: previous, authority: "authenticated-goal-policy" });
854
+ await writeJson(paths.gatewayPolicyPath, policy);
855
+ return { enabled: policy.enabled, killSwitch: policy.killSwitch, revision: policy.revision };
856
+ });
857
+ }
858
+
859
+ export async function assignGoal({ root = process.cwd(), goalId, agentId, ownerSubjectId, projectId, groupId = null,
860
+ priority = 70, successCriterion, nextSafeStep = null, steps = null, deadline = null, confirmation, now = new Date() }) {
861
+ if (confirmation !== CONFIRMATION) throw new Error("goal assignment requires explicit local owner confirmation");
862
+ const paths = await pathsFor(root);
863
+ return withLock(paths, async () => {
864
+ const [policy, runtime, personas] = await Promise.all([
865
+ readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
866
+ readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime),
867
+ loadPersonaRuntime(paths.catalog.root, paths.catalog)
868
+ ]);
869
+ goalId = exactId(goalId, "goalId"); agentId = exactId(agentId, "agentId");
870
+ projectId = exactId(projectId, "projectId"); groupId = exactId(groupId, "groupId", true);
871
+ const leadIdentity = assertActivePersona(personas.policy, personas.runtime, agentId, projectId, groupId);
872
+ const createdAt = timestamp(now);
873
+ const active = policy.goals.find((item) => item.agentId === agentId && item.status === "active" && item.goalId !== goalId);
874
+ if (active) throw new Error("an agent may have only one active focused goal");
875
+ const plan = steps === null ? null : createGoalPlan(steps, createdAt, agentId, {
876
+ goals: policy.goals, scope: { goalId, projectId, groupId, before: createdAt }
877
+ });
878
+ if (plan) {
879
+ for (const stepAgentId of new Set(plan.steps.map((step) => step.agentId))) {
880
+ const stepIdentity = assertActivePersona(personas.policy, personas.runtime, stepAgentId, projectId, groupId);
881
+ if (stepIdentity.binding.tenantId !== leadIdentity.binding.tenantId) {
882
+ throw new Error("goal-plan team members must share the authenticated tenant and exact project group");
883
+ }
884
+ }
885
+ }
886
+ const firstStep = plan ? plan.steps.find((step) => step.stepId === plan.currentStepId) : null;
887
+ const goal = { goalId, agentId, ownerSubjectId: exactId(ownerSubjectId, "ownerSubjectId"),
888
+ projectId, groupId, priority: Number(priority), successCriterion: safeText(successCriterion, "successCriterion"),
889
+ nextSafeStep: safeText(firstStep?.title || nextSafeStep, "nextSafeStep"), deadline: deadline === null ? null : timestamp(deadline),
890
+ status: "active", checkpoint: null, heartbeatAt: null, blocker: null, createdAt, updatedAt: createdAt,
891
+ plan, authority: "authenticated-goal-policy" };
892
+ if (!validGoal(goal)) throw new Error("goal assignment is invalid");
893
+ const previous = policy.goals.find((item) => item.goalId === goal.goalId);
894
+ if (previous && [previous.agentId, previous.ownerSubjectId, previous.projectId, previous.groupId].join("\0")
895
+ !== [goal.agentId, goal.ownerSubjectId, goal.projectId, goal.groupId].join("\0")) throw new Error("goal scope is immutable");
896
+ if (previous?.plan && previous.plan.definitionsDigest !== goal.plan?.definitionsDigest) {
897
+ throw new Error("goal plan definitions are immutable; assign a new goal ID");
898
+ }
899
+ if (previous?.plan && previous.status === "blocked") {
900
+ const blockedStep = currentPlanStep(previous);
901
+ if (!blockedStep || blockedStep.status !== "blocked") throw new Error("blocked goal plan has no bound blocked step");
902
+ if ((blockedStep.knowledgeGaps || []).some((gap) => gap.status === "open")) {
903
+ throw new Error("blocked goal plan has an open knowledge gap; resolve it with goal-clarify");
904
+ }
905
+ if (blockedStep.execution?.explorationMaxAttempts !== undefined
906
+ && currentExecutionAttempt(blockedStep.execution, blockedStep.executionOutcomes || []) === null) {
907
+ throw new Error("bounded exploration is exhausted or stopped by a blocking defect; assign a new goal ID");
908
+ }
909
+ policy.history.push({ kind: "goal", at: createdAt, value: structuredClone(previous), authority: "authenticated-goal-policy" });
910
+ blockedStep.status = "active"; blockedStep.blocker = null; blockedStep.updatedAt = createdAt;
911
+ previous.status = "active"; previous.blocker = null; previous.updatedAt = createdAt; previous.plan.revision += 1;
912
+ policy.revision += 1;
913
+ const key = planQueueKey(previous.goalId, blockedStep.stepId, "owner-resume", String(previous.plan.revision));
914
+ const queued = newGoalQueue(previous, blockedStep, "follow-up", key, createdAt);
915
+ runtime.queue.push(queued); runtime.revision += 1;
916
+ appendReceipt(runtime, "goal-step-resumed", queued.queueId, createdAt, { goalStepId: blockedStep.stepId });
917
+ await Promise.all([writeJson(paths.gatewayPolicyPath, policy), writeJson(paths.gatewayRuntimePath, runtime)]);
918
+ return { goal: structuredClone(previous), gatewayPolicyPath: paths.gatewayPolicyPath, resumed: true };
919
+ }
920
+ if (previous?.plan) return { goal: structuredClone(previous), gatewayPolicyPath: paths.gatewayPolicyPath, duplicate: true };
921
+ if (previous) {
922
+ policy.history.push({ kind: "goal", at: createdAt, value: structuredClone(previous), authority: "authenticated-goal-policy" });
923
+ goal.createdAt = previous.createdAt;
924
+ }
925
+ policy.goals = policy.goals.filter((item) => item.goalId !== goal.goalId);
926
+ policy.goals.push(goal);
927
+ policy.revision += 1;
928
+ const key = firstStep ? planQueueKey(goal.goalId, firstStep.stepId, "assignment") : "goal:" + goal.goalId + ":assignment";
929
+ if (!runtime.queue.some((item) => item.dedupeKey === key)) {
930
+ runtime.queue.push(newGoalQueue(goal, firstStep, "assignment", key, createdAt));
931
+ runtime.revision += 1;
932
+ appendReceipt(runtime, "queued", "gateway-queue:" + sha256(key).slice(0, 32), createdAt, { kind: "assignment", dedupeKey: key });
933
+ }
934
+ await Promise.all([writeJson(paths.gatewayPolicyPath, policy), writeJson(paths.gatewayRuntimePath, runtime)]);
935
+ return { goal, gatewayPolicyPath: paths.gatewayPolicyPath };
936
+ });
937
+ }
938
+
939
+ export async function resolveGoalKnowledgeGap({ root = process.cwd(), goalId, gapId, answer,
940
+ answerSource = "owner-input", sourceDigest = null, confirmation, now = new Date() }) {
941
+ if (confirmation !== CONFIRMATION) throw new Error("knowledge gap resolution requires explicit local owner confirmation");
942
+ const paths = await pathsFor(root);
943
+ return withLock(paths, async () => {
944
+ const [policy, runtime, personas] = await Promise.all([
945
+ readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
946
+ readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime),
947
+ loadPersonaRuntime(paths.catalog.root, paths.catalog)
948
+ ]);
949
+ goalId = exactId(goalId, "goalId"); gapId = exactId(gapId, "gapId");
950
+ const goal = policy.goals.find((item) => item.goalId === goalId);
951
+ const step = goal && currentPlanStep(goal);
952
+ if (!goal?.plan || !step) throw new Error("knowledge gap resolution requires the exact current goal-plan step");
953
+ assertActivePersona(personas.policy, personas.runtime, goal.agentId, goal.projectId, goal.groupId);
954
+ const gap = (step.knowledgeGaps || []).find((item) => item.gapId === gapId);
955
+ if (!gap) throw new Error("knowledge gap is not bound to the current goal-plan step");
956
+ answer = safeKnowledgeText(answer, "answer", 2000);
957
+ if (!KNOWLEDGE_EVIDENCE.has(answerSource) || answerSource !== gap.requiredEvidence) {
958
+ throw new Error("answer source does not satisfy the requested evidence class");
959
+ }
960
+ sourceDigest = sourceDigest === null || sourceDigest === undefined || sourceDigest === "" ? null : String(sourceDigest);
961
+ if ((answerSource === "objective-observation") !== (/^[a-f0-9]{64}$/.test(sourceDigest || ""))) {
962
+ throw new Error("objective observation answers require one exact SHA-256 source digest");
963
+ }
964
+ if (gap.status === "resolved") {
965
+ if (gap.answer !== answer || gap.answerSource !== answerSource || gap.sourceDigest !== sourceDigest) {
966
+ throw new Error("knowledge gap already has a different bound resolution");
967
+ }
968
+ return { goal: structuredClone(goal), gap: structuredClone(gap), duplicate: true };
969
+ }
970
+ if (goal.status !== "blocked" || step.status !== "blocked") {
971
+ throw new Error("open knowledge gap resolution requires the exact blocked goal-plan step");
972
+ }
973
+ const resolvedAt = timestamp(now);
974
+ const candidate = { ...gap, status: "resolved", answer, answerSource, sourceDigest,
975
+ resolvedAt, resolvedBySubjectId: goal.ownerSubjectId, resolutionDigest: null };
976
+ candidate.resolutionDigest = sha256(JSON.stringify(knowledgeGapResolutionMaterial(candidate)));
977
+ if (!validKnowledgeGap(candidate)) throw new Error("knowledge gap resolution is invalid");
978
+ policy.history.push({ kind: "goal", at: resolvedAt, value: structuredClone(goal), authority: "authenticated-goal-policy" });
979
+ Object.assign(gap, candidate);
980
+ step.status = "active"; step.blocker = null; step.updatedAt = resolvedAt;
981
+ goal.status = "active"; goal.blocker = null; goal.updatedAt = resolvedAt; goal.nextSafeStep = step.title;
982
+ goal.plan.revision += 1; policy.revision += 1;
983
+ const key = planQueueKey(goal.goalId, step.stepId, "clarified", gap.resolutionDigest.slice(0, 20));
984
+ let queued = runtime.queue.find((item) => item.dedupeKey === key);
985
+ if (!queued) {
986
+ queued = newGoalQueue(goal, step, "follow-up", key, resolvedAt);
987
+ runtime.queue.push(queued); runtime.revision += 1;
988
+ appendReceipt(runtime, "knowledge-gap-resolved", queued.queueId, resolvedAt, {
989
+ goalStepId: step.stepId, gapId: gap.gapId, answerSource
990
+ });
991
+ }
992
+ await Promise.all([writeJson(paths.gatewayPolicyPath, policy), writeJson(paths.gatewayRuntimePath, runtime)]);
993
+ return { goal: structuredClone(goal), gap: structuredClone(gap), queueId: queued.queueId, duplicate: false };
994
+ });
995
+ }
996
+
997
+ export async function enqueueGatewayWake({ root = process.cwd(), kind, agentId, projectId, groupId = null, goalId = null,
998
+ channelEventId = null, dedupeKey, availableAt = null, now = new Date() }) {
999
+ if (!WAKE_KINDS.has(kind)) throw new Error("unsupported gateway wake kind");
1000
+ const paths = await pathsFor(root);
1001
+ return withLock(paths, async () => {
1002
+ const [policy, runtime, personas] = await Promise.all([
1003
+ readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
1004
+ readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime),
1005
+ loadPersonaRuntime(paths.catalog.root, paths.catalog)
1006
+ ]);
1007
+ if (!policy.enabled || policy.killSwitch) throw new Error("gateway is disabled by local policy");
1008
+ agentId = exactId(agentId, "agentId"); projectId = exactId(projectId, "projectId"); groupId = exactId(groupId, "groupId", true);
1009
+ assertActivePersona(personas.policy, personas.runtime, agentId, projectId, groupId);
1010
+ goalId = exactId(goalId, "goalId", true); channelEventId = exactId(channelEventId, "channelEventId", true);
1011
+ const key = exactId(dedupeKey, "dedupeKey");
1012
+ const existing = runtime.queue.find((item) => item.dedupeKey === key);
1013
+ if (existing) return { item: existing, duplicate: true };
1014
+ const createdAt = timestamp(now);
1015
+ const item = { queueId: "gateway-queue:" + sha256(key).slice(0, 32), dedupeKey: key, kind, agentId, projectId, groupId,
1016
+ goalId, goalStepId: null, channelEventId, priority: PRIORITY[kind], status: "pending", attempts: 0, lease: null,
1017
+ availableAt: availableAt === null ? createdAt : timestamp(availableAt), createdAt, updatedAt: createdAt,
1018
+ completedAt: null, lastError: null, authority: "execution-state-only" };
1019
+ runtime.queue.push(item); runtime.revision += 1;
1020
+ appendReceipt(runtime, "queued", item.queueId, createdAt, { kind, dedupeKey: key });
1021
+ await writeJson(paths.gatewayRuntimePath, runtime);
1022
+ return { item, duplicate: false };
1023
+ });
1024
+ }
1025
+
1026
+ export async function reconcileGateway({ root = process.cwd(), now = new Date() }) {
1027
+ const paths = await pathsFor(root);
1028
+ return withLock(paths, async () => {
1029
+ const [policy, runtime, channelPolicy, channel, attention, personas, { graph }] = await Promise.all([
1030
+ readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
1031
+ readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime),
1032
+ loadChannelPolicy(paths.catalog.root, paths.catalog), loadChannelRuntime(paths.catalog.root, paths.catalog),
1033
+ loadAttention(paths.catalog.root, paths.catalog), loadPersonaRuntime(paths.catalog.root, paths.catalog),
1034
+ loadGraph(paths.catalog.root, paths.catalog)
1035
+ ]);
1036
+ const current = timestamp(now);
1037
+ const findings = personaRuntimeFindings(personas.policy, personas.runtime, graph);
1038
+ if (findings.length) throw new Error("persona runtime is unhealthy: " + findings.join(", "));
1039
+ const channelFindings = channelRuntimeFindings(channel.runtime, channelPolicy.policy, graph);
1040
+ if (channelFindings.length) throw new Error("channel runtime is unhealthy: " + channelFindings.join(", "));
1041
+ const attentionIssues = attentionFindings(attention.attention);
1042
+ if (attentionIssues.length) throw new Error("attention runtime is unhealthy: " + attentionIssues.join(", "));
1043
+ for (const lane of runtime.lanes.filter((item) => item.status === "leased" && new Date(item.expiresAt) <= new Date(current))) {
1044
+ const item = runtime.queue.find((entry) => entry.queueId === lane.queueId && entry.status === "leased");
1045
+ if (item) { preserve(runtime, "queue", item, "lease-expired", current); item.status = item.attempts >= 3 ? "dead-letter" : "pending"; item.lease = null; item.updatedAt = current; }
1046
+ lane.status = "expired"; lane.updatedAt = current;
1047
+ }
1048
+ for (const outbox of runtime.outbox.filter((item) => item.status === "sending")) {
1049
+ preserve(runtime, "outbox", outbox, "ambiguous-send-recovery", current);
1050
+ outbox.status = "delivery-unknown"; outbox.updatedAt = current;
1051
+ appendReceipt(runtime, "delivery-unknown", outbox.outboxId, current, { reason: "crash-during-send" });
1052
+ }
1053
+ let policyChanged = false;
1054
+ if (policy.enabled && !policy.killSwitch) {
1055
+ for (const goal of policy.goals.filter((item) => item.status === "active" && item.plan)) {
1056
+ const step = currentPlanStep(goal);
1057
+ if (!step) throw new Error("active goal plan has no current step");
1058
+ try {
1059
+ assertActivePersona(personas.policy, personas.runtime, planStepAgentId(goal, step), goal.projectId, goal.groupId);
1060
+ } catch {
1061
+ policy.history.push({ kind: "goal", at: current, value: structuredClone(goal), authority: "authenticated-goal-policy" });
1062
+ const blocker = "Assigned team member is unavailable in this exact project group.";
1063
+ step.status = "blocked"; step.blocker = blocker; step.updatedAt = current;
1064
+ goal.status = "blocked"; goal.blocker = blocker; goal.updatedAt = current; goal.plan.revision += 1;
1065
+ for (const queued of runtime.queue.filter((item) => item.goalId === goal.goalId
1066
+ && item.goalStepId === step.stepId && ["pending", "leased"].includes(item.status))) {
1067
+ preserve(runtime, "queue", queued, "step-agent-unavailable", current);
1068
+ queued.status = "cancelled"; queued.lease = null; queued.completedAt = current; queued.updatedAt = current;
1069
+ for (const lane of runtime.lanes.filter((item) => item.queueId === queued.queueId && item.status === "leased")) {
1070
+ lane.status = "expired"; lane.updatedAt = current;
1071
+ }
1072
+ appendReceipt(runtime, "step-agent-unavailable", queued.queueId, current, { goalStepId: step.stepId });
1073
+ }
1074
+ policy.revision += 1; policyChanged = true;
1075
+ continue;
1076
+ }
1077
+ const runnable = runtime.queue.some((item) => item.goalId === goal.goalId && item.goalStepId === step.stepId
1078
+ && item.agentId === planStepAgentId(goal, step)
1079
+ && ["pending", "leased", "awaiting-delivery"].includes(item.status));
1080
+ if (!runnable) {
1081
+ const key = planQueueKey(goal.goalId, step.stepId, "recovery", String(goal.plan.revision));
1082
+ if (!runtime.queue.some((item) => item.dedupeKey === key)) {
1083
+ const queued = newGoalQueue(goal, step, "follow-up", key, current);
1084
+ runtime.queue.push(queued);
1085
+ appendReceipt(runtime, "queued", queued.queueId, current, { kind: "follow-up", goalStepId: step.stepId });
1086
+ }
1087
+ }
1088
+ }
1089
+ for (const goal of policy.goals.filter((item) => item.status === "active" && item.deadline
1090
+ && new Date(item.deadline) <= new Date(current))) {
1091
+ const step = currentPlanStep(goal);
1092
+ const deadlineAgentId = planStepAgentId(goal, step);
1093
+ try { assertActivePersona(personas.policy, personas.runtime, deadlineAgentId, goal.projectId, goal.groupId); }
1094
+ catch { continue; }
1095
+ const key = "goal:" + goal.goalId + ":deadline:" + goal.deadline;
1096
+ if (!runtime.queue.some((item) => item.dedupeKey === key)) runtime.queue.push({
1097
+ queueId: "gateway-queue:" + sha256(key).slice(0, 32), dedupeKey: key, kind: "deadline",
1098
+ agentId: deadlineAgentId, projectId: goal.projectId, groupId: goal.groupId, goalId: goal.goalId,
1099
+ goalStepId: step?.stepId || null, channelEventId: null, priority: PRIORITY.deadline, status: "pending", attempts: 0, lease: null,
1100
+ availableAt: current, createdAt: current, updatedAt: current, completedAt: null, lastError: null,
1101
+ authority: "execution-state-only"
1102
+ });
1103
+ }
1104
+ for (const event of channel.runtime.events.filter((item) => item.status === "pending")) {
1105
+ if (!personas.runtime.personas.some((persona) => persona.personaId === event.agentId && persona.status === "active")) continue;
1106
+ const key = "channel:" + event.eventId;
1107
+ if (!runtime.queue.some((item) => item.dedupeKey === key)) runtime.queue.push({ queueId: "gateway-queue:" + sha256(key).slice(0, 32),
1108
+ dedupeKey: key, kind: "direct-message", agentId: event.agentId, projectId: event.projectId, groupId: event.groupId,
1109
+ goalId: null, goalStepId: null, channelEventId: event.eventId, priority: PRIORITY["direct-message"], status: "pending", attempts: 0,
1110
+ lease: null, availableAt: current, createdAt: current, updatedAt: current, completedAt: null, lastError: null, authority: "execution-state-only" });
1111
+ }
1112
+ for (const event of attention.attention.events.filter((item) => item.entityId
1113
+ && ((item.kind === "promise" && item.status === "open") || (item.kind === "blocker" && item.status === "resolved")))) {
1114
+ try { assertActivePersona(personas.policy, personas.runtime, event.entityId, event.projectId, event.groupId); }
1115
+ catch { continue; }
1116
+ const kind = event.kind === "promise" ? "promise" : "resolved-blocker";
1117
+ const key = "attention:" + event.id + ":" + event.status;
1118
+ if (!runtime.queue.some((entry) => entry.dedupeKey === key)) runtime.queue.push({ queueId: "gateway-queue:" + sha256(key).slice(0, 32),
1119
+ dedupeKey: key, kind, agentId: event.entityId, projectId: event.projectId, groupId: event.groupId,
1120
+ goalId: null, goalStepId: null, channelEventId: null, priority: PRIORITY[kind], status: "pending", attempts: 0, lease: null,
1121
+ availableAt: event.dueAt || current, createdAt: current, updatedAt: current, completedAt: null, lastError: null, authority: "execution-state-only" });
1122
+ }
1123
+ }
1124
+ runtime.health.gateway = policy.enabled && !policy.killSwitch ? "running" : "stopped";
1125
+ runtime.health.scheduler = "healthy"; runtime.health.queue = "healthy"; runtime.health.lastReconciledAt = current;
1126
+ runtime.revision += 1;
1127
+ if (policyChanged) await Promise.all([
1128
+ writeJson(paths.gatewayPolicyPath, policy), writeJson(paths.gatewayRuntimePath, runtime)
1129
+ ]);
1130
+ else await writeJson(paths.gatewayRuntimePath, runtime);
1131
+ return { policy, runtime, recovered: true };
1132
+ });
1133
+ }
1134
+
1135
+ export async function claimGatewayWork({ root = process.cwd(), workerId, leaseSeconds = 120, now = new Date() }) {
1136
+ const paths = await pathsFor(root);
1137
+ return withLock(paths, async () => {
1138
+ const [policy, runtime, personas] = await Promise.all([
1139
+ readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
1140
+ readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime),
1141
+ loadPersonaRuntime(paths.catalog.root, paths.catalog)
1142
+ ]);
1143
+ if (!policy.enabled || policy.killSwitch) return { item: null, reason: "disabled" };
1144
+ const current = timestamp(now); workerId = exactId(workerId, "workerId");
1145
+ const seconds = Number(leaseSeconds);
1146
+ if (!Number.isInteger(seconds) || seconds < 15 || seconds > 900) throw new Error("leaseSeconds must be 15-900");
1147
+ const items = runtime.queue.filter((item) => item.status === "pending" && new Date(item.availableAt) <= new Date(current)
1148
+ && !currentLane(runtime, item.agentId));
1149
+ items.sort((a, b) => effectiveQueuePriority(policy, b) - effectiveQueuePriority(policy, a)
1150
+ || a.createdAt.localeCompare(b.createdAt) || a.queueId.localeCompare(b.queueId));
1151
+ let item = null; let revoked = false;
1152
+ for (const candidate of items) {
1153
+ try {
1154
+ assertActivePersona(personas.policy, personas.runtime, candidate.agentId, candidate.projectId, candidate.groupId);
1155
+ } catch {
1156
+ preserve(runtime, "queue", candidate, "identity-revoked", current);
1157
+ candidate.status = "cancelled"; candidate.completedAt = current; candidate.updatedAt = current;
1158
+ appendReceipt(runtime, "identity-revoked", candidate.queueId, current, {});
1159
+ runtime.revision += 1; revoked = true;
1160
+ continue;
1161
+ }
1162
+ if (candidate.goalStepId) {
1163
+ const goal = policy.goals.find((entry) => entry.goalId === candidate.goalId);
1164
+ const step = goal && currentPlanStep(goal);
1165
+ if (!goal?.plan || goal.status !== "active" || step?.stepId !== candidate.goalStepId || step.status !== "active"
1166
+ || candidate.agentId !== planStepAgentId(goal, step)) {
1167
+ preserve(runtime, "queue", candidate, "plan-step-stale", current);
1168
+ candidate.status = "cancelled"; candidate.completedAt = current; candidate.updatedAt = current;
1169
+ appendReceipt(runtime, "plan-step-stale", candidate.queueId, current, { goalStepId: candidate.goalStepId });
1170
+ runtime.revision += 1; revoked = true;
1171
+ continue;
1172
+ }
1173
+ }
1174
+ const resourceConflict = runtime.queue.some((leased) => leased.status === "leased"
1175
+ && leased.queueId !== candidate.queueId && conflictingResources(policy, candidate, leased).length > 0);
1176
+ if (resourceConflict) continue;
1177
+ item = candidate;
1178
+ break;
1179
+ }
1180
+ if (!item && revoked) await writeJson(paths.gatewayRuntimePath, runtime);
1181
+ if (!item) return { item: null, reason: runtime.queue.some((entry) => entry.status === "pending") ? "waiting" : "idle/needs-goal" };
1182
+ preserve(runtime, "queue", item, "leased", current);
1183
+ item.status = "leased"; item.attempts += 1; item.updatedAt = current;
1184
+ item.lease = { workerId, claimedAt: current, expiresAt: new Date(new Date(current).getTime() + seconds * 1000).toISOString() };
1185
+ runtime.lanes = runtime.lanes.filter((lane) => lane.agentId !== item.agentId || lane.status !== "leased");
1186
+ runtime.lanes.push({ agentId: item.agentId, queueId: item.queueId, workerId, status: "leased", claimedAt: current,
1187
+ expiresAt: item.lease.expiresAt, updatedAt: current, authority: "execution-state-only" });
1188
+ runtime.health.worker = "healthy"; runtime.health.lastTickAt = current; runtime.revision += 1;
1189
+ const receipt = appendReceipt(runtime, "leased", item.queueId, current, { workerId, attempt: item.attempts });
1190
+ await writeJson(paths.gatewayRuntimePath, runtime);
1191
+ return { item: structuredClone(item), receipt };
1192
+ });
1193
+ }
1194
+
1195
+ function exactReplyBinding(channelPolicy, event) {
1196
+ const binding = channelPolicy.bindings.find((item) => item.id === event.bindingId && item.status === "active"
1197
+ && item.agentId === event.agentId && item.projectId === event.projectId && item.groupId === event.groupId
1198
+ && item.provider === event.provider && item.tenantId === event.tenantId && item.accountId === event.accountId
1199
+ && item.chatId === event.chatId && item.threadId === event.threadId && item.capabilities.includes("reply"));
1200
+ if (!binding) throw new Error("current exact channel reply capability is unavailable");
1201
+ return binding;
1202
+ }
1203
+
1204
+ export async function completeGatewayRun({ root = process.cwd(), queueId, workerId, result, now = new Date() }) {
1205
+ const paths = await pathsFor(root);
1206
+ return withLock(paths, async () => {
1207
+ const [policy, runtime, channelPolicy, channelRuntime, personas] = await Promise.all([
1208
+ readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
1209
+ readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime),
1210
+ loadChannelPolicy(paths.catalog.root, paths.catalog), loadChannelRuntime(paths.catalog.root, paths.catalog),
1211
+ loadPersonaRuntime(paths.catalog.root, paths.catalog)
1212
+ ]);
1213
+ if (!policy.enabled || policy.killSwitch) throw new Error("gateway was disabled before run completion");
1214
+ const item = runtime.queue.find((entry) => entry.queueId === exactId(queueId, "queueId"));
1215
+ workerId = exactId(workerId, "workerId");
1216
+ if (!item || item.status !== "leased" || item.lease?.workerId !== workerId) throw new Error("run completion requires the exact active queue lease");
1217
+ assertActivePersona(personas.policy, personas.runtime, item.agentId, item.projectId, item.groupId);
1218
+ const current = timestamp(now);
1219
+ const lane = runtime.lanes.find((entry) => entry.queueId === item.queueId && entry.workerId === workerId && entry.status === "leased");
1220
+ if (!lane) throw new Error("agent lane lease is missing");
1221
+ const boundGoal = item.goalId ? policy.goals.find((entry) => entry.goalId === item.goalId) : null;
1222
+ const boundStep = item.goalStepId ? boundGoal && currentPlanStep(boundGoal) : null;
1223
+ if (item.goalStepId) {
1224
+ if (!boundGoal?.plan || boundGoal.status !== "active" || boundStep?.stepId !== item.goalStepId
1225
+ || boundStep.status !== "active" || item.agentId !== planStepAgentId(boundGoal, boundStep)) {
1226
+ throw new Error("run completion is not bound to the current active goal step");
1227
+ }
1228
+ }
1229
+ const knowledgeGapRequest = result?.knowledgeGap === undefined || result?.knowledgeGap === null
1230
+ ? null : result.knowledgeGap;
1231
+ if (knowledgeGapRequest && (item.channelEventId || !item.goalStepId || !boundGoal?.plan)) {
1232
+ throw new Error("knowledge gaps require an exact goal-plan step without a channel obligation");
1233
+ }
1234
+ if (knowledgeGapRequest && (result?.blocked || result?.completed)) {
1235
+ throw new Error("knowledge gap result cannot also complete or generically block a step");
1236
+ }
1237
+ let executionReview = boundStep?.execution && result?.completed
1238
+ ? reviewExecutionResult(boundStep.execution, boundStep.executionOutcomes || [],
1239
+ item.queueId, result.execution, current) : null;
1240
+ if (executionReview && (boundStep.executionOutcomes || []).length >= 8) {
1241
+ executionReview = { passed: false, outcome: null, reason: "Execution outcome history is full." };
1242
+ }
1243
+ const text = result?.text ? safeText(result.text, "result.text", 16000) : null;
1244
+ let clarification = null; let exploration = null;
1245
+ preserve(runtime, "queue", item, "run-completed", current);
1246
+ if (item.channelEventId) {
1247
+ if (!text) throw new Error("a channel obligation requires a non-empty response");
1248
+ const voice = evaluateVoiceOutput(text);
1249
+ if (!voice.ok) throw new Error("channel response contains a prohibited attachment or consciousness claim");
1250
+ const event = channelRuntime.runtime.events.find((entry) => entry.eventId === item.channelEventId);
1251
+ if (!event) throw new Error("channel event disappeared before delivery preparation");
1252
+ const binding = exactReplyBinding(channelPolicy.policy, event);
1253
+ const idempotencyKey = "delivery:" + sha256([event.eventId, binding.id, event.chatId, event.threadId || "", event.replyTo || ""].join("\0")).slice(0, 32);
1254
+ let outbox = runtime.outbox.find((entry) => entry.idempotencyKey === idempotencyKey);
1255
+ if (!outbox) {
1256
+ outbox = { outboxId: "gateway-outbox:" + sha256(idempotencyKey).slice(0, 32), queueId: item.queueId,
1257
+ idempotencyKey, bindingId: binding.id, eventId: event.eventId, provider: event.provider, tenantId: event.tenantId,
1258
+ accountId: event.accountId, chatId: event.chatId, threadId: event.threadId, replyTo: event.replyTo,
1259
+ text, status: "prepared", attempts: 0, nextAttemptAt: current, createdAt: current, updatedAt: current,
1260
+ deliveredAt: null, adapterReceipt: null, lastError: null, authority: "delivery-state-only" };
1261
+ runtime.outbox.push(outbox);
1262
+ }
1263
+ item.status = "awaiting-delivery";
1264
+ } else {
1265
+ item.status = result?.blocked || knowledgeGapRequest || (executionReview && !executionReview.passed)
1266
+ ? "blocked" : "completed"; item.completedAt = current;
1267
+ const goal = boundGoal;
1268
+ if (goal) {
1269
+ policy.history.push({ kind: "goal", at: current, value: structuredClone(goal), authority: "authenticated-goal-policy" });
1270
+ const checkpoint = result?.checkpoint === undefined ? goal.checkpoint : safeCheckpoint(result.checkpoint);
1271
+ goal.checkpoint = checkpoint; goal.heartbeatAt = current;
1272
+ goal.blocker = result?.blocked ? safeText(result.blocker || "Run blocked.", "blocker", 500)
1273
+ : executionReview && !executionReview.passed ? executionReview.reason : null;
1274
+ if (goal.plan && item.goalStepId) {
1275
+ const step = currentPlanStep(goal);
1276
+ step.checkpoint = checkpoint; step.updatedAt = current;
1277
+ if (knowledgeGapRequest) {
1278
+ if (!Array.isArray(step.knowledgeGaps)) step.knowledgeGaps = [];
1279
+ const proposed = createKnowledgeGap(goal, step, item.queueId, knowledgeGapRequest, current);
1280
+ const existing = step.knowledgeGaps.find((gap) => gap.gapId === proposed.gapId);
1281
+ if (!existing && step.knowledgeGaps.length >= 16) throw new Error("goal step exceeds 16 knowledge gaps");
1282
+ if (existing?.status === "resolved") {
1283
+ goal.blocker = "The host repeated an already resolved knowledge gap.";
1284
+ step.status = "blocked"; step.blocker = goal.blocker; goal.status = "blocked";
1285
+ appendReceipt(runtime, "knowledge-gap-regression", item.queueId, current, {
1286
+ goalStepId: step.stepId, gapId: existing.gapId
1287
+ });
1288
+ } else {
1289
+ const gap = existing || proposed;
1290
+ if (!existing) step.knowledgeGaps.push(gap);
1291
+ goal.blocker = gap.question; step.status = "blocked"; step.blocker = gap.question; goal.status = "blocked";
1292
+ clarification = structuredClone(gap);
1293
+ appendReceipt(runtime, "knowledge-gap-opened", item.queueId, current, {
1294
+ goalStepId: step.stepId, gapId: gap.gapId, requiredEvidence: gap.requiredEvidence
1295
+ });
1296
+ }
1297
+ goal.plan.revision += 1;
1298
+ } else if (result?.blocked) {
1299
+ step.status = "blocked"; step.blocker = goal.blocker; goal.status = "blocked";
1300
+ } else if (executionReview && !executionReview.passed) {
1301
+ if (executionReview.outcome) step.executionOutcomes.push(executionReview.outcome);
1302
+ if (executionReview.outcome && executionReview.nextAttempt) {
1303
+ item.status = "completed"; step.status = "active"; step.blocker = null;
1304
+ goal.status = "active"; goal.blocker = null; goal.nextSafeStep = step.title;
1305
+ goal.plan.revision += 1;
1306
+ const key = planQueueKey(goal.goalId, step.stepId, "explore",
1307
+ executionReview.outcome.digest.slice(0, 20));
1308
+ let queued = runtime.queue.find((entry) => entry.dedupeKey === key);
1309
+ if (!queued) {
1310
+ queued = newGoalQueue(goal, step, "follow-up", key, current);
1311
+ runtime.queue.push(queued);
1312
+ }
1313
+ exploration = { queueId: queued.queueId, ...executionReview.nextAttempt };
1314
+ appendReceipt(runtime, "execution-exploration-continued", queued.queueId, current, {
1315
+ goalStepId: step.stepId, failedOutcomeId: executionReview.outcome.outcomeId,
1316
+ attempt: executionReview.nextAttempt.attempt,
1317
+ strategyId: executionReview.nextAttempt.strategyId
1318
+ });
1319
+ } else {
1320
+ step.status = "blocked"; step.blocker = goal.blocker; goal.status = "blocked";
1321
+ appendReceipt(runtime, executionReview.outcome ? "execution-gate-failed" : "execution-proof-invalid",
1322
+ item.queueId, current, { goalStepId: step.stepId,
1323
+ outcomeId: executionReview.outcome?.outcomeId || null });
1324
+ }
1325
+ } else if (result?.completed) {
1326
+ if (executionReview?.outcome) {
1327
+ step.executionOutcomes.push(executionReview.outcome);
1328
+ appendReceipt(runtime, "execution-gate-passed", item.queueId, current, {
1329
+ goalStepId: step.stepId, outcomeId: executionReview.outcome.outcomeId
1330
+ });
1331
+ }
1332
+ step.status = "completed"; step.completedAt = current; step.completedByQueueId = item.queueId; step.blocker = null;
1333
+ goal.plan.currentStepId = null; goal.plan.revision += 1;
1334
+ const next = activateNextPlanStep(goal.plan, current);
1335
+ if (next) {
1336
+ goal.status = "active"; goal.nextSafeStep = next.title; goal.blocker = null;
1337
+ const key = planQueueKey(goal.goalId, next.stepId, "ready", String(goal.plan.revision));
1338
+ if (!runtime.queue.some((entry) => entry.dedupeKey === key)) {
1339
+ const queued = newGoalQueue(goal, next, "follow-up", key, current);
1340
+ runtime.queue.push(queued);
1341
+ appendReceipt(runtime, "goal-step-ready", queued.queueId, current, { goalStepId: next.stepId });
1342
+ }
1343
+ } else {
1344
+ goal.status = "completed";
1345
+ }
1346
+ } else {
1347
+ goal.status = "active"; step.blocker = null;
1348
+ }
1349
+ } else if (!goal.plan) {
1350
+ goal.status = result?.completed ? "completed" : result?.blocked ? "blocked" : "active";
1351
+ }
1352
+ goal.updatedAt = current;
1353
+ policy.revision += 1;
1354
+ if (goal.status === "active" && (!goal.plan || !result?.completed)) {
1355
+ const checkpointDigest = sha256(JSON.stringify(goal.checkpoint || { heartbeatAt: current })).slice(0, 20);
1356
+ const step = currentPlanStep(goal);
1357
+ const key = step ? planQueueKey(goal.goalId, step.stepId, "follow-up", checkpointDigest)
1358
+ : "goal:" + goal.goalId + ":follow-up:" + checkpointDigest;
1359
+ if (!runtime.queue.some((entry) => entry.dedupeKey === key)) runtime.queue.push({
1360
+ queueId: "gateway-queue:" + sha256(key).slice(0, 32), dedupeKey: key, kind: "follow-up",
1361
+ agentId: planStepAgentId(goal, step), projectId: goal.projectId, groupId: goal.groupId, goalId: goal.goalId,
1362
+ goalStepId: step?.stepId || null, channelEventId: null, priority: goal.priority, status: "pending", attempts: 0, lease: null,
1363
+ availableAt: new Date(new Date(current).getTime() + 60000).toISOString(), createdAt: current, updatedAt: current,
1364
+ completedAt: null, lastError: null, authority: "execution-state-only"
1365
+ });
1366
+ }
1367
+ }
1368
+ }
1369
+ item.lease = null; item.updatedAt = current; lane.status = "completed"; lane.updatedAt = current;
1370
+ runtime.health.host = "healthy"; runtime.health.worker = "healthy"; runtime.health.lastTickAt = current;
1371
+ appendReceipt(runtime, "run-terminal", item.queueId, current, { status: item.status, goalStepId: item.goalStepId || null }); runtime.revision += 1;
1372
+ await Promise.all([writeJson(paths.gatewayPolicyPath, policy), writeJson(paths.gatewayRuntimePath, runtime)]);
1373
+ return { item, outbox: runtime.outbox.find((entry) => entry.queueId === item.queueId) || null,
1374
+ clarification, exploration, executionReview };
1375
+ });
1376
+ }
1377
+
1378
+ export async function failGatewayRun({ root = process.cwd(), queueId, workerId, error, retryAfterMs = 5000,
1379
+ now = new Date() }) {
1380
+ const paths = await pathsFor(root);
1381
+ return withLock(paths, async () => {
1382
+ const [policy, runtime] = await Promise.all([
1383
+ readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
1384
+ readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime)
1385
+ ]);
1386
+ const item = runtime.queue.find((entry) => entry.queueId === exactId(queueId, "queueId"));
1387
+ workerId = exactId(workerId, "workerId");
1388
+ if (!item || item.status !== "leased" || item.lease?.workerId !== workerId) {
1389
+ throw new Error("run failure requires the exact active queue lease");
1390
+ }
1391
+ const lane = runtime.lanes.find((entry) => entry.queueId === item.queueId && entry.workerId === workerId
1392
+ && entry.status === "leased");
1393
+ if (!lane) throw new Error("agent lane lease is missing");
1394
+ const current = timestamp(now);
1395
+ const message = safeText(String(error || "host runtime unavailable"), "runError", 500);
1396
+ const delay = Number(retryAfterMs);
1397
+ if (!Number.isFinite(delay) || delay < 250 || delay > 300000) throw new Error("retryAfterMs must be 250-300000");
1398
+ preserve(runtime, "queue", item, "run-failed", current);
1399
+ item.status = item.attempts >= 3 ? "dead-letter" : "pending";
1400
+ item.lease = null; item.lastError = message; item.updatedAt = current;
1401
+ item.availableAt = new Date(new Date(current).getTime() + delay).toISOString();
1402
+ if (item.status === "dead-letter") item.completedAt = current;
1403
+ lane.status = "completed"; lane.updatedAt = current;
1404
+ runtime.health.host = "failed"; runtime.health.worker = "degraded"; runtime.health.lastTickAt = current;
1405
+ runtime.revision += 1;
1406
+ const receipt = appendReceipt(runtime, item.status === "dead-letter" ? "run-dead-letter" : "run-retry",
1407
+ item.queueId, current, { attempt: item.attempts });
1408
+ await writeJson(paths.gatewayRuntimePath, runtime);
1409
+ return { item, receipt, policyEnabled: policy.enabled && !policy.killSwitch };
1410
+ });
1411
+ }
1412
+
1413
+ export async function deliverPrepared({ root = process.cwd(), outboxId, adapter, now = new Date() }) {
1414
+ if (!adapter || typeof adapter.send !== "function") throw new Error("delivery adapter is unavailable");
1415
+ const paths = await pathsFor(root);
1416
+ let prepared;
1417
+ await withLock(paths, async () => {
1418
+ const [policy, runtime, channelPolicy, channelRuntime, personas] = await Promise.all([
1419
+ readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
1420
+ readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime),
1421
+ loadChannelPolicy(paths.catalog.root, paths.catalog), loadChannelRuntime(paths.catalog.root, paths.catalog),
1422
+ loadPersonaRuntime(paths.catalog.root, paths.catalog)
1423
+ ]);
1424
+ if (!policy.enabled || policy.killSwitch) throw new Error("gateway was disabled before delivery");
1425
+ const outbox = runtime.outbox.find((item) => item.outboxId === exactId(outboxId, "outboxId"));
1426
+ if (!outbox) throw new Error("unknown outbox item");
1427
+ if (["delivered", "acknowledged"].includes(outbox.status)) { prepared = { duplicate: true, outbox: structuredClone(outbox) }; return; }
1428
+ if (outbox.status !== "prepared" && outbox.status !== "failed") throw new Error("outbox item is not safely retryable");
1429
+ if (new Date(outbox.nextAttemptAt) > new Date(timestamp(now))) throw new Error("outbox retry is not due");
1430
+ const event = channelRuntime.runtime.events.find((item) => item.eventId === outbox.eventId);
1431
+ if (!event) throw new Error("outbox channel event is missing");
1432
+ try {
1433
+ assertActivePersona(personas.policy, personas.runtime, event.agentId, event.projectId, event.groupId);
1434
+ exactReplyBinding(channelPolicy.policy, event);
1435
+ }
1436
+ catch (error) {
1437
+ const current = timestamp(now);
1438
+ preserve(runtime, "outbox", outbox, "capability-revoked", current);
1439
+ outbox.status = "dead-letter"; outbox.lastError = safeText(error.message, "adapterError", 500);
1440
+ outbox.updatedAt = current;
1441
+ const queue = runtime.queue.find((item) => item.queueId === outbox.queueId);
1442
+ if (queue) { queue.status = "dead-letter"; queue.completedAt = current; queue.updatedAt = current; }
1443
+ runtime.health.adapter = "failed"; runtime.revision += 1;
1444
+ const receipt = appendReceipt(runtime, "dead-letter", outbox.outboxId, current, { reason: "identity-or-capability-revoked" });
1445
+ await writeJson(paths.gatewayRuntimePath, runtime);
1446
+ prepared = { duplicate: false, terminal: true, outbox: structuredClone(outbox), receipt };
1447
+ return;
1448
+ }
1449
+ preserve(runtime, "outbox", outbox, "sending", timestamp(now));
1450
+ outbox.status = "sending"; outbox.attempts += 1; outbox.updatedAt = timestamp(now); runtime.revision += 1;
1451
+ appendReceipt(runtime, "sending", outbox.outboxId, outbox.updatedAt, { attempt: outbox.attempts });
1452
+ await writeJson(paths.gatewayRuntimePath, runtime);
1453
+ prepared = { duplicate: false, outbox: structuredClone(outbox) };
1454
+ });
1455
+ if (prepared.duplicate || prepared.terminal) return prepared;
1456
+ let outcome;
1457
+ try { outcome = await adapter.send(structuredClone(prepared.outbox)); }
1458
+ catch (error) { outcome = { ok: false, effect: "unknown", error: error.message }; }
1459
+ return withLock(paths, async () => {
1460
+ const runtime = await readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime);
1461
+ const outbox = runtime.outbox.find((item) => item.outboxId === prepared.outbox.outboxId);
1462
+ if (!outbox || outbox.status !== "sending") throw new Error("outbox sending state changed unexpectedly");
1463
+ const current = timestamp(now);
1464
+ preserve(runtime, "outbox", outbox, outcome?.ok ? "delivered" : "send-failed", current);
1465
+ if (outcome?.ok) {
1466
+ outbox.status = "delivered"; outbox.deliveredAt = current; outbox.adapterReceipt = safeText(String(outcome.receipt || "delivered"), "adapterReceipt", 500);
1467
+ runtime.health.adapter = "healthy";
1468
+ const queue = runtime.queue.find((item) => item.queueId === outbox.queueId);
1469
+ if (queue) { queue.status = "completed"; queue.completedAt = current; queue.updatedAt = current; }
1470
+ } else if (outcome?.effect === "none") {
1471
+ outbox.status = outbox.attempts < 3 ? "failed" : "dead-letter";
1472
+ outbox.lastError = safeText(String(outcome.error || "adapter failure"), "adapterError", 500);
1473
+ runtime.health.adapter = outbox.status === "failed" ? "degraded" : "failed";
1474
+ if (outbox.status === "failed") {
1475
+ const delay = Math.min(300000, Number(outcome.retryAfterMs) || 1000 * (2 ** outbox.attempts));
1476
+ outbox.nextAttemptAt = new Date(new Date(current).getTime() + delay).toISOString();
1477
+ } else {
1478
+ const queue = runtime.queue.find((item) => item.queueId === outbox.queueId);
1479
+ if (queue) { queue.status = "dead-letter"; queue.completedAt = current; queue.updatedAt = current; }
1480
+ }
1481
+ } else {
1482
+ outbox.status = "delivery-unknown"; outbox.lastError = safeText(String(outcome?.error || "delivery outcome is ambiguous"), "adapterError", 500);
1483
+ runtime.health.adapter = "failed";
1484
+ }
1485
+ outbox.updatedAt = current; runtime.revision += 1;
1486
+ const receipt = appendReceipt(runtime, outbox.status, outbox.outboxId, current, { adapterReceipt: outbox.adapterReceipt });
1487
+ await writeJson(paths.gatewayRuntimePath, runtime);
1488
+ return { outbox, receipt, duplicate: false };
1489
+ });
1490
+ }
1491
+
1492
+ export async function updateGatewayHealth({ root = process.cwd(), worker = null, adapter = null, host = null,
1493
+ now = new Date() } = {}) {
1494
+ const paths = await pathsFor(root);
1495
+ return withLock(paths, async () => {
1496
+ const runtime = await readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime);
1497
+ for (const [key, value] of Object.entries({ worker, adapter, host })) {
1498
+ if (value !== null) {
1499
+ if (!HEALTH_VALUES.has(value)) throw new Error("unsupported gateway health value");
1500
+ runtime.health[key] = value;
1501
+ }
1502
+ }
1503
+ runtime.health.lastTickAt = timestamp(now); runtime.revision += 1;
1504
+ await writeJson(paths.gatewayRuntimePath, runtime);
1505
+ return structuredClone(runtime.health);
1506
+ });
1507
+ }
1508
+
1509
+ export async function loadGatewayRuntime(root = process.cwd(), catalog = null) {
1510
+ const paths = await pathsFor(root, catalog);
1511
+ const [policy, runtime] = await Promise.all([
1512
+ readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
1513
+ readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime)
1514
+ ]);
1515
+ return { policy, runtime, ...paths };
1516
+ }
1517
+
1518
+ export async function inspectGatewayRuntime(root = process.cwd(), catalog = null) {
1519
+ const paths = await pathsFor(root, catalog); const errors = [];
1520
+ let policy = emptyPolicy(paths.catalog.root); let runtime = emptyRuntime(paths.catalog.root);
1521
+ try { policy = await readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy); } catch (error) { errors.push("policy:" + error.message); }
1522
+ try { runtime = await readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime); } catch (error) { errors.push("runtime:" + error.message); }
1523
+ return { policy, runtime, errors, ...paths };
1524
+ }
1525
+
1526
+ export function gatewayRuntimeFindings(policy, runtime) {
1527
+ const findings = [];
1528
+ const active = new Set();
1529
+ const goalIds = new Set();
1530
+ for (const goal of policy.goals) {
1531
+ if (!validGoal(goal)) findings.push("invalid-goal:" + (goal?.goalId || "unknown"));
1532
+ else if (!validGoalTransferProofs(goal, policy.goals)) findings.push("invalid-strategy-transfer:" + goal.goalId);
1533
+ if (goalIds.has(goal.goalId)) findings.push("duplicate-goal:" + goal.goalId);
1534
+ goalIds.add(goal.goalId);
1535
+ if (goal.status === "active" && active.has(goal.agentId)) findings.push("multiple-active-goals:" + goal.agentId);
1536
+ if (goal.status === "active") active.add(goal.agentId);
1537
+ }
1538
+ for (const item of policy.history) if (!validPolicyHistory(item)) findings.push("invalid-gateway-policy-history");
1539
+ const queueIds = new Set(); const dedupeKeys = new Set();
1540
+ for (const item of runtime.queue) {
1541
+ if (!validQueue(item)) findings.push("invalid-queue-item:" + (item?.queueId || "unknown"));
1542
+ if (queueIds.has(item.queueId)) findings.push("duplicate-queue-item:" + item.queueId);
1543
+ if (dedupeKeys.has(item.dedupeKey)) findings.push("duplicate-queue-dedupe:" + item.dedupeKey);
1544
+ queueIds.add(item.queueId); dedupeKeys.add(item.dedupeKey);
1545
+ if (item.goalStepId) {
1546
+ const goal = policy.goals.find((entry) => entry.goalId === item.goalId);
1547
+ const step = goal?.plan?.steps.find((entry) => entry.stepId === item.goalStepId);
1548
+ if (!step) findings.push("orphan-goal-step:" + item.queueId);
1549
+ else if (item.agentId !== planStepAgentId(goal, step)) findings.push("goal-step-agent-mismatch:" + item.queueId);
1550
+ }
1551
+ }
1552
+ const outboxIds = new Set(); const idempotencyKeys = new Set();
1553
+ for (const item of runtime.outbox) {
1554
+ if (!validOutbox(item)) findings.push("invalid-outbox-item:" + (item?.outboxId || "unknown"));
1555
+ if (!queueIds.has(item.queueId)) findings.push("orphan-outbox-item:" + item.outboxId);
1556
+ if (outboxIds.has(item.outboxId)) findings.push("duplicate-outbox-item:" + item.outboxId);
1557
+ if (idempotencyKeys.has(item.idempotencyKey)) findings.push("duplicate-outbox-idempotency:" + item.idempotencyKey);
1558
+ outboxIds.add(item.outboxId); idempotencyKeys.add(item.idempotencyKey);
1559
+ }
1560
+ for (const item of runtime.receipts) if (!validReceipt(item)
1561
+ || (!queueIds.has(item.objectId) && !outboxIds.has(item.objectId))) findings.push("invalid-gateway-receipt:" + (item?.id || "unknown"));
1562
+ for (const item of runtime.history) if (!validHistory(item)) findings.push("invalid-gateway-history:" + (item?.objectId || "unknown"));
1563
+ const leased = runtime.lanes.filter((item) => item.status === "leased");
1564
+ for (const lane of runtime.lanes) {
1565
+ if (!validLane(lane)) findings.push("invalid-agent-lane:" + (lane?.queueId || "unknown"));
1566
+ const queue = runtime.queue.find((item) => item.queueId === lane.queueId);
1567
+ if (!queue || (lane.status === "leased" && (queue.status !== "leased" || queue.lease?.workerId !== lane.workerId))) {
1568
+ findings.push("agent-lane-queue-mismatch:" + (lane.queueId || "unknown"));
1569
+ }
1570
+ }
1571
+ if (new Set(leased.map((item) => item.agentId)).size !== leased.length) findings.push("duplicate-agent-lane");
1572
+ if (!validHealth(runtime.health)) findings.push("invalid-gateway-health");
1573
+ return findings;
1574
+ }
1575
+
1576
+ export function gatewayHealthFindings(policy, runtime, { now = new Date(), staleAfterMs = 180000 } = {}) {
1577
+ if (!policy.enabled || policy.killSwitch) return [];
1578
+ const findings = [];
1579
+ if (runtime.health.gateway !== "running") findings.push("gateway-not-running");
1580
+ if (runtime.health.scheduler !== "healthy") findings.push("scheduler-not-healthy");
1581
+ if (runtime.health.queue !== "healthy") findings.push("queue-not-healthy");
1582
+ if (!new Set(["healthy", "degraded"]).has(runtime.health.worker)) findings.push("worker-not-healthy");
1583
+ if (!new Set(["healthy", "degraded"]).has(runtime.health.adapter)) findings.push("adapter-not-healthy");
1584
+ const current = now instanceof Date ? now : new Date(now);
1585
+ const tick = runtime.health.lastTickAt === null ? null : new Date(runtime.health.lastTickAt);
1586
+ const reconciliation = runtime.health.lastReconciledAt === null ? null : new Date(runtime.health.lastReconciledAt);
1587
+ if (!tick || current.getTime() - tick.getTime() > staleAfterMs) findings.push("worker-heartbeat-stale");
1588
+ if (!reconciliation || current.getTime() - reconciliation.getTime() > staleAfterMs) findings.push("scheduler-heartbeat-stale");
1589
+ return findings;
1590
+ }
1591
+
1592
+ export async function gatewayContext({ root = process.cwd(), agentId = null } = {}) {
1593
+ const { policy, runtime } = await loadGatewayRuntime(root);
1594
+ const findings = gatewayRuntimeFindings(policy, runtime);
1595
+ if (findings.length) throw new Error("gateway runtime failed closed: " + findings.join(", "));
1596
+ const exactAgentId = agentId === null ? null : exactId(agentId, "agentId");
1597
+ const goals = policy.goals.filter((item) => exactAgentId === null || item.agentId === exactAgentId
1598
+ || item.plan?.steps.some((step) => planStepAgentId(item, step) === exactAgentId));
1599
+ const executionAttempts = goals.map((goal) => {
1600
+ const step = currentPlanStep(goal); const attempt = executionAttemptForStep(step);
1601
+ return attempt && (exactAgentId === null || planStepAgentId(goal, step) === exactAgentId)
1602
+ ? { goalId: goal.goalId, goalStepId: step.stepId, ...attempt } : null;
1603
+ }).filter(Boolean);
1604
+ const queue = runtime.queue.filter((item) => exactAgentId === null || item.agentId === exactAgentId);
1605
+ const queueIds = new Set(queue.map((item) => item.queueId));
1606
+ const leased = runtime.queue.filter((item) => item.status === "leased");
1607
+ const resourceWaits = queue.filter((item) => item.status === "pending").map((item) => {
1608
+ const blockers = leased.map((entry) => ({ entry, resources: conflictingResources(policy, item, entry) }))
1609
+ .filter(({ resources }) => resources.length > 0);
1610
+ return blockers.length ? {
1611
+ queueId: item.queueId,
1612
+ resources: [...new Set(blockers.flatMap(({ resources }) => resources))].sort(),
1613
+ blockedByQueueIds: blockers.map(({ entry }) => entry.queueId).sort(),
1614
+ authority: "execution-state-only"
1615
+ } : null;
1616
+ }).filter(Boolean);
1617
+ return { schema: "agentspine.gateway-context/v1", enabled: policy.enabled, killSwitch: policy.killSwitch,
1618
+ goals: structuredClone(goals), queue: structuredClone(queue),
1619
+ outbox: structuredClone(runtime.outbox.filter((item) => queueIds.has(item.queueId))),
1620
+ resourceWaits, executionAttempts,
1621
+ health: structuredClone(runtime.health), healthFindings: gatewayHealthFindings(policy, runtime),
1622
+ authority: "execution-state-only" };
1623
+ }