@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.
Files changed (131) hide show
  1. package/README.md +312 -221
  2. package/agents/step.md +4 -11
  3. package/dist/index.js +2556 -961
  4. package/examples/mr-comments.workflow.yaml +18 -8
  5. package/examples/prompts/mr-comments/implement.md +13 -9
  6. package/examples/prompts/mr-comments/plan.md +44 -15
  7. package/examples/prompts/mr-comments/verify.md +8 -7
  8. package/examples/settings.yaml +7 -1
  9. package/examples/starter-kit/mr-comment.workflow.yaml +149 -0
  10. package/examples/starter-kit/mr-review.workflow.yaml +130 -0
  11. package/examples/starter-kit/steps/mr-comment/fetch.md +33 -0
  12. package/examples/starter-kit/steps/mr-comment/implement.md +38 -0
  13. package/examples/starter-kit/steps/mr-comment/plan.md +62 -0
  14. package/examples/starter-kit/steps/mr-comment/publish.md +36 -0
  15. package/examples/starter-kit/steps/mr-comment/verify.md +35 -0
  16. package/examples/starter-kit/steps/mr-review/fetch.md +25 -0
  17. package/examples/starter-kit/steps/mr-review/publish.md +30 -0
  18. package/examples/starter-kit/steps/mr-review/review.md +69 -0
  19. package/examples/starter-kit/steps/mr-review/verify.md +24 -0
  20. package/examples/starter-kit/steps/shared/prepare-workspace.md +57 -0
  21. package/examples/starter-kit/steps/ticket/implement.md +35 -0
  22. package/examples/starter-kit/steps/ticket/plan.md +40 -0
  23. package/examples/starter-kit/steps/ticket/verify.md +29 -0
  24. package/examples/starter-kit/steps/work/implement.md +34 -0
  25. package/examples/starter-kit/steps/work/plan.md +38 -0
  26. package/examples/starter-kit/steps/work/verify.md +30 -0
  27. package/examples/starter-kit/ticket.workflow.yaml +100 -0
  28. package/examples/starter-kit/work.workflow.yaml +97 -0
  29. package/package.json +3 -3
  30. package/schemas/workflow.schema.json +78 -35
  31. package/src/command-names.ts +1 -0
  32. package/src/commands.ts +33 -4
  33. package/src/config/ceiling.ts +6 -8
  34. package/src/config/load-workflows.ts +12 -1
  35. package/src/config/step-digests.ts +19 -0
  36. package/src/config/types.ts +17 -11
  37. package/src/config/validation/permissions.ts +5 -45
  38. package/src/config/validation/prompt.ts +3 -0
  39. package/src/config/validation/shared.ts +0 -2
  40. package/src/config/validation/step.ts +106 -2
  41. package/src/config/validation/workflow.ts +76 -0
  42. package/src/engine/create-run.ts +4 -0
  43. package/src/engine/gate-transitions.ts +58 -6
  44. package/src/engine/reconciliation-history.ts +61 -24
  45. package/src/engine/run-advance.ts +37 -4
  46. package/src/engine/run-lifecycle.ts +11 -0
  47. package/src/engine/run-reconciliation.ts +51 -21
  48. package/src/engine/run-validation.ts +236 -0
  49. package/src/engine/run-workflow-validation.ts +191 -0
  50. package/src/engine/state-types.ts +109 -4
  51. package/src/engine/state.ts +15 -1
  52. package/src/engine/step-trace.ts +495 -0
  53. package/src/engine/transitions.ts +1 -0
  54. package/src/harness/action-context.ts +31 -6
  55. package/src/harness/core-actions.ts +30 -1
  56. package/src/harness/delegation-control-actions.ts +27 -2
  57. package/src/harness/delegation-failure.ts +42 -9
  58. package/src/harness/delegation-plan.ts +96 -40
  59. package/src/harness/delegation-recovery-validation.ts +3 -1
  60. package/src/harness/delegation-response-actions.ts +73 -3
  61. package/src/harness/delegation-retry-policy.ts +5 -3
  62. package/src/harness/dependencies.ts +11 -2
  63. package/src/harness/gate-submission-action.ts +5 -31
  64. package/src/harness/lifecycle-actions.ts +4 -5
  65. package/src/harness/pause-actions.ts +10 -1
  66. package/src/harness/plannotator-result-actions.ts +20 -11
  67. package/src/harness/prompt-gate-actions.ts +40 -2
  68. package/src/harness/resume-action.ts +75 -1
  69. package/src/harness/start-actions.ts +77 -3
  70. package/src/harness/status-actions.ts +7 -17
  71. package/src/harness/step-effects.ts +61 -0
  72. package/src/harness/step-execution-actions.ts +181 -25
  73. package/src/harness/step-reporting.ts +168 -0
  74. package/src/harness/types.ts +1 -0
  75. package/src/harness/workspace-directory.ts +67 -0
  76. package/src/harness.ts +37 -9
  77. package/src/integrations/plannotator-requests.ts +40 -5
  78. package/src/integrations/subagents/child-policy-sections.ts +71 -72
  79. package/src/integrations/subagents/child-policy-types.ts +7 -7
  80. package/src/integrations/subagents/child-policy-validation.ts +10 -4
  81. package/src/integrations/subagents/child-runtime-completion.ts +1 -0
  82. package/src/integrations/subagents/child-runtime-files.ts +29 -86
  83. package/src/integrations/subagents/child-runtime-policy.ts +8 -33
  84. package/src/integrations/subagents/child-runtime.ts +3 -15
  85. package/src/integrations/subagents/delegated-result.ts +1 -0
  86. package/src/integrations/subagents/diagnostic-format.ts +5 -0
  87. package/src/integrations/subagents/diagnostic-types.ts +7 -1
  88. package/src/integrations/subagents/diagnostics.ts +2 -0
  89. package/src/integrations/subagents/failure-correlation.ts +52 -2
  90. package/src/integrations/subagents/failure-transcript.ts +60 -1
  91. package/src/integrations/subagents/replay-audit.ts +0 -1
  92. package/src/integrations/subagents/replay-safety.ts +12 -27
  93. package/src/integrations/subagents/session-diagnostics.ts +102 -3
  94. package/src/policy/bash-authorization.ts +2 -21
  95. package/src/policy/bash-types.ts +0 -5
  96. package/src/policy/bash.ts +1 -2
  97. package/src/policy/tool-call-authorization.ts +1 -7
  98. package/src/preflight.ts +0 -5
  99. package/src/prompt/retry-task.ts +11 -3
  100. package/src/prompt/step-contract.ts +10 -56
  101. package/src/prompt/step-sections.ts +8 -10
  102. package/src/prompt/step-task.ts +41 -10
  103. package/src/prompt/template.ts +5 -2
  104. package/src/runtime/completion-tool.ts +14 -0
  105. package/src/runtime/main-step-completion.ts +3 -0
  106. package/src/runtime/main-step-policy.ts +2 -1
  107. package/src/runtime/main-step-runtime-types.ts +9 -1
  108. package/src/runtime/main-step-runtime.ts +2 -0
  109. package/src/runtime/main-step-state.ts +6 -0
  110. package/src/runtime/main-step-trace.ts +45 -0
  111. package/src/runtime/step-result.ts +57 -0
  112. package/src/step-log.ts +178 -0
  113. package/src/workflow-doctor.ts +254 -0
  114. package/src/workflow-status/format-status.ts +32 -3
  115. package/src/workflow-status/render-board.ts +7 -4
  116. package/src/workflow-status/render-path.ts +51 -24
  117. package/src/workflow-status/render-step-detail.ts +379 -0
  118. package/src/workflow-status/render-summary.ts +22 -0
  119. package/src/workflow-status/transcript-reader.ts +231 -0
  120. package/src/workflow-status/types.ts +8 -0
  121. package/src/workflow-status/view.ts +161 -14
  122. package/src/workflow-status.ts +6 -0
  123. package/src/policy/approved-command-extraction.ts +0 -71
  124. package/src/policy/approved-commands.ts +0 -15
  125. package/src/policy/bash-read-only.ts +0 -161
  126. package/src/policy/restricted-git.ts +0 -30
  127. package/src/policy/reviewed-artifact.ts +0 -97
  128. package/src/policy/reviewed-command-safety.ts +0 -150
  129. package/src/policy/reviewed-command-shape.ts +0 -49
  130. package/src/policy/reviewed-repository-contract.ts +0 -138
  131. 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
- /** Legacy v1 field. New runs use the reviewed artifact as the handoff. */
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. This is the only provenance
49
- * source from which reviewed Bash capabilities may be derived.
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. */
@@ -1,10 +1,24 @@
1
1
  export { createRun } from './create-run.ts';
2
2
  export { isWorkflowRun } from './run-validation.ts';
3
- export { RUN_STATE_VERSION } from './state-types.ts';
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';