blun-king-cli 9.1.565 → 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.
@@ -17,6 +17,9 @@ export const GATEWAY_RUNTIME_SCHEMA = "agentspine.gateway-runtime/v1";
17
17
  export const GATEWAY_EVENT_SCHEMA = "agentspine.gateway-event/v1";
18
18
  export const GOAL_PLAN_SCHEMA = "agentspine.goal-plan/v1";
19
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";
20
23
 
21
24
  const CONFIRMATION = "local-owner-confirmed";
22
25
  const MAX_BYTES = 8 * 1024 * 1024;
@@ -33,6 +36,7 @@ const SECRET_KEY_RE = /"(?:api[-_ ]?key|token|password|secret|credential)"\s*:/i
33
36
  const AUTHORITY_RE = /\b(?:permission|rights?|roles?|owner|trusted|delegat|authorized|approval|production|payment|spending|tool capability|send capability)\b/i;
34
37
  const HEALTH_VALUES = new Set(["stopped", "running", "unknown", "healthy", "degraded", "failed"]);
35
38
  const KNOWLEDGE_EVIDENCE = new Set(["owner-input", "objective-observation"]);
39
+ const METRIC_OPERATORS = new Set(["gte", "lte", "eq"]);
36
40
 
37
41
  function emptyPolicy(root) {
38
42
  return { schema: GATEWAY_POLICY_SCHEMA, root, revision: 0, enabled: false, killSwitch: false, goals: [], history: [] };
@@ -136,8 +140,358 @@ function validKnowledgeGap(gap) {
136
140
  return gap.resolutionDigest === sha256(JSON.stringify(knowledgeGapResolutionMaterial(gap)));
137
141
  }
138
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
+
139
489
  function planDefinitionMaterial(steps) {
140
- return steps.map(({ stepId, title, successCriterion, dependsOn }) => ({ stepId, title, successCriterion, dependsOn }));
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
+ }));
141
495
  }
142
496
 
143
497
  function validGoalPlan(plan) {
@@ -149,6 +503,14 @@ function validGoalPlan(plan) {
149
503
  if (ids.size !== plan.steps.length || ids.has(undefined)) return false;
150
504
  for (const step of plan.steps) {
151
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)))))
152
514
  && typeof step.successCriterion === "string" && step.successCriterion.length > 0 && step.successCriterion.length <= 1000
153
515
  && Array.isArray(step.dependsOn) && new Set(step.dependsOn).size === step.dependsOn.length
154
516
  && step.dependsOn.every((dependency) => ids.has(dependency) && dependency !== step.stepId)
@@ -161,7 +523,8 @@ function validGoalPlan(plan) {
161
523
  && new Set(step.knowledgeGaps.map((gap) => gap.gapId)).size === step.knowledgeGaps.length
162
524
  && step.knowledgeGaps.every((gap) => validKnowledgeGap(gap)
163
525
  && gap.goalStepId === step.stepId && gap.planDefinitionsDigest === plan.definitionsDigest)))
164
- && Number.isFinite(new Date(step.updatedAt).getTime()))) return false;
526
+ && Number.isFinite(new Date(step.updatedAt).getTime())
527
+ && (step.executionOutcomes || []).every((outcome) => new Date(outcome.observedAt) <= new Date(step.updatedAt)))) return false;
165
528
  }
166
529
  const visiting = new Set(); const visited = new Set();
167
530
  const visit = (stepId) => {
@@ -181,6 +544,8 @@ function validGoalPlan(plan) {
181
544
  if (plan.steps.some((step) => (step.knowledgeGaps || []).filter((gap) => gap.status === "open").length > 1)) return false;
182
545
  if (plan.steps.some((step) => (step.knowledgeGaps || []).some((gap) => gap.status === "open")
183
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;
184
549
  if (plan.definitionsDigest !== sha256(JSON.stringify(planDefinitionMaterial(plan.steps)))) return false;
185
550
  const current = plan.steps.filter((step) => ["active", "blocked"].includes(step.status));
186
551
  return current.length <= 1 && (plan.currentStepId === null
@@ -195,15 +560,19 @@ function activateNextPlanStep(plan, now) {
195
560
  return next;
196
561
  }
197
562
 
198
- function createGoalPlan(steps, now) {
563
+ function createGoalPlan(steps, now, defaultAgentId, transferContext) {
199
564
  if (!Array.isArray(steps) || steps.length === 0 || steps.length > 32) throw new Error("goal plan requires 1-32 steps");
200
565
  const normalized = steps.map((step, index) => ({
201
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),
202
571
  title: safeText(step?.title, `steps[${index}].title`, 500),
203
572
  successCriterion: safeText(step?.successCriterion, `steps[${index}].successCriterion`),
204
573
  dependsOn: Array.isArray(step?.dependsOn) ? step.dependsOn.map((dependency) => exactId(dependency, `steps[${index}].dependsOn`)) : [],
205
574
  status: "pending", checkpoint: null, blocker: null, completedAt: null, completedByQueueId: null,
206
- knowledgeGaps: [], updatedAt: now
575
+ knowledgeGaps: [], executionOutcomes: [], updatedAt: now
207
576
  }));
208
577
  const plan = { schema: GOAL_PLAN_SCHEMA, revision: 0, currentStepId: null, steps: normalized,
209
578
  definitionsDigest: sha256(JSON.stringify(planDefinitionMaterial(normalized))), authority: "context-only-plan" };
@@ -216,6 +585,31 @@ function currentPlanStep(goal) {
216
585
  return goal.plan?.steps.find((step) => step.stepId === goal.plan.currentStepId) || null;
217
586
  }
218
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
+
219
613
  function createKnowledgeGap(goal, step, queueId, request, now) {
220
614
  const gap = {
221
615
  schema: KNOWLEDGE_GAP_SCHEMA, gapId: null, goalId: goal.goalId, goalStepId: step.stepId,
@@ -244,8 +638,8 @@ function planQueueKey(goalId, stepId, phase, suffix = "") {
244
638
 
245
639
  function newGoalQueue(goal, step, kind, key, current, availableAt = current) {
246
640
  return { queueId: "gateway-queue:" + sha256(key).slice(0, 32), dedupeKey: key, kind,
247
- agentId: goal.agentId, projectId: goal.projectId, groupId: goal.groupId, goalId: goal.goalId,
248
- goalStepId: step?.stepId || null, channelEventId: null, priority: PRIORITY[kind], status: "pending",
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",
249
643
  attempts: 0, lease: null, availableAt, createdAt: current, updatedAt: current,
250
644
  completedAt: null, lastError: null, authority: "execution-state-only" };
251
645
  }
@@ -345,6 +739,7 @@ function normalizePolicy(value, root) {
345
739
  if (!value || value.schema !== GATEWAY_POLICY_SCHEMA || value.root !== root
346
740
  || !Number.isInteger(value.revision) || typeof value.enabled !== "boolean" || typeof value.killSwitch !== "boolean"
347
741
  || !Array.isArray(value.goals) || !Array.isArray(value.history) || value.goals.some((item) => !validGoal(item))
742
+ || value.goals.some((item) => !validGoalTransferProofs(item, value.goals))
348
743
  || value.history.some((item) => !validPolicyHistory(item))) {
349
744
  throw new Error("gateway policy is invalid; autonomous runtime is disabled");
350
745
  }
@@ -471,14 +866,25 @@ export async function assignGoal({ root = process.cwd(), goalId, agentId, ownerS
471
866
  readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime),
472
867
  loadPersonaRuntime(paths.catalog.root, paths.catalog)
473
868
  ]);
474
- agentId = exactId(agentId, "agentId"); projectId = exactId(projectId, "projectId"); groupId = exactId(groupId, "groupId", true);
475
- assertActivePersona(personas.policy, personas.runtime, agentId, projectId, groupId);
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);
476
872
  const createdAt = timestamp(now);
477
873
  const active = policy.goals.find((item) => item.agentId === agentId && item.status === "active" && item.goalId !== goalId);
478
874
  if (active) throw new Error("an agent may have only one active focused goal");
479
- const plan = steps === null ? null : createGoalPlan(steps, createdAt);
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
+ }
480
886
  const firstStep = plan ? plan.steps.find((step) => step.stepId === plan.currentStepId) : null;
481
- const goal = { goalId: exactId(goalId, "goalId"), agentId, ownerSubjectId: exactId(ownerSubjectId, "ownerSubjectId"),
887
+ const goal = { goalId, agentId, ownerSubjectId: exactId(ownerSubjectId, "ownerSubjectId"),
482
888
  projectId, groupId, priority: Number(priority), successCriterion: safeText(successCriterion, "successCriterion"),
483
889
  nextSafeStep: safeText(firstStep?.title || nextSafeStep, "nextSafeStep"), deadline: deadline === null ? null : timestamp(deadline),
484
890
  status: "active", checkpoint: null, heartbeatAt: null, blocker: null, createdAt, updatedAt: createdAt,
@@ -496,6 +902,10 @@ export async function assignGoal({ root = process.cwd(), goalId, agentId, ownerS
496
902
  if ((blockedStep.knowledgeGaps || []).some((gap) => gap.status === "open")) {
497
903
  throw new Error("blocked goal plan has an open knowledge gap; resolve it with goal-clarify");
498
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
+ }
499
909
  policy.history.push({ kind: "goal", at: createdAt, value: structuredClone(previous), authority: "authenticated-goal-policy" });
500
910
  blockedStep.status = "active"; blockedStep.blocker = null; blockedStep.updatedAt = createdAt;
501
911
  previous.status = "active"; previous.blocker = null; previous.updatedAt = createdAt; previous.plan.revision += 1;
@@ -640,11 +1050,32 @@ export async function reconcileGateway({ root = process.cwd(), now = new Date()
640
1050
  outbox.status = "delivery-unknown"; outbox.updatedAt = current;
641
1051
  appendReceipt(runtime, "delivery-unknown", outbox.outboxId, current, { reason: "crash-during-send" });
642
1052
  }
1053
+ let policyChanged = false;
643
1054
  if (policy.enabled && !policy.killSwitch) {
644
1055
  for (const goal of policy.goals.filter((item) => item.status === "active" && item.plan)) {
645
1056
  const step = currentPlanStep(goal);
646
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
+ }
647
1077
  const runnable = runtime.queue.some((item) => item.goalId === goal.goalId && item.goalStepId === step.stepId
1078
+ && item.agentId === planStepAgentId(goal, step)
648
1079
  && ["pending", "leased", "awaiting-delivery"].includes(item.status));
649
1080
  if (!runnable) {
650
1081
  const key = planQueueKey(goal.goalId, step.stepId, "recovery", String(goal.plan.revision));
@@ -657,13 +1088,15 @@ export async function reconcileGateway({ root = process.cwd(), now = new Date()
657
1088
  }
658
1089
  for (const goal of policy.goals.filter((item) => item.status === "active" && item.deadline
659
1090
  && new Date(item.deadline) <= new Date(current))) {
660
- try { assertActivePersona(personas.policy, personas.runtime, goal.agentId, goal.projectId, goal.groupId); }
1091
+ const step = currentPlanStep(goal);
1092
+ const deadlineAgentId = planStepAgentId(goal, step);
1093
+ try { assertActivePersona(personas.policy, personas.runtime, deadlineAgentId, goal.projectId, goal.groupId); }
661
1094
  catch { continue; }
662
1095
  const key = "goal:" + goal.goalId + ":deadline:" + goal.deadline;
663
1096
  if (!runtime.queue.some((item) => item.dedupeKey === key)) runtime.queue.push({
664
1097
  queueId: "gateway-queue:" + sha256(key).slice(0, 32), dedupeKey: key, kind: "deadline",
665
- agentId: goal.agentId, projectId: goal.projectId, groupId: goal.groupId, goalId: goal.goalId,
666
- goalStepId: currentPlanStep(goal)?.stepId || null, channelEventId: null, priority: PRIORITY.deadline, status: "pending", attempts: 0, lease: null,
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,
667
1100
  availableAt: current, createdAt: current, updatedAt: current, completedAt: null, lastError: null,
668
1101
  authority: "execution-state-only"
669
1102
  });
@@ -691,7 +1124,10 @@ export async function reconcileGateway({ root = process.cwd(), now = new Date()
691
1124
  runtime.health.gateway = policy.enabled && !policy.killSwitch ? "running" : "stopped";
692
1125
  runtime.health.scheduler = "healthy"; runtime.health.queue = "healthy"; runtime.health.lastReconciledAt = current;
693
1126
  runtime.revision += 1;
694
- await writeJson(paths.gatewayRuntimePath, runtime);
1127
+ if (policyChanged) await Promise.all([
1128
+ writeJson(paths.gatewayPolicyPath, policy), writeJson(paths.gatewayRuntimePath, runtime)
1129
+ ]);
1130
+ else await writeJson(paths.gatewayRuntimePath, runtime);
695
1131
  return { policy, runtime, recovered: true };
696
1132
  });
697
1133
  }
@@ -710,7 +1146,8 @@ export async function claimGatewayWork({ root = process.cwd(), workerId, leaseSe
710
1146
  if (!Number.isInteger(seconds) || seconds < 15 || seconds > 900) throw new Error("leaseSeconds must be 15-900");
711
1147
  const items = runtime.queue.filter((item) => item.status === "pending" && new Date(item.availableAt) <= new Date(current)
712
1148
  && !currentLane(runtime, item.agentId));
713
- items.sort((a, b) => b.priority - a.priority || a.createdAt.localeCompare(b.createdAt) || a.queueId.localeCompare(b.queueId));
1149
+ items.sort((a, b) => effectiveQueuePriority(policy, b) - effectiveQueuePriority(policy, a)
1150
+ || a.createdAt.localeCompare(b.createdAt) || a.queueId.localeCompare(b.queueId));
714
1151
  let item = null; let revoked = false;
715
1152
  for (const candidate of items) {
716
1153
  try {
@@ -725,7 +1162,8 @@ export async function claimGatewayWork({ root = process.cwd(), workerId, leaseSe
725
1162
  if (candidate.goalStepId) {
726
1163
  const goal = policy.goals.find((entry) => entry.goalId === candidate.goalId);
727
1164
  const step = goal && currentPlanStep(goal);
728
- if (!goal?.plan || goal.status !== "active" || step?.stepId !== candidate.goalStepId || step.status !== "active") {
1165
+ if (!goal?.plan || goal.status !== "active" || step?.stepId !== candidate.goalStepId || step.status !== "active"
1166
+ || candidate.agentId !== planStepAgentId(goal, step)) {
729
1167
  preserve(runtime, "queue", candidate, "plan-step-stale", current);
730
1168
  candidate.status = "cancelled"; candidate.completedAt = current; candidate.updatedAt = current;
731
1169
  appendReceipt(runtime, "plan-step-stale", candidate.queueId, current, { goalStepId: candidate.goalStepId });
@@ -733,6 +1171,9 @@ export async function claimGatewayWork({ root = process.cwd(), workerId, leaseSe
733
1171
  continue;
734
1172
  }
735
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;
736
1177
  item = candidate;
737
1178
  break;
738
1179
  }
@@ -778,9 +1219,10 @@ export async function completeGatewayRun({ root = process.cwd(), queueId, worker
778
1219
  const lane = runtime.lanes.find((entry) => entry.queueId === item.queueId && entry.workerId === workerId && entry.status === "leased");
779
1220
  if (!lane) throw new Error("agent lane lease is missing");
780
1221
  const boundGoal = item.goalId ? policy.goals.find((entry) => entry.goalId === item.goalId) : null;
1222
+ const boundStep = item.goalStepId ? boundGoal && currentPlanStep(boundGoal) : null;
781
1223
  if (item.goalStepId) {
782
- const step = boundGoal && currentPlanStep(boundGoal);
783
- if (!boundGoal?.plan || boundGoal.status !== "active" || step?.stepId !== item.goalStepId || step.status !== "active") {
1224
+ if (!boundGoal?.plan || boundGoal.status !== "active" || boundStep?.stepId !== item.goalStepId
1225
+ || boundStep.status !== "active" || item.agentId !== planStepAgentId(boundGoal, boundStep)) {
784
1226
  throw new Error("run completion is not bound to the current active goal step");
785
1227
  }
786
1228
  }
@@ -792,8 +1234,14 @@ export async function completeGatewayRun({ root = process.cwd(), queueId, worker
792
1234
  if (knowledgeGapRequest && (result?.blocked || result?.completed)) {
793
1235
  throw new Error("knowledge gap result cannot also complete or generically block a step");
794
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
+ }
795
1243
  const text = result?.text ? safeText(result.text, "result.text", 16000) : null;
796
- let clarification = null;
1244
+ let clarification = null; let exploration = null;
797
1245
  preserve(runtime, "queue", item, "run-completed", current);
798
1246
  if (item.channelEventId) {
799
1247
  if (!text) throw new Error("a channel obligation requires a non-empty response");
@@ -814,13 +1262,15 @@ export async function completeGatewayRun({ root = process.cwd(), queueId, worker
814
1262
  }
815
1263
  item.status = "awaiting-delivery";
816
1264
  } else {
817
- item.status = result?.blocked || knowledgeGapRequest ? "blocked" : "completed"; item.completedAt = current;
1265
+ item.status = result?.blocked || knowledgeGapRequest || (executionReview && !executionReview.passed)
1266
+ ? "blocked" : "completed"; item.completedAt = current;
818
1267
  const goal = boundGoal;
819
1268
  if (goal) {
820
1269
  policy.history.push({ kind: "goal", at: current, value: structuredClone(goal), authority: "authenticated-goal-policy" });
821
1270
  const checkpoint = result?.checkpoint === undefined ? goal.checkpoint : safeCheckpoint(result.checkpoint);
822
1271
  goal.checkpoint = checkpoint; goal.heartbeatAt = current;
823
- goal.blocker = result?.blocked ? safeText(result.blocker || "Run blocked.", "blocker", 500) : null;
1272
+ goal.blocker = result?.blocked ? safeText(result.blocker || "Run blocked.", "blocker", 500)
1273
+ : executionReview && !executionReview.passed ? executionReview.reason : null;
824
1274
  if (goal.plan && item.goalStepId) {
825
1275
  const step = currentPlanStep(goal);
826
1276
  step.checkpoint = checkpoint; step.updatedAt = current;
@@ -847,7 +1297,38 @@ export async function completeGatewayRun({ root = process.cwd(), queueId, worker
847
1297
  goal.plan.revision += 1;
848
1298
  } else if (result?.blocked) {
849
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
+ }
850
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
+ }
851
1332
  step.status = "completed"; step.completedAt = current; step.completedByQueueId = item.queueId; step.blocker = null;
852
1333
  goal.plan.currentStepId = null; goal.plan.revision += 1;
853
1334
  const next = activateNextPlanStep(goal.plan, current);
@@ -877,8 +1358,8 @@ export async function completeGatewayRun({ root = process.cwd(), queueId, worker
877
1358
  : "goal:" + goal.goalId + ":follow-up:" + checkpointDigest;
878
1359
  if (!runtime.queue.some((entry) => entry.dedupeKey === key)) runtime.queue.push({
879
1360
  queueId: "gateway-queue:" + sha256(key).slice(0, 32), dedupeKey: key, kind: "follow-up",
880
- agentId: goal.agentId, projectId: goal.projectId, groupId: goal.groupId, goalId: goal.goalId,
881
- goalStepId: step?.stepId || null, channelEventId: null, priority: PRIORITY["follow-up"], status: "pending", attempts: 0, lease: null,
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,
882
1363
  availableAt: new Date(new Date(current).getTime() + 60000).toISOString(), createdAt: current, updatedAt: current,
883
1364
  completedAt: null, lastError: null, authority: "execution-state-only"
884
1365
  });
@@ -889,7 +1370,8 @@ export async function completeGatewayRun({ root = process.cwd(), queueId, worker
889
1370
  runtime.health.host = "healthy"; runtime.health.worker = "healthy"; runtime.health.lastTickAt = current;
890
1371
  appendReceipt(runtime, "run-terminal", item.queueId, current, { status: item.status, goalStepId: item.goalStepId || null }); runtime.revision += 1;
891
1372
  await Promise.all([writeJson(paths.gatewayPolicyPath, policy), writeJson(paths.gatewayRuntimePath, runtime)]);
892
- return { item, outbox: runtime.outbox.find((entry) => entry.queueId === item.queueId) || null, clarification };
1373
+ return { item, outbox: runtime.outbox.find((entry) => entry.queueId === item.queueId) || null,
1374
+ clarification, exploration, executionReview };
893
1375
  });
894
1376
  }
895
1377
 
@@ -1047,6 +1529,7 @@ export function gatewayRuntimeFindings(policy, runtime) {
1047
1529
  const goalIds = new Set();
1048
1530
  for (const goal of policy.goals) {
1049
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);
1050
1533
  if (goalIds.has(goal.goalId)) findings.push("duplicate-goal:" + goal.goalId);
1051
1534
  goalIds.add(goal.goalId);
1052
1535
  if (goal.status === "active" && active.has(goal.agentId)) findings.push("multiple-active-goals:" + goal.agentId);
@@ -1061,7 +1544,9 @@ export function gatewayRuntimeFindings(policy, runtime) {
1061
1544
  queueIds.add(item.queueId); dedupeKeys.add(item.dedupeKey);
1062
1545
  if (item.goalStepId) {
1063
1546
  const goal = policy.goals.find((entry) => entry.goalId === item.goalId);
1064
- if (!goal?.plan?.steps.some((step) => step.stepId === item.goalStepId)) findings.push("orphan-goal-step:" + item.queueId);
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);
1065
1550
  }
1066
1551
  }
1067
1552
  const outboxIds = new Set(); const idempotencyKeys = new Set();
@@ -1108,12 +1593,31 @@ export async function gatewayContext({ root = process.cwd(), agentId = null } =
1108
1593
  const { policy, runtime } = await loadGatewayRuntime(root);
1109
1594
  const findings = gatewayRuntimeFindings(policy, runtime);
1110
1595
  if (findings.length) throw new Error("gateway runtime failed closed: " + findings.join(", "));
1111
- const goals = policy.goals.filter((item) => agentId === null || item.agentId === exactId(agentId, "agentId"));
1112
- const queue = runtime.queue.filter((item) => agentId === null || item.agentId === exactId(agentId, "agentId"));
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);
1113
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);
1114
1617
  return { schema: "agentspine.gateway-context/v1", enabled: policy.enabled, killSwitch: policy.killSwitch,
1115
1618
  goals: structuredClone(goals), queue: structuredClone(queue),
1116
1619
  outbox: structuredClone(runtime.outbox.filter((item) => queueIds.has(item.queueId))),
1620
+ resourceWaits, executionAttempts,
1117
1621
  health: structuredClone(runtime.health), healthFindings: gatewayHealthFindings(policy, runtime),
1118
1622
  authority: "execution-state-only" };
1119
1623
  }