@wichayutdew/pi-workflows 2.1.0 → 2.3.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 +31 -978
- package/dist/index.js +995 -756
- package/examples/starter-kit/mr-review.workflow.yaml +3 -1
- package/examples/starter-kit/steps/mr-review/publish.md +9 -2
- package/examples/starter-kit/steps/mr-review/verify.md +16 -5
- package/package.json +1 -1
- package/schemas/workflow.schema.json +1 -0
- package/src/command-names.ts +1 -0
- package/src/commands.ts +14 -0
- package/src/config/validation/prompt.ts +2 -0
- package/src/engine/create-run.ts +3 -0
- package/src/engine/run-advance.ts +16 -0
- package/src/engine/run-lifecycle.ts +69 -0
- package/src/engine/run-validation.ts +16 -0
- package/src/engine/state-types.ts +7 -0
- package/src/engine/transitions.ts +1 -0
- package/src/harness/action-context.ts +6 -0
- package/src/harness/core-actions.ts +2 -0
- package/src/harness/dependencies.ts +4 -0
- package/src/harness/lifecycle-actions.ts +14 -1
- package/src/harness/session-persistence.ts +66 -0
- package/src/harness/start-actions.ts +188 -1
- package/src/harness.ts +20 -0
- package/src/prompt/step-sections.ts +14 -0
- package/src/prompt/step-task.ts +3 -0
- package/src/prompt/template.ts +2 -0
- package/src/workflow-status/render-summary.ts +3 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
import { createRun } from '../engine/state.ts';
|
|
3
|
+
import { restartRun } from '../engine/transitions.ts';
|
|
4
|
+
import type { LoadedWorkflow } from '../config/types.ts';
|
|
3
5
|
import { analyzeWorkflow, formatWorkflowDoctor } from '../workflow-doctor.ts';
|
|
4
6
|
import { formatWorkflowList } from '../workflow-list.ts';
|
|
5
7
|
import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
|
|
@@ -36,6 +38,32 @@ function isCurrentSession(
|
|
|
36
38
|
return session.isSessionActive && session.sessionEpoch === sessionEpoch;
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
type RestartWorkspaceBinding = {
|
|
42
|
+
readonly cwd: string;
|
|
43
|
+
readonly allowedRoots: ReadonlyArray<string>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function completedWorkspaceBinding(
|
|
47
|
+
run: NonNullable<HarnessActionContext['run']>,
|
|
48
|
+
workflow: LoadedWorkflow,
|
|
49
|
+
): RestartWorkspaceBinding | undefined {
|
|
50
|
+
for (let index = run.history.length - 1; index >= 0; index -= 1) {
|
|
51
|
+
const entry = run.history[index];
|
|
52
|
+
if (!entry?.workspaceCwd) continue;
|
|
53
|
+
const step = workflow.definition.steps[entry.stepId];
|
|
54
|
+
if (!step?.workspace || !step.workspace.bindOn.includes(entry.outcome)) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`workspace-binding step "${entry.stepId}" no longer matches the completed iteration`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
cwd: entry.workspaceCwd,
|
|
61
|
+
allowedRoots: step.workspace.allowedRoots,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
39
67
|
export type StartActions = {
|
|
40
68
|
listWorkflows: (
|
|
41
69
|
this: HarnessActionContext,
|
|
@@ -53,6 +81,12 @@ export type StartActions = {
|
|
|
53
81
|
startContext: WorkflowStartContext,
|
|
54
82
|
sessionEpoch: number,
|
|
55
83
|
) => Promise<void>;
|
|
84
|
+
restartNow: (
|
|
85
|
+
this: HarnessActionContext,
|
|
86
|
+
input: string,
|
|
87
|
+
startContext: WorkflowStartContext,
|
|
88
|
+
sessionEpoch: number,
|
|
89
|
+
) => Promise<void>;
|
|
56
90
|
reloadNow: (
|
|
57
91
|
this: HarnessActionContext,
|
|
58
92
|
context: ExtensionCommandContext,
|
|
@@ -231,6 +265,159 @@ async function startNow(
|
|
|
231
265
|
this.launchCurrentStep(workflow);
|
|
232
266
|
}
|
|
233
267
|
|
|
268
|
+
async function restartNow(
|
|
269
|
+
this: HarnessActionContext,
|
|
270
|
+
input: string,
|
|
271
|
+
startContext: WorkflowStartContext,
|
|
272
|
+
sessionEpoch: number,
|
|
273
|
+
): Promise<void> {
|
|
274
|
+
const { context } = startContext;
|
|
275
|
+
const completedRun = this.run;
|
|
276
|
+
if (!completedRun || completedRun.status !== 'completed') {
|
|
277
|
+
context.ui.notify('Only a completed workflow can be restarted', 'warning');
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (this.activeDelegation) {
|
|
281
|
+
context.ui.notify(
|
|
282
|
+
`Cannot restart while subagent "${this.activeDelegation.agent}" is still cancelling`,
|
|
283
|
+
'warning',
|
|
284
|
+
);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (!completedRun.startCwd) {
|
|
288
|
+
context.ui.notify(
|
|
289
|
+
'Cannot restart this workflow on the same worktree because its original start directory was not captured; start a new workflow instead',
|
|
290
|
+
'error',
|
|
291
|
+
);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (!context.isIdle()) {
|
|
295
|
+
context.abort();
|
|
296
|
+
await startContext.waitForIdle();
|
|
297
|
+
}
|
|
298
|
+
if (!isCurrentSession(this, sessionEpoch) || this.run !== completedRun) {
|
|
299
|
+
context.ui.notify(
|
|
300
|
+
'Workflow restart was superseded by a session or workflow change',
|
|
301
|
+
'warning',
|
|
302
|
+
);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
this.captureSkills(startContext.skills());
|
|
307
|
+
if (!(await this.reloadCatalog(context, false))) {
|
|
308
|
+
context.ui.notify(
|
|
309
|
+
'Workflow restart was superseded by a newer configuration load',
|
|
310
|
+
'warning',
|
|
311
|
+
);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (!isCurrentSession(this, sessionEpoch) || this.run !== completedRun) {
|
|
315
|
+
context.ui.notify(
|
|
316
|
+
'Workflow restart was superseded by a session or workflow change',
|
|
317
|
+
'warning',
|
|
318
|
+
);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const workflow = this.catalog.workflows.get(completedRun.workflowId);
|
|
323
|
+
if (!workflow) {
|
|
324
|
+
context.ui.notify(
|
|
325
|
+
`Workflow "${completedRun.workflowId}" is no longer loaded`,
|
|
326
|
+
'error',
|
|
327
|
+
);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const livenessErrors = analyzeWorkflow(workflow.definition).issues.filter(
|
|
331
|
+
(issue) => issue.level === 'error',
|
|
332
|
+
);
|
|
333
|
+
if (livenessErrors.length > 0) {
|
|
334
|
+
context.ui.notify(
|
|
335
|
+
`Cannot restart workflow; run /workflow-doctor ${workflow.definition.id}:\n${livenessErrors.map((issue) => issue.message).join('\n')}`,
|
|
336
|
+
'error',
|
|
337
|
+
);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const preflightErrors = this.preflight(workflow, workflow.definition.start);
|
|
341
|
+
if (preflightErrors.length > 0) {
|
|
342
|
+
context.ui.notify(
|
|
343
|
+
`Cannot restart workflow:\n${preflightErrors.join('\n')}`,
|
|
344
|
+
'error',
|
|
345
|
+
);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
let canonicalStartCwd: string;
|
|
350
|
+
let canonicalSessionCwd: string;
|
|
351
|
+
try {
|
|
352
|
+
canonicalStartCwd = this.dependencies.resolveWorkspaceDirectory({
|
|
353
|
+
candidateCwd: completedRun.startCwd,
|
|
354
|
+
startCwd: completedRun.startCwd,
|
|
355
|
+
allowedRoots: ['.'],
|
|
356
|
+
});
|
|
357
|
+
canonicalSessionCwd = this.dependencies.resolveWorkspaceDirectory({
|
|
358
|
+
candidateCwd: context.cwd,
|
|
359
|
+
startCwd: context.cwd,
|
|
360
|
+
allowedRoots: ['.'],
|
|
361
|
+
});
|
|
362
|
+
} catch (error) {
|
|
363
|
+
context.ui.notify(
|
|
364
|
+
`Cannot restart workflow on its captured worktree: ${
|
|
365
|
+
error instanceof Error ? error.message : String(error)
|
|
366
|
+
}`,
|
|
367
|
+
'error',
|
|
368
|
+
);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (
|
|
372
|
+
canonicalStartCwd !== completedRun.startCwd ||
|
|
373
|
+
canonicalSessionCwd !== canonicalStartCwd
|
|
374
|
+
) {
|
|
375
|
+
context.ui.notify(
|
|
376
|
+
'Current session cwd does not match the captured workflow start directory',
|
|
377
|
+
'error',
|
|
378
|
+
);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
const binding = completedWorkspaceBinding(completedRun, workflow);
|
|
384
|
+
if (binding) {
|
|
385
|
+
const canonicalWorkspaceCwd = this.dependencies.resolveWorkspaceDirectory(
|
|
386
|
+
{
|
|
387
|
+
candidateCwd: binding.cwd,
|
|
388
|
+
startCwd: canonicalStartCwd,
|
|
389
|
+
allowedRoots: binding.allowedRoots,
|
|
390
|
+
},
|
|
391
|
+
);
|
|
392
|
+
if (canonicalWorkspaceCwd !== binding.cwd) {
|
|
393
|
+
throw new Error(
|
|
394
|
+
'previous workspace no longer resolves to its captured canonical directory',
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
this.run = restartRun(
|
|
399
|
+
workflow,
|
|
400
|
+
completedRun,
|
|
401
|
+
input.trim() || completedRun.input,
|
|
402
|
+
this.pi.getActiveTools(),
|
|
403
|
+
this.dependencies.now(),
|
|
404
|
+
);
|
|
405
|
+
} catch (error) {
|
|
406
|
+
context.ui.notify(
|
|
407
|
+
`Cannot restart workflow on the same worktree: ${
|
|
408
|
+
error instanceof Error ? error.message : String(error)
|
|
409
|
+
}`,
|
|
410
|
+
'error',
|
|
411
|
+
);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
this.persist();
|
|
416
|
+
this.isolateMainSessionTools();
|
|
417
|
+
this.updateStatus();
|
|
418
|
+
this.launchCurrentStep(workflow);
|
|
419
|
+
}
|
|
420
|
+
|
|
234
421
|
async function reloadNow(
|
|
235
422
|
this: HarnessActionContext,
|
|
236
423
|
context: ExtensionCommandContext,
|
|
@@ -253,5 +440,5 @@ async function reloadNow(
|
|
|
253
440
|
* Returns workflow listing, start, and reload actions for harness composition.
|
|
254
441
|
*/
|
|
255
442
|
export function createStartActions(): StartActions {
|
|
256
|
-
return { listWorkflows, doctorWorkflows, startNow, reloadNow };
|
|
443
|
+
return { listWorkflows, doctorWorkflows, startNow, restartNow, reloadNow };
|
|
257
444
|
}
|
package/src/harness.ts
CHANGED
|
@@ -123,6 +123,11 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
123
123
|
startContext: WorkflowStartContext,
|
|
124
124
|
sessionEpoch: number,
|
|
125
125
|
) => Promise<void> = START_ACTIONS.startNow;
|
|
126
|
+
private readonly restartNow: (
|
|
127
|
+
input: string,
|
|
128
|
+
startContext: WorkflowStartContext,
|
|
129
|
+
sessionEpoch: number,
|
|
130
|
+
) => Promise<void> = START_ACTIONS.restartNow;
|
|
126
131
|
private readonly reloadNow: (
|
|
127
132
|
context: ExtensionCommandContext,
|
|
128
133
|
) => Promise<void> = START_ACTIONS.reloadNow;
|
|
@@ -345,6 +350,21 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
345
350
|
);
|
|
346
351
|
}
|
|
347
352
|
|
|
353
|
+
/** Starts another completed iteration in its existing workflow worktree. */
|
|
354
|
+
restart(input: string, context: ExtensionCommandContext): Promise<void> {
|
|
355
|
+
return this.enqueueMutation(context, (sessionEpoch) =>
|
|
356
|
+
this.restartNow(
|
|
357
|
+
input,
|
|
358
|
+
{
|
|
359
|
+
context,
|
|
360
|
+
skills: () => context.getSystemPromptOptions().skills,
|
|
361
|
+
waitForIdle: () => context.waitForIdle(),
|
|
362
|
+
},
|
|
363
|
+
sessionEpoch,
|
|
364
|
+
),
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
348
368
|
/** Pauses the active workflow while retaining its checkpoint. */
|
|
349
369
|
pause(reason: string, context: ExtensionCommandContext): Promise<void> {
|
|
350
370
|
return this.enqueueMutation(context, () => this.pauseNow(reason, context));
|
|
@@ -58,6 +58,20 @@ export function buildDelegatedHandoffSection(
|
|
|
58
58
|
];
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/** Builds the immutable same-worktree constraint for a restarted iteration. */
|
|
62
|
+
export function buildRestartWorkspaceSection(
|
|
63
|
+
workspaceCwd: string | undefined,
|
|
64
|
+
): ReadonlyArray<string> {
|
|
65
|
+
if (!workspaceCwd) return [];
|
|
66
|
+
return [
|
|
67
|
+
'## Restart workspace constraint',
|
|
68
|
+
'',
|
|
69
|
+
`This iteration must reuse and rebind exactly this existing workspace: ${workspaceCwd}`,
|
|
70
|
+
'Do not create or substitute another workspace. If it cannot be safely reused, complete with a configured non-binding outcome that pauses the workflow.',
|
|
71
|
+
'',
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
|
|
61
75
|
/**
|
|
62
76
|
* Builds non-interactive recovery guidance specific to delegated steps.
|
|
63
77
|
*
|
package/src/prompt/step-task.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { createStepContract } from './step-contract.ts';
|
|
|
4
4
|
import {
|
|
5
5
|
buildDelegatedCompletionInstructions,
|
|
6
6
|
buildDelegatedHandoffSection,
|
|
7
|
+
buildRestartWorkspaceSection,
|
|
7
8
|
buildResourceSection,
|
|
8
9
|
} from './step-sections.ts';
|
|
9
10
|
import {
|
|
@@ -119,6 +120,7 @@ export function buildStepTask(options: BuildStepTaskOptions): string {
|
|
|
119
120
|
'',
|
|
120
121
|
`Workflow: ${workflow.definition.id}`,
|
|
121
122
|
`Run: ${run.runId}`,
|
|
123
|
+
`Iteration: ${run.iteration ?? 1}`,
|
|
122
124
|
`Step: ${run.currentStepId} (${step.title})`,
|
|
123
125
|
...(isDelegated
|
|
124
126
|
? [
|
|
@@ -132,6 +134,7 @@ export function buildStepTask(options: BuildStepTaskOptions): string {
|
|
|
132
134
|
prompt,
|
|
133
135
|
'',
|
|
134
136
|
...(isDelegated ? buildDelegatedHandoffSection(handoff) : []),
|
|
137
|
+
...buildRestartWorkspaceSection(run.restartWorkspaceCwd),
|
|
135
138
|
...buildResumeInputSection(
|
|
136
139
|
run,
|
|
137
140
|
RESUME_INPUT_PLACEHOLDER.test(promptTemplate),
|
package/src/prompt/template.ts
CHANGED
|
@@ -72,6 +72,7 @@ export function createTemplateValues({
|
|
|
72
72
|
return {
|
|
73
73
|
'workflow.input': run.input,
|
|
74
74
|
'workflow.id': workflow.definition.id,
|
|
75
|
+
'workflow.iteration': String(run.iteration ?? 1),
|
|
75
76
|
'run.id': run.runId,
|
|
76
77
|
'step.id': run.currentStepId,
|
|
77
78
|
'step.title': step.title,
|
|
@@ -81,5 +82,6 @@ export function createTemplateValues({
|
|
|
81
82
|
'gate.artifact': run.gateArtifact ?? '',
|
|
82
83
|
'gate.feedback': run.gateFeedback,
|
|
83
84
|
'resume.input': run.resumeInput ?? '',
|
|
85
|
+
'restart.workspace': run.restartWorkspaceCwd ?? '',
|
|
84
86
|
};
|
|
85
87
|
}
|
|
@@ -82,6 +82,9 @@ export function renderSummaryLines(
|
|
|
82
82
|
]
|
|
83
83
|
: []),
|
|
84
84
|
...keyValueLines(theme, 'run', run.runId, width),
|
|
85
|
+
...(run.iteration && run.iteration > 1
|
|
86
|
+
? [...keyValueLines(theme, 'iteration', String(run.iteration), width)]
|
|
87
|
+
: []),
|
|
85
88
|
...keyValueLines(
|
|
86
89
|
theme,
|
|
87
90
|
'status',
|