@osolmaz/pi-workflows 0.9.0 → 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.
- package/README.md +50 -16
- package/dist/builtins/autodevise.workflow.d.ts +58 -0
- package/dist/builtins/autodevise.workflow.js +190 -0
- package/dist/builtins/autodevise.workflow.js.map +1 -0
- package/dist/builtins/autoimplement.workflow.d.ts +154 -0
- package/dist/builtins/autoimplement.workflow.js +729 -0
- package/dist/builtins/autoimplement.workflow.js.map +1 -0
- package/dist/builtins/catalog.js +5 -1
- package/dist/builtins/catalog.js.map +1 -1
- package/dist/builtins/index.d.ts +3 -0
- package/dist/builtins/index.js +4 -0
- package/dist/builtins/index.js.map +1 -0
- package/dist/builtins/monitor.workflow.d.ts +25 -3
- package/dist/builtins/monitor.workflow.js +200 -13
- package/dist/builtins/monitor.workflow.js.map +1 -1
- package/dist/extension/herdr-viewer.js +2 -6
- package/dist/extension/herdr-viewer.js.map +1 -1
- package/dist/render/graph-render.js +13 -2
- package/dist/render/graph-render.js.map +1 -1
- package/dist/workflows/catalog.d.ts +1 -0
- package/dist/workflows/catalog.js +6 -0
- package/dist/workflows/catalog.js.map +1 -1
- package/dist/workflows/composition.d.ts +45 -0
- package/dist/workflows/composition.js +471 -0
- package/dist/workflows/composition.js.map +1 -0
- package/dist/workflows/decision.d.ts +11 -5
- package/dist/workflows/decision.js.map +1 -1
- package/dist/workflows/definition.d.ts +22 -3
- package/dist/workflows/definition.js +46 -3
- package/dist/workflows/definition.js.map +1 -1
- package/dist/workflows/engine.js +115 -16
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/graph.js +8 -6
- package/dist/workflows/graph.js.map +1 -1
- package/dist/workflows/index.d.ts +3 -2
- package/dist/workflows/index.js +2 -1
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/loader.d.ts +5 -4
- package/dist/workflows/loader.js +118 -18
- package/dist/workflows/loader.js.map +1 -1
- package/dist/workflows/schema.d.ts +3 -1
- package/dist/workflows/schema.js +49 -2
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/store.js +32 -2
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +77 -2
- package/docs/CONTROLLERS.md +1 -1
- package/docs/DESIGN_PHILOSOPHY.md +1 -1
- package/docs/MONITOR.md +35 -18
- package/docs/WORKFLOW_COMPOSITION.md +326 -0
- package/docs/plans/2026-08-19-workflow-composition-plan.md +300 -0
- package/docs/run-bundles.md +24 -10
- package/docs/workflows.md +65 -12
- package/examples/workflows/autodevise.workflow.ts +1 -0
- package/examples/workflows/autoimplement.workflow.ts +1 -92
- package/herdr-plugin.toml +1 -1
- package/package.json +5 -1
- package/skills/monitor/SKILL.md +6 -1
- package/skills/pi-workflows/SKILL.md +3 -1
- package/src/builtins/autodevise.workflow.ts +231 -0
- package/src/builtins/autoimplement.workflow.ts +856 -0
- package/src/builtins/catalog.ts +5 -1
- package/src/builtins/index.ts +13 -0
- package/src/builtins/monitor.workflow.ts +242 -15
- package/src/extension/herdr-viewer.ts +1 -6
- package/src/render/graph-render.ts +14 -2
- package/src/workflows/catalog.ts +7 -0
- package/src/workflows/composition.ts +627 -0
- package/src/workflows/decision.ts +12 -5
- package/src/workflows/definition.ts +118 -8
- package/src/workflows/engine.ts +151 -18
- package/src/workflows/graph.ts +8 -6
- package/src/workflows/index.ts +20 -0
- package/src/workflows/loader.ts +186 -18
- package/src/workflows/schema.ts +62 -2
- package/src/workflows/store.ts +37 -2
- package/src/workflows/types.ts +109 -2
- package/examples/workflows/elegant-solution.workflow.ts +0 -95
package/src/builtins/catalog.ts
CHANGED
|
@@ -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: "
|
|
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 {
|
|
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 =
|
|
39
|
-
|
|
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([
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
{
|
|
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
|
],
|
|
@@ -111,7 +111,7 @@ export class HerdrWorkflowViewer {
|
|
|
111
111
|
return { ...opened, reused: false };
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
const opened = await this.openPluginPane(target, placement, caller
|
|
114
|
+
const opened = await this.openPluginPane(target, placement, caller);
|
|
115
115
|
if (placement === "left" || placement === "above") {
|
|
116
116
|
try {
|
|
117
117
|
await this.run("herdr", [
|
|
@@ -167,7 +167,6 @@ export class HerdrWorkflowViewer {
|
|
|
167
167
|
target: WorkflowViewTarget,
|
|
168
168
|
placement: Exclude<ViewerPlacement, "workspace">,
|
|
169
169
|
caller: HerdrPane,
|
|
170
|
-
cwd: string,
|
|
171
170
|
): Promise<OpenedPane> {
|
|
172
171
|
const args = [
|
|
173
172
|
"plugin",
|
|
@@ -177,8 +176,6 @@ export class HerdrWorkflowViewer {
|
|
|
177
176
|
HERDR_PLUGIN_ID,
|
|
178
177
|
"--entrypoint",
|
|
179
178
|
HERDR_PLUGIN_ENTRYPOINT,
|
|
180
|
-
"--cwd",
|
|
181
|
-
cwd,
|
|
182
179
|
"--env",
|
|
183
180
|
`PI_WORKFLOWS_RUN_ID=${target.runId}`,
|
|
184
181
|
"--env",
|
|
@@ -230,8 +227,6 @@ export class HerdrWorkflowViewer {
|
|
|
230
227
|
"tab",
|
|
231
228
|
"--workspace",
|
|
232
229
|
created.workspaceId,
|
|
233
|
-
"--cwd",
|
|
234
|
-
cwd,
|
|
235
230
|
"--env",
|
|
236
231
|
`PI_WORKFLOWS_RUN_ID=${target.runId}`,
|
|
237
232
|
"--env",
|
|
@@ -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
|
|
322
|
+
const displayNodeId = hierarchicalNodeLabel(nodeId, node);
|
|
323
|
+
const text = `${displayNodeId} [${nodeType}] ${timing}`;
|
|
323
324
|
return {
|
|
324
325
|
cell,
|
|
325
326
|
text,
|
|
326
|
-
nodeId: sanitizeText(
|
|
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[];
|
package/src/workflows/catalog.ts
CHANGED
|
@@ -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"}`,
|