@wichayutdew/pi-workflows 1.0.1 → 2.0.1
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 +320 -223
- package/agents/step.md +4 -11
- package/dist/index.js +2554 -976
- package/examples/mr-comments.workflow.yaml +16 -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 -44
- package/src/command-names.ts +1 -0
- package/src/commands.ts +33 -4
- package/src/config/ceiling.ts +6 -12
- package/src/config/load-workflows.ts +12 -1
- package/src/config/step-digests.ts +19 -0
- package/src/config/types.ts +17 -22
- package/src/config/validation/permissions.ts +5 -66
- package/src/config/validation/prompt.ts +1 -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 +3 -0
- package/src/engine/gate-transitions.ts +58 -9
- 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 +235 -0
- package/src/engine/run-workflow-validation.ts +191 -0
- package/src/engine/state-types.ts +108 -5
- 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 -84
- 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 +3 -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
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { isAbsolute, win32 } from 'node:path';
|
|
2
|
+
import {
|
|
3
|
+
MAX_WORKSPACE_ALLOWED_ROOTS,
|
|
4
|
+
MAX_WORKSPACE_PATH_CHARS,
|
|
5
|
+
type PromptSpec,
|
|
6
|
+
type StepWorkspaceBinding,
|
|
7
|
+
type WorkflowGate,
|
|
8
|
+
type WorkflowStep,
|
|
9
|
+
} from '../types.ts';
|
|
2
10
|
import { parsePermissions, parseRequirements } from './permissions.ts';
|
|
3
11
|
import {
|
|
4
12
|
isJsonObject,
|
|
5
13
|
OUTCOME_PATTERN,
|
|
6
14
|
readInteger,
|
|
7
15
|
readString,
|
|
16
|
+
readStringList,
|
|
8
17
|
rejectUnknownKeys,
|
|
9
18
|
type ValidationErrors,
|
|
10
19
|
} from './shared.ts';
|
|
@@ -135,7 +144,7 @@ function parseGate(
|
|
|
135
144
|
rejectedOutcome,
|
|
136
145
|
timeoutMs: readInteger(
|
|
137
146
|
value.timeoutMs,
|
|
138
|
-
|
|
147
|
+
30_000,
|
|
139
148
|
`${path}.timeoutMs`,
|
|
140
149
|
errors,
|
|
141
150
|
{ min: 1_000, max: 30_000 },
|
|
@@ -143,6 +152,85 @@ function parseGate(
|
|
|
143
152
|
};
|
|
144
153
|
}
|
|
145
154
|
|
|
155
|
+
function parseWorkspaceRoots(
|
|
156
|
+
value: unknown,
|
|
157
|
+
path: string,
|
|
158
|
+
errors: ValidationErrors,
|
|
159
|
+
): Array<string> {
|
|
160
|
+
if (value === undefined) return ['.'];
|
|
161
|
+
if (!Array.isArray(value)) {
|
|
162
|
+
errors.push(`${path}: expected an array of relative paths`);
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
if (value.length > MAX_WORKSPACE_ALLOWED_ROOTS) {
|
|
166
|
+
errors.push(
|
|
167
|
+
`${path}: at most ${MAX_WORKSPACE_ALLOWED_ROOTS} relative paths are allowed`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const roots = value
|
|
172
|
+
.slice(0, MAX_WORKSPACE_ALLOWED_ROOTS)
|
|
173
|
+
.reduce<Array<string>>((result, item, index) => {
|
|
174
|
+
const itemPath = `${path}[${index}]`;
|
|
175
|
+
if (typeof item !== 'string' || !item || item.trim() !== item) {
|
|
176
|
+
errors.push(`${itemPath}: expected a non-empty relative path`);
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
const root = item;
|
|
180
|
+
if (root.length > MAX_WORKSPACE_PATH_CHARS) {
|
|
181
|
+
errors.push(
|
|
182
|
+
`${itemPath}: path exceeds ${MAX_WORKSPACE_PATH_CHARS} characters`,
|
|
183
|
+
);
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
if (
|
|
187
|
+
root.includes('\0') ||
|
|
188
|
+
isAbsolute(root) ||
|
|
189
|
+
win32.parse(root).root !== ''
|
|
190
|
+
) {
|
|
191
|
+
errors.push(`${itemPath}: expected a relative path`);
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
if (result.includes(root)) {
|
|
195
|
+
errors.push(`${itemPath}: duplicate value "${root}"`);
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
return [...result, root];
|
|
199
|
+
}, []);
|
|
200
|
+
if (roots.length === 0) {
|
|
201
|
+
errors.push(`${path}: at least one relative path is required`);
|
|
202
|
+
}
|
|
203
|
+
return roots;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function parseWorkspace(
|
|
207
|
+
value: unknown,
|
|
208
|
+
path: string,
|
|
209
|
+
errors: ValidationErrors,
|
|
210
|
+
): StepWorkspaceBinding | undefined {
|
|
211
|
+
if (value === undefined) return undefined;
|
|
212
|
+
if (!isJsonObject(value)) {
|
|
213
|
+
errors.push(`${path}: expected an object`);
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
rejectUnknownKeys(value, ['bindOn', 'allowedRoots'], path, errors);
|
|
217
|
+
const bindOn = readStringList(
|
|
218
|
+
value.bindOn,
|
|
219
|
+
`${path}.bindOn`,
|
|
220
|
+
errors,
|
|
221
|
+
OUTCOME_PATTERN,
|
|
222
|
+
);
|
|
223
|
+
if (bindOn.length === 0) {
|
|
224
|
+
errors.push(`${path}.bindOn: at least one outcome is required`);
|
|
225
|
+
}
|
|
226
|
+
const allowedRoots = parseWorkspaceRoots(
|
|
227
|
+
value.allowedRoots,
|
|
228
|
+
`${path}.allowedRoots`,
|
|
229
|
+
errors,
|
|
230
|
+
);
|
|
231
|
+
return { bindOn, allowedRoots };
|
|
232
|
+
}
|
|
233
|
+
|
|
146
234
|
/** Parse one workflow step and validate relationships within that step. */
|
|
147
235
|
export function parseWorkflowStep(
|
|
148
236
|
value: unknown,
|
|
@@ -164,6 +252,7 @@ export function parseWorkflowStep(
|
|
|
164
252
|
'requires',
|
|
165
253
|
'transitions',
|
|
166
254
|
'gate',
|
|
255
|
+
'workspace',
|
|
167
256
|
],
|
|
168
257
|
path,
|
|
169
258
|
errors,
|
|
@@ -196,6 +285,11 @@ export function parseWorkflowStep(
|
|
|
196
285
|
errors,
|
|
197
286
|
);
|
|
198
287
|
const gate = parseGate(value.gate, `${path}.gate`, errors);
|
|
288
|
+
const workspace = parseWorkspace(
|
|
289
|
+
value.workspace,
|
|
290
|
+
`${path}.workspace`,
|
|
291
|
+
errors,
|
|
292
|
+
);
|
|
199
293
|
|
|
200
294
|
if (gate) {
|
|
201
295
|
if (!Object.hasOwn(transitions, gate.approvedOutcome)) {
|
|
@@ -214,6 +308,15 @@ export function parseWorkflowStep(
|
|
|
214
308
|
);
|
|
215
309
|
}
|
|
216
310
|
}
|
|
311
|
+
if (workspace) {
|
|
312
|
+
workspace.bindOn.forEach((outcome) => {
|
|
313
|
+
if (!Object.hasOwn(transitions, outcome)) {
|
|
314
|
+
errors.push(
|
|
315
|
+
`${path}.workspace.bindOn: unknown transition outcome "${outcome}"`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|
|
217
320
|
|
|
218
321
|
if (!title || !prompt) return undefined;
|
|
219
322
|
return {
|
|
@@ -224,5 +327,6 @@ export function parseWorkflowStep(
|
|
|
224
327
|
requires,
|
|
225
328
|
transitions,
|
|
226
329
|
...(gate ? { gate } : {}),
|
|
330
|
+
...(workspace ? { workspace } : {}),
|
|
227
331
|
};
|
|
228
332
|
}
|
|
@@ -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 {
|
|
@@ -36,6 +38,7 @@ export const createRun = (
|
|
|
36
38
|
updatedAt: now,
|
|
37
39
|
reviewedArtifact: '',
|
|
38
40
|
reviewedFeedback: '',
|
|
41
|
+
...(cwd ? { startCwd: cwd, cwd } : {}),
|
|
39
42
|
stepHandoff: '',
|
|
40
43
|
lastSummary: '',
|
|
41
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,30 +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
|
-
? {
|
|
173
|
-
reviewedArtifact: pendingGate.artifact,
|
|
174
|
-
reviewedFeedback: resolution.feedback,
|
|
175
|
-
}
|
|
176
|
-
: {}),
|
|
177
200
|
pausedFrom: undefined,
|
|
178
201
|
pauseReason: undefined,
|
|
179
202
|
gateFeedback: resolution.feedback,
|
|
180
203
|
},
|
|
181
204
|
now,
|
|
182
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;
|
|
183
225
|
return {
|
|
184
|
-
...
|
|
226
|
+
...advanced,
|
|
227
|
+
history,
|
|
228
|
+
...(completedApprovedGate
|
|
229
|
+
? {
|
|
230
|
+
reviewedArtifact: pendingGate.artifact,
|
|
231
|
+
reviewedFeedback: resolution.feedback,
|
|
232
|
+
}
|
|
233
|
+
: {}),
|
|
185
234
|
gateFeedback: resolution.feedback,
|
|
186
235
|
};
|
|
187
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
|
);
|