@useorgx/openclaw-plugin 0.7.0 → 0.7.3

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 (124) hide show
  1. package/README.md +42 -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/{DG6y9wJI.js → 8dksYiq4.js} +1 -1
  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/{PAUiij_z.js → B5zYRHc3.js} +1 -1
  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/{CFGKRAzG.js → B6wPWJ35.js} +1 -1
  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/BWEwjt1W.js +1 -0
  15. package/dashboard/dist/assets/BWEwjt1W.js.br +0 -0
  16. package/dashboard/dist/assets/BWEwjt1W.js.gz +0 -0
  17. package/dashboard/dist/assets/BzRbDCAD.css +1 -0
  18. package/dashboard/dist/assets/BzRbDCAD.css.br +0 -0
  19. package/dashboard/dist/assets/BzRbDCAD.css.gz +0 -0
  20. package/dashboard/dist/assets/{DNxKz-GV.js → C8uM3AX8.js} +1 -1
  21. package/dashboard/dist/assets/C8uM3AX8.js.br +0 -0
  22. package/dashboard/dist/assets/C8uM3AX8.js.gz +0 -0
  23. package/dashboard/dist/assets/C9jy61eu.js +212 -0
  24. package/dashboard/dist/assets/C9jy61eu.js.br +0 -0
  25. package/dashboard/dist/assets/C9jy61eu.js.gz +0 -0
  26. package/dashboard/dist/assets/{CE38zU4U.js → CC63EwFD.js} +1 -1
  27. package/dashboard/dist/assets/CC63EwFD.js.br +0 -0
  28. package/dashboard/dist/assets/CC63EwFD.js.gz +0 -0
  29. package/dashboard/dist/assets/{nByHNHoW.js → CZaT3ob_.js} +1 -1
  30. package/dashboard/dist/assets/CZaT3ob_.js.br +0 -0
  31. package/dashboard/dist/assets/CZaT3ob_.js.gz +0 -0
  32. package/dashboard/dist/assets/{tS9mbYZi.js → CgaottFX.js} +1 -1
  33. package/dashboard/dist/assets/CgaottFX.js.br +0 -0
  34. package/dashboard/dist/assets/CgaottFX.js.gz +0 -0
  35. package/dashboard/dist/assets/CzCxAZlW.js +1 -0
  36. package/dashboard/dist/assets/CzCxAZlW.js.br +0 -0
  37. package/dashboard/dist/assets/CzCxAZlW.js.gz +0 -0
  38. package/dashboard/dist/assets/D3iMTYEj.js +1 -0
  39. package/dashboard/dist/assets/D3iMTYEj.js.br +0 -0
  40. package/dashboard/dist/assets/D3iMTYEj.js.gz +0 -0
  41. package/dashboard/dist/assets/{DjcdE6jC.js → D8JNX8kq.js} +1 -1
  42. package/dashboard/dist/assets/D8JNX8kq.js.br +0 -0
  43. package/dashboard/dist/assets/D8JNX8kq.js.gz +0 -0
  44. package/dashboard/dist/assets/DnA8dpj6.js +1 -0
  45. package/dashboard/dist/assets/DnA8dpj6.js.br +0 -0
  46. package/dashboard/dist/assets/DnA8dpj6.js.gz +0 -0
  47. package/dashboard/dist/assets/{DbNoijHm.js → IUexzymk.js} +1 -1
  48. package/dashboard/dist/assets/IUexzymk.js.br +0 -0
  49. package/dashboard/dist/assets/IUexzymk.js.gz +0 -0
  50. package/dashboard/dist/assets/{CZZTvkQZ.js → rttbDbEx.js} +1 -1
  51. package/dashboard/dist/assets/rttbDbEx.js.br +0 -0
  52. package/dashboard/dist/assets/rttbDbEx.js.gz +0 -0
  53. package/dashboard/dist/index.html +2 -2
  54. package/dashboard/dist/index.html.br +0 -0
  55. package/dashboard/dist/index.html.gz +0 -0
  56. package/dist/contracts/practice-exercise-schema.d.ts +216 -0
  57. package/dist/contracts/practice-exercise-schema.js +314 -0
  58. package/dist/contracts/shared-types.d.ts +2 -2
  59. package/dist/contracts/shared-types.js +22 -0
  60. package/dist/contracts/types.d.ts +20 -0
  61. package/dist/fs-utils.js +1 -13
  62. package/dist/http/helpers/auto-continue-engine.js +638 -24
  63. package/dist/http/helpers/autopilot-runtime.js +22 -7
  64. package/dist/http/helpers/autopilot-slice-utils.js +0 -2
  65. package/dist/http/helpers/kickoff-context.js +30 -0
  66. package/dist/http/helpers/slice-run-projections.js +19 -2
  67. package/dist/http/index.js +151 -93
  68. package/dist/http/routes/agent-suite.js +87 -0
  69. package/dist/http/routes/entities.js +1 -63
  70. package/dist/http/routes/live-snapshot.d.ts +1 -0
  71. package/dist/http/routes/live-snapshot.js +15 -4
  72. package/dist/http/routes/live-terminal.js +1 -108
  73. package/dist/http/routes/live-triage.js +1 -57
  74. package/dist/http/routes/mission-control-actions.js +0 -88
  75. package/dist/http/routes/mission-control-read.js +73 -8
  76. package/dist/mcp-http-handler.d.ts +3 -0
  77. package/dist/mcp-http-handler.js +2 -2
  78. package/dist/openclaw.plugin.json +1 -1
  79. package/dist/paths.js +14 -2
  80. package/dist/reporting/rollups.d.ts +0 -12
  81. package/dist/reporting/rollups.js +0 -35
  82. package/openclaw.plugin.json +1 -1
  83. package/package.json +7 -3
  84. package/dashboard/dist/assets/BXWDRGm-.js +0 -1
  85. package/dashboard/dist/assets/BXWDRGm-.js.br +0 -0
  86. package/dashboard/dist/assets/BXWDRGm-.js.gz +0 -0
  87. package/dashboard/dist/assets/CE38zU4U.js.br +0 -0
  88. package/dashboard/dist/assets/CE38zU4U.js.gz +0 -0
  89. package/dashboard/dist/assets/CFGKRAzG.js.br +0 -0
  90. package/dashboard/dist/assets/CFGKRAzG.js.gz +0 -0
  91. package/dashboard/dist/assets/CGGR2GZh.js +0 -1
  92. package/dashboard/dist/assets/CGGR2GZh.js.br +0 -0
  93. package/dashboard/dist/assets/CGGR2GZh.js.gz +0 -0
  94. package/dashboard/dist/assets/CPFiTmlw.js +0 -8
  95. package/dashboard/dist/assets/CPFiTmlw.js.br +0 -0
  96. package/dashboard/dist/assets/CPFiTmlw.js.gz +0 -0
  97. package/dashboard/dist/assets/CZZTvkQZ.js.br +0 -0
  98. package/dashboard/dist/assets/CZZTvkQZ.js.gz +0 -0
  99. package/dashboard/dist/assets/D-bf6hEI.js +0 -213
  100. package/dashboard/dist/assets/D-bf6hEI.js.br +0 -0
  101. package/dashboard/dist/assets/D-bf6hEI.js.gz +0 -0
  102. package/dashboard/dist/assets/DG6y9wJI.js.br +0 -0
  103. package/dashboard/dist/assets/DG6y9wJI.js.gz +0 -0
  104. package/dashboard/dist/assets/DNxKz-GV.js.br +0 -0
  105. package/dashboard/dist/assets/DNxKz-GV.js.gz +0 -0
  106. package/dashboard/dist/assets/DW_rKUic.js +0 -11
  107. package/dashboard/dist/assets/DW_rKUic.js.br +0 -0
  108. package/dashboard/dist/assets/DW_rKUic.js.gz +0 -0
  109. package/dashboard/dist/assets/DbNoijHm.js.br +0 -0
  110. package/dashboard/dist/assets/DbNoijHm.js.gz +0 -0
  111. package/dashboard/dist/assets/DjcdE6jC.js.br +0 -0
  112. package/dashboard/dist/assets/DjcdE6jC.js.gz +0 -0
  113. package/dashboard/dist/assets/FZYuCDnt.js +0 -1
  114. package/dashboard/dist/assets/FZYuCDnt.js.br +0 -0
  115. package/dashboard/dist/assets/FZYuCDnt.js.gz +0 -0
  116. package/dashboard/dist/assets/PAUiij_z.js.br +0 -0
  117. package/dashboard/dist/assets/PAUiij_z.js.gz +0 -0
  118. package/dashboard/dist/assets/h5biQs2I.css +0 -1
  119. package/dashboard/dist/assets/h5biQs2I.css.br +0 -0
  120. package/dashboard/dist/assets/h5biQs2I.css.gz +0 -0
  121. package/dashboard/dist/assets/nByHNHoW.js.br +0 -0
  122. package/dashboard/dist/assets/nByHNHoW.js.gz +0 -0
  123. package/dashboard/dist/assets/tS9mbYZi.js.br +0 -0
  124. package/dashboard/dist/assets/tS9mbYZi.js.gz +0 -0
@@ -34,11 +34,548 @@ export function createAutoContinueEngine(deps) {
34
34
  const decisionAutoResolveGuardedEnabled = String(process.env.DECISION_AUTO_RESOLVE_GUARDED_ENABLED ?? "true")
35
35
  .trim()
36
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();
37
66
  /** Spread into any metadata object to flag mock-worker activity. */
38
67
  function mockMeta(slice) {
39
68
  return slice.isMockWorker ? { mock: true } : {};
40
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
+ };
41
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
+ };
42
579
  const inferredRunId = (typeof input.sourceRunId === "string" && input.sourceRunId.trim().length > 0
43
580
  ? input.sourceRunId.trim()
44
581
  : null) ??
@@ -57,6 +594,18 @@ export function createAutoContinueEngine(deps) {
57
594
  const sourceRefBase = input.sourceRef && typeof input.sourceRef === "object" && !Array.isArray(input.sourceRef)
58
595
  ? input.sourceRef
59
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);
60
609
  const normalizedInput = {
61
610
  ...input,
62
611
  sourceRunId: inferredRunId,
@@ -68,25 +617,65 @@ export function createAutoContinueEngine(deps) {
68
617
  session_id: sourceRefBase.session_id ?? inferredSessionId,
69
618
  stream_id: sourceRefBase.stream_id ?? inferredStreamId,
70
619
  workstream_id: sourceRefBase.workstream_id ?? input.workstreamId ?? null,
620
+ source_client: sourceRefBase.source_client ??
621
+ sourceRefBase.sourceClient ??
622
+ inferredSourceClient,
71
623
  },
72
624
  metadata: {
73
- ...(input.metadata ?? {}),
625
+ ...metadataBase,
74
626
  source_system: input.sourceSystem ?? null,
75
627
  conflict_source: input.conflictSource ?? null,
628
+ source_client: metadataBase.source_client ??
629
+ metadataBase.sourceClient ??
630
+ inferredSourceClient,
76
631
  },
77
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);
78
647
  const result = await requestDecisionSafe(normalizedInput);
79
648
  if (typeof result === "boolean") {
80
649
  return { queued: result, decisionIds: [] };
81
650
  }
82
651
  if (result && typeof result === "object" && "queued" in result) {
83
652
  const record = result;
84
- const decisionIds = Array.isArray(record.decisionIds)
653
+ let decisionIds = Array.isArray(record.decisionIds)
85
654
  ? record.decisionIds
86
655
  .filter((entry) => typeof entry === "string")
87
656
  .map((entry) => entry.trim())
88
657
  .filter(Boolean)
89
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
+ }
90
679
  return {
91
680
  queued: Boolean(record.queued),
92
681
  decisionIds,
@@ -152,6 +741,20 @@ export function createAutoContinueEngine(deps) {
152
741
  return "status_updates_applied";
153
742
  if (eventName.includes("artifact_registered"))
154
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";
155
758
  if (eventName.includes("decision_requested"))
156
759
  return "decision_requested";
157
760
  if (eventName.includes("decision_resolved"))
@@ -229,6 +832,8 @@ export function createAutoContinueEngine(deps) {
229
832
  run_id: input.slice?.runId ?? null,
230
833
  slice_run_id: input.slice?.runId ?? null,
231
834
  correlation_id: input.slice?.runId ?? null,
835
+ source_client: input.slice?.sourceClient ?? "unknown",
836
+ runtime_client: input.slice?.sourceClient ?? "unknown",
232
837
  workstream_id: workstreamId,
233
838
  workstream_title: input.workstreamTitle ?? input.slice?.workstreamTitle ?? null,
234
839
  task_id: taskId,
@@ -266,7 +871,6 @@ export function createAutoContinueEngine(deps) {
266
871
  ...(input.extra ?? {}),
267
872
  };
268
873
  };
269
- const autoContinueSliceRuns = new Map();
270
874
  // Keep child handles alive so stdout/stderr capture remains reliable even when the process is detached.
271
875
  const autoContinueSliceChildren = new Map();
272
876
  const autoContinueSliceLastHeartbeatMs = new Map();
@@ -765,6 +1369,14 @@ export function createAutoContinueEngine(deps) {
765
1369
  await updateInitiativeMetadata(input.initiativeId, patch);
766
1370
  }
767
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;
768
1380
  const now = new Date().toISOString();
769
1381
  ensureRunInternals(input.run);
770
1382
  const activeRunIds = listActiveSliceRunIds(input.run);
@@ -791,6 +1403,9 @@ export function createAutoContinueEngine(deps) {
791
1403
  if (input.error)
792
1404
  input.run.lastError = input.error;
793
1405
  clearSpawnGuardRetryStateForInitiative(input.run.initiativeId);
1406
+ if (!preserveQuestionAutoAnswerState) {
1407
+ clearQuestionAutoAnswerStateForInitiative(input.run.initiativeId);
1408
+ }
794
1409
  for (const runId of activeRunIds) {
795
1410
  clearAutoContinueSliceTransientState(runId);
796
1411
  }
@@ -820,13 +1435,6 @@ export function createAutoContinueEngine(deps) {
820
1435
  ? input.run.allowedWorkstreamIds[0]
821
1436
  : null;
822
1437
  const scopeSuffix = scopedWorkstreamId ? ` [workstream ${scopedWorkstreamId}]` : "";
823
- const decisionRequired = input.reason === "blocked" && input.decisionRequired === true;
824
- const decisionIds = Array.isArray(input.decisionIds)
825
- ? input.decisionIds
826
- .filter((entry) => typeof entry === "string")
827
- .map((entry) => entry.trim())
828
- .filter(Boolean)
829
- : [];
830
1438
  const budgetValue = typeof input.run.tokenBudget === "number" ? input.run.tokenBudget : "unbounded";
831
1439
  const message = input.reason === "completed"
832
1440
  ? `Autopilot stopped: current dispatch scope completed${scopeSuffix}.`
@@ -1291,13 +1899,11 @@ export function createAutoContinueEngine(deps) {
1291
1899
  item?.blocking === true);
1292
1900
  };
1293
1901
  const decisions = allDecisions.filter((item) => !isParserSyntheticFallbackDecision(item));
1294
- const blockingDecisionCount = decisions.filter((item) => typeof item.blocking === "boolean" ? item.blocking : defaultDecisionBlocking).length;
1295
- const nonBlockingDecisionCount = Math.max(0, decisions.length - blockingDecisionCount);
1296
- const effectiveParsedStatus = parsedStatus === "completed" && blockingDecisionCount > 0
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
1297
1905
  ? "needs_decision"
1298
- : parsedStatus === "needs_decision" && blockingDecisionCount === 0
1299
- ? "completed"
1300
- : parsedStatus;
1906
+ : parsedStatus;
1301
1907
  slice.status =
1302
1908
  effectiveParsedStatus === "completed"
1303
1909
  ? "completed"
@@ -1574,9 +2180,9 @@ export function createAutoContinueEngine(deps) {
1574
2180
  requested_by_agent_name: run.agentName,
1575
2181
  status: effectiveParsedStatus,
1576
2182
  artifacts: artifacts.length,
1577
- decisions: decisions.length,
1578
- blocking_decisions: blockingDecisionCount,
1579
- non_blocking_decisions: nonBlockingDecisionCount,
2183
+ decisions: allDecisions.length,
2184
+ blocking_decisions: normalizedBlockingDecisionCount,
2185
+ non_blocking_decisions: normalizedNonBlockingDecisionCount,
1580
2186
  status_updates: statusUpdateResult.applied,
1581
2187
  status_updates_buffered: statusUpdateResult.buffered,
1582
2188
  reported_skill_evidence_count: skillEvidence.length,
@@ -1665,13 +2271,13 @@ export function createAutoContinueEngine(deps) {
1665
2271
  parsed_status: effectiveParsedStatus,
1666
2272
  has_output: Boolean(parsed),
1667
2273
  artifacts: artifacts.length,
1668
- decisions: decisions.length,
1669
- blocking_decisions: blockingDecisionCount,
1670
- non_blocking_decisions: nonBlockingDecisionCount,
2274
+ decisions: allDecisions.length,
2275
+ blocking_decisions: normalizedBlockingDecisionCount,
2276
+ non_blocking_decisions: normalizedNonBlockingDecisionCount,
1671
2277
  decision_ids: decisionIds,
1672
2278
  blocking_decision_ids: Array.from(new Set(blockingDecisionIds)),
1673
2279
  non_blocking_decision_ids: Array.from(new Set(nonBlockingDecisionIds)),
1674
- decision_required: blockingDecisionQueued,
2280
+ decision_required: blockingDecisionQueued || effectiveParsedStatus === "needs_decision",
1675
2281
  status_updates_applied: statusUpdateResult.applied,
1676
2282
  status_updates_buffered: statusUpdateResult.buffered,
1677
2283
  reported_skill_evidence_count: skillEvidence.length,
@@ -2736,6 +3342,7 @@ export function createAutoContinueEngine(deps) {
2736
3342
  // Try server KickoffContext (includes team context, acceptance criteria, etc.)
2737
3343
  let prompt;
2738
3344
  let kickoffContextHash = null;
3345
+ let kickoffRuntimeSettings = null;
2739
3346
  if (fetchKickoffContextSafeFn && renderKickoffMessageFn) {
2740
3347
  let kickoff = null;
2741
3348
  try {
@@ -2752,6 +3359,7 @@ export function createAutoContinueEngine(deps) {
2752
3359
  // best effort: fall back to local prompt
2753
3360
  }
2754
3361
  if (kickoff) {
3362
+ kickoffRuntimeSettings = kickoff.runtime_settings ?? null;
2755
3363
  const rendered = renderKickoffMessageFn({
2756
3364
  baseMessage: `Execute workstream slice for ${workstreamTitle ?? selectedWorkstreamId}`,
2757
3365
  kickoff,
@@ -2797,6 +3405,7 @@ export function createAutoContinueEngine(deps) {
2797
3405
  schemaPath,
2798
3406
  });
2799
3407
  }
3408
+ questionAutoAnswerPolicyByScope.set(questionScopeKey(run.initiativeId, selectedWorkstreamId), normalizeQuestionAutoAnswerPolicy(kickoffRuntimeSettings));
2800
3409
  // Append per-scope directive for milestone/workstream scopes.
2801
3410
  if (run.scope !== "task") {
2802
3411
  const msNodes = scopeMilestoneIds
@@ -3210,8 +3819,13 @@ export function createAutoContinueEngine(deps) {
3210
3819
  if (decisionIsBlocking(record))
3211
3820
  continue;
3212
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);
3213
3823
  if (typeof client.decideDecision === "function") {
3214
- await client.decideDecision(decisionId, "approve", { note: autoApprovalNote });
3824
+ await client.decideDecision(decisionId, "approve", {
3825
+ note: autoApprovalNote,
3826
+ source_client: autoApprovalSourceClient,
3827
+ sourceClient: autoApprovalSourceClient,
3828
+ });
3215
3829
  }
3216
3830
  else {
3217
3831
  await client.updateEntity("decision", decisionId, {