@wichayutdew/pi-workflows 1.0.0 → 2.0.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 +312 -221
- package/agents/step.md +4 -11
- package/dist/index.js +2556 -961
- package/examples/mr-comments.workflow.yaml +18 -8
- package/examples/prompts/mr-comments/implement.md +13 -9
- package/examples/prompts/mr-comments/plan.md +44 -15
- package/examples/prompts/mr-comments/verify.md +8 -7
- package/examples/settings.yaml +7 -1
- package/examples/starter-kit/mr-comment.workflow.yaml +149 -0
- package/examples/starter-kit/mr-review.workflow.yaml +130 -0
- package/examples/starter-kit/steps/mr-comment/fetch.md +33 -0
- package/examples/starter-kit/steps/mr-comment/implement.md +38 -0
- package/examples/starter-kit/steps/mr-comment/plan.md +62 -0
- package/examples/starter-kit/steps/mr-comment/publish.md +36 -0
- package/examples/starter-kit/steps/mr-comment/verify.md +35 -0
- package/examples/starter-kit/steps/mr-review/fetch.md +25 -0
- package/examples/starter-kit/steps/mr-review/publish.md +30 -0
- package/examples/starter-kit/steps/mr-review/review.md +69 -0
- package/examples/starter-kit/steps/mr-review/verify.md +24 -0
- package/examples/starter-kit/steps/shared/prepare-workspace.md +57 -0
- package/examples/starter-kit/steps/ticket/implement.md +35 -0
- package/examples/starter-kit/steps/ticket/plan.md +40 -0
- package/examples/starter-kit/steps/ticket/verify.md +29 -0
- package/examples/starter-kit/steps/work/implement.md +34 -0
- package/examples/starter-kit/steps/work/plan.md +38 -0
- package/examples/starter-kit/steps/work/verify.md +30 -0
- package/examples/starter-kit/ticket.workflow.yaml +100 -0
- package/examples/starter-kit/work.workflow.yaml +97 -0
- package/package.json +3 -3
- package/schemas/workflow.schema.json +78 -35
- package/src/command-names.ts +1 -0
- package/src/commands.ts +33 -4
- package/src/config/ceiling.ts +6 -8
- package/src/config/load-workflows.ts +12 -1
- package/src/config/step-digests.ts +19 -0
- package/src/config/types.ts +17 -11
- package/src/config/validation/permissions.ts +5 -45
- package/src/config/validation/prompt.ts +3 -0
- package/src/config/validation/shared.ts +0 -2
- package/src/config/validation/step.ts +106 -2
- package/src/config/validation/workflow.ts +76 -0
- package/src/engine/create-run.ts +4 -0
- package/src/engine/gate-transitions.ts +58 -6
- package/src/engine/reconciliation-history.ts +61 -24
- package/src/engine/run-advance.ts +37 -4
- package/src/engine/run-lifecycle.ts +11 -0
- package/src/engine/run-reconciliation.ts +51 -21
- package/src/engine/run-validation.ts +236 -0
- package/src/engine/run-workflow-validation.ts +191 -0
- package/src/engine/state-types.ts +109 -4
- package/src/engine/state.ts +15 -1
- package/src/engine/step-trace.ts +495 -0
- package/src/engine/transitions.ts +1 -0
- package/src/harness/action-context.ts +31 -6
- package/src/harness/core-actions.ts +30 -1
- package/src/harness/delegation-control-actions.ts +27 -2
- package/src/harness/delegation-failure.ts +42 -9
- package/src/harness/delegation-plan.ts +96 -40
- package/src/harness/delegation-recovery-validation.ts +3 -1
- package/src/harness/delegation-response-actions.ts +73 -3
- package/src/harness/delegation-retry-policy.ts +5 -3
- package/src/harness/dependencies.ts +11 -2
- package/src/harness/gate-submission-action.ts +5 -31
- package/src/harness/lifecycle-actions.ts +4 -5
- package/src/harness/pause-actions.ts +10 -1
- package/src/harness/plannotator-result-actions.ts +20 -11
- package/src/harness/prompt-gate-actions.ts +40 -2
- package/src/harness/resume-action.ts +75 -1
- package/src/harness/start-actions.ts +77 -3
- package/src/harness/status-actions.ts +7 -17
- package/src/harness/step-effects.ts +61 -0
- package/src/harness/step-execution-actions.ts +181 -25
- package/src/harness/step-reporting.ts +168 -0
- package/src/harness/types.ts +1 -0
- package/src/harness/workspace-directory.ts +67 -0
- package/src/harness.ts +37 -9
- package/src/integrations/plannotator-requests.ts +40 -5
- package/src/integrations/subagents/child-policy-sections.ts +71 -72
- package/src/integrations/subagents/child-policy-types.ts +7 -7
- package/src/integrations/subagents/child-policy-validation.ts +10 -4
- package/src/integrations/subagents/child-runtime-completion.ts +1 -0
- package/src/integrations/subagents/child-runtime-files.ts +29 -86
- package/src/integrations/subagents/child-runtime-policy.ts +8 -33
- package/src/integrations/subagents/child-runtime.ts +3 -15
- package/src/integrations/subagents/delegated-result.ts +1 -0
- package/src/integrations/subagents/diagnostic-format.ts +5 -0
- package/src/integrations/subagents/diagnostic-types.ts +7 -1
- package/src/integrations/subagents/diagnostics.ts +2 -0
- package/src/integrations/subagents/failure-correlation.ts +52 -2
- package/src/integrations/subagents/failure-transcript.ts +60 -1
- package/src/integrations/subagents/replay-audit.ts +0 -1
- package/src/integrations/subagents/replay-safety.ts +12 -27
- package/src/integrations/subagents/session-diagnostics.ts +102 -3
- package/src/policy/bash-authorization.ts +2 -21
- package/src/policy/bash-types.ts +0 -5
- package/src/policy/bash.ts +1 -2
- package/src/policy/tool-call-authorization.ts +1 -7
- package/src/preflight.ts +0 -5
- package/src/prompt/retry-task.ts +11 -3
- package/src/prompt/step-contract.ts +10 -56
- package/src/prompt/step-sections.ts +8 -10
- package/src/prompt/step-task.ts +41 -10
- package/src/prompt/template.ts +5 -2
- package/src/runtime/completion-tool.ts +14 -0
- package/src/runtime/main-step-completion.ts +3 -0
- package/src/runtime/main-step-policy.ts +2 -1
- package/src/runtime/main-step-runtime-types.ts +9 -1
- package/src/runtime/main-step-runtime.ts +2 -0
- package/src/runtime/main-step-state.ts +6 -0
- package/src/runtime/main-step-trace.ts +45 -0
- package/src/runtime/step-result.ts +57 -0
- package/src/step-log.ts +178 -0
- package/src/workflow-doctor.ts +254 -0
- package/src/workflow-status/format-status.ts +32 -3
- package/src/workflow-status/render-board.ts +7 -4
- package/src/workflow-status/render-path.ts +51 -24
- package/src/workflow-status/render-step-detail.ts +379 -0
- package/src/workflow-status/render-summary.ts +22 -0
- package/src/workflow-status/transcript-reader.ts +231 -0
- package/src/workflow-status/types.ts +8 -0
- package/src/workflow-status/view.ts +161 -14
- package/src/workflow-status.ts +6 -0
- package/src/policy/approved-command-extraction.ts +0 -71
- package/src/policy/approved-commands.ts +0 -15
- package/src/policy/bash-read-only.ts +0 -161
- package/src/policy/restricted-git.ts +0 -30
- package/src/policy/reviewed-artifact.ts +0 -97
- package/src/policy/reviewed-command-safety.ts +0 -150
- package/src/policy/reviewed-command-shape.ts +0 -49
- package/src/policy/reviewed-repository-contract.ts +0 -138
- package/src/policy/reviewed-repository-cwd.ts +0 -102
|
@@ -67,6 +67,82 @@ function validateGraph(
|
|
|
67
67
|
}
|
|
68
68
|
});
|
|
69
69
|
});
|
|
70
|
+
validateWorkspaceGraph(steps, errors);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function validateWorkspaceGraph(
|
|
74
|
+
steps: Readonly<Record<string, WorkflowStep>>,
|
|
75
|
+
errors: ValidationErrors,
|
|
76
|
+
): void {
|
|
77
|
+
const binders = Object.entries(steps).filter(
|
|
78
|
+
(
|
|
79
|
+
entry,
|
|
80
|
+
): entry is [
|
|
81
|
+
string,
|
|
82
|
+
WorkflowStep & { workspace: NonNullable<WorkflowStep['workspace']> },
|
|
83
|
+
] => entry[1].workspace !== undefined,
|
|
84
|
+
);
|
|
85
|
+
const firstBinderId = binders[0]?.[0];
|
|
86
|
+
|
|
87
|
+
binders.forEach(([stepId, step], index) => {
|
|
88
|
+
const workspacePath = `workflow.steps.${stepId}.workspace`;
|
|
89
|
+
if (index > 0) {
|
|
90
|
+
errors.push(
|
|
91
|
+
`${workspacePath}: only one workspace-binding step is allowed; "${firstBinderId}" also configures workspace binding`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
if (!step.subagent) {
|
|
95
|
+
errors.push(`${workspacePath}: workspace binding requires a subagent`);
|
|
96
|
+
}
|
|
97
|
+
if (step.gate) {
|
|
98
|
+
errors.push(
|
|
99
|
+
`${workspacePath}: workspace binding is not allowed on a gated step`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const downstream = step.workspace.bindOn.flatMap((outcome) => {
|
|
104
|
+
const target = step.transitions[outcome];
|
|
105
|
+
if (target === '$done' || target === '$pause') {
|
|
106
|
+
errors.push(
|
|
107
|
+
`workflow.steps.${stepId}.transitions.${outcome}: workspace-binding outcome must target an ordinary step`,
|
|
108
|
+
);
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
return target && Object.hasOwn(steps, target) ? [target] : [];
|
|
112
|
+
});
|
|
113
|
+
validateWorkspaceDescendants(steps, downstream, errors);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function validateWorkspaceDescendants(
|
|
118
|
+
steps: Readonly<Record<string, WorkflowStep>>,
|
|
119
|
+
initialStepIds: ReadonlyArray<string>,
|
|
120
|
+
errors: ValidationErrors,
|
|
121
|
+
): void {
|
|
122
|
+
const pending = [...initialStepIds];
|
|
123
|
+
const visited = new Set<string>();
|
|
124
|
+
while (pending.length > 0) {
|
|
125
|
+
const stepId = pending.pop();
|
|
126
|
+
if (!stepId || visited.has(stepId)) continue;
|
|
127
|
+
visited.add(stepId);
|
|
128
|
+
const step = steps[stepId];
|
|
129
|
+
if (!step) continue;
|
|
130
|
+
if (!step.subagent) {
|
|
131
|
+
errors.push(
|
|
132
|
+
`workflow.steps.${stepId}.subagent: every nonterminal step reachable after workspace binding must use a subagent`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
Object.values(step.transitions).forEach((target) => {
|
|
136
|
+
if (
|
|
137
|
+
target !== '$done' &&
|
|
138
|
+
target !== '$pause' &&
|
|
139
|
+
Object.hasOwn(steps, target) &&
|
|
140
|
+
!visited.has(target)
|
|
141
|
+
) {
|
|
142
|
+
pending.push(target);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
70
146
|
}
|
|
71
147
|
|
|
72
148
|
/** Validate and normalize an untrusted workflow definition. */
|
package/src/engine/create-run.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { WorkflowRun } from './state-types.ts';
|
|
|
10
10
|
* @param baselineTools - Tool names available before workflow restrictions.
|
|
11
11
|
* @param runId - Stable run identifier.
|
|
12
12
|
* @param now - Creation timestamp.
|
|
13
|
+
* @param cwd - Canonical working directory captured at workflow start.
|
|
13
14
|
* @returns A new running workflow state.
|
|
14
15
|
*/
|
|
15
16
|
export const createRun = (
|
|
@@ -18,6 +19,7 @@ export const createRun = (
|
|
|
18
19
|
baselineTools: ReadonlyArray<string>,
|
|
19
20
|
runId: string,
|
|
20
21
|
now: number,
|
|
22
|
+
cwd?: string,
|
|
21
23
|
): WorkflowRun => {
|
|
22
24
|
const startStepId = workflow.definition.start;
|
|
23
25
|
return {
|
|
@@ -35,6 +37,8 @@ export const createRun = (
|
|
|
35
37
|
startedAt: now,
|
|
36
38
|
updatedAt: now,
|
|
37
39
|
reviewedArtifact: '',
|
|
40
|
+
reviewedFeedback: '',
|
|
41
|
+
...(cwd ? { startCwd: cwd, cwd } : {}),
|
|
38
42
|
stepHandoff: '',
|
|
39
43
|
lastSummary: '',
|
|
40
44
|
gateFeedback: '',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { LoadedWorkflow } from '../config/types.ts';
|
|
2
2
|
import { advanceRun } from './run-advance.ts';
|
|
3
|
+
import { recordCurrentGateDecision } from './step-trace.ts';
|
|
3
4
|
import type { GateResolution, WorkflowRun } from './state-types.ts';
|
|
4
5
|
import { currentStep, withRunUpdate } from './transition-helpers.ts';
|
|
5
6
|
|
|
@@ -12,6 +13,7 @@ import { currentStep, withRunUpdate } from './transition-helpers.ts';
|
|
|
12
13
|
* @param artifact - Artifact to display for review.
|
|
13
14
|
* @param requestId - Stable gate request identifier.
|
|
14
15
|
* @param now - Update timestamp.
|
|
16
|
+
* @param summary - Compact step handoff, separate from the review artifact.
|
|
15
17
|
* @returns A new awaiting-gate workflow state.
|
|
16
18
|
* @throws When the run or gate submission is invalid.
|
|
17
19
|
*/
|
|
@@ -22,6 +24,7 @@ export const beginGate = (
|
|
|
22
24
|
artifact: string,
|
|
23
25
|
requestId: string,
|
|
24
26
|
now: number,
|
|
27
|
+
summary: string,
|
|
25
28
|
): WorkflowRun => {
|
|
26
29
|
if (run.status !== 'running') {
|
|
27
30
|
throw new Error(
|
|
@@ -38,6 +41,9 @@ export const beginGate = (
|
|
|
38
41
|
if (!artifact.trim()) {
|
|
39
42
|
throw new Error('gate submission requires a non-empty artifact');
|
|
40
43
|
}
|
|
44
|
+
if (!summary.trim()) {
|
|
45
|
+
throw new Error('gate submission requires a non-empty summary');
|
|
46
|
+
}
|
|
41
47
|
if (!requestId) throw new Error('gate submission requires a request id');
|
|
42
48
|
|
|
43
49
|
return withRunUpdate(
|
|
@@ -49,6 +55,7 @@ export const beginGate = (
|
|
|
49
55
|
requestId,
|
|
50
56
|
stepId: run.currentStepId,
|
|
51
57
|
artifact,
|
|
58
|
+
summary,
|
|
52
59
|
submittedOutcome: outcome,
|
|
53
60
|
requestedAt: now,
|
|
54
61
|
},
|
|
@@ -102,6 +109,9 @@ export const failGate = (
|
|
|
102
109
|
status: 'running',
|
|
103
110
|
pendingGate: undefined,
|
|
104
111
|
gateFeedback: reason,
|
|
112
|
+
pausedFrom: undefined,
|
|
113
|
+
pauseReason: undefined,
|
|
114
|
+
failedStepId: undefined,
|
|
105
115
|
},
|
|
106
116
|
now,
|
|
107
117
|
)
|
|
@@ -158,27 +168,69 @@ export const resolveGate = (
|
|
|
158
168
|
const outcome = resolution.approved
|
|
159
169
|
? step.gate.approvedOutcome
|
|
160
170
|
: step.gate.rejectedOutcome;
|
|
171
|
+
const stepStructuralDigest =
|
|
172
|
+
workflow.stepStructuralDigests[pendingGate.stepId] ?? '';
|
|
173
|
+
if (resolution.approved && !stepStructuralDigest) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`gated step "${pendingGate.stepId}" has no structural digest`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
161
178
|
const summary = resolution.approved
|
|
162
|
-
? pendingGate.
|
|
179
|
+
? (pendingGate.summary ?? '')
|
|
163
180
|
: resolution.feedback
|
|
164
181
|
? `Gate rejected: ${resolution.feedback}`
|
|
165
182
|
: 'Gate rejected';
|
|
166
|
-
const
|
|
183
|
+
const decidedRun = recordCurrentGateDecision(
|
|
167
184
|
run,
|
|
185
|
+
{
|
|
186
|
+
provider: pendingGate.provider,
|
|
187
|
+
requestId: pendingGate.requestId,
|
|
188
|
+
approved: resolution.approved,
|
|
189
|
+
feedback: resolution.feedback,
|
|
190
|
+
resolvedAt: resolution.resolvedAt,
|
|
191
|
+
...(pendingGate.reviewId ? { reviewId: pendingGate.reviewId } : {}),
|
|
192
|
+
},
|
|
193
|
+
now,
|
|
194
|
+
);
|
|
195
|
+
const runnableRun = withRunUpdate(
|
|
196
|
+
decidedRun,
|
|
168
197
|
{
|
|
169
198
|
status: 'running',
|
|
170
199
|
pendingGate: undefined,
|
|
171
|
-
...(resolution.approved
|
|
172
|
-
? { reviewedArtifact: pendingGate.artifact }
|
|
173
|
-
: {}),
|
|
174
200
|
pausedFrom: undefined,
|
|
175
201
|
pauseReason: undefined,
|
|
176
202
|
gateFeedback: resolution.feedback,
|
|
177
203
|
},
|
|
178
204
|
now,
|
|
179
205
|
);
|
|
206
|
+
const advanced = advanceRun(workflow, runnableRun, outcome, summary, now);
|
|
207
|
+
const completedApprovedGate =
|
|
208
|
+
resolution.approved && advanced.history.length > runnableRun.history.length;
|
|
209
|
+
const history = completedApprovedGate
|
|
210
|
+
? advanced.history.map((entry, index) =>
|
|
211
|
+
index === advanced.history.length - 1
|
|
212
|
+
? {
|
|
213
|
+
...entry,
|
|
214
|
+
artifact: pendingGate.artifact,
|
|
215
|
+
approval: {
|
|
216
|
+
requestId: pendingGate.requestId,
|
|
217
|
+
artifact: pendingGate.artifact,
|
|
218
|
+
feedback: resolution.feedback,
|
|
219
|
+
stepStructuralDigest,
|
|
220
|
+
},
|
|
221
|
+
}
|
|
222
|
+
: entry,
|
|
223
|
+
)
|
|
224
|
+
: advanced.history;
|
|
180
225
|
return {
|
|
181
|
-
...
|
|
226
|
+
...advanced,
|
|
227
|
+
history,
|
|
228
|
+
...(completedApprovedGate
|
|
229
|
+
? {
|
|
230
|
+
reviewedArtifact: pendingGate.artifact,
|
|
231
|
+
reviewedFeedback: resolution.feedback,
|
|
232
|
+
}
|
|
233
|
+
: {}),
|
|
182
234
|
gateFeedback: resolution.feedback,
|
|
183
235
|
};
|
|
184
236
|
};
|
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
import type { LoadedWorkflow } from '../config/types.ts';
|
|
2
2
|
import type { StepHistoryEntry, WorkflowRun } from './state-types.ts';
|
|
3
3
|
|
|
4
|
+
type ReviewedApproval = {
|
|
5
|
+
readonly artifact: string;
|
|
6
|
+
readonly feedback: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const isApprovedGateEntry = (
|
|
10
|
+
workflow: LoadedWorkflow,
|
|
11
|
+
entry: StepHistoryEntry,
|
|
12
|
+
): boolean => {
|
|
13
|
+
const gate = workflow.definition.steps[entry.stepId]?.gate;
|
|
14
|
+
return gate !== undefined && entry.outcome === gate.approvedOutcome;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const latestApprovedGateEntry = (
|
|
18
|
+
workflow: LoadedWorkflow,
|
|
19
|
+
history: ReadonlyArray<StepHistoryEntry>,
|
|
20
|
+
): { readonly entry: StepHistoryEntry; readonly index: number } | undefined => {
|
|
21
|
+
for (let index = history.length - 1; index >= 0; index -= 1) {
|
|
22
|
+
const entry = history[index];
|
|
23
|
+
if (entry && isApprovedGateEntry(workflow, entry)) return { entry, index };
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
};
|
|
27
|
+
|
|
4
28
|
/**
|
|
5
29
|
* Rebuilds step visit counts from retained history and the current step.
|
|
6
30
|
*
|
|
@@ -26,37 +50,49 @@ export const rebuildVisits = (
|
|
|
26
50
|
};
|
|
27
51
|
|
|
28
52
|
/**
|
|
29
|
-
*
|
|
53
|
+
* Restores the execution directory represented by retained history.
|
|
30
54
|
*
|
|
31
|
-
* @param
|
|
32
|
-
* @param run - Existing workflow state.
|
|
55
|
+
* @param run - Existing workflow state with immutable start directory.
|
|
33
56
|
* @param history - History retained after reconciliation.
|
|
34
|
-
* @returns The retained
|
|
57
|
+
* @returns The latest retained binding, or the workflow start directory.
|
|
35
58
|
*/
|
|
36
|
-
export const
|
|
37
|
-
workflow: LoadedWorkflow,
|
|
59
|
+
export const retainedWorkspaceCwd = (
|
|
38
60
|
run: WorkflowRun,
|
|
39
61
|
history: ReadonlyArray<StepHistoryEntry>,
|
|
40
|
-
): string => {
|
|
41
|
-
|
|
42
|
-
|
|
62
|
+
): string | undefined => {
|
|
63
|
+
for (let index = history.length - 1; index >= 0; index -= 1) {
|
|
64
|
+
const cwd = history[index]?.workspaceCwd;
|
|
65
|
+
if (cwd) return cwd;
|
|
66
|
+
}
|
|
67
|
+
return run.startCwd ?? run.cwd;
|
|
68
|
+
};
|
|
43
69
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Restores one reviewed approval from the latest retained approving history.
|
|
72
|
+
*
|
|
73
|
+
* Approved history records carry their own artifact, feedback, gate request
|
|
74
|
+
* identity, and prompt-excluded structural digest.
|
|
75
|
+
*
|
|
76
|
+
* @param workflow - Updated workflow.
|
|
77
|
+
* @param history - History retained after reconciliation.
|
|
78
|
+
* @returns The retained pair, or `undefined` when provenance was lost.
|
|
79
|
+
*/
|
|
80
|
+
export const retainedReviewedApproval = (
|
|
81
|
+
workflow: LoadedWorkflow,
|
|
82
|
+
history: ReadonlyArray<StepHistoryEntry>,
|
|
83
|
+
): ReviewedApproval | undefined => {
|
|
84
|
+
const retainedApproval = latestApprovedGateEntry(workflow, history);
|
|
85
|
+
const approval = retainedApproval?.entry.approval;
|
|
86
|
+
return approval
|
|
87
|
+
? { artifact: approval.artifact, feedback: approval.feedback }
|
|
88
|
+
: undefined;
|
|
53
89
|
};
|
|
54
90
|
|
|
55
91
|
/**
|
|
56
92
|
* Refreshes the digest of retained approved-gate history.
|
|
57
93
|
*
|
|
58
|
-
* The
|
|
59
|
-
* prompt-only
|
|
94
|
+
* The approved history's prompt-excluded structural digest must still match,
|
|
95
|
+
* allowing only a prompt-only refresh without discarding the reviewed value.
|
|
60
96
|
*
|
|
61
97
|
* @param run - Existing workflow state.
|
|
62
98
|
* @param workflow - Updated workflow.
|
|
@@ -66,18 +102,19 @@ export const refreshApprovedGateHistory = (
|
|
|
66
102
|
run: WorkflowRun,
|
|
67
103
|
workflow: LoadedWorkflow,
|
|
68
104
|
): WorkflowRun => {
|
|
69
|
-
const reviewedArtifact = run.reviewedArtifact ?? '';
|
|
70
|
-
if (!reviewedArtifact) return run;
|
|
71
|
-
|
|
72
105
|
const history = run.history.map((entry) => {
|
|
73
106
|
const gate = workflow.definition.steps[entry.stepId]?.gate;
|
|
74
107
|
const currentDigest = workflow.stepDigests[entry.stepId];
|
|
108
|
+
const currentStructuralDigest =
|
|
109
|
+
workflow.stepStructuralDigests[entry.stepId];
|
|
75
110
|
const shouldRefresh =
|
|
76
111
|
gate !== undefined &&
|
|
77
112
|
typeof currentDigest === 'string' &&
|
|
78
113
|
currentDigest.length > 0 &&
|
|
114
|
+
typeof currentStructuralDigest === 'string' &&
|
|
115
|
+
currentStructuralDigest.length > 0 &&
|
|
79
116
|
entry.outcome === gate.approvedOutcome &&
|
|
80
|
-
entry.
|
|
117
|
+
entry.approval?.stepStructuralDigest === currentStructuralDigest &&
|
|
81
118
|
entry.stepDigest !== currentDigest;
|
|
82
119
|
return shouldRefresh ? { ...entry, stepDigest: currentDigest } : entry;
|
|
83
120
|
});
|
|
@@ -2,16 +2,29 @@ import type { LoadedWorkflow } from '../config/types.ts';
|
|
|
2
2
|
import type { StepHistoryEntry, WorkflowRun } from './state-types.ts';
|
|
3
3
|
import { currentStep, withRunUpdate } from './transition-helpers.ts';
|
|
4
4
|
|
|
5
|
+
export type RunStepEffects = {
|
|
6
|
+
/** Canonical directory selected through the step's YAML-authorized result. */
|
|
7
|
+
readonly workspaceCwd?: string | undefined;
|
|
8
|
+
};
|
|
9
|
+
|
|
5
10
|
const completedStep = (
|
|
6
11
|
run: WorkflowRun,
|
|
7
12
|
outcome: string,
|
|
8
13
|
summary: string,
|
|
9
14
|
now: number,
|
|
15
|
+
effects: RunStepEffects,
|
|
10
16
|
): StepHistoryEntry => ({
|
|
11
17
|
stepId: run.currentStepId,
|
|
12
18
|
stepDigest: run.currentStepDigest,
|
|
13
19
|
outcome,
|
|
14
20
|
summary,
|
|
21
|
+
...(effects.workspaceCwd ? { workspaceCwd: effects.workspaceCwd } : {}),
|
|
22
|
+
...(run.currentStepAttempts?.length
|
|
23
|
+
? { attempts: run.currentStepAttempts }
|
|
24
|
+
: {}),
|
|
25
|
+
...(run.currentStepOmittedAttempts
|
|
26
|
+
? { omittedAttempts: run.currentStepOmittedAttempts }
|
|
27
|
+
: {}),
|
|
15
28
|
completedAt: now,
|
|
16
29
|
});
|
|
17
30
|
|
|
@@ -23,6 +36,7 @@ const completedStep = (
|
|
|
23
36
|
* @param outcome - Submitted step outcome.
|
|
24
37
|
* @param summary - Step handoff summary.
|
|
25
38
|
* @param now - Update timestamp.
|
|
39
|
+
* @param effects - Validated declarative effects accepted with this result.
|
|
26
40
|
* @returns A new paused, running, or completed workflow state.
|
|
27
41
|
* @throws When the run, outcome, current step, or transition target is invalid.
|
|
28
42
|
*/
|
|
@@ -32,6 +46,7 @@ export const advanceRun = (
|
|
|
32
46
|
outcome: string,
|
|
33
47
|
summary: string,
|
|
34
48
|
now: number,
|
|
49
|
+
effects: RunStepEffects = {},
|
|
35
50
|
): WorkflowRun => {
|
|
36
51
|
if (run.status !== 'running') {
|
|
37
52
|
throw new Error(
|
|
@@ -55,6 +70,14 @@ export const advanceRun = (
|
|
|
55
70
|
`outcome "${outcome}" is not valid for step "${run.currentStepId}"`,
|
|
56
71
|
);
|
|
57
72
|
}
|
|
73
|
+
const shouldBindWorkspace = step.workspace?.bindOn.includes(outcome) ?? false;
|
|
74
|
+
if (shouldBindWorkspace !== Boolean(effects.workspaceCwd)) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
shouldBindWorkspace
|
|
77
|
+
? `outcome "${outcome}" requires a validated workspace binding`
|
|
78
|
+
: `outcome "${outcome}" cannot bind a workspace`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
58
81
|
if (target === '$pause') {
|
|
59
82
|
return withRunUpdate(
|
|
60
83
|
run,
|
|
@@ -63,30 +86,35 @@ export const advanceRun = (
|
|
|
63
86
|
pausedFrom: 'running',
|
|
64
87
|
pauseReason: summary || `Step "${run.currentStepId}" requested a pause`,
|
|
65
88
|
lastSummary: summary,
|
|
89
|
+
resumeInput: undefined,
|
|
66
90
|
},
|
|
67
91
|
now,
|
|
68
92
|
);
|
|
69
93
|
}
|
|
70
94
|
|
|
71
|
-
const completed = completedStep(run, outcome, summary, now);
|
|
95
|
+
const completed = completedStep(run, outcome, summary, now, effects);
|
|
96
|
+
const cwd = effects.workspaceCwd ?? run.cwd;
|
|
72
97
|
if (target === '$done') {
|
|
73
98
|
return withRunUpdate(
|
|
74
99
|
run,
|
|
75
100
|
{
|
|
76
101
|
status: 'completed',
|
|
77
102
|
history: [...run.history, completed],
|
|
103
|
+
currentStepAttempts: undefined,
|
|
104
|
+
currentStepOmittedAttempts: undefined,
|
|
105
|
+
...(cwd ? { cwd } : {}),
|
|
78
106
|
stepHandoff: summary,
|
|
79
107
|
lastSummary: summary,
|
|
80
108
|
gateFeedback: '',
|
|
81
109
|
pausedFrom: undefined,
|
|
82
110
|
pendingGate: undefined,
|
|
111
|
+
resumeInput: undefined,
|
|
83
112
|
},
|
|
84
113
|
now,
|
|
85
114
|
);
|
|
86
115
|
}
|
|
87
116
|
|
|
88
|
-
|
|
89
|
-
if (!nextStep) {
|
|
117
|
+
if (!workflow.definition.steps[target]) {
|
|
90
118
|
throw new Error(`transition target "${target}" does not exist`);
|
|
91
119
|
}
|
|
92
120
|
|
|
@@ -97,11 +125,13 @@ export const advanceRun = (
|
|
|
97
125
|
status: 'paused',
|
|
98
126
|
pausedFrom: 'running',
|
|
99
127
|
pauseReason: `Step "${target}" exceeded maxStepVisits (${workflow.definition.maxStepVisits})`,
|
|
128
|
+
failedStepId: target,
|
|
100
129
|
}
|
|
101
130
|
: {
|
|
102
131
|
status: 'running',
|
|
103
132
|
pausedFrom: undefined,
|
|
104
133
|
pauseReason: undefined,
|
|
134
|
+
failedStepId: undefined,
|
|
105
135
|
};
|
|
106
136
|
|
|
107
137
|
return withRunUpdate(
|
|
@@ -112,10 +142,13 @@ export const advanceRun = (
|
|
|
112
142
|
currentStepDigest: workflow.stepDigests[target] ?? '',
|
|
113
143
|
visits: { ...run.visits, [target]: nextVisitCount },
|
|
114
144
|
history: [...run.history, completed],
|
|
145
|
+
currentStepAttempts: undefined,
|
|
146
|
+
currentStepOmittedAttempts: undefined,
|
|
147
|
+
...(cwd ? { cwd } : {}),
|
|
115
148
|
stepHandoff: summary,
|
|
116
149
|
lastSummary: summary,
|
|
117
150
|
gateFeedback: '',
|
|
118
|
-
|
|
151
|
+
resumeInput: undefined,
|
|
119
152
|
},
|
|
120
153
|
now,
|
|
121
154
|
);
|
|
@@ -98,6 +98,16 @@ export const resumeRun = (run: WorkflowRun, now: number): WorkflowRun => {
|
|
|
98
98
|
);
|
|
99
99
|
};
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Replaces the user-authored guidance for one explicit resume attempt.
|
|
103
|
+
*/
|
|
104
|
+
export const setResumeInput = (
|
|
105
|
+
run: WorkflowRun,
|
|
106
|
+
input: string,
|
|
107
|
+
now: number,
|
|
108
|
+
): WorkflowRun =>
|
|
109
|
+
withRunUpdate(run, { resumeInput: input.trim() || undefined }, now);
|
|
110
|
+
|
|
101
111
|
/**
|
|
102
112
|
* Aborts a run and clears resumable gate state.
|
|
103
113
|
*
|
|
@@ -119,6 +129,7 @@ export const abortRun = (
|
|
|
119
129
|
pausedFrom: undefined,
|
|
120
130
|
failedStepId: undefined,
|
|
121
131
|
pendingGate: undefined,
|
|
132
|
+
resumeInput: undefined,
|
|
122
133
|
},
|
|
123
134
|
now,
|
|
124
135
|
);
|
|
@@ -2,11 +2,13 @@ import type { LoadedWorkflow } from '../config/types.ts';
|
|
|
2
2
|
import {
|
|
3
3
|
rebuildVisits,
|
|
4
4
|
refreshApprovedGateHistory,
|
|
5
|
-
|
|
5
|
+
retainedReviewedApproval,
|
|
6
|
+
retainedWorkspaceCwd,
|
|
6
7
|
} from './reconciliation-history.ts';
|
|
7
8
|
import type { WorkflowRun } from './state-types.ts';
|
|
8
9
|
import { withRunUpdate } from './transition-helpers.ts';
|
|
9
10
|
import type { ReconcileResult } from './transition-types.ts';
|
|
11
|
+
import { validateRunWorkflowSemantics } from './run-workflow-validation.ts';
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Reconciles persisted run state with a reloaded workflow configuration.
|
|
@@ -30,33 +32,61 @@ export const reconcileRun = (
|
|
|
30
32
|
error: `run belongs to "${run.workflowId}", not "${workflow.definition.id}"`,
|
|
31
33
|
};
|
|
32
34
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
const reconciledRun =
|
|
36
|
+
run.workflowDigest === workflow.digest
|
|
37
|
+
? run
|
|
38
|
+
: refreshApprovedGateHistory(run, workflow);
|
|
39
|
+
const changedHistoryIndex =
|
|
40
|
+
run.workflowDigest === workflow.digest
|
|
41
|
+
? -1
|
|
42
|
+
: reconciledRun.history.findIndex(
|
|
43
|
+
(entry) => workflow.stepDigests[entry.stepId] !== entry.stepDigest,
|
|
44
|
+
);
|
|
45
|
+
const changedHistoryEntry =
|
|
46
|
+
changedHistoryIndex >= 0
|
|
47
|
+
? reconciledRun.history[changedHistoryIndex]
|
|
48
|
+
: undefined;
|
|
49
|
+
if (
|
|
50
|
+
changedHistoryEntry &&
|
|
51
|
+
!workflow.definition.steps[changedHistoryEntry.stepId]
|
|
52
|
+
) {
|
|
53
|
+
return {
|
|
54
|
+
changed: true,
|
|
55
|
+
error: 'a completed step was removed; abort or restore the configuration',
|
|
56
|
+
};
|
|
35
57
|
}
|
|
36
|
-
if (
|
|
58
|
+
if (
|
|
59
|
+
changedHistoryIndex < 0 &&
|
|
60
|
+
!workflow.definition.steps[run.currentStepId]
|
|
61
|
+
) {
|
|
37
62
|
return {
|
|
38
63
|
changed: true,
|
|
39
64
|
error: `current step "${run.currentStepId}" was removed; abort or restore the configuration`,
|
|
40
65
|
};
|
|
41
66
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
67
|
+
const semanticError = validateRunWorkflowSemantics(reconciledRun, workflow);
|
|
68
|
+
if (semanticError) {
|
|
69
|
+
return {
|
|
70
|
+
changed: run.workflowDigest !== workflow.digest,
|
|
71
|
+
error: `workflow checkpoint is inconsistent: ${semanticError}`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (run.workflowDigest === workflow.digest) {
|
|
75
|
+
return { run, changed: false };
|
|
76
|
+
}
|
|
47
77
|
if (changedHistoryIndex >= 0) {
|
|
48
|
-
const changedEntry =
|
|
49
|
-
if (!changedEntry
|
|
50
|
-
return {
|
|
51
|
-
changed: true,
|
|
52
|
-
error:
|
|
53
|
-
'a completed step was removed; abort or restore the configuration',
|
|
54
|
-
};
|
|
78
|
+
const changedEntry = changedHistoryEntry;
|
|
79
|
+
if (!changedEntry) {
|
|
80
|
+
return { changed: true, error: 'changed history entry is unavailable' };
|
|
55
81
|
}
|
|
56
82
|
|
|
57
83
|
const retainedHistory = reconciledRun.history.slice(0, changedHistoryIndex);
|
|
58
84
|
const restartedStep = changedEntry.stepId;
|
|
59
85
|
const stepHandoff = retainedHistory.at(-1)?.summary ?? '';
|
|
86
|
+
const reviewedApproval = retainedReviewedApproval(
|
|
87
|
+
workflow,
|
|
88
|
+
retainedHistory,
|
|
89
|
+
);
|
|
60
90
|
return {
|
|
61
91
|
changed: true,
|
|
62
92
|
restartedStep,
|
|
@@ -68,12 +98,12 @@ export const reconcileRun = (
|
|
|
68
98
|
currentStepId: restartedStep,
|
|
69
99
|
currentStepDigest: workflow.stepDigests[restartedStep] ?? '',
|
|
70
100
|
history: retainedHistory,
|
|
101
|
+
currentStepAttempts: changedEntry.attempts,
|
|
102
|
+
currentStepOmittedAttempts: changedEntry.omittedAttempts,
|
|
71
103
|
visits: rebuildVisits(retainedHistory, restartedStep),
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
retainedHistory,
|
|
76
|
-
),
|
|
104
|
+
cwd: retainedWorkspaceCwd(reconciledRun, retainedHistory),
|
|
105
|
+
reviewedArtifact: reviewedApproval?.artifact ?? '',
|
|
106
|
+
reviewedFeedback: reviewedApproval?.feedback ?? '',
|
|
77
107
|
stepHandoff,
|
|
78
108
|
lastSummary: stepHandoff,
|
|
79
109
|
pendingGate: undefined,
|