@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
|
@@ -1,23 +1,220 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
1
2
|
import {
|
|
3
|
+
MAX_RESUME_INPUT_CHARS,
|
|
4
|
+
MAX_STEP_TRACE_ARTIFACT_CHARS,
|
|
5
|
+
MAX_STEP_TRACE_ATTEMPTS,
|
|
6
|
+
MAX_STEP_TRACE_LOG_CHARS,
|
|
7
|
+
MAX_STEP_TRACE_LOG_EVENT_CHARS,
|
|
8
|
+
MAX_STEP_TRACE_LOG_EVENTS,
|
|
9
|
+
MAX_STEP_TRACE_SUMMARY_CHARS,
|
|
10
|
+
MAX_STEP_TRACE_TASK_CHARS,
|
|
11
|
+
MAX_WORKFLOW_TRACE_CHARS,
|
|
2
12
|
RUN_STATE_VERSION,
|
|
13
|
+
type GateApproval,
|
|
3
14
|
type GateResolution,
|
|
4
15
|
type PendingGate,
|
|
16
|
+
type StepAttemptResult,
|
|
17
|
+
type StepExecutionAttempt,
|
|
18
|
+
type StepGateDecision,
|
|
5
19
|
type StepHistoryEntry,
|
|
6
20
|
type WorkflowRun,
|
|
7
21
|
type WorkflowRunStatus,
|
|
8
22
|
} from './state-types.ts';
|
|
23
|
+
import { workflowTraceChars } from './step-trace.ts';
|
|
9
24
|
|
|
10
25
|
type UnknownRecord = Readonly<Record<string, unknown>>;
|
|
11
26
|
|
|
12
27
|
const isRecord = (value: unknown): value is UnknownRecord =>
|
|
13
28
|
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
14
29
|
|
|
30
|
+
const isAbsoluteCwd = (value: unknown): value is string =>
|
|
31
|
+
typeof value === 'string' &&
|
|
32
|
+
value.length > 0 &&
|
|
33
|
+
isAbsolute(value) &&
|
|
34
|
+
!value.includes('\0');
|
|
35
|
+
|
|
36
|
+
const isGateApproval = (value: unknown): value is GateApproval =>
|
|
37
|
+
isRecord(value) &&
|
|
38
|
+
typeof value.requestId === 'string' &&
|
|
39
|
+
value.requestId.length > 0 &&
|
|
40
|
+
typeof value.artifact === 'string' &&
|
|
41
|
+
value.artifact.trim().length > 0 &&
|
|
42
|
+
typeof value.feedback === 'string' &&
|
|
43
|
+
typeof value.stepStructuralDigest === 'string' &&
|
|
44
|
+
value.stepStructuralDigest.length > 0;
|
|
45
|
+
|
|
46
|
+
const isStepAttemptResult = (value: unknown): value is StepAttemptResult =>
|
|
47
|
+
isRecord(value) &&
|
|
48
|
+
typeof value.outcome === 'string' &&
|
|
49
|
+
typeof value.summary === 'string' &&
|
|
50
|
+
value.summary.length <= MAX_STEP_TRACE_SUMMARY_CHARS &&
|
|
51
|
+
(value.summaryTruncated === undefined || value.summaryTruncated === true) &&
|
|
52
|
+
(value.artifact === undefined || typeof value.artifact === 'string') &&
|
|
53
|
+
(typeof value.artifact !== 'string' ||
|
|
54
|
+
value.artifact.length <= MAX_STEP_TRACE_ARTIFACT_CHARS) &&
|
|
55
|
+
(value.artifactTruncated === undefined || value.artifactTruncated === true) &&
|
|
56
|
+
!(value.artifactTruncated === true && value.artifact === undefined) &&
|
|
57
|
+
(value.workspaceCwd === undefined || isAbsoluteCwd(value.workspaceCwd));
|
|
58
|
+
|
|
59
|
+
const isStepGateDecision = (value: unknown): value is StepGateDecision =>
|
|
60
|
+
isRecord(value) &&
|
|
61
|
+
(value.provider === 'prompt' || value.provider === 'plannotator') &&
|
|
62
|
+
typeof value.requestId === 'string' &&
|
|
63
|
+
value.requestId.length > 0 &&
|
|
64
|
+
typeof value.approved === 'boolean' &&
|
|
65
|
+
typeof value.feedback === 'string' &&
|
|
66
|
+
value.feedback.length <= MAX_STEP_TRACE_SUMMARY_CHARS &&
|
|
67
|
+
(value.feedbackTruncated === undefined || value.feedbackTruncated === true) &&
|
|
68
|
+
typeof value.resolvedAt === 'number' &&
|
|
69
|
+
(value.reviewId === undefined || typeof value.reviewId === 'string');
|
|
70
|
+
|
|
71
|
+
const isSafeTraceIdentityField = (value: unknown): value is string =>
|
|
72
|
+
typeof value === 'string' &&
|
|
73
|
+
value.length > 0 &&
|
|
74
|
+
!value.includes('\0') &&
|
|
75
|
+
!value.includes('/') &&
|
|
76
|
+
!value.includes('\\') &&
|
|
77
|
+
value !== '.' &&
|
|
78
|
+
value !== '..';
|
|
79
|
+
|
|
80
|
+
const hasValidMainStepLog = (value: UnknownRecord): boolean => {
|
|
81
|
+
const log = value.log;
|
|
82
|
+
if (
|
|
83
|
+
log !== undefined &&
|
|
84
|
+
(!Array.isArray(log) ||
|
|
85
|
+
log.length > MAX_STEP_TRACE_LOG_EVENTS ||
|
|
86
|
+
!log.every(
|
|
87
|
+
(line) =>
|
|
88
|
+
typeof line === 'string' &&
|
|
89
|
+
line.length > 0 &&
|
|
90
|
+
line.length <= MAX_STEP_TRACE_LOG_EVENT_CHARS,
|
|
91
|
+
) ||
|
|
92
|
+
log.reduce<number>(
|
|
93
|
+
(total, line) => total + (typeof line === 'string' ? line.length : 0),
|
|
94
|
+
0,
|
|
95
|
+
) > MAX_STEP_TRACE_LOG_CHARS)
|
|
96
|
+
) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
if (value.logTruncated !== undefined && value.logTruncated !== true) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
if (
|
|
103
|
+
value.omittedLogEvents !== undefined &&
|
|
104
|
+
(!Number.isSafeInteger(value.omittedLogEvents) ||
|
|
105
|
+
(value.omittedLogEvents as number) <= 0)
|
|
106
|
+
) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
return (
|
|
110
|
+
(value.logTruncated === true) ===
|
|
111
|
+
(typeof value.omittedLogEvents === 'number') &&
|
|
112
|
+
!(
|
|
113
|
+
log === undefined &&
|
|
114
|
+
(value.logTruncated !== undefined || value.omittedLogEvents !== undefined)
|
|
115
|
+
)
|
|
116
|
+
);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const isStepExecutionAttempt = (
|
|
120
|
+
value: unknown,
|
|
121
|
+
): value is StepExecutionAttempt => {
|
|
122
|
+
if (
|
|
123
|
+
!isRecord(value) ||
|
|
124
|
+
(value.kind !== 'main' && value.kind !== 'subagent') ||
|
|
125
|
+
typeof value.requestId !== 'string' ||
|
|
126
|
+
value.requestId.length === 0 ||
|
|
127
|
+
value.requestId.includes('\0') ||
|
|
128
|
+
(value.ordinal !== undefined &&
|
|
129
|
+
(!Number.isSafeInteger(value.ordinal) ||
|
|
130
|
+
(value.ordinal as number) <= 0)) ||
|
|
131
|
+
typeof value.task !== 'string' ||
|
|
132
|
+
value.task.trim().length === 0 ||
|
|
133
|
+
value.task.length > MAX_STEP_TRACE_TASK_CHARS ||
|
|
134
|
+
(value.taskTruncated !== undefined && value.taskTruncated !== true) ||
|
|
135
|
+
(value.omittedTaskChars !== undefined &&
|
|
136
|
+
(!Number.isSafeInteger(value.omittedTaskChars) ||
|
|
137
|
+
(value.omittedTaskChars as number) <= 0)) ||
|
|
138
|
+
(value.taskTruncated === true) !==
|
|
139
|
+
(typeof value.omittedTaskChars === 'number') ||
|
|
140
|
+
typeof value.startedAt !== 'number' ||
|
|
141
|
+
(value.result !== undefined && !isStepAttemptResult(value.result)) ||
|
|
142
|
+
(value.gateDecision !== undefined &&
|
|
143
|
+
!isStepGateDecision(value.gateDecision))
|
|
144
|
+
) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
if (value.kind === 'main') {
|
|
148
|
+
return (
|
|
149
|
+
value.agent === undefined &&
|
|
150
|
+
value.transcript === undefined &&
|
|
151
|
+
hasValidMainStepLog(value)
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
if (
|
|
155
|
+
value.log !== undefined ||
|
|
156
|
+
value.logTruncated !== undefined ||
|
|
157
|
+
value.omittedLogEvents !== undefined
|
|
158
|
+
) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
if (typeof value.agent !== 'string' || value.agent.length === 0) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
if (value.transcript === undefined) return true;
|
|
165
|
+
if (!isRecord(value.transcript)) return false;
|
|
166
|
+
const transcript = value.transcript;
|
|
167
|
+
if (
|
|
168
|
+
!isAbsoluteCwd(transcript.trustedRoot) ||
|
|
169
|
+
!isAbsoluteCwd(transcript.sessionFile) ||
|
|
170
|
+
!isSafeTraceIdentityField(transcript.runId) ||
|
|
171
|
+
!Number.isSafeInteger(transcript.childIndex) ||
|
|
172
|
+
(transcript.childIndex as number) < 0
|
|
173
|
+
) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
return (
|
|
177
|
+
resolve(
|
|
178
|
+
transcript.trustedRoot,
|
|
179
|
+
transcript.runId,
|
|
180
|
+
`run-${String(transcript.childIndex)}`,
|
|
181
|
+
'session.jsonl',
|
|
182
|
+
) === resolve(transcript.sessionFile)
|
|
183
|
+
);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const isStepExecutionAttempts = (
|
|
187
|
+
value: unknown,
|
|
188
|
+
): value is ReadonlyArray<StepExecutionAttempt> =>
|
|
189
|
+
Array.isArray(value) &&
|
|
190
|
+
value.length <= MAX_STEP_TRACE_ATTEMPTS &&
|
|
191
|
+
value.every(isStepExecutionAttempt) &&
|
|
192
|
+
new Set(value.map((attempt) => attempt.requestId)).size === value.length &&
|
|
193
|
+
value.every((attempt, index) => {
|
|
194
|
+
if (attempt.ordinal === undefined) return true;
|
|
195
|
+
const ordinal = attempt.ordinal;
|
|
196
|
+
return value
|
|
197
|
+
.slice(0, index)
|
|
198
|
+
.every(
|
|
199
|
+
(earlier) => earlier.ordinal === undefined || earlier.ordinal < ordinal,
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
|
|
15
203
|
const isStepHistoryEntry = (value: unknown): value is StepHistoryEntry =>
|
|
16
204
|
isRecord(value) &&
|
|
17
205
|
typeof value.stepId === 'string' &&
|
|
18
206
|
typeof value.stepDigest === 'string' &&
|
|
19
207
|
typeof value.outcome === 'string' &&
|
|
20
208
|
typeof value.summary === 'string' &&
|
|
209
|
+
(value.workspaceCwd === undefined || isAbsoluteCwd(value.workspaceCwd)) &&
|
|
210
|
+
(value.artifact === undefined || typeof value.artifact === 'string') &&
|
|
211
|
+
(value.approval === undefined ||
|
|
212
|
+
(isGateApproval(value.approval) &&
|
|
213
|
+
value.artifact === value.approval.artifact)) &&
|
|
214
|
+
(value.attempts === undefined || isStepExecutionAttempts(value.attempts)) &&
|
|
215
|
+
(value.omittedAttempts === undefined ||
|
|
216
|
+
(Number.isSafeInteger(value.omittedAttempts) &&
|
|
217
|
+
(value.omittedAttempts as number) > 0)) &&
|
|
21
218
|
typeof value.completedAt === 'number';
|
|
22
219
|
|
|
23
220
|
const isGateResolution = (value: unknown): value is GateResolution =>
|
|
@@ -49,6 +246,10 @@ const isVisitCounts = (value: unknown): value is Record<string, number> =>
|
|
|
49
246
|
const isOptionalString = (value: unknown): value is string | undefined =>
|
|
50
247
|
value === undefined || typeof value === 'string';
|
|
51
248
|
|
|
249
|
+
const isOptionalResumeInput = (value: unknown): value is string | undefined =>
|
|
250
|
+
value === undefined ||
|
|
251
|
+
(typeof value === 'string' && value.length <= MAX_RESUME_INPUT_CHARS);
|
|
252
|
+
|
|
52
253
|
const isWorkflowRunStatus = (value: unknown): value is WorkflowRunStatus =>
|
|
53
254
|
value === 'running' ||
|
|
54
255
|
value === 'paused' ||
|
|
@@ -75,6 +276,29 @@ const hasValidGateState = (
|
|
|
75
276
|
(run.status === 'awaiting-gate' ||
|
|
76
277
|
(run.status === 'paused' && run.pausedFrom === 'awaiting-gate'));
|
|
77
278
|
|
|
279
|
+
const hasValidWorkspaceState = (
|
|
280
|
+
run: UnknownRecord,
|
|
281
|
+
history: ReadonlyArray<StepHistoryEntry>,
|
|
282
|
+
): boolean => {
|
|
283
|
+
const startCwd = run.startCwd;
|
|
284
|
+
const cwd = run.cwd;
|
|
285
|
+
if (startCwd !== undefined && !isAbsoluteCwd(startCwd)) return false;
|
|
286
|
+
if (cwd !== undefined && !isAbsoluteCwd(cwd)) return false;
|
|
287
|
+
|
|
288
|
+
const bindings = history.flatMap((entry) =>
|
|
289
|
+
entry.workspaceCwd ? [entry.workspaceCwd] : [],
|
|
290
|
+
);
|
|
291
|
+
if (new Set(bindings).size > 1) return false;
|
|
292
|
+
const boundCwd = bindings.at(-1);
|
|
293
|
+
if (boundCwd !== undefined) {
|
|
294
|
+
return isAbsoluteCwd(startCwd) && isAbsoluteCwd(cwd) && cwd === boundCwd;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Legacy checkpoints have only `cwd`; new checkpoints keep start/current
|
|
298
|
+
// equal until a structured workspace binding is accepted.
|
|
299
|
+
return startCwd === undefined || cwd === startCwd;
|
|
300
|
+
};
|
|
301
|
+
|
|
78
302
|
/**
|
|
79
303
|
* Validates an unknown value as a persisted workflow run.
|
|
80
304
|
*
|
|
@@ -100,6 +324,11 @@ export const isWorkflowRun = (value: unknown): value is WorkflowRun => {
|
|
|
100
324
|
value.baselineTools.every((tool) => typeof tool === 'string') &&
|
|
101
325
|
Array.isArray(value.history) &&
|
|
102
326
|
value.history.every(isStepHistoryEntry) &&
|
|
327
|
+
(value.currentStepAttempts === undefined ||
|
|
328
|
+
isStepExecutionAttempts(value.currentStepAttempts)) &&
|
|
329
|
+
(value.currentStepOmittedAttempts === undefined ||
|
|
330
|
+
(Number.isSafeInteger(value.currentStepOmittedAttempts) &&
|
|
331
|
+
(value.currentStepOmittedAttempts as number) > 0)) &&
|
|
103
332
|
isVisitCounts(value.visits) &&
|
|
104
333
|
typeof value.startedAt === 'number' &&
|
|
105
334
|
typeof value.updatedAt === 'number' &&
|
|
@@ -109,7 +338,9 @@ export const isWorkflowRun = (value: unknown): value is WorkflowRun => {
|
|
|
109
338
|
|
|
110
339
|
const hasValidOptionalFields =
|
|
111
340
|
isOptionalString(value.reviewedArtifact) &&
|
|
341
|
+
isOptionalString(value.reviewedFeedback) &&
|
|
112
342
|
isOptionalString(value.stepHandoff) &&
|
|
343
|
+
isOptionalResumeInput(value.resumeInput) &&
|
|
113
344
|
isOptionalString(value.pauseReason) &&
|
|
114
345
|
isOptionalString(value.failedStepId) &&
|
|
115
346
|
(value.pausedFrom === undefined ||
|
|
@@ -121,6 +352,11 @@ export const isWorkflowRun = (value: unknown): value is WorkflowRun => {
|
|
|
121
352
|
if (pendingGate !== undefined && !isPendingGate(pendingGate)) return false;
|
|
122
353
|
|
|
123
354
|
return (
|
|
355
|
+
workflowTraceChars(value as WorkflowRun) <= MAX_WORKFLOW_TRACE_CHARS &&
|
|
356
|
+
hasValidWorkspaceState(
|
|
357
|
+
value,
|
|
358
|
+
value.history as ReadonlyArray<StepHistoryEntry>,
|
|
359
|
+
) &&
|
|
124
360
|
hasValidPauseState(value) &&
|
|
125
361
|
hasValidFailureState(value) &&
|
|
126
362
|
hasValidGateState(value, pendingGate)
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { LoadedWorkflow, WorkflowStep } from '../config/types.ts';
|
|
2
|
+
import type { StepHistoryEntry, WorkflowRun } from './state-types.ts';
|
|
3
|
+
|
|
4
|
+
const sameVisitCounts = (
|
|
5
|
+
actual: Readonly<Record<string, number>>,
|
|
6
|
+
expected: Readonly<Record<string, number>>,
|
|
7
|
+
): boolean => {
|
|
8
|
+
const actualEntries = Object.entries(actual).sort(([left], [right]) =>
|
|
9
|
+
left.localeCompare(right),
|
|
10
|
+
);
|
|
11
|
+
const expectedEntries = Object.entries(expected).sort(([left], [right]) =>
|
|
12
|
+
left.localeCompare(right),
|
|
13
|
+
);
|
|
14
|
+
return (
|
|
15
|
+
actualEntries.length === expectedEntries.length &&
|
|
16
|
+
actualEntries.every(
|
|
17
|
+
([stepId, count], index) =>
|
|
18
|
+
expectedEntries[index]?.[0] === stepId &&
|
|
19
|
+
expectedEntries[index][1] === count,
|
|
20
|
+
)
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const validateHistoryMetadata = (
|
|
25
|
+
workflow: LoadedWorkflow,
|
|
26
|
+
step: WorkflowStep,
|
|
27
|
+
entry: StepHistoryEntry,
|
|
28
|
+
): string | undefined => {
|
|
29
|
+
const shouldBind = step.workspace?.bindOn.includes(entry.outcome) === true;
|
|
30
|
+
if (shouldBind !== (entry.workspaceCwd !== undefined)) {
|
|
31
|
+
return shouldBind
|
|
32
|
+
? `history step "${entry.stepId}" is missing its workspace binding`
|
|
33
|
+
: `history step "${entry.stepId}" has an unauthorized workspace binding`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const isApprovedGate =
|
|
37
|
+
step.gate !== undefined && entry.outcome === step.gate.approvedOutcome;
|
|
38
|
+
if (isApprovedGate && !entry.approval) {
|
|
39
|
+
return `history step "${entry.stepId}" is missing authoritative gate approval`;
|
|
40
|
+
}
|
|
41
|
+
if (isApprovedGate && entry.artifact !== entry.approval?.artifact) {
|
|
42
|
+
return `history step "${entry.stepId}" approval artifact is inconsistent`;
|
|
43
|
+
}
|
|
44
|
+
if (!isApprovedGate && (entry.approval || entry.artifact !== undefined)) {
|
|
45
|
+
return `history step "${entry.stepId}" has approval data for a non-approved outcome`;
|
|
46
|
+
}
|
|
47
|
+
if (
|
|
48
|
+
isApprovedGate &&
|
|
49
|
+
entry.approval?.stepStructuralDigest !==
|
|
50
|
+
workflow.stepStructuralDigests[entry.stepId]
|
|
51
|
+
) {
|
|
52
|
+
return `history step "${entry.stepId}" approval does not match its configured structure`;
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const validatePendingGate = (
|
|
58
|
+
run: WorkflowRun,
|
|
59
|
+
step: WorkflowStep,
|
|
60
|
+
): string | undefined => {
|
|
61
|
+
const pending = run.pendingGate;
|
|
62
|
+
if (!pending) return undefined;
|
|
63
|
+
if (
|
|
64
|
+
!step.gate ||
|
|
65
|
+
pending.stepId !== run.currentStepId ||
|
|
66
|
+
pending.provider !== step.gate.provider ||
|
|
67
|
+
pending.submittedOutcome !== step.gate.submitOutcome
|
|
68
|
+
) {
|
|
69
|
+
return 'pending gate does not match the current workflow step';
|
|
70
|
+
}
|
|
71
|
+
if (pending.provider === 'prompt' && pending.reviewId !== undefined) {
|
|
72
|
+
return 'built-in prompt gate cannot carry a Plannotator review id';
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Validates persisted control-flow state against the loaded declarative graph.
|
|
79
|
+
*
|
|
80
|
+
* When configuration changed, the first digest-mismatched history entry is
|
|
81
|
+
* intentionally left to reconciliation, which restarts that exact reachable
|
|
82
|
+
* step and discards everything after it.
|
|
83
|
+
*/
|
|
84
|
+
export function validateRunWorkflowSemantics(
|
|
85
|
+
run: WorkflowRun,
|
|
86
|
+
workflow: LoadedWorkflow,
|
|
87
|
+
): string | undefined {
|
|
88
|
+
const sameWorkflowDigest = run.workflowDigest === workflow.digest;
|
|
89
|
+
let expectedStepId = workflow.definition.start;
|
|
90
|
+
let reachedDone = false;
|
|
91
|
+
let boundWorkspaceCwd: string | undefined;
|
|
92
|
+
let latestApproval:
|
|
93
|
+
{ readonly artifact: string; readonly feedback: string } | undefined;
|
|
94
|
+
const expectedVisits: Record<string, number> = { [expectedStepId]: 1 };
|
|
95
|
+
|
|
96
|
+
for (let index = 0; index < run.history.length; index += 1) {
|
|
97
|
+
const entry = run.history[index];
|
|
98
|
+
if (!entry) continue;
|
|
99
|
+
if (reachedDone) {
|
|
100
|
+
return 'workflow history continues after a $done transition';
|
|
101
|
+
}
|
|
102
|
+
if (entry.stepId !== expectedStepId) {
|
|
103
|
+
return `history step "${entry.stepId}" is not reachable after "${expectedStepId}"`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const step = workflow.definition.steps[entry.stepId];
|
|
107
|
+
const configuredDigest = workflow.stepDigests[entry.stepId];
|
|
108
|
+
if (!step || entry.stepDigest !== configuredDigest) {
|
|
109
|
+
return sameWorkflowDigest
|
|
110
|
+
? `history step "${entry.stepId}" does not match the active workflow digest`
|
|
111
|
+
: undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const metadataError = validateHistoryMetadata(workflow, step, entry);
|
|
115
|
+
if (metadataError) return metadataError;
|
|
116
|
+
if (entry.workspaceCwd) {
|
|
117
|
+
if (
|
|
118
|
+
boundWorkspaceCwd !== undefined &&
|
|
119
|
+
boundWorkspaceCwd !== entry.workspaceCwd
|
|
120
|
+
) {
|
|
121
|
+
return 'workflow history attempts to replace an existing workspace binding';
|
|
122
|
+
}
|
|
123
|
+
boundWorkspaceCwd = entry.workspaceCwd;
|
|
124
|
+
}
|
|
125
|
+
if (entry.approval) {
|
|
126
|
+
latestApproval = {
|
|
127
|
+
artifact: entry.approval.artifact,
|
|
128
|
+
feedback: entry.approval.feedback,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const target = step.transitions[entry.outcome];
|
|
133
|
+
if (!target) {
|
|
134
|
+
return `history outcome "${entry.outcome}" is not configured for step "${entry.stepId}"`;
|
|
135
|
+
}
|
|
136
|
+
if (target === '$pause') {
|
|
137
|
+
return `history step "${entry.stepId}" records a non-completing $pause transition`;
|
|
138
|
+
}
|
|
139
|
+
if (target === '$done') {
|
|
140
|
+
if (index !== run.history.length - 1) {
|
|
141
|
+
return 'workflow history continues after a $done transition';
|
|
142
|
+
}
|
|
143
|
+
reachedDone = true;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
expectedStepId = target;
|
|
148
|
+
expectedVisits[target] = (expectedVisits[target] ?? 0) + 1;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (run.currentStepId !== expectedStepId) {
|
|
152
|
+
return `current step "${run.currentStepId}" does not match reachable step "${expectedStepId}"`;
|
|
153
|
+
}
|
|
154
|
+
if (reachedDone !== (run.status === 'completed')) {
|
|
155
|
+
return reachedDone
|
|
156
|
+
? 'a workflow that reached $done must be completed'
|
|
157
|
+
: 'a completed workflow has no $done transition in its history';
|
|
158
|
+
}
|
|
159
|
+
if (
|
|
160
|
+
run.status === 'completed' &&
|
|
161
|
+
run.currentStepDigest !== run.history.at(-1)?.stepDigest
|
|
162
|
+
) {
|
|
163
|
+
return 'completed workflow current-step digest does not match its terminal history';
|
|
164
|
+
}
|
|
165
|
+
if (!sameVisitCounts(run.visits, expectedVisits)) {
|
|
166
|
+
return 'workflow visit counts do not match its execution history';
|
|
167
|
+
}
|
|
168
|
+
const reviewedArtifact = run.reviewedArtifact ?? '';
|
|
169
|
+
const reviewedFeedback = run.reviewedFeedback ?? '';
|
|
170
|
+
if (
|
|
171
|
+
reviewedArtifact !== (latestApproval?.artifact ?? '') ||
|
|
172
|
+
reviewedFeedback !== (latestApproval?.feedback ?? '')
|
|
173
|
+
) {
|
|
174
|
+
return 'reviewed artifact and feedback do not match authoritative approval history';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const currentStep = workflow.definition.steps[run.currentStepId];
|
|
178
|
+
if (!currentStep) {
|
|
179
|
+
return `current step "${run.currentStepId}" is missing from the workflow`;
|
|
180
|
+
}
|
|
181
|
+
const currentStepChanged =
|
|
182
|
+
run.currentStepDigest !== workflow.stepDigests[run.currentStepId];
|
|
183
|
+
if (sameWorkflowDigest && currentStepChanged) {
|
|
184
|
+
return `current step "${run.currentStepId}" does not match the active workflow digest`;
|
|
185
|
+
}
|
|
186
|
+
if (boundWorkspaceCwd && !currentStep.subagent) {
|
|
187
|
+
return `bound workflow current step "${run.currentStepId}" must use a subagent`;
|
|
188
|
+
}
|
|
189
|
+
if (currentStepChanged) return undefined;
|
|
190
|
+
return validatePendingGate(run, currentStep);
|
|
191
|
+
}
|
|
@@ -1,13 +1,105 @@
|
|
|
1
1
|
export const RUN_STATE_VERSION = 1 as const;
|
|
2
|
+
export const MAX_RESUME_INPUT_CHARS = 16_000;
|
|
3
|
+
export const MAX_STEP_TRACE_TASK_CHARS = 64_000;
|
|
4
|
+
export const MAX_STEP_TRACE_ATTEMPTS = 16;
|
|
5
|
+
export const MAX_STEP_TRACE_SUMMARY_CHARS = 8_000;
|
|
6
|
+
export const MAX_STEP_TRACE_ARTIFACT_CHARS = 16_000;
|
|
7
|
+
export const MAX_STEP_TRACE_LOG_EVENTS = 400;
|
|
8
|
+
export const MAX_STEP_TRACE_LOG_EVENT_CHARS = 2_000;
|
|
9
|
+
export const MAX_STEP_TRACE_LOG_CHARS = 120_000;
|
|
10
|
+
export const MAX_WORKFLOW_TRACE_CHARS = 2_000_000;
|
|
2
11
|
|
|
3
12
|
export type WorkflowRunStatus =
|
|
4
13
|
'running' | 'paused' | 'awaiting-gate' | 'completed' | 'aborted';
|
|
5
14
|
|
|
15
|
+
export type GateApproval = {
|
|
16
|
+
/** Stable identity of the gate request that produced this approval. */
|
|
17
|
+
readonly requestId: string;
|
|
18
|
+
readonly artifact: string;
|
|
19
|
+
readonly feedback: string;
|
|
20
|
+
/** Prompt-excluded step structure approved by this gate request. */
|
|
21
|
+
readonly stepStructuralDigest: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type StepAttemptResult = {
|
|
25
|
+
readonly outcome: string;
|
|
26
|
+
readonly summary: string;
|
|
27
|
+
readonly summaryTruncated?: true | undefined;
|
|
28
|
+
readonly artifact?: string | undefined;
|
|
29
|
+
readonly artifactTruncated?: true | undefined;
|
|
30
|
+
readonly workspaceCwd?: string | undefined;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type StepGateDecision = {
|
|
34
|
+
readonly provider: 'prompt' | 'plannotator';
|
|
35
|
+
readonly requestId: string;
|
|
36
|
+
readonly approved: boolean;
|
|
37
|
+
readonly feedback: string;
|
|
38
|
+
readonly feedbackTruncated?: true | undefined;
|
|
39
|
+
readonly resolvedAt: number;
|
|
40
|
+
readonly reviewId?: string | undefined;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type SubagentTranscriptReference = {
|
|
44
|
+
/** Trusted parent-derived directory containing this child run. */
|
|
45
|
+
readonly trustedRoot: string;
|
|
46
|
+
/** Exact child session returned by the delegation protocol. */
|
|
47
|
+
readonly sessionFile: string;
|
|
48
|
+
/** Pi-subagents run identity used to confine the session path. */
|
|
49
|
+
readonly runId: string;
|
|
50
|
+
readonly childIndex: number;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type StepExecutionAttempt =
|
|
54
|
+
| {
|
|
55
|
+
readonly kind: 'main';
|
|
56
|
+
readonly requestId: string;
|
|
57
|
+
/** One-based attempt position within this step visit. */
|
|
58
|
+
readonly ordinal?: number | undefined;
|
|
59
|
+
/** Exact workflow task sent to the main agent. */
|
|
60
|
+
readonly task: string;
|
|
61
|
+
readonly taskTruncated?: true | undefined;
|
|
62
|
+
readonly omittedTaskChars?: number | undefined;
|
|
63
|
+
/** Redacted assistant/tool events retained from the start of this attempt. */
|
|
64
|
+
readonly log?: ReadonlyArray<string> | undefined;
|
|
65
|
+
/** Whether later events were omitted from the bounded prefix. */
|
|
66
|
+
readonly logTruncated?: true | undefined;
|
|
67
|
+
readonly omittedLogEvents?: number | undefined;
|
|
68
|
+
readonly startedAt: number;
|
|
69
|
+
readonly result?: StepAttemptResult | undefined;
|
|
70
|
+
readonly gateDecision?: StepGateDecision | undefined;
|
|
71
|
+
}
|
|
72
|
+
| {
|
|
73
|
+
readonly kind: 'subagent';
|
|
74
|
+
readonly requestId: string;
|
|
75
|
+
/** One-based attempt position within this step visit. */
|
|
76
|
+
readonly ordinal?: number | undefined;
|
|
77
|
+
readonly agent: string;
|
|
78
|
+
/** Exact task body sent after the private child-policy envelope. */
|
|
79
|
+
readonly task: string;
|
|
80
|
+
readonly taskTruncated?: true | undefined;
|
|
81
|
+
readonly omittedTaskChars?: number | undefined;
|
|
82
|
+
readonly startedAt: number;
|
|
83
|
+
readonly transcript?: SubagentTranscriptReference | undefined;
|
|
84
|
+
readonly result?: StepAttemptResult | undefined;
|
|
85
|
+
readonly gateDecision?: StepGateDecision | undefined;
|
|
86
|
+
};
|
|
87
|
+
|
|
6
88
|
export type StepHistoryEntry = {
|
|
7
89
|
readonly stepId: string;
|
|
8
90
|
readonly stepDigest: string;
|
|
9
91
|
readonly outcome: string;
|
|
10
92
|
readonly summary: string;
|
|
93
|
+
/** Canonical execution directory selected by this completed step. */
|
|
94
|
+
readonly workspaceCwd?: string | undefined;
|
|
95
|
+
/** Artifact projection paired exactly with `approval` on approved gates. */
|
|
96
|
+
readonly artifact?: string | undefined;
|
|
97
|
+
/** Complete approval provenance for a newly completed gate step. */
|
|
98
|
+
readonly approval?: GateApproval | undefined;
|
|
99
|
+
/** Durable execution evidence for every attempt of this completed step. */
|
|
100
|
+
readonly attempts?: ReadonlyArray<StepExecutionAttempt> | undefined;
|
|
101
|
+
/** Older attempts compacted to keep the checkpoint bounded. */
|
|
102
|
+
readonly omittedAttempts?: number | undefined;
|
|
11
103
|
readonly completedAt: number;
|
|
12
104
|
};
|
|
13
105
|
|
|
@@ -22,8 +114,8 @@ export type PendingGate = {
|
|
|
22
114
|
readonly requestId: string;
|
|
23
115
|
readonly stepId: string;
|
|
24
116
|
readonly artifact: string;
|
|
25
|
-
/**
|
|
26
|
-
summary?: string | undefined;
|
|
117
|
+
/** Compact step handoff kept separate from the opaque review artifact. */
|
|
118
|
+
readonly summary?: string | undefined;
|
|
27
119
|
readonly submittedOutcome: string;
|
|
28
120
|
readonly requestedAt: number;
|
|
29
121
|
readonly reviewId?: string | undefined;
|
|
@@ -42,13 +134,24 @@ export type WorkflowRun = {
|
|
|
42
134
|
readonly baselineTools: ReadonlyArray<string>;
|
|
43
135
|
readonly visits: Readonly<Record<string, number>>;
|
|
44
136
|
readonly history: ReadonlyArray<StepHistoryEntry>;
|
|
137
|
+
/** Attempts for the current, not-yet-completed step. */
|
|
138
|
+
readonly currentStepAttempts?:
|
|
139
|
+
ReadonlyArray<StepExecutionAttempt> | undefined;
|
|
140
|
+
/** Older current-step attempts compacted from the checkpoint. */
|
|
141
|
+
readonly currentStepOmittedAttempts?: number | undefined;
|
|
45
142
|
readonly startedAt: number;
|
|
46
143
|
readonly updatedAt: number;
|
|
47
144
|
/**
|
|
48
|
-
* Most recent human-approved gate artifact.
|
|
49
|
-
*
|
|
145
|
+
* Most recent human-approved gate artifact. The workflow prompt decides how
|
|
146
|
+
* to interpret this opaque value.
|
|
50
147
|
*/
|
|
51
148
|
readonly reviewedArtifact?: string | undefined;
|
|
149
|
+
/** Final feedback paired with the approved artifact. */
|
|
150
|
+
readonly reviewedFeedback?: string | undefined;
|
|
151
|
+
/** Immutable working directory captured when the workflow started. */
|
|
152
|
+
readonly startCwd?: string | undefined;
|
|
153
|
+
/** Current canonical execution directory for delegated workflow steps. */
|
|
154
|
+
readonly cwd?: string | undefined;
|
|
52
155
|
/**
|
|
53
156
|
* Input inherited from the previous completed step. Unlike `lastSummary`,
|
|
54
157
|
* this survives a paused attempt of the current step.
|
|
@@ -56,6 +159,8 @@ export type WorkflowRun = {
|
|
|
56
159
|
readonly stepHandoff?: string | undefined;
|
|
57
160
|
readonly lastSummary: string;
|
|
58
161
|
readonly gateFeedback: string;
|
|
162
|
+
/** User-authored guidance supplied for the current resume attempt. */
|
|
163
|
+
readonly resumeInput?: string | undefined;
|
|
59
164
|
readonly pauseReason?: string | undefined;
|
|
60
165
|
readonly pausedFrom?: 'running' | 'awaiting-gate' | undefined;
|
|
61
166
|
/** Current step when execution failed and was paused for a resumable retry. */
|
package/src/engine/state.ts
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
export { createRun } from './create-run.ts';
|
|
2
2
|
export { isWorkflowRun } from './run-validation.ts';
|
|
3
|
-
export {
|
|
3
|
+
export {
|
|
4
|
+
MAX_RESUME_INPUT_CHARS,
|
|
5
|
+
MAX_STEP_TRACE_ATTEMPTS,
|
|
6
|
+
MAX_STEP_TRACE_LOG_CHARS,
|
|
7
|
+
MAX_STEP_TRACE_LOG_EVENT_CHARS,
|
|
8
|
+
MAX_STEP_TRACE_LOG_EVENTS,
|
|
9
|
+
MAX_STEP_TRACE_TASK_CHARS,
|
|
10
|
+
MAX_WORKFLOW_TRACE_CHARS,
|
|
11
|
+
RUN_STATE_VERSION,
|
|
12
|
+
} from './state-types.ts';
|
|
4
13
|
export type {
|
|
14
|
+
GateApproval,
|
|
5
15
|
GateResolution,
|
|
6
16
|
PendingGate,
|
|
17
|
+
StepAttemptResult,
|
|
18
|
+
StepExecutionAttempt,
|
|
19
|
+
StepGateDecision,
|
|
7
20
|
StepHistoryEntry,
|
|
21
|
+
SubagentTranscriptReference,
|
|
8
22
|
WorkflowRun,
|
|
9
23
|
WorkflowRunStatus,
|
|
10
24
|
} from './state-types.ts';
|