@useorgx/openclaw-plugin 0.4.9 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/README.md +35 -0
  2. package/dashboard/dist/assets/BJgZIVUQ.js +53 -0
  3. package/dashboard/dist/assets/BJgZIVUQ.js.br +0 -0
  4. package/dashboard/dist/assets/BJgZIVUQ.js.gz +0 -0
  5. package/dashboard/dist/assets/BXWDRGm-.js +1 -0
  6. package/dashboard/dist/assets/BXWDRGm-.js.br +0 -0
  7. package/dashboard/dist/assets/BXWDRGm-.js.gz +0 -0
  8. package/dashboard/dist/assets/BgOYB78t.js +4 -0
  9. package/dashboard/dist/assets/BgOYB78t.js.br +0 -0
  10. package/dashboard/dist/assets/BgOYB78t.js.gz +0 -0
  11. package/dashboard/dist/assets/C-KIc3Wc.js.br +0 -0
  12. package/dashboard/dist/assets/C-KIc3Wc.js.gz +0 -0
  13. package/dashboard/dist/assets/CE38zU4U.js +1 -0
  14. package/dashboard/dist/assets/CE38zU4U.js.br +0 -0
  15. package/dashboard/dist/assets/CE38zU4U.js.gz +0 -0
  16. package/dashboard/dist/assets/CFGKRAzG.js +1 -0
  17. package/dashboard/dist/assets/CFGKRAzG.js.br +0 -0
  18. package/dashboard/dist/assets/CFGKRAzG.js.gz +0 -0
  19. package/dashboard/dist/assets/CGGR2GZh.js +1 -0
  20. package/dashboard/dist/assets/CGGR2GZh.js.br +0 -0
  21. package/dashboard/dist/assets/CGGR2GZh.js.gz +0 -0
  22. package/dashboard/dist/assets/CL_wXqR7.js +1 -0
  23. package/dashboard/dist/assets/CL_wXqR7.js.br +0 -0
  24. package/dashboard/dist/assets/CL_wXqR7.js.gz +0 -0
  25. package/dashboard/dist/assets/CPFiTmlw.js +8 -0
  26. package/dashboard/dist/assets/CPFiTmlw.js.br +0 -0
  27. package/dashboard/dist/assets/CPFiTmlw.js.gz +0 -0
  28. package/dashboard/dist/assets/CZZTvkQZ.js +1 -0
  29. package/dashboard/dist/assets/CZZTvkQZ.js.br +0 -0
  30. package/dashboard/dist/assets/CZZTvkQZ.js.gz +0 -0
  31. package/dashboard/dist/assets/{CpJsfbXo.js → CxQ08qFN.js} +2 -2
  32. package/dashboard/dist/assets/CxQ08qFN.js.br +0 -0
  33. package/dashboard/dist/assets/CxQ08qFN.js.gz +0 -0
  34. package/dashboard/dist/assets/D-bf6hEI.js +213 -0
  35. package/dashboard/dist/assets/D-bf6hEI.js.br +0 -0
  36. package/dashboard/dist/assets/D-bf6hEI.js.gz +0 -0
  37. package/dashboard/dist/assets/DG6y9wJI.js +2 -0
  38. package/dashboard/dist/assets/DG6y9wJI.js.br +0 -0
  39. package/dashboard/dist/assets/DG6y9wJI.js.gz +0 -0
  40. package/dashboard/dist/assets/DNxKz-GV.js +1 -0
  41. package/dashboard/dist/assets/DNxKz-GV.js.br +0 -0
  42. package/dashboard/dist/assets/DNxKz-GV.js.gz +0 -0
  43. package/dashboard/dist/assets/DW_rKUic.js +11 -0
  44. package/dashboard/dist/assets/DW_rKUic.js.br +0 -0
  45. package/dashboard/dist/assets/DW_rKUic.js.gz +0 -0
  46. package/dashboard/dist/assets/DbNoijHm.js +1 -0
  47. package/dashboard/dist/assets/DbNoijHm.js.br +0 -0
  48. package/dashboard/dist/assets/DbNoijHm.js.gz +0 -0
  49. package/dashboard/dist/assets/DjcdE6jC.js +2 -0
  50. package/dashboard/dist/assets/DjcdE6jC.js.br +0 -0
  51. package/dashboard/dist/assets/DjcdE6jC.js.gz +0 -0
  52. package/dashboard/dist/assets/FZYuCDnt.js +1 -0
  53. package/dashboard/dist/assets/FZYuCDnt.js.br +0 -0
  54. package/dashboard/dist/assets/FZYuCDnt.js.gz +0 -0
  55. package/dashboard/dist/assets/PAUiij_z.js +1 -0
  56. package/dashboard/dist/assets/PAUiij_z.js.br +0 -0
  57. package/dashboard/dist/assets/PAUiij_z.js.gz +0 -0
  58. package/dashboard/dist/assets/cNrhgGc1.js +8 -0
  59. package/dashboard/dist/assets/cNrhgGc1.js.br +0 -0
  60. package/dashboard/dist/assets/cNrhgGc1.js.gz +0 -0
  61. package/dashboard/dist/assets/h5biQs2I.css +1 -0
  62. package/dashboard/dist/assets/h5biQs2I.css.br +0 -0
  63. package/dashboard/dist/assets/h5biQs2I.css.gz +0 -0
  64. package/dashboard/dist/assets/ic2FaMnh.js +1 -0
  65. package/dashboard/dist/assets/ic2FaMnh.js.br +0 -0
  66. package/dashboard/dist/assets/ic2FaMnh.js.gz +0 -0
  67. package/dashboard/dist/assets/nByHNHoW.js +1 -0
  68. package/dashboard/dist/assets/nByHNHoW.js.br +0 -0
  69. package/dashboard/dist/assets/nByHNHoW.js.gz +0 -0
  70. package/dashboard/dist/assets/qm8xLgv-.css +1 -0
  71. package/dashboard/dist/assets/qm8xLgv-.css.br +0 -0
  72. package/dashboard/dist/assets/qm8xLgv-.css.gz +0 -0
  73. package/dashboard/dist/assets/tS9mbYZi.js +1 -0
  74. package/dashboard/dist/assets/tS9mbYZi.js.br +0 -0
  75. package/dashboard/dist/assets/tS9mbYZi.js.gz +0 -0
  76. package/dashboard/dist/brand/anthropic-mark.svg.br +0 -0
  77. package/dashboard/dist/brand/anthropic-mark.svg.gz +0 -0
  78. package/dashboard/dist/brand/openai-mark.svg.br +0 -0
  79. package/dashboard/dist/brand/openai-mark.svg.gz +0 -0
  80. package/dashboard/dist/brand/openclaw-mark.svg.br +0 -0
  81. package/dashboard/dist/brand/openclaw-mark.svg.gz +0 -0
  82. package/dashboard/dist/brand/xandy-orchestrator.png +0 -0
  83. package/dashboard/dist/index.html +7 -5
  84. package/dashboard/dist/index.html.br +0 -0
  85. package/dashboard/dist/index.html.gz +0 -0
  86. package/dist/activity-actor-fields.js +26 -4
  87. package/dist/activity-store.js +34 -8
  88. package/dist/agent-context-store.js +79 -17
  89. package/dist/agent-run-store.js +44 -3
  90. package/dist/agent-suite.d.ts +9 -0
  91. package/dist/agent-suite.js +149 -9
  92. package/dist/artifacts/artifact-domain-schemas.d.ts +66 -0
  93. package/dist/artifacts/artifact-domain-schemas.js +357 -0
  94. package/dist/artifacts/register-artifact.d.ts +4 -3
  95. package/dist/artifacts/register-artifact.js +170 -57
  96. package/dist/chat-store.d.ts +157 -0
  97. package/dist/chat-store.js +586 -0
  98. package/dist/cli/orgx.js +11 -0
  99. package/dist/contracts/client.d.ts +43 -3
  100. package/dist/contracts/client.js +159 -30
  101. package/dist/contracts/retro-schema.d.ts +81 -0
  102. package/dist/contracts/retro-schema.js +80 -0
  103. package/dist/contracts/shared-types.d.ts +159 -0
  104. package/dist/contracts/shared-types.js +177 -1
  105. package/dist/contracts/skill-pack-schema.d.ts +192 -0
  106. package/dist/contracts/skill-pack-schema.js +180 -0
  107. package/dist/contracts/types.d.ts +227 -2
  108. package/dist/entities/auto-assignment.js +43 -17
  109. package/dist/event-sanitization.d.ts +11 -0
  110. package/dist/event-sanitization.js +113 -0
  111. package/dist/fs-utils.js +13 -1
  112. package/dist/gateway-watchdog.d.ts +5 -0
  113. package/dist/gateway-watchdog.js +50 -0
  114. package/dist/hooks/post-reporting-event.mjs +1 -5
  115. package/dist/http/helpers/activity-headline.js +13 -132
  116. package/dist/http/helpers/auto-continue-engine.d.ts +198 -10
  117. package/dist/http/helpers/auto-continue-engine.js +2531 -186
  118. package/dist/http/helpers/autopilot-operations.d.ts +19 -0
  119. package/dist/http/helpers/autopilot-operations.js +182 -31
  120. package/dist/http/helpers/autopilot-runtime.d.ts +1 -0
  121. package/dist/http/helpers/autopilot-runtime.js +308 -20
  122. package/dist/http/helpers/autopilot-slice-utils.d.ts +18 -0
  123. package/dist/http/helpers/autopilot-slice-utils.js +516 -93
  124. package/dist/http/helpers/decision-mapper.d.ts +40 -0
  125. package/dist/http/helpers/decision-mapper.js +223 -7
  126. package/dist/http/helpers/dispatch-lifecycle.d.ts +19 -2
  127. package/dist/http/helpers/dispatch-lifecycle.js +242 -37
  128. package/dist/http/helpers/kickoff-context.js +74 -0
  129. package/dist/http/helpers/llm-client.d.ts +47 -0
  130. package/dist/http/helpers/llm-client.js +256 -0
  131. package/dist/http/helpers/mission-control.d.ts +102 -3
  132. package/dist/http/helpers/mission-control.js +498 -9
  133. package/dist/http/helpers/sentinel-catalog.d.ts +23 -0
  134. package/dist/http/helpers/sentinel-catalog.js +193 -0
  135. package/dist/http/helpers/session-classification.d.ts +9 -0
  136. package/dist/http/helpers/session-classification.js +564 -0
  137. package/dist/http/helpers/slice-experience-v2.d.ts +137 -0
  138. package/dist/http/helpers/slice-experience-v2.js +677 -0
  139. package/dist/http/helpers/slice-run-projections.d.ts +72 -0
  140. package/dist/http/helpers/slice-run-projections.js +860 -0
  141. package/dist/http/helpers/triage-mapper.d.ts +43 -0
  142. package/dist/http/helpers/triage-mapper.js +549 -0
  143. package/dist/http/helpers/value-utils.js +7 -2
  144. package/dist/http/helpers/workspace-scope.d.ts +15 -0
  145. package/dist/http/helpers/workspace-scope.js +170 -0
  146. package/dist/http/index.js +1354 -97
  147. package/dist/http/routes/agent-suite.d.ts +9 -0
  148. package/dist/http/routes/agent-suite.js +207 -8
  149. package/dist/http/routes/agents-catalog.js +64 -19
  150. package/dist/http/routes/chat.d.ts +19 -0
  151. package/dist/http/routes/chat.js +522 -0
  152. package/dist/http/routes/decision-actions.d.ts +8 -1
  153. package/dist/http/routes/decision-actions.js +42 -5
  154. package/dist/http/routes/dispatch-gateway-envelope.d.ts +25 -0
  155. package/dist/http/routes/dispatch-gateway-envelope.js +26 -0
  156. package/dist/http/routes/entities.d.ts +16 -0
  157. package/dist/http/routes/entities.js +294 -6
  158. package/dist/http/routes/live-legacy.d.ts +5 -0
  159. package/dist/http/routes/live-legacy.js +23 -509
  160. package/dist/http/routes/live-misc.d.ts +12 -0
  161. package/dist/http/routes/live-misc.js +251 -31
  162. package/dist/http/routes/live-snapshot.d.ts +48 -2
  163. package/dist/http/routes/live-snapshot.js +638 -19
  164. package/dist/http/routes/live-terminal.d.ts +11 -0
  165. package/dist/http/routes/live-terminal.js +261 -0
  166. package/dist/http/routes/live-triage.d.ts +61 -0
  167. package/dist/http/routes/live-triage.js +248 -0
  168. package/dist/http/routes/mission-control-actions.d.ts +49 -1
  169. package/dist/http/routes/mission-control-actions.js +1334 -84
  170. package/dist/http/routes/mission-control-read.d.ts +48 -3
  171. package/dist/http/routes/mission-control-read.js +1593 -20
  172. package/dist/http/routes/realtime-orchestrator.d.ts +10 -0
  173. package/dist/http/routes/realtime-orchestrator.js +74 -0
  174. package/dist/http/routes/run-control.d.ts +5 -2
  175. package/dist/http/routes/run-control.js +10 -0
  176. package/dist/http/routes/sentinels-catalog.d.ts +7 -0
  177. package/dist/http/routes/sentinels-catalog.js +24 -0
  178. package/dist/http/routes/summary.js +10 -3
  179. package/dist/http/routes/usage.d.ts +24 -0
  180. package/dist/http/routes/usage.js +362 -0
  181. package/dist/http/routes/work-artifacts.js +28 -9
  182. package/dist/index.js +165 -27
  183. package/dist/local-openclaw.js +29 -6
  184. package/dist/mcp-client-setup.js +3 -3
  185. package/dist/mcp-http-handler.js +33 -59
  186. package/dist/next-up-queue-store.d.ts +16 -1
  187. package/dist/next-up-queue-store.js +89 -7
  188. package/dist/outbox.d.ts +5 -0
  189. package/dist/outbox.js +113 -9
  190. package/dist/paths.js +24 -5
  191. package/dist/reporting/rollups.d.ts +53 -0
  192. package/dist/reporting/rollups.js +148 -0
  193. package/dist/retro/domain-templates.d.ts +45 -0
  194. package/dist/retro/domain-templates.js +297 -0
  195. package/dist/retro/quality-rubric.d.ts +33 -0
  196. package/dist/retro/quality-rubric.js +213 -0
  197. package/dist/runtime-cleanup.d.ts +18 -0
  198. package/dist/runtime-cleanup.js +87 -0
  199. package/dist/services/background.d.ts +11 -0
  200. package/dist/services/background.js +22 -0
  201. package/dist/services/experiment-randomization.d.ts +21 -0
  202. package/dist/services/experiment-randomization.js +63 -0
  203. package/dist/skill-pack-state.d.ts +36 -5
  204. package/dist/skill-pack-state.js +273 -29
  205. package/dist/sync/local-agent-telemetry.d.ts +13 -0
  206. package/dist/sync/local-agent-telemetry.js +128 -0
  207. package/dist/sync/outbox-replay.js +131 -24
  208. package/dist/team-context-store.d.ts +23 -0
  209. package/dist/team-context-store.js +116 -0
  210. package/dist/telemetry/posthog.js +4 -2
  211. package/dist/tools/core-tools.d.ts +10 -14
  212. package/dist/tools/core-tools.js +1289 -24
  213. package/dist/types.d.ts +2 -0
  214. package/dist/types.js +2 -0
  215. package/dist/worker-supervisor.js +23 -0
  216. package/package.json +14 -4
  217. package/dashboard/dist/assets/B3ziCA02.js +0 -8
  218. package/dashboard/dist/assets/B5NEElEI.css +0 -1
  219. package/dashboard/dist/assets/BhapSNAs.js +0 -215
  220. package/dashboard/dist/assets/iFdvE7lx.js +0 -1
  221. package/dashboard/dist/assets/jRJsmpYM.js +0 -1
  222. package/dashboard/dist/assets/sAhvFnpk.js +0 -4
@@ -1,26 +1,271 @@
1
1
  import { randomUUID as randomUuidFn } from "node:crypto";
2
+ import { existsSync } from "node:fs";
3
+ import { readdir, stat, unlink } from "node:fs/promises";
2
4
  import { homedir } from "node:os";
3
- import { join } from "node:path";
4
- import { upsertAgentContext } from "../../agent-context-store.js";
5
+ import { dirname, join } from "node:path";
6
+ import { normalizeActivityActionPhase, normalizeActivityActionType, } from "../../contracts/shared-types.js";
7
+ import { upsertAgentContext, upsertRunContext } from "../../agent-context-store.js";
8
+ import { appendTeamCompletion } from "../../team-context-store.js";
5
9
  import { readOpenClawGatewayPort, readOpenClawSettingsSnapshot, } from "../../openclaw-settings.js";
6
10
  import { resolveRuntimeHookToken, } from "../../runtime-instance-store.js";
7
11
  import { detectMcpHandshakeFailure, shouldKillWorker } from "../../worker-supervisor.js";
8
12
  import { getOrgxPluginConfigDir } from "../../paths.js";
9
- import { buildMissionControlGraph, DEFAULT_TOKEN_BUDGET_ASSUMPTIONS, dedupeStrings, deriveExecutionPolicy, isDispatchableWorkstreamStatus, isDoneStatus, isTodoStatus, readBudgetEnvNumber, summarizeSpawnGuardBlockReason, } from "./mission-control.js";
13
+ import { buildMissionControlGraph, DEFAULT_TOKEN_BUDGET_ASSUMPTIONS, dedupeStrings, detectBehaviorConfigDrift, deriveBehaviorAutomationLevel, deriveBehaviorConfigContext, deriveExecutionPolicy, evaluateScopeCompletion, isDispatchableWorkstreamStatus, isDoneStatus, isTodoStatus, readBudgetEnvNumber, selectSliceTasksByScope, SLICE_SCOPE_TIMEOUT_MULTIPLIER, spawnGuardIsRateLimited, summarizeSpawnGuardBlockReason, } from "./mission-control.js";
10
14
  import { createAutopilotRuntime } from "./autopilot-runtime.js";
11
- import { buildWorkstreamSlicePrompt, createCodexBinResolver, ensureAutopilotSliceSchemaPath, fileUpdatedAtEpochMs, parseSliceResult, readFileTailSafe, readSliceOutputFile, } from "./autopilot-slice-utils.js";
15
+ import { buildScopeDirective, buildSliceOutputInstructions, buildWorkstreamSlicePrompt, createCodexBinResolver, ensureAutopilotSliceSchemaPath, fileUpdatedAtEpochMs, parseSliceResult, readFileTailSafe, readSliceOutputFile, } from "./autopilot-slice-utils.js";
12
16
  import { pickString } from "./value-utils.js";
17
+ function resolveAutopilotDefaultCwd(filename) {
18
+ let cursor = dirname(filename);
19
+ for (let i = 0; i < 12; i += 1) {
20
+ if (existsSync(join(cursor, "package.json")))
21
+ return cursor;
22
+ const parent = dirname(cursor);
23
+ if (!parent || parent === cursor)
24
+ break;
25
+ cursor = parent;
26
+ }
27
+ return homedir();
28
+ }
13
29
  export function createAutoContinueEngine(deps) {
14
30
  const { client, safeErrorMessage, pidAlive, stopProcess, resolveOrgxAgentForDomain, checkSpawnGuardSafe, syncParentRollupsForTask, emitActivitySafe, requestDecisionSafe, registerArtifactSafe, applyAgentStatusUpdatesSafe, upsertRuntimeInstanceFromHook, broadcastRuntimeSse, clearSnapshotResponseCache, resolveByokEnvOverrides, } = deps;
15
31
  const randomUUID = deps.randomUUID ?? randomUuidFn;
32
+ const fetchKickoffContextSafeFn = deps.fetchKickoffContextSafe ?? null;
33
+ const renderKickoffMessageFn = deps.renderKickoffMessage ?? null;
34
+ const decisionAutoResolveGuardedEnabled = String(process.env.DECISION_AUTO_RESOLVE_GUARDED_ENABLED ?? "true")
35
+ .trim()
36
+ .toLowerCase() !== "false";
37
+ /** Spread into any metadata object to flag mock-worker activity. */
38
+ function mockMeta(slice) {
39
+ return slice.isMockWorker ? { mock: true } : {};
40
+ }
41
+ const requestDecisionQueued = async (input) => {
42
+ const inferredRunId = (typeof input.sourceRunId === "string" && input.sourceRunId.trim().length > 0
43
+ ? input.sourceRunId.trim()
44
+ : null) ??
45
+ (typeof input.correlationId === "string" && input.correlationId.trim().length > 0
46
+ ? input.correlationId.trim()
47
+ : null);
48
+ const inferredSessionId = (typeof input.sourceSessionId === "string" && input.sourceSessionId.trim().length > 0
49
+ ? input.sourceSessionId.trim()
50
+ : null) ?? inferredRunId;
51
+ const inferredStreamId = (typeof input.sourceStreamId === "string" && input.sourceStreamId.trim().length > 0
52
+ ? input.sourceStreamId.trim()
53
+ : null) ??
54
+ (typeof input.workstreamId === "string" && input.workstreamId.trim().length > 0
55
+ ? input.workstreamId.trim()
56
+ : null);
57
+ const sourceRefBase = input.sourceRef && typeof input.sourceRef === "object" && !Array.isArray(input.sourceRef)
58
+ ? input.sourceRef
59
+ : {};
60
+ const normalizedInput = {
61
+ ...input,
62
+ sourceRunId: inferredRunId,
63
+ sourceSessionId: inferredSessionId,
64
+ sourceStreamId: inferredStreamId,
65
+ sourceRef: {
66
+ ...sourceRefBase,
67
+ run_id: sourceRefBase.run_id ?? inferredRunId,
68
+ session_id: sourceRefBase.session_id ?? inferredSessionId,
69
+ stream_id: sourceRefBase.stream_id ?? inferredStreamId,
70
+ workstream_id: sourceRefBase.workstream_id ?? input.workstreamId ?? null,
71
+ },
72
+ metadata: {
73
+ ...(input.metadata ?? {}),
74
+ source_system: input.sourceSystem ?? null,
75
+ conflict_source: input.conflictSource ?? null,
76
+ },
77
+ };
78
+ const result = await requestDecisionSafe(normalizedInput);
79
+ if (typeof result === "boolean") {
80
+ return { queued: result, decisionIds: [] };
81
+ }
82
+ if (result && typeof result === "object" && "queued" in result) {
83
+ const record = result;
84
+ const decisionIds = Array.isArray(record.decisionIds)
85
+ ? record.decisionIds
86
+ .filter((entry) => typeof entry === "string")
87
+ .map((entry) => entry.trim())
88
+ .filter(Boolean)
89
+ : [];
90
+ return {
91
+ queued: Boolean(record.queued),
92
+ decisionIds,
93
+ };
94
+ }
95
+ return { queued: false, decisionIds: [] };
96
+ };
16
97
  const __filename = deps.filename;
17
98
  const autoContinueRuns = new Map();
18
99
  const localInitiativeStatusOverrides = new Map();
100
+ const localTaskStatusOverrides = new Map();
101
+ const localMilestoneStatusOverrides = new Map();
19
102
  let autoContinueTickInFlight = null;
20
103
  const AUTO_CONTINUE_TICK_MS = readBudgetEnvNumber("ORGX_AUTO_CONTINUE_TICK_MS", 2_500, {
21
104
  min: 250,
22
105
  max: 60_000,
23
106
  });
107
+ const AUTO_CONTINUE_PARALLEL_MIN = 1;
108
+ const AUTO_CONTINUE_PARALLEL_MAX = 5;
109
+ const AUTO_CONTINUE_MAX_PARALLEL_DEFAULT = Math.max(AUTO_CONTINUE_PARALLEL_MIN, Math.min(AUTO_CONTINUE_PARALLEL_MAX, Math.round(readBudgetEnvNumber("ORGX_AUTO_CONTINUE_MAX_PARALLEL_DEFAULT", 5, { min: AUTO_CONTINUE_PARALLEL_MIN, max: AUTO_CONTINUE_PARALLEL_MAX }))));
110
+ const normalizeParallelMode = (_value) => "iwmt";
111
+ const normalizeMaxParallelSlices = (value, fallback) => {
112
+ const normalizedFallback = Math.max(AUTO_CONTINUE_PARALLEL_MIN, Math.min(AUTO_CONTINUE_PARALLEL_MAX, Math.round(fallback || AUTO_CONTINUE_PARALLEL_MIN)));
113
+ if (typeof value === "number" && Number.isFinite(value)) {
114
+ const parsed = Math.round(value);
115
+ return Math.max(AUTO_CONTINUE_PARALLEL_MIN, Math.min(AUTO_CONTINUE_PARALLEL_MAX, parsed));
116
+ }
117
+ if (typeof value === "string" && value.trim().length > 0) {
118
+ const parsed = Number(value);
119
+ if (Number.isFinite(parsed)) {
120
+ const rounded = Math.round(parsed);
121
+ return Math.max(AUTO_CONTINUE_PARALLEL_MIN, Math.min(AUTO_CONTINUE_PARALLEL_MAX, rounded));
122
+ }
123
+ }
124
+ return normalizedFallback;
125
+ };
126
+ const buildSliceEnrichment = (input) => {
127
+ const eventName = typeof input.event === "string" && input.event.trim().length > 0
128
+ ? input.event.trim().toLowerCase()
129
+ : null;
130
+ const inferredActionType = (() => {
131
+ if (!eventName)
132
+ return "execute_task";
133
+ if (eventName === "orchestrator_dispatch")
134
+ return "orchestrator_dispatch";
135
+ if (eventName.includes("slice_dispatched"))
136
+ return "dispatch_slice";
137
+ if (eventName.includes("slice_started") || eventName === "session_start") {
138
+ return "run_started";
139
+ }
140
+ if (eventName.includes("slice_heartbeat") || eventName === "heartbeat") {
141
+ return "run_heartbeat";
142
+ }
143
+ if (eventName.includes("slice_handoff"))
144
+ return "slice_handoff";
145
+ if (eventName.includes("spawn_guard_rate_limited"))
146
+ return "spawn_guard_rate_limited";
147
+ if (eventName.includes("spawn_guard_blocked"))
148
+ return "spawn_guard_blocked";
149
+ if (eventName.includes("status_updates_buffered"))
150
+ return "status_updates_buffered";
151
+ if (eventName.includes("status_updates"))
152
+ return "status_updates_applied";
153
+ if (eventName.includes("artifact_registered"))
154
+ return "artifact_registered";
155
+ if (eventName.includes("decision_requested"))
156
+ return "decision_requested";
157
+ if (eventName.includes("decision_resolved"))
158
+ return "decision_resolved";
159
+ if (eventName === "auto_continue_started")
160
+ return "auto_continue_started";
161
+ if (eventName === "auto_continue_stopped")
162
+ return "auto_continue_stopped";
163
+ if (eventName.includes("behavior_config") || eventName.includes("behavior_automation")) {
164
+ return "behavior_config_review";
165
+ }
166
+ if (eventName.includes("transition"))
167
+ return "run_state_transition";
168
+ if (eventName.includes("auto_fix"))
169
+ return "auto_fix";
170
+ if (eventName.includes("milestone_completed"))
171
+ return "milestone_completed";
172
+ if (eventName.includes("error") || eventName.includes("failed"))
173
+ return "run_failed";
174
+ if (eventName.includes("result") || eventName.includes("completed"))
175
+ return "run_completed";
176
+ return eventName.replace(/[^a-z0-9]+/g, "_");
177
+ })();
178
+ const actionType = normalizeActivityActionType(input.actionType ?? inferredActionType) ?? inferredActionType;
179
+ const inferredActionPhase = (() => {
180
+ if (!eventName)
181
+ return "execution";
182
+ if (eventName === "orchestrator_dispatch" || eventName.includes("slice_dispatched")) {
183
+ return "dispatch";
184
+ }
185
+ if (eventName.includes("handoff"))
186
+ return "handoff";
187
+ if (eventName.includes("heartbeat"))
188
+ return "execution";
189
+ if (eventName.includes("decision_"))
190
+ return "review";
191
+ if (eventName.includes("blocked") ||
192
+ eventName.includes("rate_limited") ||
193
+ eventName.includes("stall") ||
194
+ eventName.includes("timeout")) {
195
+ return "blocked";
196
+ }
197
+ if (eventName.includes("error") || eventName.includes("failed"))
198
+ return "error";
199
+ if (eventName.includes("result") || eventName.includes("completed") || eventName === "auto_continue_stopped") {
200
+ return "completed";
201
+ }
202
+ if (eventName === "auto_continue_started")
203
+ return "intent";
204
+ return "execution";
205
+ })();
206
+ const actionPhase = normalizeActivityActionPhase(input.actionPhase ?? inferredActionPhase) ??
207
+ inferredActionPhase;
208
+ const workstreamId = (input.workstreamId ?? input.slice?.workstreamId ?? "").trim() || null;
209
+ const taskId = (input.taskId ?? input.slice?.taskIds?.[0] ?? "").trim() || null;
210
+ const requiredSkills = Array.isArray(input.requiredSkills)
211
+ ? input.requiredSkills
212
+ : input.slice?.requiredSkills ?? null;
213
+ return {
214
+ event: input.event ?? null,
215
+ action_type: actionType,
216
+ action_phase: actionPhase,
217
+ initiative_id: input.run.initiativeId,
218
+ requested_by_agent_id: input.run.agentId,
219
+ requested_by_agent_name: input.run.agentName,
220
+ requester_agent_id: input.run.agentId,
221
+ requester_agent_name: input.run.agentName,
222
+ agent_id: input.slice?.agentId ?? null,
223
+ agent_name: input.slice?.agentName ?? null,
224
+ executor_agent_id: input.slice?.agentId ?? null,
225
+ executor_agent_name: input.slice?.agentName ?? null,
226
+ source_run_id: input.slice?.runId ?? null,
227
+ source_session_id: input.slice?.runId ?? null,
228
+ source_stream_id: workstreamId,
229
+ run_id: input.slice?.runId ?? null,
230
+ slice_run_id: input.slice?.runId ?? null,
231
+ correlation_id: input.slice?.runId ?? null,
232
+ workstream_id: workstreamId,
233
+ workstream_title: input.workstreamTitle ?? input.slice?.workstreamTitle ?? null,
234
+ task_id: taskId,
235
+ task_title: input.taskTitle ?? null,
236
+ milestone_ids: input.slice?.milestoneIds ?? null,
237
+ task_ids: input.slice?.taskIds ?? null,
238
+ domain: input.domain ?? input.slice?.domain ?? null,
239
+ required_skills: requiredSkills,
240
+ skill_pack: requiredSkills,
241
+ model_tier: input.modelTier ?? null,
242
+ scope: input.slice?.scope ?? input.run.scope,
243
+ actors: {
244
+ requester: {
245
+ agent_id: input.run.agentId ?? null,
246
+ agent_name: input.run.agentName ?? null,
247
+ },
248
+ dispatcher: {
249
+ agent_id: input.run.agentId ?? null,
250
+ agent_name: input.run.agentName ?? null,
251
+ },
252
+ executor: {
253
+ agent_id: input.slice?.agentId ?? null,
254
+ agent_name: input.slice?.agentName ?? null,
255
+ },
256
+ },
257
+ scope_context: {
258
+ initiative_id: input.run.initiativeId,
259
+ workstream_id: workstreamId,
260
+ task_id: taskId,
261
+ task_ids: input.slice?.taskIds ?? null,
262
+ milestone_ids: input.slice?.milestoneIds ?? null,
263
+ },
264
+ next_actions: input.nextActions ?? null,
265
+ user_summary: input.userSummary ?? null,
266
+ ...(input.extra ?? {}),
267
+ };
268
+ };
24
269
  const autoContinueSliceRuns = new Map();
25
270
  // Keep child handles alive so stdout/stderr capture remains reliable even when the process is detached.
26
271
  const autoContinueSliceChildren = new Map();
@@ -32,7 +277,6 @@ export function createAutoContinueEngine(deps) {
32
277
  autoContinueSliceChildren.delete(id);
33
278
  autoContinueSliceLastHeartbeatMs.delete(id);
34
279
  };
35
- const AUTO_CONTINUE_SLICE_MAX_TASKS = 6;
36
280
  const AUTO_CONTINUE_SLICE_TIMEOUT_MS = readBudgetEnvNumber("ORGX_AUTOPILOT_SLICE_TIMEOUT_MS", 55 * 60_000,
37
281
  // Keep test runs fast; real-world defaults are still ~1h unless overridden.
38
282
  { min: 250, max: 6 * 60 * 60_000 });
@@ -42,6 +286,302 @@ export function createAutoContinueEngine(deps) {
42
286
  const AUTO_CONTINUE_SLICE_HEARTBEAT_MS = 12_000;
43
287
  const AUTO_CONTINUE_SLICE_SCHEMA_FILENAME = "autopilot-slice-schema.json";
44
288
  const AUTO_CONTINUE_SLICE_LOG_DIRNAME = "autopilot-logs";
289
+ // Prune old autopilot logs on engine init (7-day TTL, 50 MB cap).
290
+ const AUTOPILOT_LOG_TTL_MS = 7 * 24 * 60 * 60_000;
291
+ const AUTOPILOT_LOG_MAX_BYTES = 50 * 1024 * 1024;
292
+ (async () => {
293
+ try {
294
+ const logsDir = join(getOrgxPluginConfigDir(), AUTO_CONTINUE_SLICE_LOG_DIRNAME);
295
+ if (!existsSync(logsDir))
296
+ return;
297
+ const entries = await readdir(logsDir);
298
+ const now = Date.now();
299
+ const fileStats = [];
300
+ for (const name of entries) {
301
+ if (!name.endsWith(".log") && !name.endsWith(".output.json"))
302
+ continue;
303
+ const filePath = join(logsDir, name);
304
+ try {
305
+ const s = await stat(filePath);
306
+ if (s.mtimeMs < now - AUTOPILOT_LOG_TTL_MS) {
307
+ await unlink(filePath);
308
+ }
309
+ else {
310
+ fileStats.push({ name, path: filePath, mtimeMs: s.mtimeMs, size: s.size });
311
+ }
312
+ }
313
+ catch { /* skip */ }
314
+ }
315
+ // Enforce total size cap by deleting oldest first.
316
+ fileStats.sort((a, b) => a.mtimeMs - b.mtimeMs);
317
+ let totalSize = fileStats.reduce((sum, f) => sum + f.size, 0);
318
+ for (const f of fileStats) {
319
+ if (totalSize <= AUTOPILOT_LOG_MAX_BYTES)
320
+ break;
321
+ try {
322
+ await unlink(f.path);
323
+ }
324
+ catch { /* skip */ }
325
+ totalSize -= f.size;
326
+ }
327
+ }
328
+ catch { /* best effort */ }
329
+ })();
330
+ const AUTO_FIX_DEFAULT_GRACE_MS = readBudgetEnvNumber("ORGX_AUTOPILOT_AUTOFIX_GRACE_MS", 10_000, { min: 1_000, max: 120_000 });
331
+ const AUTO_CONTINUE_SPAWN_GUARD_RETRY_MS = readBudgetEnvNumber("ORGX_AUTO_CONTINUE_SPAWN_GUARD_RETRY_MS", 15_000, { min: 1_000, max: 15 * 60_000 });
332
+ const autoFixByScope = new Map();
333
+ const autoContinueSpawnGuardRetryByTask = new Map();
334
+ const getSpawnGuardRetryAtMs = (initiativeId, taskId) => {
335
+ const taskKey = taskId.trim();
336
+ if (!taskKey)
337
+ return 0;
338
+ const entry = autoContinueSpawnGuardRetryByTask.get(taskKey);
339
+ if (!entry)
340
+ return 0;
341
+ if (entry.initiativeId !== initiativeId || entry.retryAtMs <= Date.now()) {
342
+ autoContinueSpawnGuardRetryByTask.delete(taskKey);
343
+ return 0;
344
+ }
345
+ return entry.retryAtMs;
346
+ };
347
+ const clearSpawnGuardRetryStateForInitiative = (initiativeId) => {
348
+ for (const [taskId, entry] of autoContinueSpawnGuardRetryByTask.entries()) {
349
+ if (entry.initiativeId !== initiativeId)
350
+ continue;
351
+ autoContinueSpawnGuardRetryByTask.delete(taskId);
352
+ }
353
+ };
354
+ const normalizeStatusValue = (value) => {
355
+ if (typeof value !== "string")
356
+ return "";
357
+ return value.trim().toLowerCase().replace(/[\s-]+/g, "_");
358
+ };
359
+ const listActiveSliceRunIds = (run) => {
360
+ ensureRunInternals(run);
361
+ const ids = new Set();
362
+ for (const id of run.activeSliceRunIds ?? []) {
363
+ const normalized = (id ?? "").trim();
364
+ if (normalized)
365
+ ids.add(normalized);
366
+ }
367
+ for (const lane of Object.values(run.laneByWorkstreamId ?? {})) {
368
+ const activeRunId = (lane.activeRunId ?? "").trim();
369
+ if (activeRunId)
370
+ ids.add(activeRunId);
371
+ }
372
+ return Array.from(ids);
373
+ };
374
+ const upsertLane = (run, workstreamId, patch) => {
375
+ const normalizedWorkstreamId = workstreamId.trim();
376
+ if (!normalizedWorkstreamId) {
377
+ throw new Error("workstreamId is required");
378
+ }
379
+ const existing = run.laneByWorkstreamId[normalizedWorkstreamId] ?? {
380
+ workstreamId: normalizedWorkstreamId,
381
+ state: "idle",
382
+ activeRunId: null,
383
+ activeTaskIds: [],
384
+ blockedReason: null,
385
+ waitingOnWorkstreamIds: [],
386
+ retryAt: null,
387
+ updatedAt: new Date().toISOString(),
388
+ };
389
+ const next = {
390
+ ...existing,
391
+ ...patch,
392
+ workstreamId: normalizedWorkstreamId,
393
+ updatedAt: patch.updatedAt ?? new Date().toISOString(),
394
+ activeTaskIds: Array.isArray(patch.activeTaskIds)
395
+ ? dedupeStrings(patch.activeTaskIds.map((id) => (id ?? "").trim()).filter(Boolean))
396
+ : existing.activeTaskIds,
397
+ waitingOnWorkstreamIds: Array.isArray(patch.waitingOnWorkstreamIds)
398
+ ? dedupeStrings(patch.waitingOnWorkstreamIds.map((id) => (id ?? "").trim()).filter(Boolean))
399
+ : existing.waitingOnWorkstreamIds,
400
+ };
401
+ run.laneByWorkstreamId[normalizedWorkstreamId] = next;
402
+ return next;
403
+ };
404
+ const setLaneState = (run, input) => {
405
+ return upsertLane(run, input.workstreamId, {
406
+ state: input.state,
407
+ activeRunId: input.activeRunId === undefined ? undefined : (input.activeRunId ?? "").trim() || null,
408
+ activeTaskIds: input.activeTaskIds,
409
+ blockedReason: input.blockedReason === undefined
410
+ ? undefined
411
+ : (input.blockedReason ?? "").trim() || null,
412
+ waitingOnWorkstreamIds: input.waitingOnWorkstreamIds,
413
+ retryAt: input.retryAt === undefined ? undefined : input.retryAt,
414
+ });
415
+ };
416
+ const removeActiveSliceFromRun = (run, input) => {
417
+ const sliceRunId = input.sliceRunId.trim();
418
+ if (!sliceRunId)
419
+ return;
420
+ run.activeSliceRunIds = run.activeSliceRunIds.filter((id) => id !== sliceRunId);
421
+ const taskIds = new Set(Array.isArray(input.taskIds)
422
+ ? input.taskIds.map((id) => (id ?? "").trim()).filter(Boolean)
423
+ : []);
424
+ if (taskIds.size > 0) {
425
+ run.activeTaskIds = run.activeTaskIds.filter((id) => !taskIds.has(id));
426
+ }
427
+ const normalizedWorkstreamId = (input.workstreamId ?? "").trim();
428
+ if (normalizedWorkstreamId) {
429
+ const lane = run.laneByWorkstreamId[normalizedWorkstreamId];
430
+ if (lane && lane.activeRunId === sliceRunId) {
431
+ setLaneState(run, {
432
+ workstreamId: normalizedWorkstreamId,
433
+ state: lane.state === "blocked" ? "blocked" : "idle",
434
+ activeRunId: null,
435
+ activeTaskIds: [],
436
+ retryAt: lane.retryAt ?? null,
437
+ waitingOnWorkstreamIds: lane.waitingOnWorkstreamIds ?? [],
438
+ blockedReason: lane.state === "blocked" ? lane.blockedReason : null,
439
+ });
440
+ }
441
+ }
442
+ };
443
+ const syncLegacyRunPointers = (run) => {
444
+ ensureRunInternals(run);
445
+ const activeIds = listActiveSliceRunIds(run);
446
+ run.activeSliceRunIds = activeIds;
447
+ run.activeTaskIds = dedupeStrings((run.activeTaskIds ?? []).map((id) => (id ?? "").trim()).filter(Boolean));
448
+ run.activeRunId = activeIds[0] ?? null;
449
+ run.activeTaskId = run.activeTaskIds[0] ?? null;
450
+ if (!run.activeRunId) {
451
+ run.activeTaskTokenEstimate = null;
452
+ }
453
+ };
454
+ const ensureRunInternals = (run) => {
455
+ if (!Array.isArray(run.activeSliceRunIds))
456
+ run.activeSliceRunIds = [];
457
+ if (!Array.isArray(run.activeTaskIds))
458
+ run.activeTaskIds = [];
459
+ if (!run.laneByWorkstreamId || typeof run.laneByWorkstreamId !== "object") {
460
+ run.laneByWorkstreamId = {};
461
+ }
462
+ if (!Array.isArray(run.blockedWorkstreamIds))
463
+ run.blockedWorkstreamIds = [];
464
+ run.maxParallelSlices = normalizeMaxParallelSlices(run.maxParallelSlices, AUTO_CONTINUE_MAX_PARALLEL_DEFAULT);
465
+ run.parallelMode = normalizeParallelMode(run.parallelMode);
466
+ run.tokenBudget = normalizeTokenBudget(run.tokenBudget, defaultAutoContinueTokenBudget());
467
+ };
468
+ const recordLocalStatusOverrides = (input) => {
469
+ const initiativeId = input.initiativeId.trim();
470
+ if (!initiativeId)
471
+ return;
472
+ if (input.taskUpdates.length > 0) {
473
+ const scoped = localTaskStatusOverrides.get(initiativeId) ?? new Map();
474
+ for (const update of input.taskUpdates) {
475
+ const taskId = update.taskId.trim();
476
+ const status = normalizeStatusValue(update.status);
477
+ if (!taskId || !status)
478
+ continue;
479
+ scoped.set(taskId, {
480
+ status,
481
+ updatedAt: input.updatedAt,
482
+ reason: update.reason,
483
+ });
484
+ }
485
+ if (scoped.size > 0) {
486
+ localTaskStatusOverrides.set(initiativeId, scoped);
487
+ }
488
+ }
489
+ if (input.milestoneUpdates.length > 0) {
490
+ const scoped = localMilestoneStatusOverrides.get(initiativeId) ?? new Map();
491
+ for (const update of input.milestoneUpdates) {
492
+ const milestoneId = update.milestoneId.trim();
493
+ const status = normalizeStatusValue(update.status);
494
+ if (!milestoneId || !status)
495
+ continue;
496
+ scoped.set(milestoneId, {
497
+ status,
498
+ updatedAt: input.updatedAt,
499
+ reason: update.reason,
500
+ });
501
+ }
502
+ if (scoped.size > 0) {
503
+ localMilestoneStatusOverrides.set(initiativeId, scoped);
504
+ }
505
+ }
506
+ };
507
+ const applyLocalStatusOverridesToGraph = (initiativeId, nodeById) => {
508
+ const scopedTaskOverrides = localTaskStatusOverrides.get(initiativeId) ?? null;
509
+ if (scopedTaskOverrides) {
510
+ for (const [taskId, override] of scopedTaskOverrides.entries()) {
511
+ const node = nodeById.get(taskId);
512
+ if (!node || node.type !== "task")
513
+ continue;
514
+ const remoteStatus = normalizeStatusValue(node.status);
515
+ node.status = override.status;
516
+ if (remoteStatus === override.status) {
517
+ scopedTaskOverrides.delete(taskId);
518
+ }
519
+ }
520
+ if (scopedTaskOverrides.size === 0) {
521
+ localTaskStatusOverrides.delete(initiativeId);
522
+ }
523
+ }
524
+ const scopedMilestoneOverrides = localMilestoneStatusOverrides.get(initiativeId) ?? null;
525
+ if (scopedMilestoneOverrides) {
526
+ for (const [milestoneId, override] of scopedMilestoneOverrides.entries()) {
527
+ const node = nodeById.get(milestoneId);
528
+ if (!node || node.type !== "milestone")
529
+ continue;
530
+ const remoteStatus = normalizeStatusValue(node.status);
531
+ node.status = override.status;
532
+ if (remoteStatus === override.status) {
533
+ scopedMilestoneOverrides.delete(milestoneId);
534
+ }
535
+ }
536
+ if (scopedMilestoneOverrides.size === 0) {
537
+ localMilestoneStatusOverrides.delete(initiativeId);
538
+ }
539
+ }
540
+ };
541
+ const isPendingDecisionStatus = (value) => {
542
+ const normalized = normalizeStatusValue(value);
543
+ if (!normalized)
544
+ return false;
545
+ return (normalized === "pending" ||
546
+ normalized === "open" ||
547
+ normalized === "requested" ||
548
+ normalized === "awaiting_review" ||
549
+ normalized === "awaiting_approval" ||
550
+ normalized === "queued");
551
+ };
552
+ const decisionMatchesWorkstream = (record, workstreamId, runId) => {
553
+ const directWorkstream = pickString(record, ["workstream_id", "workstreamId"])?.trim() ?? "";
554
+ if (directWorkstream && directWorkstream === workstreamId)
555
+ return true;
556
+ const correlationId = pickString(record, ["correlation_id", "correlationId"])?.trim() ?? "";
557
+ if (runId && correlationId && correlationId === runId)
558
+ return true;
559
+ const metadataRaw = record.metadata;
560
+ const metadata = metadataRaw && typeof metadataRaw === "object" && !Array.isArray(metadataRaw)
561
+ ? metadataRaw
562
+ : null;
563
+ if (!metadata)
564
+ return false;
565
+ const nestedWorkstream = pickString(metadata, ["workstream_id", "workstreamId"])?.trim() ?? "";
566
+ if (nestedWorkstream && nestedWorkstream === workstreamId)
567
+ return true;
568
+ const nestedCorrelation = pickString(metadata, ["correlation_id", "correlationId"])?.trim() ?? "";
569
+ if (runId && nestedCorrelation && nestedCorrelation === runId)
570
+ return true;
571
+ return false;
572
+ };
573
+ const decisionIsBlocking = (record) => {
574
+ const direct = record.blocking;
575
+ if (typeof direct === "boolean")
576
+ return direct;
577
+ const metadataRaw = record.metadata;
578
+ if (metadataRaw && typeof metadataRaw === "object" && !Array.isArray(metadataRaw)) {
579
+ const nested = metadataRaw.blocking;
580
+ if (typeof nested === "boolean")
581
+ return nested;
582
+ }
583
+ return true;
584
+ };
45
585
  const setLocalInitiativeStatusOverride = (initiativeId, status) => {
46
586
  const normalizedId = initiativeId.trim();
47
587
  if (!normalizedId)
@@ -104,27 +644,52 @@ export function createAutoContinueEngine(deps) {
104
644
  : node),
105
645
  };
106
646
  };
107
- function normalizeTokenBudget(value, fallback) {
647
+ function parseTokenBudget(value) {
108
648
  if (typeof value === "number" && Number.isFinite(value)) {
649
+ if (value <= 0)
650
+ return null;
109
651
  return Math.max(1_000, Math.round(value));
110
652
  }
111
- if (typeof value === "string" && value.trim().length > 0) {
112
- const parsed = Number(value);
653
+ if (typeof value === "string") {
654
+ const trimmed = value.trim();
655
+ if (!trimmed)
656
+ return null;
657
+ const normalized = trimmed.toLowerCase();
658
+ if (normalized === "0" ||
659
+ normalized === "off" ||
660
+ normalized === "none" ||
661
+ normalized === "false" ||
662
+ normalized === "unlimited" ||
663
+ normalized === "null") {
664
+ return null;
665
+ }
666
+ const parsed = Number(trimmed);
113
667
  if (Number.isFinite(parsed)) {
668
+ if (parsed <= 0)
669
+ return null;
114
670
  return Math.max(1_000, Math.round(parsed));
115
671
  }
116
672
  }
117
- return Math.max(1_000, Math.round(fallback));
673
+ return null;
674
+ }
675
+ function normalizeTokenBudget(value, fallback) {
676
+ const parsed = parseTokenBudget(value);
677
+ if (parsed !== null)
678
+ return parsed;
679
+ return fallback;
118
680
  }
119
681
  function defaultAutoContinueTokenBudget() {
120
- const hours = readBudgetEnvNumber("ORGX_AUTO_CONTINUE_BUDGET_HOURS", 4, {
121
- min: 0.05,
122
- max: 24,
123
- });
124
- const fallback = DEFAULT_TOKEN_BUDGET_ASSUMPTIONS.tokensPerHour *
125
- hours *
126
- DEFAULT_TOKEN_BUDGET_ASSUMPTIONS.contingencyMultiplier;
127
- return normalizeTokenBudget(process.env.ORGX_AUTO_CONTINUE_TOKEN_BUDGET, fallback);
682
+ const explicitBudget = parseTokenBudget(process.env.ORGX_AUTO_CONTINUE_TOKEN_BUDGET);
683
+ if (explicitBudget !== null)
684
+ return explicitBudget;
685
+ // Token budget guardrails are now explicit-only: either pass a budget when starting
686
+ // auto-continue or set ORGX_AUTO_CONTINUE_TOKEN_BUDGET directly.
687
+ // Legacy fallback toggles (for example ORGX_AUTO_CONTINUE_ENFORCE_TOKEN_BUDGET)
688
+ // are intentionally ignored to prevent hidden auto-stop behavior.
689
+ return null;
690
+ }
691
+ function defaultAutoContinueMaxParallelSlices() {
692
+ return AUTO_CONTINUE_MAX_PARALLEL_DEFAULT;
128
693
  }
129
694
  function estimateTokensForDurationHours(durationHours) {
130
695
  if (!Number.isFinite(durationHours) || durationHours <= 0)
@@ -136,7 +701,8 @@ export function createAutoContinueEngine(deps) {
136
701
  }
137
702
  // Helpers used by previous task-level auto-continue implementation were removed in v2.
138
703
  // readOpenClawSessionSummary was used by the previous task-level auto-continue implementation.
139
- // Autopilot v2 dispatches workstream slices via codex and does not rely on OpenClaw session JSONL.
704
+ // Autopilot v2 dispatches workstream slices via runtime workers (codex/claude-code)
705
+ // and does not rely on OpenClaw session JSONL.
140
706
  async function fetchInitiativeEntity(initiativeId) {
141
707
  try {
142
708
  const list = await client.listEntities("initiative", { limit: 200 });
@@ -159,7 +725,18 @@ export function createAutoContinueEngine(deps) {
159
725
  await client.updateEntity("initiative", initiativeId, { metadata: nextMeta });
160
726
  }
161
727
  async function updateInitiativeAutoContinueState(input) {
728
+ syncLegacyRunPointers(input.run);
162
729
  const now = new Date().toISOString();
730
+ const laneStates = Object.values(input.run.laneByWorkstreamId ?? {}).map((lane) => ({
731
+ workstream_id: lane.workstreamId,
732
+ state: lane.state,
733
+ active_run_id: lane.activeRunId,
734
+ active_task_ids: lane.activeTaskIds,
735
+ blocked_reason: lane.blockedReason,
736
+ waiting_on_workstream_ids: lane.waitingOnWorkstreamIds,
737
+ retry_at: lane.retryAt,
738
+ updated_at: lane.updatedAt,
739
+ }));
163
740
  const patch = {
164
741
  auto_continue_enabled: input.run.status === "running" || input.run.status === "stopping",
165
742
  auto_continue_status: input.run.status,
@@ -171,29 +748,52 @@ export function createAutoContinueEngine(deps) {
171
748
  auto_continue_tokens_used: input.run.tokensUsed,
172
749
  auto_continue_active_task_id: input.run.activeTaskId,
173
750
  auto_continue_active_run_id: input.run.activeRunId,
751
+ auto_continue_active_task_ids: input.run.activeTaskIds,
752
+ auto_continue_active_run_ids: input.run.activeSliceRunIds,
174
753
  auto_continue_active_task_token_estimate: input.run.activeTaskTokenEstimate,
175
754
  auto_continue_last_task_id: input.run.lastTaskId,
176
755
  auto_continue_last_run_id: input.run.lastRunId,
177
756
  auto_continue_include_verification: input.run.includeVerification,
178
757
  auto_continue_workstream_filter: input.run.allowedWorkstreamIds,
758
+ auto_continue_parallel_mode: input.run.parallelMode,
759
+ auto_continue_max_parallel: input.run.maxParallelSlices,
760
+ auto_continue_lane_states: laneStates,
761
+ auto_continue_blocked_workstream_ids: input.run.blockedWorkstreamIds,
762
+ auto_continue_ignore_spawn_guard_rate_limit: input.run.ignoreSpawnGuardRateLimit,
179
763
  ...(input.run.lastError ? { auto_continue_last_error: input.run.lastError } : {}),
180
764
  };
181
765
  await updateInitiativeMetadata(input.initiativeId, patch);
182
766
  }
183
767
  async function stopAutoContinueRun(input) {
184
768
  const now = new Date().toISOString();
185
- const activeRunId = input.run.activeRunId;
769
+ ensureRunInternals(input.run);
770
+ const activeRunIds = listActiveSliceRunIds(input.run);
186
771
  input.run.status = "stopped";
187
772
  input.run.stopReason = input.reason;
188
773
  input.run.stoppedAt = now;
189
774
  input.run.updatedAt = now;
190
775
  input.run.stopRequested = false;
776
+ input.run.activeSliceRunIds = [];
777
+ input.run.activeTaskIds = [];
191
778
  input.run.activeRunId = null;
192
779
  input.run.activeTaskId = null;
193
780
  input.run.activeTaskTokenEstimate = null;
781
+ for (const lane of Object.values(input.run.laneByWorkstreamId ?? {})) {
782
+ if (lane.activeRunId || lane.activeTaskIds.length > 0) {
783
+ setLaneState(input.run, {
784
+ workstreamId: lane.workstreamId,
785
+ state: lane.state === "blocked" ? "blocked" : "idle",
786
+ activeRunId: null,
787
+ activeTaskIds: [],
788
+ });
789
+ }
790
+ }
194
791
  if (input.error)
195
792
  input.run.lastError = input.error;
196
- clearAutoContinueSliceTransientState(activeRunId);
793
+ clearSpawnGuardRetryStateForInitiative(input.run.initiativeId);
794
+ for (const runId of activeRunIds) {
795
+ clearAutoContinueSliceTransientState(runId);
796
+ }
197
797
  // Only pause the initiative on non-terminal stops (error, blocked, user-requested).
198
798
  // Completed / budget-exhausted runs should not override the initiative status.
199
799
  if (input.reason !== "completed" && input.reason !== "budget_exhausted") {
@@ -215,17 +815,29 @@ export function createAutoContinueEngine(deps) {
215
815
  catch {
216
816
  // best effort
217
817
  }
218
- const scopeSuffix = Array.isArray(input.run.allowedWorkstreamIds) && input.run.allowedWorkstreamIds.length === 1
219
- ? ` (${input.run.allowedWorkstreamIds[0]})`
220
- : "";
818
+ const primaryActiveRunId = activeRunIds[0] ?? null;
819
+ const scopedWorkstreamId = Array.isArray(input.run.allowedWorkstreamIds) && input.run.allowedWorkstreamIds.length === 1
820
+ ? input.run.allowedWorkstreamIds[0]
821
+ : null;
822
+ const scopeSuffix = scopedWorkstreamId ? ` [workstream ${scopedWorkstreamId}]` : "";
823
+ const decisionRequired = input.reason === "blocked" && input.decisionRequired === true;
824
+ const decisionIds = Array.isArray(input.decisionIds)
825
+ ? input.decisionIds
826
+ .filter((entry) => typeof entry === "string")
827
+ .map((entry) => entry.trim())
828
+ .filter(Boolean)
829
+ : [];
830
+ const budgetValue = typeof input.run.tokenBudget === "number" ? input.run.tokenBudget : "unbounded";
221
831
  const message = input.reason === "completed"
222
832
  ? `Autopilot stopped: current dispatch scope completed${scopeSuffix}.`
223
833
  : input.reason === "budget_exhausted"
224
- ? `Autopilot stopped: token budget exhausted (${input.run.tokensUsed}/${input.run.tokenBudget}).`
834
+ ? `Autopilot stopped: token budget exhausted (${input.run.tokensUsed}/${budgetValue}).`
225
835
  : input.reason === "stopped"
226
836
  ? `Autopilot stopped by user request${scopeSuffix}.`
227
837
  : input.reason === "blocked"
228
- ? `Autopilot stopped: blocked pending decision${scopeSuffix}.`
838
+ ? decisionRequired
839
+ ? `Autopilot stopped: blocked awaiting decision${scopeSuffix}.`
840
+ : `Autopilot stopped: blocked${scopeSuffix}.`
229
841
  : `Autopilot stopped due to error${scopeSuffix}.`;
230
842
  const phase = input.reason === "completed"
231
843
  ? "completed"
@@ -237,26 +849,83 @@ export function createAutoContinueEngine(deps) {
237
849
  : input.reason === "budget_exhausted" || input.reason === "stopped"
238
850
  ? "warn"
239
851
  : "error";
852
+ const errorLocation = input.reason === "blocked"
853
+ ? "mission-control.auto-continue.engine.blocked"
854
+ : input.reason === "error"
855
+ ? "mission-control.auto-continue.engine.error"
856
+ : null;
857
+ const stopRunContext = {
858
+ initiativeId: input.run.initiativeId,
859
+ agentId: input.run.agentId,
860
+ agentName: input.run.agentName,
861
+ scope: input.run.scope,
862
+ };
240
863
  await emitActivitySafe({
241
864
  initiativeId: input.run.initiativeId,
242
- runId: activeRunId ?? input.run.lastRunId ?? undefined,
243
- correlationId: activeRunId ?? input.run.lastRunId ?? undefined,
865
+ runId: primaryActiveRunId ?? input.run.lastRunId ?? undefined,
866
+ correlationId: primaryActiveRunId ?? input.run.lastRunId ?? undefined,
244
867
  phase,
245
868
  level,
869
+ progressPct: input.reason === "completed" ? 100 : input.reason === "blocked" ? 65 : 0,
870
+ nextStep: input.reason === "completed"
871
+ ? "Select the next queue item or enable autoplay for continuous dispatch."
872
+ : input.reason === "blocked"
873
+ ? "Resolve blocker decisions, then resume or restart autoplay."
874
+ : input.reason === "budget_exhausted"
875
+ ? "Increase token budget or scope down work before restarting autoplay."
876
+ : input.reason === "stopped"
877
+ ? "Restart autoplay when ready."
878
+ : "Inspect error details and relaunch once fixed.",
246
879
  message,
247
880
  metadata: {
248
- event: "auto_continue_stopped",
881
+ ...buildSliceEnrichment({
882
+ run: stopRunContext,
883
+ workstreamId: scopedWorkstreamId,
884
+ event: "auto_continue_stopped",
885
+ }),
249
886
  stop_reason: input.reason,
250
- requested_by_agent_id: input.run.agentId,
251
- requested_by_agent_name: input.run.agentName,
252
- active_run_id: activeRunId,
887
+ active_run_id: primaryActiveRunId,
888
+ active_run_ids: activeRunIds,
253
889
  last_run_id: input.run.lastRunId,
254
890
  token_budget: input.run.tokenBudget,
255
891
  tokens_used: input.run.tokensUsed,
256
892
  allowed_workstream_ids: input.run.allowedWorkstreamIds,
893
+ max_parallel_slices: input.run.maxParallelSlices,
894
+ scope_workstream_id: scopedWorkstreamId,
895
+ decision_required: decisionRequired,
896
+ decision_ids: decisionIds,
897
+ decision_count: decisionIds.length,
257
898
  last_error: input.run.lastError,
899
+ error_location: errorLocation,
258
900
  },
259
901
  });
902
+ // Emit autopilot_transition event for state observers.
903
+ try {
904
+ await emitActivitySafe({
905
+ initiativeId: input.run.initiativeId,
906
+ runId: primaryActiveRunId ?? input.run.lastRunId ?? undefined,
907
+ correlationId: primaryActiveRunId ?? input.run.lastRunId ?? undefined,
908
+ phase,
909
+ level: "info",
910
+ progressPct: input.reason === "completed" ? 100 : input.reason === "blocked" ? 65 : 0,
911
+ message: `Autopilot state: running → ${input.reason === "completed" ? "idle" : input.reason === "stopped" ? "idle" : input.reason}.`,
912
+ metadata: {
913
+ ...buildSliceEnrichment({
914
+ run: stopRunContext,
915
+ workstreamId: scopedWorkstreamId,
916
+ event: "autopilot_transition",
917
+ actionType: "run_state_transition",
918
+ }),
919
+ old_state: "running",
920
+ new_state: input.reason === "completed" || input.reason === "stopped" ? "idle" : input.reason === "blocked" ? "blocked" : input.reason === "error" ? "error" : "idle",
921
+ reason: input.reason,
922
+ workspace_id: input.run.allowedWorkstreamIds?.[0] ?? null,
923
+ },
924
+ });
925
+ }
926
+ catch {
927
+ // best effort
928
+ }
260
929
  }
261
930
  const codexBinResolver = createCodexBinResolver();
262
931
  const resolveCodexBinInfo = () => codexBinResolver.resolveCodexBinInfo();
@@ -274,11 +943,15 @@ export function createAutoContinueEngine(deps) {
274
943
  if (run.status !== "running" && run.status !== "stopping")
275
944
  return;
276
945
  const now = new Date().toISOString();
277
- // 1) If we have an active slice, wait for it to finish and then register outcomes.
278
- if (run.activeRunId) {
279
- const slice = autoContinueSliceRuns.get(run.activeRunId) ?? null;
946
+ syncLegacyRunPointers(run);
947
+ // 1) Reconcile each active slice lane and register outcomes when complete.
948
+ const activeRunIdsForTick = listActiveSliceRunIds(run);
949
+ for (const activeRunIdForTick of activeRunIdsForTick) {
950
+ run.activeRunId = activeRunIdForTick;
951
+ const slice = autoContinueSliceRuns.get(activeRunIdForTick) ?? null;
280
952
  if (!slice) {
281
953
  // Legacy/unknown pointer; clear so we can continue.
954
+ removeActiveSliceFromRun(run, { sliceRunId: activeRunIdForTick });
282
955
  run.activeRunId = null;
283
956
  run.activeTaskId = null;
284
957
  run.updatedAt = now;
@@ -327,12 +1000,18 @@ export function createAutoContinueEngine(deps) {
327
1000
  requested_by_agent_name: run.agentName,
328
1001
  domain: slice.domain,
329
1002
  required_skills: slice.requiredSkills,
1003
+ behavior_config_id: slice.behaviorConfigId,
1004
+ behavior_config_version: slice.behaviorConfigVersion,
1005
+ behavior_config_hash: slice.behaviorConfigHash,
1006
+ policy_source: slice.behaviorPolicySource,
1007
+ behavior_automation_level: slice.behaviorAutomationLevel,
330
1008
  workstream_id: slice.workstreamId,
331
1009
  workstream_title: slice.workstreamTitle ?? null,
332
1010
  task_ids: slice.taskIds,
333
1011
  milestone_ids: slice.milestoneIds,
334
1012
  log_path: slice.logPath,
335
1013
  output_path: slice.outputPath,
1014
+ ...mockMeta(slice),
336
1015
  },
337
1016
  });
338
1017
  }
@@ -344,8 +1023,10 @@ export function createAutoContinueEngine(deps) {
344
1023
  const startedAtEpochMs = Date.parse(slice.startedAt);
345
1024
  const fallbackEpochMs = Number.isFinite(startedAtEpochMs) ? startedAtEpochMs : nowMs;
346
1025
  const outputUpdatedAtEpochMs = fileUpdatedAtEpochMs(slice.outputPath, fallbackEpochMs);
347
- // Treat stdout/output freshness as progress; stderr noise should not prevent stall detection.
348
- const stallUpdatedAtEpochMs = outputUpdatedAtEpochMs;
1026
+ const logUpdatedAtEpochMs = fileUpdatedAtEpochMs(slice.logPath, fallbackEpochMs);
1027
+ // Some codex runs only materialize output.json at process exit. Treat recent log activity
1028
+ // as liveness signal so active slices are not falsely marked as stalled.
1029
+ const stallUpdatedAtEpochMs = Math.max(outputUpdatedAtEpochMs, logUpdatedAtEpochMs);
349
1030
  const logTail = readFileTailSafe(slice.logPath, 64_000);
350
1031
  const mcpHandshake = detectMcpHandshakeFailure(logTail);
351
1032
  if (mcpHandshake) {
@@ -369,21 +1050,28 @@ export function createAutoContinueEngine(deps) {
369
1050
  correlationId: slice.runId,
370
1051
  phase: "blocked",
371
1052
  level: "error",
1053
+ progressPct: 55,
1054
+ nextStep: "Review MCP diagnostics, then choose retry, skip, or pause for investigation.",
372
1055
  message: `Autopilot slice MCP failed: ${slice.workstreamTitle ?? slice.workstreamId}.`,
373
1056
  metadata: {
374
- event: "autopilot_slice_mcp_handshake_failed",
375
- requested_by_agent_id: run.agentId,
376
- requested_by_agent_name: run.agentName,
1057
+ ...buildSliceEnrichment({
1058
+ run,
1059
+ slice,
1060
+ workstreamId: slice.workstreamId,
1061
+ workstreamTitle: slice.workstreamTitle ?? null,
1062
+ domain: slice.domain,
1063
+ requiredSkills: slice.requiredSkills,
1064
+ event: "autopilot_slice_mcp_handshake_failed",
1065
+ }),
1066
+ error_location: "mission-control.auto-continue.engine.slice.mcp-handshake",
377
1067
  mcp_server: mcpHandshake.server,
378
1068
  mcp_line: mcpHandshake.line,
379
- workstream_id: slice.workstreamId,
380
- task_ids: slice.taskIds,
381
- milestone_ids: slice.milestoneIds,
382
1069
  log_path: slice.logPath,
383
1070
  output_path: slice.outputPath,
1071
+ ...mockMeta(slice),
384
1072
  },
385
1073
  });
386
- await requestDecisionSafe({
1074
+ const decisionResult = await requestDecisionQueued({
387
1075
  initiativeId: run.initiativeId,
388
1076
  correlationId: slice.runId,
389
1077
  title: `Autopilot slice MCP failed: ${slice.workstreamTitle ?? slice.workstreamId}`,
@@ -395,19 +1083,63 @@ export function createAutoContinueEngine(deps) {
395
1083
  "Skip this workstream for now",
396
1084
  ],
397
1085
  blocking: true,
1086
+ decisionType: "autopilot_failure",
1087
+ workstreamId: slice.workstreamId,
1088
+ agentId: slice.agentId,
1089
+ sourceSystem: "orgx-autopilot",
1090
+ conflictSource: "mcp_handshake_failure",
1091
+ dedupeKey: [
1092
+ "autopilot",
1093
+ run.initiativeId,
1094
+ slice.workstreamId,
1095
+ "mcp_handshake_failure",
1096
+ mcpHandshake.server ?? "unknown",
1097
+ ].join(":"),
1098
+ recommendedAction: "Retry once. If it fails again, pause autopilot and inspect MCP server configuration.",
1099
+ sourceRunId: slice.runId,
1100
+ sourceRef: {
1101
+ run_id: slice.runId,
1102
+ workstream_id: slice.workstreamId,
1103
+ mcp_server: mcpHandshake.server ?? null,
1104
+ },
1105
+ evidenceRefs: [
1106
+ {
1107
+ evidence_type: "mcp_diagnostic",
1108
+ title: "MCP handshake failure",
1109
+ summary: `MCP handshake failed${mcpHandshake.server ? ` for ${mcpHandshake.server}` : ""}.`,
1110
+ source_pointer: slice.logPath,
1111
+ payload: {
1112
+ mcp_server: mcpHandshake.server ?? null,
1113
+ mcp_line: mcpHandshake.line ?? null,
1114
+ output_path: slice.outputPath,
1115
+ },
1116
+ },
1117
+ ],
1118
+ });
1119
+ setLaneState(run, {
1120
+ workstreamId: slice.workstreamId,
1121
+ state: "blocked",
1122
+ activeRunId: null,
1123
+ activeTaskIds: [],
1124
+ blockedReason: slice.lastError,
1125
+ waitingOnWorkstreamIds: [],
1126
+ retryAt: null,
398
1127
  });
399
1128
  await stopAutoContinueRun({
400
1129
  run,
401
1130
  reason: "blocked",
402
1131
  error: slice.lastError,
1132
+ decisionRequired: decisionResult.queued,
1133
+ decisionIds: decisionResult.decisionIds,
403
1134
  });
404
1135
  return;
405
1136
  }
1137
+ const scopeTimeoutMs = AUTO_CONTINUE_SLICE_TIMEOUT_MS * SLICE_SCOPE_TIMEOUT_MULTIPLIER[slice.scope ?? "task"];
406
1138
  const killDecision = shouldKillWorker({
407
1139
  nowEpochMs: nowMs,
408
1140
  startedAtEpochMs: fallbackEpochMs,
409
1141
  logUpdatedAtEpochMs: stallUpdatedAtEpochMs,
410
- }, { timeoutMs: AUTO_CONTINUE_SLICE_TIMEOUT_MS, stallMs: AUTO_CONTINUE_SLICE_LOG_STALL_MS });
1142
+ }, { timeoutMs: scopeTimeoutMs, stallMs: AUTO_CONTINUE_SLICE_LOG_STALL_MS });
411
1143
  if (killDecision.kill) {
412
1144
  try {
413
1145
  await stopProcess(pid);
@@ -420,7 +1152,7 @@ export function createAutoContinueEngine(deps) {
420
1152
  slice.updatedAt = now;
421
1153
  slice.lastError =
422
1154
  killDecision.kind === "timeout"
423
- ? `Autopilot slice timed out after ${Math.round(AUTO_CONTINUE_SLICE_TIMEOUT_MS / 60_000)} minutes.`
1155
+ ? `Autopilot slice timed out after ${Math.round(scopeTimeoutMs / 60_000)} minutes.`
424
1156
  : `Autopilot slice stalled (no output) for ${Math.round(AUTO_CONTINUE_SLICE_LOG_STALL_MS / 60_000)} minutes.`;
425
1157
  autoContinueSliceRuns.set(slice.runId, slice);
426
1158
  run.lastError = slice.lastError;
@@ -434,22 +1166,31 @@ export function createAutoContinueEngine(deps) {
434
1166
  correlationId: slice.runId,
435
1167
  phase: "blocked",
436
1168
  level: "error",
1169
+ progressPct: 55,
1170
+ nextStep: "Open logs/output, decide retry or pause, and capture blocker context for handoff.",
437
1171
  message: `Autopilot slice ${humanLabel}: ${slice.workstreamTitle ?? slice.workstreamId}.`,
438
1172
  metadata: {
439
- event,
440
- requested_by_agent_id: run.agentId,
441
- requested_by_agent_name: run.agentName,
442
- workstream_id: slice.workstreamId,
443
- task_ids: slice.taskIds,
444
- milestone_ids: slice.milestoneIds,
1173
+ ...buildSliceEnrichment({
1174
+ run,
1175
+ slice,
1176
+ workstreamId: slice.workstreamId,
1177
+ workstreamTitle: slice.workstreamTitle ?? null,
1178
+ domain: slice.domain,
1179
+ requiredSkills: slice.requiredSkills,
1180
+ event,
1181
+ }),
1182
+ error_location: killDecision.kind === "timeout"
1183
+ ? "mission-control.auto-continue.engine.slice.timeout"
1184
+ : "mission-control.auto-continue.engine.slice.stall",
445
1185
  log_path: slice.logPath,
446
1186
  output_path: slice.outputPath,
447
1187
  reason: killDecision.reason,
448
1188
  elapsed_ms: killDecision.elapsedMs,
449
1189
  idle_ms: killDecision.idleMs,
1190
+ ...mockMeta(slice),
450
1191
  },
451
1192
  });
452
- await requestDecisionSafe({
1193
+ const decisionResult = await requestDecisionQueued({
453
1194
  initiativeId: run.initiativeId,
454
1195
  correlationId: slice.runId,
455
1196
  title: `Autopilot slice ${humanLabel}: ${slice.workstreamTitle ?? slice.workstreamId}`,
@@ -461,11 +1202,63 @@ export function createAutoContinueEngine(deps) {
461
1202
  "Skip this workstream for now",
462
1203
  ],
463
1204
  blocking: true,
1205
+ decisionType: "autopilot_failure",
1206
+ workstreamId: slice.workstreamId,
1207
+ agentId: slice.agentId,
1208
+ sourceSystem: "orgx-autopilot",
1209
+ conflictSource: killDecision.kind === "timeout"
1210
+ ? "slice_timeout"
1211
+ : "slice_stall_no_output",
1212
+ dedupeKey: [
1213
+ "autopilot",
1214
+ run.initiativeId,
1215
+ slice.workstreamId,
1216
+ killDecision.kind === "timeout"
1217
+ ? "slice_timeout"
1218
+ : "slice_stall_no_output",
1219
+ ].join(":"),
1220
+ recommendedAction: "Review logs and output, then retry once. If repeated, pause autopilot and investigate worker/runtime health.",
1221
+ sourceRunId: slice.runId,
1222
+ sourceRef: {
1223
+ run_id: slice.runId,
1224
+ workstream_id: slice.workstreamId,
1225
+ kill_kind: killDecision.kind,
1226
+ elapsed_ms: killDecision.elapsedMs,
1227
+ idle_ms: killDecision.idleMs,
1228
+ },
1229
+ evidenceRefs: [
1230
+ {
1231
+ evidence_type: killDecision.kind === "timeout"
1232
+ ? "timeout_diagnostic"
1233
+ : "stall_diagnostic",
1234
+ title: killDecision.kind === "timeout"
1235
+ ? "Slice timed out"
1236
+ : "Slice stalled",
1237
+ summary: killDecision.reason,
1238
+ source_pointer: slice.logPath,
1239
+ payload: {
1240
+ elapsed_ms: killDecision.elapsedMs,
1241
+ idle_ms: killDecision.idleMs,
1242
+ output_path: slice.outputPath,
1243
+ },
1244
+ },
1245
+ ],
1246
+ });
1247
+ setLaneState(run, {
1248
+ workstreamId: slice.workstreamId,
1249
+ state: "blocked",
1250
+ activeRunId: null,
1251
+ activeTaskIds: [],
1252
+ blockedReason: slice.lastError,
1253
+ waitingOnWorkstreamIds: [],
1254
+ retryAt: null,
464
1255
  });
465
1256
  await stopAutoContinueRun({
466
1257
  run,
467
1258
  reason: "blocked",
468
1259
  error: slice.lastError,
1260
+ decisionRequired: decisionResult.queued,
1261
+ decisionIds: decisionResult.decisionIds,
469
1262
  });
470
1263
  return;
471
1264
  }
@@ -478,7 +1271,7 @@ export function createAutoContinueEngine(deps) {
478
1271
  }
479
1272
  }
480
1273
  if (!outputComplete)
481
- return;
1274
+ continue;
482
1275
  }
483
1276
  }
484
1277
  // Slice finished.
@@ -486,15 +1279,25 @@ export function createAutoContinueEngine(deps) {
486
1279
  const parsed = raw ? parseSliceResult(raw) : null;
487
1280
  const parsedStatus = parsed?.status ?? "error";
488
1281
  const defaultDecisionBlocking = parsedStatus === "completed" ? false : true;
489
- const decisions = Array.isArray(parsed?.decisions_needed)
1282
+ const allDecisions = Array.isArray(parsed?.decisions_needed)
490
1283
  ? (parsed?.decisions_needed ?? [])
491
1284
  .filter((item) => Boolean(item && typeof item.question === "string" && item.question.trim()))
492
1285
  : [];
1286
+ const isParserSyntheticFallbackDecision = (item) => {
1287
+ const question = String(item?.question ?? "").trim().toLowerCase();
1288
+ const summary = String(item?.summary ?? "").trim().toLowerCase();
1289
+ return ((question.includes("missing required blocking decision") ||
1290
+ summary.includes("parser inserted a blocking decision")) &&
1291
+ item?.blocking === true);
1292
+ };
1293
+ const decisions = allDecisions.filter((item) => !isParserSyntheticFallbackDecision(item));
493
1294
  const blockingDecisionCount = decisions.filter((item) => typeof item.blocking === "boolean" ? item.blocking : defaultDecisionBlocking).length;
494
1295
  const nonBlockingDecisionCount = Math.max(0, decisions.length - blockingDecisionCount);
495
1296
  const effectiveParsedStatus = parsedStatus === "completed" && blockingDecisionCount > 0
496
1297
  ? "needs_decision"
497
- : parsedStatus;
1298
+ : parsedStatus === "needs_decision" && blockingDecisionCount === 0
1299
+ ? "completed"
1300
+ : parsedStatus;
498
1301
  slice.status =
499
1302
  effectiveParsedStatus === "completed"
500
1303
  ? "completed"
@@ -510,32 +1313,157 @@ export function createAutoContinueEngine(deps) {
510
1313
  autoContinueSliceRuns.set(slice.runId, slice);
511
1314
  clearAutoContinueSliceTransientState(slice.runId);
512
1315
  // Token accounting: codex CLI doesn't provide tokens here; use the modeled estimate.
513
- const modeledTokens = slice.tokenEstimate ?? run.activeTaskTokenEstimate ?? 0;
1316
+ const modeledTokens = slice.tokenEstimate ?? 0;
514
1317
  run.tokensUsed += Math.max(0, modeledTokens);
515
1318
  run.activeTaskTokenEstimate = null;
516
1319
  const artifacts = Array.isArray(parsed?.artifacts)
517
1320
  ? (parsed?.artifacts ?? [])
518
1321
  .filter((item) => Boolean(item && typeof item.name === "string" && item.name.trim()))
519
1322
  : [];
1323
+ const artifactEvidenceRefs = artifacts.map((artifact) => ({
1324
+ evidence_type: "artifact",
1325
+ title: artifact.name.trim(),
1326
+ summary: artifact.description?.trim() || "Slice artifact output",
1327
+ source_pointer: artifact.url ?? slice.outputPath,
1328
+ payload: {
1329
+ artifact_type: artifact.artifact_type ?? null,
1330
+ confidence_score: artifact.confidence_score ?? null,
1331
+ task_ids: Array.isArray(artifact.task_ids) && artifact.task_ids.length > 0
1332
+ ? artifact.task_ids
1333
+ : slice.taskIds,
1334
+ milestone_id: artifact.milestone_id ?? slice.milestoneIds[0] ?? null,
1335
+ },
1336
+ }));
1337
+ const nextActions = Array.isArray(parsed?.next_actions)
1338
+ ? parsed.next_actions
1339
+ .filter((item) => typeof item === "string")
1340
+ .map((item) => item.trim())
1341
+ .filter(Boolean)
1342
+ : [];
1343
+ const userSummary = (typeof parsed?.summary === "string" && parsed.summary.trim().length > 0
1344
+ ? parsed.summary.trim()
1345
+ : null) ??
1346
+ nextActions[0] ??
1347
+ (slice.status === "completed"
1348
+ ? `Slice completed for ${slice.workstreamTitle ?? slice.workstreamId}.`
1349
+ : `Slice blocked for ${slice.workstreamTitle ?? slice.workstreamId}.`);
1350
+ const nextStepHint = nextActions[0] ??
1351
+ (slice.status === "completed"
1352
+ ? "No follow-up action returned by worker."
1353
+ : "Resolve blocker to continue execution.");
1354
+ const skillEvidence = Array.isArray(parsed?.skill_evidence)
1355
+ ? parsed.skill_evidence
1356
+ .map((item) => ({
1357
+ skill: typeof item?.skill === "string"
1358
+ ? item.skill.trim()
1359
+ : "",
1360
+ skill_file: typeof item?.skill_file === "string"
1361
+ ? item.skill_file.trim()
1362
+ : null,
1363
+ skill_sha256: typeof item?.skill_sha256 === "string"
1364
+ ? item.skill_sha256.trim().toLowerCase()
1365
+ : null,
1366
+ skill_heading: typeof item?.skill_heading === "string"
1367
+ ? item.skill_heading.trim()
1368
+ : null,
1369
+ }))
1370
+ .filter((item) => item.skill.length > 0)
1371
+ : [];
1372
+ const reportedSkillNames = Array.from(new Set(skillEvidence
1373
+ .map((entry) => entry.skill.replace(/^\$/, "").trim())
1374
+ .filter(Boolean)));
1375
+ const reportedSkillSha256Count = skillEvidence.filter((entry) => typeof entry.skill_sha256 === "string" && entry.skill_sha256.length > 0).length;
520
1376
  const taskUpdates = Array.isArray(parsed?.task_updates)
521
1377
  ? parsed.task_updates
522
1378
  : [];
523
1379
  const milestoneUpdates = Array.isArray(parsed?.milestone_updates)
524
1380
  ? parsed.milestone_updates
525
1381
  : [];
1382
+ const resultEnvelope = {
1383
+ summary: userSummary,
1384
+ parsed_status: effectiveParsedStatus,
1385
+ task_updates: taskUpdates,
1386
+ milestone_updates: milestoneUpdates,
1387
+ next_actions: nextActions,
1388
+ artifacts: artifacts.map((artifact) => ({
1389
+ name: artifact.name,
1390
+ artifact_type: artifact.artifact_type ?? null,
1391
+ url: artifact.url ?? null,
1392
+ })),
1393
+ };
1394
+ const evidenceEnvelope = {
1395
+ artifacts: artifacts.map((artifact) => ({
1396
+ name: artifact.name,
1397
+ artifact_type: artifact.artifact_type ?? null,
1398
+ source_pointer: artifact.url ?? null,
1399
+ })),
1400
+ files: [slice.outputPath, slice.logPath].filter(Boolean),
1401
+ logs: [slice.logPath].filter(Boolean),
1402
+ };
1403
+ let blockingDecisionQueued = false;
1404
+ const blockingDecisionIds = [];
1405
+ const nonBlockingDecisionIds = [];
526
1406
  for (const decision of decisions) {
527
- await requestDecisionSafe({
1407
+ const isBlocking = typeof decision.blocking === "boolean" ? decision.blocking : defaultDecisionBlocking;
1408
+ const normalizedQuestion = decision.question.trim();
1409
+ const decisionResult = await requestDecisionQueued({
528
1410
  initiativeId: run.initiativeId,
529
1411
  correlationId: slice.runId,
530
- title: decision.question.trim(),
1412
+ title: normalizedQuestion,
531
1413
  summary: decision.summary ?? parsed?.summary ?? null,
532
1414
  urgency: decision.urgency ?? "high",
533
1415
  options: Array.isArray(decision.options)
534
1416
  ? decision.options.filter((opt) => typeof opt === "string" && opt.trim())
535
1417
  : [],
536
- blocking: typeof decision.blocking === "boolean" ? decision.blocking : defaultDecisionBlocking,
1418
+ blocking: isBlocking,
1419
+ decisionType: isBlocking
1420
+ ? "autopilot_blocking_decision"
1421
+ : "autopilot_followup_decision",
1422
+ workstreamId: slice.workstreamId,
1423
+ agentId: slice.agentId,
1424
+ sourceSystem: "orgx-autopilot",
1425
+ conflictSource: parsedStatus === "needs_decision"
1426
+ ? "slice_needs_decision"
1427
+ : "slice_reported_decision",
1428
+ dedupeKey: [
1429
+ "autopilot",
1430
+ run.initiativeId,
1431
+ slice.workstreamId,
1432
+ "slice_reported_decision",
1433
+ normalizedQuestion.toLowerCase(),
1434
+ ].join(":"),
1435
+ recommendedAction: nextActions[0] ??
1436
+ "Resolve this decision to continue the slice or safely defer workstream execution.",
1437
+ sourceRunId: slice.runId,
1438
+ sourceRef: {
1439
+ run_id: slice.runId,
1440
+ workstream_id: slice.workstreamId,
1441
+ parsed_status: parsedStatus,
1442
+ },
1443
+ evidenceRefs: [
1444
+ {
1445
+ evidence_type: "slice_output_summary",
1446
+ title: "Slice requested a decision",
1447
+ summary: decision.summary ?? parsed?.summary ?? "Decision required by slice output.",
1448
+ source_pointer: slice.outputPath,
1449
+ payload: {
1450
+ log_path: slice.logPath,
1451
+ blocking: isBlocking,
1452
+ },
1453
+ },
1454
+ ...artifactEvidenceRefs,
1455
+ ],
537
1456
  });
1457
+ if (decisionResult.queued && isBlocking)
1458
+ blockingDecisionQueued = true;
1459
+ if (decisionResult.decisionIds.length > 0) {
1460
+ if (isBlocking)
1461
+ blockingDecisionIds.push(...decisionResult.decisionIds);
1462
+ else
1463
+ nonBlockingDecisionIds.push(...decisionResult.decisionIds);
1464
+ }
538
1465
  }
1466
+ const decisionIds = Array.from(new Set([...blockingDecisionIds, ...nonBlockingDecisionIds]));
539
1467
  for (const artifact of artifacts) {
540
1468
  await registerArtifactSafe({
541
1469
  initiativeId: run.initiativeId,
@@ -543,16 +1471,86 @@ export function createAutoContinueEngine(deps) {
543
1471
  agentId: slice.agentId,
544
1472
  agentName: slice.agentName,
545
1473
  workstreamId: slice.workstreamId,
1474
+ fallbackMilestoneId: slice.milestoneIds[0] ?? null,
1475
+ fallbackTaskIds: slice.taskIds,
546
1476
  artifact,
1477
+ isMockWorker: slice.isMockWorker,
547
1478
  });
548
1479
  }
1480
+ // --- Proof ladder gate: check completion tasks for proof readiness ---
1481
+ // Phase 1: warn-only. Does not block status transitions but creates
1482
+ // a decision request when proof is missing for done/completed tasks.
1483
+ const doneTaskUpdates = taskUpdates.filter((tu) => tu.status === "done" || tu.status === "completed");
1484
+ if (doneTaskUpdates.length > 0 && !slice.isMockWorker) {
1485
+ const proofStrictness = process.env.ORGX_PROOF_STRICTNESS ?? "warn";
1486
+ for (const dtu of doneTaskUpdates) {
1487
+ try {
1488
+ const qp = new URLSearchParams({ task_id: dtu.task_id });
1489
+ const proofResult = await client.rawRequest("GET", `/api/flywheel/proof-status?${qp.toString()}`).catch(() => null);
1490
+ // If proof API unavailable, skip gracefully (phase 1)
1491
+ if (!proofResult)
1492
+ continue;
1493
+ const overallPassed = proofResult?.overall_passed === true;
1494
+ if (!overallPassed && proofStrictness === "block") {
1495
+ // Hard block: downgrade to needs_review
1496
+ dtu.status = "needs_review";
1497
+ const reasonCodes = Array.isArray(proofResult?.reason_codes)
1498
+ ? proofResult.reason_codes.join(", ")
1499
+ : "incomplete_proof";
1500
+ await requestDecisionSafe({
1501
+ initiativeId: run.initiativeId,
1502
+ correlationId: slice.runId,
1503
+ title: `Task ${dtu.task_id} missing proof for completion`,
1504
+ summary: `Proof chain incomplete (${reasonCodes}). Task held in needs_review until proof is resolved.`,
1505
+ urgency: "high",
1506
+ blocking: true,
1507
+ decisionType: "proof_incomplete",
1508
+ workstreamId: slice.workstreamId,
1509
+ agentId: slice.agentId,
1510
+ sourceRunId: slice.runId,
1511
+ dedupeKey: `proof-gate:${dtu.task_id}:${slice.runId}`,
1512
+ metadata: { proof_result: proofResult },
1513
+ });
1514
+ }
1515
+ else if (!overallPassed) {
1516
+ // Warn-only: emit activity but allow transition
1517
+ await emitActivitySafe({
1518
+ initiativeId: run.initiativeId,
1519
+ runId: slice.runId,
1520
+ correlationId: slice.runId,
1521
+ phase: "review",
1522
+ level: "warn",
1523
+ message: `Task ${dtu.task_id} completing with incomplete proof chain.`,
1524
+ metadata: {
1525
+ event: "proof_gate_warning",
1526
+ task_id: dtu.task_id,
1527
+ proof_result: proofResult,
1528
+ },
1529
+ });
1530
+ }
1531
+ }
1532
+ catch {
1533
+ // Best-effort proof check; don't block on transient failures
1534
+ }
1535
+ }
1536
+ }
549
1537
  const statusUpdateResult = await applyAgentStatusUpdatesSafe({
550
1538
  initiativeId: run.initiativeId,
551
1539
  runId: slice.runId,
552
1540
  correlationId: slice.runId,
553
1541
  taskUpdates,
554
1542
  milestoneUpdates,
1543
+ isMockWorker: slice.isMockWorker,
555
1544
  });
1545
+ if (statusUpdateResult.taskUpdates.length > 0 ||
1546
+ statusUpdateResult.milestoneUpdates.length > 0) {
1547
+ recordLocalStatusOverrides({
1548
+ initiativeId: run.initiativeId,
1549
+ updatedAt: now,
1550
+ taskUpdates: statusUpdateResult.taskUpdates,
1551
+ milestoneUpdates: statusUpdateResult.milestoneUpdates,
1552
+ });
1553
+ }
556
1554
  try {
557
1555
  writeRuntimeEvent({
558
1556
  sourceClient: slice.sourceClient,
@@ -564,9 +1562,14 @@ export function createAutoContinueEngine(deps) {
564
1562
  agentId: slice.agentId,
565
1563
  agentName: slice.agentName ?? null,
566
1564
  phase: slice.status === "completed" ? "completed" : "blocked",
567
- message: parsed?.summary ?? slice.lastError ?? "Autopilot slice finished.",
1565
+ message: userSummary ?? slice.lastError ?? "Autopilot slice finished.",
568
1566
  metadata: {
569
1567
  event: "autopilot_slice_finished",
1568
+ initiative_id: run.initiativeId,
1569
+ run_id: slice.runId,
1570
+ slice_run_id: slice.runId,
1571
+ workstream_id: slice.workstreamId,
1572
+ correlation_id: slice.runId,
570
1573
  requested_by_agent_id: run.agentId,
571
1574
  requested_by_agent_name: run.agentName,
572
1575
  status: effectiveParsedStatus,
@@ -576,55 +1579,149 @@ export function createAutoContinueEngine(deps) {
576
1579
  non_blocking_decisions: nonBlockingDecisionCount,
577
1580
  status_updates: statusUpdateResult.applied,
578
1581
  status_updates_buffered: statusUpdateResult.buffered,
1582
+ reported_skill_evidence_count: skillEvidence.length,
1583
+ reported_skill_sha256_count: reportedSkillSha256Count,
1584
+ reported_skill_names: reportedSkillNames,
1585
+ action_type: normalizeActivityActionType("run_completed"),
1586
+ action_phase: normalizeActivityActionPhase(slice.status === "completed" ? "completed" : "blocked"),
1587
+ result: resultEnvelope,
1588
+ evidence: evidenceEnvelope,
1589
+ ...mockMeta(slice),
1590
+ user_summary: userSummary,
1591
+ next_actions: nextActions,
579
1592
  },
580
1593
  });
581
1594
  }
582
1595
  catch {
583
1596
  // best effort
584
1597
  }
1598
+ if (slice.status === "completed") {
1599
+ await emitActivitySafe({
1600
+ initiativeId: run.initiativeId,
1601
+ runId: slice.runId,
1602
+ correlationId: slice.runId,
1603
+ phase: "handoff",
1604
+ level: "info",
1605
+ message: `Handoff ready for ${slice.workstreamTitle ?? slice.workstreamId}.`,
1606
+ progressPct: 80,
1607
+ nextStep: nextStepHint,
1608
+ metadata: buildSliceEnrichment({
1609
+ run,
1610
+ slice,
1611
+ workstreamId: slice.workstreamId,
1612
+ workstreamTitle: slice.workstreamTitle ?? null,
1613
+ domain: slice.domain,
1614
+ requiredSkills: slice.requiredSkills,
1615
+ nextActions,
1616
+ userSummary,
1617
+ event: "autopilot_slice_handoff",
1618
+ extra: {
1619
+ parsed_status: effectiveParsedStatus,
1620
+ artifacts: artifacts.length,
1621
+ decisions: decisions.length,
1622
+ decision_ids: decisionIds,
1623
+ output_path: slice.outputPath,
1624
+ log_path: slice.logPath,
1625
+ task_updates: taskUpdates,
1626
+ milestone_updates: milestoneUpdates,
1627
+ result: resultEnvelope,
1628
+ evidence: evidenceEnvelope,
1629
+ ...mockMeta(slice),
1630
+ },
1631
+ }),
1632
+ });
1633
+ }
585
1634
  await emitActivitySafe({
586
1635
  initiativeId: run.initiativeId,
587
1636
  runId: slice.runId,
588
1637
  correlationId: slice.runId,
589
1638
  phase: slice.status === "completed" ? "completed" : "blocked",
590
1639
  level: slice.status === "completed" ? "info" : "warn",
1640
+ progressPct: slice.status === "completed" ? 100 : 65,
1641
+ nextStep: nextStepHint,
591
1642
  message: slice.status === "completed"
592
1643
  ? `Autopilot slice completed for ${slice.workstreamTitle ?? slice.workstreamId} (${slice.taskIds.length} task${slice.taskIds.length === 1 ? "" : "s"}).`
593
1644
  : `Autopilot slice blocked: ${slice.workstreamTitle ?? slice.workstreamId}.`,
594
1645
  metadata: {
595
- event: "autopilot_slice_result",
596
- requested_by_agent_id: run.agentId,
597
- requested_by_agent_name: run.agentName,
598
- agent_id: slice.agentId,
599
- agent_name: slice.agentName,
600
- domain: slice.domain,
601
- required_skills: slice.requiredSkills,
602
- workstream_id: slice.workstreamId,
603
- task_ids: slice.taskIds,
604
- milestone_ids: slice.milestoneIds,
1646
+ ...buildSliceEnrichment({
1647
+ run,
1648
+ slice,
1649
+ workstreamId: slice.workstreamId,
1650
+ workstreamTitle: slice.workstreamTitle ?? null,
1651
+ domain: slice.domain,
1652
+ requiredSkills: slice.requiredSkills,
1653
+ nextActions,
1654
+ userSummary,
1655
+ event: "autopilot_slice_result",
1656
+ }),
1657
+ error_location: slice.status === "completed"
1658
+ ? null
1659
+ : "mission-control.auto-continue.engine.slice.result",
1660
+ behavior_config_id: slice.behaviorConfigId,
1661
+ behavior_config_version: slice.behaviorConfigVersion,
1662
+ behavior_config_hash: slice.behaviorConfigHash,
1663
+ policy_source: slice.behaviorPolicySource,
1664
+ behavior_automation_level: slice.behaviorAutomationLevel,
605
1665
  parsed_status: effectiveParsedStatus,
606
1666
  has_output: Boolean(parsed),
607
1667
  artifacts: artifacts.length,
608
1668
  decisions: decisions.length,
609
1669
  blocking_decisions: blockingDecisionCount,
610
1670
  non_blocking_decisions: nonBlockingDecisionCount,
611
- decision_required: blockingDecisionCount > 0,
1671
+ decision_ids: decisionIds,
1672
+ blocking_decision_ids: Array.from(new Set(blockingDecisionIds)),
1673
+ non_blocking_decision_ids: Array.from(new Set(nonBlockingDecisionIds)),
1674
+ decision_required: blockingDecisionQueued,
612
1675
  status_updates_applied: statusUpdateResult.applied,
613
1676
  status_updates_buffered: statusUpdateResult.buffered,
1677
+ reported_skill_evidence_count: skillEvidence.length,
1678
+ reported_skill_sha256_count: reportedSkillSha256Count,
1679
+ reported_skill_names: reportedSkillNames,
614
1680
  output_path: slice.outputPath,
615
1681
  log_path: slice.logPath,
616
1682
  error: slice.lastError,
1683
+ next_actions: nextActions,
1684
+ task_updates: taskUpdates,
1685
+ milestone_updates: milestoneUpdates,
1686
+ result: resultEnvelope,
1687
+ evidence: evidenceEnvelope,
1688
+ ...mockMeta(slice),
1689
+ user_summary: userSummary,
617
1690
  },
618
1691
  });
1692
+ // Append to local team context for cross-agent awareness on subsequent slices.
1693
+ if (slice.status === "completed") {
1694
+ try {
1695
+ appendTeamCompletion(run.initiativeId, {
1696
+ domain: slice.domain ?? "unknown",
1697
+ task_title: slice.workstreamTitle ?? slice.workstreamId,
1698
+ summary: parsed?.summary ?? "Completed.",
1699
+ key_outputs: artifacts.map((a) => a.name).filter(Boolean).slice(0, 5),
1700
+ completed_at: new Date().toISOString(),
1701
+ });
1702
+ }
1703
+ catch {
1704
+ // best effort: do not block the engine on store failure
1705
+ }
1706
+ }
619
1707
  if (slice.status !== "completed") {
620
- if (slice.status === "error" && decisions.length === 0) {
621
- await requestDecisionSafe({
1708
+ let fallbackDecisionResult = {
1709
+ queued: false,
1710
+ decisionIds: [],
1711
+ };
1712
+ if (!blockingDecisionQueued) {
1713
+ const blockedLike = slice.status === "blocked";
1714
+ fallbackDecisionResult = await requestDecisionQueued({
622
1715
  initiativeId: run.initiativeId,
623
1716
  correlationId: slice.runId,
624
- title: `Autopilot slice failed: ${slice.workstreamTitle ?? slice.workstreamId}`,
1717
+ title: blockedLike
1718
+ ? `Autopilot slice blocked: ${slice.workstreamTitle ?? slice.workstreamId}`
1719
+ : `Autopilot slice failed: ${slice.workstreamTitle ?? slice.workstreamId}`,
625
1720
  summary: parsed?.summary ??
626
1721
  slice.lastError ??
627
- "The slice failed without producing a valid output contract. Review logs/output and decide whether to retry or pause autopilot.",
1722
+ (blockedLike
1723
+ ? "The slice reported a blocked/decision-required state without a blocking decision payload. Review logs/output and decide whether to retry, unblock, or skip."
1724
+ : "The slice failed without producing a valid output contract. Review logs/output and decide whether to retry or pause autopilot."),
628
1725
  urgency: "high",
629
1726
  options: [
630
1727
  "Retry this workstream slice",
@@ -632,14 +1729,66 @@ export function createAutoContinueEngine(deps) {
632
1729
  "Skip this workstream for now",
633
1730
  ],
634
1731
  blocking: true,
1732
+ decisionType: blockedLike ? "autopilot_blocked_without_decision" : "autopilot_failure",
1733
+ workstreamId: slice.workstreamId,
1734
+ agentId: slice.agentId,
1735
+ sourceSystem: "orgx-autopilot",
1736
+ conflictSource: blockedLike
1737
+ ? "slice_missing_blocking_decision"
1738
+ : "slice_invalid_output",
1739
+ dedupeKey: [
1740
+ "autopilot",
1741
+ run.initiativeId,
1742
+ slice.workstreamId,
1743
+ blockedLike ? "slice_missing_blocking_decision" : "slice_invalid_output",
1744
+ ].join(":"),
1745
+ recommendedAction: nextActions[0] ??
1746
+ "Review the output contract and logs, then retry or pause autopilot until the blocker is resolved.",
1747
+ sourceRunId: slice.runId,
1748
+ sourceRef: {
1749
+ run_id: slice.runId,
1750
+ workstream_id: slice.workstreamId,
1751
+ parsed_status: effectiveParsedStatus,
1752
+ },
1753
+ evidenceRefs: [
1754
+ {
1755
+ evidence_type: "slice_output_validation",
1756
+ title: "Slice output requires fallback decision",
1757
+ summary: parsed?.summary ??
1758
+ slice.lastError ??
1759
+ "Slice did not provide a blocking decision payload.",
1760
+ source_pointer: slice.outputPath,
1761
+ payload: {
1762
+ log_path: slice.logPath,
1763
+ parsed_status: effectiveParsedStatus,
1764
+ },
1765
+ },
1766
+ ...artifactEvidenceRefs,
1767
+ ],
635
1768
  });
636
1769
  }
1770
+ setLaneState(run, {
1771
+ workstreamId: slice.workstreamId,
1772
+ state: "blocked",
1773
+ activeRunId: null,
1774
+ activeTaskIds: [],
1775
+ blockedReason: parsed?.summary ??
1776
+ slice.lastError ??
1777
+ `Slice returned status: ${effectiveParsedStatus}`,
1778
+ waitingOnWorkstreamIds: [],
1779
+ retryAt: null,
1780
+ });
1781
+ if (!run.blockedWorkstreamIds.includes(slice.workstreamId)) {
1782
+ run.blockedWorkstreamIds.push(slice.workstreamId);
1783
+ }
637
1784
  await stopAutoContinueRun({
638
1785
  run,
639
1786
  reason: slice.status === "error" ? "error" : "blocked",
640
1787
  error: parsed?.summary ??
641
1788
  slice.lastError ??
642
1789
  `Slice returned status: ${effectiveParsedStatus}`,
1790
+ decisionRequired: blockingDecisionQueued || fallbackDecisionResult.queued,
1791
+ decisionIds: Array.from(new Set([...decisionIds, ...fallbackDecisionResult.decisionIds])),
643
1792
  });
644
1793
  return;
645
1794
  }
@@ -654,7 +1803,7 @@ export function createAutoContinueEngine(deps) {
654
1803
  const attentionSummary = completionHadNoOutcome
655
1804
  ? "The slice reported completion but did not produce artifacts or status updates. Decide whether to retry, request stronger output, or mark tasks manually."
656
1805
  : "The slice exited without a valid output contract. Review logs/output and decide whether to retry or pause autopilot.";
657
- await requestDecisionSafe({
1806
+ const decisionResult = await requestDecisionQueued({
658
1807
  initiativeId: run.initiativeId,
659
1808
  correlationId: slice.runId,
660
1809
  title: attentionTitle,
@@ -666,7 +1815,61 @@ export function createAutoContinueEngine(deps) {
666
1815
  "Skip this workstream for now",
667
1816
  ],
668
1817
  blocking: true,
1818
+ decisionType: completionHadNoOutcome
1819
+ ? "autopilot_completed_without_outcome"
1820
+ : "autopilot_failure",
1821
+ workstreamId: slice.workstreamId,
1822
+ agentId: slice.agentId,
1823
+ sourceSystem: "orgx-autopilot",
1824
+ conflictSource: completionHadNoOutcome
1825
+ ? "slice_completed_without_outcome"
1826
+ : "slice_invalid_output",
1827
+ dedupeKey: [
1828
+ "autopilot",
1829
+ run.initiativeId,
1830
+ slice.workstreamId,
1831
+ completionHadNoOutcome
1832
+ ? "slice_completed_without_outcome"
1833
+ : "slice_invalid_output",
1834
+ ].join(":"),
1835
+ recommendedAction: nextActions[0] ??
1836
+ "Verify slice outputs and status updates, then retry once or pause for investigation.",
1837
+ sourceRunId: slice.runId,
1838
+ sourceRef: {
1839
+ run_id: slice.runId,
1840
+ workstream_id: slice.workstreamId,
1841
+ parsed_status: parsedStatus,
1842
+ },
1843
+ evidenceRefs: [
1844
+ {
1845
+ evidence_type: "slice_output_validation",
1846
+ title: "Slice output needs verification",
1847
+ summary: attentionSummary,
1848
+ source_pointer: slice.outputPath,
1849
+ payload: {
1850
+ log_path: slice.logPath,
1851
+ parsed_status: parsedStatus,
1852
+ completion_had_no_outcome: completionHadNoOutcome,
1853
+ },
1854
+ },
1855
+ ...artifactEvidenceRefs,
1856
+ ],
1857
+ });
1858
+ setLaneState(run, {
1859
+ workstreamId: slice.workstreamId,
1860
+ state: "blocked",
1861
+ activeRunId: null,
1862
+ activeTaskIds: [],
1863
+ blockedReason: slice.lastError ??
1864
+ (completionHadNoOutcome
1865
+ ? "Slice completed without verifiable outcomes."
1866
+ : "Slice failed or returned invalid output."),
1867
+ waitingOnWorkstreamIds: [],
1868
+ retryAt: null,
669
1869
  });
1870
+ if (!run.blockedWorkstreamIds.includes(slice.workstreamId)) {
1871
+ run.blockedWorkstreamIds.push(slice.workstreamId);
1872
+ }
670
1873
  await stopAutoContinueRun({
671
1874
  run,
672
1875
  reason: completionHadNoOutcome ? "blocked" : "error",
@@ -674,13 +1877,31 @@ export function createAutoContinueEngine(deps) {
674
1877
  (completionHadNoOutcome
675
1878
  ? "Slice completed without verifiable outcomes."
676
1879
  : "Slice failed or returned invalid output."),
1880
+ decisionRequired: completionHadNoOutcome && decisionResult.queued,
1881
+ decisionIds: decisionResult.decisionIds,
677
1882
  });
678
1883
  return;
679
1884
  }
680
1885
  run.lastRunId = slice.runId;
681
- run.lastTaskId = run.activeTaskId ?? run.lastTaskId;
682
- run.activeRunId = null;
683
- run.activeTaskId = null;
1886
+ run.lastTaskId = slice.taskIds[0] ?? run.lastTaskId;
1887
+ removeActiveSliceFromRun(run, {
1888
+ sliceRunId: slice.runId,
1889
+ taskIds: slice.taskIds,
1890
+ workstreamId: slice.workstreamId,
1891
+ });
1892
+ setLaneState(run, {
1893
+ workstreamId: slice.workstreamId,
1894
+ state: "completed",
1895
+ activeRunId: null,
1896
+ activeTaskIds: [],
1897
+ blockedReason: null,
1898
+ waitingOnWorkstreamIds: [],
1899
+ retryAt: null,
1900
+ });
1901
+ run.blockedWorkstreamIds = run.blockedWorkstreamIds.filter((id) => id !== slice.workstreamId);
1902
+ syncLegacyRunPointers(run);
1903
+ // Do not keep prior rate-limit/runtime errors after a completed slice.
1904
+ run.lastError = null;
684
1905
  run.updatedAt = now;
685
1906
  try {
686
1907
  await updateInitiativeAutoContinueState({
@@ -691,6 +1912,50 @@ export function createAutoContinueEngine(deps) {
691
1912
  catch {
692
1913
  // best effort
693
1914
  }
1915
+ // Evaluate scope-level completion for milestone/workstream scopes.
1916
+ if (slice.scope && slice.scope !== "task") {
1917
+ try {
1918
+ const scopeGraph = applyLocalInitiativeOverrideToGraph(await buildMissionControlGraph(client, run.initiativeId));
1919
+ const scopeNodeById = new Map(scopeGraph.nodes.map((n) => [n.id, n]));
1920
+ const scopeResult = evaluateScopeCompletion({
1921
+ scope: slice.scope,
1922
+ milestoneIds: slice.scopeMilestoneIds ?? [],
1923
+ workstreamId: slice.workstreamId,
1924
+ nodeById: scopeNodeById,
1925
+ });
1926
+ if (scopeResult.scopeComplete) {
1927
+ await emitActivitySafe({
1928
+ initiativeId: run.initiativeId,
1929
+ runId: slice.runId,
1930
+ correlationId: slice.runId,
1931
+ phase: "completed",
1932
+ level: "info",
1933
+ progressPct: 100,
1934
+ nextStep: slice.scope === "milestone"
1935
+ ? "Queue the next milestone-ready slice."
1936
+ : "Select the next dispatchable workstream from Next Up.",
1937
+ message: `${slice.scope === "milestone" ? "Milestone" : "Workstream"} scope completed for ${slice.workstreamTitle ?? slice.workstreamId}.`,
1938
+ metadata: {
1939
+ ...buildSliceEnrichment({
1940
+ run,
1941
+ slice,
1942
+ workstreamId: slice.workstreamId,
1943
+ workstreamTitle: slice.workstreamTitle ?? null,
1944
+ domain: slice.domain,
1945
+ requiredSkills: slice.requiredSkills,
1946
+ event: "scope_completed",
1947
+ }),
1948
+ scope: slice.scope,
1949
+ milestone_ids: slice.scopeMilestoneIds,
1950
+ remaining_tasks: 0,
1951
+ },
1952
+ });
1953
+ }
1954
+ }
1955
+ catch {
1956
+ // best-effort scope completion check
1957
+ }
1958
+ }
694
1959
  if (run.stopAfterSlice) {
695
1960
  run.stopAfterSlice = false;
696
1961
  await stopAutoContinueRun({ run, reason: "completed" });
@@ -702,17 +1967,26 @@ export function createAutoContinueEngine(deps) {
702
1967
  }
703
1968
  }
704
1969
  }
1970
+ syncLegacyRunPointers(run);
705
1971
  if (run.stopRequested) {
706
1972
  run.status = "stopping";
707
1973
  run.updatedAt = now;
708
1974
  await stopAutoContinueRun({ run, reason: "stopped" });
709
1975
  return;
710
1976
  }
1977
+ const tokenBudgetValue = typeof run.tokenBudget === "number" && Number.isFinite(run.tokenBudget)
1978
+ ? run.tokenBudget
1979
+ : null;
711
1980
  // 2) Enforce token guardrail before starting a new slice.
712
- if (run.tokensUsed >= run.tokenBudget) {
1981
+ if (tokenBudgetValue !== null && run.tokensUsed >= tokenBudgetValue) {
713
1982
  await stopAutoContinueRun({ run, reason: "budget_exhausted" });
714
1983
  return;
715
1984
  }
1985
+ const activeSliceCount = listActiveSliceRunIds(run).length;
1986
+ if (activeSliceCount >= run.maxParallelSlices) {
1987
+ run.updatedAt = now;
1988
+ return;
1989
+ }
716
1990
  // 3) Pick next workstream slice and dispatch.
717
1991
  let graph;
718
1992
  try {
@@ -728,6 +2002,7 @@ export function createAutoContinueEngine(deps) {
728
2002
  }
729
2003
  const nodes = graph.nodes;
730
2004
  const nodeById = new Map(nodes.map((node) => [node.id, node]));
2005
+ applyLocalStatusOverridesToGraph(run.initiativeId, nodeById);
731
2006
  const taskNodes = nodes.filter((node) => node.type === "task");
732
2007
  const todoTasks = taskNodes.filter((node) => isTodoStatus(node.status));
733
2008
  if (todoTasks.length === 0) {
@@ -746,6 +2021,7 @@ export function createAutoContinueEngine(deps) {
746
2021
  };
747
2022
  // Select the next eligible workstream by scanning ordered todos.
748
2023
  let selectedWorkstreamId = null;
2024
+ let deferredBySpawnGuardRateLimit = 0;
749
2025
  for (const taskId of graph.recentTodos) {
750
2026
  const node = nodeById.get(taskId);
751
2027
  if (!node || node.type !== "task")
@@ -763,6 +2039,18 @@ export function createAutoContinueEngine(deps) {
763
2039
  }
764
2040
  if (!node.workstreamId)
765
2041
  continue;
2042
+ if (run.blockedWorkstreamIds.includes(node.workstreamId))
2043
+ continue;
2044
+ const lane = run.laneByWorkstreamId[node.workstreamId] ?? null;
2045
+ if (lane?.state === "running" && lane.activeRunId)
2046
+ continue;
2047
+ if (lane?.state === "rate_limited" && lane.retryAt) {
2048
+ const retryAtMs = Date.parse(lane.retryAt);
2049
+ if (Number.isFinite(retryAtMs) && retryAtMs > Date.now()) {
2050
+ deferredBySpawnGuardRateLimit += 1;
2051
+ continue;
2052
+ }
2053
+ }
766
2054
  const ws = nodeById.get(node.workstreamId);
767
2055
  if (ws && !isDispatchableWorkstreamStatus(ws.status))
768
2056
  continue;
@@ -770,32 +2058,107 @@ export function createAutoContinueEngine(deps) {
770
2058
  continue;
771
2059
  if (taskHasBlockedParent(node))
772
2060
  continue;
2061
+ const retryAtMs = getSpawnGuardRetryAtMs(run.initiativeId, node.id);
2062
+ if (retryAtMs > 0) {
2063
+ deferredBySpawnGuardRateLimit += 1;
2064
+ continue;
2065
+ }
773
2066
  selectedWorkstreamId = node.workstreamId;
774
2067
  break;
775
2068
  }
776
2069
  if (!selectedWorkstreamId) {
777
- await stopAutoContinueRun({ run, reason: "blocked" });
778
- return;
779
- }
780
- const workstreamNode = nodeById.get(selectedWorkstreamId) ?? null;
781
- const workstreamTitle = workstreamNode?.title ?? null;
782
- const initiativeNode = nodes.find((node) => node.type === "initiative") ?? null;
783
- const initiativeTitle = initiativeNode?.title ?? `Initiative ${run.initiativeId.slice(0, 8)}`;
784
- const sliceTaskNodes = graph.recentTodos
785
- .map((taskId) => nodeById.get(taskId))
786
- .filter((node) => Boolean(node &&
787
- node.type === "task" &&
788
- node.workstreamId === selectedWorkstreamId &&
789
- isTodoStatus(node.status) &&
790
- taskIsReady(node) &&
791
- !taskHasBlockedParent(node) &&
792
- (run.includeVerification ||
793
- !/^verification[ \t]+scenario/i.test(String(node.title ?? "")))))
794
- .slice(0, AUTO_CONTINUE_SLICE_MAX_TASKS);
795
- const primaryTask = sliceTaskNodes[0] ?? null;
796
- if (!primaryTask) {
797
- await stopAutoContinueRun({ run, reason: "blocked" });
798
- return;
2070
+ const waitingByWorkstream = new Map();
2071
+ for (const task of taskNodes) {
2072
+ if (!isTodoStatus(task.status))
2073
+ continue;
2074
+ if (!run.includeVerification &&
2075
+ typeof task.title === "string" &&
2076
+ /^verification[ \t]+scenario/i.test(task.title)) {
2077
+ continue;
2078
+ }
2079
+ const workstreamId = (task.workstreamId ?? "").trim();
2080
+ if (!workstreamId)
2081
+ continue;
2082
+ if (Array.isArray(run.allowedWorkstreamIds) &&
2083
+ run.allowedWorkstreamIds.length > 0 &&
2084
+ !run.allowedWorkstreamIds.includes(workstreamId)) {
2085
+ continue;
2086
+ }
2087
+ if (run.blockedWorkstreamIds.includes(workstreamId)) {
2088
+ continue;
2089
+ }
2090
+ const blockedParents = taskHasBlockedParent(task);
2091
+ const unresolvedDepWorkstreamIds = task.dependencyIds
2092
+ .map((depId) => nodeById.get(depId))
2093
+ .filter((dep) => Boolean(dep && !isDoneStatus(dep.status)))
2094
+ .map((dep) => (dep.workstreamId ?? "").trim())
2095
+ .filter(Boolean);
2096
+ if (blockedParents || unresolvedDepWorkstreamIds.length > 0) {
2097
+ const existing = waitingByWorkstream.get(workstreamId) ?? [];
2098
+ waitingByWorkstream.set(workstreamId, dedupeStrings([...existing, ...unresolvedDepWorkstreamIds]));
2099
+ }
2100
+ }
2101
+ for (const [workstreamId, waitingOnWorkstreamIds] of waitingByWorkstream.entries()) {
2102
+ setLaneState(run, {
2103
+ workstreamId,
2104
+ state: "waiting_dependency",
2105
+ activeRunId: null,
2106
+ activeTaskIds: [],
2107
+ blockedReason: null,
2108
+ waitingOnWorkstreamIds,
2109
+ retryAt: null,
2110
+ });
2111
+ }
2112
+ if (listActiveSliceRunIds(run).length > 0) {
2113
+ run.updatedAt = now;
2114
+ return;
2115
+ }
2116
+ if (deferredBySpawnGuardRateLimit > 0) {
2117
+ run.updatedAt = now;
2118
+ return;
2119
+ }
2120
+ if (run.allowedWorkstreamIds && run.allowedWorkstreamIds.length > 0) {
2121
+ const scopedTodoCount = taskNodes.filter((node) => {
2122
+ if (!isTodoStatus(node.status))
2123
+ return false;
2124
+ if (!run.includeVerification &&
2125
+ typeof node.title === "string" &&
2126
+ /^verification[ \t]+scenario/i.test(node.title)) {
2127
+ return false;
2128
+ }
2129
+ if (!node.workstreamId)
2130
+ return false;
2131
+ return run.allowedWorkstreamIds?.includes(node.workstreamId) ?? false;
2132
+ }).length;
2133
+ if (scopedTodoCount === 0) {
2134
+ await stopAutoContinueRun({ run, reason: "completed" });
2135
+ return;
2136
+ }
2137
+ }
2138
+ await stopAutoContinueRun({ run, reason: "blocked" });
2139
+ return;
2140
+ }
2141
+ const workstreamNode = nodeById.get(selectedWorkstreamId) ?? null;
2142
+ const workstreamTitle = workstreamNode?.title ?? null;
2143
+ const initiativeNode = nodes.find((node) => node.type === "initiative") ?? null;
2144
+ const initiativeTitle = initiativeNode?.title ?? `Initiative ${run.initiativeId.slice(0, 8)}`;
2145
+ const scopeSelection = selectSliceTasksByScope({
2146
+ scope: run.scope,
2147
+ workstreamId: selectedWorkstreamId,
2148
+ recentTodos: graph.recentTodos,
2149
+ nodeById,
2150
+ includeVerification: run.includeVerification,
2151
+ });
2152
+ const sliceTaskNodes = scopeSelection.tasks;
2153
+ const scopeMilestoneIds = scopeSelection.milestoneIds;
2154
+ const primaryTask = sliceTaskNodes[0] ?? null;
2155
+ if (!primaryTask) {
2156
+ if (listActiveSliceRunIds(run).length > 0) {
2157
+ run.updatedAt = now;
2158
+ return;
2159
+ }
2160
+ await stopAutoContinueRun({ run, reason: "blocked" });
2161
+ return;
799
2162
  }
800
2163
  let cappedSliceTaskNodes = sliceTaskNodes;
801
2164
  let expectedDurationHours = cappedSliceTaskNodes.reduce((acc, t) => acc +
@@ -803,14 +2166,14 @@ export function createAutoContinueEngine(deps) {
803
2166
  ? Math.max(0, t.expectedDurationHours)
804
2167
  : 0), 0);
805
2168
  let tokenEstimate = estimateTokensForDurationHours(expectedDurationHours);
806
- const remainingTokens = run.tokenBudget - run.tokensUsed;
807
- if (remainingTokens <= 0) {
2169
+ const remainingTokens = tokenBudgetValue !== null ? tokenBudgetValue - run.tokensUsed : null;
2170
+ if (remainingTokens !== null && remainingTokens <= 0) {
808
2171
  await stopAutoContinueRun({ run, reason: "budget_exhausted" });
809
2172
  return;
810
2173
  }
811
2174
  // If the modeled slice exceeds the remaining budget, shrink the slice to fit rather than
812
2175
  // stopping immediately (Play should still dispatch at least the primary task when possible).
813
- if (tokenEstimate > 0 && tokenEstimate > remainingTokens) {
2176
+ if (remainingTokens !== null && tokenEstimate > 0 && tokenEstimate > remainingTokens) {
814
2177
  const nextSlice = [];
815
2178
  let hours = 0;
816
2179
  for (const task of sliceTaskNodes) {
@@ -832,12 +2195,300 @@ export function createAutoContinueEngine(deps) {
832
2195
  expectedDurationHours = hours;
833
2196
  tokenEstimate = estimateTokensForDurationHours(expectedDurationHours);
834
2197
  }
835
- if (tokenEstimate > 0 && tokenEstimate > remainingTokens) {
2198
+ if (remainingTokens !== null && tokenEstimate > 0 && tokenEstimate > remainingTokens) {
836
2199
  await stopAutoContinueRun({ run, reason: "budget_exhausted" });
837
2200
  return;
838
2201
  }
839
2202
  const executionPolicy = deriveExecutionPolicy(primaryTask, workstreamNode);
2203
+ const behaviorConfig = deriveBehaviorConfigContext(primaryTask, workstreamNode);
2204
+ const behaviorAutomationLevel = deriveBehaviorAutomationLevel(primaryTask, workstreamNode);
840
2205
  const sliceRunId = randomUUID();
2206
+ await emitActivitySafe({
2207
+ initiativeId: run.initiativeId,
2208
+ runId: sliceRunId,
2209
+ correlationId: sliceRunId,
2210
+ phase: "intent",
2211
+ level: "info",
2212
+ progressPct: 5,
2213
+ message: `Orchestrator selected ${workstreamTitle ?? selectedWorkstreamId} for the next slice.`,
2214
+ nextStep: `Preparing dispatch checks before spawning ${executionPolicy.domain} execution.`,
2215
+ metadata: {
2216
+ ...buildSliceEnrichment({
2217
+ run,
2218
+ taskId: primaryTask.id,
2219
+ taskTitle: primaryTask.title ?? null,
2220
+ workstreamId: selectedWorkstreamId,
2221
+ workstreamTitle: workstreamTitle ?? null,
2222
+ domain: executionPolicy.domain,
2223
+ requiredSkills: executionPolicy.requiredSkills,
2224
+ event: "orchestrator_dispatch",
2225
+ }),
2226
+ scope: run.scope,
2227
+ candidate_task_count: sliceTaskNodes.length,
2228
+ },
2229
+ });
2230
+ const behaviorConfigDrift = detectBehaviorConfigDrift({
2231
+ taskNode: primaryTask,
2232
+ workstreamNode,
2233
+ behaviorConfig,
2234
+ behaviorAutomationLevel,
2235
+ });
2236
+ if (behaviorConfigDrift) {
2237
+ await emitActivitySafe({
2238
+ initiativeId: run.initiativeId,
2239
+ runId: sliceRunId,
2240
+ correlationId: sliceRunId,
2241
+ phase: "review",
2242
+ level: "warn",
2243
+ progressPct: 15,
2244
+ message: `Behavior config drift detected for ${workstreamTitle ?? selectedWorkstreamId}; ` +
2245
+ `runtime behavior differs from declared workstream config.`,
2246
+ metadata: {
2247
+ ...buildSliceEnrichment({
2248
+ run,
2249
+ taskId: primaryTask.id,
2250
+ taskTitle: primaryTask.title ?? null,
2251
+ workstreamId: selectedWorkstreamId,
2252
+ workstreamTitle: workstreamTitle ?? null,
2253
+ domain: executionPolicy.domain,
2254
+ requiredSkills: executionPolicy.requiredSkills,
2255
+ event: "auto_continue_behavior_config_drift_detected",
2256
+ }),
2257
+ drift_fields: behaviorConfigDrift.fields,
2258
+ declared_behavior_config_id: behaviorConfigDrift.declared.configId,
2259
+ declared_behavior_config_version: behaviorConfigDrift.declared.version,
2260
+ declared_behavior_config_hash: behaviorConfigDrift.declared.hash,
2261
+ declared_policy_source: behaviorConfigDrift.declared.policySource,
2262
+ declared_behavior_context: behaviorConfigDrift.declared.context,
2263
+ declared_behavior_automation_level: behaviorConfigDrift.declared.automationLevel,
2264
+ runtime_behavior_config_id: behaviorConfigDrift.runtime.configId,
2265
+ runtime_behavior_config_version: behaviorConfigDrift.runtime.version,
2266
+ runtime_behavior_config_hash: behaviorConfigDrift.runtime.hash,
2267
+ runtime_policy_source: behaviorConfigDrift.runtime.policySource,
2268
+ runtime_behavior_context: behaviorConfigDrift.runtime.context,
2269
+ runtime_behavior_automation_level: behaviorConfigDrift.runtime.automationLevel,
2270
+ error_location: "mission-control.auto-continue.engine.behavior-config.drift",
2271
+ },
2272
+ nextStep: "Review task/workstream behavior metadata and reconcile the declared config if override is unintended.",
2273
+ });
2274
+ }
2275
+ if (behaviorConfig.requiresApproval) {
2276
+ const blockedReason = `Behavior config approval required before dispatch for ${workstreamTitle ?? selectedWorkstreamId}.`;
2277
+ await emitActivitySafe({
2278
+ initiativeId: run.initiativeId,
2279
+ runId: sliceRunId,
2280
+ correlationId: sliceRunId,
2281
+ phase: "blocked",
2282
+ level: "warn",
2283
+ progressPct: 20,
2284
+ message: blockedReason,
2285
+ metadata: {
2286
+ ...buildSliceEnrichment({
2287
+ run,
2288
+ taskId: primaryTask.id,
2289
+ taskTitle: primaryTask.title ?? null,
2290
+ workstreamId: selectedWorkstreamId,
2291
+ workstreamTitle: workstreamTitle ?? null,
2292
+ domain: executionPolicy.domain,
2293
+ requiredSkills: executionPolicy.requiredSkills,
2294
+ event: "auto_continue_behavior_config_approval_required",
2295
+ }),
2296
+ behavior_config_id: behaviorConfig.configId,
2297
+ behavior_config_version: behaviorConfig.version,
2298
+ behavior_config_hash: behaviorConfig.hash,
2299
+ behavior_approval_status: behaviorConfig.approvalStatus,
2300
+ behavior_approval_decision_id: behaviorConfig.approvalDecisionId,
2301
+ blocked_reason: blockedReason,
2302
+ error_location: "mission-control.auto-continue.engine.behavior-config.approval",
2303
+ },
2304
+ nextStep: "Approve the behavior config, then rerun Play/auto-continue for this workstream.",
2305
+ });
2306
+ const decisionResult = await requestDecisionQueued({
2307
+ initiativeId: run.initiativeId,
2308
+ correlationId: sliceRunId,
2309
+ title: `Approve behavior config for ${workstreamTitle ?? selectedWorkstreamId}`,
2310
+ summary: [
2311
+ `Autopilot paused before dispatch because behavior config requires approval.`,
2312
+ `Task: ${primaryTask.id}.`,
2313
+ behaviorConfig.configId ? `Config: ${behaviorConfig.configId}.` : "",
2314
+ behaviorConfig.version ? `Version: ${behaviorConfig.version}.` : "",
2315
+ behaviorConfig.approvalStatus ? `Approval status: ${behaviorConfig.approvalStatus}.` : "",
2316
+ ]
2317
+ .filter(Boolean)
2318
+ .join(" "),
2319
+ urgency: "high",
2320
+ options: [
2321
+ "Approve config and continue execution",
2322
+ "Reject config and revise policy",
2323
+ "Pause this workstream",
2324
+ ],
2325
+ blocking: true,
2326
+ decisionType: "autopilot_behavior_config_approval",
2327
+ workstreamId: selectedWorkstreamId,
2328
+ agentId: run.agentId,
2329
+ sourceSystem: "orgx-autopilot",
2330
+ conflictSource: "behavior_config_requires_approval",
2331
+ dedupeKey: [
2332
+ "autopilot",
2333
+ run.initiativeId,
2334
+ selectedWorkstreamId,
2335
+ "behavior_config_requires_approval",
2336
+ behaviorConfig.configId ?? "default",
2337
+ behaviorConfig.version ?? "unknown",
2338
+ ].join(":"),
2339
+ recommendedAction: "Resolve approval state before allowing autopilot to spawn a worker.",
2340
+ sourceRunId: sliceRunId,
2341
+ sourceRef: {
2342
+ run_id: sliceRunId,
2343
+ workstream_id: selectedWorkstreamId,
2344
+ task_id: primaryTask.id,
2345
+ behavior_config_id: behaviorConfig.configId,
2346
+ behavior_approval_status: behaviorConfig.approvalStatus,
2347
+ behavior_approval_decision_id: behaviorConfig.approvalDecisionId,
2348
+ },
2349
+ });
2350
+ if (!run.blockedWorkstreamIds.includes(selectedWorkstreamId)) {
2351
+ run.blockedWorkstreamIds.push(selectedWorkstreamId);
2352
+ }
2353
+ setLaneState(run, {
2354
+ workstreamId: selectedWorkstreamId,
2355
+ state: "blocked",
2356
+ activeRunId: null,
2357
+ activeTaskIds: [],
2358
+ blockedReason,
2359
+ waitingOnWorkstreamIds: [],
2360
+ retryAt: null,
2361
+ });
2362
+ await stopAutoContinueRun({
2363
+ run,
2364
+ reason: "blocked",
2365
+ error: blockedReason,
2366
+ decisionRequired: decisionResult.queued,
2367
+ decisionIds: decisionResult.decisionIds,
2368
+ });
2369
+ return;
2370
+ }
2371
+ const isManualPlayDispatch = run.stopAfterSlice &&
2372
+ Array.isArray(run.allowedWorkstreamIds) &&
2373
+ run.allowedWorkstreamIds.length === 1;
2374
+ if (behaviorAutomationLevel === "manual" && !isManualPlayDispatch) {
2375
+ const blockedReason = `Automation level manual prevents auto-continue dispatch for ${workstreamTitle ?? selectedWorkstreamId}.`;
2376
+ await emitActivitySafe({
2377
+ initiativeId: run.initiativeId,
2378
+ runId: sliceRunId,
2379
+ correlationId: sliceRunId,
2380
+ phase: "blocked",
2381
+ level: "warn",
2382
+ progressPct: 20,
2383
+ message: blockedReason,
2384
+ metadata: {
2385
+ ...buildSliceEnrichment({
2386
+ run,
2387
+ taskId: primaryTask.id,
2388
+ taskTitle: primaryTask.title ?? null,
2389
+ workstreamId: selectedWorkstreamId,
2390
+ workstreamTitle: workstreamTitle ?? null,
2391
+ domain: executionPolicy.domain,
2392
+ requiredSkills: executionPolicy.requiredSkills,
2393
+ event: "auto_continue_behavior_automation_manual_blocked",
2394
+ }),
2395
+ behavior_config_id: behaviorConfig.configId,
2396
+ behavior_config_version: behaviorConfig.version,
2397
+ behavior_automation_level: behaviorAutomationLevel,
2398
+ blocked_reason: blockedReason,
2399
+ error_location: "mission-control.auto-continue.engine.behavior.automation.manual",
2400
+ },
2401
+ nextStep: "Use manual Play to dispatch this workstream slice.",
2402
+ });
2403
+ const decisionResult = await requestDecisionQueued({
2404
+ initiativeId: run.initiativeId,
2405
+ correlationId: sliceRunId,
2406
+ title: `Manual dispatch required for ${workstreamTitle ?? selectedWorkstreamId}`,
2407
+ summary: [
2408
+ "Autopilot paused because behavior automation level is manual.",
2409
+ `Task: ${primaryTask.id}.`,
2410
+ behaviorConfig.configId ? `Config: ${behaviorConfig.configId}.` : "",
2411
+ behaviorConfig.version ? `Version: ${behaviorConfig.version}.` : "",
2412
+ ]
2413
+ .filter(Boolean)
2414
+ .join(" "),
2415
+ urgency: "high",
2416
+ options: [
2417
+ "Dispatch this workstream manually now",
2418
+ "Switch automation level to supervised",
2419
+ "Switch automation level to auto",
2420
+ ],
2421
+ blocking: true,
2422
+ decisionType: "autopilot_behavior_manual_dispatch_required",
2423
+ workstreamId: selectedWorkstreamId,
2424
+ agentId: run.agentId,
2425
+ sourceSystem: "orgx-autopilot",
2426
+ conflictSource: "behavior_automation_level_manual",
2427
+ dedupeKey: [
2428
+ "autopilot",
2429
+ run.initiativeId,
2430
+ selectedWorkstreamId,
2431
+ "behavior_automation_level_manual",
2432
+ behaviorConfig.configId ?? "default",
2433
+ behaviorConfig.version ?? "unknown",
2434
+ ].join(":"),
2435
+ recommendedAction: "Dispatch manually for this workstream, or switch behavior automation level before rerunning auto-continue.",
2436
+ sourceRunId: sliceRunId,
2437
+ sourceRef: {
2438
+ run_id: sliceRunId,
2439
+ workstream_id: selectedWorkstreamId,
2440
+ task_id: primaryTask.id,
2441
+ behavior_config_id: behaviorConfig.configId,
2442
+ behavior_automation_level: behaviorAutomationLevel,
2443
+ },
2444
+ });
2445
+ if (!run.blockedWorkstreamIds.includes(selectedWorkstreamId)) {
2446
+ run.blockedWorkstreamIds.push(selectedWorkstreamId);
2447
+ }
2448
+ setLaneState(run, {
2449
+ workstreamId: selectedWorkstreamId,
2450
+ state: "blocked",
2451
+ activeRunId: null,
2452
+ activeTaskIds: [],
2453
+ blockedReason,
2454
+ waitingOnWorkstreamIds: [],
2455
+ retryAt: null,
2456
+ });
2457
+ await stopAutoContinueRun({
2458
+ run,
2459
+ reason: "blocked",
2460
+ error: blockedReason,
2461
+ decisionRequired: decisionResult.queued,
2462
+ decisionIds: decisionResult.decisionIds,
2463
+ });
2464
+ return;
2465
+ }
2466
+ if (behaviorAutomationLevel === "supervised" && !run.stopAfterSlice) {
2467
+ run.stopAfterSlice = true;
2468
+ await emitActivitySafe({
2469
+ initiativeId: run.initiativeId,
2470
+ runId: sliceRunId,
2471
+ correlationId: sliceRunId,
2472
+ phase: "execution",
2473
+ level: "info",
2474
+ progressPct: 25,
2475
+ message: `Supervised automation level: dispatching one slice for ${workstreamTitle ?? selectedWorkstreamId}.`,
2476
+ metadata: {
2477
+ ...buildSliceEnrichment({
2478
+ run,
2479
+ taskId: primaryTask.id,
2480
+ taskTitle: primaryTask.title ?? null,
2481
+ workstreamId: selectedWorkstreamId,
2482
+ workstreamTitle: workstreamTitle ?? null,
2483
+ domain: executionPolicy.domain,
2484
+ requiredSkills: executionPolicy.requiredSkills,
2485
+ event: "auto_continue_behavior_automation_supervised_one_shot",
2486
+ }),
2487
+ behavior_automation_level: behaviorAutomationLevel,
2488
+ },
2489
+ nextStep: "Resume to dispatch the next slice after this one completes.",
2490
+ });
2491
+ }
841
2492
  const spawnGuardResult = await checkSpawnGuardSafe({
842
2493
  domain: executionPolicy.domain,
843
2494
  taskId: primaryTask.id,
@@ -850,60 +2501,224 @@ export function createAutoContinueEngine(deps) {
850
2501
  const allowed = spawnGuardResult.allowed;
851
2502
  if (allowed === false) {
852
2503
  const blockedReason = summarizeSpawnGuardBlockReason(spawnGuardResult);
853
- // Maintain existing behavior: mark the primary task blocked when a quality gate denies dispatch.
854
- try {
855
- await client.updateEntity("task", primaryTask.id, { status: "blocked" });
856
- }
857
- catch {
858
- // best effort
859
- }
860
- try {
861
- await syncParentRollupsForTask({
2504
+ const retryable = spawnGuardIsRateLimited(spawnGuardResult);
2505
+ const rateLimitOverrideRequested = retryable && run.ignoreSpawnGuardRateLimit;
2506
+ if (retryable && !rateLimitOverrideRequested) {
2507
+ const retryAtMs = Date.now() + AUTO_CONTINUE_SPAWN_GUARD_RETRY_MS;
2508
+ const retryAtIso = new Date(retryAtMs).toISOString();
2509
+ autoContinueSpawnGuardRetryByTask.set(primaryTask.id, {
862
2510
  initiativeId: run.initiativeId,
863
- taskId: primaryTask.id,
2511
+ retryAtMs,
2512
+ });
2513
+ setLaneState(run, {
864
2514
  workstreamId: selectedWorkstreamId,
865
- milestoneId: primaryTask.milestoneId,
2515
+ state: "rate_limited",
2516
+ activeRunId: null,
2517
+ activeTaskIds: [],
2518
+ blockedReason,
2519
+ waitingOnWorkstreamIds: [],
2520
+ retryAt: retryAtIso,
2521
+ });
2522
+ await emitActivitySafe({
2523
+ initiativeId: run.initiativeId,
2524
+ runId: sliceRunId,
866
2525
  correlationId: sliceRunId,
2526
+ phase: "blocked",
2527
+ level: "warn",
2528
+ progressPct: 25,
2529
+ message: `Autopilot spawn guard rate-limited ${workstreamTitle ?? selectedWorkstreamId}; retrying shortly.`,
2530
+ metadata: {
2531
+ ...buildSliceEnrichment({
2532
+ run,
2533
+ taskId: primaryTask.id,
2534
+ taskTitle: primaryTask.title ?? null,
2535
+ workstreamId: selectedWorkstreamId,
2536
+ workstreamTitle: workstreamTitle ?? null,
2537
+ domain: executionPolicy.domain,
2538
+ requiredSkills: executionPolicy.requiredSkills,
2539
+ event: "auto_continue_spawn_guard_rate_limited",
2540
+ }),
2541
+ blocked_reason: blockedReason,
2542
+ error_location: "mission-control.auto-continue.engine.spawn-guard.rate-limited",
2543
+ next_retry_at: retryAtIso,
2544
+ next_retry_in_ms: AUTO_CONTINUE_SPAWN_GUARD_RETRY_MS,
2545
+ spawn_guard: spawnGuardResult,
2546
+ },
2547
+ nextStep: "Retry dispatch when spawn rate limits recover.",
867
2548
  });
2549
+ run.lastError = blockedReason;
2550
+ run.updatedAt = now;
2551
+ syncLegacyRunPointers(run);
2552
+ try {
2553
+ await updateInitiativeAutoContinueState({
2554
+ initiativeId: run.initiativeId,
2555
+ run,
2556
+ });
2557
+ }
2558
+ catch {
2559
+ // best effort
2560
+ }
2561
+ return;
868
2562
  }
869
- catch {
870
- // best effort
2563
+ if (rateLimitOverrideRequested) {
2564
+ const overrideMode = run.stopAfterSlice &&
2565
+ Array.isArray(run.allowedWorkstreamIds) &&
2566
+ run.allowedWorkstreamIds.length === 1
2567
+ ? "Play"
2568
+ : "Auto-continue";
2569
+ await emitActivitySafe({
2570
+ initiativeId: run.initiativeId,
2571
+ runId: sliceRunId,
2572
+ correlationId: sliceRunId,
2573
+ phase: "execution",
2574
+ level: "warn",
2575
+ progressPct: 25,
2576
+ message: `${overrideMode} override: dispatching ${workstreamTitle ?? selectedWorkstreamId} despite spawn guard rate limit.`,
2577
+ metadata: {
2578
+ ...buildSliceEnrichment({
2579
+ run,
2580
+ taskId: primaryTask.id,
2581
+ taskTitle: primaryTask.title ?? null,
2582
+ workstreamId: selectedWorkstreamId,
2583
+ workstreamTitle: workstreamTitle ?? null,
2584
+ domain: executionPolicy.domain,
2585
+ requiredSkills: executionPolicy.requiredSkills,
2586
+ event: "auto_continue_spawn_guard_rate_limit_overridden",
2587
+ }),
2588
+ blocked_reason: blockedReason,
2589
+ error_location: "mission-control.auto-continue.engine.spawn-guard.override",
2590
+ spawn_guard: spawnGuardResult,
2591
+ },
2592
+ nextStep: "Manual Play requested immediate execution for this single workstream slice.",
2593
+ });
2594
+ run.lastError = null;
2595
+ run.updatedAt = now;
2596
+ setLaneState(run, {
2597
+ workstreamId: selectedWorkstreamId,
2598
+ state: "idle",
2599
+ activeRunId: null,
2600
+ activeTaskIds: [],
2601
+ blockedReason: null,
2602
+ waitingOnWorkstreamIds: [],
2603
+ retryAt: null,
2604
+ });
2605
+ }
2606
+ else {
2607
+ // Maintain existing behavior: mark the primary task blocked when a quality gate denies dispatch.
2608
+ try {
2609
+ await client.updateEntity("task", primaryTask.id, { status: "blocked" });
2610
+ }
2611
+ catch {
2612
+ // best effort
2613
+ }
2614
+ try {
2615
+ await syncParentRollupsForTask({
2616
+ initiativeId: run.initiativeId,
2617
+ taskId: primaryTask.id,
2618
+ workstreamId: selectedWorkstreamId,
2619
+ milestoneId: primaryTask.milestoneId,
2620
+ correlationId: sliceRunId,
2621
+ });
2622
+ }
2623
+ catch {
2624
+ // best effort
2625
+ }
2626
+ await emitActivitySafe({
2627
+ initiativeId: run.initiativeId,
2628
+ runId: sliceRunId,
2629
+ correlationId: sliceRunId,
2630
+ phase: "blocked",
2631
+ level: "error",
2632
+ progressPct: 25,
2633
+ message: `Autopilot blocked by spawn guard for ${workstreamTitle ?? selectedWorkstreamId}.`,
2634
+ metadata: {
2635
+ ...buildSliceEnrichment({
2636
+ run,
2637
+ taskId: primaryTask.id,
2638
+ taskTitle: primaryTask.title ?? null,
2639
+ workstreamId: selectedWorkstreamId,
2640
+ workstreamTitle: workstreamTitle ?? null,
2641
+ domain: executionPolicy.domain,
2642
+ requiredSkills: executionPolicy.requiredSkills,
2643
+ event: "auto_continue_spawn_guard_blocked",
2644
+ }),
2645
+ blocked_reason: blockedReason,
2646
+ error_location: "mission-control.auto-continue.engine.spawn-guard.blocked",
2647
+ spawn_guard: spawnGuardResult,
2648
+ },
2649
+ });
2650
+ const decisionResult = await requestDecisionQueued({
2651
+ initiativeId: run.initiativeId,
2652
+ correlationId: sliceRunId,
2653
+ title: `Unblock autopilot for ${workstreamTitle ?? selectedWorkstreamId}`,
2654
+ summary: [
2655
+ `Spawn guard denied dispatch for primary task ${primaryTask.id}.`,
2656
+ `Reason: ${blockedReason}`,
2657
+ `Domain: ${executionPolicy.domain}`,
2658
+ `Required skills: ${executionPolicy.requiredSkills.join(", ")}`,
2659
+ ].join(" "),
2660
+ urgency: "high",
2661
+ options: [
2662
+ "Approve exception and continue",
2663
+ "Reassign slice/domain",
2664
+ "Pause and investigate quality gate",
2665
+ ],
2666
+ blocking: true,
2667
+ decisionType: "autopilot_spawn_guard_block",
2668
+ workstreamId: selectedWorkstreamId,
2669
+ agentId: run.agentId,
2670
+ sourceSystem: "orgx-autopilot",
2671
+ conflictSource: "spawn_guard_blocked",
2672
+ dedupeKey: [
2673
+ "autopilot",
2674
+ run.initiativeId,
2675
+ selectedWorkstreamId,
2676
+ "spawn_guard_blocked",
2677
+ executionPolicy.domain,
2678
+ ].join(":"),
2679
+ recommendedAction: "Choose exception, reassignment, or pause so dispatch can proceed safely.",
2680
+ sourceRunId: sliceRunId,
2681
+ sourceRef: {
2682
+ run_id: sliceRunId,
2683
+ workstream_id: selectedWorkstreamId,
2684
+ task_id: primaryTask.id,
2685
+ domain: executionPolicy.domain,
2686
+ },
2687
+ evidenceRefs: [
2688
+ {
2689
+ evidence_type: "spawn_guard_result",
2690
+ title: "Spawn guard denied dispatch",
2691
+ summary: blockedReason,
2692
+ source_pointer: null,
2693
+ payload: {
2694
+ spawn_guard: spawnGuardResult,
2695
+ task_id: primaryTask.id,
2696
+ domain: executionPolicy.domain,
2697
+ },
2698
+ },
2699
+ ],
2700
+ });
2701
+ if (!run.blockedWorkstreamIds.includes(selectedWorkstreamId)) {
2702
+ run.blockedWorkstreamIds.push(selectedWorkstreamId);
2703
+ }
2704
+ setLaneState(run, {
2705
+ workstreamId: selectedWorkstreamId,
2706
+ state: "blocked",
2707
+ activeRunId: null,
2708
+ activeTaskIds: [],
2709
+ blockedReason,
2710
+ waitingOnWorkstreamIds: [],
2711
+ retryAt: null,
2712
+ });
2713
+ await stopAutoContinueRun({
2714
+ run,
2715
+ reason: "blocked",
2716
+ error: blockedReason,
2717
+ decisionRequired: decisionResult.queued,
2718
+ decisionIds: decisionResult.decisionIds,
2719
+ });
2720
+ return;
871
2721
  }
872
- await emitActivitySafe({
873
- initiativeId: run.initiativeId,
874
- runId: sliceRunId,
875
- correlationId: sliceRunId,
876
- phase: "blocked",
877
- level: "error",
878
- message: `Autopilot blocked by spawn guard for ${workstreamTitle ?? selectedWorkstreamId}.`,
879
- metadata: {
880
- event: "auto_continue_spawn_guard_blocked",
881
- task_id: primaryTask.id,
882
- workstream_id: selectedWorkstreamId,
883
- blocked_reason: blockedReason,
884
- spawn_guard: spawnGuardResult,
885
- },
886
- });
887
- await requestDecisionSafe({
888
- initiativeId: run.initiativeId,
889
- correlationId: sliceRunId,
890
- title: `Unblock autopilot for ${workstreamTitle ?? selectedWorkstreamId}`,
891
- summary: [
892
- `Spawn guard denied dispatch for primary task ${primaryTask.id}.`,
893
- `Reason: ${blockedReason}`,
894
- `Domain: ${executionPolicy.domain}`,
895
- `Required skills: ${executionPolicy.requiredSkills.join(", ")}`,
896
- ].join(" "),
897
- urgency: "high",
898
- options: [
899
- "Approve exception and continue",
900
- "Reassign slice/domain",
901
- "Pause and investigate quality gate",
902
- ],
903
- blocking: true,
904
- });
905
- await stopAutoContinueRun({ run, reason: "blocked", error: blockedReason });
906
- return;
907
2722
  }
908
2723
  }
909
2724
  const milestoneIds = dedupeStrings(cappedSliceTaskNodes.map((t) => (t.milestoneId ?? "").trim()).filter(Boolean));
@@ -918,25 +2733,92 @@ export function createAutoContinueEngine(deps) {
918
2733
  milestoneId: t.milestoneId ?? null,
919
2734
  }));
920
2735
  const schemaPath = ensureAutopilotSliceSchemaPath(AUTO_CONTINUE_SLICE_SCHEMA_FILENAME);
921
- const prompt = buildWorkstreamSlicePrompt({
922
- initiativeTitle,
923
- initiativeId: run.initiativeId,
924
- workstreamId: selectedWorkstreamId,
925
- workstreamTitle: workstreamTitle ?? `Workstream ${selectedWorkstreamId.slice(0, 8)}`,
926
- milestoneSummaries,
927
- taskSummaries,
928
- executionPolicy,
929
- runId: sliceRunId,
930
- schemaPath,
931
- });
2736
+ // Try server KickoffContext (includes team context, acceptance criteria, etc.)
2737
+ let prompt;
2738
+ let kickoffContextHash = null;
2739
+ if (fetchKickoffContextSafeFn && renderKickoffMessageFn) {
2740
+ let kickoff = null;
2741
+ try {
2742
+ kickoff = await fetchKickoffContextSafeFn(client, {
2743
+ initiative_id: run.initiativeId,
2744
+ workstream_id: selectedWorkstreamId,
2745
+ task_id: primaryTask.id,
2746
+ domain: executionPolicy.domain,
2747
+ required_skills: executionPolicy.requiredSkills,
2748
+ agent_id: resolveOrgxAgentForDomain(executionPolicy.domain).id,
2749
+ });
2750
+ }
2751
+ catch {
2752
+ // best effort: fall back to local prompt
2753
+ }
2754
+ if (kickoff) {
2755
+ const rendered = renderKickoffMessageFn({
2756
+ baseMessage: `Execute workstream slice for ${workstreamTitle ?? selectedWorkstreamId}`,
2757
+ kickoff,
2758
+ domain: executionPolicy.domain,
2759
+ requiredSkills: executionPolicy.requiredSkills,
2760
+ });
2761
+ const sliceInstructions = buildSliceOutputInstructions({
2762
+ runId: sliceRunId,
2763
+ schemaPath,
2764
+ requiredSkills: executionPolicy.requiredSkills,
2765
+ });
2766
+ prompt = rendered.message + "\n\n" + sliceInstructions;
2767
+ kickoffContextHash = rendered.contextHash;
2768
+ }
2769
+ else {
2770
+ // Fallback: existing local prompt (offline/degraded mode)
2771
+ prompt = buildWorkstreamSlicePrompt({
2772
+ initiativeTitle,
2773
+ initiativeId: run.initiativeId,
2774
+ workstreamId: selectedWorkstreamId,
2775
+ workstreamTitle: workstreamTitle ?? `Workstream ${selectedWorkstreamId.slice(0, 8)}`,
2776
+ milestoneSummaries,
2777
+ taskSummaries,
2778
+ executionPolicy,
2779
+ behaviorConfig,
2780
+ runId: sliceRunId,
2781
+ schemaPath,
2782
+ });
2783
+ }
2784
+ }
2785
+ else {
2786
+ // No KickoffContext functions available: use local prompt
2787
+ prompt = buildWorkstreamSlicePrompt({
2788
+ initiativeTitle,
2789
+ initiativeId: run.initiativeId,
2790
+ workstreamId: selectedWorkstreamId,
2791
+ workstreamTitle: workstreamTitle ?? `Workstream ${selectedWorkstreamId.slice(0, 8)}`,
2792
+ milestoneSummaries,
2793
+ taskSummaries,
2794
+ executionPolicy,
2795
+ behaviorConfig,
2796
+ runId: sliceRunId,
2797
+ schemaPath,
2798
+ });
2799
+ }
2800
+ // Append per-scope directive for milestone/workstream scopes.
2801
+ if (run.scope !== "task") {
2802
+ const msNodes = scopeMilestoneIds
2803
+ .map((id) => nodeById.get(id))
2804
+ .filter((n) => Boolean(n));
2805
+ const scopeDirective = buildScopeDirective(run.scope, {
2806
+ milestoneTitles: msNodes.map((n) => n.title),
2807
+ workstreamTitle: workstreamTitle ?? undefined,
2808
+ taskCount: cappedSliceTaskNodes.length,
2809
+ });
2810
+ if (scopeDirective) {
2811
+ prompt = prompt + "\n\n" + scopeDirective;
2812
+ }
2813
+ }
932
2814
  const logsDir = join(getOrgxPluginConfigDir(), AUTO_CONTINUE_SLICE_LOG_DIRNAME);
933
2815
  const logPath = join(logsDir, `${sliceRunId}.log`);
934
2816
  const outputPath = join(logsDir, `${sliceRunId}.output.json`);
935
- let workerCwd = (process.env.ORGX_AUTOPILOT_CWD ?? "").trim() || process.cwd();
936
- // LaunchAgents often start with cwd="/". Prefer a stable, user-owned directory
937
- // so relative paths and codex sandboxing behave consistently.
2817
+ const configuredWorkerCwd = (process.env.ORGX_AUTOPILOT_CWD ?? "").trim();
2818
+ let workerCwd = configuredWorkerCwd || resolveAutopilotDefaultCwd(__filename);
2819
+ // LaunchAgents sometimes start with cwd="/". Fall back to plugin root (or home if unresolved).
938
2820
  if (!workerCwd || workerCwd === "/") {
939
- workerCwd = homedir();
2821
+ workerCwd = resolveAutopilotDefaultCwd(__filename);
940
2822
  }
941
2823
  const sliceAgent = resolveOrgxAgentForDomain(executionPolicy.domain);
942
2824
  const workerKind = (process.env.ORGX_AUTOPILOT_WORKER_KIND ?? "").trim().toLowerCase();
@@ -960,6 +2842,7 @@ export function createAutoContinueEngine(deps) {
960
2842
  cwd: workerCwd,
961
2843
  logPath,
962
2844
  outputPath,
2845
+ outputSchemaPath: schemaPath,
963
2846
  env: {
964
2847
  ORGX_SOURCE_CLIENT: executorSourceClient,
965
2848
  ORGX_RUN_ID: sliceRunId,
@@ -968,8 +2851,16 @@ export function createAutoContinueEngine(deps) {
968
2851
  ORGX_WORKSTREAM_ID: selectedWorkstreamId,
969
2852
  ORGX_WORKSTREAM_TITLE: workstreamTitle ?? undefined,
970
2853
  ORGX_TASK_ID: primaryTask.id,
2854
+ ORGX_REQUIRED_SKILLS: executionPolicy.requiredSkills.join(","),
2855
+ ORGX_BEHAVIOR_CONFIG_ID: behaviorConfig.configId ?? undefined,
2856
+ ORGX_BEHAVIOR_CONFIG_VERSION: behaviorConfig.version ?? undefined,
2857
+ ORGX_BEHAVIOR_CONFIG_HASH: behaviorConfig.hash ?? undefined,
2858
+ ORGX_POLICY_SOURCE: behaviorConfig.policySource ?? undefined,
2859
+ ORGX_AUTOMATION_LEVEL: behaviorAutomationLevel,
2860
+ ORGX_BEHAVIOR_CONTEXT: behaviorConfig.context ?? undefined,
971
2861
  ORGX_AGENT_ID: sliceAgent.id,
972
2862
  ORGX_AGENT_NAME: sliceAgent.name,
2863
+ ORGX_KICKOFF_CONTEXT_HASH: kickoffContextHash ?? undefined,
973
2864
  ORGX_OUTPUT_PATH: outputPath,
974
2865
  ORGX_RUNTIME_HOOK_URL: runtimeHookUrl ?? undefined,
975
2866
  ORGX_HOOK_TOKEN: runtimeHookToken ?? undefined,
@@ -985,6 +2876,11 @@ export function createAutoContinueEngine(deps) {
985
2876
  agentName: sliceAgent.name,
986
2877
  domain: executionPolicy.domain,
987
2878
  requiredSkills: executionPolicy.requiredSkills,
2879
+ behaviorConfigId: behaviorConfig.configId,
2880
+ behaviorConfigVersion: behaviorConfig.version,
2881
+ behaviorConfigHash: behaviorConfig.hash,
2882
+ behaviorPolicySource: behaviorConfig.policySource,
2883
+ behaviorAutomationLevel,
988
2884
  sourceClient: executorSourceClient,
989
2885
  pid: spawned.pid,
990
2886
  status: "running",
@@ -996,7 +2892,10 @@ export function createAutoContinueEngine(deps) {
996
2892
  logPath,
997
2893
  taskIds: cappedSliceTaskNodes.map((t) => t.id),
998
2894
  milestoneIds,
2895
+ scope: run.scope,
2896
+ scopeMilestoneIds: scopeMilestoneIds,
999
2897
  lastError: null,
2898
+ isMockWorker: workerKind === "mock",
1000
2899
  };
1001
2900
  autoContinueSliceRuns.set(sliceRunId, slice);
1002
2901
  try {
@@ -1013,15 +2912,28 @@ export function createAutoContinueEngine(deps) {
1013
2912
  message: `Autopilot slice started: ${workstreamTitle ?? selectedWorkstreamId}`,
1014
2913
  metadata: {
1015
2914
  event: "autopilot_slice_started",
2915
+ initiative_id: run.initiativeId,
2916
+ run_id: sliceRunId,
2917
+ slice_run_id: sliceRunId,
2918
+ workstream_id: selectedWorkstreamId,
2919
+ correlation_id: sliceRunId,
1016
2920
  requested_by_agent_id: run.agentId,
1017
2921
  requested_by_agent_name: run.agentName,
1018
2922
  domain: executionPolicy.domain,
1019
2923
  required_skills: executionPolicy.requiredSkills,
2924
+ behavior_config_id: behaviorConfig.configId,
2925
+ behavior_config_version: behaviorConfig.version,
2926
+ behavior_config_hash: behaviorConfig.hash,
2927
+ policy_source: behaviorConfig.policySource,
2928
+ behavior_automation_level: behaviorAutomationLevel,
1020
2929
  task_ids: slice.taskIds,
1021
2930
  initiative_title: initiativeTitle ?? null,
1022
2931
  workstream_title: workstreamTitle ?? null,
2932
+ scope: slice.scope,
2933
+ scope_milestone_ids: slice.scopeMilestoneIds,
1023
2934
  log_path: logPath,
1024
2935
  output_path: outputPath,
2936
+ ...mockMeta(slice),
1025
2937
  },
1026
2938
  });
1027
2939
  }
@@ -1033,24 +2945,34 @@ export function createAutoContinueEngine(deps) {
1033
2945
  initiativeId: run.initiativeId,
1034
2946
  runId: sliceRunId,
1035
2947
  correlationId: sliceRunId,
2948
+ progressPct: 10,
2949
+ nextStep: `Worker ${sliceAgent.name} is executing ${workstreamTitle ?? selectedWorkstreamId}.`,
1036
2950
  phase: "execution",
1037
2951
  level: "info",
1038
2952
  message: `Autopilot dispatched slice for ${workstreamTitle ?? selectedWorkstreamId}.`,
1039
2953
  metadata: {
1040
- event: "autopilot_slice_dispatched",
1041
- requested_by_agent_id: run.agentId,
1042
- requested_by_agent_name: run.agentName,
1043
- agent_id: slice.agentId,
1044
- agent_name: sliceAgent.name,
1045
- domain: executionPolicy.domain,
1046
- required_skills: executionPolicy.requiredSkills,
2954
+ ...buildSliceEnrichment({
2955
+ run,
2956
+ slice,
2957
+ taskId: primaryTask.id,
2958
+ taskTitle: primaryTask.title ?? null,
2959
+ workstreamId: selectedWorkstreamId,
2960
+ workstreamTitle: workstreamTitle ?? null,
2961
+ domain: executionPolicy.domain,
2962
+ requiredSkills: executionPolicy.requiredSkills,
2963
+ event: "autopilot_slice_dispatched",
2964
+ }),
2965
+ behavior_config_id: behaviorConfig.configId,
2966
+ behavior_config_version: behaviorConfig.version,
2967
+ behavior_config_hash: behaviorConfig.hash,
2968
+ policy_source: behaviorConfig.policySource,
2969
+ behavior_automation_level: behaviorAutomationLevel,
1047
2970
  initiative_title: initiativeTitle ?? null,
1048
- workstream_id: selectedWorkstreamId,
1049
- workstream_title: workstreamTitle ?? null,
1050
- task_ids: slice.taskIds,
1051
- milestone_ids: milestoneIds,
2971
+ scope: slice.scope,
2972
+ scope_milestone_ids: slice.scopeMilestoneIds,
1052
2973
  log_path: logPath,
1053
2974
  output_path: outputPath,
2975
+ ...mockMeta(slice),
1054
2976
  },
1055
2977
  });
1056
2978
  upsertAgentContext({
@@ -1060,11 +2982,34 @@ export function createAutoContinueEngine(deps) {
1060
2982
  workstreamId: selectedWorkstreamId,
1061
2983
  taskId: primaryTask.id,
1062
2984
  });
2985
+ upsertRunContext({
2986
+ runId: sliceRunId,
2987
+ agentId: slice.agentId,
2988
+ initiativeId: run.initiativeId,
2989
+ initiativeTitle: initiativeTitle ?? null,
2990
+ workstreamId: selectedWorkstreamId,
2991
+ taskId: primaryTask.id,
2992
+ });
1063
2993
  run.lastTaskId = primaryTask.id;
1064
2994
  run.lastRunId = sliceRunId;
1065
- run.activeTaskId = primaryTask.id;
1066
- run.activeRunId = sliceRunId;
2995
+ run.activeSliceRunIds = dedupeStrings([
2996
+ ...run.activeSliceRunIds,
2997
+ sliceRunId,
2998
+ ]);
2999
+ run.activeTaskIds = dedupeStrings([...run.activeTaskIds, ...slice.taskIds]);
3000
+ setLaneState(run, {
3001
+ workstreamId: selectedWorkstreamId,
3002
+ state: "running",
3003
+ activeRunId: sliceRunId,
3004
+ activeTaskIds: slice.taskIds,
3005
+ blockedReason: null,
3006
+ waitingOnWorkstreamIds: [],
3007
+ retryAt: null,
3008
+ });
1067
3009
  run.activeTaskTokenEstimate = tokenEstimate > 0 ? tokenEstimate : null;
3010
+ syncLegacyRunPointers(run);
3011
+ // Clear stale errors when a new slice dispatches successfully.
3012
+ run.lastError = null;
1068
3013
  run.updatedAt = now;
1069
3014
  try {
1070
3015
  await client.updateEntity("initiative", run.initiativeId, { status: "active" });
@@ -1095,7 +3040,7 @@ export function createAutoContinueEngine(deps) {
1095
3040
  }
1096
3041
  catch (err) {
1097
3042
  // Never let one loop crash the whole handler.
1098
- run.lastError = safeErrorMessage(err);
3043
+ run.lastError = `[mission-control.auto-continue.engine.tick-all] ${safeErrorMessage(err)}`;
1099
3044
  run.updatedAt = new Date().toISOString();
1100
3045
  await stopAutoContinueRun({ run, reason: "error", error: run.lastError });
1101
3046
  }
@@ -1123,12 +3068,319 @@ export function createAutoContinueEngine(deps) {
1123
3068
  const run = autoContinueRuns.get(initiativeId) ?? null;
1124
3069
  if (!run)
1125
3070
  return null;
3071
+ ensureRunInternals(run);
1126
3072
  if (run.status !== "running" && run.status !== "stopping")
1127
3073
  return null;
1128
- if (!Array.isArray(run.allowedWorkstreamIds) || run.allowedWorkstreamIds.length === 0) {
3074
+ if (Array.isArray(run.allowedWorkstreamIds) &&
3075
+ run.allowedWorkstreamIds.length > 0 &&
3076
+ !run.allowedWorkstreamIds.includes(workstreamId)) {
3077
+ return null;
3078
+ }
3079
+ const lane = run.laneByWorkstreamId[workstreamId] ?? null;
3080
+ if (lane &&
3081
+ (lane.state === "running" ||
3082
+ lane.state === "blocked" ||
3083
+ lane.state === "waiting_dependency" ||
3084
+ lane.state === "rate_limited")) {
3085
+ return run;
3086
+ }
3087
+ if (Array.isArray(run.allowedWorkstreamIds) &&
3088
+ run.allowedWorkstreamIds.length > 0 &&
3089
+ run.allowedWorkstreamIds.includes(workstreamId) &&
3090
+ (run.status === "running" || run.status === "stopping")) {
1129
3091
  return run;
1130
3092
  }
1131
- return run.allowedWorkstreamIds.includes(workstreamId) ? run : null;
3093
+ return null;
3094
+ }
3095
+ function getAutoContinueLaneForWorkstream(initiativeId, workstreamId) {
3096
+ const run = autoContinueRuns.get(initiativeId) ?? null;
3097
+ if (!run)
3098
+ return null;
3099
+ ensureRunInternals(run);
3100
+ return run.laneByWorkstreamId[workstreamId] ?? null;
3101
+ }
3102
+ async function scheduleAutoFixForWorkstream(input) {
3103
+ const initiativeId = input.initiativeId.trim();
3104
+ const workstreamId = input.workstreamId.trim();
3105
+ if (!initiativeId || !workstreamId) {
3106
+ throw new Error("initiativeId and workstreamId are required");
3107
+ }
3108
+ const runId = (input.runId ?? "").trim() || null;
3109
+ const sourceEvent = (input.event ?? "").trim() || null;
3110
+ const requestedByAgentId = (input.requestedByAgentId ?? "").trim() || null;
3111
+ const requestedByAgentName = (input.requestedByAgentName ?? "").trim() || null;
3112
+ const providedGraceMs = typeof input.graceMs === "number" && Number.isFinite(input.graceMs)
3113
+ ? Math.floor(input.graceMs)
3114
+ : null;
3115
+ const graceMs = Math.max(1_000, Math.min(120_000, providedGraceMs ?? AUTO_FIX_DEFAULT_GRACE_MS));
3116
+ const key = `${initiativeId}:${workstreamId}`;
3117
+ const existing = autoFixByScope.get(key);
3118
+ if (existing?.timer)
3119
+ clearTimeout(existing.timer);
3120
+ const scheduledAt = new Date().toISOString();
3121
+ const dueAt = new Date(Date.now() + graceMs).toISOString();
3122
+ const requestId = randomUUID();
3123
+ const resolveAutoFixRunContext = () => {
3124
+ const activeRun = autoContinueRuns.get(initiativeId) ?? null;
3125
+ return {
3126
+ initiativeId,
3127
+ agentId: activeRun?.agentId ?? requestedByAgentId ?? "main",
3128
+ agentName: activeRun?.agentName ?? requestedByAgentName ?? null,
3129
+ scope: activeRun?.scope ?? "task",
3130
+ };
3131
+ };
3132
+ const emitSkip = async (reason, details) => {
3133
+ await emitActivitySafe({
3134
+ initiativeId,
3135
+ runId: runId ?? undefined,
3136
+ correlationId: runId ?? undefined,
3137
+ phase: "review",
3138
+ level: reason === "error" ? "error" : "warn",
3139
+ message: reason === "paused_by_user"
3140
+ ? `Auto-fix skipped for ${workstreamId}: paused during grace window.`
3141
+ : reason === "already_running"
3142
+ ? `Auto-fix skipped for ${workstreamId}: workstream already running.`
3143
+ : reason === "missing_workstream"
3144
+ ? `Auto-fix skipped for ${workstreamId}: workstream data unavailable.`
3145
+ : reason === "missing_scope"
3146
+ ? `Auto-fix skipped: scope metadata was incomplete.`
3147
+ : `Auto-fix failed for ${workstreamId}.`,
3148
+ metadata: {
3149
+ ...buildSliceEnrichment({
3150
+ run: resolveAutoFixRunContext(),
3151
+ workstreamId,
3152
+ event: "autopilot_autofix_skipped",
3153
+ actionType: "auto_fix",
3154
+ }),
3155
+ reason,
3156
+ run_id: runId,
3157
+ source_event: sourceEvent,
3158
+ grace_ms: graceMs,
3159
+ request_id: requestId,
3160
+ scheduled_at: scheduledAt,
3161
+ due_at: dueAt,
3162
+ ...(details ?? {}),
3163
+ },
3164
+ });
3165
+ };
3166
+ const executeScheduledAutoFix = async () => {
3167
+ const pending = autoFixByScope.get(key);
3168
+ if (!pending || pending.requestId !== requestId)
3169
+ return;
3170
+ autoFixByScope.delete(key);
3171
+ const existingRun = autoContinueRuns.get(initiativeId) ?? null;
3172
+ if (existingRun &&
3173
+ (existingRun.stopRequested ||
3174
+ existingRun.status === "stopping" ||
3175
+ existingRun.stopReason === "stopped")) {
3176
+ await emitSkip("paused_by_user");
3177
+ return;
3178
+ }
3179
+ if (existingRun &&
3180
+ (existingRun.status === "running" || existingRun.status === "stopping") &&
3181
+ listActiveSliceRunIds(existingRun).length > 0) {
3182
+ const activeRunIds = listActiveSliceRunIds(existingRun);
3183
+ await emitSkip("already_running", {
3184
+ active_run_id: activeRunIds[0] ?? null,
3185
+ active_run_ids: activeRunIds,
3186
+ run_status: existingRun.status,
3187
+ });
3188
+ return;
3189
+ }
3190
+ let optionalDecisionsApproved = 0;
3191
+ if (decisionAutoResolveGuardedEnabled) {
3192
+ try {
3193
+ const decisionResult = await client.listEntities("decision", {
3194
+ initiative_id: initiativeId,
3195
+ status: "pending",
3196
+ limit: 500,
3197
+ });
3198
+ const decisionRows = Array.isArray(decisionResult?.data) ? decisionResult.data : [];
3199
+ for (const row of decisionRows) {
3200
+ if (!row || typeof row !== "object")
3201
+ continue;
3202
+ const record = row;
3203
+ const decisionId = pickString(record, ["id"])?.trim() ?? "";
3204
+ if (!decisionId)
3205
+ continue;
3206
+ if (!isPendingDecisionStatus(record.status ?? record.decision_status))
3207
+ continue;
3208
+ if (!decisionMatchesWorkstream(record, workstreamId, runId))
3209
+ continue;
3210
+ if (decisionIsBlocking(record))
3211
+ continue;
3212
+ const autoApprovalNote = "Auto-approved by OrgX auto-fix (non-blocking follow-up decision).";
3213
+ if (typeof client.decideDecision === "function") {
3214
+ await client.decideDecision(decisionId, "approve", { note: autoApprovalNote });
3215
+ }
3216
+ else {
3217
+ await client.updateEntity("decision", decisionId, {
3218
+ status: "approved",
3219
+ resolution_summary: autoApprovalNote,
3220
+ });
3221
+ }
3222
+ optionalDecisionsApproved += 1;
3223
+ }
3224
+ }
3225
+ catch {
3226
+ // best effort
3227
+ }
3228
+ }
3229
+ let resetTaskCount = 0;
3230
+ try {
3231
+ const taskResult = await client.listEntities("task", {
3232
+ initiative_id: initiativeId,
3233
+ workstream_id: workstreamId,
3234
+ limit: 100,
3235
+ });
3236
+ const taskRows = Array.isArray(taskResult?.data) ? taskResult.data : [];
3237
+ if (taskRows.length === 0) {
3238
+ await emitSkip("missing_workstream");
3239
+ return;
3240
+ }
3241
+ for (const row of taskRows) {
3242
+ if (!row || typeof row !== "object")
3243
+ continue;
3244
+ const record = row;
3245
+ const taskId = pickString(record, ["id"])?.trim() ?? "";
3246
+ if (!taskId)
3247
+ continue;
3248
+ const status = normalizeStatusValue(record.status);
3249
+ if (!status || status === "todo" || status === "done" || status === "completed") {
3250
+ continue;
3251
+ }
3252
+ const shouldReset = status === "in_progress" ||
3253
+ status === "inprogress" ||
3254
+ status === "active" ||
3255
+ status === "running" ||
3256
+ status === "working" ||
3257
+ status === "planning" ||
3258
+ status === "dispatching" ||
3259
+ status === "pending" ||
3260
+ status === "blocked" ||
3261
+ status === "stalled" ||
3262
+ status === "failed" ||
3263
+ status === "error";
3264
+ if (!shouldReset)
3265
+ continue;
3266
+ await client.updateEntity("task", taskId, { status: "todo" });
3267
+ resetTaskCount += 1;
3268
+ }
3269
+ }
3270
+ catch {
3271
+ // best effort
3272
+ }
3273
+ const latestRun = autoContinueRuns.get(initiativeId) ?? null;
3274
+ const dispatchAgentId = latestRun?.agentId ??
3275
+ requestedByAgentId ??
3276
+ "main";
3277
+ const dispatchAgentName = latestRun?.agentName ??
3278
+ requestedByAgentName ??
3279
+ null;
3280
+ const dispatchRun = await startAutoContinueRun({
3281
+ initiativeId,
3282
+ agentId: dispatchAgentId,
3283
+ agentName: dispatchAgentName,
3284
+ // Auto-fix retries should follow current defaults unless an operator explicitly
3285
+ // starts a run with a budget override.
3286
+ tokenBudget: null,
3287
+ includeVerification: latestRun?.includeVerification ?? false,
3288
+ allowedWorkstreamIds: [workstreamId],
3289
+ maxParallelSlices: 1,
3290
+ parallelMode: latestRun?.parallelMode ?? "iwmt",
3291
+ stopAfterSlice: true,
3292
+ ignoreSpawnGuardRateLimit: latestRun?.ignoreSpawnGuardRateLimit ?? false,
3293
+ });
3294
+ await tickAutoContinueRun(dispatchRun);
3295
+ await emitActivitySafe({
3296
+ initiativeId,
3297
+ runId: dispatchRun.activeRunId ?? runId ?? undefined,
3298
+ correlationId: dispatchRun.activeRunId ?? runId ?? undefined,
3299
+ phase: "execution",
3300
+ level: "info",
3301
+ message: `Auto-fix dispatched for ${workstreamId}.`,
3302
+ metadata: {
3303
+ ...buildSliceEnrichment({
3304
+ run: {
3305
+ initiativeId,
3306
+ agentId: dispatchAgentId,
3307
+ agentName: dispatchAgentName,
3308
+ scope: dispatchRun.scope,
3309
+ },
3310
+ workstreamId,
3311
+ event: "autopilot_autofix_executed",
3312
+ actionType: "auto_fix",
3313
+ }),
3314
+ source_event: sourceEvent,
3315
+ run_id: runId,
3316
+ grace_ms: graceMs,
3317
+ request_id: requestId,
3318
+ scheduled_at: scheduledAt,
3319
+ due_at: dueAt,
3320
+ optional_decisions_auto_approved: optionalDecisionsApproved,
3321
+ reset_task_count: resetTaskCount,
3322
+ dispatched_run_id: dispatchRun.activeRunId,
3323
+ dispatch_agent_id: dispatchAgentId,
3324
+ dispatch_agent_name: dispatchAgentName,
3325
+ },
3326
+ });
3327
+ };
3328
+ const pending = {
3329
+ requestId,
3330
+ key,
3331
+ initiativeId,
3332
+ workstreamId,
3333
+ runId,
3334
+ sourceEvent,
3335
+ requestedByAgentId,
3336
+ requestedByAgentName,
3337
+ graceMs,
3338
+ scheduledAt,
3339
+ dueAt,
3340
+ timer: null,
3341
+ };
3342
+ const timer = setTimeout(() => {
3343
+ void executeScheduledAutoFix().catch(async (err) => {
3344
+ autoFixByScope.delete(key);
3345
+ await emitSkip("error", {
3346
+ error: safeErrorMessage(err),
3347
+ });
3348
+ });
3349
+ }, graceMs);
3350
+ pending.timer = timer;
3351
+ autoFixByScope.set(key, pending);
3352
+ await emitActivitySafe({
3353
+ initiativeId,
3354
+ runId: runId ?? undefined,
3355
+ correlationId: runId ?? undefined,
3356
+ phase: "review",
3357
+ level: "info",
3358
+ message: `Auto-fix scheduled for ${workstreamId} in ${Math.round(graceMs / 1000)}s.`,
3359
+ metadata: {
3360
+ ...buildSliceEnrichment({
3361
+ run: resolveAutoFixRunContext(),
3362
+ workstreamId,
3363
+ event: "autopilot_autofix_scheduled",
3364
+ actionType: "auto_fix",
3365
+ }),
3366
+ source_event: sourceEvent,
3367
+ run_id: runId,
3368
+ grace_ms: graceMs,
3369
+ request_id: requestId,
3370
+ scheduled_at: scheduledAt,
3371
+ due_at: dueAt,
3372
+ },
3373
+ });
3374
+ return {
3375
+ requestId,
3376
+ initiativeId,
3377
+ workstreamId,
3378
+ runId,
3379
+ sourceEvent,
3380
+ graceMs,
3381
+ scheduledAt,
3382
+ dueAt,
3383
+ };
1132
3384
  }
1133
3385
  async function startAutoContinueRun(input) {
1134
3386
  const now = new Date().toISOString();
@@ -1142,6 +3394,10 @@ export function createAutoContinueEngine(deps) {
1142
3394
  includeVerification: false,
1143
3395
  allowedWorkstreamIds: null,
1144
3396
  stopAfterSlice: false,
3397
+ ignoreSpawnGuardRateLimit: false,
3398
+ maxParallelSlices: AUTO_CONTINUE_MAX_PARALLEL_DEFAULT,
3399
+ parallelMode: "iwmt",
3400
+ scope: "task",
1145
3401
  tokenBudget: defaultAutoContinueTokenBudget(),
1146
3402
  tokensUsed: 0,
1147
3403
  status: "running",
@@ -1153,10 +3409,15 @@ export function createAutoContinueEngine(deps) {
1153
3409
  lastError: null,
1154
3410
  lastTaskId: null,
1155
3411
  lastRunId: null,
3412
+ activeSliceRunIds: [],
3413
+ activeTaskIds: [],
3414
+ laneByWorkstreamId: {},
3415
+ blockedWorkstreamIds: [],
1156
3416
  activeTaskId: null,
1157
3417
  activeRunId: null,
1158
3418
  activeTaskTokenEstimate: null,
1159
3419
  };
3420
+ ensureRunInternals(run);
1160
3421
  run.agentId = input.agentId;
1161
3422
  run.agentName =
1162
3423
  typeof input.agentName === "string" && input.agentName.trim().length > 0
@@ -1164,8 +3425,24 @@ export function createAutoContinueEngine(deps) {
1164
3425
  : null;
1165
3426
  run.includeVerification = input.includeVerification;
1166
3427
  run.allowedWorkstreamIds = input.allowedWorkstreamIds;
3428
+ run.maxParallelSlices = normalizeMaxParallelSlices(input.maxParallelSlices, run.maxParallelSlices || AUTO_CONTINUE_MAX_PARALLEL_DEFAULT);
3429
+ run.parallelMode = normalizeParallelMode(input.parallelMode ?? run.parallelMode);
1167
3430
  run.stopAfterSlice = Boolean(input.stopAfterSlice);
1168
- run.tokenBudget = normalizeTokenBudget(input.tokenBudget, run.tokenBudget || defaultAutoContinueTokenBudget());
3431
+ run.ignoreSpawnGuardRateLimit = Boolean(input.ignoreSpawnGuardRateLimit);
3432
+ run.scope = input.scope ?? "task";
3433
+ const hasExplicitTokenBudgetInput = input.tokenBudget !== null &&
3434
+ input.tokenBudget !== undefined &&
3435
+ !(typeof input.tokenBudget === "string" && input.tokenBudget.trim().length === 0);
3436
+ if (hasExplicitTokenBudgetInput) {
3437
+ run.tokenBudget = normalizeTokenBudget(input.tokenBudget, defaultAutoContinueTokenBudget());
3438
+ }
3439
+ else {
3440
+ // On fresh restarts, reset to current defaults instead of inheriting stale prior limits.
3441
+ // While a run is live, keep its active budget unless explicitly overridden.
3442
+ run.tokenBudget = existingIsLive
3443
+ ? normalizeTokenBudget(run.tokenBudget, defaultAutoContinueTokenBudget())
3444
+ : defaultAutoContinueTokenBudget();
3445
+ }
1169
3446
  run.status = "running";
1170
3447
  run.stopReason = null;
1171
3448
  run.stopRequested = false;
@@ -1178,10 +3455,15 @@ export function createAutoContinueEngine(deps) {
1178
3455
  run.startedAt = now;
1179
3456
  run.lastTaskId = null;
1180
3457
  run.lastRunId = null;
3458
+ run.activeSliceRunIds = [];
3459
+ run.activeTaskIds = [];
3460
+ run.blockedWorkstreamIds = [];
3461
+ run.laneByWorkstreamId = {};
1181
3462
  run.activeTaskId = null;
1182
3463
  run.activeRunId = null;
1183
3464
  run.activeTaskTokenEstimate = null;
1184
3465
  }
3466
+ syncLegacyRunPointers(run);
1185
3467
  autoContinueRuns.set(input.initiativeId, run);
1186
3468
  void client
1187
3469
  .updateEntity("initiative", input.initiativeId, { status: "active" })
@@ -1194,6 +3476,66 @@ export function createAutoContinueEngine(deps) {
1194
3476
  }).catch(() => {
1195
3477
  // best effort
1196
3478
  });
3479
+ if (!existingIsLive || forceFreshRun) {
3480
+ const startRunContext = {
3481
+ initiativeId: run.initiativeId,
3482
+ agentId: run.agentId,
3483
+ agentName: run.agentName,
3484
+ scope: run.scope,
3485
+ };
3486
+ try {
3487
+ await emitActivitySafe({
3488
+ initiativeId: input.initiativeId,
3489
+ runId: run.lastRunId ?? undefined,
3490
+ correlationId: run.lastRunId ?? undefined,
3491
+ phase: "intent",
3492
+ level: "info",
3493
+ message: "Autopilot enabled. Dispatch will continue from Next Up automatically.",
3494
+ metadata: {
3495
+ ...buildSliceEnrichment({
3496
+ run: startRunContext,
3497
+ event: "auto_continue_started",
3498
+ }),
3499
+ token_budget: run.tokenBudget,
3500
+ include_verification: run.includeVerification,
3501
+ allowed_workstream_ids: run.allowedWorkstreamIds,
3502
+ max_parallel_slices: run.maxParallelSlices,
3503
+ parallel_mode: run.parallelMode,
3504
+ scope: run.scope,
3505
+ ignore_spawn_guard_rate_limit: run.ignoreSpawnGuardRateLimit,
3506
+ },
3507
+ nextStep: "Watch Activity for dispatch and slice-complete updates.",
3508
+ });
3509
+ }
3510
+ catch {
3511
+ // best effort
3512
+ }
3513
+ // Emit transition: idle → running
3514
+ try {
3515
+ await emitActivitySafe({
3516
+ initiativeId: input.initiativeId,
3517
+ runId: run.lastRunId ?? undefined,
3518
+ correlationId: run.lastRunId ?? undefined,
3519
+ phase: "intent",
3520
+ level: "info",
3521
+ message: "Autopilot state: idle → running.",
3522
+ metadata: {
3523
+ ...buildSliceEnrichment({
3524
+ run: startRunContext,
3525
+ event: "autopilot_transition",
3526
+ actionType: "run_state_transition",
3527
+ }),
3528
+ old_state: "idle",
3529
+ new_state: "running",
3530
+ reason: "started",
3531
+ workspace_id: run.allowedWorkstreamIds?.[0] ?? null,
3532
+ },
3533
+ });
3534
+ }
3535
+ catch {
3536
+ // best effort
3537
+ }
3538
+ }
1197
3539
  return run;
1198
3540
  }
1199
3541
  return {
@@ -1203,6 +3545,7 @@ export function createAutoContinueEngine(deps) {
1203
3545
  writeRuntimeEvent,
1204
3546
  autoContinueTickMs: AUTO_CONTINUE_TICK_MS,
1205
3547
  defaultAutoContinueTokenBudget,
3548
+ defaultAutoContinueMaxParallelSlices,
1206
3549
  setLocalInitiativeStatusOverride,
1207
3550
  clearLocalInitiativeStatusOverride,
1208
3551
  applyLocalInitiativeOverrides,
@@ -1213,6 +3556,8 @@ export function createAutoContinueEngine(deps) {
1213
3556
  tickAllAutoContinue,
1214
3557
  isInitiativeActiveStatus,
1215
3558
  runningAutoContinueForWorkstream,
3559
+ getAutoContinueLaneForWorkstream,
3560
+ scheduleAutoFixForWorkstream,
1216
3561
  startAutoContinueRun,
1217
3562
  };
1218
3563
  }