@useorgx/openclaw-plugin 0.4.9 → 0.7.2

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 (224) hide show
  1. package/README.md +77 -11
  2. package/dashboard/dist/assets/6mILZQ2a.js +1 -0
  3. package/dashboard/dist/assets/6mILZQ2a.js.br +0 -0
  4. package/dashboard/dist/assets/6mILZQ2a.js.gz +0 -0
  5. package/dashboard/dist/assets/8dksYiq4.js +2 -0
  6. package/dashboard/dist/assets/8dksYiq4.js.br +0 -0
  7. package/dashboard/dist/assets/8dksYiq4.js.gz +0 -0
  8. package/dashboard/dist/assets/B5zYRHc3.js +1 -0
  9. package/dashboard/dist/assets/B5zYRHc3.js.br +0 -0
  10. package/dashboard/dist/assets/B5zYRHc3.js.gz +0 -0
  11. package/dashboard/dist/assets/B6wPWJ35.js +1 -0
  12. package/dashboard/dist/assets/B6wPWJ35.js.br +0 -0
  13. package/dashboard/dist/assets/B6wPWJ35.js.gz +0 -0
  14. package/dashboard/dist/assets/BJgZIVUQ.js +53 -0
  15. package/dashboard/dist/assets/BJgZIVUQ.js.br +0 -0
  16. package/dashboard/dist/assets/BJgZIVUQ.js.gz +0 -0
  17. package/dashboard/dist/assets/BWEwjt1W.js +1 -0
  18. package/dashboard/dist/assets/BWEwjt1W.js.br +0 -0
  19. package/dashboard/dist/assets/BWEwjt1W.js.gz +0 -0
  20. package/dashboard/dist/assets/BgOYB78t.js +4 -0
  21. package/dashboard/dist/assets/BgOYB78t.js.br +0 -0
  22. package/dashboard/dist/assets/BgOYB78t.js.gz +0 -0
  23. package/dashboard/dist/assets/BzRbDCAD.css +1 -0
  24. package/dashboard/dist/assets/BzRbDCAD.css.br +0 -0
  25. package/dashboard/dist/assets/BzRbDCAD.css.gz +0 -0
  26. package/dashboard/dist/assets/C-KIc3Wc.js.br +0 -0
  27. package/dashboard/dist/assets/C-KIc3Wc.js.gz +0 -0
  28. package/dashboard/dist/assets/C8uM3AX8.js +1 -0
  29. package/dashboard/dist/assets/C8uM3AX8.js.br +0 -0
  30. package/dashboard/dist/assets/C8uM3AX8.js.gz +0 -0
  31. package/dashboard/dist/assets/C9jy61eu.js +212 -0
  32. package/dashboard/dist/assets/C9jy61eu.js.br +0 -0
  33. package/dashboard/dist/assets/C9jy61eu.js.gz +0 -0
  34. package/dashboard/dist/assets/CC63EwFD.js +1 -0
  35. package/dashboard/dist/assets/CC63EwFD.js.br +0 -0
  36. package/dashboard/dist/assets/CC63EwFD.js.gz +0 -0
  37. package/dashboard/dist/assets/CL_wXqR7.js +1 -0
  38. package/dashboard/dist/assets/CL_wXqR7.js.br +0 -0
  39. package/dashboard/dist/assets/CL_wXqR7.js.gz +0 -0
  40. package/dashboard/dist/assets/CZaT3ob_.js +1 -0
  41. package/dashboard/dist/assets/CZaT3ob_.js.br +0 -0
  42. package/dashboard/dist/assets/CZaT3ob_.js.gz +0 -0
  43. package/dashboard/dist/assets/CgaottFX.js +1 -0
  44. package/dashboard/dist/assets/CgaottFX.js.br +0 -0
  45. package/dashboard/dist/assets/CgaottFX.js.gz +0 -0
  46. package/dashboard/dist/assets/{CpJsfbXo.js → CxQ08qFN.js} +2 -2
  47. package/dashboard/dist/assets/CxQ08qFN.js.br +0 -0
  48. package/dashboard/dist/assets/CxQ08qFN.js.gz +0 -0
  49. package/dashboard/dist/assets/CzCxAZlW.js +1 -0
  50. package/dashboard/dist/assets/CzCxAZlW.js.br +0 -0
  51. package/dashboard/dist/assets/CzCxAZlW.js.gz +0 -0
  52. package/dashboard/dist/assets/D3iMTYEj.js +1 -0
  53. package/dashboard/dist/assets/D3iMTYEj.js.br +0 -0
  54. package/dashboard/dist/assets/D3iMTYEj.js.gz +0 -0
  55. package/dashboard/dist/assets/D8JNX8kq.js +2 -0
  56. package/dashboard/dist/assets/D8JNX8kq.js.br +0 -0
  57. package/dashboard/dist/assets/D8JNX8kq.js.gz +0 -0
  58. package/dashboard/dist/assets/DnA8dpj6.js +1 -0
  59. package/dashboard/dist/assets/DnA8dpj6.js.br +0 -0
  60. package/dashboard/dist/assets/DnA8dpj6.js.gz +0 -0
  61. package/dashboard/dist/assets/IUexzymk.js +1 -0
  62. package/dashboard/dist/assets/IUexzymk.js.br +0 -0
  63. package/dashboard/dist/assets/IUexzymk.js.gz +0 -0
  64. package/dashboard/dist/assets/cNrhgGc1.js +8 -0
  65. package/dashboard/dist/assets/cNrhgGc1.js.br +0 -0
  66. package/dashboard/dist/assets/cNrhgGc1.js.gz +0 -0
  67. package/dashboard/dist/assets/ic2FaMnh.js +1 -0
  68. package/dashboard/dist/assets/ic2FaMnh.js.br +0 -0
  69. package/dashboard/dist/assets/ic2FaMnh.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/rttbDbEx.js +1 -0
  74. package/dashboard/dist/assets/rttbDbEx.js.br +0 -0
  75. package/dashboard/dist/assets/rttbDbEx.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/practice-exercise-schema.d.ts +216 -0
  102. package/dist/contracts/practice-exercise-schema.js +314 -0
  103. package/dist/contracts/retro-schema.d.ts +81 -0
  104. package/dist/contracts/retro-schema.js +80 -0
  105. package/dist/contracts/shared-types.d.ts +159 -0
  106. package/dist/contracts/shared-types.js +199 -1
  107. package/dist/contracts/skill-pack-schema.d.ts +192 -0
  108. package/dist/contracts/skill-pack-schema.js +180 -0
  109. package/dist/contracts/types.d.ts +247 -2
  110. package/dist/entities/auto-assignment.js +43 -17
  111. package/dist/event-sanitization.d.ts +11 -0
  112. package/dist/event-sanitization.js +113 -0
  113. package/dist/gateway-watchdog.d.ts +5 -0
  114. package/dist/gateway-watchdog.js +50 -0
  115. package/dist/hooks/post-reporting-event.mjs +1 -5
  116. package/dist/http/helpers/activity-headline.js +13 -132
  117. package/dist/http/helpers/auto-continue-engine.d.ts +198 -10
  118. package/dist/http/helpers/auto-continue-engine.js +3145 -186
  119. package/dist/http/helpers/autopilot-operations.d.ts +19 -0
  120. package/dist/http/helpers/autopilot-operations.js +182 -31
  121. package/dist/http/helpers/autopilot-runtime.d.ts +1 -0
  122. package/dist/http/helpers/autopilot-runtime.js +328 -25
  123. package/dist/http/helpers/autopilot-slice-utils.d.ts +18 -0
  124. package/dist/http/helpers/autopilot-slice-utils.js +514 -93
  125. package/dist/http/helpers/decision-mapper.d.ts +40 -0
  126. package/dist/http/helpers/decision-mapper.js +223 -7
  127. package/dist/http/helpers/dispatch-lifecycle.d.ts +19 -2
  128. package/dist/http/helpers/dispatch-lifecycle.js +242 -37
  129. package/dist/http/helpers/kickoff-context.js +104 -0
  130. package/dist/http/helpers/llm-client.d.ts +47 -0
  131. package/dist/http/helpers/llm-client.js +256 -0
  132. package/dist/http/helpers/mission-control.d.ts +102 -3
  133. package/dist/http/helpers/mission-control.js +498 -9
  134. package/dist/http/helpers/sentinel-catalog.d.ts +23 -0
  135. package/dist/http/helpers/sentinel-catalog.js +193 -0
  136. package/dist/http/helpers/session-classification.d.ts +9 -0
  137. package/dist/http/helpers/session-classification.js +564 -0
  138. package/dist/http/helpers/slice-experience-v2.d.ts +137 -0
  139. package/dist/http/helpers/slice-experience-v2.js +677 -0
  140. package/dist/http/helpers/slice-run-projections.d.ts +72 -0
  141. package/dist/http/helpers/slice-run-projections.js +877 -0
  142. package/dist/http/helpers/triage-mapper.d.ts +43 -0
  143. package/dist/http/helpers/triage-mapper.js +549 -0
  144. package/dist/http/helpers/value-utils.js +7 -2
  145. package/dist/http/helpers/workspace-scope.d.ts +15 -0
  146. package/dist/http/helpers/workspace-scope.js +170 -0
  147. package/dist/http/index.js +1420 -105
  148. package/dist/http/routes/agent-suite.d.ts +9 -0
  149. package/dist/http/routes/agent-suite.js +294 -8
  150. package/dist/http/routes/agents-catalog.js +64 -19
  151. package/dist/http/routes/chat.d.ts +19 -0
  152. package/dist/http/routes/chat.js +522 -0
  153. package/dist/http/routes/decision-actions.d.ts +8 -1
  154. package/dist/http/routes/decision-actions.js +42 -5
  155. package/dist/http/routes/dispatch-gateway-envelope.d.ts +25 -0
  156. package/dist/http/routes/dispatch-gateway-envelope.js +26 -0
  157. package/dist/http/routes/entities.d.ts +16 -0
  158. package/dist/http/routes/entities.js +232 -6
  159. package/dist/http/routes/live-legacy.d.ts +5 -0
  160. package/dist/http/routes/live-legacy.js +23 -509
  161. package/dist/http/routes/live-misc.d.ts +12 -0
  162. package/dist/http/routes/live-misc.js +251 -31
  163. package/dist/http/routes/live-snapshot.d.ts +49 -2
  164. package/dist/http/routes/live-snapshot.js +653 -23
  165. package/dist/http/routes/live-terminal.d.ts +11 -0
  166. package/dist/http/routes/live-terminal.js +154 -0
  167. package/dist/http/routes/live-triage.d.ts +61 -0
  168. package/dist/http/routes/live-triage.js +192 -0
  169. package/dist/http/routes/mission-control-actions.d.ts +49 -1
  170. package/dist/http/routes/mission-control-actions.js +1246 -84
  171. package/dist/http/routes/mission-control-read.d.ts +48 -3
  172. package/dist/http/routes/mission-control-read.js +1658 -20
  173. package/dist/http/routes/realtime-orchestrator.d.ts +10 -0
  174. package/dist/http/routes/realtime-orchestrator.js +74 -0
  175. package/dist/http/routes/run-control.d.ts +5 -2
  176. package/dist/http/routes/run-control.js +10 -0
  177. package/dist/http/routes/sentinels-catalog.d.ts +7 -0
  178. package/dist/http/routes/sentinels-catalog.js +24 -0
  179. package/dist/http/routes/summary.js +10 -3
  180. package/dist/http/routes/usage.d.ts +24 -0
  181. package/dist/http/routes/usage.js +362 -0
  182. package/dist/http/routes/work-artifacts.js +28 -9
  183. package/dist/index.js +165 -27
  184. package/dist/local-openclaw.js +29 -6
  185. package/dist/mcp-client-setup.js +3 -3
  186. package/dist/mcp-http-handler.d.ts +3 -0
  187. package/dist/mcp-http-handler.js +34 -60
  188. package/dist/next-up-queue-store.d.ts +16 -1
  189. package/dist/next-up-queue-store.js +89 -7
  190. package/dist/outbox.d.ts +5 -0
  191. package/dist/outbox.js +113 -9
  192. package/dist/paths.js +36 -5
  193. package/dist/reporting/rollups.d.ts +41 -0
  194. package/dist/reporting/rollups.js +113 -0
  195. package/dist/retro/domain-templates.d.ts +45 -0
  196. package/dist/retro/domain-templates.js +297 -0
  197. package/dist/retro/quality-rubric.d.ts +33 -0
  198. package/dist/retro/quality-rubric.js +213 -0
  199. package/dist/runtime-cleanup.d.ts +18 -0
  200. package/dist/runtime-cleanup.js +87 -0
  201. package/dist/services/background.d.ts +11 -0
  202. package/dist/services/background.js +22 -0
  203. package/dist/services/experiment-randomization.d.ts +21 -0
  204. package/dist/services/experiment-randomization.js +63 -0
  205. package/dist/skill-pack-state.d.ts +36 -5
  206. package/dist/skill-pack-state.js +273 -29
  207. package/dist/sync/local-agent-telemetry.d.ts +13 -0
  208. package/dist/sync/local-agent-telemetry.js +128 -0
  209. package/dist/sync/outbox-replay.js +131 -24
  210. package/dist/team-context-store.d.ts +23 -0
  211. package/dist/team-context-store.js +116 -0
  212. package/dist/telemetry/posthog.js +4 -2
  213. package/dist/tools/core-tools.d.ts +10 -14
  214. package/dist/tools/core-tools.js +1289 -24
  215. package/dist/types.d.ts +2 -0
  216. package/dist/types.js +2 -0
  217. package/dist/worker-supervisor.js +23 -0
  218. package/package.json +20 -6
  219. package/dashboard/dist/assets/B3ziCA02.js +0 -8
  220. package/dashboard/dist/assets/B5NEElEI.css +0 -1
  221. package/dashboard/dist/assets/BhapSNAs.js +0 -215
  222. package/dashboard/dist/assets/iFdvE7lx.js +0 -1
  223. package/dashboard/dist/assets/jRJsmpYM.js +0 -1
  224. package/dashboard/dist/assets/sAhvFnpk.js +0 -4
@@ -1,27 +1,876 @@
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
+ const questionAutoAnswerPolicyByScope = new Map();
38
+ const pendingQuestionAutoAnswerByScope = new Map();
39
+ const QUESTION_AUTO_ANSWER_DEFAULT_TIMEOUT_SECONDS = readBudgetEnvNumber("ORGX_QUESTION_AUTO_ANSWER_TIMEOUT_SEC", readBudgetEnvNumber("ORGX_QUESTION_AUTO_ANSWER_DELAY_SECONDS", 60, {
40
+ min: 1,
41
+ max: 900,
42
+ }), { min: 1, max: 3600 });
43
+ const QUESTION_AUTO_ANSWER_DEFAULT_ENABLED = String(process.env.ORGX_QUESTION_AUTO_ANSWER_ENABLED ?? "true")
44
+ .trim()
45
+ .toLowerCase() !== "false";
46
+ const QUESTION_AUTO_ANSWER_DEFAULT_MODE = String(process.env.ORGX_QUESTION_AUTO_ANSWER_POLICY ?? "contextual")
47
+ .trim()
48
+ .toLowerCase() === "approve_non_blocking"
49
+ ? "approve_non_blocking"
50
+ : String(process.env.ORGX_QUESTION_AUTO_ANSWER_POLICY ?? "contextual")
51
+ .trim()
52
+ .toLowerCase() === "defer_non_blocking"
53
+ ? "defer_non_blocking"
54
+ : "contextual";
55
+ const QUESTION_BLOCKING_BEHAVIOR_DEFAULT = String(process.env.ORGX_QUESTION_BLOCKING_BEHAVIOR ?? "require_human")
56
+ .trim()
57
+ .toLowerCase() === "guarded_auto_resolve_then_human"
58
+ ? "guarded_auto_resolve_then_human"
59
+ : "require_human";
60
+ const QUESTION_AUTO_ANSWER_DEFAULT_ACTION = String(process.env.ORGX_QUESTION_AUTO_ANSWER_ACTION ?? "approve")
61
+ .trim()
62
+ .toLowerCase() === "reject"
63
+ ? "reject"
64
+ : "approve";
65
+ const autoContinueSliceRuns = new Map();
66
+ /** Spread into any metadata object to flag mock-worker activity. */
67
+ function mockMeta(slice) {
68
+ return slice.isMockWorker ? { mock: true } : {};
69
+ }
70
+ function normalizeRuntimeSourceClient(value) {
71
+ const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
72
+ if (!normalized)
73
+ return "unknown";
74
+ if (normalized === "codex")
75
+ return "codex";
76
+ if (normalized === "claude-code" || normalized === "claude_code")
77
+ return "claude-code";
78
+ if (normalized === "openclaw")
79
+ return "openclaw";
80
+ if (normalized === "api")
81
+ return "api";
82
+ return "unknown";
83
+ }
84
+ const normalizeQuestionAutoAnswerPolicy = (runtimeSettings) => {
85
+ const workspaceDefaults = runtimeSettings?.workspace_question_defaults &&
86
+ typeof runtimeSettings.workspace_question_defaults === "object"
87
+ ? runtimeSettings.workspace_question_defaults
88
+ : null;
89
+ const enabledRaw = runtimeSettings?.question_auto_answer_enabled;
90
+ const timeoutRaw = runtimeSettings?.question_auto_answer_timeout_sec ??
91
+ runtimeSettings?.question_auto_answer_delay_seconds;
92
+ const workspaceTimeoutRaw = workspaceDefaults?.question_auto_answer_timeout_sec ??
93
+ workspaceDefaults
94
+ ?.question_auto_answer_delay_seconds;
95
+ const actionRaw = runtimeSettings?.question_auto_answer_action;
96
+ const modeRaw = runtimeSettings?.question_auto_answer_policy;
97
+ const workspaceModeRaw = workspaceDefaults?.question_auto_answer_policy;
98
+ const blockingBehaviorRaw = runtimeSettings?.question_blocking_behavior;
99
+ const workspaceBlockingBehaviorRaw = workspaceDefaults?.question_blocking_behavior;
100
+ const policyVersionRaw = runtimeSettings?.question_policy_version;
101
+ const timeoutSeconds = typeof timeoutRaw === "number" && Number.isFinite(timeoutRaw)
102
+ ? Math.max(1, Math.min(3600, Math.floor(timeoutRaw)))
103
+ : typeof workspaceTimeoutRaw === "number" && Number.isFinite(workspaceTimeoutRaw)
104
+ ? Math.max(1, Math.min(3600, Math.floor(workspaceTimeoutRaw)))
105
+ : QUESTION_AUTO_ANSWER_DEFAULT_TIMEOUT_SECONDS;
106
+ const mode = modeRaw === "approve_non_blocking" ||
107
+ modeRaw === "defer_non_blocking" ||
108
+ modeRaw === "contextual"
109
+ ? modeRaw
110
+ : workspaceModeRaw === "approve_non_blocking" ||
111
+ workspaceModeRaw === "defer_non_blocking" ||
112
+ workspaceModeRaw === "contextual"
113
+ ? workspaceModeRaw
114
+ : QUESTION_AUTO_ANSWER_DEFAULT_MODE;
115
+ const action = actionRaw === "reject" || actionRaw === "approve"
116
+ ? actionRaw
117
+ : mode === "defer_non_blocking"
118
+ ? "reject"
119
+ : QUESTION_AUTO_ANSWER_DEFAULT_ACTION;
120
+ const blockingBehavior = blockingBehaviorRaw === "guarded_auto_resolve_then_human" ||
121
+ blockingBehaviorRaw === "require_human"
122
+ ? blockingBehaviorRaw
123
+ : workspaceBlockingBehaviorRaw === "guarded_auto_resolve_then_human" ||
124
+ workspaceBlockingBehaviorRaw === "require_human"
125
+ ? workspaceBlockingBehaviorRaw
126
+ : QUESTION_BLOCKING_BEHAVIOR_DEFAULT;
127
+ const enabled = typeof enabledRaw === "boolean"
128
+ ? enabledRaw
129
+ : typeof workspaceDefaults?.question_auto_answer_enabled === "boolean"
130
+ ? workspaceDefaults.question_auto_answer_enabled
131
+ : QUESTION_AUTO_ANSWER_DEFAULT_ENABLED;
132
+ const policyVersion = typeof policyVersionRaw === "number" && Number.isFinite(policyVersionRaw)
133
+ ? Math.max(1, Math.min(10, Math.floor(policyVersionRaw)))
134
+ : 1;
135
+ return {
136
+ enabled,
137
+ timeoutSeconds,
138
+ mode,
139
+ action,
140
+ blockingBehavior,
141
+ policyVersion,
142
+ };
143
+ };
144
+ const questionScopeKey = (initiativeId, workstreamId) => {
145
+ const normalizedInitiativeId = (initiativeId ?? "").trim() || "unknown_initiative";
146
+ const normalizedWorkstreamId = (workstreamId ?? "").trim() || "all_workstreams";
147
+ return `${normalizedInitiativeId}::${normalizedWorkstreamId}`;
148
+ };
149
+ const resolveQuestionPolicy = (initiativeId, workstreamId) => {
150
+ const scoped = questionAutoAnswerPolicyByScope.get(questionScopeKey(initiativeId, workstreamId));
151
+ if (scoped)
152
+ return scoped;
153
+ const initiativeWide = questionAutoAnswerPolicyByScope.get(questionScopeKey(initiativeId, null));
154
+ if (initiativeWide)
155
+ return initiativeWide;
156
+ return {
157
+ enabled: QUESTION_AUTO_ANSWER_DEFAULT_ENABLED,
158
+ timeoutSeconds: QUESTION_AUTO_ANSWER_DEFAULT_TIMEOUT_SECONDS,
159
+ mode: QUESTION_AUTO_ANSWER_DEFAULT_MODE,
160
+ action: QUESTION_AUTO_ANSWER_DEFAULT_ACTION,
161
+ blockingBehavior: QUESTION_BLOCKING_BEHAVIOR_DEFAULT,
162
+ policyVersion: 1,
163
+ };
164
+ };
165
+ const clearQuestionAutoAnswerStateForInitiative = (initiativeId) => {
166
+ const normalizedInitiativeId = (initiativeId ?? "").trim();
167
+ if (!normalizedInitiativeId)
168
+ return;
169
+ for (const [key, pending] of pendingQuestionAutoAnswerByScope.entries()) {
170
+ if ((pending.initiativeId ?? "").trim() !== normalizedInitiativeId)
171
+ continue;
172
+ if (pending.timer) {
173
+ clearTimeout(pending.timer);
174
+ }
175
+ pendingQuestionAutoAnswerByScope.delete(key);
176
+ }
177
+ };
178
+ const processQuestionAutoAnswer = async (key, pending) => {
179
+ pendingQuestionAutoAnswerByScope.delete(key);
180
+ const note = pending.action === "approve"
181
+ ? "Auto-approved after timeout: no human answer received within configured delay."
182
+ : "Auto-rejected after timeout: no human answer received within configured delay.";
183
+ const decisionIds = pending.decisionIds;
184
+ if (decisionIds.length === 0) {
185
+ return;
186
+ }
187
+ await emitActivitySafe({
188
+ initiativeId: pending.initiativeId,
189
+ runId: pending.sourceRunId,
190
+ correlationId: pending.sourceRunId,
191
+ phase: "review",
192
+ level: "info",
193
+ progressPct: 0,
194
+ nextStep: "Applying question answer policy to unresolved items.",
195
+ message: "Question auto-answered after timeout; applying decision updates.",
196
+ metadata: {
197
+ event: "question_auto_answered",
198
+ action_type: normalizeActivityActionType("question_auto_answered"),
199
+ action_phase: normalizeActivityActionPhase("review"),
200
+ initiative_id: pending.initiativeId,
201
+ workstream_id: pending.workstreamId,
202
+ source_run_id: pending.sourceRunId,
203
+ source_client: pending.sourceClient,
204
+ decision_ids: decisionIds,
205
+ decision_count: decisionIds.length,
206
+ decision_action: pending.action,
207
+ timeout_seconds_applied: pending.timeoutSeconds,
208
+ },
209
+ });
210
+ let applied = 0;
211
+ let failed = 0;
212
+ const failures = [];
213
+ for (const decisionId of decisionIds) {
214
+ try {
215
+ await client.decideDecision(decisionId, pending.action, { note });
216
+ applied += 1;
217
+ }
218
+ catch (err) {
219
+ failed += 1;
220
+ failures.push({
221
+ id: decisionId,
222
+ error: safeErrorMessage(err),
223
+ });
224
+ }
225
+ }
226
+ await emitActivitySafe({
227
+ initiativeId: pending.initiativeId,
228
+ runId: pending.sourceRunId,
229
+ correlationId: pending.sourceRunId,
230
+ phase: failed > 0 ? "blocked" : "review",
231
+ level: failed > 0 ? "warn" : "info",
232
+ progressPct: 100,
233
+ nextStep: failed > 0
234
+ ? "Review failed auto-answer decisions and resolve manually."
235
+ : "Decision queue was auto-resolved; run can continue.",
236
+ message: failed > 0
237
+ ? `Question answers processed (${applied} applied, ${failed} failed).`
238
+ : `Question answer ${pending.action} applied to ${applied} queued items.`,
239
+ metadata: {
240
+ event: failed > 0 ? "question_answer_failed" : "question_answer_applied",
241
+ action_type: normalizeActivityActionType(failed > 0 ? "question_answer_failed" : "question_answer_applied"),
242
+ action_phase: normalizeActivityActionPhase(failed > 0 ? "blocked" : "review"),
243
+ initiative_id: pending.initiativeId,
244
+ workstream_id: pending.workstreamId,
245
+ source_run_id: pending.sourceRunId,
246
+ source_client: pending.sourceClient,
247
+ question_policy_mode: pending.mode,
248
+ question_policy_version: pending.policyVersion,
249
+ decision_action: pending.action,
250
+ decision_ids: decisionIds,
251
+ decision_count: decisionIds.length,
252
+ applied_count: applied,
253
+ failed_count: failed,
254
+ resolution_source: "policy_timeout",
255
+ timeout_seconds_applied: pending.timeoutSeconds,
256
+ failures,
257
+ },
258
+ });
259
+ };
260
+ const armQuestionAutoAnswerTimer = (key, pending, delaySeconds) => {
261
+ if (pending.timer) {
262
+ clearTimeout(pending.timer);
263
+ }
264
+ pending.timer = setTimeout(() => {
265
+ void (async () => {
266
+ try {
267
+ await emitActivitySafe({
268
+ initiativeId: pending.initiativeId,
269
+ runId: pending.sourceRunId,
270
+ correlationId: pending.sourceRunId,
271
+ phase: "review",
272
+ level: "info",
273
+ progressPct: 100,
274
+ nextStep: "Applying configured decision action sequentially.",
275
+ message: "Question timeout reached; applying auto-answer policy.",
276
+ metadata: {
277
+ event: "question_timeout_started",
278
+ action_type: normalizeActivityActionType("question_timeout_started"),
279
+ action_phase: normalizeActivityActionPhase("review"),
280
+ initiative_id: pending.initiativeId,
281
+ workstream_id: pending.workstreamId,
282
+ source_run_id: pending.sourceRunId,
283
+ source_client: pending.sourceClient,
284
+ decision_ids: pending.decisionIds,
285
+ decision_count: pending.decisionIds.length,
286
+ decision_action: pending.action,
287
+ question_policy_mode: pending.mode,
288
+ question_policy_version: pending.policyVersion,
289
+ timeout_seconds_applied: pending.timeoutSeconds,
290
+ },
291
+ });
292
+ await processQuestionAutoAnswer(key, pending);
293
+ }
294
+ catch (err) {
295
+ await emitActivitySafe({
296
+ initiativeId: pending.initiativeId,
297
+ runId: pending.sourceRunId,
298
+ correlationId: pending.sourceRunId,
299
+ phase: "blocked",
300
+ level: "warn",
301
+ progressPct: 100,
302
+ nextStep: "Review and resolve the queued question manually.",
303
+ message: "Question auto-answer failed before apply.",
304
+ metadata: {
305
+ event: "question_answer_failed",
306
+ action_type: normalizeActivityActionType("question_answer_failed"),
307
+ action_phase: normalizeActivityActionPhase("blocked"),
308
+ initiative_id: pending.initiativeId,
309
+ workstream_id: pending.workstreamId,
310
+ source_run_id: pending.sourceRunId,
311
+ source_client: pending.sourceClient,
312
+ decision_ids: pending.decisionIds,
313
+ decision_count: pending.decisionIds.length,
314
+ decision_action: pending.action,
315
+ failed_count: pending.decisionIds.length,
316
+ resolution_source: "policy_timeout",
317
+ timeout_seconds_applied: pending.timeoutSeconds,
318
+ question_policy_mode: pending.mode,
319
+ question_policy_version: pending.policyVersion,
320
+ error: safeErrorMessage(err),
321
+ },
322
+ });
323
+ }
324
+ })();
325
+ }, delaySeconds * 1_000);
326
+ pending.timer.unref?.();
327
+ };
328
+ const scheduleQuestionAutoAnswer = async (input) => {
329
+ const decisionIds = dedupeStrings(input.decisionIds
330
+ .map((entry) => (entry ?? "").trim())
331
+ .filter(Boolean));
332
+ if (decisionIds.length === 0)
333
+ return;
334
+ const policy = resolveQuestionPolicy(input.initiativeId, input.workstreamId);
335
+ await emitActivitySafe({
336
+ initiativeId: input.initiativeId,
337
+ runId: input.sourceRunId,
338
+ correlationId: input.sourceRunId,
339
+ phase: "review",
340
+ level: "info",
341
+ progressPct: 0,
342
+ nextStep: input.blocking
343
+ ? "Blocking question requires human review."
344
+ : `Auto-answer in ${policy.timeoutSeconds}s unless human responds.`,
345
+ message: input.blocking
346
+ ? "Blocking question surfaced for human decision."
347
+ : "Question surfaced and queued for timeout policy.",
348
+ metadata: {
349
+ event: "question_asked",
350
+ action_type: normalizeActivityActionType("question_asked"),
351
+ action_phase: normalizeActivityActionPhase("review"),
352
+ initiative_id: input.initiativeId,
353
+ workstream_id: input.workstreamId,
354
+ source_run_id: input.sourceRunId,
355
+ source_client: input.sourceClient,
356
+ decision_ids: decisionIds,
357
+ decision_count: decisionIds.length,
358
+ blocking: input.blocking,
359
+ question_policy_mode: policy.mode,
360
+ question_policy_version: policy.policyVersion,
361
+ timeout_seconds_applied: policy.timeoutSeconds,
362
+ },
363
+ });
364
+ if (input.blocking) {
365
+ await emitActivitySafe({
366
+ initiativeId: input.initiativeId,
367
+ runId: input.sourceRunId,
368
+ correlationId: input.sourceRunId,
369
+ phase: "blocked",
370
+ level: "info",
371
+ progressPct: 0,
372
+ nextStep: policy.blockingBehavior === "guarded_auto_resolve_then_human" &&
373
+ decisionAutoResolveGuardedEnabled
374
+ ? "Awaiting guarded remediation and/or human decision."
375
+ : "Awaiting human decision response.",
376
+ message: policy.blockingBehavior === "guarded_auto_resolve_then_human" &&
377
+ decisionAutoResolveGuardedEnabled
378
+ ? "Blocking question requires human decision after guarded remediation."
379
+ : "Blocking question requires human decision.",
380
+ metadata: {
381
+ event: "review_item_created",
382
+ action_type: normalizeActivityActionType("review_item_created"),
383
+ action_phase: normalizeActivityActionPhase("blocked"),
384
+ initiative_id: input.initiativeId,
385
+ workstream_id: input.workstreamId,
386
+ source_run_id: input.sourceRunId,
387
+ source_client: input.sourceClient,
388
+ decision_ids: decisionIds,
389
+ decision_count: decisionIds.length,
390
+ blocking: true,
391
+ reason: "blocking_question_requires_human",
392
+ question_policy_mode: policy.mode,
393
+ question_policy_version: policy.policyVersion,
394
+ question_blocking_behavior: policy.blockingBehavior,
395
+ },
396
+ });
397
+ return;
398
+ }
399
+ if (!policy.enabled) {
400
+ await emitActivitySafe({
401
+ initiativeId: input.initiativeId,
402
+ runId: input.sourceRunId,
403
+ correlationId: input.sourceRunId,
404
+ phase: "review",
405
+ level: "info",
406
+ progressPct: 0,
407
+ nextStep: "Awaiting human decision response.",
408
+ message: "Question auto-answer is disabled for this agent policy.",
409
+ metadata: {
410
+ event: "review_item_created",
411
+ action_type: normalizeActivityActionType("review_item_created"),
412
+ action_phase: normalizeActivityActionPhase("review"),
413
+ initiative_id: input.initiativeId,
414
+ workstream_id: input.workstreamId,
415
+ source_run_id: input.sourceRunId,
416
+ source_client: input.sourceClient,
417
+ decision_ids: decisionIds,
418
+ reason: "policy_disabled",
419
+ question_policy_mode: policy.mode,
420
+ question_policy_version: policy.policyVersion,
421
+ },
422
+ });
423
+ return;
424
+ }
425
+ const key = questionScopeKey(input.initiativeId, input.workstreamId);
426
+ const dueAtEpoch = Date.now() + policy.timeoutSeconds * 1_000;
427
+ const existing = pendingQuestionAutoAnswerByScope.get(key);
428
+ if (existing) {
429
+ existing.decisionIds = dedupeStrings([...existing.decisionIds, ...decisionIds]);
430
+ existing.sourceRunId = input.sourceRunId ?? existing.sourceRunId;
431
+ existing.sourceClient = input.sourceClient || existing.sourceClient;
432
+ existing.action = policy.action;
433
+ existing.mode = policy.mode;
434
+ existing.policyVersion = policy.policyVersion;
435
+ existing.timeoutSeconds = policy.timeoutSeconds;
436
+ existing.dueAt = new Date(dueAtEpoch).toISOString();
437
+ armQuestionAutoAnswerTimer(key, existing, policy.timeoutSeconds);
438
+ await emitActivitySafe({
439
+ initiativeId: input.initiativeId,
440
+ runId: input.sourceRunId,
441
+ correlationId: input.sourceRunId,
442
+ phase: "review",
443
+ level: "info",
444
+ progressPct: 0,
445
+ nextStep: `Auto-answer in ${policy.timeoutSeconds}s unless human responds.`,
446
+ message: "Extended timeout for queued unanswered decision(s).",
447
+ metadata: {
448
+ event: "question_timeout_started",
449
+ action_type: normalizeActivityActionType("question_timeout_started"),
450
+ action_phase: normalizeActivityActionPhase("review"),
451
+ initiative_id: input.initiativeId,
452
+ workstream_id: input.workstreamId,
453
+ source_run_id: input.sourceRunId,
454
+ source_client: input.sourceClient,
455
+ decision_ids: existing.decisionIds,
456
+ decision_count: existing.decisionIds.length,
457
+ decision_action: existing.action,
458
+ timeout_seconds_applied: policy.timeoutSeconds,
459
+ question_policy_mode: policy.mode,
460
+ question_policy_version: policy.policyVersion,
461
+ due_at: existing.dueAt,
462
+ reason: input.reason,
463
+ },
464
+ });
465
+ return;
466
+ }
467
+ const pending = {
468
+ key,
469
+ initiativeId: input.initiativeId ?? "",
470
+ workstreamId: input.workstreamId ?? null,
471
+ sourceRunId: input.sourceRunId ?? null,
472
+ sourceClient: input.sourceClient,
473
+ action: policy.action,
474
+ mode: policy.mode,
475
+ policyVersion: policy.policyVersion,
476
+ timeoutSeconds: policy.timeoutSeconds,
477
+ dueAt: new Date(dueAtEpoch).toISOString(),
478
+ timer: null,
479
+ decisionIds,
480
+ };
481
+ armQuestionAutoAnswerTimer(key, pending, policy.timeoutSeconds);
482
+ pendingQuestionAutoAnswerByScope.set(key, pending);
483
+ await emitActivitySafe({
484
+ initiativeId: input.initiativeId,
485
+ runId: input.sourceRunId,
486
+ correlationId: input.sourceRunId,
487
+ phase: "review",
488
+ level: "info",
489
+ progressPct: 0,
490
+ nextStep: `Auto-answer in ${policy.timeoutSeconds}s unless human responds.`,
491
+ message: "Queued unanswered decision(s) for timeout auto-answer.",
492
+ metadata: {
493
+ event: "question_timeout_started",
494
+ action_type: normalizeActivityActionType("question_timeout_started"),
495
+ action_phase: normalizeActivityActionPhase("review"),
496
+ initiative_id: input.initiativeId,
497
+ workstream_id: input.workstreamId,
498
+ source_run_id: input.sourceRunId,
499
+ source_client: input.sourceClient,
500
+ decision_ids: decisionIds,
501
+ decision_count: decisionIds.length,
502
+ decision_action: policy.action,
503
+ timeout_seconds_applied: policy.timeoutSeconds,
504
+ question_policy_mode: policy.mode,
505
+ question_policy_version: policy.policyVersion,
506
+ due_at: pending.dueAt,
507
+ reason: input.reason,
508
+ },
509
+ });
510
+ };
511
+ const requestDecisionQueued = async (input) => {
512
+ const asRecord = (value) => {
513
+ if (!value || typeof value !== "object" || Array.isArray(value))
514
+ return null;
515
+ return value;
516
+ };
517
+ const normalizeId = (value) => {
518
+ if (typeof value !== "string")
519
+ return null;
520
+ const trimmed = value.trim();
521
+ return trimmed.length > 0 ? trimmed : null;
522
+ };
523
+ const normalizeLower = (value) => {
524
+ if (typeof value !== "string")
525
+ return "";
526
+ return value.trim().toLowerCase();
527
+ };
528
+ const recoverQueuedDecisionIds = async (recoverInput) => {
529
+ try {
530
+ const pending = await client.getLiveDecisions({ status: "pending", limit: 100 });
531
+ const rows = Array.isArray(pending?.decisions) ? pending.decisions : [];
532
+ const wantedTitle = normalizeLower(recoverInput.title);
533
+ const wantedRunId = normalizeLower(recoverInput.sourceRunId ?? "");
534
+ const wantedWorkstreamId = normalizeLower(recoverInput.workstreamId ?? "");
535
+ const recentThreshold = Date.now() - 10 * 60 * 1_000;
536
+ const ids = [];
537
+ const seen = new Set();
538
+ for (const row of rows) {
539
+ const record = asRecord(row);
540
+ if (!record)
541
+ continue;
542
+ const id = normalizeId(record.id) ??
543
+ normalizeId(record.entity_id) ??
544
+ normalizeId(record.decision_id);
545
+ if (!id || seen.has(id))
546
+ continue;
547
+ const metadata = asRecord(record.metadata);
548
+ const sourceRef = asRecord(record.source_ref) ?? asRecord(metadata?.source_ref);
549
+ const rowWorkstreamId = normalizeLower(record.workstream_id) ||
550
+ normalizeLower(record.workstreamId) ||
551
+ normalizeLower(metadata?.source_stream_id) ||
552
+ normalizeLower(sourceRef?.workstream_id) ||
553
+ normalizeLower(sourceRef?.stream_id);
554
+ const rowRunId = normalizeLower(record.source_run_id) ||
555
+ normalizeLower(record.sourceRunId) ||
556
+ normalizeLower(metadata?.run_id) ||
557
+ normalizeLower(metadata?.correlation_id) ||
558
+ normalizeLower(sourceRef?.run_id);
559
+ const rowTitle = normalizeLower(record.title) || normalizeLower(metadata?.title);
560
+ const updatedAtRaw = normalizeId(record.updated_at) ?? normalizeId(record.created_at);
561
+ const updatedAtEpoch = updatedAtRaw ? Date.parse(updatedAtRaw) : NaN;
562
+ const recentEnough = !Number.isFinite(updatedAtEpoch) || updatedAtEpoch >= recentThreshold;
563
+ const workstreamMatches = !wantedWorkstreamId || rowWorkstreamId === wantedWorkstreamId;
564
+ const runMatches = Boolean(wantedRunId) && rowRunId === wantedRunId;
565
+ const titleMatches = Boolean(wantedTitle) && rowTitle === wantedTitle;
566
+ if (!workstreamMatches)
567
+ continue;
568
+ if (!(runMatches || (titleMatches && recentEnough)))
569
+ continue;
570
+ seen.add(id);
571
+ ids.push(id);
572
+ }
573
+ return ids;
574
+ }
575
+ catch {
576
+ return [];
577
+ }
578
+ };
579
+ const inferredRunId = (typeof input.sourceRunId === "string" && input.sourceRunId.trim().length > 0
580
+ ? input.sourceRunId.trim()
581
+ : null) ??
582
+ (typeof input.correlationId === "string" && input.correlationId.trim().length > 0
583
+ ? input.correlationId.trim()
584
+ : null);
585
+ const inferredSessionId = (typeof input.sourceSessionId === "string" && input.sourceSessionId.trim().length > 0
586
+ ? input.sourceSessionId.trim()
587
+ : null) ?? inferredRunId;
588
+ const inferredStreamId = (typeof input.sourceStreamId === "string" && input.sourceStreamId.trim().length > 0
589
+ ? input.sourceStreamId.trim()
590
+ : null) ??
591
+ (typeof input.workstreamId === "string" && input.workstreamId.trim().length > 0
592
+ ? input.workstreamId.trim()
593
+ : null);
594
+ const sourceRefBase = input.sourceRef && typeof input.sourceRef === "object" && !Array.isArray(input.sourceRef)
595
+ ? input.sourceRef
596
+ : {};
597
+ const metadataBase = input.metadata && typeof input.metadata === "object" && !Array.isArray(input.metadata)
598
+ ? input.metadata
599
+ : {};
600
+ const metadataSourceClient = (typeof metadataBase.source_client === "string" && metadataBase.source_client.trim().length > 0
601
+ ? metadataBase.source_client.trim()
602
+ : null) ??
603
+ (typeof metadataBase.sourceClient === "string" && metadataBase.sourceClient.trim().length > 0
604
+ ? metadataBase.sourceClient.trim()
605
+ : null);
606
+ const inferredSourceClient = normalizeRuntimeSourceClient(metadataSourceClient ??
607
+ process.env.ORGX_AUTOPILOT_EXECUTOR ??
608
+ process.env.ORGX_AUTOPILOT_WORKER_KIND);
609
+ const normalizedInput = {
610
+ ...input,
611
+ sourceRunId: inferredRunId,
612
+ sourceSessionId: inferredSessionId,
613
+ sourceStreamId: inferredStreamId,
614
+ sourceRef: {
615
+ ...sourceRefBase,
616
+ run_id: sourceRefBase.run_id ?? inferredRunId,
617
+ session_id: sourceRefBase.session_id ?? inferredSessionId,
618
+ stream_id: sourceRefBase.stream_id ?? inferredStreamId,
619
+ workstream_id: sourceRefBase.workstream_id ?? input.workstreamId ?? null,
620
+ source_client: sourceRefBase.source_client ??
621
+ sourceRefBase.sourceClient ??
622
+ inferredSourceClient,
623
+ },
624
+ metadata: {
625
+ ...metadataBase,
626
+ source_system: input.sourceSystem ?? null,
627
+ conflict_source: input.conflictSource ?? null,
628
+ source_client: metadataBase.source_client ??
629
+ metadataBase.sourceClient ??
630
+ inferredSourceClient,
631
+ },
632
+ };
633
+ const linkedSlice = inferredRunId ? autoContinueSliceRuns.get(inferredRunId) ?? null : null;
634
+ const sourceClientFromInput = typeof normalizedInput.metadata?.source_client === "string"
635
+ ? normalizedInput.metadata.source_client
636
+ : null;
637
+ const sourceClient = sourceClientFromInput ??
638
+ linkedSlice?.sourceClient ??
639
+ "unknown";
640
+ const scopedWorkstreamId = ((typeof normalizedInput.workstreamId === "string" &&
641
+ normalizedInput.workstreamId.trim().length > 0
642
+ ? normalizedInput.workstreamId.trim()
643
+ : null) ??
644
+ inferredStreamId ??
645
+ linkedSlice?.workstreamId ??
646
+ null);
647
+ const result = await requestDecisionSafe(normalizedInput);
648
+ if (typeof result === "boolean") {
649
+ return { queued: result, decisionIds: [] };
650
+ }
651
+ if (result && typeof result === "object" && "queued" in result) {
652
+ const record = result;
653
+ let decisionIds = Array.isArray(record.decisionIds)
654
+ ? record.decisionIds
655
+ .filter((entry) => typeof entry === "string")
656
+ .map((entry) => entry.trim())
657
+ .filter(Boolean)
658
+ : [];
659
+ if (Boolean(record.queued) && decisionIds.length === 0) {
660
+ decisionIds = await recoverQueuedDecisionIds({
661
+ title: normalizedInput.title,
662
+ sourceRunId: inferredRunId,
663
+ workstreamId: scopedWorkstreamId,
664
+ });
665
+ }
666
+ if (Boolean(record.queued) && decisionIds.length > 0) {
667
+ await scheduleQuestionAutoAnswer({
668
+ initiativeId: normalizedInput.initiativeId,
669
+ workstreamId: scopedWorkstreamId,
670
+ sourceRunId: inferredRunId,
671
+ sourceClient,
672
+ decisionIds,
673
+ blocking: Boolean(normalizedInput.blocking),
674
+ reason: typeof normalizedInput.conflictSource === "string"
675
+ ? normalizedInput.conflictSource
676
+ : null,
677
+ });
678
+ }
679
+ return {
680
+ queued: Boolean(record.queued),
681
+ decisionIds,
682
+ };
683
+ }
684
+ return { queued: false, decisionIds: [] };
685
+ };
16
686
  const __filename = deps.filename;
17
687
  const autoContinueRuns = new Map();
18
688
  const localInitiativeStatusOverrides = new Map();
689
+ const localTaskStatusOverrides = new Map();
690
+ const localMilestoneStatusOverrides = new Map();
19
691
  let autoContinueTickInFlight = null;
20
692
  const AUTO_CONTINUE_TICK_MS = readBudgetEnvNumber("ORGX_AUTO_CONTINUE_TICK_MS", 2_500, {
21
693
  min: 250,
22
694
  max: 60_000,
23
695
  });
24
- const autoContinueSliceRuns = new Map();
696
+ const AUTO_CONTINUE_PARALLEL_MIN = 1;
697
+ const AUTO_CONTINUE_PARALLEL_MAX = 5;
698
+ 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 }))));
699
+ const normalizeParallelMode = (_value) => "iwmt";
700
+ const normalizeMaxParallelSlices = (value, fallback) => {
701
+ const normalizedFallback = Math.max(AUTO_CONTINUE_PARALLEL_MIN, Math.min(AUTO_CONTINUE_PARALLEL_MAX, Math.round(fallback || AUTO_CONTINUE_PARALLEL_MIN)));
702
+ if (typeof value === "number" && Number.isFinite(value)) {
703
+ const parsed = Math.round(value);
704
+ return Math.max(AUTO_CONTINUE_PARALLEL_MIN, Math.min(AUTO_CONTINUE_PARALLEL_MAX, parsed));
705
+ }
706
+ if (typeof value === "string" && value.trim().length > 0) {
707
+ const parsed = Number(value);
708
+ if (Number.isFinite(parsed)) {
709
+ const rounded = Math.round(parsed);
710
+ return Math.max(AUTO_CONTINUE_PARALLEL_MIN, Math.min(AUTO_CONTINUE_PARALLEL_MAX, rounded));
711
+ }
712
+ }
713
+ return normalizedFallback;
714
+ };
715
+ const buildSliceEnrichment = (input) => {
716
+ const eventName = typeof input.event === "string" && input.event.trim().length > 0
717
+ ? input.event.trim().toLowerCase()
718
+ : null;
719
+ const inferredActionType = (() => {
720
+ if (!eventName)
721
+ return "execute_task";
722
+ if (eventName === "orchestrator_dispatch")
723
+ return "orchestrator_dispatch";
724
+ if (eventName.includes("slice_dispatched"))
725
+ return "dispatch_slice";
726
+ if (eventName.includes("slice_started") || eventName === "session_start") {
727
+ return "run_started";
728
+ }
729
+ if (eventName.includes("slice_heartbeat") || eventName === "heartbeat") {
730
+ return "run_heartbeat";
731
+ }
732
+ if (eventName.includes("slice_handoff"))
733
+ return "slice_handoff";
734
+ if (eventName.includes("spawn_guard_rate_limited"))
735
+ return "spawn_guard_rate_limited";
736
+ if (eventName.includes("spawn_guard_blocked"))
737
+ return "spawn_guard_blocked";
738
+ if (eventName.includes("status_updates_buffered"))
739
+ return "status_updates_buffered";
740
+ if (eventName.includes("status_updates"))
741
+ return "status_updates_applied";
742
+ if (eventName.includes("artifact_registered"))
743
+ return "artifact_registered";
744
+ if (eventName.includes("question_asked"))
745
+ return "question_asked";
746
+ if (eventName.includes("question_timeout_started"))
747
+ return "question_timeout_started";
748
+ if (eventName.includes("question_auto_answered"))
749
+ return "question_auto_answered";
750
+ if (eventName.includes("question_answer_applied"))
751
+ return "question_answer_applied";
752
+ if (eventName.includes("question_answer_failed"))
753
+ return "question_answer_failed";
754
+ if (eventName.includes("review_item_created"))
755
+ return "review_item_created";
756
+ if (eventName.includes("review_item_resolved"))
757
+ return "review_item_resolved";
758
+ if (eventName.includes("decision_requested"))
759
+ return "decision_requested";
760
+ if (eventName.includes("decision_resolved"))
761
+ return "decision_resolved";
762
+ if (eventName === "auto_continue_started")
763
+ return "auto_continue_started";
764
+ if (eventName === "auto_continue_stopped")
765
+ return "auto_continue_stopped";
766
+ if (eventName.includes("behavior_config") || eventName.includes("behavior_automation")) {
767
+ return "behavior_config_review";
768
+ }
769
+ if (eventName.includes("transition"))
770
+ return "run_state_transition";
771
+ if (eventName.includes("auto_fix"))
772
+ return "auto_fix";
773
+ if (eventName.includes("milestone_completed"))
774
+ return "milestone_completed";
775
+ if (eventName.includes("error") || eventName.includes("failed"))
776
+ return "run_failed";
777
+ if (eventName.includes("result") || eventName.includes("completed"))
778
+ return "run_completed";
779
+ return eventName.replace(/[^a-z0-9]+/g, "_");
780
+ })();
781
+ const actionType = normalizeActivityActionType(input.actionType ?? inferredActionType) ?? inferredActionType;
782
+ const inferredActionPhase = (() => {
783
+ if (!eventName)
784
+ return "execution";
785
+ if (eventName === "orchestrator_dispatch" || eventName.includes("slice_dispatched")) {
786
+ return "dispatch";
787
+ }
788
+ if (eventName.includes("handoff"))
789
+ return "handoff";
790
+ if (eventName.includes("heartbeat"))
791
+ return "execution";
792
+ if (eventName.includes("decision_"))
793
+ return "review";
794
+ if (eventName.includes("blocked") ||
795
+ eventName.includes("rate_limited") ||
796
+ eventName.includes("stall") ||
797
+ eventName.includes("timeout")) {
798
+ return "blocked";
799
+ }
800
+ if (eventName.includes("error") || eventName.includes("failed"))
801
+ return "error";
802
+ if (eventName.includes("result") || eventName.includes("completed") || eventName === "auto_continue_stopped") {
803
+ return "completed";
804
+ }
805
+ if (eventName === "auto_continue_started")
806
+ return "intent";
807
+ return "execution";
808
+ })();
809
+ const actionPhase = normalizeActivityActionPhase(input.actionPhase ?? inferredActionPhase) ??
810
+ inferredActionPhase;
811
+ const workstreamId = (input.workstreamId ?? input.slice?.workstreamId ?? "").trim() || null;
812
+ const taskId = (input.taskId ?? input.slice?.taskIds?.[0] ?? "").trim() || null;
813
+ const requiredSkills = Array.isArray(input.requiredSkills)
814
+ ? input.requiredSkills
815
+ : input.slice?.requiredSkills ?? null;
816
+ return {
817
+ event: input.event ?? null,
818
+ action_type: actionType,
819
+ action_phase: actionPhase,
820
+ initiative_id: input.run.initiativeId,
821
+ requested_by_agent_id: input.run.agentId,
822
+ requested_by_agent_name: input.run.agentName,
823
+ requester_agent_id: input.run.agentId,
824
+ requester_agent_name: input.run.agentName,
825
+ agent_id: input.slice?.agentId ?? null,
826
+ agent_name: input.slice?.agentName ?? null,
827
+ executor_agent_id: input.slice?.agentId ?? null,
828
+ executor_agent_name: input.slice?.agentName ?? null,
829
+ source_run_id: input.slice?.runId ?? null,
830
+ source_session_id: input.slice?.runId ?? null,
831
+ source_stream_id: workstreamId,
832
+ run_id: input.slice?.runId ?? null,
833
+ slice_run_id: input.slice?.runId ?? null,
834
+ correlation_id: input.slice?.runId ?? null,
835
+ source_client: input.slice?.sourceClient ?? "unknown",
836
+ runtime_client: input.slice?.sourceClient ?? "unknown",
837
+ workstream_id: workstreamId,
838
+ workstream_title: input.workstreamTitle ?? input.slice?.workstreamTitle ?? null,
839
+ task_id: taskId,
840
+ task_title: input.taskTitle ?? null,
841
+ milestone_ids: input.slice?.milestoneIds ?? null,
842
+ task_ids: input.slice?.taskIds ?? null,
843
+ domain: input.domain ?? input.slice?.domain ?? null,
844
+ required_skills: requiredSkills,
845
+ skill_pack: requiredSkills,
846
+ model_tier: input.modelTier ?? null,
847
+ scope: input.slice?.scope ?? input.run.scope,
848
+ actors: {
849
+ requester: {
850
+ agent_id: input.run.agentId ?? null,
851
+ agent_name: input.run.agentName ?? null,
852
+ },
853
+ dispatcher: {
854
+ agent_id: input.run.agentId ?? null,
855
+ agent_name: input.run.agentName ?? null,
856
+ },
857
+ executor: {
858
+ agent_id: input.slice?.agentId ?? null,
859
+ agent_name: input.slice?.agentName ?? null,
860
+ },
861
+ },
862
+ scope_context: {
863
+ initiative_id: input.run.initiativeId,
864
+ workstream_id: workstreamId,
865
+ task_id: taskId,
866
+ task_ids: input.slice?.taskIds ?? null,
867
+ milestone_ids: input.slice?.milestoneIds ?? null,
868
+ },
869
+ next_actions: input.nextActions ?? null,
870
+ user_summary: input.userSummary ?? null,
871
+ ...(input.extra ?? {}),
872
+ };
873
+ };
25
874
  // Keep child handles alive so stdout/stderr capture remains reliable even when the process is detached.
26
875
  const autoContinueSliceChildren = new Map();
27
876
  const autoContinueSliceLastHeartbeatMs = new Map();
@@ -32,7 +881,6 @@ export function createAutoContinueEngine(deps) {
32
881
  autoContinueSliceChildren.delete(id);
33
882
  autoContinueSliceLastHeartbeatMs.delete(id);
34
883
  };
35
- const AUTO_CONTINUE_SLICE_MAX_TASKS = 6;
36
884
  const AUTO_CONTINUE_SLICE_TIMEOUT_MS = readBudgetEnvNumber("ORGX_AUTOPILOT_SLICE_TIMEOUT_MS", 55 * 60_000,
37
885
  // Keep test runs fast; real-world defaults are still ~1h unless overridden.
38
886
  { min: 250, max: 6 * 60 * 60_000 });
@@ -42,6 +890,302 @@ export function createAutoContinueEngine(deps) {
42
890
  const AUTO_CONTINUE_SLICE_HEARTBEAT_MS = 12_000;
43
891
  const AUTO_CONTINUE_SLICE_SCHEMA_FILENAME = "autopilot-slice-schema.json";
44
892
  const AUTO_CONTINUE_SLICE_LOG_DIRNAME = "autopilot-logs";
893
+ // Prune old autopilot logs on engine init (7-day TTL, 50 MB cap).
894
+ const AUTOPILOT_LOG_TTL_MS = 7 * 24 * 60 * 60_000;
895
+ const AUTOPILOT_LOG_MAX_BYTES = 50 * 1024 * 1024;
896
+ (async () => {
897
+ try {
898
+ const logsDir = join(getOrgxPluginConfigDir(), AUTO_CONTINUE_SLICE_LOG_DIRNAME);
899
+ if (!existsSync(logsDir))
900
+ return;
901
+ const entries = await readdir(logsDir);
902
+ const now = Date.now();
903
+ const fileStats = [];
904
+ for (const name of entries) {
905
+ if (!name.endsWith(".log") && !name.endsWith(".output.json"))
906
+ continue;
907
+ const filePath = join(logsDir, name);
908
+ try {
909
+ const s = await stat(filePath);
910
+ if (s.mtimeMs < now - AUTOPILOT_LOG_TTL_MS) {
911
+ await unlink(filePath);
912
+ }
913
+ else {
914
+ fileStats.push({ name, path: filePath, mtimeMs: s.mtimeMs, size: s.size });
915
+ }
916
+ }
917
+ catch { /* skip */ }
918
+ }
919
+ // Enforce total size cap by deleting oldest first.
920
+ fileStats.sort((a, b) => a.mtimeMs - b.mtimeMs);
921
+ let totalSize = fileStats.reduce((sum, f) => sum + f.size, 0);
922
+ for (const f of fileStats) {
923
+ if (totalSize <= AUTOPILOT_LOG_MAX_BYTES)
924
+ break;
925
+ try {
926
+ await unlink(f.path);
927
+ }
928
+ catch { /* skip */ }
929
+ totalSize -= f.size;
930
+ }
931
+ }
932
+ catch { /* best effort */ }
933
+ })();
934
+ const AUTO_FIX_DEFAULT_GRACE_MS = readBudgetEnvNumber("ORGX_AUTOPILOT_AUTOFIX_GRACE_MS", 10_000, { min: 1_000, max: 120_000 });
935
+ const AUTO_CONTINUE_SPAWN_GUARD_RETRY_MS = readBudgetEnvNumber("ORGX_AUTO_CONTINUE_SPAWN_GUARD_RETRY_MS", 15_000, { min: 1_000, max: 15 * 60_000 });
936
+ const autoFixByScope = new Map();
937
+ const autoContinueSpawnGuardRetryByTask = new Map();
938
+ const getSpawnGuardRetryAtMs = (initiativeId, taskId) => {
939
+ const taskKey = taskId.trim();
940
+ if (!taskKey)
941
+ return 0;
942
+ const entry = autoContinueSpawnGuardRetryByTask.get(taskKey);
943
+ if (!entry)
944
+ return 0;
945
+ if (entry.initiativeId !== initiativeId || entry.retryAtMs <= Date.now()) {
946
+ autoContinueSpawnGuardRetryByTask.delete(taskKey);
947
+ return 0;
948
+ }
949
+ return entry.retryAtMs;
950
+ };
951
+ const clearSpawnGuardRetryStateForInitiative = (initiativeId) => {
952
+ for (const [taskId, entry] of autoContinueSpawnGuardRetryByTask.entries()) {
953
+ if (entry.initiativeId !== initiativeId)
954
+ continue;
955
+ autoContinueSpawnGuardRetryByTask.delete(taskId);
956
+ }
957
+ };
958
+ const normalizeStatusValue = (value) => {
959
+ if (typeof value !== "string")
960
+ return "";
961
+ return value.trim().toLowerCase().replace(/[\s-]+/g, "_");
962
+ };
963
+ const listActiveSliceRunIds = (run) => {
964
+ ensureRunInternals(run);
965
+ const ids = new Set();
966
+ for (const id of run.activeSliceRunIds ?? []) {
967
+ const normalized = (id ?? "").trim();
968
+ if (normalized)
969
+ ids.add(normalized);
970
+ }
971
+ for (const lane of Object.values(run.laneByWorkstreamId ?? {})) {
972
+ const activeRunId = (lane.activeRunId ?? "").trim();
973
+ if (activeRunId)
974
+ ids.add(activeRunId);
975
+ }
976
+ return Array.from(ids);
977
+ };
978
+ const upsertLane = (run, workstreamId, patch) => {
979
+ const normalizedWorkstreamId = workstreamId.trim();
980
+ if (!normalizedWorkstreamId) {
981
+ throw new Error("workstreamId is required");
982
+ }
983
+ const existing = run.laneByWorkstreamId[normalizedWorkstreamId] ?? {
984
+ workstreamId: normalizedWorkstreamId,
985
+ state: "idle",
986
+ activeRunId: null,
987
+ activeTaskIds: [],
988
+ blockedReason: null,
989
+ waitingOnWorkstreamIds: [],
990
+ retryAt: null,
991
+ updatedAt: new Date().toISOString(),
992
+ };
993
+ const next = {
994
+ ...existing,
995
+ ...patch,
996
+ workstreamId: normalizedWorkstreamId,
997
+ updatedAt: patch.updatedAt ?? new Date().toISOString(),
998
+ activeTaskIds: Array.isArray(patch.activeTaskIds)
999
+ ? dedupeStrings(patch.activeTaskIds.map((id) => (id ?? "").trim()).filter(Boolean))
1000
+ : existing.activeTaskIds,
1001
+ waitingOnWorkstreamIds: Array.isArray(patch.waitingOnWorkstreamIds)
1002
+ ? dedupeStrings(patch.waitingOnWorkstreamIds.map((id) => (id ?? "").trim()).filter(Boolean))
1003
+ : existing.waitingOnWorkstreamIds,
1004
+ };
1005
+ run.laneByWorkstreamId[normalizedWorkstreamId] = next;
1006
+ return next;
1007
+ };
1008
+ const setLaneState = (run, input) => {
1009
+ return upsertLane(run, input.workstreamId, {
1010
+ state: input.state,
1011
+ activeRunId: input.activeRunId === undefined ? undefined : (input.activeRunId ?? "").trim() || null,
1012
+ activeTaskIds: input.activeTaskIds,
1013
+ blockedReason: input.blockedReason === undefined
1014
+ ? undefined
1015
+ : (input.blockedReason ?? "").trim() || null,
1016
+ waitingOnWorkstreamIds: input.waitingOnWorkstreamIds,
1017
+ retryAt: input.retryAt === undefined ? undefined : input.retryAt,
1018
+ });
1019
+ };
1020
+ const removeActiveSliceFromRun = (run, input) => {
1021
+ const sliceRunId = input.sliceRunId.trim();
1022
+ if (!sliceRunId)
1023
+ return;
1024
+ run.activeSliceRunIds = run.activeSliceRunIds.filter((id) => id !== sliceRunId);
1025
+ const taskIds = new Set(Array.isArray(input.taskIds)
1026
+ ? input.taskIds.map((id) => (id ?? "").trim()).filter(Boolean)
1027
+ : []);
1028
+ if (taskIds.size > 0) {
1029
+ run.activeTaskIds = run.activeTaskIds.filter((id) => !taskIds.has(id));
1030
+ }
1031
+ const normalizedWorkstreamId = (input.workstreamId ?? "").trim();
1032
+ if (normalizedWorkstreamId) {
1033
+ const lane = run.laneByWorkstreamId[normalizedWorkstreamId];
1034
+ if (lane && lane.activeRunId === sliceRunId) {
1035
+ setLaneState(run, {
1036
+ workstreamId: normalizedWorkstreamId,
1037
+ state: lane.state === "blocked" ? "blocked" : "idle",
1038
+ activeRunId: null,
1039
+ activeTaskIds: [],
1040
+ retryAt: lane.retryAt ?? null,
1041
+ waitingOnWorkstreamIds: lane.waitingOnWorkstreamIds ?? [],
1042
+ blockedReason: lane.state === "blocked" ? lane.blockedReason : null,
1043
+ });
1044
+ }
1045
+ }
1046
+ };
1047
+ const syncLegacyRunPointers = (run) => {
1048
+ ensureRunInternals(run);
1049
+ const activeIds = listActiveSliceRunIds(run);
1050
+ run.activeSliceRunIds = activeIds;
1051
+ run.activeTaskIds = dedupeStrings((run.activeTaskIds ?? []).map((id) => (id ?? "").trim()).filter(Boolean));
1052
+ run.activeRunId = activeIds[0] ?? null;
1053
+ run.activeTaskId = run.activeTaskIds[0] ?? null;
1054
+ if (!run.activeRunId) {
1055
+ run.activeTaskTokenEstimate = null;
1056
+ }
1057
+ };
1058
+ const ensureRunInternals = (run) => {
1059
+ if (!Array.isArray(run.activeSliceRunIds))
1060
+ run.activeSliceRunIds = [];
1061
+ if (!Array.isArray(run.activeTaskIds))
1062
+ run.activeTaskIds = [];
1063
+ if (!run.laneByWorkstreamId || typeof run.laneByWorkstreamId !== "object") {
1064
+ run.laneByWorkstreamId = {};
1065
+ }
1066
+ if (!Array.isArray(run.blockedWorkstreamIds))
1067
+ run.blockedWorkstreamIds = [];
1068
+ run.maxParallelSlices = normalizeMaxParallelSlices(run.maxParallelSlices, AUTO_CONTINUE_MAX_PARALLEL_DEFAULT);
1069
+ run.parallelMode = normalizeParallelMode(run.parallelMode);
1070
+ run.tokenBudget = normalizeTokenBudget(run.tokenBudget, defaultAutoContinueTokenBudget());
1071
+ };
1072
+ const recordLocalStatusOverrides = (input) => {
1073
+ const initiativeId = input.initiativeId.trim();
1074
+ if (!initiativeId)
1075
+ return;
1076
+ if (input.taskUpdates.length > 0) {
1077
+ const scoped = localTaskStatusOverrides.get(initiativeId) ?? new Map();
1078
+ for (const update of input.taskUpdates) {
1079
+ const taskId = update.taskId.trim();
1080
+ const status = normalizeStatusValue(update.status);
1081
+ if (!taskId || !status)
1082
+ continue;
1083
+ scoped.set(taskId, {
1084
+ status,
1085
+ updatedAt: input.updatedAt,
1086
+ reason: update.reason,
1087
+ });
1088
+ }
1089
+ if (scoped.size > 0) {
1090
+ localTaskStatusOverrides.set(initiativeId, scoped);
1091
+ }
1092
+ }
1093
+ if (input.milestoneUpdates.length > 0) {
1094
+ const scoped = localMilestoneStatusOverrides.get(initiativeId) ?? new Map();
1095
+ for (const update of input.milestoneUpdates) {
1096
+ const milestoneId = update.milestoneId.trim();
1097
+ const status = normalizeStatusValue(update.status);
1098
+ if (!milestoneId || !status)
1099
+ continue;
1100
+ scoped.set(milestoneId, {
1101
+ status,
1102
+ updatedAt: input.updatedAt,
1103
+ reason: update.reason,
1104
+ });
1105
+ }
1106
+ if (scoped.size > 0) {
1107
+ localMilestoneStatusOverrides.set(initiativeId, scoped);
1108
+ }
1109
+ }
1110
+ };
1111
+ const applyLocalStatusOverridesToGraph = (initiativeId, nodeById) => {
1112
+ const scopedTaskOverrides = localTaskStatusOverrides.get(initiativeId) ?? null;
1113
+ if (scopedTaskOverrides) {
1114
+ for (const [taskId, override] of scopedTaskOverrides.entries()) {
1115
+ const node = nodeById.get(taskId);
1116
+ if (!node || node.type !== "task")
1117
+ continue;
1118
+ const remoteStatus = normalizeStatusValue(node.status);
1119
+ node.status = override.status;
1120
+ if (remoteStatus === override.status) {
1121
+ scopedTaskOverrides.delete(taskId);
1122
+ }
1123
+ }
1124
+ if (scopedTaskOverrides.size === 0) {
1125
+ localTaskStatusOverrides.delete(initiativeId);
1126
+ }
1127
+ }
1128
+ const scopedMilestoneOverrides = localMilestoneStatusOverrides.get(initiativeId) ?? null;
1129
+ if (scopedMilestoneOverrides) {
1130
+ for (const [milestoneId, override] of scopedMilestoneOverrides.entries()) {
1131
+ const node = nodeById.get(milestoneId);
1132
+ if (!node || node.type !== "milestone")
1133
+ continue;
1134
+ const remoteStatus = normalizeStatusValue(node.status);
1135
+ node.status = override.status;
1136
+ if (remoteStatus === override.status) {
1137
+ scopedMilestoneOverrides.delete(milestoneId);
1138
+ }
1139
+ }
1140
+ if (scopedMilestoneOverrides.size === 0) {
1141
+ localMilestoneStatusOverrides.delete(initiativeId);
1142
+ }
1143
+ }
1144
+ };
1145
+ const isPendingDecisionStatus = (value) => {
1146
+ const normalized = normalizeStatusValue(value);
1147
+ if (!normalized)
1148
+ return false;
1149
+ return (normalized === "pending" ||
1150
+ normalized === "open" ||
1151
+ normalized === "requested" ||
1152
+ normalized === "awaiting_review" ||
1153
+ normalized === "awaiting_approval" ||
1154
+ normalized === "queued");
1155
+ };
1156
+ const decisionMatchesWorkstream = (record, workstreamId, runId) => {
1157
+ const directWorkstream = pickString(record, ["workstream_id", "workstreamId"])?.trim() ?? "";
1158
+ if (directWorkstream && directWorkstream === workstreamId)
1159
+ return true;
1160
+ const correlationId = pickString(record, ["correlation_id", "correlationId"])?.trim() ?? "";
1161
+ if (runId && correlationId && correlationId === runId)
1162
+ return true;
1163
+ const metadataRaw = record.metadata;
1164
+ const metadata = metadataRaw && typeof metadataRaw === "object" && !Array.isArray(metadataRaw)
1165
+ ? metadataRaw
1166
+ : null;
1167
+ if (!metadata)
1168
+ return false;
1169
+ const nestedWorkstream = pickString(metadata, ["workstream_id", "workstreamId"])?.trim() ?? "";
1170
+ if (nestedWorkstream && nestedWorkstream === workstreamId)
1171
+ return true;
1172
+ const nestedCorrelation = pickString(metadata, ["correlation_id", "correlationId"])?.trim() ?? "";
1173
+ if (runId && nestedCorrelation && nestedCorrelation === runId)
1174
+ return true;
1175
+ return false;
1176
+ };
1177
+ const decisionIsBlocking = (record) => {
1178
+ const direct = record.blocking;
1179
+ if (typeof direct === "boolean")
1180
+ return direct;
1181
+ const metadataRaw = record.metadata;
1182
+ if (metadataRaw && typeof metadataRaw === "object" && !Array.isArray(metadataRaw)) {
1183
+ const nested = metadataRaw.blocking;
1184
+ if (typeof nested === "boolean")
1185
+ return nested;
1186
+ }
1187
+ return true;
1188
+ };
45
1189
  const setLocalInitiativeStatusOverride = (initiativeId, status) => {
46
1190
  const normalizedId = initiativeId.trim();
47
1191
  if (!normalizedId)
@@ -104,27 +1248,52 @@ export function createAutoContinueEngine(deps) {
104
1248
  : node),
105
1249
  };
106
1250
  };
107
- function normalizeTokenBudget(value, fallback) {
1251
+ function parseTokenBudget(value) {
108
1252
  if (typeof value === "number" && Number.isFinite(value)) {
1253
+ if (value <= 0)
1254
+ return null;
109
1255
  return Math.max(1_000, Math.round(value));
110
1256
  }
111
- if (typeof value === "string" && value.trim().length > 0) {
112
- const parsed = Number(value);
1257
+ if (typeof value === "string") {
1258
+ const trimmed = value.trim();
1259
+ if (!trimmed)
1260
+ return null;
1261
+ const normalized = trimmed.toLowerCase();
1262
+ if (normalized === "0" ||
1263
+ normalized === "off" ||
1264
+ normalized === "none" ||
1265
+ normalized === "false" ||
1266
+ normalized === "unlimited" ||
1267
+ normalized === "null") {
1268
+ return null;
1269
+ }
1270
+ const parsed = Number(trimmed);
113
1271
  if (Number.isFinite(parsed)) {
1272
+ if (parsed <= 0)
1273
+ return null;
114
1274
  return Math.max(1_000, Math.round(parsed));
115
1275
  }
116
1276
  }
117
- return Math.max(1_000, Math.round(fallback));
1277
+ return null;
1278
+ }
1279
+ function normalizeTokenBudget(value, fallback) {
1280
+ const parsed = parseTokenBudget(value);
1281
+ if (parsed !== null)
1282
+ return parsed;
1283
+ return fallback;
118
1284
  }
119
1285
  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);
1286
+ const explicitBudget = parseTokenBudget(process.env.ORGX_AUTO_CONTINUE_TOKEN_BUDGET);
1287
+ if (explicitBudget !== null)
1288
+ return explicitBudget;
1289
+ // Token budget guardrails are now explicit-only: either pass a budget when starting
1290
+ // auto-continue or set ORGX_AUTO_CONTINUE_TOKEN_BUDGET directly.
1291
+ // Legacy fallback toggles (for example ORGX_AUTO_CONTINUE_ENFORCE_TOKEN_BUDGET)
1292
+ // are intentionally ignored to prevent hidden auto-stop behavior.
1293
+ return null;
1294
+ }
1295
+ function defaultAutoContinueMaxParallelSlices() {
1296
+ return AUTO_CONTINUE_MAX_PARALLEL_DEFAULT;
128
1297
  }
129
1298
  function estimateTokensForDurationHours(durationHours) {
130
1299
  if (!Number.isFinite(durationHours) || durationHours <= 0)
@@ -136,7 +1305,8 @@ export function createAutoContinueEngine(deps) {
136
1305
  }
137
1306
  // Helpers used by previous task-level auto-continue implementation were removed in v2.
138
1307
  // 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.
1308
+ // Autopilot v2 dispatches workstream slices via runtime workers (codex/claude-code)
1309
+ // and does not rely on OpenClaw session JSONL.
140
1310
  async function fetchInitiativeEntity(initiativeId) {
141
1311
  try {
142
1312
  const list = await client.listEntities("initiative", { limit: 200 });
@@ -159,7 +1329,18 @@ export function createAutoContinueEngine(deps) {
159
1329
  await client.updateEntity("initiative", initiativeId, { metadata: nextMeta });
160
1330
  }
161
1331
  async function updateInitiativeAutoContinueState(input) {
1332
+ syncLegacyRunPointers(input.run);
162
1333
  const now = new Date().toISOString();
1334
+ const laneStates = Object.values(input.run.laneByWorkstreamId ?? {}).map((lane) => ({
1335
+ workstream_id: lane.workstreamId,
1336
+ state: lane.state,
1337
+ active_run_id: lane.activeRunId,
1338
+ active_task_ids: lane.activeTaskIds,
1339
+ blocked_reason: lane.blockedReason,
1340
+ waiting_on_workstream_ids: lane.waitingOnWorkstreamIds,
1341
+ retry_at: lane.retryAt,
1342
+ updated_at: lane.updatedAt,
1343
+ }));
163
1344
  const patch = {
164
1345
  auto_continue_enabled: input.run.status === "running" || input.run.status === "stopping",
165
1346
  auto_continue_status: input.run.status,
@@ -171,29 +1352,63 @@ export function createAutoContinueEngine(deps) {
171
1352
  auto_continue_tokens_used: input.run.tokensUsed,
172
1353
  auto_continue_active_task_id: input.run.activeTaskId,
173
1354
  auto_continue_active_run_id: input.run.activeRunId,
1355
+ auto_continue_active_task_ids: input.run.activeTaskIds,
1356
+ auto_continue_active_run_ids: input.run.activeSliceRunIds,
174
1357
  auto_continue_active_task_token_estimate: input.run.activeTaskTokenEstimate,
175
1358
  auto_continue_last_task_id: input.run.lastTaskId,
176
1359
  auto_continue_last_run_id: input.run.lastRunId,
177
1360
  auto_continue_include_verification: input.run.includeVerification,
178
1361
  auto_continue_workstream_filter: input.run.allowedWorkstreamIds,
1362
+ auto_continue_parallel_mode: input.run.parallelMode,
1363
+ auto_continue_max_parallel: input.run.maxParallelSlices,
1364
+ auto_continue_lane_states: laneStates,
1365
+ auto_continue_blocked_workstream_ids: input.run.blockedWorkstreamIds,
1366
+ auto_continue_ignore_spawn_guard_rate_limit: input.run.ignoreSpawnGuardRateLimit,
179
1367
  ...(input.run.lastError ? { auto_continue_last_error: input.run.lastError } : {}),
180
1368
  };
181
1369
  await updateInitiativeMetadata(input.initiativeId, patch);
182
1370
  }
183
1371
  async function stopAutoContinueRun(input) {
1372
+ const decisionRequired = input.reason === "blocked" && input.decisionRequired === true;
1373
+ const decisionIds = Array.isArray(input.decisionIds)
1374
+ ? input.decisionIds
1375
+ .filter((entry) => typeof entry === "string")
1376
+ .map((entry) => entry.trim())
1377
+ .filter(Boolean)
1378
+ : [];
1379
+ const preserveQuestionAutoAnswerState = input.reason === "blocked" && decisionRequired && decisionIds.length > 0;
184
1380
  const now = new Date().toISOString();
185
- const activeRunId = input.run.activeRunId;
1381
+ ensureRunInternals(input.run);
1382
+ const activeRunIds = listActiveSliceRunIds(input.run);
186
1383
  input.run.status = "stopped";
187
1384
  input.run.stopReason = input.reason;
188
1385
  input.run.stoppedAt = now;
189
1386
  input.run.updatedAt = now;
190
1387
  input.run.stopRequested = false;
1388
+ input.run.activeSliceRunIds = [];
1389
+ input.run.activeTaskIds = [];
191
1390
  input.run.activeRunId = null;
192
1391
  input.run.activeTaskId = null;
193
1392
  input.run.activeTaskTokenEstimate = null;
1393
+ for (const lane of Object.values(input.run.laneByWorkstreamId ?? {})) {
1394
+ if (lane.activeRunId || lane.activeTaskIds.length > 0) {
1395
+ setLaneState(input.run, {
1396
+ workstreamId: lane.workstreamId,
1397
+ state: lane.state === "blocked" ? "blocked" : "idle",
1398
+ activeRunId: null,
1399
+ activeTaskIds: [],
1400
+ });
1401
+ }
1402
+ }
194
1403
  if (input.error)
195
1404
  input.run.lastError = input.error;
196
- clearAutoContinueSliceTransientState(activeRunId);
1405
+ clearSpawnGuardRetryStateForInitiative(input.run.initiativeId);
1406
+ if (!preserveQuestionAutoAnswerState) {
1407
+ clearQuestionAutoAnswerStateForInitiative(input.run.initiativeId);
1408
+ }
1409
+ for (const runId of activeRunIds) {
1410
+ clearAutoContinueSliceTransientState(runId);
1411
+ }
197
1412
  // Only pause the initiative on non-terminal stops (error, blocked, user-requested).
198
1413
  // Completed / budget-exhausted runs should not override the initiative status.
199
1414
  if (input.reason !== "completed" && input.reason !== "budget_exhausted") {
@@ -215,17 +1430,22 @@ export function createAutoContinueEngine(deps) {
215
1430
  catch {
216
1431
  // best effort
217
1432
  }
218
- const scopeSuffix = Array.isArray(input.run.allowedWorkstreamIds) && input.run.allowedWorkstreamIds.length === 1
219
- ? ` (${input.run.allowedWorkstreamIds[0]})`
220
- : "";
1433
+ const primaryActiveRunId = activeRunIds[0] ?? null;
1434
+ const scopedWorkstreamId = Array.isArray(input.run.allowedWorkstreamIds) && input.run.allowedWorkstreamIds.length === 1
1435
+ ? input.run.allowedWorkstreamIds[0]
1436
+ : null;
1437
+ const scopeSuffix = scopedWorkstreamId ? ` [workstream ${scopedWorkstreamId}]` : "";
1438
+ const budgetValue = typeof input.run.tokenBudget === "number" ? input.run.tokenBudget : "unbounded";
221
1439
  const message = input.reason === "completed"
222
1440
  ? `Autopilot stopped: current dispatch scope completed${scopeSuffix}.`
223
1441
  : input.reason === "budget_exhausted"
224
- ? `Autopilot stopped: token budget exhausted (${input.run.tokensUsed}/${input.run.tokenBudget}).`
1442
+ ? `Autopilot stopped: token budget exhausted (${input.run.tokensUsed}/${budgetValue}).`
225
1443
  : input.reason === "stopped"
226
1444
  ? `Autopilot stopped by user request${scopeSuffix}.`
227
1445
  : input.reason === "blocked"
228
- ? `Autopilot stopped: blocked pending decision${scopeSuffix}.`
1446
+ ? decisionRequired
1447
+ ? `Autopilot stopped: blocked awaiting decision${scopeSuffix}.`
1448
+ : `Autopilot stopped: blocked${scopeSuffix}.`
229
1449
  : `Autopilot stopped due to error${scopeSuffix}.`;
230
1450
  const phase = input.reason === "completed"
231
1451
  ? "completed"
@@ -237,26 +1457,83 @@ export function createAutoContinueEngine(deps) {
237
1457
  : input.reason === "budget_exhausted" || input.reason === "stopped"
238
1458
  ? "warn"
239
1459
  : "error";
1460
+ const errorLocation = input.reason === "blocked"
1461
+ ? "mission-control.auto-continue.engine.blocked"
1462
+ : input.reason === "error"
1463
+ ? "mission-control.auto-continue.engine.error"
1464
+ : null;
1465
+ const stopRunContext = {
1466
+ initiativeId: input.run.initiativeId,
1467
+ agentId: input.run.agentId,
1468
+ agentName: input.run.agentName,
1469
+ scope: input.run.scope,
1470
+ };
240
1471
  await emitActivitySafe({
241
1472
  initiativeId: input.run.initiativeId,
242
- runId: activeRunId ?? input.run.lastRunId ?? undefined,
243
- correlationId: activeRunId ?? input.run.lastRunId ?? undefined,
1473
+ runId: primaryActiveRunId ?? input.run.lastRunId ?? undefined,
1474
+ correlationId: primaryActiveRunId ?? input.run.lastRunId ?? undefined,
244
1475
  phase,
245
1476
  level,
1477
+ progressPct: input.reason === "completed" ? 100 : input.reason === "blocked" ? 65 : 0,
1478
+ nextStep: input.reason === "completed"
1479
+ ? "Select the next queue item or enable autoplay for continuous dispatch."
1480
+ : input.reason === "blocked"
1481
+ ? "Resolve blocker decisions, then resume or restart autoplay."
1482
+ : input.reason === "budget_exhausted"
1483
+ ? "Increase token budget or scope down work before restarting autoplay."
1484
+ : input.reason === "stopped"
1485
+ ? "Restart autoplay when ready."
1486
+ : "Inspect error details and relaunch once fixed.",
246
1487
  message,
247
1488
  metadata: {
248
- event: "auto_continue_stopped",
1489
+ ...buildSliceEnrichment({
1490
+ run: stopRunContext,
1491
+ workstreamId: scopedWorkstreamId,
1492
+ event: "auto_continue_stopped",
1493
+ }),
249
1494
  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,
1495
+ active_run_id: primaryActiveRunId,
1496
+ active_run_ids: activeRunIds,
253
1497
  last_run_id: input.run.lastRunId,
254
1498
  token_budget: input.run.tokenBudget,
255
1499
  tokens_used: input.run.tokensUsed,
256
1500
  allowed_workstream_ids: input.run.allowedWorkstreamIds,
1501
+ max_parallel_slices: input.run.maxParallelSlices,
1502
+ scope_workstream_id: scopedWorkstreamId,
1503
+ decision_required: decisionRequired,
1504
+ decision_ids: decisionIds,
1505
+ decision_count: decisionIds.length,
257
1506
  last_error: input.run.lastError,
1507
+ error_location: errorLocation,
258
1508
  },
259
1509
  });
1510
+ // Emit autopilot_transition event for state observers.
1511
+ try {
1512
+ await emitActivitySafe({
1513
+ initiativeId: input.run.initiativeId,
1514
+ runId: primaryActiveRunId ?? input.run.lastRunId ?? undefined,
1515
+ correlationId: primaryActiveRunId ?? input.run.lastRunId ?? undefined,
1516
+ phase,
1517
+ level: "info",
1518
+ progressPct: input.reason === "completed" ? 100 : input.reason === "blocked" ? 65 : 0,
1519
+ message: `Autopilot state: running → ${input.reason === "completed" ? "idle" : input.reason === "stopped" ? "idle" : input.reason}.`,
1520
+ metadata: {
1521
+ ...buildSliceEnrichment({
1522
+ run: stopRunContext,
1523
+ workstreamId: scopedWorkstreamId,
1524
+ event: "autopilot_transition",
1525
+ actionType: "run_state_transition",
1526
+ }),
1527
+ old_state: "running",
1528
+ new_state: input.reason === "completed" || input.reason === "stopped" ? "idle" : input.reason === "blocked" ? "blocked" : input.reason === "error" ? "error" : "idle",
1529
+ reason: input.reason,
1530
+ workspace_id: input.run.allowedWorkstreamIds?.[0] ?? null,
1531
+ },
1532
+ });
1533
+ }
1534
+ catch {
1535
+ // best effort
1536
+ }
260
1537
  }
261
1538
  const codexBinResolver = createCodexBinResolver();
262
1539
  const resolveCodexBinInfo = () => codexBinResolver.resolveCodexBinInfo();
@@ -274,11 +1551,15 @@ export function createAutoContinueEngine(deps) {
274
1551
  if (run.status !== "running" && run.status !== "stopping")
275
1552
  return;
276
1553
  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;
1554
+ syncLegacyRunPointers(run);
1555
+ // 1) Reconcile each active slice lane and register outcomes when complete.
1556
+ const activeRunIdsForTick = listActiveSliceRunIds(run);
1557
+ for (const activeRunIdForTick of activeRunIdsForTick) {
1558
+ run.activeRunId = activeRunIdForTick;
1559
+ const slice = autoContinueSliceRuns.get(activeRunIdForTick) ?? null;
280
1560
  if (!slice) {
281
1561
  // Legacy/unknown pointer; clear so we can continue.
1562
+ removeActiveSliceFromRun(run, { sliceRunId: activeRunIdForTick });
282
1563
  run.activeRunId = null;
283
1564
  run.activeTaskId = null;
284
1565
  run.updatedAt = now;
@@ -327,12 +1608,18 @@ export function createAutoContinueEngine(deps) {
327
1608
  requested_by_agent_name: run.agentName,
328
1609
  domain: slice.domain,
329
1610
  required_skills: slice.requiredSkills,
1611
+ behavior_config_id: slice.behaviorConfigId,
1612
+ behavior_config_version: slice.behaviorConfigVersion,
1613
+ behavior_config_hash: slice.behaviorConfigHash,
1614
+ policy_source: slice.behaviorPolicySource,
1615
+ behavior_automation_level: slice.behaviorAutomationLevel,
330
1616
  workstream_id: slice.workstreamId,
331
1617
  workstream_title: slice.workstreamTitle ?? null,
332
1618
  task_ids: slice.taskIds,
333
1619
  milestone_ids: slice.milestoneIds,
334
1620
  log_path: slice.logPath,
335
1621
  output_path: slice.outputPath,
1622
+ ...mockMeta(slice),
336
1623
  },
337
1624
  });
338
1625
  }
@@ -344,8 +1631,10 @@ export function createAutoContinueEngine(deps) {
344
1631
  const startedAtEpochMs = Date.parse(slice.startedAt);
345
1632
  const fallbackEpochMs = Number.isFinite(startedAtEpochMs) ? startedAtEpochMs : nowMs;
346
1633
  const outputUpdatedAtEpochMs = fileUpdatedAtEpochMs(slice.outputPath, fallbackEpochMs);
347
- // Treat stdout/output freshness as progress; stderr noise should not prevent stall detection.
348
- const stallUpdatedAtEpochMs = outputUpdatedAtEpochMs;
1634
+ const logUpdatedAtEpochMs = fileUpdatedAtEpochMs(slice.logPath, fallbackEpochMs);
1635
+ // Some codex runs only materialize output.json at process exit. Treat recent log activity
1636
+ // as liveness signal so active slices are not falsely marked as stalled.
1637
+ const stallUpdatedAtEpochMs = Math.max(outputUpdatedAtEpochMs, logUpdatedAtEpochMs);
349
1638
  const logTail = readFileTailSafe(slice.logPath, 64_000);
350
1639
  const mcpHandshake = detectMcpHandshakeFailure(logTail);
351
1640
  if (mcpHandshake) {
@@ -369,21 +1658,28 @@ export function createAutoContinueEngine(deps) {
369
1658
  correlationId: slice.runId,
370
1659
  phase: "blocked",
371
1660
  level: "error",
1661
+ progressPct: 55,
1662
+ nextStep: "Review MCP diagnostics, then choose retry, skip, or pause for investigation.",
372
1663
  message: `Autopilot slice MCP failed: ${slice.workstreamTitle ?? slice.workstreamId}.`,
373
1664
  metadata: {
374
- event: "autopilot_slice_mcp_handshake_failed",
375
- requested_by_agent_id: run.agentId,
376
- requested_by_agent_name: run.agentName,
1665
+ ...buildSliceEnrichment({
1666
+ run,
1667
+ slice,
1668
+ workstreamId: slice.workstreamId,
1669
+ workstreamTitle: slice.workstreamTitle ?? null,
1670
+ domain: slice.domain,
1671
+ requiredSkills: slice.requiredSkills,
1672
+ event: "autopilot_slice_mcp_handshake_failed",
1673
+ }),
1674
+ error_location: "mission-control.auto-continue.engine.slice.mcp-handshake",
377
1675
  mcp_server: mcpHandshake.server,
378
1676
  mcp_line: mcpHandshake.line,
379
- workstream_id: slice.workstreamId,
380
- task_ids: slice.taskIds,
381
- milestone_ids: slice.milestoneIds,
382
1677
  log_path: slice.logPath,
383
1678
  output_path: slice.outputPath,
1679
+ ...mockMeta(slice),
384
1680
  },
385
1681
  });
386
- await requestDecisionSafe({
1682
+ const decisionResult = await requestDecisionQueued({
387
1683
  initiativeId: run.initiativeId,
388
1684
  correlationId: slice.runId,
389
1685
  title: `Autopilot slice MCP failed: ${slice.workstreamTitle ?? slice.workstreamId}`,
@@ -395,19 +1691,63 @@ export function createAutoContinueEngine(deps) {
395
1691
  "Skip this workstream for now",
396
1692
  ],
397
1693
  blocking: true,
1694
+ decisionType: "autopilot_failure",
1695
+ workstreamId: slice.workstreamId,
1696
+ agentId: slice.agentId,
1697
+ sourceSystem: "orgx-autopilot",
1698
+ conflictSource: "mcp_handshake_failure",
1699
+ dedupeKey: [
1700
+ "autopilot",
1701
+ run.initiativeId,
1702
+ slice.workstreamId,
1703
+ "mcp_handshake_failure",
1704
+ mcpHandshake.server ?? "unknown",
1705
+ ].join(":"),
1706
+ recommendedAction: "Retry once. If it fails again, pause autopilot and inspect MCP server configuration.",
1707
+ sourceRunId: slice.runId,
1708
+ sourceRef: {
1709
+ run_id: slice.runId,
1710
+ workstream_id: slice.workstreamId,
1711
+ mcp_server: mcpHandshake.server ?? null,
1712
+ },
1713
+ evidenceRefs: [
1714
+ {
1715
+ evidence_type: "mcp_diagnostic",
1716
+ title: "MCP handshake failure",
1717
+ summary: `MCP handshake failed${mcpHandshake.server ? ` for ${mcpHandshake.server}` : ""}.`,
1718
+ source_pointer: slice.logPath,
1719
+ payload: {
1720
+ mcp_server: mcpHandshake.server ?? null,
1721
+ mcp_line: mcpHandshake.line ?? null,
1722
+ output_path: slice.outputPath,
1723
+ },
1724
+ },
1725
+ ],
1726
+ });
1727
+ setLaneState(run, {
1728
+ workstreamId: slice.workstreamId,
1729
+ state: "blocked",
1730
+ activeRunId: null,
1731
+ activeTaskIds: [],
1732
+ blockedReason: slice.lastError,
1733
+ waitingOnWorkstreamIds: [],
1734
+ retryAt: null,
398
1735
  });
399
1736
  await stopAutoContinueRun({
400
1737
  run,
401
1738
  reason: "blocked",
402
1739
  error: slice.lastError,
1740
+ decisionRequired: decisionResult.queued,
1741
+ decisionIds: decisionResult.decisionIds,
403
1742
  });
404
1743
  return;
405
1744
  }
1745
+ const scopeTimeoutMs = AUTO_CONTINUE_SLICE_TIMEOUT_MS * SLICE_SCOPE_TIMEOUT_MULTIPLIER[slice.scope ?? "task"];
406
1746
  const killDecision = shouldKillWorker({
407
1747
  nowEpochMs: nowMs,
408
1748
  startedAtEpochMs: fallbackEpochMs,
409
1749
  logUpdatedAtEpochMs: stallUpdatedAtEpochMs,
410
- }, { timeoutMs: AUTO_CONTINUE_SLICE_TIMEOUT_MS, stallMs: AUTO_CONTINUE_SLICE_LOG_STALL_MS });
1750
+ }, { timeoutMs: scopeTimeoutMs, stallMs: AUTO_CONTINUE_SLICE_LOG_STALL_MS });
411
1751
  if (killDecision.kill) {
412
1752
  try {
413
1753
  await stopProcess(pid);
@@ -420,7 +1760,7 @@ export function createAutoContinueEngine(deps) {
420
1760
  slice.updatedAt = now;
421
1761
  slice.lastError =
422
1762
  killDecision.kind === "timeout"
423
- ? `Autopilot slice timed out after ${Math.round(AUTO_CONTINUE_SLICE_TIMEOUT_MS / 60_000)} minutes.`
1763
+ ? `Autopilot slice timed out after ${Math.round(scopeTimeoutMs / 60_000)} minutes.`
424
1764
  : `Autopilot slice stalled (no output) for ${Math.round(AUTO_CONTINUE_SLICE_LOG_STALL_MS / 60_000)} minutes.`;
425
1765
  autoContinueSliceRuns.set(slice.runId, slice);
426
1766
  run.lastError = slice.lastError;
@@ -434,22 +1774,31 @@ export function createAutoContinueEngine(deps) {
434
1774
  correlationId: slice.runId,
435
1775
  phase: "blocked",
436
1776
  level: "error",
1777
+ progressPct: 55,
1778
+ nextStep: "Open logs/output, decide retry or pause, and capture blocker context for handoff.",
437
1779
  message: `Autopilot slice ${humanLabel}: ${slice.workstreamTitle ?? slice.workstreamId}.`,
438
1780
  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,
1781
+ ...buildSliceEnrichment({
1782
+ run,
1783
+ slice,
1784
+ workstreamId: slice.workstreamId,
1785
+ workstreamTitle: slice.workstreamTitle ?? null,
1786
+ domain: slice.domain,
1787
+ requiredSkills: slice.requiredSkills,
1788
+ event,
1789
+ }),
1790
+ error_location: killDecision.kind === "timeout"
1791
+ ? "mission-control.auto-continue.engine.slice.timeout"
1792
+ : "mission-control.auto-continue.engine.slice.stall",
445
1793
  log_path: slice.logPath,
446
1794
  output_path: slice.outputPath,
447
1795
  reason: killDecision.reason,
448
1796
  elapsed_ms: killDecision.elapsedMs,
449
1797
  idle_ms: killDecision.idleMs,
1798
+ ...mockMeta(slice),
450
1799
  },
451
1800
  });
452
- await requestDecisionSafe({
1801
+ const decisionResult = await requestDecisionQueued({
453
1802
  initiativeId: run.initiativeId,
454
1803
  correlationId: slice.runId,
455
1804
  title: `Autopilot slice ${humanLabel}: ${slice.workstreamTitle ?? slice.workstreamId}`,
@@ -461,11 +1810,63 @@ export function createAutoContinueEngine(deps) {
461
1810
  "Skip this workstream for now",
462
1811
  ],
463
1812
  blocking: true,
1813
+ decisionType: "autopilot_failure",
1814
+ workstreamId: slice.workstreamId,
1815
+ agentId: slice.agentId,
1816
+ sourceSystem: "orgx-autopilot",
1817
+ conflictSource: killDecision.kind === "timeout"
1818
+ ? "slice_timeout"
1819
+ : "slice_stall_no_output",
1820
+ dedupeKey: [
1821
+ "autopilot",
1822
+ run.initiativeId,
1823
+ slice.workstreamId,
1824
+ killDecision.kind === "timeout"
1825
+ ? "slice_timeout"
1826
+ : "slice_stall_no_output",
1827
+ ].join(":"),
1828
+ recommendedAction: "Review logs and output, then retry once. If repeated, pause autopilot and investigate worker/runtime health.",
1829
+ sourceRunId: slice.runId,
1830
+ sourceRef: {
1831
+ run_id: slice.runId,
1832
+ workstream_id: slice.workstreamId,
1833
+ kill_kind: killDecision.kind,
1834
+ elapsed_ms: killDecision.elapsedMs,
1835
+ idle_ms: killDecision.idleMs,
1836
+ },
1837
+ evidenceRefs: [
1838
+ {
1839
+ evidence_type: killDecision.kind === "timeout"
1840
+ ? "timeout_diagnostic"
1841
+ : "stall_diagnostic",
1842
+ title: killDecision.kind === "timeout"
1843
+ ? "Slice timed out"
1844
+ : "Slice stalled",
1845
+ summary: killDecision.reason,
1846
+ source_pointer: slice.logPath,
1847
+ payload: {
1848
+ elapsed_ms: killDecision.elapsedMs,
1849
+ idle_ms: killDecision.idleMs,
1850
+ output_path: slice.outputPath,
1851
+ },
1852
+ },
1853
+ ],
1854
+ });
1855
+ setLaneState(run, {
1856
+ workstreamId: slice.workstreamId,
1857
+ state: "blocked",
1858
+ activeRunId: null,
1859
+ activeTaskIds: [],
1860
+ blockedReason: slice.lastError,
1861
+ waitingOnWorkstreamIds: [],
1862
+ retryAt: null,
464
1863
  });
465
1864
  await stopAutoContinueRun({
466
1865
  run,
467
1866
  reason: "blocked",
468
1867
  error: slice.lastError,
1868
+ decisionRequired: decisionResult.queued,
1869
+ decisionIds: decisionResult.decisionIds,
469
1870
  });
470
1871
  return;
471
1872
  }
@@ -478,7 +1879,7 @@ export function createAutoContinueEngine(deps) {
478
1879
  }
479
1880
  }
480
1881
  if (!outputComplete)
481
- return;
1882
+ continue;
482
1883
  }
483
1884
  }
484
1885
  // Slice finished.
@@ -486,13 +1887,21 @@ export function createAutoContinueEngine(deps) {
486
1887
  const parsed = raw ? parseSliceResult(raw) : null;
487
1888
  const parsedStatus = parsed?.status ?? "error";
488
1889
  const defaultDecisionBlocking = parsedStatus === "completed" ? false : true;
489
- const decisions = Array.isArray(parsed?.decisions_needed)
1890
+ const allDecisions = Array.isArray(parsed?.decisions_needed)
490
1891
  ? (parsed?.decisions_needed ?? [])
491
1892
  .filter((item) => Boolean(item && typeof item.question === "string" && item.question.trim()))
492
1893
  : [];
493
- const blockingDecisionCount = decisions.filter((item) => typeof item.blocking === "boolean" ? item.blocking : defaultDecisionBlocking).length;
494
- const nonBlockingDecisionCount = Math.max(0, decisions.length - blockingDecisionCount);
495
- const effectiveParsedStatus = parsedStatus === "completed" && blockingDecisionCount > 0
1894
+ const isParserSyntheticFallbackDecision = (item) => {
1895
+ const question = String(item?.question ?? "").trim().toLowerCase();
1896
+ const summary = String(item?.summary ?? "").trim().toLowerCase();
1897
+ return ((question.includes("missing required blocking decision") ||
1898
+ summary.includes("parser inserted a blocking decision")) &&
1899
+ item?.blocking === true);
1900
+ };
1901
+ const decisions = allDecisions.filter((item) => !isParserSyntheticFallbackDecision(item));
1902
+ const normalizedBlockingDecisionCount = allDecisions.filter((item) => typeof item.blocking === "boolean" ? item.blocking : defaultDecisionBlocking).length;
1903
+ const normalizedNonBlockingDecisionCount = Math.max(0, allDecisions.length - normalizedBlockingDecisionCount);
1904
+ const effectiveParsedStatus = parsedStatus === "completed" && normalizedBlockingDecisionCount > 0
496
1905
  ? "needs_decision"
497
1906
  : parsedStatus;
498
1907
  slice.status =
@@ -510,32 +1919,157 @@ export function createAutoContinueEngine(deps) {
510
1919
  autoContinueSliceRuns.set(slice.runId, slice);
511
1920
  clearAutoContinueSliceTransientState(slice.runId);
512
1921
  // Token accounting: codex CLI doesn't provide tokens here; use the modeled estimate.
513
- const modeledTokens = slice.tokenEstimate ?? run.activeTaskTokenEstimate ?? 0;
1922
+ const modeledTokens = slice.tokenEstimate ?? 0;
514
1923
  run.tokensUsed += Math.max(0, modeledTokens);
515
1924
  run.activeTaskTokenEstimate = null;
516
1925
  const artifacts = Array.isArray(parsed?.artifacts)
517
1926
  ? (parsed?.artifacts ?? [])
518
1927
  .filter((item) => Boolean(item && typeof item.name === "string" && item.name.trim()))
519
1928
  : [];
520
- const taskUpdates = Array.isArray(parsed?.task_updates)
521
- ? parsed.task_updates
1929
+ const artifactEvidenceRefs = artifacts.map((artifact) => ({
1930
+ evidence_type: "artifact",
1931
+ title: artifact.name.trim(),
1932
+ summary: artifact.description?.trim() || "Slice artifact output",
1933
+ source_pointer: artifact.url ?? slice.outputPath,
1934
+ payload: {
1935
+ artifact_type: artifact.artifact_type ?? null,
1936
+ confidence_score: artifact.confidence_score ?? null,
1937
+ task_ids: Array.isArray(artifact.task_ids) && artifact.task_ids.length > 0
1938
+ ? artifact.task_ids
1939
+ : slice.taskIds,
1940
+ milestone_id: artifact.milestone_id ?? slice.milestoneIds[0] ?? null,
1941
+ },
1942
+ }));
1943
+ const nextActions = Array.isArray(parsed?.next_actions)
1944
+ ? parsed.next_actions
1945
+ .filter((item) => typeof item === "string")
1946
+ .map((item) => item.trim())
1947
+ .filter(Boolean)
1948
+ : [];
1949
+ const userSummary = (typeof parsed?.summary === "string" && parsed.summary.trim().length > 0
1950
+ ? parsed.summary.trim()
1951
+ : null) ??
1952
+ nextActions[0] ??
1953
+ (slice.status === "completed"
1954
+ ? `Slice completed for ${slice.workstreamTitle ?? slice.workstreamId}.`
1955
+ : `Slice blocked for ${slice.workstreamTitle ?? slice.workstreamId}.`);
1956
+ const nextStepHint = nextActions[0] ??
1957
+ (slice.status === "completed"
1958
+ ? "No follow-up action returned by worker."
1959
+ : "Resolve blocker to continue execution.");
1960
+ const skillEvidence = Array.isArray(parsed?.skill_evidence)
1961
+ ? parsed.skill_evidence
1962
+ .map((item) => ({
1963
+ skill: typeof item?.skill === "string"
1964
+ ? item.skill.trim()
1965
+ : "",
1966
+ skill_file: typeof item?.skill_file === "string"
1967
+ ? item.skill_file.trim()
1968
+ : null,
1969
+ skill_sha256: typeof item?.skill_sha256 === "string"
1970
+ ? item.skill_sha256.trim().toLowerCase()
1971
+ : null,
1972
+ skill_heading: typeof item?.skill_heading === "string"
1973
+ ? item.skill_heading.trim()
1974
+ : null,
1975
+ }))
1976
+ .filter((item) => item.skill.length > 0)
1977
+ : [];
1978
+ const reportedSkillNames = Array.from(new Set(skillEvidence
1979
+ .map((entry) => entry.skill.replace(/^\$/, "").trim())
1980
+ .filter(Boolean)));
1981
+ const reportedSkillSha256Count = skillEvidence.filter((entry) => typeof entry.skill_sha256 === "string" && entry.skill_sha256.length > 0).length;
1982
+ const taskUpdates = Array.isArray(parsed?.task_updates)
1983
+ ? parsed.task_updates
522
1984
  : [];
523
1985
  const milestoneUpdates = Array.isArray(parsed?.milestone_updates)
524
1986
  ? parsed.milestone_updates
525
1987
  : [];
1988
+ const resultEnvelope = {
1989
+ summary: userSummary,
1990
+ parsed_status: effectiveParsedStatus,
1991
+ task_updates: taskUpdates,
1992
+ milestone_updates: milestoneUpdates,
1993
+ next_actions: nextActions,
1994
+ artifacts: artifacts.map((artifact) => ({
1995
+ name: artifact.name,
1996
+ artifact_type: artifact.artifact_type ?? null,
1997
+ url: artifact.url ?? null,
1998
+ })),
1999
+ };
2000
+ const evidenceEnvelope = {
2001
+ artifacts: artifacts.map((artifact) => ({
2002
+ name: artifact.name,
2003
+ artifact_type: artifact.artifact_type ?? null,
2004
+ source_pointer: artifact.url ?? null,
2005
+ })),
2006
+ files: [slice.outputPath, slice.logPath].filter(Boolean),
2007
+ logs: [slice.logPath].filter(Boolean),
2008
+ };
2009
+ let blockingDecisionQueued = false;
2010
+ const blockingDecisionIds = [];
2011
+ const nonBlockingDecisionIds = [];
526
2012
  for (const decision of decisions) {
527
- await requestDecisionSafe({
2013
+ const isBlocking = typeof decision.blocking === "boolean" ? decision.blocking : defaultDecisionBlocking;
2014
+ const normalizedQuestion = decision.question.trim();
2015
+ const decisionResult = await requestDecisionQueued({
528
2016
  initiativeId: run.initiativeId,
529
2017
  correlationId: slice.runId,
530
- title: decision.question.trim(),
2018
+ title: normalizedQuestion,
531
2019
  summary: decision.summary ?? parsed?.summary ?? null,
532
2020
  urgency: decision.urgency ?? "high",
533
2021
  options: Array.isArray(decision.options)
534
2022
  ? decision.options.filter((opt) => typeof opt === "string" && opt.trim())
535
2023
  : [],
536
- blocking: typeof decision.blocking === "boolean" ? decision.blocking : defaultDecisionBlocking,
2024
+ blocking: isBlocking,
2025
+ decisionType: isBlocking
2026
+ ? "autopilot_blocking_decision"
2027
+ : "autopilot_followup_decision",
2028
+ workstreamId: slice.workstreamId,
2029
+ agentId: slice.agentId,
2030
+ sourceSystem: "orgx-autopilot",
2031
+ conflictSource: parsedStatus === "needs_decision"
2032
+ ? "slice_needs_decision"
2033
+ : "slice_reported_decision",
2034
+ dedupeKey: [
2035
+ "autopilot",
2036
+ run.initiativeId,
2037
+ slice.workstreamId,
2038
+ "slice_reported_decision",
2039
+ normalizedQuestion.toLowerCase(),
2040
+ ].join(":"),
2041
+ recommendedAction: nextActions[0] ??
2042
+ "Resolve this decision to continue the slice or safely defer workstream execution.",
2043
+ sourceRunId: slice.runId,
2044
+ sourceRef: {
2045
+ run_id: slice.runId,
2046
+ workstream_id: slice.workstreamId,
2047
+ parsed_status: parsedStatus,
2048
+ },
2049
+ evidenceRefs: [
2050
+ {
2051
+ evidence_type: "slice_output_summary",
2052
+ title: "Slice requested a decision",
2053
+ summary: decision.summary ?? parsed?.summary ?? "Decision required by slice output.",
2054
+ source_pointer: slice.outputPath,
2055
+ payload: {
2056
+ log_path: slice.logPath,
2057
+ blocking: isBlocking,
2058
+ },
2059
+ },
2060
+ ...artifactEvidenceRefs,
2061
+ ],
537
2062
  });
2063
+ if (decisionResult.queued && isBlocking)
2064
+ blockingDecisionQueued = true;
2065
+ if (decisionResult.decisionIds.length > 0) {
2066
+ if (isBlocking)
2067
+ blockingDecisionIds.push(...decisionResult.decisionIds);
2068
+ else
2069
+ nonBlockingDecisionIds.push(...decisionResult.decisionIds);
2070
+ }
538
2071
  }
2072
+ const decisionIds = Array.from(new Set([...blockingDecisionIds, ...nonBlockingDecisionIds]));
539
2073
  for (const artifact of artifacts) {
540
2074
  await registerArtifactSafe({
541
2075
  initiativeId: run.initiativeId,
@@ -543,16 +2077,86 @@ export function createAutoContinueEngine(deps) {
543
2077
  agentId: slice.agentId,
544
2078
  agentName: slice.agentName,
545
2079
  workstreamId: slice.workstreamId,
2080
+ fallbackMilestoneId: slice.milestoneIds[0] ?? null,
2081
+ fallbackTaskIds: slice.taskIds,
546
2082
  artifact,
2083
+ isMockWorker: slice.isMockWorker,
547
2084
  });
548
2085
  }
2086
+ // --- Proof ladder gate: check completion tasks for proof readiness ---
2087
+ // Phase 1: warn-only. Does not block status transitions but creates
2088
+ // a decision request when proof is missing for done/completed tasks.
2089
+ const doneTaskUpdates = taskUpdates.filter((tu) => tu.status === "done" || tu.status === "completed");
2090
+ if (doneTaskUpdates.length > 0 && !slice.isMockWorker) {
2091
+ const proofStrictness = process.env.ORGX_PROOF_STRICTNESS ?? "warn";
2092
+ for (const dtu of doneTaskUpdates) {
2093
+ try {
2094
+ const qp = new URLSearchParams({ task_id: dtu.task_id });
2095
+ const proofResult = await client.rawRequest("GET", `/api/flywheel/proof-status?${qp.toString()}`).catch(() => null);
2096
+ // If proof API unavailable, skip gracefully (phase 1)
2097
+ if (!proofResult)
2098
+ continue;
2099
+ const overallPassed = proofResult?.overall_passed === true;
2100
+ if (!overallPassed && proofStrictness === "block") {
2101
+ // Hard block: downgrade to needs_review
2102
+ dtu.status = "needs_review";
2103
+ const reasonCodes = Array.isArray(proofResult?.reason_codes)
2104
+ ? proofResult.reason_codes.join(", ")
2105
+ : "incomplete_proof";
2106
+ await requestDecisionSafe({
2107
+ initiativeId: run.initiativeId,
2108
+ correlationId: slice.runId,
2109
+ title: `Task ${dtu.task_id} missing proof for completion`,
2110
+ summary: `Proof chain incomplete (${reasonCodes}). Task held in needs_review until proof is resolved.`,
2111
+ urgency: "high",
2112
+ blocking: true,
2113
+ decisionType: "proof_incomplete",
2114
+ workstreamId: slice.workstreamId,
2115
+ agentId: slice.agentId,
2116
+ sourceRunId: slice.runId,
2117
+ dedupeKey: `proof-gate:${dtu.task_id}:${slice.runId}`,
2118
+ metadata: { proof_result: proofResult },
2119
+ });
2120
+ }
2121
+ else if (!overallPassed) {
2122
+ // Warn-only: emit activity but allow transition
2123
+ await emitActivitySafe({
2124
+ initiativeId: run.initiativeId,
2125
+ runId: slice.runId,
2126
+ correlationId: slice.runId,
2127
+ phase: "review",
2128
+ level: "warn",
2129
+ message: `Task ${dtu.task_id} completing with incomplete proof chain.`,
2130
+ metadata: {
2131
+ event: "proof_gate_warning",
2132
+ task_id: dtu.task_id,
2133
+ proof_result: proofResult,
2134
+ },
2135
+ });
2136
+ }
2137
+ }
2138
+ catch {
2139
+ // Best-effort proof check; don't block on transient failures
2140
+ }
2141
+ }
2142
+ }
549
2143
  const statusUpdateResult = await applyAgentStatusUpdatesSafe({
550
2144
  initiativeId: run.initiativeId,
551
2145
  runId: slice.runId,
552
2146
  correlationId: slice.runId,
553
2147
  taskUpdates,
554
2148
  milestoneUpdates,
2149
+ isMockWorker: slice.isMockWorker,
555
2150
  });
2151
+ if (statusUpdateResult.taskUpdates.length > 0 ||
2152
+ statusUpdateResult.milestoneUpdates.length > 0) {
2153
+ recordLocalStatusOverrides({
2154
+ initiativeId: run.initiativeId,
2155
+ updatedAt: now,
2156
+ taskUpdates: statusUpdateResult.taskUpdates,
2157
+ milestoneUpdates: statusUpdateResult.milestoneUpdates,
2158
+ });
2159
+ }
556
2160
  try {
557
2161
  writeRuntimeEvent({
558
2162
  sourceClient: slice.sourceClient,
@@ -564,67 +2168,166 @@ export function createAutoContinueEngine(deps) {
564
2168
  agentId: slice.agentId,
565
2169
  agentName: slice.agentName ?? null,
566
2170
  phase: slice.status === "completed" ? "completed" : "blocked",
567
- message: parsed?.summary ?? slice.lastError ?? "Autopilot slice finished.",
2171
+ message: userSummary ?? slice.lastError ?? "Autopilot slice finished.",
568
2172
  metadata: {
569
2173
  event: "autopilot_slice_finished",
2174
+ initiative_id: run.initiativeId,
2175
+ run_id: slice.runId,
2176
+ slice_run_id: slice.runId,
2177
+ workstream_id: slice.workstreamId,
2178
+ correlation_id: slice.runId,
570
2179
  requested_by_agent_id: run.agentId,
571
2180
  requested_by_agent_name: run.agentName,
572
2181
  status: effectiveParsedStatus,
573
2182
  artifacts: artifacts.length,
574
- decisions: decisions.length,
575
- blocking_decisions: blockingDecisionCount,
576
- non_blocking_decisions: nonBlockingDecisionCount,
2183
+ decisions: allDecisions.length,
2184
+ blocking_decisions: normalizedBlockingDecisionCount,
2185
+ non_blocking_decisions: normalizedNonBlockingDecisionCount,
577
2186
  status_updates: statusUpdateResult.applied,
578
2187
  status_updates_buffered: statusUpdateResult.buffered,
2188
+ reported_skill_evidence_count: skillEvidence.length,
2189
+ reported_skill_sha256_count: reportedSkillSha256Count,
2190
+ reported_skill_names: reportedSkillNames,
2191
+ action_type: normalizeActivityActionType("run_completed"),
2192
+ action_phase: normalizeActivityActionPhase(slice.status === "completed" ? "completed" : "blocked"),
2193
+ result: resultEnvelope,
2194
+ evidence: evidenceEnvelope,
2195
+ ...mockMeta(slice),
2196
+ user_summary: userSummary,
2197
+ next_actions: nextActions,
579
2198
  },
580
2199
  });
581
2200
  }
582
2201
  catch {
583
2202
  // best effort
584
2203
  }
2204
+ if (slice.status === "completed") {
2205
+ await emitActivitySafe({
2206
+ initiativeId: run.initiativeId,
2207
+ runId: slice.runId,
2208
+ correlationId: slice.runId,
2209
+ phase: "handoff",
2210
+ level: "info",
2211
+ message: `Handoff ready for ${slice.workstreamTitle ?? slice.workstreamId}.`,
2212
+ progressPct: 80,
2213
+ nextStep: nextStepHint,
2214
+ metadata: buildSliceEnrichment({
2215
+ run,
2216
+ slice,
2217
+ workstreamId: slice.workstreamId,
2218
+ workstreamTitle: slice.workstreamTitle ?? null,
2219
+ domain: slice.domain,
2220
+ requiredSkills: slice.requiredSkills,
2221
+ nextActions,
2222
+ userSummary,
2223
+ event: "autopilot_slice_handoff",
2224
+ extra: {
2225
+ parsed_status: effectiveParsedStatus,
2226
+ artifacts: artifacts.length,
2227
+ decisions: decisions.length,
2228
+ decision_ids: decisionIds,
2229
+ output_path: slice.outputPath,
2230
+ log_path: slice.logPath,
2231
+ task_updates: taskUpdates,
2232
+ milestone_updates: milestoneUpdates,
2233
+ result: resultEnvelope,
2234
+ evidence: evidenceEnvelope,
2235
+ ...mockMeta(slice),
2236
+ },
2237
+ }),
2238
+ });
2239
+ }
585
2240
  await emitActivitySafe({
586
2241
  initiativeId: run.initiativeId,
587
2242
  runId: slice.runId,
588
2243
  correlationId: slice.runId,
589
2244
  phase: slice.status === "completed" ? "completed" : "blocked",
590
2245
  level: slice.status === "completed" ? "info" : "warn",
2246
+ progressPct: slice.status === "completed" ? 100 : 65,
2247
+ nextStep: nextStepHint,
591
2248
  message: slice.status === "completed"
592
2249
  ? `Autopilot slice completed for ${slice.workstreamTitle ?? slice.workstreamId} (${slice.taskIds.length} task${slice.taskIds.length === 1 ? "" : "s"}).`
593
2250
  : `Autopilot slice blocked: ${slice.workstreamTitle ?? slice.workstreamId}.`,
594
2251
  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,
2252
+ ...buildSliceEnrichment({
2253
+ run,
2254
+ slice,
2255
+ workstreamId: slice.workstreamId,
2256
+ workstreamTitle: slice.workstreamTitle ?? null,
2257
+ domain: slice.domain,
2258
+ requiredSkills: slice.requiredSkills,
2259
+ nextActions,
2260
+ userSummary,
2261
+ event: "autopilot_slice_result",
2262
+ }),
2263
+ error_location: slice.status === "completed"
2264
+ ? null
2265
+ : "mission-control.auto-continue.engine.slice.result",
2266
+ behavior_config_id: slice.behaviorConfigId,
2267
+ behavior_config_version: slice.behaviorConfigVersion,
2268
+ behavior_config_hash: slice.behaviorConfigHash,
2269
+ policy_source: slice.behaviorPolicySource,
2270
+ behavior_automation_level: slice.behaviorAutomationLevel,
605
2271
  parsed_status: effectiveParsedStatus,
606
2272
  has_output: Boolean(parsed),
607
2273
  artifacts: artifacts.length,
608
- decisions: decisions.length,
609
- blocking_decisions: blockingDecisionCount,
610
- non_blocking_decisions: nonBlockingDecisionCount,
611
- decision_required: blockingDecisionCount > 0,
2274
+ decisions: allDecisions.length,
2275
+ blocking_decisions: normalizedBlockingDecisionCount,
2276
+ non_blocking_decisions: normalizedNonBlockingDecisionCount,
2277
+ decision_ids: decisionIds,
2278
+ blocking_decision_ids: Array.from(new Set(blockingDecisionIds)),
2279
+ non_blocking_decision_ids: Array.from(new Set(nonBlockingDecisionIds)),
2280
+ decision_required: blockingDecisionQueued || effectiveParsedStatus === "needs_decision",
612
2281
  status_updates_applied: statusUpdateResult.applied,
613
2282
  status_updates_buffered: statusUpdateResult.buffered,
2283
+ reported_skill_evidence_count: skillEvidence.length,
2284
+ reported_skill_sha256_count: reportedSkillSha256Count,
2285
+ reported_skill_names: reportedSkillNames,
614
2286
  output_path: slice.outputPath,
615
2287
  log_path: slice.logPath,
616
2288
  error: slice.lastError,
2289
+ next_actions: nextActions,
2290
+ task_updates: taskUpdates,
2291
+ milestone_updates: milestoneUpdates,
2292
+ result: resultEnvelope,
2293
+ evidence: evidenceEnvelope,
2294
+ ...mockMeta(slice),
2295
+ user_summary: userSummary,
617
2296
  },
618
2297
  });
2298
+ // Append to local team context for cross-agent awareness on subsequent slices.
2299
+ if (slice.status === "completed") {
2300
+ try {
2301
+ appendTeamCompletion(run.initiativeId, {
2302
+ domain: slice.domain ?? "unknown",
2303
+ task_title: slice.workstreamTitle ?? slice.workstreamId,
2304
+ summary: parsed?.summary ?? "Completed.",
2305
+ key_outputs: artifacts.map((a) => a.name).filter(Boolean).slice(0, 5),
2306
+ completed_at: new Date().toISOString(),
2307
+ });
2308
+ }
2309
+ catch {
2310
+ // best effort: do not block the engine on store failure
2311
+ }
2312
+ }
619
2313
  if (slice.status !== "completed") {
620
- if (slice.status === "error" && decisions.length === 0) {
621
- await requestDecisionSafe({
2314
+ let fallbackDecisionResult = {
2315
+ queued: false,
2316
+ decisionIds: [],
2317
+ };
2318
+ if (!blockingDecisionQueued) {
2319
+ const blockedLike = slice.status === "blocked";
2320
+ fallbackDecisionResult = await requestDecisionQueued({
622
2321
  initiativeId: run.initiativeId,
623
2322
  correlationId: slice.runId,
624
- title: `Autopilot slice failed: ${slice.workstreamTitle ?? slice.workstreamId}`,
2323
+ title: blockedLike
2324
+ ? `Autopilot slice blocked: ${slice.workstreamTitle ?? slice.workstreamId}`
2325
+ : `Autopilot slice failed: ${slice.workstreamTitle ?? slice.workstreamId}`,
625
2326
  summary: parsed?.summary ??
626
2327
  slice.lastError ??
627
- "The slice failed without producing a valid output contract. Review logs/output and decide whether to retry or pause autopilot.",
2328
+ (blockedLike
2329
+ ? "The slice reported a blocked/decision-required state without a blocking decision payload. Review logs/output and decide whether to retry, unblock, or skip."
2330
+ : "The slice failed without producing a valid output contract. Review logs/output and decide whether to retry or pause autopilot."),
628
2331
  urgency: "high",
629
2332
  options: [
630
2333
  "Retry this workstream slice",
@@ -632,14 +2335,66 @@ export function createAutoContinueEngine(deps) {
632
2335
  "Skip this workstream for now",
633
2336
  ],
634
2337
  blocking: true,
2338
+ decisionType: blockedLike ? "autopilot_blocked_without_decision" : "autopilot_failure",
2339
+ workstreamId: slice.workstreamId,
2340
+ agentId: slice.agentId,
2341
+ sourceSystem: "orgx-autopilot",
2342
+ conflictSource: blockedLike
2343
+ ? "slice_missing_blocking_decision"
2344
+ : "slice_invalid_output",
2345
+ dedupeKey: [
2346
+ "autopilot",
2347
+ run.initiativeId,
2348
+ slice.workstreamId,
2349
+ blockedLike ? "slice_missing_blocking_decision" : "slice_invalid_output",
2350
+ ].join(":"),
2351
+ recommendedAction: nextActions[0] ??
2352
+ "Review the output contract and logs, then retry or pause autopilot until the blocker is resolved.",
2353
+ sourceRunId: slice.runId,
2354
+ sourceRef: {
2355
+ run_id: slice.runId,
2356
+ workstream_id: slice.workstreamId,
2357
+ parsed_status: effectiveParsedStatus,
2358
+ },
2359
+ evidenceRefs: [
2360
+ {
2361
+ evidence_type: "slice_output_validation",
2362
+ title: "Slice output requires fallback decision",
2363
+ summary: parsed?.summary ??
2364
+ slice.lastError ??
2365
+ "Slice did not provide a blocking decision payload.",
2366
+ source_pointer: slice.outputPath,
2367
+ payload: {
2368
+ log_path: slice.logPath,
2369
+ parsed_status: effectiveParsedStatus,
2370
+ },
2371
+ },
2372
+ ...artifactEvidenceRefs,
2373
+ ],
635
2374
  });
636
2375
  }
2376
+ setLaneState(run, {
2377
+ workstreamId: slice.workstreamId,
2378
+ state: "blocked",
2379
+ activeRunId: null,
2380
+ activeTaskIds: [],
2381
+ blockedReason: parsed?.summary ??
2382
+ slice.lastError ??
2383
+ `Slice returned status: ${effectiveParsedStatus}`,
2384
+ waitingOnWorkstreamIds: [],
2385
+ retryAt: null,
2386
+ });
2387
+ if (!run.blockedWorkstreamIds.includes(slice.workstreamId)) {
2388
+ run.blockedWorkstreamIds.push(slice.workstreamId);
2389
+ }
637
2390
  await stopAutoContinueRun({
638
2391
  run,
639
2392
  reason: slice.status === "error" ? "error" : "blocked",
640
2393
  error: parsed?.summary ??
641
2394
  slice.lastError ??
642
2395
  `Slice returned status: ${effectiveParsedStatus}`,
2396
+ decisionRequired: blockingDecisionQueued || fallbackDecisionResult.queued,
2397
+ decisionIds: Array.from(new Set([...decisionIds, ...fallbackDecisionResult.decisionIds])),
643
2398
  });
644
2399
  return;
645
2400
  }
@@ -654,7 +2409,7 @@ export function createAutoContinueEngine(deps) {
654
2409
  const attentionSummary = completionHadNoOutcome
655
2410
  ? "The slice reported completion but did not produce artifacts or status updates. Decide whether to retry, request stronger output, or mark tasks manually."
656
2411
  : "The slice exited without a valid output contract. Review logs/output and decide whether to retry or pause autopilot.";
657
- await requestDecisionSafe({
2412
+ const decisionResult = await requestDecisionQueued({
658
2413
  initiativeId: run.initiativeId,
659
2414
  correlationId: slice.runId,
660
2415
  title: attentionTitle,
@@ -666,7 +2421,61 @@ export function createAutoContinueEngine(deps) {
666
2421
  "Skip this workstream for now",
667
2422
  ],
668
2423
  blocking: true,
2424
+ decisionType: completionHadNoOutcome
2425
+ ? "autopilot_completed_without_outcome"
2426
+ : "autopilot_failure",
2427
+ workstreamId: slice.workstreamId,
2428
+ agentId: slice.agentId,
2429
+ sourceSystem: "orgx-autopilot",
2430
+ conflictSource: completionHadNoOutcome
2431
+ ? "slice_completed_without_outcome"
2432
+ : "slice_invalid_output",
2433
+ dedupeKey: [
2434
+ "autopilot",
2435
+ run.initiativeId,
2436
+ slice.workstreamId,
2437
+ completionHadNoOutcome
2438
+ ? "slice_completed_without_outcome"
2439
+ : "slice_invalid_output",
2440
+ ].join(":"),
2441
+ recommendedAction: nextActions[0] ??
2442
+ "Verify slice outputs and status updates, then retry once or pause for investigation.",
2443
+ sourceRunId: slice.runId,
2444
+ sourceRef: {
2445
+ run_id: slice.runId,
2446
+ workstream_id: slice.workstreamId,
2447
+ parsed_status: parsedStatus,
2448
+ },
2449
+ evidenceRefs: [
2450
+ {
2451
+ evidence_type: "slice_output_validation",
2452
+ title: "Slice output needs verification",
2453
+ summary: attentionSummary,
2454
+ source_pointer: slice.outputPath,
2455
+ payload: {
2456
+ log_path: slice.logPath,
2457
+ parsed_status: parsedStatus,
2458
+ completion_had_no_outcome: completionHadNoOutcome,
2459
+ },
2460
+ },
2461
+ ...artifactEvidenceRefs,
2462
+ ],
669
2463
  });
2464
+ setLaneState(run, {
2465
+ workstreamId: slice.workstreamId,
2466
+ state: "blocked",
2467
+ activeRunId: null,
2468
+ activeTaskIds: [],
2469
+ blockedReason: slice.lastError ??
2470
+ (completionHadNoOutcome
2471
+ ? "Slice completed without verifiable outcomes."
2472
+ : "Slice failed or returned invalid output."),
2473
+ waitingOnWorkstreamIds: [],
2474
+ retryAt: null,
2475
+ });
2476
+ if (!run.blockedWorkstreamIds.includes(slice.workstreamId)) {
2477
+ run.blockedWorkstreamIds.push(slice.workstreamId);
2478
+ }
670
2479
  await stopAutoContinueRun({
671
2480
  run,
672
2481
  reason: completionHadNoOutcome ? "blocked" : "error",
@@ -674,13 +2483,31 @@ export function createAutoContinueEngine(deps) {
674
2483
  (completionHadNoOutcome
675
2484
  ? "Slice completed without verifiable outcomes."
676
2485
  : "Slice failed or returned invalid output."),
2486
+ decisionRequired: completionHadNoOutcome && decisionResult.queued,
2487
+ decisionIds: decisionResult.decisionIds,
677
2488
  });
678
2489
  return;
679
2490
  }
680
2491
  run.lastRunId = slice.runId;
681
- run.lastTaskId = run.activeTaskId ?? run.lastTaskId;
682
- run.activeRunId = null;
683
- run.activeTaskId = null;
2492
+ run.lastTaskId = slice.taskIds[0] ?? run.lastTaskId;
2493
+ removeActiveSliceFromRun(run, {
2494
+ sliceRunId: slice.runId,
2495
+ taskIds: slice.taskIds,
2496
+ workstreamId: slice.workstreamId,
2497
+ });
2498
+ setLaneState(run, {
2499
+ workstreamId: slice.workstreamId,
2500
+ state: "completed",
2501
+ activeRunId: null,
2502
+ activeTaskIds: [],
2503
+ blockedReason: null,
2504
+ waitingOnWorkstreamIds: [],
2505
+ retryAt: null,
2506
+ });
2507
+ run.blockedWorkstreamIds = run.blockedWorkstreamIds.filter((id) => id !== slice.workstreamId);
2508
+ syncLegacyRunPointers(run);
2509
+ // Do not keep prior rate-limit/runtime errors after a completed slice.
2510
+ run.lastError = null;
684
2511
  run.updatedAt = now;
685
2512
  try {
686
2513
  await updateInitiativeAutoContinueState({
@@ -691,6 +2518,50 @@ export function createAutoContinueEngine(deps) {
691
2518
  catch {
692
2519
  // best effort
693
2520
  }
2521
+ // Evaluate scope-level completion for milestone/workstream scopes.
2522
+ if (slice.scope && slice.scope !== "task") {
2523
+ try {
2524
+ const scopeGraph = applyLocalInitiativeOverrideToGraph(await buildMissionControlGraph(client, run.initiativeId));
2525
+ const scopeNodeById = new Map(scopeGraph.nodes.map((n) => [n.id, n]));
2526
+ const scopeResult = evaluateScopeCompletion({
2527
+ scope: slice.scope,
2528
+ milestoneIds: slice.scopeMilestoneIds ?? [],
2529
+ workstreamId: slice.workstreamId,
2530
+ nodeById: scopeNodeById,
2531
+ });
2532
+ if (scopeResult.scopeComplete) {
2533
+ await emitActivitySafe({
2534
+ initiativeId: run.initiativeId,
2535
+ runId: slice.runId,
2536
+ correlationId: slice.runId,
2537
+ phase: "completed",
2538
+ level: "info",
2539
+ progressPct: 100,
2540
+ nextStep: slice.scope === "milestone"
2541
+ ? "Queue the next milestone-ready slice."
2542
+ : "Select the next dispatchable workstream from Next Up.",
2543
+ message: `${slice.scope === "milestone" ? "Milestone" : "Workstream"} scope completed for ${slice.workstreamTitle ?? slice.workstreamId}.`,
2544
+ metadata: {
2545
+ ...buildSliceEnrichment({
2546
+ run,
2547
+ slice,
2548
+ workstreamId: slice.workstreamId,
2549
+ workstreamTitle: slice.workstreamTitle ?? null,
2550
+ domain: slice.domain,
2551
+ requiredSkills: slice.requiredSkills,
2552
+ event: "scope_completed",
2553
+ }),
2554
+ scope: slice.scope,
2555
+ milestone_ids: slice.scopeMilestoneIds,
2556
+ remaining_tasks: 0,
2557
+ },
2558
+ });
2559
+ }
2560
+ }
2561
+ catch {
2562
+ // best-effort scope completion check
2563
+ }
2564
+ }
694
2565
  if (run.stopAfterSlice) {
695
2566
  run.stopAfterSlice = false;
696
2567
  await stopAutoContinueRun({ run, reason: "completed" });
@@ -702,17 +2573,26 @@ export function createAutoContinueEngine(deps) {
702
2573
  }
703
2574
  }
704
2575
  }
2576
+ syncLegacyRunPointers(run);
705
2577
  if (run.stopRequested) {
706
2578
  run.status = "stopping";
707
2579
  run.updatedAt = now;
708
2580
  await stopAutoContinueRun({ run, reason: "stopped" });
709
2581
  return;
710
2582
  }
2583
+ const tokenBudgetValue = typeof run.tokenBudget === "number" && Number.isFinite(run.tokenBudget)
2584
+ ? run.tokenBudget
2585
+ : null;
711
2586
  // 2) Enforce token guardrail before starting a new slice.
712
- if (run.tokensUsed >= run.tokenBudget) {
2587
+ if (tokenBudgetValue !== null && run.tokensUsed >= tokenBudgetValue) {
713
2588
  await stopAutoContinueRun({ run, reason: "budget_exhausted" });
714
2589
  return;
715
2590
  }
2591
+ const activeSliceCount = listActiveSliceRunIds(run).length;
2592
+ if (activeSliceCount >= run.maxParallelSlices) {
2593
+ run.updatedAt = now;
2594
+ return;
2595
+ }
716
2596
  // 3) Pick next workstream slice and dispatch.
717
2597
  let graph;
718
2598
  try {
@@ -728,6 +2608,7 @@ export function createAutoContinueEngine(deps) {
728
2608
  }
729
2609
  const nodes = graph.nodes;
730
2610
  const nodeById = new Map(nodes.map((node) => [node.id, node]));
2611
+ applyLocalStatusOverridesToGraph(run.initiativeId, nodeById);
731
2612
  const taskNodes = nodes.filter((node) => node.type === "task");
732
2613
  const todoTasks = taskNodes.filter((node) => isTodoStatus(node.status));
733
2614
  if (todoTasks.length === 0) {
@@ -746,6 +2627,7 @@ export function createAutoContinueEngine(deps) {
746
2627
  };
747
2628
  // Select the next eligible workstream by scanning ordered todos.
748
2629
  let selectedWorkstreamId = null;
2630
+ let deferredBySpawnGuardRateLimit = 0;
749
2631
  for (const taskId of graph.recentTodos) {
750
2632
  const node = nodeById.get(taskId);
751
2633
  if (!node || node.type !== "task")
@@ -763,6 +2645,18 @@ export function createAutoContinueEngine(deps) {
763
2645
  }
764
2646
  if (!node.workstreamId)
765
2647
  continue;
2648
+ if (run.blockedWorkstreamIds.includes(node.workstreamId))
2649
+ continue;
2650
+ const lane = run.laneByWorkstreamId[node.workstreamId] ?? null;
2651
+ if (lane?.state === "running" && lane.activeRunId)
2652
+ continue;
2653
+ if (lane?.state === "rate_limited" && lane.retryAt) {
2654
+ const retryAtMs = Date.parse(lane.retryAt);
2655
+ if (Number.isFinite(retryAtMs) && retryAtMs > Date.now()) {
2656
+ deferredBySpawnGuardRateLimit += 1;
2657
+ continue;
2658
+ }
2659
+ }
766
2660
  const ws = nodeById.get(node.workstreamId);
767
2661
  if (ws && !isDispatchableWorkstreamStatus(ws.status))
768
2662
  continue;
@@ -770,10 +2664,83 @@ export function createAutoContinueEngine(deps) {
770
2664
  continue;
771
2665
  if (taskHasBlockedParent(node))
772
2666
  continue;
2667
+ const retryAtMs = getSpawnGuardRetryAtMs(run.initiativeId, node.id);
2668
+ if (retryAtMs > 0) {
2669
+ deferredBySpawnGuardRateLimit += 1;
2670
+ continue;
2671
+ }
773
2672
  selectedWorkstreamId = node.workstreamId;
774
2673
  break;
775
2674
  }
776
2675
  if (!selectedWorkstreamId) {
2676
+ const waitingByWorkstream = new Map();
2677
+ for (const task of taskNodes) {
2678
+ if (!isTodoStatus(task.status))
2679
+ continue;
2680
+ if (!run.includeVerification &&
2681
+ typeof task.title === "string" &&
2682
+ /^verification[ \t]+scenario/i.test(task.title)) {
2683
+ continue;
2684
+ }
2685
+ const workstreamId = (task.workstreamId ?? "").trim();
2686
+ if (!workstreamId)
2687
+ continue;
2688
+ if (Array.isArray(run.allowedWorkstreamIds) &&
2689
+ run.allowedWorkstreamIds.length > 0 &&
2690
+ !run.allowedWorkstreamIds.includes(workstreamId)) {
2691
+ continue;
2692
+ }
2693
+ if (run.blockedWorkstreamIds.includes(workstreamId)) {
2694
+ continue;
2695
+ }
2696
+ const blockedParents = taskHasBlockedParent(task);
2697
+ const unresolvedDepWorkstreamIds = task.dependencyIds
2698
+ .map((depId) => nodeById.get(depId))
2699
+ .filter((dep) => Boolean(dep && !isDoneStatus(dep.status)))
2700
+ .map((dep) => (dep.workstreamId ?? "").trim())
2701
+ .filter(Boolean);
2702
+ if (blockedParents || unresolvedDepWorkstreamIds.length > 0) {
2703
+ const existing = waitingByWorkstream.get(workstreamId) ?? [];
2704
+ waitingByWorkstream.set(workstreamId, dedupeStrings([...existing, ...unresolvedDepWorkstreamIds]));
2705
+ }
2706
+ }
2707
+ for (const [workstreamId, waitingOnWorkstreamIds] of waitingByWorkstream.entries()) {
2708
+ setLaneState(run, {
2709
+ workstreamId,
2710
+ state: "waiting_dependency",
2711
+ activeRunId: null,
2712
+ activeTaskIds: [],
2713
+ blockedReason: null,
2714
+ waitingOnWorkstreamIds,
2715
+ retryAt: null,
2716
+ });
2717
+ }
2718
+ if (listActiveSliceRunIds(run).length > 0) {
2719
+ run.updatedAt = now;
2720
+ return;
2721
+ }
2722
+ if (deferredBySpawnGuardRateLimit > 0) {
2723
+ run.updatedAt = now;
2724
+ return;
2725
+ }
2726
+ if (run.allowedWorkstreamIds && run.allowedWorkstreamIds.length > 0) {
2727
+ const scopedTodoCount = taskNodes.filter((node) => {
2728
+ if (!isTodoStatus(node.status))
2729
+ return false;
2730
+ if (!run.includeVerification &&
2731
+ typeof node.title === "string" &&
2732
+ /^verification[ \t]+scenario/i.test(node.title)) {
2733
+ return false;
2734
+ }
2735
+ if (!node.workstreamId)
2736
+ return false;
2737
+ return run.allowedWorkstreamIds?.includes(node.workstreamId) ?? false;
2738
+ }).length;
2739
+ if (scopedTodoCount === 0) {
2740
+ await stopAutoContinueRun({ run, reason: "completed" });
2741
+ return;
2742
+ }
2743
+ }
777
2744
  await stopAutoContinueRun({ run, reason: "blocked" });
778
2745
  return;
779
2746
  }
@@ -781,19 +2748,21 @@ export function createAutoContinueEngine(deps) {
781
2748
  const workstreamTitle = workstreamNode?.title ?? null;
782
2749
  const initiativeNode = nodes.find((node) => node.type === "initiative") ?? null;
783
2750
  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);
2751
+ const scopeSelection = selectSliceTasksByScope({
2752
+ scope: run.scope,
2753
+ workstreamId: selectedWorkstreamId,
2754
+ recentTodos: graph.recentTodos,
2755
+ nodeById,
2756
+ includeVerification: run.includeVerification,
2757
+ });
2758
+ const sliceTaskNodes = scopeSelection.tasks;
2759
+ const scopeMilestoneIds = scopeSelection.milestoneIds;
795
2760
  const primaryTask = sliceTaskNodes[0] ?? null;
796
2761
  if (!primaryTask) {
2762
+ if (listActiveSliceRunIds(run).length > 0) {
2763
+ run.updatedAt = now;
2764
+ return;
2765
+ }
797
2766
  await stopAutoContinueRun({ run, reason: "blocked" });
798
2767
  return;
799
2768
  }
@@ -803,14 +2772,14 @@ export function createAutoContinueEngine(deps) {
803
2772
  ? Math.max(0, t.expectedDurationHours)
804
2773
  : 0), 0);
805
2774
  let tokenEstimate = estimateTokensForDurationHours(expectedDurationHours);
806
- const remainingTokens = run.tokenBudget - run.tokensUsed;
807
- if (remainingTokens <= 0) {
2775
+ const remainingTokens = tokenBudgetValue !== null ? tokenBudgetValue - run.tokensUsed : null;
2776
+ if (remainingTokens !== null && remainingTokens <= 0) {
808
2777
  await stopAutoContinueRun({ run, reason: "budget_exhausted" });
809
2778
  return;
810
2779
  }
811
2780
  // If the modeled slice exceeds the remaining budget, shrink the slice to fit rather than
812
2781
  // stopping immediately (Play should still dispatch at least the primary task when possible).
813
- if (tokenEstimate > 0 && tokenEstimate > remainingTokens) {
2782
+ if (remainingTokens !== null && tokenEstimate > 0 && tokenEstimate > remainingTokens) {
814
2783
  const nextSlice = [];
815
2784
  let hours = 0;
816
2785
  for (const task of sliceTaskNodes) {
@@ -832,12 +2801,300 @@ export function createAutoContinueEngine(deps) {
832
2801
  expectedDurationHours = hours;
833
2802
  tokenEstimate = estimateTokensForDurationHours(expectedDurationHours);
834
2803
  }
835
- if (tokenEstimate > 0 && tokenEstimate > remainingTokens) {
2804
+ if (remainingTokens !== null && tokenEstimate > 0 && tokenEstimate > remainingTokens) {
836
2805
  await stopAutoContinueRun({ run, reason: "budget_exhausted" });
837
2806
  return;
838
2807
  }
839
2808
  const executionPolicy = deriveExecutionPolicy(primaryTask, workstreamNode);
2809
+ const behaviorConfig = deriveBehaviorConfigContext(primaryTask, workstreamNode);
2810
+ const behaviorAutomationLevel = deriveBehaviorAutomationLevel(primaryTask, workstreamNode);
840
2811
  const sliceRunId = randomUUID();
2812
+ await emitActivitySafe({
2813
+ initiativeId: run.initiativeId,
2814
+ runId: sliceRunId,
2815
+ correlationId: sliceRunId,
2816
+ phase: "intent",
2817
+ level: "info",
2818
+ progressPct: 5,
2819
+ message: `Orchestrator selected ${workstreamTitle ?? selectedWorkstreamId} for the next slice.`,
2820
+ nextStep: `Preparing dispatch checks before spawning ${executionPolicy.domain} execution.`,
2821
+ metadata: {
2822
+ ...buildSliceEnrichment({
2823
+ run,
2824
+ taskId: primaryTask.id,
2825
+ taskTitle: primaryTask.title ?? null,
2826
+ workstreamId: selectedWorkstreamId,
2827
+ workstreamTitle: workstreamTitle ?? null,
2828
+ domain: executionPolicy.domain,
2829
+ requiredSkills: executionPolicy.requiredSkills,
2830
+ event: "orchestrator_dispatch",
2831
+ }),
2832
+ scope: run.scope,
2833
+ candidate_task_count: sliceTaskNodes.length,
2834
+ },
2835
+ });
2836
+ const behaviorConfigDrift = detectBehaviorConfigDrift({
2837
+ taskNode: primaryTask,
2838
+ workstreamNode,
2839
+ behaviorConfig,
2840
+ behaviorAutomationLevel,
2841
+ });
2842
+ if (behaviorConfigDrift) {
2843
+ await emitActivitySafe({
2844
+ initiativeId: run.initiativeId,
2845
+ runId: sliceRunId,
2846
+ correlationId: sliceRunId,
2847
+ phase: "review",
2848
+ level: "warn",
2849
+ progressPct: 15,
2850
+ message: `Behavior config drift detected for ${workstreamTitle ?? selectedWorkstreamId}; ` +
2851
+ `runtime behavior differs from declared workstream config.`,
2852
+ metadata: {
2853
+ ...buildSliceEnrichment({
2854
+ run,
2855
+ taskId: primaryTask.id,
2856
+ taskTitle: primaryTask.title ?? null,
2857
+ workstreamId: selectedWorkstreamId,
2858
+ workstreamTitle: workstreamTitle ?? null,
2859
+ domain: executionPolicy.domain,
2860
+ requiredSkills: executionPolicy.requiredSkills,
2861
+ event: "auto_continue_behavior_config_drift_detected",
2862
+ }),
2863
+ drift_fields: behaviorConfigDrift.fields,
2864
+ declared_behavior_config_id: behaviorConfigDrift.declared.configId,
2865
+ declared_behavior_config_version: behaviorConfigDrift.declared.version,
2866
+ declared_behavior_config_hash: behaviorConfigDrift.declared.hash,
2867
+ declared_policy_source: behaviorConfigDrift.declared.policySource,
2868
+ declared_behavior_context: behaviorConfigDrift.declared.context,
2869
+ declared_behavior_automation_level: behaviorConfigDrift.declared.automationLevel,
2870
+ runtime_behavior_config_id: behaviorConfigDrift.runtime.configId,
2871
+ runtime_behavior_config_version: behaviorConfigDrift.runtime.version,
2872
+ runtime_behavior_config_hash: behaviorConfigDrift.runtime.hash,
2873
+ runtime_policy_source: behaviorConfigDrift.runtime.policySource,
2874
+ runtime_behavior_context: behaviorConfigDrift.runtime.context,
2875
+ runtime_behavior_automation_level: behaviorConfigDrift.runtime.automationLevel,
2876
+ error_location: "mission-control.auto-continue.engine.behavior-config.drift",
2877
+ },
2878
+ nextStep: "Review task/workstream behavior metadata and reconcile the declared config if override is unintended.",
2879
+ });
2880
+ }
2881
+ if (behaviorConfig.requiresApproval) {
2882
+ const blockedReason = `Behavior config approval required before dispatch for ${workstreamTitle ?? selectedWorkstreamId}.`;
2883
+ await emitActivitySafe({
2884
+ initiativeId: run.initiativeId,
2885
+ runId: sliceRunId,
2886
+ correlationId: sliceRunId,
2887
+ phase: "blocked",
2888
+ level: "warn",
2889
+ progressPct: 20,
2890
+ message: blockedReason,
2891
+ metadata: {
2892
+ ...buildSliceEnrichment({
2893
+ run,
2894
+ taskId: primaryTask.id,
2895
+ taskTitle: primaryTask.title ?? null,
2896
+ workstreamId: selectedWorkstreamId,
2897
+ workstreamTitle: workstreamTitle ?? null,
2898
+ domain: executionPolicy.domain,
2899
+ requiredSkills: executionPolicy.requiredSkills,
2900
+ event: "auto_continue_behavior_config_approval_required",
2901
+ }),
2902
+ behavior_config_id: behaviorConfig.configId,
2903
+ behavior_config_version: behaviorConfig.version,
2904
+ behavior_config_hash: behaviorConfig.hash,
2905
+ behavior_approval_status: behaviorConfig.approvalStatus,
2906
+ behavior_approval_decision_id: behaviorConfig.approvalDecisionId,
2907
+ blocked_reason: blockedReason,
2908
+ error_location: "mission-control.auto-continue.engine.behavior-config.approval",
2909
+ },
2910
+ nextStep: "Approve the behavior config, then rerun Play/auto-continue for this workstream.",
2911
+ });
2912
+ const decisionResult = await requestDecisionQueued({
2913
+ initiativeId: run.initiativeId,
2914
+ correlationId: sliceRunId,
2915
+ title: `Approve behavior config for ${workstreamTitle ?? selectedWorkstreamId}`,
2916
+ summary: [
2917
+ `Autopilot paused before dispatch because behavior config requires approval.`,
2918
+ `Task: ${primaryTask.id}.`,
2919
+ behaviorConfig.configId ? `Config: ${behaviorConfig.configId}.` : "",
2920
+ behaviorConfig.version ? `Version: ${behaviorConfig.version}.` : "",
2921
+ behaviorConfig.approvalStatus ? `Approval status: ${behaviorConfig.approvalStatus}.` : "",
2922
+ ]
2923
+ .filter(Boolean)
2924
+ .join(" "),
2925
+ urgency: "high",
2926
+ options: [
2927
+ "Approve config and continue execution",
2928
+ "Reject config and revise policy",
2929
+ "Pause this workstream",
2930
+ ],
2931
+ blocking: true,
2932
+ decisionType: "autopilot_behavior_config_approval",
2933
+ workstreamId: selectedWorkstreamId,
2934
+ agentId: run.agentId,
2935
+ sourceSystem: "orgx-autopilot",
2936
+ conflictSource: "behavior_config_requires_approval",
2937
+ dedupeKey: [
2938
+ "autopilot",
2939
+ run.initiativeId,
2940
+ selectedWorkstreamId,
2941
+ "behavior_config_requires_approval",
2942
+ behaviorConfig.configId ?? "default",
2943
+ behaviorConfig.version ?? "unknown",
2944
+ ].join(":"),
2945
+ recommendedAction: "Resolve approval state before allowing autopilot to spawn a worker.",
2946
+ sourceRunId: sliceRunId,
2947
+ sourceRef: {
2948
+ run_id: sliceRunId,
2949
+ workstream_id: selectedWorkstreamId,
2950
+ task_id: primaryTask.id,
2951
+ behavior_config_id: behaviorConfig.configId,
2952
+ behavior_approval_status: behaviorConfig.approvalStatus,
2953
+ behavior_approval_decision_id: behaviorConfig.approvalDecisionId,
2954
+ },
2955
+ });
2956
+ if (!run.blockedWorkstreamIds.includes(selectedWorkstreamId)) {
2957
+ run.blockedWorkstreamIds.push(selectedWorkstreamId);
2958
+ }
2959
+ setLaneState(run, {
2960
+ workstreamId: selectedWorkstreamId,
2961
+ state: "blocked",
2962
+ activeRunId: null,
2963
+ activeTaskIds: [],
2964
+ blockedReason,
2965
+ waitingOnWorkstreamIds: [],
2966
+ retryAt: null,
2967
+ });
2968
+ await stopAutoContinueRun({
2969
+ run,
2970
+ reason: "blocked",
2971
+ error: blockedReason,
2972
+ decisionRequired: decisionResult.queued,
2973
+ decisionIds: decisionResult.decisionIds,
2974
+ });
2975
+ return;
2976
+ }
2977
+ const isManualPlayDispatch = run.stopAfterSlice &&
2978
+ Array.isArray(run.allowedWorkstreamIds) &&
2979
+ run.allowedWorkstreamIds.length === 1;
2980
+ if (behaviorAutomationLevel === "manual" && !isManualPlayDispatch) {
2981
+ const blockedReason = `Automation level manual prevents auto-continue dispatch for ${workstreamTitle ?? selectedWorkstreamId}.`;
2982
+ await emitActivitySafe({
2983
+ initiativeId: run.initiativeId,
2984
+ runId: sliceRunId,
2985
+ correlationId: sliceRunId,
2986
+ phase: "blocked",
2987
+ level: "warn",
2988
+ progressPct: 20,
2989
+ message: blockedReason,
2990
+ metadata: {
2991
+ ...buildSliceEnrichment({
2992
+ run,
2993
+ taskId: primaryTask.id,
2994
+ taskTitle: primaryTask.title ?? null,
2995
+ workstreamId: selectedWorkstreamId,
2996
+ workstreamTitle: workstreamTitle ?? null,
2997
+ domain: executionPolicy.domain,
2998
+ requiredSkills: executionPolicy.requiredSkills,
2999
+ event: "auto_continue_behavior_automation_manual_blocked",
3000
+ }),
3001
+ behavior_config_id: behaviorConfig.configId,
3002
+ behavior_config_version: behaviorConfig.version,
3003
+ behavior_automation_level: behaviorAutomationLevel,
3004
+ blocked_reason: blockedReason,
3005
+ error_location: "mission-control.auto-continue.engine.behavior.automation.manual",
3006
+ },
3007
+ nextStep: "Use manual Play to dispatch this workstream slice.",
3008
+ });
3009
+ const decisionResult = await requestDecisionQueued({
3010
+ initiativeId: run.initiativeId,
3011
+ correlationId: sliceRunId,
3012
+ title: `Manual dispatch required for ${workstreamTitle ?? selectedWorkstreamId}`,
3013
+ summary: [
3014
+ "Autopilot paused because behavior automation level is manual.",
3015
+ `Task: ${primaryTask.id}.`,
3016
+ behaviorConfig.configId ? `Config: ${behaviorConfig.configId}.` : "",
3017
+ behaviorConfig.version ? `Version: ${behaviorConfig.version}.` : "",
3018
+ ]
3019
+ .filter(Boolean)
3020
+ .join(" "),
3021
+ urgency: "high",
3022
+ options: [
3023
+ "Dispatch this workstream manually now",
3024
+ "Switch automation level to supervised",
3025
+ "Switch automation level to auto",
3026
+ ],
3027
+ blocking: true,
3028
+ decisionType: "autopilot_behavior_manual_dispatch_required",
3029
+ workstreamId: selectedWorkstreamId,
3030
+ agentId: run.agentId,
3031
+ sourceSystem: "orgx-autopilot",
3032
+ conflictSource: "behavior_automation_level_manual",
3033
+ dedupeKey: [
3034
+ "autopilot",
3035
+ run.initiativeId,
3036
+ selectedWorkstreamId,
3037
+ "behavior_automation_level_manual",
3038
+ behaviorConfig.configId ?? "default",
3039
+ behaviorConfig.version ?? "unknown",
3040
+ ].join(":"),
3041
+ recommendedAction: "Dispatch manually for this workstream, or switch behavior automation level before rerunning auto-continue.",
3042
+ sourceRunId: sliceRunId,
3043
+ sourceRef: {
3044
+ run_id: sliceRunId,
3045
+ workstream_id: selectedWorkstreamId,
3046
+ task_id: primaryTask.id,
3047
+ behavior_config_id: behaviorConfig.configId,
3048
+ behavior_automation_level: behaviorAutomationLevel,
3049
+ },
3050
+ });
3051
+ if (!run.blockedWorkstreamIds.includes(selectedWorkstreamId)) {
3052
+ run.blockedWorkstreamIds.push(selectedWorkstreamId);
3053
+ }
3054
+ setLaneState(run, {
3055
+ workstreamId: selectedWorkstreamId,
3056
+ state: "blocked",
3057
+ activeRunId: null,
3058
+ activeTaskIds: [],
3059
+ blockedReason,
3060
+ waitingOnWorkstreamIds: [],
3061
+ retryAt: null,
3062
+ });
3063
+ await stopAutoContinueRun({
3064
+ run,
3065
+ reason: "blocked",
3066
+ error: blockedReason,
3067
+ decisionRequired: decisionResult.queued,
3068
+ decisionIds: decisionResult.decisionIds,
3069
+ });
3070
+ return;
3071
+ }
3072
+ if (behaviorAutomationLevel === "supervised" && !run.stopAfterSlice) {
3073
+ run.stopAfterSlice = true;
3074
+ await emitActivitySafe({
3075
+ initiativeId: run.initiativeId,
3076
+ runId: sliceRunId,
3077
+ correlationId: sliceRunId,
3078
+ phase: "execution",
3079
+ level: "info",
3080
+ progressPct: 25,
3081
+ message: `Supervised automation level: dispatching one slice for ${workstreamTitle ?? selectedWorkstreamId}.`,
3082
+ metadata: {
3083
+ ...buildSliceEnrichment({
3084
+ run,
3085
+ taskId: primaryTask.id,
3086
+ taskTitle: primaryTask.title ?? null,
3087
+ workstreamId: selectedWorkstreamId,
3088
+ workstreamTitle: workstreamTitle ?? null,
3089
+ domain: executionPolicy.domain,
3090
+ requiredSkills: executionPolicy.requiredSkills,
3091
+ event: "auto_continue_behavior_automation_supervised_one_shot",
3092
+ }),
3093
+ behavior_automation_level: behaviorAutomationLevel,
3094
+ },
3095
+ nextStep: "Resume to dispatch the next slice after this one completes.",
3096
+ });
3097
+ }
841
3098
  const spawnGuardResult = await checkSpawnGuardSafe({
842
3099
  domain: executionPolicy.domain,
843
3100
  taskId: primaryTask.id,
@@ -850,60 +3107,224 @@ export function createAutoContinueEngine(deps) {
850
3107
  const allowed = spawnGuardResult.allowed;
851
3108
  if (allowed === false) {
852
3109
  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({
3110
+ const retryable = spawnGuardIsRateLimited(spawnGuardResult);
3111
+ const rateLimitOverrideRequested = retryable && run.ignoreSpawnGuardRateLimit;
3112
+ if (retryable && !rateLimitOverrideRequested) {
3113
+ const retryAtMs = Date.now() + AUTO_CONTINUE_SPAWN_GUARD_RETRY_MS;
3114
+ const retryAtIso = new Date(retryAtMs).toISOString();
3115
+ autoContinueSpawnGuardRetryByTask.set(primaryTask.id, {
862
3116
  initiativeId: run.initiativeId,
863
- taskId: primaryTask.id,
3117
+ retryAtMs,
3118
+ });
3119
+ setLaneState(run, {
864
3120
  workstreamId: selectedWorkstreamId,
865
- milestoneId: primaryTask.milestoneId,
3121
+ state: "rate_limited",
3122
+ activeRunId: null,
3123
+ activeTaskIds: [],
3124
+ blockedReason,
3125
+ waitingOnWorkstreamIds: [],
3126
+ retryAt: retryAtIso,
3127
+ });
3128
+ await emitActivitySafe({
3129
+ initiativeId: run.initiativeId,
3130
+ runId: sliceRunId,
866
3131
  correlationId: sliceRunId,
3132
+ phase: "blocked",
3133
+ level: "warn",
3134
+ progressPct: 25,
3135
+ message: `Autopilot spawn guard rate-limited ${workstreamTitle ?? selectedWorkstreamId}; retrying shortly.`,
3136
+ metadata: {
3137
+ ...buildSliceEnrichment({
3138
+ run,
3139
+ taskId: primaryTask.id,
3140
+ taskTitle: primaryTask.title ?? null,
3141
+ workstreamId: selectedWorkstreamId,
3142
+ workstreamTitle: workstreamTitle ?? null,
3143
+ domain: executionPolicy.domain,
3144
+ requiredSkills: executionPolicy.requiredSkills,
3145
+ event: "auto_continue_spawn_guard_rate_limited",
3146
+ }),
3147
+ blocked_reason: blockedReason,
3148
+ error_location: "mission-control.auto-continue.engine.spawn-guard.rate-limited",
3149
+ next_retry_at: retryAtIso,
3150
+ next_retry_in_ms: AUTO_CONTINUE_SPAWN_GUARD_RETRY_MS,
3151
+ spawn_guard: spawnGuardResult,
3152
+ },
3153
+ nextStep: "Retry dispatch when spawn rate limits recover.",
867
3154
  });
3155
+ run.lastError = blockedReason;
3156
+ run.updatedAt = now;
3157
+ syncLegacyRunPointers(run);
3158
+ try {
3159
+ await updateInitiativeAutoContinueState({
3160
+ initiativeId: run.initiativeId,
3161
+ run,
3162
+ });
3163
+ }
3164
+ catch {
3165
+ // best effort
3166
+ }
3167
+ return;
868
3168
  }
869
- catch {
870
- // best effort
3169
+ if (rateLimitOverrideRequested) {
3170
+ const overrideMode = run.stopAfterSlice &&
3171
+ Array.isArray(run.allowedWorkstreamIds) &&
3172
+ run.allowedWorkstreamIds.length === 1
3173
+ ? "Play"
3174
+ : "Auto-continue";
3175
+ await emitActivitySafe({
3176
+ initiativeId: run.initiativeId,
3177
+ runId: sliceRunId,
3178
+ correlationId: sliceRunId,
3179
+ phase: "execution",
3180
+ level: "warn",
3181
+ progressPct: 25,
3182
+ message: `${overrideMode} override: dispatching ${workstreamTitle ?? selectedWorkstreamId} despite spawn guard rate limit.`,
3183
+ metadata: {
3184
+ ...buildSliceEnrichment({
3185
+ run,
3186
+ taskId: primaryTask.id,
3187
+ taskTitle: primaryTask.title ?? null,
3188
+ workstreamId: selectedWorkstreamId,
3189
+ workstreamTitle: workstreamTitle ?? null,
3190
+ domain: executionPolicy.domain,
3191
+ requiredSkills: executionPolicy.requiredSkills,
3192
+ event: "auto_continue_spawn_guard_rate_limit_overridden",
3193
+ }),
3194
+ blocked_reason: blockedReason,
3195
+ error_location: "mission-control.auto-continue.engine.spawn-guard.override",
3196
+ spawn_guard: spawnGuardResult,
3197
+ },
3198
+ nextStep: "Manual Play requested immediate execution for this single workstream slice.",
3199
+ });
3200
+ run.lastError = null;
3201
+ run.updatedAt = now;
3202
+ setLaneState(run, {
3203
+ workstreamId: selectedWorkstreamId,
3204
+ state: "idle",
3205
+ activeRunId: null,
3206
+ activeTaskIds: [],
3207
+ blockedReason: null,
3208
+ waitingOnWorkstreamIds: [],
3209
+ retryAt: null,
3210
+ });
3211
+ }
3212
+ else {
3213
+ // Maintain existing behavior: mark the primary task blocked when a quality gate denies dispatch.
3214
+ try {
3215
+ await client.updateEntity("task", primaryTask.id, { status: "blocked" });
3216
+ }
3217
+ catch {
3218
+ // best effort
3219
+ }
3220
+ try {
3221
+ await syncParentRollupsForTask({
3222
+ initiativeId: run.initiativeId,
3223
+ taskId: primaryTask.id,
3224
+ workstreamId: selectedWorkstreamId,
3225
+ milestoneId: primaryTask.milestoneId,
3226
+ correlationId: sliceRunId,
3227
+ });
3228
+ }
3229
+ catch {
3230
+ // best effort
3231
+ }
3232
+ await emitActivitySafe({
3233
+ initiativeId: run.initiativeId,
3234
+ runId: sliceRunId,
3235
+ correlationId: sliceRunId,
3236
+ phase: "blocked",
3237
+ level: "error",
3238
+ progressPct: 25,
3239
+ message: `Autopilot blocked by spawn guard for ${workstreamTitle ?? selectedWorkstreamId}.`,
3240
+ metadata: {
3241
+ ...buildSliceEnrichment({
3242
+ run,
3243
+ taskId: primaryTask.id,
3244
+ taskTitle: primaryTask.title ?? null,
3245
+ workstreamId: selectedWorkstreamId,
3246
+ workstreamTitle: workstreamTitle ?? null,
3247
+ domain: executionPolicy.domain,
3248
+ requiredSkills: executionPolicy.requiredSkills,
3249
+ event: "auto_continue_spawn_guard_blocked",
3250
+ }),
3251
+ blocked_reason: blockedReason,
3252
+ error_location: "mission-control.auto-continue.engine.spawn-guard.blocked",
3253
+ spawn_guard: spawnGuardResult,
3254
+ },
3255
+ });
3256
+ const decisionResult = await requestDecisionQueued({
3257
+ initiativeId: run.initiativeId,
3258
+ correlationId: sliceRunId,
3259
+ title: `Unblock autopilot for ${workstreamTitle ?? selectedWorkstreamId}`,
3260
+ summary: [
3261
+ `Spawn guard denied dispatch for primary task ${primaryTask.id}.`,
3262
+ `Reason: ${blockedReason}`,
3263
+ `Domain: ${executionPolicy.domain}`,
3264
+ `Required skills: ${executionPolicy.requiredSkills.join(", ")}`,
3265
+ ].join(" "),
3266
+ urgency: "high",
3267
+ options: [
3268
+ "Approve exception and continue",
3269
+ "Reassign slice/domain",
3270
+ "Pause and investigate quality gate",
3271
+ ],
3272
+ blocking: true,
3273
+ decisionType: "autopilot_spawn_guard_block",
3274
+ workstreamId: selectedWorkstreamId,
3275
+ agentId: run.agentId,
3276
+ sourceSystem: "orgx-autopilot",
3277
+ conflictSource: "spawn_guard_blocked",
3278
+ dedupeKey: [
3279
+ "autopilot",
3280
+ run.initiativeId,
3281
+ selectedWorkstreamId,
3282
+ "spawn_guard_blocked",
3283
+ executionPolicy.domain,
3284
+ ].join(":"),
3285
+ recommendedAction: "Choose exception, reassignment, or pause so dispatch can proceed safely.",
3286
+ sourceRunId: sliceRunId,
3287
+ sourceRef: {
3288
+ run_id: sliceRunId,
3289
+ workstream_id: selectedWorkstreamId,
3290
+ task_id: primaryTask.id,
3291
+ domain: executionPolicy.domain,
3292
+ },
3293
+ evidenceRefs: [
3294
+ {
3295
+ evidence_type: "spawn_guard_result",
3296
+ title: "Spawn guard denied dispatch",
3297
+ summary: blockedReason,
3298
+ source_pointer: null,
3299
+ payload: {
3300
+ spawn_guard: spawnGuardResult,
3301
+ task_id: primaryTask.id,
3302
+ domain: executionPolicy.domain,
3303
+ },
3304
+ },
3305
+ ],
3306
+ });
3307
+ if (!run.blockedWorkstreamIds.includes(selectedWorkstreamId)) {
3308
+ run.blockedWorkstreamIds.push(selectedWorkstreamId);
3309
+ }
3310
+ setLaneState(run, {
3311
+ workstreamId: selectedWorkstreamId,
3312
+ state: "blocked",
3313
+ activeRunId: null,
3314
+ activeTaskIds: [],
3315
+ blockedReason,
3316
+ waitingOnWorkstreamIds: [],
3317
+ retryAt: null,
3318
+ });
3319
+ await stopAutoContinueRun({
3320
+ run,
3321
+ reason: "blocked",
3322
+ error: blockedReason,
3323
+ decisionRequired: decisionResult.queued,
3324
+ decisionIds: decisionResult.decisionIds,
3325
+ });
3326
+ return;
871
3327
  }
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
3328
  }
908
3329
  }
909
3330
  const milestoneIds = dedupeStrings(cappedSliceTaskNodes.map((t) => (t.milestoneId ?? "").trim()).filter(Boolean));
@@ -918,25 +3339,95 @@ export function createAutoContinueEngine(deps) {
918
3339
  milestoneId: t.milestoneId ?? null,
919
3340
  }));
920
3341
  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
- });
3342
+ // Try server KickoffContext (includes team context, acceptance criteria, etc.)
3343
+ let prompt;
3344
+ let kickoffContextHash = null;
3345
+ let kickoffRuntimeSettings = null;
3346
+ if (fetchKickoffContextSafeFn && renderKickoffMessageFn) {
3347
+ let kickoff = null;
3348
+ try {
3349
+ kickoff = await fetchKickoffContextSafeFn(client, {
3350
+ initiative_id: run.initiativeId,
3351
+ workstream_id: selectedWorkstreamId,
3352
+ task_id: primaryTask.id,
3353
+ domain: executionPolicy.domain,
3354
+ required_skills: executionPolicy.requiredSkills,
3355
+ agent_id: resolveOrgxAgentForDomain(executionPolicy.domain).id,
3356
+ });
3357
+ }
3358
+ catch {
3359
+ // best effort: fall back to local prompt
3360
+ }
3361
+ if (kickoff) {
3362
+ kickoffRuntimeSettings = kickoff.runtime_settings ?? null;
3363
+ const rendered = renderKickoffMessageFn({
3364
+ baseMessage: `Execute workstream slice for ${workstreamTitle ?? selectedWorkstreamId}`,
3365
+ kickoff,
3366
+ domain: executionPolicy.domain,
3367
+ requiredSkills: executionPolicy.requiredSkills,
3368
+ });
3369
+ const sliceInstructions = buildSliceOutputInstructions({
3370
+ runId: sliceRunId,
3371
+ schemaPath,
3372
+ requiredSkills: executionPolicy.requiredSkills,
3373
+ });
3374
+ prompt = rendered.message + "\n\n" + sliceInstructions;
3375
+ kickoffContextHash = rendered.contextHash;
3376
+ }
3377
+ else {
3378
+ // Fallback: existing local prompt (offline/degraded mode)
3379
+ prompt = buildWorkstreamSlicePrompt({
3380
+ initiativeTitle,
3381
+ initiativeId: run.initiativeId,
3382
+ workstreamId: selectedWorkstreamId,
3383
+ workstreamTitle: workstreamTitle ?? `Workstream ${selectedWorkstreamId.slice(0, 8)}`,
3384
+ milestoneSummaries,
3385
+ taskSummaries,
3386
+ executionPolicy,
3387
+ behaviorConfig,
3388
+ runId: sliceRunId,
3389
+ schemaPath,
3390
+ });
3391
+ }
3392
+ }
3393
+ else {
3394
+ // No KickoffContext functions available: use local prompt
3395
+ prompt = buildWorkstreamSlicePrompt({
3396
+ initiativeTitle,
3397
+ initiativeId: run.initiativeId,
3398
+ workstreamId: selectedWorkstreamId,
3399
+ workstreamTitle: workstreamTitle ?? `Workstream ${selectedWorkstreamId.slice(0, 8)}`,
3400
+ milestoneSummaries,
3401
+ taskSummaries,
3402
+ executionPolicy,
3403
+ behaviorConfig,
3404
+ runId: sliceRunId,
3405
+ schemaPath,
3406
+ });
3407
+ }
3408
+ questionAutoAnswerPolicyByScope.set(questionScopeKey(run.initiativeId, selectedWorkstreamId), normalizeQuestionAutoAnswerPolicy(kickoffRuntimeSettings));
3409
+ // Append per-scope directive for milestone/workstream scopes.
3410
+ if (run.scope !== "task") {
3411
+ const msNodes = scopeMilestoneIds
3412
+ .map((id) => nodeById.get(id))
3413
+ .filter((n) => Boolean(n));
3414
+ const scopeDirective = buildScopeDirective(run.scope, {
3415
+ milestoneTitles: msNodes.map((n) => n.title),
3416
+ workstreamTitle: workstreamTitle ?? undefined,
3417
+ taskCount: cappedSliceTaskNodes.length,
3418
+ });
3419
+ if (scopeDirective) {
3420
+ prompt = prompt + "\n\n" + scopeDirective;
3421
+ }
3422
+ }
932
3423
  const logsDir = join(getOrgxPluginConfigDir(), AUTO_CONTINUE_SLICE_LOG_DIRNAME);
933
3424
  const logPath = join(logsDir, `${sliceRunId}.log`);
934
3425
  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.
3426
+ const configuredWorkerCwd = (process.env.ORGX_AUTOPILOT_CWD ?? "").trim();
3427
+ let workerCwd = configuredWorkerCwd || resolveAutopilotDefaultCwd(__filename);
3428
+ // LaunchAgents sometimes start with cwd="/". Fall back to plugin root (or home if unresolved).
938
3429
  if (!workerCwd || workerCwd === "/") {
939
- workerCwd = homedir();
3430
+ workerCwd = resolveAutopilotDefaultCwd(__filename);
940
3431
  }
941
3432
  const sliceAgent = resolveOrgxAgentForDomain(executionPolicy.domain);
942
3433
  const workerKind = (process.env.ORGX_AUTOPILOT_WORKER_KIND ?? "").trim().toLowerCase();
@@ -960,6 +3451,7 @@ export function createAutoContinueEngine(deps) {
960
3451
  cwd: workerCwd,
961
3452
  logPath,
962
3453
  outputPath,
3454
+ outputSchemaPath: schemaPath,
963
3455
  env: {
964
3456
  ORGX_SOURCE_CLIENT: executorSourceClient,
965
3457
  ORGX_RUN_ID: sliceRunId,
@@ -968,8 +3460,16 @@ export function createAutoContinueEngine(deps) {
968
3460
  ORGX_WORKSTREAM_ID: selectedWorkstreamId,
969
3461
  ORGX_WORKSTREAM_TITLE: workstreamTitle ?? undefined,
970
3462
  ORGX_TASK_ID: primaryTask.id,
3463
+ ORGX_REQUIRED_SKILLS: executionPolicy.requiredSkills.join(","),
3464
+ ORGX_BEHAVIOR_CONFIG_ID: behaviorConfig.configId ?? undefined,
3465
+ ORGX_BEHAVIOR_CONFIG_VERSION: behaviorConfig.version ?? undefined,
3466
+ ORGX_BEHAVIOR_CONFIG_HASH: behaviorConfig.hash ?? undefined,
3467
+ ORGX_POLICY_SOURCE: behaviorConfig.policySource ?? undefined,
3468
+ ORGX_AUTOMATION_LEVEL: behaviorAutomationLevel,
3469
+ ORGX_BEHAVIOR_CONTEXT: behaviorConfig.context ?? undefined,
971
3470
  ORGX_AGENT_ID: sliceAgent.id,
972
3471
  ORGX_AGENT_NAME: sliceAgent.name,
3472
+ ORGX_KICKOFF_CONTEXT_HASH: kickoffContextHash ?? undefined,
973
3473
  ORGX_OUTPUT_PATH: outputPath,
974
3474
  ORGX_RUNTIME_HOOK_URL: runtimeHookUrl ?? undefined,
975
3475
  ORGX_HOOK_TOKEN: runtimeHookToken ?? undefined,
@@ -985,6 +3485,11 @@ export function createAutoContinueEngine(deps) {
985
3485
  agentName: sliceAgent.name,
986
3486
  domain: executionPolicy.domain,
987
3487
  requiredSkills: executionPolicy.requiredSkills,
3488
+ behaviorConfigId: behaviorConfig.configId,
3489
+ behaviorConfigVersion: behaviorConfig.version,
3490
+ behaviorConfigHash: behaviorConfig.hash,
3491
+ behaviorPolicySource: behaviorConfig.policySource,
3492
+ behaviorAutomationLevel,
988
3493
  sourceClient: executorSourceClient,
989
3494
  pid: spawned.pid,
990
3495
  status: "running",
@@ -996,7 +3501,10 @@ export function createAutoContinueEngine(deps) {
996
3501
  logPath,
997
3502
  taskIds: cappedSliceTaskNodes.map((t) => t.id),
998
3503
  milestoneIds,
3504
+ scope: run.scope,
3505
+ scopeMilestoneIds: scopeMilestoneIds,
999
3506
  lastError: null,
3507
+ isMockWorker: workerKind === "mock",
1000
3508
  };
1001
3509
  autoContinueSliceRuns.set(sliceRunId, slice);
1002
3510
  try {
@@ -1013,15 +3521,28 @@ export function createAutoContinueEngine(deps) {
1013
3521
  message: `Autopilot slice started: ${workstreamTitle ?? selectedWorkstreamId}`,
1014
3522
  metadata: {
1015
3523
  event: "autopilot_slice_started",
3524
+ initiative_id: run.initiativeId,
3525
+ run_id: sliceRunId,
3526
+ slice_run_id: sliceRunId,
3527
+ workstream_id: selectedWorkstreamId,
3528
+ correlation_id: sliceRunId,
1016
3529
  requested_by_agent_id: run.agentId,
1017
3530
  requested_by_agent_name: run.agentName,
1018
3531
  domain: executionPolicy.domain,
1019
3532
  required_skills: executionPolicy.requiredSkills,
3533
+ behavior_config_id: behaviorConfig.configId,
3534
+ behavior_config_version: behaviorConfig.version,
3535
+ behavior_config_hash: behaviorConfig.hash,
3536
+ policy_source: behaviorConfig.policySource,
3537
+ behavior_automation_level: behaviorAutomationLevel,
1020
3538
  task_ids: slice.taskIds,
1021
3539
  initiative_title: initiativeTitle ?? null,
1022
3540
  workstream_title: workstreamTitle ?? null,
3541
+ scope: slice.scope,
3542
+ scope_milestone_ids: slice.scopeMilestoneIds,
1023
3543
  log_path: logPath,
1024
3544
  output_path: outputPath,
3545
+ ...mockMeta(slice),
1025
3546
  },
1026
3547
  });
1027
3548
  }
@@ -1033,24 +3554,34 @@ export function createAutoContinueEngine(deps) {
1033
3554
  initiativeId: run.initiativeId,
1034
3555
  runId: sliceRunId,
1035
3556
  correlationId: sliceRunId,
3557
+ progressPct: 10,
3558
+ nextStep: `Worker ${sliceAgent.name} is executing ${workstreamTitle ?? selectedWorkstreamId}.`,
1036
3559
  phase: "execution",
1037
3560
  level: "info",
1038
3561
  message: `Autopilot dispatched slice for ${workstreamTitle ?? selectedWorkstreamId}.`,
1039
3562
  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,
3563
+ ...buildSliceEnrichment({
3564
+ run,
3565
+ slice,
3566
+ taskId: primaryTask.id,
3567
+ taskTitle: primaryTask.title ?? null,
3568
+ workstreamId: selectedWorkstreamId,
3569
+ workstreamTitle: workstreamTitle ?? null,
3570
+ domain: executionPolicy.domain,
3571
+ requiredSkills: executionPolicy.requiredSkills,
3572
+ event: "autopilot_slice_dispatched",
3573
+ }),
3574
+ behavior_config_id: behaviorConfig.configId,
3575
+ behavior_config_version: behaviorConfig.version,
3576
+ behavior_config_hash: behaviorConfig.hash,
3577
+ policy_source: behaviorConfig.policySource,
3578
+ behavior_automation_level: behaviorAutomationLevel,
1047
3579
  initiative_title: initiativeTitle ?? null,
1048
- workstream_id: selectedWorkstreamId,
1049
- workstream_title: workstreamTitle ?? null,
1050
- task_ids: slice.taskIds,
1051
- milestone_ids: milestoneIds,
3580
+ scope: slice.scope,
3581
+ scope_milestone_ids: slice.scopeMilestoneIds,
1052
3582
  log_path: logPath,
1053
3583
  output_path: outputPath,
3584
+ ...mockMeta(slice),
1054
3585
  },
1055
3586
  });
1056
3587
  upsertAgentContext({
@@ -1060,11 +3591,34 @@ export function createAutoContinueEngine(deps) {
1060
3591
  workstreamId: selectedWorkstreamId,
1061
3592
  taskId: primaryTask.id,
1062
3593
  });
3594
+ upsertRunContext({
3595
+ runId: sliceRunId,
3596
+ agentId: slice.agentId,
3597
+ initiativeId: run.initiativeId,
3598
+ initiativeTitle: initiativeTitle ?? null,
3599
+ workstreamId: selectedWorkstreamId,
3600
+ taskId: primaryTask.id,
3601
+ });
1063
3602
  run.lastTaskId = primaryTask.id;
1064
3603
  run.lastRunId = sliceRunId;
1065
- run.activeTaskId = primaryTask.id;
1066
- run.activeRunId = sliceRunId;
3604
+ run.activeSliceRunIds = dedupeStrings([
3605
+ ...run.activeSliceRunIds,
3606
+ sliceRunId,
3607
+ ]);
3608
+ run.activeTaskIds = dedupeStrings([...run.activeTaskIds, ...slice.taskIds]);
3609
+ setLaneState(run, {
3610
+ workstreamId: selectedWorkstreamId,
3611
+ state: "running",
3612
+ activeRunId: sliceRunId,
3613
+ activeTaskIds: slice.taskIds,
3614
+ blockedReason: null,
3615
+ waitingOnWorkstreamIds: [],
3616
+ retryAt: null,
3617
+ });
1067
3618
  run.activeTaskTokenEstimate = tokenEstimate > 0 ? tokenEstimate : null;
3619
+ syncLegacyRunPointers(run);
3620
+ // Clear stale errors when a new slice dispatches successfully.
3621
+ run.lastError = null;
1068
3622
  run.updatedAt = now;
1069
3623
  try {
1070
3624
  await client.updateEntity("initiative", run.initiativeId, { status: "active" });
@@ -1095,7 +3649,7 @@ export function createAutoContinueEngine(deps) {
1095
3649
  }
1096
3650
  catch (err) {
1097
3651
  // Never let one loop crash the whole handler.
1098
- run.lastError = safeErrorMessage(err);
3652
+ run.lastError = `[mission-control.auto-continue.engine.tick-all] ${safeErrorMessage(err)}`;
1099
3653
  run.updatedAt = new Date().toISOString();
1100
3654
  await stopAutoContinueRun({ run, reason: "error", error: run.lastError });
1101
3655
  }
@@ -1123,12 +3677,324 @@ export function createAutoContinueEngine(deps) {
1123
3677
  const run = autoContinueRuns.get(initiativeId) ?? null;
1124
3678
  if (!run)
1125
3679
  return null;
3680
+ ensureRunInternals(run);
1126
3681
  if (run.status !== "running" && run.status !== "stopping")
1127
3682
  return null;
1128
- if (!Array.isArray(run.allowedWorkstreamIds) || run.allowedWorkstreamIds.length === 0) {
3683
+ if (Array.isArray(run.allowedWorkstreamIds) &&
3684
+ run.allowedWorkstreamIds.length > 0 &&
3685
+ !run.allowedWorkstreamIds.includes(workstreamId)) {
3686
+ return null;
3687
+ }
3688
+ const lane = run.laneByWorkstreamId[workstreamId] ?? null;
3689
+ if (lane &&
3690
+ (lane.state === "running" ||
3691
+ lane.state === "blocked" ||
3692
+ lane.state === "waiting_dependency" ||
3693
+ lane.state === "rate_limited")) {
3694
+ return run;
3695
+ }
3696
+ if (Array.isArray(run.allowedWorkstreamIds) &&
3697
+ run.allowedWorkstreamIds.length > 0 &&
3698
+ run.allowedWorkstreamIds.includes(workstreamId) &&
3699
+ (run.status === "running" || run.status === "stopping")) {
1129
3700
  return run;
1130
3701
  }
1131
- return run.allowedWorkstreamIds.includes(workstreamId) ? run : null;
3702
+ return null;
3703
+ }
3704
+ function getAutoContinueLaneForWorkstream(initiativeId, workstreamId) {
3705
+ const run = autoContinueRuns.get(initiativeId) ?? null;
3706
+ if (!run)
3707
+ return null;
3708
+ ensureRunInternals(run);
3709
+ return run.laneByWorkstreamId[workstreamId] ?? null;
3710
+ }
3711
+ async function scheduleAutoFixForWorkstream(input) {
3712
+ const initiativeId = input.initiativeId.trim();
3713
+ const workstreamId = input.workstreamId.trim();
3714
+ if (!initiativeId || !workstreamId) {
3715
+ throw new Error("initiativeId and workstreamId are required");
3716
+ }
3717
+ const runId = (input.runId ?? "").trim() || null;
3718
+ const sourceEvent = (input.event ?? "").trim() || null;
3719
+ const requestedByAgentId = (input.requestedByAgentId ?? "").trim() || null;
3720
+ const requestedByAgentName = (input.requestedByAgentName ?? "").trim() || null;
3721
+ const providedGraceMs = typeof input.graceMs === "number" && Number.isFinite(input.graceMs)
3722
+ ? Math.floor(input.graceMs)
3723
+ : null;
3724
+ const graceMs = Math.max(1_000, Math.min(120_000, providedGraceMs ?? AUTO_FIX_DEFAULT_GRACE_MS));
3725
+ const key = `${initiativeId}:${workstreamId}`;
3726
+ const existing = autoFixByScope.get(key);
3727
+ if (existing?.timer)
3728
+ clearTimeout(existing.timer);
3729
+ const scheduledAt = new Date().toISOString();
3730
+ const dueAt = new Date(Date.now() + graceMs).toISOString();
3731
+ const requestId = randomUUID();
3732
+ const resolveAutoFixRunContext = () => {
3733
+ const activeRun = autoContinueRuns.get(initiativeId) ?? null;
3734
+ return {
3735
+ initiativeId,
3736
+ agentId: activeRun?.agentId ?? requestedByAgentId ?? "main",
3737
+ agentName: activeRun?.agentName ?? requestedByAgentName ?? null,
3738
+ scope: activeRun?.scope ?? "task",
3739
+ };
3740
+ };
3741
+ const emitSkip = async (reason, details) => {
3742
+ await emitActivitySafe({
3743
+ initiativeId,
3744
+ runId: runId ?? undefined,
3745
+ correlationId: runId ?? undefined,
3746
+ phase: "review",
3747
+ level: reason === "error" ? "error" : "warn",
3748
+ message: reason === "paused_by_user"
3749
+ ? `Auto-fix skipped for ${workstreamId}: paused during grace window.`
3750
+ : reason === "already_running"
3751
+ ? `Auto-fix skipped for ${workstreamId}: workstream already running.`
3752
+ : reason === "missing_workstream"
3753
+ ? `Auto-fix skipped for ${workstreamId}: workstream data unavailable.`
3754
+ : reason === "missing_scope"
3755
+ ? `Auto-fix skipped: scope metadata was incomplete.`
3756
+ : `Auto-fix failed for ${workstreamId}.`,
3757
+ metadata: {
3758
+ ...buildSliceEnrichment({
3759
+ run: resolveAutoFixRunContext(),
3760
+ workstreamId,
3761
+ event: "autopilot_autofix_skipped",
3762
+ actionType: "auto_fix",
3763
+ }),
3764
+ reason,
3765
+ run_id: runId,
3766
+ source_event: sourceEvent,
3767
+ grace_ms: graceMs,
3768
+ request_id: requestId,
3769
+ scheduled_at: scheduledAt,
3770
+ due_at: dueAt,
3771
+ ...(details ?? {}),
3772
+ },
3773
+ });
3774
+ };
3775
+ const executeScheduledAutoFix = async () => {
3776
+ const pending = autoFixByScope.get(key);
3777
+ if (!pending || pending.requestId !== requestId)
3778
+ return;
3779
+ autoFixByScope.delete(key);
3780
+ const existingRun = autoContinueRuns.get(initiativeId) ?? null;
3781
+ if (existingRun &&
3782
+ (existingRun.stopRequested ||
3783
+ existingRun.status === "stopping" ||
3784
+ existingRun.stopReason === "stopped")) {
3785
+ await emitSkip("paused_by_user");
3786
+ return;
3787
+ }
3788
+ if (existingRun &&
3789
+ (existingRun.status === "running" || existingRun.status === "stopping") &&
3790
+ listActiveSliceRunIds(existingRun).length > 0) {
3791
+ const activeRunIds = listActiveSliceRunIds(existingRun);
3792
+ await emitSkip("already_running", {
3793
+ active_run_id: activeRunIds[0] ?? null,
3794
+ active_run_ids: activeRunIds,
3795
+ run_status: existingRun.status,
3796
+ });
3797
+ return;
3798
+ }
3799
+ let optionalDecisionsApproved = 0;
3800
+ if (decisionAutoResolveGuardedEnabled) {
3801
+ try {
3802
+ const decisionResult = await client.listEntities("decision", {
3803
+ initiative_id: initiativeId,
3804
+ status: "pending",
3805
+ limit: 500,
3806
+ });
3807
+ const decisionRows = Array.isArray(decisionResult?.data) ? decisionResult.data : [];
3808
+ for (const row of decisionRows) {
3809
+ if (!row || typeof row !== "object")
3810
+ continue;
3811
+ const record = row;
3812
+ const decisionId = pickString(record, ["id"])?.trim() ?? "";
3813
+ if (!decisionId)
3814
+ continue;
3815
+ if (!isPendingDecisionStatus(record.status ?? record.decision_status))
3816
+ continue;
3817
+ if (!decisionMatchesWorkstream(record, workstreamId, runId))
3818
+ continue;
3819
+ if (decisionIsBlocking(record))
3820
+ continue;
3821
+ const autoApprovalNote = "Auto-approved by OrgX auto-fix (non-blocking follow-up decision).";
3822
+ const autoApprovalSourceClient = normalizeRuntimeSourceClient(process.env.ORGX_AUTOPILOT_EXECUTOR ?? process.env.ORGX_AUTOPILOT_WORKER_KIND);
3823
+ if (typeof client.decideDecision === "function") {
3824
+ await client.decideDecision(decisionId, "approve", {
3825
+ note: autoApprovalNote,
3826
+ source_client: autoApprovalSourceClient,
3827
+ sourceClient: autoApprovalSourceClient,
3828
+ });
3829
+ }
3830
+ else {
3831
+ await client.updateEntity("decision", decisionId, {
3832
+ status: "approved",
3833
+ resolution_summary: autoApprovalNote,
3834
+ });
3835
+ }
3836
+ optionalDecisionsApproved += 1;
3837
+ }
3838
+ }
3839
+ catch {
3840
+ // best effort
3841
+ }
3842
+ }
3843
+ let resetTaskCount = 0;
3844
+ try {
3845
+ const taskResult = await client.listEntities("task", {
3846
+ initiative_id: initiativeId,
3847
+ workstream_id: workstreamId,
3848
+ limit: 100,
3849
+ });
3850
+ const taskRows = Array.isArray(taskResult?.data) ? taskResult.data : [];
3851
+ if (taskRows.length === 0) {
3852
+ await emitSkip("missing_workstream");
3853
+ return;
3854
+ }
3855
+ for (const row of taskRows) {
3856
+ if (!row || typeof row !== "object")
3857
+ continue;
3858
+ const record = row;
3859
+ const taskId = pickString(record, ["id"])?.trim() ?? "";
3860
+ if (!taskId)
3861
+ continue;
3862
+ const status = normalizeStatusValue(record.status);
3863
+ if (!status || status === "todo" || status === "done" || status === "completed") {
3864
+ continue;
3865
+ }
3866
+ const shouldReset = status === "in_progress" ||
3867
+ status === "inprogress" ||
3868
+ status === "active" ||
3869
+ status === "running" ||
3870
+ status === "working" ||
3871
+ status === "planning" ||
3872
+ status === "dispatching" ||
3873
+ status === "pending" ||
3874
+ status === "blocked" ||
3875
+ status === "stalled" ||
3876
+ status === "failed" ||
3877
+ status === "error";
3878
+ if (!shouldReset)
3879
+ continue;
3880
+ await client.updateEntity("task", taskId, { status: "todo" });
3881
+ resetTaskCount += 1;
3882
+ }
3883
+ }
3884
+ catch {
3885
+ // best effort
3886
+ }
3887
+ const latestRun = autoContinueRuns.get(initiativeId) ?? null;
3888
+ const dispatchAgentId = latestRun?.agentId ??
3889
+ requestedByAgentId ??
3890
+ "main";
3891
+ const dispatchAgentName = latestRun?.agentName ??
3892
+ requestedByAgentName ??
3893
+ null;
3894
+ const dispatchRun = await startAutoContinueRun({
3895
+ initiativeId,
3896
+ agentId: dispatchAgentId,
3897
+ agentName: dispatchAgentName,
3898
+ // Auto-fix retries should follow current defaults unless an operator explicitly
3899
+ // starts a run with a budget override.
3900
+ tokenBudget: null,
3901
+ includeVerification: latestRun?.includeVerification ?? false,
3902
+ allowedWorkstreamIds: [workstreamId],
3903
+ maxParallelSlices: 1,
3904
+ parallelMode: latestRun?.parallelMode ?? "iwmt",
3905
+ stopAfterSlice: true,
3906
+ ignoreSpawnGuardRateLimit: latestRun?.ignoreSpawnGuardRateLimit ?? false,
3907
+ });
3908
+ await tickAutoContinueRun(dispatchRun);
3909
+ await emitActivitySafe({
3910
+ initiativeId,
3911
+ runId: dispatchRun.activeRunId ?? runId ?? undefined,
3912
+ correlationId: dispatchRun.activeRunId ?? runId ?? undefined,
3913
+ phase: "execution",
3914
+ level: "info",
3915
+ message: `Auto-fix dispatched for ${workstreamId}.`,
3916
+ metadata: {
3917
+ ...buildSliceEnrichment({
3918
+ run: {
3919
+ initiativeId,
3920
+ agentId: dispatchAgentId,
3921
+ agentName: dispatchAgentName,
3922
+ scope: dispatchRun.scope,
3923
+ },
3924
+ workstreamId,
3925
+ event: "autopilot_autofix_executed",
3926
+ actionType: "auto_fix",
3927
+ }),
3928
+ source_event: sourceEvent,
3929
+ run_id: runId,
3930
+ grace_ms: graceMs,
3931
+ request_id: requestId,
3932
+ scheduled_at: scheduledAt,
3933
+ due_at: dueAt,
3934
+ optional_decisions_auto_approved: optionalDecisionsApproved,
3935
+ reset_task_count: resetTaskCount,
3936
+ dispatched_run_id: dispatchRun.activeRunId,
3937
+ dispatch_agent_id: dispatchAgentId,
3938
+ dispatch_agent_name: dispatchAgentName,
3939
+ },
3940
+ });
3941
+ };
3942
+ const pending = {
3943
+ requestId,
3944
+ key,
3945
+ initiativeId,
3946
+ workstreamId,
3947
+ runId,
3948
+ sourceEvent,
3949
+ requestedByAgentId,
3950
+ requestedByAgentName,
3951
+ graceMs,
3952
+ scheduledAt,
3953
+ dueAt,
3954
+ timer: null,
3955
+ };
3956
+ const timer = setTimeout(() => {
3957
+ void executeScheduledAutoFix().catch(async (err) => {
3958
+ autoFixByScope.delete(key);
3959
+ await emitSkip("error", {
3960
+ error: safeErrorMessage(err),
3961
+ });
3962
+ });
3963
+ }, graceMs);
3964
+ pending.timer = timer;
3965
+ autoFixByScope.set(key, pending);
3966
+ await emitActivitySafe({
3967
+ initiativeId,
3968
+ runId: runId ?? undefined,
3969
+ correlationId: runId ?? undefined,
3970
+ phase: "review",
3971
+ level: "info",
3972
+ message: `Auto-fix scheduled for ${workstreamId} in ${Math.round(graceMs / 1000)}s.`,
3973
+ metadata: {
3974
+ ...buildSliceEnrichment({
3975
+ run: resolveAutoFixRunContext(),
3976
+ workstreamId,
3977
+ event: "autopilot_autofix_scheduled",
3978
+ actionType: "auto_fix",
3979
+ }),
3980
+ source_event: sourceEvent,
3981
+ run_id: runId,
3982
+ grace_ms: graceMs,
3983
+ request_id: requestId,
3984
+ scheduled_at: scheduledAt,
3985
+ due_at: dueAt,
3986
+ },
3987
+ });
3988
+ return {
3989
+ requestId,
3990
+ initiativeId,
3991
+ workstreamId,
3992
+ runId,
3993
+ sourceEvent,
3994
+ graceMs,
3995
+ scheduledAt,
3996
+ dueAt,
3997
+ };
1132
3998
  }
1133
3999
  async function startAutoContinueRun(input) {
1134
4000
  const now = new Date().toISOString();
@@ -1142,6 +4008,10 @@ export function createAutoContinueEngine(deps) {
1142
4008
  includeVerification: false,
1143
4009
  allowedWorkstreamIds: null,
1144
4010
  stopAfterSlice: false,
4011
+ ignoreSpawnGuardRateLimit: false,
4012
+ maxParallelSlices: AUTO_CONTINUE_MAX_PARALLEL_DEFAULT,
4013
+ parallelMode: "iwmt",
4014
+ scope: "task",
1145
4015
  tokenBudget: defaultAutoContinueTokenBudget(),
1146
4016
  tokensUsed: 0,
1147
4017
  status: "running",
@@ -1153,10 +4023,15 @@ export function createAutoContinueEngine(deps) {
1153
4023
  lastError: null,
1154
4024
  lastTaskId: null,
1155
4025
  lastRunId: null,
4026
+ activeSliceRunIds: [],
4027
+ activeTaskIds: [],
4028
+ laneByWorkstreamId: {},
4029
+ blockedWorkstreamIds: [],
1156
4030
  activeTaskId: null,
1157
4031
  activeRunId: null,
1158
4032
  activeTaskTokenEstimate: null,
1159
4033
  };
4034
+ ensureRunInternals(run);
1160
4035
  run.agentId = input.agentId;
1161
4036
  run.agentName =
1162
4037
  typeof input.agentName === "string" && input.agentName.trim().length > 0
@@ -1164,8 +4039,24 @@ export function createAutoContinueEngine(deps) {
1164
4039
  : null;
1165
4040
  run.includeVerification = input.includeVerification;
1166
4041
  run.allowedWorkstreamIds = input.allowedWorkstreamIds;
4042
+ run.maxParallelSlices = normalizeMaxParallelSlices(input.maxParallelSlices, run.maxParallelSlices || AUTO_CONTINUE_MAX_PARALLEL_DEFAULT);
4043
+ run.parallelMode = normalizeParallelMode(input.parallelMode ?? run.parallelMode);
1167
4044
  run.stopAfterSlice = Boolean(input.stopAfterSlice);
1168
- run.tokenBudget = normalizeTokenBudget(input.tokenBudget, run.tokenBudget || defaultAutoContinueTokenBudget());
4045
+ run.ignoreSpawnGuardRateLimit = Boolean(input.ignoreSpawnGuardRateLimit);
4046
+ run.scope = input.scope ?? "task";
4047
+ const hasExplicitTokenBudgetInput = input.tokenBudget !== null &&
4048
+ input.tokenBudget !== undefined &&
4049
+ !(typeof input.tokenBudget === "string" && input.tokenBudget.trim().length === 0);
4050
+ if (hasExplicitTokenBudgetInput) {
4051
+ run.tokenBudget = normalizeTokenBudget(input.tokenBudget, defaultAutoContinueTokenBudget());
4052
+ }
4053
+ else {
4054
+ // On fresh restarts, reset to current defaults instead of inheriting stale prior limits.
4055
+ // While a run is live, keep its active budget unless explicitly overridden.
4056
+ run.tokenBudget = existingIsLive
4057
+ ? normalizeTokenBudget(run.tokenBudget, defaultAutoContinueTokenBudget())
4058
+ : defaultAutoContinueTokenBudget();
4059
+ }
1169
4060
  run.status = "running";
1170
4061
  run.stopReason = null;
1171
4062
  run.stopRequested = false;
@@ -1178,10 +4069,15 @@ export function createAutoContinueEngine(deps) {
1178
4069
  run.startedAt = now;
1179
4070
  run.lastTaskId = null;
1180
4071
  run.lastRunId = null;
4072
+ run.activeSliceRunIds = [];
4073
+ run.activeTaskIds = [];
4074
+ run.blockedWorkstreamIds = [];
4075
+ run.laneByWorkstreamId = {};
1181
4076
  run.activeTaskId = null;
1182
4077
  run.activeRunId = null;
1183
4078
  run.activeTaskTokenEstimate = null;
1184
4079
  }
4080
+ syncLegacyRunPointers(run);
1185
4081
  autoContinueRuns.set(input.initiativeId, run);
1186
4082
  void client
1187
4083
  .updateEntity("initiative", input.initiativeId, { status: "active" })
@@ -1194,6 +4090,66 @@ export function createAutoContinueEngine(deps) {
1194
4090
  }).catch(() => {
1195
4091
  // best effort
1196
4092
  });
4093
+ if (!existingIsLive || forceFreshRun) {
4094
+ const startRunContext = {
4095
+ initiativeId: run.initiativeId,
4096
+ agentId: run.agentId,
4097
+ agentName: run.agentName,
4098
+ scope: run.scope,
4099
+ };
4100
+ try {
4101
+ await emitActivitySafe({
4102
+ initiativeId: input.initiativeId,
4103
+ runId: run.lastRunId ?? undefined,
4104
+ correlationId: run.lastRunId ?? undefined,
4105
+ phase: "intent",
4106
+ level: "info",
4107
+ message: "Autopilot enabled. Dispatch will continue from Next Up automatically.",
4108
+ metadata: {
4109
+ ...buildSliceEnrichment({
4110
+ run: startRunContext,
4111
+ event: "auto_continue_started",
4112
+ }),
4113
+ token_budget: run.tokenBudget,
4114
+ include_verification: run.includeVerification,
4115
+ allowed_workstream_ids: run.allowedWorkstreamIds,
4116
+ max_parallel_slices: run.maxParallelSlices,
4117
+ parallel_mode: run.parallelMode,
4118
+ scope: run.scope,
4119
+ ignore_spawn_guard_rate_limit: run.ignoreSpawnGuardRateLimit,
4120
+ },
4121
+ nextStep: "Watch Activity for dispatch and slice-complete updates.",
4122
+ });
4123
+ }
4124
+ catch {
4125
+ // best effort
4126
+ }
4127
+ // Emit transition: idle → running
4128
+ try {
4129
+ await emitActivitySafe({
4130
+ initiativeId: input.initiativeId,
4131
+ runId: run.lastRunId ?? undefined,
4132
+ correlationId: run.lastRunId ?? undefined,
4133
+ phase: "intent",
4134
+ level: "info",
4135
+ message: "Autopilot state: idle → running.",
4136
+ metadata: {
4137
+ ...buildSliceEnrichment({
4138
+ run: startRunContext,
4139
+ event: "autopilot_transition",
4140
+ actionType: "run_state_transition",
4141
+ }),
4142
+ old_state: "idle",
4143
+ new_state: "running",
4144
+ reason: "started",
4145
+ workspace_id: run.allowedWorkstreamIds?.[0] ?? null,
4146
+ },
4147
+ });
4148
+ }
4149
+ catch {
4150
+ // best effort
4151
+ }
4152
+ }
1197
4153
  return run;
1198
4154
  }
1199
4155
  return {
@@ -1203,6 +4159,7 @@ export function createAutoContinueEngine(deps) {
1203
4159
  writeRuntimeEvent,
1204
4160
  autoContinueTickMs: AUTO_CONTINUE_TICK_MS,
1205
4161
  defaultAutoContinueTokenBudget,
4162
+ defaultAutoContinueMaxParallelSlices,
1206
4163
  setLocalInitiativeStatusOverride,
1207
4164
  clearLocalInitiativeStatusOverride,
1208
4165
  applyLocalInitiativeOverrides,
@@ -1213,6 +4170,8 @@ export function createAutoContinueEngine(deps) {
1213
4170
  tickAllAutoContinue,
1214
4171
  isInitiativeActiveStatus,
1215
4172
  runningAutoContinueForWorkstream,
4173
+ getAutoContinueLaneForWorkstream,
4174
+ scheduleAutoFixForWorkstream,
1216
4175
  startAutoContinueRun,
1217
4176
  };
1218
4177
  }