@agwab/pi-workflow 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +3 -1
  2. package/dist/artifact-graph-runtime.d.ts +1 -1
  3. package/dist/artifact-graph-runtime.js +10 -5
  4. package/dist/artifact-graph-schema.js +127 -5
  5. package/dist/compiler.js +52 -19
  6. package/dist/dynamic-generated-task-runtime.js +3 -1
  7. package/dist/dynamic-profiles.d.ts +1 -1
  8. package/dist/engine-run-graph.d.ts +3 -0
  9. package/dist/engine-run-graph.js +194 -4
  10. package/dist/engine.d.ts +5 -0
  11. package/dist/engine.js +389 -41
  12. package/dist/extension.d.ts +2 -1
  13. package/dist/extension.js +30 -8
  14. package/dist/index.d.ts +11 -3
  15. package/dist/index.js +6 -1
  16. package/dist/prompt-json.d.ts +7 -0
  17. package/dist/prompt-json.js +13 -0
  18. package/dist/roles.d.ts +1 -1
  19. package/dist/roles.js +5 -8
  20. package/dist/store.d.ts +20 -1
  21. package/dist/store.js +139 -35
  22. package/dist/strings.d.ts +11 -0
  23. package/dist/strings.js +24 -0
  24. package/dist/subagent-backend.js +710 -40
  25. package/dist/types.d.ts +107 -1
  26. package/dist/verification-ontology.d.ts +31 -0
  27. package/dist/verification-ontology.js +66 -0
  28. package/dist/workflow-artifact-tool.js +5 -6
  29. package/dist/workflow-artifacts.d.ts +7 -0
  30. package/dist/workflow-artifacts.js +55 -4
  31. package/dist/workflow-fetch-cache-extension.d.ts +1 -0
  32. package/dist/workflow-fetch-cache-extension.js +57 -9
  33. package/dist/workflow-metrics.d.ts +113 -0
  34. package/dist/workflow-metrics.js +272 -0
  35. package/dist/workflow-output-artifacts.js +5 -3
  36. package/dist/workflow-partial-output.d.ts +45 -0
  37. package/dist/workflow-partial-output.js +205 -0
  38. package/dist/workflow-progress-health.js +42 -10
  39. package/dist/workflow-runtime.js +10 -1
  40. package/dist/workflow-view.js +3 -1
  41. package/dist/workflow-web-source-extension.js +194 -52
  42. package/dist/workflow-web-source.d.ts +2 -1
  43. package/dist/workflow-web-source.js +109 -30
  44. package/docs/usage.md +76 -29
  45. package/node_modules/@agwab/pi-subagent/README.md +3 -3
  46. package/node_modules/@agwab/pi-subagent/api.mjs +1 -0
  47. package/node_modules/@agwab/pi-subagent/docs/usage.md +63 -12
  48. package/node_modules/@agwab/pi-subagent/package.json +2 -2
  49. package/node_modules/@agwab/pi-subagent/src/api.ts +54 -1
  50. package/node_modules/@agwab/pi-subagent/src/artifacts/registry.ts +9 -4
  51. package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +8 -0
  52. package/node_modules/@agwab/pi-subagent/src/core/constants.ts +9 -0
  53. package/node_modules/@agwab/pi-subagent/src/core/validation.ts +21 -0
  54. package/node_modules/@agwab/pi-subagent/src/index.ts +1046 -576
  55. package/node_modules/@agwab/pi-subagent/src/orchestrate/async.ts +279 -156
  56. package/node_modules/@agwab/pi-subagent/src/orchestrate/interrupt.ts +165 -89
  57. package/node_modules/@agwab/pi-subagent/src/orchestrate/reconcile.ts +111 -65
  58. package/node_modules/@agwab/pi-subagent/src/orchestrate/run-ref.ts +219 -0
  59. package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +88 -8
  60. package/node_modules/@agwab/pi-subagent/src/orchestrate/status.ts +614 -298
  61. package/node_modules/@agwab/pi-subagent/src/panel.ts +1356 -560
  62. package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +53 -5
  63. package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +13 -6
  64. package/package.json +2 -2
  65. package/skills/workflow-guide/SKILL.md +1 -0
  66. package/src/artifact-graph-runtime.ts +19 -13
  67. package/src/artifact-graph-schema.ts +143 -3
  68. package/src/cli.mjs +52 -0
  69. package/src/compiler.ts +63 -18
  70. package/src/dynamic-generated-task-runtime.ts +3 -1
  71. package/src/dynamic-profiles.ts +1 -1
  72. package/src/engine-run-graph.ts +246 -4
  73. package/src/engine.ts +545 -38
  74. package/src/extension.ts +36 -6
  75. package/src/index.ts +52 -1
  76. package/src/prompt-json.ts +13 -0
  77. package/src/roles.ts +6 -9
  78. package/src/store.ts +194 -42
  79. package/src/strings.ts +38 -0
  80. package/src/subagent-backend.ts +921 -62
  81. package/src/types.ts +116 -2
  82. package/src/verification-ontology.ts +88 -0
  83. package/src/workflow-artifact-tool.ts +5 -7
  84. package/src/workflow-artifacts.ts +83 -3
  85. package/src/workflow-fetch-cache-extension.ts +78 -13
  86. package/src/workflow-metrics.ts +478 -0
  87. package/src/workflow-output-artifacts.ts +5 -3
  88. package/src/workflow-partial-output.ts +299 -0
  89. package/src/workflow-progress-health.ts +47 -15
  90. package/src/workflow-runtime.ts +18 -2
  91. package/src/workflow-view.ts +2 -1
  92. package/src/workflow-web-source-extension.ts +654 -232
  93. package/src/workflow-web-source.ts +153 -39
  94. package/workflows/README.md +7 -25
  95. package/workflows/deep-research/batched-verification.spec.json +253 -0
  96. package/workflows/deep-research/helpers/batch-verification-candidates.mjs +136 -0
  97. package/workflows/deep-research/helpers/claim-evidence-gate.mjs +229 -36
  98. package/workflows/deep-research/helpers/final-audit-packet.mjs +1 -4
  99. package/workflows/deep-research/helpers/normalize-input-packet.mjs +81 -2
  100. package/workflows/deep-research/helpers/render-executive.mjs +40 -26
  101. package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +89 -15
  102. package/workflows/deep-research/helpers/shadow-select-verification.mjs +229 -0
  103. package/workflows/deep-research/helpers/verification-ontology.mjs +77 -0
  104. package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +3 -3
  105. package/workflows/deep-research/schemas/deep-research-research-questions-control.schema.json +38 -0
  106. package/workflows/deep-research/schemas/deep-research-sanitize-claims-control.schema.json +63 -0
  107. package/workflows/deep-research/schemas/deep-research-verify-claims-batch-control.schema.json +47 -0
  108. package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +13 -3
  109. package/workflows/deep-research/spec.json +32 -12
  110. package/workflows/impact-review/spec.json +3 -3
  111. package/workflows/spec-review/helpers/spec-review-pipeline.mjs +1 -8
  112. package/dist/dynamic-loader.d.ts +0 -25
  113. package/dist/dynamic-loader.js +0 -13
  114. package/skills/workflow-guide/scaffolds/dag-required-reads/spec.json.validate.stderr +0 -0
  115. package/skills/workflow-guide/scaffolds/dag-required-reads/spec.json.validate.stdout +0 -13
  116. package/src/dynamic-loader.ts +0 -49
  117. package/workflows/impact-review/schemas/docs-release-impact-control.schema.json +0 -42
  118. package/workflows/impact-review/schemas/security-performance-impact-control.schema.json +0 -42
  119. package/workflows/impact-review/schemas/state-data-impact-control.schema.json +0 -42
@@ -0,0 +1,299 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ import { hashDynamicRequest } from "./dynamic-events.js";
5
+ import { readJson, writeJsonAtomic } from "./store.js";
6
+
7
+ export const WORKFLOW_PARTIAL_OUTPUT_PROTOCOL =
8
+ "workflow-partial-output-v1" as const;
9
+ export const WORKFLOW_PARTIAL_OUTPUT_LEDGER_SCHEMA =
10
+ "workflow-partial-output-ledger-v1" as const;
11
+ export const WORKFLOW_PARTIAL_OUTPUT_LEDGER_FILE = "partial-control.json";
12
+
13
+ export type WorkflowPartialOutputIssueCode =
14
+ | "invalid_json"
15
+ | "invalid_type"
16
+ | "invalid_schema"
17
+ | "invalid_path"
18
+ | "disallowed_path"
19
+ | "missing_items"
20
+ | "missing_item_id"
21
+ | "duplicate_item_id";
22
+
23
+ export interface WorkflowPartialOutputIssue {
24
+ code: WorkflowPartialOutputIssueCode;
25
+ message: string;
26
+ sectionIndex?: number;
27
+ path?: string;
28
+ itemId?: string;
29
+ }
30
+
31
+ export interface WorkflowPartialOutputItem {
32
+ path: string;
33
+ itemId: string;
34
+ itemHash: string;
35
+ item: unknown;
36
+ ordinal: number;
37
+ sectionIndex: number;
38
+ sectionItemIndex: number;
39
+ itemRef: string;
40
+ }
41
+
42
+ export interface WorkflowPartialOutputLedger {
43
+ schema: typeof WORKFLOW_PARTIAL_OUTPUT_LEDGER_SCHEMA;
44
+ protocol: typeof WORKFLOW_PARTIAL_OUTPUT_PROTOCOL;
45
+ items: WorkflowPartialOutputItem[];
46
+ issues: WorkflowPartialOutputIssue[];
47
+ }
48
+
49
+ export interface ParseWorkflowPartialOutputOptions {
50
+ allowedPaths?: readonly string[];
51
+ }
52
+
53
+ interface PartialSectionMatch {
54
+ content: string;
55
+ start: number;
56
+ end: number;
57
+ index: number;
58
+ }
59
+
60
+ const PARTIAL_CONTROL_OPEN = "partial-control";
61
+
62
+ export function partialOutputLedgerPath(taskDir: string): string {
63
+ return join(taskDir, WORKFLOW_PARTIAL_OUTPUT_LEDGER_FILE);
64
+ }
65
+
66
+ export async function readWorkflowPartialOutputLedger(
67
+ taskDir: string,
68
+ ): Promise<WorkflowPartialOutputLedger | undefined> {
69
+ return await readJson<WorkflowPartialOutputLedger>(
70
+ partialOutputLedgerPath(taskDir),
71
+ );
72
+ }
73
+
74
+ export async function writeWorkflowPartialOutputLedger(options: {
75
+ taskDir: string;
76
+ rawOutput: string;
77
+ allowedPaths?: readonly string[];
78
+ }): Promise<WorkflowPartialOutputLedger> {
79
+ const ledger = parseWorkflowPartialOutput(options.rawOutput, {
80
+ allowedPaths: options.allowedPaths,
81
+ });
82
+ await writeJsonAtomic(partialOutputLedgerPath(options.taskDir), ledger);
83
+ return ledger;
84
+ }
85
+
86
+ export async function writeWorkflowPartialOutputLedgerFromFile(options: {
87
+ taskDir: string;
88
+ outputFile: string;
89
+ allowedPaths?: readonly string[];
90
+ }): Promise<WorkflowPartialOutputLedger | undefined> {
91
+ const rawOutput = await readFile(options.outputFile, "utf8").catch(
92
+ () => undefined,
93
+ );
94
+ if (rawOutput === undefined) return undefined;
95
+ return await writeWorkflowPartialOutputLedger({
96
+ taskDir: options.taskDir,
97
+ rawOutput,
98
+ allowedPaths: options.allowedPaths,
99
+ });
100
+ }
101
+
102
+ export function stripWorkflowPartialOutputSections(raw: string): string {
103
+ if (!raw.includes(PARTIAL_CONTROL_OPEN)) return raw;
104
+ return raw.replace(partialControlSectionRegExp(), "");
105
+ }
106
+
107
+ export function parseWorkflowPartialOutput(
108
+ raw: string,
109
+ options: ParseWorkflowPartialOutputOptions = {},
110
+ ): WorkflowPartialOutputLedger {
111
+ const allowedPaths = options.allowedPaths
112
+ ? new Set(options.allowedPaths)
113
+ : undefined;
114
+ const items: WorkflowPartialOutputItem[] = [];
115
+ const issues: WorkflowPartialOutputIssue[] = [];
116
+ const byPathAndId = new Map<string, WorkflowPartialOutputItem>();
117
+
118
+ for (const section of collectPartialControlSections(raw)) {
119
+ const parsed = parsePartialSectionJson(section, issues);
120
+ if (!parsed) continue;
121
+ const path = parsePartialSectionPath(parsed, section, allowedPaths, issues);
122
+ if (!path) continue;
123
+ const rawItems = parsePartialSectionItems(parsed, section, path, issues);
124
+ if (!rawItems) continue;
125
+ for (const [sectionItemIndex, item] of rawItems.entries()) {
126
+ const itemId = stablePartialItemId(item);
127
+ if (!itemId) {
128
+ issues.push({
129
+ code: "missing_item_id",
130
+ sectionIndex: section.index,
131
+ path,
132
+ message:
133
+ "partial output items must be objects with a stable non-empty string id",
134
+ });
135
+ continue;
136
+ }
137
+ const itemHash = hashDynamicRequest(item);
138
+ const key = `${path}\0${itemId}`;
139
+ const existing = byPathAndId.get(key);
140
+ if (existing) {
141
+ if (existing.itemHash !== itemHash) {
142
+ issues.push({
143
+ code: "duplicate_item_id",
144
+ sectionIndex: section.index,
145
+ path,
146
+ itemId,
147
+ message: `partial output item ${itemId} at ${path} changed after it was published`,
148
+ });
149
+ }
150
+ continue;
151
+ }
152
+ const ordinal = items.length;
153
+ const partialItem: WorkflowPartialOutputItem = {
154
+ path,
155
+ itemId,
156
+ itemHash,
157
+ item,
158
+ ordinal,
159
+ sectionIndex: section.index,
160
+ sectionItemIndex,
161
+ itemRef: `${WORKFLOW_PARTIAL_OUTPUT_LEDGER_FILE}#/items/${ordinal}`,
162
+ };
163
+ items.push(partialItem);
164
+ byPathAndId.set(key, partialItem);
165
+ }
166
+ }
167
+
168
+ return {
169
+ schema: WORKFLOW_PARTIAL_OUTPUT_LEDGER_SCHEMA,
170
+ protocol: WORKFLOW_PARTIAL_OUTPUT_PROTOCOL,
171
+ items,
172
+ issues,
173
+ };
174
+ }
175
+
176
+ export function hasFatalPartialOutputIssue(
177
+ ledger: Pick<WorkflowPartialOutputLedger, "issues"> | undefined,
178
+ ): WorkflowPartialOutputIssue | undefined {
179
+ return ledger?.issues.find((issue) => issue.code === "duplicate_item_id");
180
+ }
181
+
182
+ function collectPartialControlSections(raw: string): PartialSectionMatch[] {
183
+ if (!raw.includes(PARTIAL_CONTROL_OPEN)) return [];
184
+ const matches: PartialSectionMatch[] = [];
185
+ const re = partialControlSectionRegExp();
186
+ let match: RegExpExecArray | null;
187
+ while ((match = re.exec(raw)) !== null) {
188
+ matches.push({
189
+ content: match[1] ?? "",
190
+ start: match.index,
191
+ end: re.lastIndex,
192
+ index: matches.length,
193
+ });
194
+ }
195
+ return matches;
196
+ }
197
+
198
+ function partialControlSectionRegExp(): RegExp {
199
+ return /[ \t]*<partial-control\s*>([\s\S]*?)<\/partial-control>[ \t]*(?:\r?\n)?/gi;
200
+ }
201
+
202
+ function parsePartialSectionJson(
203
+ section: PartialSectionMatch,
204
+ issues: WorkflowPartialOutputIssue[],
205
+ ): Record<string, unknown> | undefined {
206
+ let parsed: unknown;
207
+ try {
208
+ parsed = JSON.parse(section.content.trim());
209
+ } catch (error) {
210
+ issues.push({
211
+ code: "invalid_json",
212
+ sectionIndex: section.index,
213
+ message: error instanceof Error ? error.message : String(error),
214
+ });
215
+ return undefined;
216
+ }
217
+ if (!isRecord(parsed)) {
218
+ issues.push({
219
+ code: "invalid_type",
220
+ sectionIndex: section.index,
221
+ message: "partial-control section must contain a JSON object",
222
+ });
223
+ return undefined;
224
+ }
225
+ if (parsed.schema !== WORKFLOW_PARTIAL_OUTPUT_PROTOCOL) {
226
+ issues.push({
227
+ code: "invalid_schema",
228
+ sectionIndex: section.index,
229
+ message: `partial-control schema must be ${WORKFLOW_PARTIAL_OUTPUT_PROTOCOL}`,
230
+ });
231
+ return undefined;
232
+ }
233
+ return parsed;
234
+ }
235
+
236
+ function parsePartialSectionPath(
237
+ section: Record<string, unknown>,
238
+ match: PartialSectionMatch,
239
+ allowedPaths: Set<string> | undefined,
240
+ issues: WorkflowPartialOutputIssue[],
241
+ ): string | undefined {
242
+ const path = section.path;
243
+ if (typeof path !== "string" || !path.startsWith("$.")) {
244
+ issues.push({
245
+ code: "invalid_path",
246
+ sectionIndex: match.index,
247
+ message: "partial-control path must be a control JSONPath starting with $.",
248
+ });
249
+ return undefined;
250
+ }
251
+ if (allowedPaths && !allowedPaths.has(path)) {
252
+ issues.push({
253
+ code: "disallowed_path",
254
+ sectionIndex: match.index,
255
+ path,
256
+ message: `partial-control path ${path} is not declared for this stage`,
257
+ });
258
+ return undefined;
259
+ }
260
+ return path;
261
+ }
262
+
263
+ function parsePartialSectionItems(
264
+ section: Record<string, unknown>,
265
+ match: PartialSectionMatch,
266
+ path: string,
267
+ issues: WorkflowPartialOutputIssue[],
268
+ ): unknown[] | undefined {
269
+ const items = section.items;
270
+ if (!Array.isArray(items)) {
271
+ issues.push({
272
+ code: "missing_items",
273
+ sectionIndex: match.index,
274
+ path,
275
+ message: "partial-control items must be an array",
276
+ });
277
+ return undefined;
278
+ }
279
+ return items;
280
+ }
281
+
282
+ function stablePartialItemId(item: unknown): string | undefined {
283
+ if (!isRecord(item) || typeof item.id !== "string") return undefined;
284
+ const sanitized = sanitizePartialItemId(item.id);
285
+ return sanitized || undefined;
286
+ }
287
+
288
+ function sanitizePartialItemId(value: string): string {
289
+ return value
290
+ .trim()
291
+ .toLowerCase()
292
+ .replace(/[^a-z0-9_.-]+/g, "-")
293
+ .replace(/^-+|-+$/g, "")
294
+ .slice(0, 64);
295
+ }
296
+
297
+ function isRecord(value: unknown): value is Record<string, unknown> {
298
+ return typeof value === "object" && value !== null && !Array.isArray(value);
299
+ }
@@ -104,16 +104,16 @@ export function diagnoseWorkflowRunHealth(
104
104
  options: WorkflowHealthOptions = {},
105
105
  ): WorkflowProgressHealth {
106
106
  const nowMs = options.nowMs ?? Date.now();
107
- const runningTask = currentRunningTask(run.tasks ?? []);
108
- if (runningTask)
109
- return diagnoseWorkflowTaskHealth(runningTask, run, { nowMs });
110
-
111
107
  const problem = (run.tasks ?? []).find((task) =>
112
108
  isProblemStatus(task.status),
113
109
  );
114
110
  if (problem) return problemRunHealth(problem, nowMs);
115
111
  if (isProblemStatus(run.status)) return problemWorkflowHealth(run.status);
116
112
  if (run.status === "completed") return completedWorkflowHealth();
113
+
114
+ const runningTask = currentRunningTask(run.tasks ?? []);
115
+ if (runningTask)
116
+ return diagnoseWorkflowTaskHealth(runningTask, run, { nowMs });
117
117
  return waitingWorkflowHealth(run, nowMs);
118
118
  }
119
119
 
@@ -143,19 +143,20 @@ export function classifyWorkflowTaskDuration(
143
143
  .filter(Boolean)
144
144
  .join(" ")
145
145
  .toLowerCase();
146
- if (/\b(render|helper|support|schema|partition|gate)\b/.test(text))
147
- return "short";
148
- if (
149
- /\b(research|audit|synthesis|review|verify|verifier|normalize|plan|impact|spec)\b/.test(
150
- text,
151
- )
152
- )
153
- return "long";
154
146
  const maxRuntimeMs = task.runtime?.maxRuntimeMs;
155
147
  if (maxRuntimeMs !== undefined && Number.isFinite(maxRuntimeMs)) {
156
148
  if (maxRuntimeMs <= 5 * 60_000) return "short";
157
149
  if (maxRuntimeMs >= 60 * 60_000) return "long";
158
150
  }
151
+ if (task.kind === "support") return "short";
152
+ if (
153
+ /\b(research|audit|synthesi[sz]e?r?|review(?:er|ers|ing|s)?|verif(?:y|ier|iers|ication)|normaliz(?:e|er|ing|ation)?|plan(?:ning)?|impact|spec)\b/.test(
154
+ text,
155
+ )
156
+ )
157
+ return "long";
158
+ if (/\b(render|helper|support|schema|partition|gate)\b/.test(text))
159
+ return "short";
159
160
  return "medium";
160
161
  }
161
162
 
@@ -215,6 +216,33 @@ function waitingWorkflowHealth(
215
216
  nowMs: number,
216
217
  ): WorkflowProgressHealth {
217
218
  const hasPending = run.taskSummary.pending > 0;
219
+ const lastActivityAgeMs = ageMs(run.updatedAt, nowMs);
220
+ if (hasPending && lastActivityAgeMs !== undefined) {
221
+ if (lastActivityAgeMs >= STUCK_BY_DURATION.medium) {
222
+ return {
223
+ state: "likely-stuck",
224
+ label: "scheduler stuck",
225
+ summary: "pending tasks have not scheduled",
226
+ tone: "error",
227
+ suggestion: "resume",
228
+ reason: "no task is running and run activity is stale",
229
+ lastActivityAt: run.updatedAt,
230
+ lastActivityAgeMs,
231
+ };
232
+ }
233
+ if (lastActivityAgeMs >= STALL_BY_DURATION.medium) {
234
+ return {
235
+ state: "stalled",
236
+ label: "scheduler quiet",
237
+ summary: "pending tasks are waiting without recent activity",
238
+ tone: "warning",
239
+ suggestion: "inspect",
240
+ reason: "no task is running and run activity is stale",
241
+ lastActivityAt: run.updatedAt,
242
+ lastActivityAgeMs,
243
+ };
244
+ }
245
+ }
218
246
  return {
219
247
  state: hasPending ? "pending" : "active",
220
248
  label: hasPending ? "pending" : "active",
@@ -227,7 +255,7 @@ function waitingWorkflowHealth(
227
255
  ? "no task is currently running"
228
256
  : "workflow is still in progress",
229
257
  lastActivityAt: run.updatedAt,
230
- lastActivityAgeMs: ageMs(run.updatedAt, nowMs),
258
+ lastActivityAgeMs,
231
259
  };
232
260
  }
233
261
 
@@ -335,8 +363,12 @@ function runningContext(
335
363
  const durationClass = classifyWorkflowTaskDuration(task);
336
364
  const startedAtMs = parseTime(task.startedAt);
337
365
  const heartbeatAt = parseHeartbeatAt(task.lastMessage);
366
+ const heartbeatAgeMs = ageMs(heartbeatAt, nowMs);
338
367
  const activityAt = latestIso([heartbeatAt, run?.updatedAt, task.startedAt]);
339
368
  const lastActivityAgeMs = ageMs(activityAt, nowMs);
369
+ const hasFreshHeartbeat =
370
+ heartbeatAgeMs !== undefined &&
371
+ heartbeatAgeMs <= STALL_BY_DURATION[durationClass];
340
372
  return {
341
373
  task,
342
374
  nowMs,
@@ -346,8 +378,8 @@ function runningContext(
346
378
  activityAt,
347
379
  lastActivityAgeMs,
348
380
  heartbeatAt,
349
- heartbeatAgeMs: ageMs(heartbeatAt, nowMs),
350
- hasBackendSignal: Boolean(task.backendHandle || task.pid || heartbeatAt),
381
+ heartbeatAgeMs,
382
+ hasBackendSignal: Boolean(task.pid || hasFreshHeartbeat),
351
383
  staleMs: lastActivityAgeMs ?? Number.POSITIVE_INFINITY,
352
384
  };
353
385
  }
@@ -345,9 +345,25 @@ export function readSimpleJsonPath(value: unknown, path: string): unknown {
345
345
  const parts = path.slice(2).split(".").filter(Boolean);
346
346
  let current = value as any;
347
347
  for (const part of parts) {
348
- if (current === null || typeof current !== "object" || !(part in current))
349
- return undefined;
348
+ if (!canReadJsonPathPart(current, part)) return undefined;
350
349
  current = current[part];
351
350
  }
352
351
  return current;
353
352
  }
353
+
354
+ function canReadJsonPathPart(
355
+ value: unknown,
356
+ part: string,
357
+ ): value is Record<string, unknown> {
358
+ return (
359
+ isSafeJsonPathPart(part) && isRecord(value) && Object.hasOwn(value, part)
360
+ );
361
+ }
362
+
363
+ function isSafeJsonPathPart(part: string): boolean {
364
+ return part !== "__proto__" && part !== "prototype" && part !== "constructor";
365
+ }
366
+
367
+ function isRecord(value: unknown): value is Record<string, unknown> {
368
+ return typeof value === "object" && value !== null;
369
+ }
@@ -1370,10 +1370,11 @@ function statusForSummary(
1370
1370
  ): WorkflowRunStatus | TaskRunStatus {
1371
1371
  if (summary.running > 0) return "running";
1372
1372
  if (summary.blocked > 0) return "blocked";
1373
- if (summary.failed > 0 || summary.interrupted > 0) return "failed";
1373
+ if (summary.failed > 0) return "failed";
1374
1374
  if (summary.pending > 0) return "pending";
1375
1375
  if (summary.total > 0 && summary.completed === summary.total)
1376
1376
  return "completed";
1377
+ if (summary.interrupted > 0) return "interrupted";
1377
1378
  return "interrupted";
1378
1379
  }
1379
1380