@useorgx/openclaw-plugin 0.7.23 → 0.7.25

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 (125) hide show
  1. package/dashboard/dist/assets/{m2smti3F.js → B6VftyY6.js} +1 -1
  2. package/dashboard/dist/assets/B6VftyY6.js.br +0 -0
  3. package/dashboard/dist/assets/B6VftyY6.js.gz +0 -0
  4. package/dashboard/dist/assets/{D-FuHfT8.js → BANQdlC4.js} +1 -1
  5. package/dashboard/dist/assets/BANQdlC4.js.br +0 -0
  6. package/dashboard/dist/assets/BANQdlC4.js.gz +0 -0
  7. package/dashboard/dist/assets/{DDCPrZRt.js → BPL4CL3c.js} +1 -1
  8. package/dashboard/dist/assets/BPL4CL3c.js.br +0 -0
  9. package/dashboard/dist/assets/BPL4CL3c.js.gz +0 -0
  10. package/dashboard/dist/assets/{D0PN5_vY.js → BZCkOZ20.js} +1 -1
  11. package/dashboard/dist/assets/BZCkOZ20.js.br +0 -0
  12. package/dashboard/dist/assets/BZCkOZ20.js.gz +0 -0
  13. package/dashboard/dist/assets/{DNQ-iFO2.js → B_LdOJUa.js} +1 -1
  14. package/dashboard/dist/assets/B_LdOJUa.js.br +0 -0
  15. package/dashboard/dist/assets/B_LdOJUa.js.gz +0 -0
  16. package/dashboard/dist/assets/Bfp-wdwb.css +1 -0
  17. package/dashboard/dist/assets/Bfp-wdwb.css.br +0 -0
  18. package/dashboard/dist/assets/{C1u2SGin.css.gz → Bfp-wdwb.css.gz} +0 -0
  19. package/dashboard/dist/assets/{CZXS5i_5.js → BvFcH_Iy.js} +1 -1
  20. package/dashboard/dist/assets/BvFcH_Iy.js.br +0 -0
  21. package/dashboard/dist/assets/BvFcH_Iy.js.gz +0 -0
  22. package/dashboard/dist/assets/C0i7ABUU.js +212 -0
  23. package/dashboard/dist/assets/C0i7ABUU.js.br +0 -0
  24. package/dashboard/dist/assets/C0i7ABUU.js.gz +0 -0
  25. package/dashboard/dist/assets/CFB0MM7j.js +1 -0
  26. package/dashboard/dist/assets/CFB0MM7j.js.br +0 -0
  27. package/dashboard/dist/assets/CFB0MM7j.js.gz +0 -0
  28. package/dashboard/dist/assets/{OlLPtzdz.js → CQSRb1yu.js} +1 -1
  29. package/dashboard/dist/assets/CQSRb1yu.js.br +0 -0
  30. package/dashboard/dist/assets/CQSRb1yu.js.gz +0 -0
  31. package/dashboard/dist/assets/{CbVWL74-.js → CUoQoSm-.js} +1 -1
  32. package/dashboard/dist/assets/CUoQoSm-.js.br +0 -0
  33. package/dashboard/dist/assets/CUoQoSm-.js.gz +0 -0
  34. package/dashboard/dist/assets/Ckd1R1iE.js +1 -0
  35. package/dashboard/dist/assets/Ckd1R1iE.js.br +0 -0
  36. package/dashboard/dist/assets/Ckd1R1iE.js.gz +0 -0
  37. package/dashboard/dist/assets/{DhPuHPK7.js → CqRNb2EL.js} +1 -1
  38. package/dashboard/dist/assets/CqRNb2EL.js.br +0 -0
  39. package/dashboard/dist/assets/CqRNb2EL.js.gz +0 -0
  40. package/dashboard/dist/assets/{CGJiHCIx.js → DClUc9rw.js} +1 -1
  41. package/dashboard/dist/assets/DClUc9rw.js.br +0 -0
  42. package/dashboard/dist/assets/DClUc9rw.js.gz +0 -0
  43. package/dashboard/dist/assets/DF2PMTwT.js +1 -0
  44. package/dashboard/dist/assets/DF2PMTwT.js.br +0 -0
  45. package/dashboard/dist/assets/DF2PMTwT.js.gz +0 -0
  46. package/dashboard/dist/assets/{RN4M9u9W.js → DJYl7gyA.js} +1 -1
  47. package/dashboard/dist/assets/DJYl7gyA.js.br +0 -0
  48. package/dashboard/dist/assets/DJYl7gyA.js.gz +0 -0
  49. package/dashboard/dist/assets/{BrMXbzQ-.js → DZtNMX0t.js} +1 -1
  50. package/dashboard/dist/assets/DZtNMX0t.js.br +0 -0
  51. package/dashboard/dist/assets/DZtNMX0t.js.gz +0 -0
  52. package/dashboard/dist/assets/{LOFrVoPD.js → DlEa8PI0.js} +1 -1
  53. package/dashboard/dist/assets/DlEa8PI0.js.br +0 -0
  54. package/dashboard/dist/assets/DlEa8PI0.js.gz +0 -0
  55. package/dashboard/dist/assets/M4QxcXjh.js +1 -0
  56. package/dashboard/dist/assets/M4QxcXjh.js.br +0 -0
  57. package/dashboard/dist/assets/M4QxcXjh.js.gz +0 -0
  58. package/dashboard/dist/assets/{nra1yvJX.js → MrW1ixGx.js} +1 -1
  59. package/dashboard/dist/assets/MrW1ixGx.js.br +0 -0
  60. package/dashboard/dist/assets/MrW1ixGx.js.gz +0 -0
  61. package/dashboard/dist/index.html +2 -2
  62. package/dashboard/dist/index.html.br +0 -0
  63. package/dashboard/dist/index.html.gz +0 -0
  64. package/dist/activity-store.js +68 -8
  65. package/dist/contracts/shared-types.d.ts +28 -0
  66. package/dist/http/helpers/auto-continue-engine.js +235 -32
  67. package/dist/http/helpers/triage-mapper.js +285 -6
  68. package/dist/http/helpers/value-utils.d.ts +1 -0
  69. package/dist/http/helpers/value-utils.js +17 -0
  70. package/dist/http/index.js +89 -3
  71. package/dist/http/routes/live-triage.js +6 -1
  72. package/dist/http/routes/mission-control-actions.d.ts +9 -0
  73. package/dist/http/routes/mission-control-actions.js +157 -7
  74. package/dist/http/routes/mission-control-read.d.ts +9 -0
  75. package/dist/http/routes/mission-control-read.js +33 -0
  76. package/dist/openclaw.plugin.json +1 -1
  77. package/dist/stores/sqlite-state.d.ts +1 -1
  78. package/dist/stores/sqlite-state.js +153 -2
  79. package/openclaw.plugin.json +1 -1
  80. package/package.json +1 -1
  81. package/dashboard/dist/assets/9gFmK3Kr.js +0 -1
  82. package/dashboard/dist/assets/9gFmK3Kr.js.br +0 -0
  83. package/dashboard/dist/assets/9gFmK3Kr.js.gz +0 -0
  84. package/dashboard/dist/assets/BrMXbzQ-.js.br +0 -0
  85. package/dashboard/dist/assets/BrMXbzQ-.js.gz +0 -0
  86. package/dashboard/dist/assets/C1u2SGin.css +0 -1
  87. package/dashboard/dist/assets/C1u2SGin.css.br +0 -0
  88. package/dashboard/dist/assets/CGJiHCIx.js.br +0 -0
  89. package/dashboard/dist/assets/CGJiHCIx.js.gz +0 -0
  90. package/dashboard/dist/assets/CSd4rSuU.js +0 -212
  91. package/dashboard/dist/assets/CSd4rSuU.js.br +0 -0
  92. package/dashboard/dist/assets/CSd4rSuU.js.gz +0 -0
  93. package/dashboard/dist/assets/CZXS5i_5.js.br +0 -0
  94. package/dashboard/dist/assets/CZXS5i_5.js.gz +0 -0
  95. package/dashboard/dist/assets/CbVWL74-.js.br +0 -0
  96. package/dashboard/dist/assets/CbVWL74-.js.gz +0 -0
  97. package/dashboard/dist/assets/D-FuHfT8.js.br +0 -0
  98. package/dashboard/dist/assets/D-FuHfT8.js.gz +0 -0
  99. package/dashboard/dist/assets/D0PN5_vY.js.br +0 -0
  100. package/dashboard/dist/assets/D0PN5_vY.js.gz +0 -0
  101. package/dashboard/dist/assets/DDCPrZRt.js.br +0 -0
  102. package/dashboard/dist/assets/DDCPrZRt.js.gz +0 -0
  103. package/dashboard/dist/assets/DNQ-iFO2.js.br +0 -0
  104. package/dashboard/dist/assets/DNQ-iFO2.js.gz +0 -0
  105. package/dashboard/dist/assets/DhPuHPK7.js.br +0 -0
  106. package/dashboard/dist/assets/DhPuHPK7.js.gz +0 -0
  107. package/dashboard/dist/assets/Dhz7qPtn.js +0 -1
  108. package/dashboard/dist/assets/Dhz7qPtn.js.br +0 -0
  109. package/dashboard/dist/assets/Dhz7qPtn.js.gz +0 -0
  110. package/dashboard/dist/assets/LOFrVoPD.js.br +0 -0
  111. package/dashboard/dist/assets/LOFrVoPD.js.gz +0 -0
  112. package/dashboard/dist/assets/OlLPtzdz.js.br +0 -0
  113. package/dashboard/dist/assets/OlLPtzdz.js.gz +0 -0
  114. package/dashboard/dist/assets/RN4M9u9W.js.br +0 -0
  115. package/dashboard/dist/assets/RN4M9u9W.js.gz +0 -0
  116. package/dashboard/dist/assets/VCHu272d.js +0 -1
  117. package/dashboard/dist/assets/VCHu272d.js.br +0 -0
  118. package/dashboard/dist/assets/VCHu272d.js.gz +0 -0
  119. package/dashboard/dist/assets/m2smti3F.js.br +0 -0
  120. package/dashboard/dist/assets/m2smti3F.js.gz +0 -0
  121. package/dashboard/dist/assets/nra1yvJX.js.br +0 -0
  122. package/dashboard/dist/assets/nra1yvJX.js.gz +0 -0
  123. package/dashboard/dist/assets/qLX6NZ-J.js +0 -1
  124. package/dashboard/dist/assets/qLX6NZ-J.js.br +0 -0
  125. package/dashboard/dist/assets/qLX6NZ-J.js.gz +0 -0
@@ -53,6 +53,126 @@ function countArray(record, keys) {
53
53
  }
54
54
  return 0;
55
55
  }
56
+ function pickBoolean(record, keys) {
57
+ if (!record)
58
+ return null;
59
+ for (const key of keys) {
60
+ const candidate = record[key];
61
+ if (typeof candidate === "boolean")
62
+ return candidate;
63
+ if (typeof candidate === "string") {
64
+ const normalized = candidate.trim().toLowerCase();
65
+ if (normalized === "true" || normalized === "yes" || normalized === "1")
66
+ return true;
67
+ if (normalized === "false" || normalized === "no" || normalized === "0")
68
+ return false;
69
+ }
70
+ }
71
+ return null;
72
+ }
73
+ function normalizeDecisionOptionsFromUnknown(...values) {
74
+ const options = [];
75
+ const seen = new Set();
76
+ for (const value of values) {
77
+ if (!Array.isArray(value))
78
+ continue;
79
+ for (const entry of value) {
80
+ if (typeof entry === "string") {
81
+ const label = entry.trim();
82
+ if (!label)
83
+ continue;
84
+ const key = label.toLowerCase();
85
+ if (seen.has(key))
86
+ continue;
87
+ seen.add(key);
88
+ options.push({ label });
89
+ continue;
90
+ }
91
+ const record = asRecord(entry);
92
+ if (!record)
93
+ continue;
94
+ const label = pickString(record, ["label", "title", "name", "question"]) ??
95
+ pickString(record, ["action", "action_type", "actionType"]);
96
+ if (!label)
97
+ continue;
98
+ const id = pickString(record, ["id", "option_id", "optionId"]);
99
+ const description = pickString(record, ["description", "summary"]);
100
+ const consequences = pickString(record, ["consequences", "impact"]);
101
+ const actionType = pickString(record, ["action_type", "actionType", "action"]);
102
+ const impliedStatus = pickString(record, ["implied_status", "impliedStatus", "status"]);
103
+ const requiresNote = pickBoolean(record, ["requires_note", "requiresNote", "note_required"]);
104
+ const recommended = pickBoolean(record, ["recommended", "is_recommended", "isRecommended"]);
105
+ const key = `${(id ?? "").toLowerCase()}|${label.toLowerCase()}`;
106
+ if (seen.has(key))
107
+ continue;
108
+ seen.add(key);
109
+ options.push({
110
+ ...(id ? { id } : {}),
111
+ label,
112
+ ...(description ? { description } : {}),
113
+ ...(consequences ? { consequences } : {}),
114
+ ...(actionType ? { actionType } : {}),
115
+ ...(impliedStatus ? { impliedStatus } : {}),
116
+ ...(typeof requiresNote === "boolean" ? { requiresNote } : {}),
117
+ ...(typeof recommended === "boolean" ? { recommended } : {}),
118
+ });
119
+ }
120
+ }
121
+ return options.slice(0, 8);
122
+ }
123
+ function normalizeEvidenceFromUnknown(...values) {
124
+ const evidence = [];
125
+ const seen = new Set();
126
+ for (const value of values) {
127
+ if (!Array.isArray(value))
128
+ continue;
129
+ for (const entry of value) {
130
+ const record = asRecord(entry);
131
+ if (!record)
132
+ continue;
133
+ const title = pickString(record, ["title", "label", "name"]) ??
134
+ pickString(record, ["source_pointer", "sourcePointer", "source_url", "sourceUrl"]) ??
135
+ "Evidence";
136
+ const summary = pickString(record, ["summary", "description"]);
137
+ const url = pickString(record, ["source_url", "sourceUrl", "url"]);
138
+ const pointer = pickString(record, ["source_pointer", "sourcePointer", "path"]);
139
+ const evidenceType = pickString(record, ["evidence_type", "evidenceType", "type"]);
140
+ const confidenceRaw = record.confidence ?? record.confidence_score;
141
+ const confidence = typeof confidenceRaw === "number" && Number.isFinite(confidenceRaw)
142
+ ? Math.max(0, Math.min(1, confidenceRaw))
143
+ : null;
144
+ const key = `${title.toLowerCase()}|${url ?? ""}|${pointer ?? ""}`;
145
+ if (seen.has(key))
146
+ continue;
147
+ seen.add(key);
148
+ evidence.push({
149
+ title,
150
+ ...(summary ? { summary } : {}),
151
+ ...(url ? { url } : {}),
152
+ ...(pointer ? { pointer } : {}),
153
+ ...(evidenceType ? { evidenceType } : {}),
154
+ ...(confidence !== null ? { confidence } : {}),
155
+ });
156
+ }
157
+ }
158
+ return evidence.slice(0, 8);
159
+ }
160
+ function pickHierarchy(record, keys) {
161
+ if (!record)
162
+ return [];
163
+ for (const key of keys) {
164
+ const candidate = record[key];
165
+ if (!Array.isArray(candidate))
166
+ continue;
167
+ const normalized = candidate
168
+ .filter((entry) => typeof entry === "string")
169
+ .map((entry) => entry.trim())
170
+ .filter(Boolean);
171
+ if (normalized.length > 0)
172
+ return normalized;
173
+ }
174
+ return [];
175
+ }
56
176
  function deriveInterventionContext(input) {
57
177
  const metadata = asRecord(input.metadata);
58
178
  const result = asRecord(metadata?.result);
@@ -101,6 +221,28 @@ function deriveInterventionContext(input) {
101
221
  countArray(metadata, ["task_updates", "taskUpdates"]);
102
222
  const milestoneUpdateCount = countArray(result, ["milestone_updates", "milestoneUpdates"]) ||
103
223
  countArray(metadata, ["milestone_updates", "milestoneUpdates"]);
224
+ const decisionPrompt = pickString(metadata, ["decision_prompt", "decisionPrompt", "question", "decision_title", "decisionTitle"]) ??
225
+ pickString(result, ["decision_prompt", "decisionPrompt", "question"]);
226
+ const decisionSummary = pickString(metadata, ["decision_summary", "decisionSummary", "summary", "context"]) ??
227
+ pickString(result, ["decision_summary", "decisionSummary", "summary"]);
228
+ const decisionOptions = normalizeDecisionOptionsFromUnknown(metadata?.decision_options, metadata?.decisionOptions, metadata?.options, result?.decision_options, result?.decisionOptions, result?.options);
229
+ const recommendedAction = pickString(metadata, ["recommended_action", "recommendedAction"]) ??
230
+ pickString(result, ["recommended_action", "recommendedAction"]) ??
231
+ requiredAction;
232
+ const scopeHierarchy = [
233
+ ...pickHierarchy(metadata, ["scope_hierarchy", "scopeHierarchy"]),
234
+ ...pickHierarchy(result, ["scope_hierarchy", "scopeHierarchy"]),
235
+ ].filter((entry, index, source) => source.indexOf(entry) === index);
236
+ const currentRunState = pickString(metadata, ["current_run_state", "currentRunState", "runtime_state", "runtimeState", "status"]) ??
237
+ pickString(result, ["current_run_state", "currentRunState", "runtime_state", "runtimeState", "status"]);
238
+ const impactIfDelayed = pickString(metadata, ["impact_if_delayed", "impactIfDelayed"]) ??
239
+ pickString(result, ["impact_if_delayed", "impactIfDelayed"]);
240
+ const evidence = normalizeEvidenceFromUnknown(metadata?.evidence_refs, metadata?.evidenceRefs, result?.evidence_refs, result?.evidenceRefs);
241
+ const artifacts = pickStringArray(metadata, ["artifacts_created", "artifact_titles", "artifactTitles"]);
242
+ const updatesApplied = [
243
+ ...pickStringArray(metadata, ["updates_applied", "updatesApplied"]),
244
+ ...pickStringArray(result, ["updates_applied", "updatesApplied"]),
245
+ ];
104
246
  const context = {
105
247
  blockerReason,
106
248
  waitingOn,
@@ -114,6 +256,16 @@ function deriveInterventionContext(input) {
114
256
  decisionIds: decisionIds.length > 0 ? Array.from(new Set(decisionIds)) : [],
115
257
  taskUpdateCount: taskUpdateCount > 0 ? taskUpdateCount : undefined,
116
258
  milestoneUpdateCount: milestoneUpdateCount > 0 ? milestoneUpdateCount : undefined,
259
+ decisionPrompt,
260
+ decisionSummary,
261
+ decisionOptions: decisionOptions.length > 0 ? decisionOptions : undefined,
262
+ recommendedAction,
263
+ scopeHierarchy: scopeHierarchy.length > 0 ? scopeHierarchy : undefined,
264
+ currentRunState,
265
+ impactIfDelayed,
266
+ artifacts: artifacts.length > 0 ? Array.from(new Set(artifacts)) : undefined,
267
+ evidence: evidence.length > 0 ? evidence : undefined,
268
+ updatesApplied: updatesApplied.length > 0 ? Array.from(new Set(updatesApplied)) : undefined,
117
269
  };
118
270
  const hasValue = [
119
271
  context.blockerReason,
@@ -125,9 +277,19 @@ function deriveInterventionContext(input) {
125
277
  context.retryable,
126
278
  context.taskUpdateCount,
127
279
  context.milestoneUpdateCount,
280
+ context.decisionPrompt,
281
+ context.decisionSummary,
282
+ context.recommendedAction,
283
+ context.currentRunState,
284
+ context.impactIfDelayed,
128
285
  context.suggestedActions?.length,
129
286
  context.nextActions?.length,
130
287
  context.decisionIds?.length,
288
+ context.decisionOptions?.length,
289
+ context.scopeHierarchy?.length,
290
+ context.artifacts?.length,
291
+ context.evidence?.length,
292
+ context.updatesApplied?.length,
131
293
  ].some((entry) => {
132
294
  if (typeof entry === "number")
133
295
  return entry > 0;
@@ -310,6 +472,72 @@ const FAILURE_MAPPINGS = {
310
472
  },
311
473
  ],
312
474
  },
475
+ decision_required: {
476
+ kind: "decision_required",
477
+ severity: "high",
478
+ recommendedAction: "Review the options and choose the next move",
479
+ defaultTitle: (ctx) => `Decision required${ctx.workstreamTitle ? `: ${ctx.workstreamTitle}` : ""}`,
480
+ defaultSummary: (ctx) => `${ctx.workstreamTitle ?? "This workstream"} cannot continue until a decision is made. ${ctx.reason ?? "Review the recommendation and choose a direction."}`,
481
+ actions: () => [
482
+ {
483
+ action: "approve",
484
+ label: "Approve path",
485
+ description: "Accept the recommended option and continue",
486
+ consequences: "Autopilot will continue with the approved direction.",
487
+ requiresNote: false,
488
+ available: true,
489
+ },
490
+ {
491
+ action: "reject",
492
+ label: "Reject path",
493
+ description: "Decline this path and provide direction",
494
+ consequences: "The run stays paused until new direction is provided.",
495
+ requiresNote: true,
496
+ available: true,
497
+ },
498
+ {
499
+ action: "snooze",
500
+ label: "Snooze",
501
+ description: "Defer this intervention",
502
+ consequences: "This decision returns to the queue later.",
503
+ requiresNote: false,
504
+ available: true,
505
+ },
506
+ ],
507
+ },
508
+ review_required: {
509
+ kind: "review_required",
510
+ severity: "medium",
511
+ recommendedAction: "Review the update and confirm the next step",
512
+ defaultTitle: (ctx) => `Review required${ctx.workstreamTitle ? `: ${ctx.workstreamTitle}` : ""}`,
513
+ defaultSummary: (ctx) => `${ctx.workstreamTitle ?? "This workstream"} surfaced something that needs judgment before it proceeds. ${ctx.reason ?? "Review the evidence and confirm what should happen next."}`,
514
+ actions: () => [
515
+ {
516
+ action: "approve",
517
+ label: "Approve",
518
+ description: "Confirm the proposed next step",
519
+ consequences: "The run continues with the reviewed direction.",
520
+ requiresNote: false,
521
+ available: true,
522
+ },
523
+ {
524
+ action: "reject",
525
+ label: "Send back",
526
+ description: "Request a different approach",
527
+ consequences: "The run pauses until new direction is provided.",
528
+ requiresNote: true,
529
+ available: true,
530
+ },
531
+ {
532
+ action: "snooze",
533
+ label: "Snooze",
534
+ description: "Return to this later",
535
+ consequences: "The review request will surface again later.",
536
+ requiresNote: false,
537
+ available: true,
538
+ },
539
+ ],
540
+ },
313
541
  budget_exhausted: {
314
542
  kind: "blocked_intervention",
315
543
  severity: "critical",
@@ -432,6 +660,19 @@ export async function mapFailureToTriageItem(input) {
432
660
  if (input.outputPath) {
433
661
  proofBundle.artifactRefs.push(input.outputPath);
434
662
  }
663
+ for (const artifact of intervention?.artifacts ?? []) {
664
+ if (!proofBundle.artifactRefs.includes(artifact)) {
665
+ proofBundle.artifactRefs.push(artifact);
666
+ }
667
+ }
668
+ for (const evidence of intervention?.evidence ?? []) {
669
+ if (evidence.url && !proofBundle.artifactRefs.includes(evidence.url)) {
670
+ proofBundle.artifactRefs.push(evidence.url);
671
+ }
672
+ if (evidence.pointer && !proofBundle.logRefs.includes(evidence.pointer)) {
673
+ proofBundle.logRefs.push(evidence.pointer);
674
+ }
675
+ }
435
676
  const impact = {
436
677
  initiativeCount: input.initiativeId ? 1 : 0,
437
678
  workstreamCount: input.workstreamId ? 1 : 0,
@@ -589,6 +830,41 @@ export function mapDecisionToTriageItem(decision) {
589
830
  });
590
831
  const blocking = metadata?.blocking !== false;
591
832
  const options = Array.isArray(decision.options) ? decision.options : [];
833
+ const enrichedIntervention = intervention || options.length > 0 || decision.context || decision.recommendedAction || decision.evidenceRefs?.length
834
+ ? {
835
+ ...(intervention ?? {}),
836
+ decisionPrompt: intervention?.decisionPrompt ??
837
+ decision.title,
838
+ decisionSummary: intervention?.decisionSummary ??
839
+ decision.context ??
840
+ null,
841
+ decisionOptions: intervention?.decisionOptions && intervention.decisionOptions.length > 0
842
+ ? intervention.decisionOptions
843
+ : options.map((option) => ({
844
+ id: option.id,
845
+ label: option.label,
846
+ description: option.description ?? null,
847
+ consequences: option.consequences ?? null,
848
+ actionType: option.actionType ?? null,
849
+ impliedStatus: option.impliedStatus ?? null,
850
+ requiresNote: option.requiresNote,
851
+ recommended: decision.selectedOptionId != null ? decision.selectedOptionId === option.id : false,
852
+ })),
853
+ recommendedAction: intervention?.recommendedAction ??
854
+ decision.recommendedAction ??
855
+ null,
856
+ evidence: intervention?.evidence && intervention.evidence.length > 0
857
+ ? intervention.evidence
858
+ : (decision.evidenceRefs ?? []).map((ref) => ({
859
+ title: ref.title ?? ref.sourcePointer ?? ref.sourceUrl ?? "Evidence",
860
+ summary: ref.summary ?? null,
861
+ url: ref.sourceUrl ?? null,
862
+ pointer: ref.sourcePointer ?? null,
863
+ evidenceType: ref.evidenceType ?? null,
864
+ confidence: ref.confidence ?? null,
865
+ })),
866
+ }
867
+ : null;
592
868
  const optionActions = options
593
869
  .map((option) => {
594
870
  const implied = (option.impliedStatus ?? "").toLowerCase();
@@ -657,7 +933,7 @@ export function mapDecisionToTriageItem(decision) {
657
933
  fileChanges: [],
658
934
  prRefs: [],
659
935
  logRefs: [],
660
- decisionRefs: Array.from(new Set([decision.id, ...(intervention?.decisionIds ?? [])].filter(Boolean))),
936
+ decisionRefs: Array.from(new Set([decision.id, ...(enrichedIntervention?.decisionIds ?? [])].filter(Boolean))),
661
937
  };
662
938
  if (decision.evidenceRefs) {
663
939
  for (const ref of decision.evidenceRefs) {
@@ -668,16 +944,19 @@ export function mapDecisionToTriageItem(decision) {
668
944
  }
669
945
  }
670
946
  const summaryBase = (typeof decision.context === "string" && decision.context.trim()) ||
671
- intervention?.blockerReason ||
947
+ enrichedIntervention?.blockerReason ||
672
948
  decision.title;
673
949
  const summarySuffix = [
674
- intervention?.waitingOn ? `Waiting on ${intervention.waitingOn}.` : null,
675
- intervention?.requiredAction ? `Required action: ${intervention.requiredAction}.` : null,
950
+ enrichedIntervention?.waitingOn ? `Waiting on ${enrichedIntervention.waitingOn}.` : null,
951
+ enrichedIntervention?.requiredAction ? `Required action: ${enrichedIntervention.requiredAction}.` : null,
676
952
  ]
677
953
  .filter((entry) => Boolean(entry))
678
954
  .join(" ");
679
955
  const summary = summarySuffix.length > 0 ? `${summaryBase} ${summarySuffix}` : summaryBase;
680
- const recommendedAction = decision.recommendedAction ?? intervention?.requiredAction ?? null;
956
+ const recommendedAction = decision.recommendedAction ??
957
+ enrichedIntervention?.recommendedAction ??
958
+ enrichedIntervention?.requiredAction ??
959
+ null;
681
960
  return {
682
961
  id: `triage-decision-${decision.id}`,
683
962
  kind: mapping?.kind ?? "decision_required",
@@ -698,7 +977,7 @@ export function mapDecisionToTriageItem(decision) {
698
977
  blocking,
699
978
  recommendedAction,
700
979
  agentId: decision.agentId ?? null,
701
- intervention,
980
+ intervention: enrichedIntervention,
702
981
  impact: {
703
982
  initiativeCount: decision.initiativeId ? 1 : 0,
704
983
  workstreamCount: decision.workstreamId ? 1 : 0,
@@ -1,5 +1,6 @@
1
1
  export declare function pickString(record: Record<string, unknown>, keys: string[]): string | null;
2
2
  export declare function pickNumber(record: Record<string, unknown>, keys: string[]): number | null;
3
+ export declare function pickBoolean(record: Record<string, unknown> | null, keys: string[]): boolean | null;
3
4
  export declare function pickHeaderString(headers: Record<string, string | string[] | undefined>, keys: string[]): string | null;
4
5
  export declare function toIsoString(value: string | null): string | null;
5
6
  export declare function parsePositiveInt(raw: string | null, fallback: number, max?: number): number;
@@ -23,6 +23,23 @@ export function pickNumber(record, keys) {
23
23
  }
24
24
  return null;
25
25
  }
26
+ export function pickBoolean(record, keys) {
27
+ if (!record)
28
+ return null;
29
+ for (const key of keys) {
30
+ const value = record[key];
31
+ if (typeof value === "boolean")
32
+ return value;
33
+ if (typeof value === "string") {
34
+ const normalized = value.trim().toLowerCase();
35
+ if (normalized === "true" || normalized === "yes" || normalized === "1")
36
+ return true;
37
+ if (normalized === "false" || normalized === "no" || normalized === "0")
38
+ return false;
39
+ }
40
+ }
41
+ return null;
42
+ }
26
43
  export function pickHeaderString(headers, keys) {
27
44
  for (const key of keys) {
28
45
  const candidates = [key, key.toLowerCase(), key.toUpperCase()];
@@ -56,7 +56,7 @@ import { configureOpenClawProviderRouting, fetchBillingStatusSafe, isPidAlive, l
56
56
  import { fetchKickoffContextSafe, renderKickoffMessage } from "./helpers/kickoff-context.js";
57
57
  import { createDispatchLifecycle } from "./helpers/dispatch-lifecycle.js";
58
58
  import { createRuntimeSseHub } from "./helpers/runtime-sse.js";
59
- import { parseBooleanQuery, parsePositiveInt, pickHeaderString, pickNumber, pickString, } from "./helpers/value-utils.js";
59
+ import { parseBooleanQuery, pickBoolean, parsePositiveInt, pickHeaderString, pickNumber, pickString, } from "./helpers/value-utils.js";
60
60
  import { registerAgentControlRoutes } from "./routes/agent-control.js";
61
61
  import { registerAgentSuiteRoutes } from "./routes/agent-suite.js";
62
62
  import { registerAgentsCatalogRoutes } from "./routes/agents-catalog.js";
@@ -2411,6 +2411,30 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2411
2411
  ? entry.blockers[0] ??
2412
2412
  (statusValues.includes("failed") ? "Latest run failed" : "Workstream blocked")
2413
2413
  : null,
2414
+ canStartNow: queueState === QueueState.QUEUED || queueState === QueueState.IDLE,
2415
+ startReasonCode: queueState === QueueState.RUNNING
2416
+ ? "already_running"
2417
+ : hasBlocked
2418
+ ? "blocked"
2419
+ : queueState === QueueState.QUEUED || queueState === QueueState.IDLE
2420
+ ? "dispatchable"
2421
+ : "not_startable",
2422
+ startReasonLabel: hasBlocked
2423
+ ? entry.blockers[0] ??
2424
+ (statusValues.includes("failed") ? "Latest run failed." : "Workstream blocked.")
2425
+ : queueState === QueueState.RUNNING
2426
+ ? "Already running."
2427
+ : "Ready to start this workstream.",
2428
+ dispatchableTask: queueState === QueueState.QUEUED || queueState === QueueState.IDLE
2429
+ ? {
2430
+ id: entry.latest.id ?? `${entry.initiativeId}:${entry.workstreamId}`,
2431
+ title: (entry.latest.lastEventSummary ?? "").trim() ||
2432
+ (entry.latest.title ?? "").trim() ||
2433
+ entry.workstreamTitle,
2434
+ scope: "workstream",
2435
+ milestoneId: null,
2436
+ }
2437
+ : null,
2414
2438
  isPinned: pinnedRankByKey.has(pinKey),
2415
2439
  pinnedRank: pinnedRankByKey.get(pinKey) ?? null,
2416
2440
  autoContinue: null,
@@ -2503,6 +2527,14 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2503
2527
  const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
2504
2528
  const workstreamNodes = graph.nodes.filter((node) => node.type === "workstream");
2505
2529
  const runningWorkstreams = new Set();
2530
+ const initiativeRun = autoContinueRuns.get(initiativeId) ?? null;
2531
+ const initiativeActiveRunIds = Array.isArray(initiativeRun?.activeSliceRunIds)
2532
+ ? initiativeRun.activeSliceRunIds
2533
+ .filter((id) => typeof id === "string" && id.trim().length > 0)
2534
+ .map((id) => id.trim())
2535
+ : typeof initiativeRun?.activeRunId === "string" && initiativeRun.activeRunId.trim().length > 0
2536
+ ? [initiativeRun.activeRunId.trim()]
2537
+ : [];
2506
2538
  const taskIsReady = (task) => task.dependencyIds.every((depId) => {
2507
2539
  const dependency = nodeById.get(depId);
2508
2540
  return dependency ? isDoneStatus(dependency.status) : true;
@@ -2703,6 +2735,36 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2703
2735
  if (isSuppressed(initiativeId, workstream.id) && queueState !== QueueState.RUNNING) {
2704
2736
  continue;
2705
2737
  }
2738
+ const dispatchableTask = preferredReadyTask ?? readyTask ?? null;
2739
+ const initiativeHasConcurrentRun = initiativeActiveRunIds.length > 0 &&
2740
+ laneState !== LaneState.RUNNING &&
2741
+ !(autoContinueRun && autoContinueRun.status === RunStatus.RUNNING);
2742
+ const canStartNow = Boolean(dispatchableTask) &&
2743
+ queueState !== QueueState.RUNNING &&
2744
+ queueState !== QueueState.BLOCKED &&
2745
+ !initiativeHasConcurrentRun;
2746
+ const startReasonCode = canStartNow
2747
+ ? "dispatchable"
2748
+ : queueState === QueueState.RUNNING
2749
+ ? "already_running"
2750
+ : initiativeHasConcurrentRun
2751
+ ? "initiative_run_active"
2752
+ : queueState === QueueState.BLOCKED
2753
+ ? "blocked"
2754
+ : !dispatchableTask
2755
+ ? "no_dispatchable_task"
2756
+ : "not_startable";
2757
+ const startReasonLabel = canStartNow
2758
+ ? `Ready to start ${dispatchableTask?.title ?? "this workstream"}.`
2759
+ : initiativeHasConcurrentRun
2760
+ ? "Autopilot is already running for this initiative."
2761
+ : queueState === QueueState.RUNNING
2762
+ ? "Already running."
2763
+ : blockReason
2764
+ ? blockReason
2765
+ : dispatchableTask
2766
+ ? "This workstream is not ready to start."
2767
+ : "No dispatchable task is available right now.";
2706
2768
  runningWorkstreams.add(workstream.id);
2707
2769
  const assignedRunnerAgents = [];
2708
2770
  const assignedRunnerSeen = new Set();
@@ -2763,6 +2825,17 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2763
2825
  runnerSource,
2764
2826
  queueState,
2765
2827
  blockReason,
2828
+ canStartNow,
2829
+ startReasonCode,
2830
+ startReasonLabel,
2831
+ dispatchableTask: dispatchableTask
2832
+ ? {
2833
+ id: dispatchableTask.id,
2834
+ title: dispatchableTask.title,
2835
+ scope: defaultScope,
2836
+ milestoneId: dispatchableTask.milestoneId ?? null,
2837
+ }
2838
+ : null,
2766
2839
  isPinned: Boolean(pin),
2767
2840
  pinnedRank: pin ? (pinnedRankByKey.get(pinKey) ?? null) : null,
2768
2841
  sliceScope: defaultScope,
@@ -2870,6 +2943,12 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2870
2943
  blockReason: queueState === QueueState.BLOCKED
2871
2944
  ? lane?.blockedReason ?? "Blocked"
2872
2945
  : null,
2946
+ canStartNow: false,
2947
+ startReasonCode: queueState === QueueState.RUNNING ? "already_running" : "initiative_run_active",
2948
+ startReasonLabel: queueState === QueueState.RUNNING
2949
+ ? "Already running."
2950
+ : "Autopilot is already running for this initiative.",
2951
+ dispatchableTask: null,
2873
2952
  isPinned: Boolean(pinnedByKey.get(`${initiativeId}:${workstream.id}`)),
2874
2953
  pinnedRank: pinnedRankByKey.get(`${initiativeId}:${workstream.id}`) ?? null,
2875
2954
  sliceScope,
@@ -3696,13 +3775,18 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3696
3775
  const keys = [...scopedKeys, ...fallbackKeys];
3697
3776
  const seen = new Set();
3698
3777
  const mapped = [];
3699
- const deriveFailureType = (eventNameRaw, actionTypeRaw, reasonRaw) => {
3778
+ const deriveFailureType = (eventNameRaw, actionTypeRaw, reasonRaw, blockingRaw) => {
3700
3779
  const eventName = (eventNameRaw ?? "").trim().toLowerCase();
3701
3780
  const actionType = (actionTypeRaw ?? "").trim().toLowerCase();
3702
3781
  const reason = (reasonRaw ?? "").trim().toLowerCase();
3703
3782
  const signature = `${eventName} ${actionType} ${reason}`;
3704
3783
  if (!signature.trim())
3705
3784
  return null;
3785
+ if (signature.includes("question_asked") ||
3786
+ signature.includes("review_item_created") ||
3787
+ signature.includes("decision_requested")) {
3788
+ return blockingRaw === false ? "review_required" : "decision_required";
3789
+ }
3706
3790
  if (signature.includes("status_updates_buffered"))
3707
3791
  return "status_updates_buffered";
3708
3792
  if (signature.includes("question_answer_failed"))
@@ -3766,8 +3850,10 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3766
3850
  pickString(resultRecord ?? {}, ["error", "reason", "blocked_reason", "blockedReason", "summary"]) ??
3767
3851
  pickString(metadataRecord, ["error", "reason", "message", "blocked_reason", "blockedReason"]) ??
3768
3852
  pickString(activityRecord, ["description", "summary", "title"]);
3853
+ const blockingSignal = pickBoolean(metadataRecord, ["blocking"]) ??
3854
+ pickBoolean(resultRecord, ["blocking"]);
3769
3855
  const failureType = deriveFailureType(pickString(metadataRecord, ["event", "event_name"]), pickString(metadataRecord, ["action_type", "actionType"]) ??
3770
- pickString(activityRecord, ["type"]), reasonText);
3856
+ pickString(activityRecord, ["type"]), reasonText, blockingSignal);
3771
3857
  if (!failureType)
3772
3858
  continue;
3773
3859
  const runId = pickString(metadataRecord, ["run_id", "source_run_id"]) ?? pickString(activityRecord, ["runId"]);
@@ -9,7 +9,12 @@ export function registerLiveTriageRoutes(router, deps) {
9
9
  // ─── GET /live/triage ─────────────────────────────────────────────
10
10
  router.add("GET", "live/triage", async ({ res, query }) => {
11
11
  const workspaceId = query.get("workspace_id") || null;
12
- const statusFilter = query.get("status") || "open";
12
+ const requestedStatus = query.get("status") || "open";
13
+ const statusFilter = requestedStatus === "pending"
14
+ ? "open"
15
+ : requestedStatus === "closed"
16
+ ? "resolved"
17
+ : requestedStatus;
13
18
  const limitStr = query.get("limit");
14
19
  const limit = limitStr ? Math.min(Math.max(1, parseInt(limitStr, 10) || 50), 200) : 50;
15
20
  const degraded = [];
@@ -23,6 +23,15 @@ type NextUpQueue = {
23
23
  sliceTaskIds?: string[];
24
24
  sliceTaskCount?: number | null;
25
25
  sliceMilestoneId?: string | null;
26
+ canStartNow?: boolean;
27
+ startReasonCode?: string | null;
28
+ startReasonLabel?: string | null;
29
+ dispatchableTask?: {
30
+ id: string;
31
+ title: string;
32
+ scope: "task" | "milestone" | "workstream";
33
+ milestoneId?: string | null;
34
+ } | null;
26
35
  executionPolicy?: {
27
36
  domain?: string;
28
37
  requiredSkills?: string[];