@osolmaz/pi-workflows 0.9.1 → 0.10.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 (75) hide show
  1. package/README.md +50 -16
  2. package/dist/builtins/autodevise.workflow.d.ts +58 -0
  3. package/dist/builtins/autodevise.workflow.js +190 -0
  4. package/dist/builtins/autodevise.workflow.js.map +1 -0
  5. package/dist/builtins/autoimplement.workflow.d.ts +154 -0
  6. package/dist/builtins/autoimplement.workflow.js +729 -0
  7. package/dist/builtins/autoimplement.workflow.js.map +1 -0
  8. package/dist/builtins/catalog.js +5 -1
  9. package/dist/builtins/catalog.js.map +1 -1
  10. package/dist/builtins/index.d.ts +3 -0
  11. package/dist/builtins/index.js +4 -0
  12. package/dist/builtins/index.js.map +1 -0
  13. package/dist/builtins/monitor.workflow.d.ts +25 -3
  14. package/dist/builtins/monitor.workflow.js +200 -13
  15. package/dist/builtins/monitor.workflow.js.map +1 -1
  16. package/dist/render/graph-render.js +13 -2
  17. package/dist/render/graph-render.js.map +1 -1
  18. package/dist/workflows/catalog.d.ts +1 -0
  19. package/dist/workflows/catalog.js +6 -0
  20. package/dist/workflows/catalog.js.map +1 -1
  21. package/dist/workflows/composition.d.ts +45 -0
  22. package/dist/workflows/composition.js +471 -0
  23. package/dist/workflows/composition.js.map +1 -0
  24. package/dist/workflows/decision.d.ts +11 -5
  25. package/dist/workflows/decision.js.map +1 -1
  26. package/dist/workflows/definition.d.ts +22 -3
  27. package/dist/workflows/definition.js +46 -3
  28. package/dist/workflows/definition.js.map +1 -1
  29. package/dist/workflows/engine.js +115 -16
  30. package/dist/workflows/engine.js.map +1 -1
  31. package/dist/workflows/graph.js +8 -6
  32. package/dist/workflows/graph.js.map +1 -1
  33. package/dist/workflows/index.d.ts +3 -2
  34. package/dist/workflows/index.js +2 -1
  35. package/dist/workflows/index.js.map +1 -1
  36. package/dist/workflows/loader.d.ts +5 -4
  37. package/dist/workflows/loader.js +118 -18
  38. package/dist/workflows/loader.js.map +1 -1
  39. package/dist/workflows/schema.d.ts +3 -1
  40. package/dist/workflows/schema.js +49 -2
  41. package/dist/workflows/schema.js.map +1 -1
  42. package/dist/workflows/store.js +32 -2
  43. package/dist/workflows/store.js.map +1 -1
  44. package/dist/workflows/types.d.ts +77 -2
  45. package/docs/CONTROLLERS.md +1 -1
  46. package/docs/DESIGN_PHILOSOPHY.md +1 -1
  47. package/docs/MONITOR.md +35 -18
  48. package/docs/WORKFLOW_COMPOSITION.md +326 -0
  49. package/docs/plans/2026-08-19-workflow-composition-plan.md +300 -0
  50. package/docs/run-bundles.md +24 -10
  51. package/docs/workflows.md +65 -12
  52. package/examples/workflows/autodevise.workflow.ts +1 -0
  53. package/examples/workflows/autoimplement.workflow.ts +1 -92
  54. package/herdr-plugin.toml +1 -1
  55. package/package.json +5 -1
  56. package/skills/monitor/SKILL.md +6 -1
  57. package/skills/pi-workflows/SKILL.md +3 -1
  58. package/src/builtins/autodevise.workflow.ts +231 -0
  59. package/src/builtins/autoimplement.workflow.ts +856 -0
  60. package/src/builtins/catalog.ts +5 -1
  61. package/src/builtins/index.ts +13 -0
  62. package/src/builtins/monitor.workflow.ts +242 -15
  63. package/src/render/graph-render.ts +14 -2
  64. package/src/workflows/catalog.ts +7 -0
  65. package/src/workflows/composition.ts +627 -0
  66. package/src/workflows/decision.ts +12 -5
  67. package/src/workflows/definition.ts +118 -8
  68. package/src/workflows/engine.ts +151 -18
  69. package/src/workflows/graph.ts +8 -6
  70. package/src/workflows/index.ts +20 -0
  71. package/src/workflows/loader.ts +186 -18
  72. package/src/workflows/schema.ts +62 -2
  73. package/src/workflows/store.ts +37 -2
  74. package/src/workflows/types.ts +109 -2
  75. package/examples/workflows/elegant-solution.workflow.ts +0 -95
@@ -1,10 +1,14 @@
1
1
  import { BuiltinWorkflowCatalog } from "../workflows/catalog.js";
2
+ import autodeviseWorkflow from "./autodevise.workflow.js";
3
+ import autoimplementWorkflow from "./autoimplement.workflow.js";
2
4
  import monitorWorkflow from "./monitor.workflow.js";
3
5
 
4
6
  export const builtinWorkflowCatalog = new BuiltinWorkflowCatalog([
7
+ { id: "autodevise", revision: "1", definition: autodeviseWorkflow },
8
+ { id: "autoimplement", revision: "1", definition: autoimplementWorkflow },
5
9
  {
6
10
  id: "monitor",
7
- revision: "3",
11
+ revision: "4",
8
12
  definition: monitorWorkflow,
9
13
  legacySources: [
10
14
  {
@@ -0,0 +1,13 @@
1
+ export {
2
+ autodeviseWorkflow,
3
+ type AutodeviseBlocked,
4
+ type AutodeviseInput,
5
+ type AutodeviseReady,
6
+ } from "./autodevise.workflow.js";
7
+ export {
8
+ autoimplementWorkflow,
9
+ type AutoimplementBlocked,
10
+ type AutoimplementCompleted,
11
+ type AutoimplementInput,
12
+ } from "./autoimplement.workflow.js";
13
+ export { default as monitorWorkflow, type MonitorInput } from "./monitor.workflow.js";
@@ -1,4 +1,13 @@
1
- import { action, agent, compute, defineWorkflow, notify, shell } from "../workflows/definition.js";
1
+ import {
2
+ action,
3
+ agent,
4
+ compute,
5
+ defineWorkflow,
6
+ includeWorkflow,
7
+ includedResult,
8
+ notify,
9
+ shell,
10
+ } from "../workflows/definition.js";
2
11
  import {
3
12
  estimateProgress,
4
13
  formatProgressReport,
@@ -11,6 +20,8 @@ import type {
11
20
  WorkflowProgressData,
12
21
  } from "../workflows/types.js";
13
22
  import { validateProgressData } from "../workflows/updates.js";
23
+ import autodeviseWorkflow from "./autodevise.workflow.js";
24
+ import autoimplementWorkflow, { type AutoimplementInput } from "./autoimplement.workflow.js";
14
25
 
15
26
  const MIN_INTERVAL_MINUTES = 1;
16
27
  const MAX_INTERVAL_MINUTES = 24 * 60;
@@ -27,22 +38,45 @@ const MAX_REASON_CHARS = 2_000;
27
38
  const SLEEP_TIMEOUT_MARGIN_MS = 60_000;
28
39
  const NODE_TIMEOUT_MARGIN_MS = 2 * 60_000;
29
40
 
41
+ export type MonitorRepairPolicy = {
42
+ authorized: true;
43
+ scope?: string;
44
+ constraints?: string[];
45
+ repository?: string;
46
+ baseBranch?: string;
47
+ merge?: boolean;
48
+ };
49
+
30
50
  export type MonitorInput = {
31
51
  task: string;
32
52
  everyMinutes?: number;
33
53
  stopWhen?: string;
34
54
  maxChecks?: number;
35
55
  checkTimeoutMinutes?: number;
56
+ repair?: MonitorRepairPolicy;
36
57
  };
37
58
 
38
- type MonitorConfig = Required<MonitorInput>;
39
- type MonitorRoute = "continue" | "stop";
59
+ type MonitorConfig = {
60
+ task: string;
61
+ everyMinutes: number;
62
+ stopWhen: string;
63
+ maxChecks: number;
64
+ checkTimeoutMinutes: number;
65
+ repair?: MonitorRepairPolicy;
66
+ };
67
+ type MonitorRoute = "continue" | "repair" | "stop";
40
68
  type MonitorTrack = { key: string; data: WorkflowProgressData };
69
+ type MonitorRepairRequest = {
70
+ problem: string;
71
+ evidence: unknown;
72
+ issueFingerprint: string;
73
+ };
41
74
  type MonitorCheck = {
42
75
  route: MonitorRoute;
43
76
  observation: string;
44
77
  report: string;
45
78
  progress?: { tracks: MonitorTrack[] };
79
+ repair?: MonitorRepairRequest;
46
80
  reason: string;
47
81
  };
48
82
  type MonitorEstimate = { tracks: ProgressTrackState[] };
@@ -79,7 +113,14 @@ async function waitForUpdateSlot(signal: AbortSignal): Promise<void> {
79
113
 
80
114
  export function prepareMonitorInput(input: unknown): MonitorConfig {
81
115
  const value = requireRecord(input, "monitor input") as Partial<MonitorInput>;
82
- const allowed = new Set(["task", "everyMinutes", "stopWhen", "maxChecks", "checkTimeoutMinutes"]);
116
+ const allowed = new Set([
117
+ "task",
118
+ "everyMinutes",
119
+ "stopWhen",
120
+ "maxChecks",
121
+ "checkTimeoutMinutes",
122
+ "repair",
123
+ ]);
83
124
  for (const field of Object.keys(value)) {
84
125
  if (!allowed.has(field)) throw new Error(`monitor input field ${field} is not supported`);
85
126
  }
@@ -109,6 +150,34 @@ export function prepareMonitorInput(input: unknown): MonitorConfig {
109
150
  `checkTimeoutMinutes must be an integer from ${MIN_CHECK_TIMEOUT_MINUTES} through ${MAX_CHECK_TIMEOUT_MINUTES}`,
110
151
  );
111
152
  }
153
+ let repair: MonitorRepairPolicy | undefined;
154
+ if (value.repair !== undefined) {
155
+ const raw = requireRecord(value.repair, "repair policy");
156
+ if (raw.authorized !== true) throw new Error("repair policy must set authorized to true");
157
+ if (
158
+ raw.constraints !== undefined &&
159
+ (!Array.isArray(raw.constraints) || raw.constraints.some((item) => typeof item !== "string"))
160
+ ) {
161
+ throw new Error("repair constraints must be an array of strings");
162
+ }
163
+ if (raw.merge !== undefined && typeof raw.merge !== "boolean") {
164
+ throw new Error("repair merge must be a boolean");
165
+ }
166
+ repair = {
167
+ authorized: true,
168
+ ...(raw.scope !== undefined
169
+ ? { scope: requireBoundedString(raw.scope, "repair scope", 4_000) }
170
+ : {}),
171
+ ...(raw.constraints !== undefined ? { constraints: [...raw.constraints] as string[] } : {}),
172
+ ...(raw.repository !== undefined
173
+ ? { repository: requireBoundedString(raw.repository, "repair repository", 4_000) }
174
+ : {}),
175
+ ...(raw.baseBranch !== undefined
176
+ ? { baseBranch: requireBoundedString(raw.baseBranch, "repair base branch", 256) }
177
+ : {}),
178
+ ...(raw.merge !== undefined ? { merge: raw.merge !== false } : {}),
179
+ };
180
+ }
112
181
  return {
113
182
  task,
114
183
  everyMinutes,
@@ -118,6 +187,7 @@ export function prepareMonitorInput(input: unknown): MonitorConfig {
118
187
  : requireBoundedString(value.stopWhen, "stopWhen", 4_000),
119
188
  maxChecks,
120
189
  checkTimeoutMinutes,
190
+ ...(repair !== undefined ? { repair } : {}),
121
191
  };
122
192
  }
123
193
 
@@ -130,13 +200,17 @@ function completedChecks(context: WorkflowNodeContext): number {
130
200
  .length;
131
201
  }
132
202
 
133
- export function validateMonitorCheck(output: unknown): MonitorCheck {
203
+ export function validateMonitorCheck(output: unknown, repairAuthorized = false): MonitorCheck {
134
204
  const value = requireRecord(output, "monitor check output");
135
- const allowed = new Set(["route", "observation", "report", "progress", "reason"]);
205
+ const allowed = new Set(["route", "observation", "report", "progress", "repair", "reason"]);
136
206
  for (const key of Object.keys(value))
137
207
  if (!allowed.has(key)) throw new Error(`monitor check field ${key} is not supported`);
138
- if (value.route !== "continue" && value.route !== "stop")
139
- throw new Error("route must be continue or stop");
208
+ if (value.route !== "continue" && value.route !== "repair" && value.route !== "stop") {
209
+ throw new Error("route must be continue, repair, or stop");
210
+ }
211
+ if (value.route === "repair" && !repairAuthorized) {
212
+ throw new Error("route repair requires explicit monitor repair authorization");
213
+ }
140
214
  const check: MonitorCheck = {
141
215
  route: value.route,
142
216
  observation: requireBoundedString(value.observation, "observation", MAX_OBSERVATION_CHARS),
@@ -144,6 +218,21 @@ export function validateMonitorCheck(output: unknown): MonitorCheck {
144
218
  reason: requireBoundedString(value.reason, "reason", MAX_REASON_CHARS),
145
219
  };
146
220
  if (value.progress !== undefined) check.progress = validateMonitorProgress(value.progress);
221
+ if (value.repair !== undefined) {
222
+ const repair = requireRecord(value.repair, "monitor repair request");
223
+ check.repair = {
224
+ problem: requireBoundedString(repair.problem, "repair problem", 8_000),
225
+ evidence: repair.evidence ?? null,
226
+ issueFingerprint: requireBoundedString(
227
+ repair.issueFingerprint,
228
+ "repair issue fingerprint",
229
+ 256,
230
+ ),
231
+ };
232
+ }
233
+ if (value.route === "repair" && check.repair === undefined) {
234
+ throw new Error("route repair requires repair details");
235
+ }
147
236
  return check;
148
237
  }
149
238
 
@@ -190,6 +279,40 @@ function estimateTracks(outputs: Record<string, unknown>): MonitorEstimate {
190
279
  };
191
280
  }
192
281
 
282
+ function repeatedRepairWithoutProgress(context: WorkflowNodeContext): boolean {
283
+ const current = context.outputs.check as MonitorCheck;
284
+ const fingerprint = current.repair?.issueFingerprint;
285
+ if (fingerprint === undefined) return false;
286
+ const steps = context.state.steps;
287
+ const currentCheckIndex = steps.findLastIndex((step) => step.nodeId === "check");
288
+ for (let index = currentCheckIndex - 1; index >= 0; index -= 1) {
289
+ const step = steps[index];
290
+ if (step?.nodeId !== "check") continue;
291
+ const prior = step.output as MonitorCheck;
292
+ if (prior.repair?.issueFingerprint !== fingerprint) continue;
293
+ return steps
294
+ .slice(index + 1, currentCheckIndex)
295
+ .some((candidate) => candidate.nodeId === "implementation");
296
+ }
297
+ return false;
298
+ }
299
+
300
+ function repairBlockedReason(outputs: Record<string, unknown>): string {
301
+ const guard = outputs.repairGuard as { reason?: string; route?: string } | undefined;
302
+ if (guard?.route === "blocked" && guard.reason !== undefined) return guard.reason;
303
+ const implementation = outputs.implementation as
304
+ | { exit?: string; output?: { reason?: string } }
305
+ | undefined;
306
+ const design = outputs.initialDesign as
307
+ | { exit?: string; output?: { reason?: string } }
308
+ | undefined;
309
+ return (
310
+ implementation?.output?.reason ??
311
+ design?.output?.reason ??
312
+ "The repair did not produce new verified progress."
313
+ );
314
+ }
315
+
193
316
  function reportMessage(context: WorkflowNodeContext): string {
194
317
  const check = context.outputs.check as MonitorCheck;
195
318
  const estimate = context.outputs.estimate as MonitorEstimate;
@@ -219,7 +342,10 @@ function reportMessage(context: WorkflowNodeContext): string {
219
342
  }
220
343
 
221
344
  const monitorWorkflow: WorkflowDefinition = defineWorkflow({
345
+ source: import.meta.url,
346
+ contractId: "pi-workflows.monitor.v1",
222
347
  name: "monitor",
348
+ input: prepareMonitorInput,
223
349
  title: ({ input }) => {
224
350
  try {
225
351
  return `monitor: ${prepareMonitorInput(input).task.slice(0, 80)}`;
@@ -228,7 +354,53 @@ const monitorWorkflow: WorkflowDefinition = defineWorkflow({
228
354
  }
229
355
  },
230
356
  startAt: "prepare",
231
- maxSteps: 9_010,
357
+ maxSteps: 200_000,
358
+ includes: {
359
+ initialDesign: includeWorkflow({
360
+ workflow: "autodevise",
361
+ contract: autodeviseWorkflow,
362
+ input: ({ outputs }) => {
363
+ const config = configFrom(outputs);
364
+ const repair = (outputs.check as MonitorCheck).repair;
365
+ if (repair === undefined) throw new Error("monitor repair details are missing");
366
+ return {
367
+ problem: repair.problem,
368
+ ...(config.repair?.scope !== undefined ? { scope: config.repair.scope } : {}),
369
+ ...(config.repair?.constraints !== undefined
370
+ ? { constraints: config.repair.constraints }
371
+ : {}),
372
+ newEvidence: repair.evidence,
373
+ };
374
+ },
375
+ }),
376
+ implementation: includeWorkflow({
377
+ workflow: "autoimplement",
378
+ contract: autoimplementWorkflow,
379
+ input: ({ outputs }) => {
380
+ const config = configFrom(outputs);
381
+ const repair = (outputs.check as MonitorCheck).repair;
382
+ const design = includedResult(autodeviseWorkflow, outputs.initialDesign);
383
+ if (design.exit !== "ready") throw new Error("monitor design did not return a ready plan");
384
+ if (repair === undefined) throw new Error("monitor repair details are missing");
385
+ const request: AutoimplementInput = {
386
+ task: repair.problem,
387
+ plan: design.output.plan,
388
+ ...(config.repair?.scope !== undefined ? { scope: config.repair.scope } : {}),
389
+ ...(config.repair?.constraints !== undefined
390
+ ? { constraints: config.repair.constraints }
391
+ : {}),
392
+ ...(config.repair?.repository !== undefined
393
+ ? { repository: config.repair.repository }
394
+ : {}),
395
+ ...(config.repair?.baseBranch !== undefined
396
+ ? { baseBranch: config.repair.baseBranch }
397
+ : {}),
398
+ merge: config.repair?.merge === true,
399
+ };
400
+ return request;
401
+ },
402
+ }),
403
+ },
232
404
  nodes: {
233
405
  prepare: compute({ run: ({ input }) => prepareMonitorInput(input) }),
234
406
  check: agent({
@@ -248,15 +420,21 @@ const monitorWorkflow: WorkflowDefinition = defineWorkflow({
248
420
  priorEstimate?.tracks.length
249
421
  ? `Previous progress: ${formatProgressReport(priorEstimate.tracks.map((track) => track.estimate))}`
250
422
  : "There is no previous measured progress.",
251
- "Use available tools to inspect the current source of truth. Observe only unless the task explicitly authorizes a mutation.",
423
+ config.repair === undefined
424
+ ? "Observe only. This monitor has no mutation authorization."
425
+ : "Repair is explicitly authorized within the supplied repair policy. Choose repair only for a concrete issue that can be changed within that scope. Include a stable issue fingerprint based on the issue and observed target state. Do not change protected model, benchmark, credential, hardware, spending, or scope decisions.",
426
+ "Use available tools to inspect the current source of truth.",
252
427
  "You are the regular Pi model running this check and the observation adapter. When useful measurable facts appear during the check, publish them with workflow action update. Include the latest tracks in the final submission. Do not require the monitored target to implement a Pi-specific progress API, file, store, schema, or command.",
253
428
  "Every accepted check must include a concise user-facing report. Add progress tracks only when the target provides measurable facts. Submit observed counts and target-provided finish times; do not invent rates or an ETA.",
254
- "Choose route continue or stop.",
429
+ config.repair === undefined
430
+ ? "Choose route continue or stop."
431
+ : "Choose route continue, repair, or stop.",
255
432
  ].join("\n\n");
256
433
  },
257
434
  expectedOutput:
258
- '{ "route": "continue" | "stop", "observation": "current factual state", "report": "concise status update", "progress": { "tracks": [{ "key": "stable-key", "data": { "schema": "pi-workflows.progress.v1", "status": "running", "completed": 1, "total": 2, "unit": "items" } }] } (optional), "reason": "short reason" }',
259
- validate: (output) => validateMonitorCheck(output),
435
+ '{ "route": "continue" | "repair" | "stop", "observation": "current factual state", "report": "concise status update", "progress": { "tracks": [{ "key": "stable-key", "data": { "schema": "pi-workflows.progress.v1", "status": "running", "completed": 1, "total": 2, "unit": "items" } }] } (optional), "repair": { "problem": "fixable issue", "evidence": "observed evidence", "issueFingerprint": "stable issue and target-state fingerprint" } (required for repair), "reason": "short reason" }',
436
+ validate: (output, context) =>
437
+ validateMonitorCheck(output, configFrom(context.outputs).repair !== undefined),
260
438
  }),
261
439
  estimate: compute({ run: ({ outputs }) => estimateTracks(outputs) }),
262
440
  publish_progress: action({
@@ -289,9 +467,36 @@ const monitorWorkflow: WorkflowDefinition = defineWorkflow({
289
467
  checks,
290
468
  };
291
469
  }
470
+ if (check.route === "repair") return { route: "repair", reason: check.reason, checks };
292
471
  return { route: "continue", reason: check.reason, checks };
293
472
  },
294
473
  }),
474
+ repairGuard: compute({
475
+ run: (context) =>
476
+ repeatedRepairWithoutProgress(context)
477
+ ? {
478
+ route: "blocked",
479
+ reason:
480
+ "The same issue returned after a completed repair with no changed target evidence.",
481
+ }
482
+ : { route: "repair", reason: "The issue is new or has changed evidence." },
483
+ }),
484
+ repairBlocked: compute({
485
+ run: (context) => ({
486
+ reason: repairBlockedReason(context.outputs),
487
+ observation: (context.outputs.check as MonitorCheck).observation,
488
+ checks: completedChecks(context),
489
+ reported: true,
490
+ }),
491
+ }),
492
+ repairReport: notify({
493
+ statusDetail: "reporting blocked monitor repair",
494
+ kind: "final",
495
+ message: ({ outputs }) => {
496
+ const result = outputs.repairBlocked as { reason: string };
497
+ return `Automatic repair stopped: ${result.reason}`;
498
+ },
499
+ }),
295
500
  schedule: action({
296
501
  statusDetail: "scheduling next monitor check",
297
502
  run: async ({ outputs, publishUpdate }) => {
@@ -331,11 +536,17 @@ const monitorWorkflow: WorkflowDefinition = defineWorkflow({
331
536
  run: ({ outputs }) => {
332
537
  const check = outputs.check as MonitorCheck;
333
538
  const decision = outputs.decide as { reason?: string; checks?: number } | undefined;
539
+ const repair = outputs.repairBlocked as { reason?: string } | undefined;
334
540
  return {
335
- reason: decision?.reason ?? check.reason,
541
+ reason: repair?.reason ?? decision?.reason ?? check.reason,
336
542
  observation: check.observation,
337
543
  checks: decision?.checks ?? 1,
338
544
  reported: true,
545
+ ...(outputs.implementation !== undefined
546
+ ? { repair: outputs.implementation }
547
+ : repair !== undefined
548
+ ? { repair }
549
+ : {}),
339
550
  };
340
551
  },
341
552
  }),
@@ -346,7 +557,23 @@ const monitorWorkflow: WorkflowDefinition = defineWorkflow({
346
557
  { from: "estimate", to: "publish_progress" },
347
558
  { from: "publish_progress", to: "report" },
348
559
  { from: "report", to: "decide" },
349
- { from: "decide", switch: { on: "$.route", cases: { stop: "finish", continue: "schedule" } } },
560
+ {
561
+ from: "decide",
562
+ switch: {
563
+ on: "$.route",
564
+ cases: { stop: "finish", continue: "schedule", repair: "repairGuard" },
565
+ },
566
+ },
567
+ {
568
+ from: "repairGuard",
569
+ switch: { on: "$.route", cases: { repair: "initialDesign", blocked: "repairBlocked" } },
570
+ },
571
+ { from: "initialDesign.ready", to: "implementation" },
572
+ { from: "initialDesign.blocked", to: "repairBlocked" },
573
+ { from: "implementation.completed", to: "check" },
574
+ { from: "implementation.blocked", to: "repairBlocked" },
575
+ { from: "repairBlocked", to: "repairReport" },
576
+ { from: "repairReport", to: "finish" },
350
577
  { from: "schedule", to: "sleep" },
351
578
  { from: "sleep", to: "check" },
352
579
  ],
@@ -319,11 +319,12 @@ function renderCellText(
319
319
  const count = atLatestStep && state.currentNode === nodeId ? Math.max(attempts, 1) : attempts;
320
320
  const timing =
321
321
  attempt || count > 0 ? `${count} attempt${count === 1 ? "" : "s"} · ${elapsed}` : "not visited";
322
- const text = `${nodeId} [${nodeType}] ${timing}`;
322
+ const displayNodeId = hierarchicalNodeLabel(nodeId, node);
323
+ const text = `${displayNodeId} [${nodeType}] ${timing}`;
323
324
  return {
324
325
  cell,
325
326
  text,
326
- nodeId: sanitizeText(nodeId),
327
+ nodeId: sanitizeText(displayNodeId),
327
328
  nodeType,
328
329
  typeBadge: node ? nodeTypeBadge(node.nodeType, node.actionExecution) : "? unknown",
329
330
  status,
@@ -337,6 +338,17 @@ function renderCellText(
337
338
  };
338
339
  }
339
340
 
341
+ function hierarchicalNodeLabel(
342
+ nodeId: string,
343
+ node: WorkflowDefinitionSnapshot["nodes"][string] | undefined,
344
+ ): string {
345
+ if (node?.mountPath === undefined || node.localNodeId === undefined) return nodeId;
346
+ const path = node.mountPath.join(" › ");
347
+ if (node.includeTransition === "entry") return `${path} · enter`;
348
+ if (node.includeTransition === "exit") return `${path} · ${node.localNodeId} exit`;
349
+ return `${path} › ${node.localNodeId}`;
350
+ }
351
+
340
352
  type RankGeometry = {
341
353
  cells: RenderedCell[];
342
354
  centers: number[];
@@ -83,6 +83,13 @@ export class BuiltinWorkflowCatalog {
83
83
  return this.byName.get(name);
84
84
  }
85
85
 
86
+ sourceForDefinition(definition: WorkflowDefinition): WorkflowSource | undefined {
87
+ const entry = this.list().find((candidate) => candidate.definition === definition);
88
+ return entry === undefined
89
+ ? undefined
90
+ : { kind: "builtin", id: entry.id, revision: entry.revision };
91
+ }
92
+
86
93
  resolve(
87
94
  source: WorkflowSource,
88
95
  runId = `builtin:${source.kind === "builtin" ? source.id : "unknown"}`,