@wichayutdew/pi-workflows 0.3.0 → 1.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.
Files changed (144) hide show
  1. package/README.md +55 -39
  2. package/dist/index.js +5771 -4794
  3. package/examples/mr-comments.workflow.yaml +2 -0
  4. package/package.json +1 -1
  5. package/schemas/workflow.schema.json +10 -1
  6. package/src/command-names.ts +7 -1
  7. package/src/commands.ts +126 -52
  8. package/src/config/catalog.ts +162 -0
  9. package/src/config/ceiling.ts +120 -116
  10. package/src/config/command-conflicts.ts +4 -4
  11. package/src/config/diagnostics.ts +28 -0
  12. package/src/config/load-settings.ts +37 -0
  13. package/src/config/load-types.ts +53 -0
  14. package/src/config/load-workflows.ts +135 -0
  15. package/src/config/load.ts +43 -311
  16. package/src/config/types.ts +150 -137
  17. package/src/config/validate.ts +12 -1261
  18. package/src/config/validation/permissions.ts +312 -0
  19. package/src/config/validation/prompt.ts +21 -0
  20. package/src/config/validation/settings.ts +122 -0
  21. package/src/config/validation/shared.ts +129 -0
  22. package/src/config/validation/shortcut.ts +118 -0
  23. package/src/config/validation/step.ts +228 -0
  24. package/src/config/validation/subagent.ts +288 -0
  25. package/src/config/validation/workflow.ts +156 -0
  26. package/src/config/yaml.ts +29 -0
  27. package/src/digest.ts +38 -6
  28. package/src/engine/checkpoint.ts +13 -9
  29. package/src/engine/create-run.ts +43 -0
  30. package/src/engine/gate-transitions.ts +187 -0
  31. package/src/engine/reconciliation-history.ts +88 -0
  32. package/src/engine/resume.ts +19 -7
  33. package/src/engine/run-advance.ts +122 -0
  34. package/src/engine/run-lifecycle.ts +124 -0
  35. package/src/engine/run-reconciliation.ts +116 -0
  36. package/src/engine/run-validation.ts +129 -0
  37. package/src/engine/state-types.ts +66 -0
  38. package/src/engine/state.ts +10 -193
  39. package/src/engine/transition-helpers.ts +28 -0
  40. package/src/engine/transition-types.ts +8 -0
  41. package/src/engine/transitions.ts +17 -471
  42. package/src/harness/action-context.ts +181 -0
  43. package/src/harness/catalog.ts +45 -0
  44. package/src/harness/context-idle.ts +21 -0
  45. package/src/harness/core-actions.ts +299 -0
  46. package/src/harness/delegation-control-actions.ts +250 -0
  47. package/src/harness/delegation-failure.ts +215 -0
  48. package/src/harness/delegation-plan.ts +219 -0
  49. package/src/harness/delegation-recovery-validation.ts +159 -0
  50. package/src/harness/delegation-response-actions.ts +295 -0
  51. package/src/harness/delegation-retry-policy.ts +118 -0
  52. package/src/harness/dependencies.ts +165 -0
  53. package/src/harness/gate-submission-action.ts +154 -0
  54. package/src/harness/lifecycle-actions.ts +154 -0
  55. package/src/harness/pause-actions.ts +120 -0
  56. package/src/harness/plannotator-result-actions.ts +123 -0
  57. package/src/harness/prompt-gate-actions.ts +270 -0
  58. package/src/harness/resume-action.ts +261 -0
  59. package/src/harness/start-actions.ts +183 -0
  60. package/src/harness/status-actions.ts +191 -0
  61. package/src/harness/step-execution-actions.ts +283 -0
  62. package/src/harness/types.ts +76 -0
  63. package/src/harness.ts +236 -2303
  64. package/src/index.ts +73 -11
  65. package/src/integrations/plannotator-requests.ts +127 -0
  66. package/src/integrations/plannotator-responses.ts +141 -0
  67. package/src/integrations/plannotator-types.ts +44 -0
  68. package/src/integrations/plannotator.ts +16 -235
  69. package/src/integrations/prompt-gate.ts +17 -10
  70. package/src/integrations/subagents/child-policy-envelope.ts +119 -0
  71. package/src/integrations/subagents/child-policy-paths.ts +66 -0
  72. package/src/integrations/subagents/child-policy-sections.ts +186 -0
  73. package/src/integrations/subagents/child-policy-types.ts +32 -0
  74. package/src/integrations/subagents/child-policy-validation.ts +163 -0
  75. package/src/integrations/subagents/child-runtime-completion.ts +56 -0
  76. package/src/integrations/subagents/child-runtime-dependencies.ts +47 -0
  77. package/src/integrations/subagents/child-runtime-files.ts +156 -0
  78. package/src/integrations/subagents/child-runtime-policy.ts +83 -0
  79. package/src/integrations/subagents/child-runtime-types.ts +30 -0
  80. package/src/integrations/subagents/child-runtime.ts +140 -301
  81. package/src/integrations/subagents/client-delegation.ts +181 -0
  82. package/src/integrations/subagents/client-messages.ts +66 -0
  83. package/src/integrations/subagents/client-types.ts +36 -0
  84. package/src/integrations/subagents/client.ts +127 -219
  85. package/src/integrations/subagents/delegated-result.ts +31 -0
  86. package/src/integrations/subagents/diagnostic-format.ts +40 -0
  87. package/src/integrations/subagents/diagnostic-text.ts +114 -0
  88. package/src/integrations/subagents/diagnostic-types.ts +77 -0
  89. package/src/integrations/subagents/diagnostics.ts +24 -977
  90. package/src/integrations/subagents/failure-correlation.ts +235 -0
  91. package/src/integrations/subagents/failure-transcript.ts +192 -0
  92. package/src/integrations/subagents/hidden-bash-failure.ts +98 -0
  93. package/src/integrations/subagents/protocol-events.ts +27 -0
  94. package/src/integrations/subagents/protocol.ts +30 -375
  95. package/src/integrations/subagents/replay-audit.ts +147 -0
  96. package/src/integrations/subagents/replay-safety.ts +82 -0
  97. package/src/integrations/subagents/session-diagnostics.ts +258 -0
  98. package/src/policy/approved-command-extraction.ts +71 -0
  99. package/src/policy/approved-commands.ts +15 -432
  100. package/src/policy/bash-authorization.ts +86 -0
  101. package/src/policy/bash-read-only.ts +161 -0
  102. package/src/policy/bash-types.ts +20 -0
  103. package/src/policy/bash.ts +4 -346
  104. package/src/policy/completion-batch.ts +21 -16
  105. package/src/policy/immutable-input.ts +5 -2
  106. package/src/policy/mcp-authorization.ts +71 -0
  107. package/src/policy/restricted-command.ts +141 -0
  108. package/src/policy/restricted-git.ts +30 -0
  109. package/src/policy/reviewed-artifact.ts +97 -0
  110. package/src/policy/reviewed-command-safety.ts +150 -0
  111. package/src/policy/reviewed-command-shape.ts +49 -0
  112. package/src/policy/reviewed-repository-contract.ts +138 -0
  113. package/src/policy/reviewed-repository-cwd.ts +102 -0
  114. package/src/policy/tool-call-authorization.ts +58 -0
  115. package/src/policy/tool-selection.ts +72 -0
  116. package/src/policy/tool-types.ts +14 -0
  117. package/src/policy/tools.ts +11 -150
  118. package/src/preflight.ts +96 -61
  119. package/src/prompt/main-workflow-notice.ts +41 -0
  120. package/src/prompt/retry-task.ts +83 -0
  121. package/src/prompt/step-contract.ts +101 -0
  122. package/src/prompt/step-sections.ts +75 -0
  123. package/src/prompt/step-task.ts +156 -0
  124. package/src/prompt/template.ts +83 -0
  125. package/src/prompt.ts +10 -254
  126. package/src/runtime/completion-tool.ts +3 -0
  127. package/src/runtime/main-step-completion.ts +78 -0
  128. package/src/runtime/main-step-lifecycle.ts +37 -0
  129. package/src/runtime/main-step-policy.ts +84 -0
  130. package/src/runtime/main-step-runtime-types.ts +64 -0
  131. package/src/runtime/main-step-runtime.ts +129 -200
  132. package/src/runtime/main-step-state.ts +85 -0
  133. package/src/runtime/serial-task-queue.ts +48 -13
  134. package/src/runtime/step-result.ts +48 -27
  135. package/src/workflow-list.ts +15 -6
  136. package/src/workflow-status/format-status.ts +52 -0
  137. package/src/workflow-status/formatting.ts +178 -0
  138. package/src/workflow-status/layout.ts +109 -0
  139. package/src/workflow-status/render-board.ts +101 -0
  140. package/src/workflow-status/render-path.ts +123 -0
  141. package/src/workflow-status/render-summary.ts +144 -0
  142. package/src/workflow-status/types.ts +40 -0
  143. package/src/workflow-status/view.ts +273 -0
  144. package/src/workflow-status.ts +20 -820
@@ -0,0 +1,228 @@
1
+ import type { PromptSpec, WorkflowGate, WorkflowStep } from '../types.ts';
2
+ import { parsePermissions, parseRequirements } from './permissions.ts';
3
+ import {
4
+ isJsonObject,
5
+ OUTCOME_PATTERN,
6
+ readInteger,
7
+ readString,
8
+ rejectUnknownKeys,
9
+ type ValidationErrors,
10
+ } from './shared.ts';
11
+ import { parseStepSubagent } from './subagent.ts';
12
+
13
+ function parsePrompt(
14
+ value: unknown,
15
+ path: string,
16
+ errors: ValidationErrors,
17
+ ): PromptSpec | undefined {
18
+ if (typeof value === 'string') {
19
+ const inline = readString(value, path, errors);
20
+ return inline ? { inline } : undefined;
21
+ }
22
+ if (!isJsonObject(value)) {
23
+ errors.push(`${path}: expected a string or an object`);
24
+ return undefined;
25
+ }
26
+ rejectUnknownKeys(value, ['file'], path, errors);
27
+ const file = readString(value.file, `${path}.file`, errors);
28
+ if (!file) return undefined;
29
+ if (file.startsWith('/') || file.includes('\0')) {
30
+ errors.push(`${path}.file: expected a safe relative path`);
31
+ return undefined;
32
+ }
33
+ return { file };
34
+ }
35
+
36
+ function parseTransitions(
37
+ value: unknown,
38
+ path: string,
39
+ errors: ValidationErrors,
40
+ ): Record<string, string> {
41
+ if (!isJsonObject(value)) {
42
+ errors.push(`${path}: expected an object`);
43
+ return {};
44
+ }
45
+ const transitions = Object.entries(value).reduce<Record<string, string>>(
46
+ (result, [outcome, targetValue]) => {
47
+ if (!OUTCOME_PATTERN.test(outcome)) {
48
+ errors.push(`${path}: invalid outcome "${outcome}"`);
49
+ return result;
50
+ }
51
+ const target = readString(targetValue, `${path}.${outcome}`, errors);
52
+ return target ? { ...result, [outcome]: target } : result;
53
+ },
54
+ {},
55
+ );
56
+ if (Object.keys(transitions).length === 0) {
57
+ errors.push(`${path}: at least one transition is required`);
58
+ }
59
+ return transitions;
60
+ }
61
+
62
+ function parseGate(
63
+ value: unknown,
64
+ path: string,
65
+ errors: ValidationErrors,
66
+ ): WorkflowGate | undefined {
67
+ if (value === undefined) return undefined;
68
+ if (!isJsonObject(value)) {
69
+ errors.push(`${path}: expected an object`);
70
+ return undefined;
71
+ }
72
+ rejectUnknownKeys(
73
+ value,
74
+ [
75
+ 'provider',
76
+ 'submitOutcome',
77
+ 'approvedOutcome',
78
+ 'rejectedOutcome',
79
+ 'timeoutMs',
80
+ ],
81
+ path,
82
+ errors,
83
+ );
84
+
85
+ const providerValue =
86
+ value.provider === undefined
87
+ ? 'prompt'
88
+ : readString(value.provider, `${path}.provider`, errors);
89
+ const provider =
90
+ providerValue === 'prompt' || providerValue === 'plannotator'
91
+ ? providerValue
92
+ : undefined;
93
+ if (!provider) {
94
+ errors.push(`${path}.provider: expected prompt or plannotator`);
95
+ }
96
+ const submitOutcome = readString(
97
+ value.submitOutcome,
98
+ `${path}.submitOutcome`,
99
+ errors,
100
+ { pattern: OUTCOME_PATTERN },
101
+ );
102
+ const approvedOutcome = readString(
103
+ value.approvedOutcome,
104
+ `${path}.approvedOutcome`,
105
+ errors,
106
+ { pattern: OUTCOME_PATTERN },
107
+ );
108
+ const rejectedOutcome = readString(
109
+ value.rejectedOutcome,
110
+ `${path}.rejectedOutcome`,
111
+ errors,
112
+ { pattern: OUTCOME_PATTERN },
113
+ );
114
+ if (provider === 'prompt' && value.timeoutMs !== undefined) {
115
+ errors.push(`${path}.timeoutMs: only valid with provider "plannotator"`);
116
+ }
117
+ if (!submitOutcome || !approvedOutcome || !rejectedOutcome || !provider) {
118
+ return undefined;
119
+ }
120
+ if (approvedOutcome === rejectedOutcome) {
121
+ errors.push(`${path}: approvedOutcome and rejectedOutcome must differ`);
122
+ }
123
+
124
+ return provider === 'prompt'
125
+ ? {
126
+ provider,
127
+ submitOutcome,
128
+ approvedOutcome,
129
+ rejectedOutcome,
130
+ }
131
+ : {
132
+ provider,
133
+ submitOutcome,
134
+ approvedOutcome,
135
+ rejectedOutcome,
136
+ timeoutMs: readInteger(
137
+ value.timeoutMs,
138
+ 5_000,
139
+ `${path}.timeoutMs`,
140
+ errors,
141
+ { min: 1_000, max: 30_000 },
142
+ ),
143
+ };
144
+ }
145
+
146
+ /** Parse one workflow step and validate relationships within that step. */
147
+ export function parseWorkflowStep(
148
+ value: unknown,
149
+ stepId: string,
150
+ path: string,
151
+ errors: ValidationErrors,
152
+ ): WorkflowStep | undefined {
153
+ if (!isJsonObject(value)) {
154
+ errors.push(`${path}: expected an object`);
155
+ return undefined;
156
+ }
157
+ rejectUnknownKeys(
158
+ value,
159
+ [
160
+ 'title',
161
+ 'prompt',
162
+ 'subagent',
163
+ 'permissions',
164
+ 'requires',
165
+ 'transitions',
166
+ 'gate',
167
+ ],
168
+ path,
169
+ errors,
170
+ );
171
+
172
+ const title =
173
+ value.title === undefined
174
+ ? stepId
175
+ : readString(value.title, `${path}.title`, errors);
176
+ const prompt = parsePrompt(value.prompt, `${path}.prompt`, errors);
177
+ const subagent = parseStepSubagent(
178
+ value.subagent,
179
+ `${path}.subagent`,
180
+ errors,
181
+ );
182
+ const permissions = parsePermissions(
183
+ value.permissions,
184
+ `${path}.permissions`,
185
+ errors,
186
+ );
187
+ const requires = parseRequirements(
188
+ value.requires,
189
+ permissions,
190
+ `${path}.requires`,
191
+ errors,
192
+ );
193
+ const transitions = parseTransitions(
194
+ value.transitions,
195
+ `${path}.transitions`,
196
+ errors,
197
+ );
198
+ const gate = parseGate(value.gate, `${path}.gate`, errors);
199
+
200
+ if (gate) {
201
+ if (!Object.hasOwn(transitions, gate.approvedOutcome)) {
202
+ errors.push(
203
+ `${path}.transitions: missing gate outcome "${gate.approvedOutcome}"`,
204
+ );
205
+ }
206
+ if (!Object.hasOwn(transitions, gate.rejectedOutcome)) {
207
+ errors.push(
208
+ `${path}.transitions: missing gate outcome "${gate.rejectedOutcome}"`,
209
+ );
210
+ }
211
+ if (Object.hasOwn(transitions, gate.submitOutcome)) {
212
+ errors.push(
213
+ `${path}.transitions: submitOutcome is handled by the gate and must not be a transition`,
214
+ );
215
+ }
216
+ }
217
+
218
+ if (!title || !prompt) return undefined;
219
+ return {
220
+ title,
221
+ prompt,
222
+ ...(subagent ? { subagent } : {}),
223
+ permissions,
224
+ requires,
225
+ transitions,
226
+ ...(gate ? { gate } : {}),
227
+ };
228
+ }
@@ -0,0 +1,288 @@
1
+ import {
2
+ DEFAULT_STEP_SUBAGENT,
3
+ SUBAGENT_RUNTIME_NAME_PATTERN,
4
+ type StepSubagent,
5
+ type SubagentContext,
6
+ type SubagentPermissionCeiling,
7
+ type SubagentToolBudget,
8
+ type SubagentTurnBudget,
9
+ } from '../types.ts';
10
+ import {
11
+ isJsonObject,
12
+ readBoolean,
13
+ readInteger,
14
+ readString,
15
+ readStringList,
16
+ rejectUnknownKeys,
17
+ RESOURCE_SELECTOR_PATTERN,
18
+ TOOL_PATTERN,
19
+ type ValidationErrors,
20
+ } from './shared.ts';
21
+
22
+ function parseSubagentTurnBudget(
23
+ value: unknown,
24
+ path: string,
25
+ errors: ValidationErrors,
26
+ ): SubagentTurnBudget | undefined {
27
+ if (value === undefined) return undefined;
28
+ if (!isJsonObject(value)) {
29
+ errors.push(`${path}: expected an object`);
30
+ return undefined;
31
+ }
32
+ rejectUnknownKeys(value, ['maxTurns', 'graceTurns'], path, errors);
33
+ const maxTurns = readInteger(value.maxTurns, 0, `${path}.maxTurns`, errors, {
34
+ min: 1,
35
+ max: 1_000,
36
+ });
37
+ const graceTurns =
38
+ value.graceTurns === undefined
39
+ ? undefined
40
+ : readInteger(value.graceTurns, 0, `${path}.graceTurns`, errors, {
41
+ min: 0,
42
+ max: 100,
43
+ });
44
+ if (maxTurns === 0) return undefined;
45
+ return {
46
+ maxTurns,
47
+ ...(graceTurns !== undefined ? { graceTurns } : {}),
48
+ };
49
+ }
50
+
51
+ function parseSubagentToolBudget(
52
+ value: unknown,
53
+ path: string,
54
+ errors: ValidationErrors,
55
+ ): SubagentToolBudget | undefined {
56
+ if (value === undefined) return undefined;
57
+ if (!isJsonObject(value)) {
58
+ errors.push(`${path}: expected an object`);
59
+ return undefined;
60
+ }
61
+ rejectUnknownKeys(value, ['soft', 'hard', 'block'], path, errors);
62
+ const hard = readInteger(value.hard, 0, `${path}.hard`, errors, {
63
+ min: 1,
64
+ max: 100_000,
65
+ });
66
+ const soft =
67
+ value.soft === undefined
68
+ ? undefined
69
+ : readInteger(value.soft, 0, `${path}.soft`, errors, {
70
+ min: 1,
71
+ max: 100_000,
72
+ });
73
+
74
+ let block: Array<string> | '*' | undefined;
75
+ if (value.block === '*') {
76
+ block = '*';
77
+ } else if (value.block !== undefined) {
78
+ block = readStringList(value.block, `${path}.block`, errors, TOOL_PATTERN);
79
+ if (block.length === 0) {
80
+ errors.push(`${path}.block: expected "*" or at least one tool name`);
81
+ }
82
+ }
83
+ if (soft !== undefined && hard > 0 && soft > hard) {
84
+ errors.push(`${path}.soft: must not exceed hard`);
85
+ }
86
+ if (hard === 0) return undefined;
87
+ return {
88
+ hard,
89
+ ...(soft !== undefined ? { soft } : {}),
90
+ ...(block !== undefined ? { block } : {}),
91
+ };
92
+ }
93
+
94
+ /** Parse an optional isolated-step subagent configuration. */
95
+ export function parseStepSubagent(
96
+ value: unknown,
97
+ path: string,
98
+ errors: ValidationErrors,
99
+ ): StepSubagent | undefined {
100
+ if (value === undefined) return undefined;
101
+ if (typeof value === 'string') {
102
+ const agent =
103
+ readString(value, path, errors, {
104
+ pattern: SUBAGENT_RUNTIME_NAME_PATTERN,
105
+ }) ?? DEFAULT_STEP_SUBAGENT.agent;
106
+ return { ...DEFAULT_STEP_SUBAGENT, agent };
107
+ }
108
+ if (!isJsonObject(value)) {
109
+ errors.push(`${path}: expected an agent profile name or object`);
110
+ return undefined;
111
+ }
112
+ rejectUnknownKeys(
113
+ value,
114
+ [
115
+ 'agent',
116
+ 'context',
117
+ 'model',
118
+ 'timeoutMs',
119
+ 'turnBudget',
120
+ 'toolBudget',
121
+ 'artifacts',
122
+ 'retryToolFailures',
123
+ ],
124
+ path,
125
+ errors,
126
+ );
127
+
128
+ const agent =
129
+ value.agent === undefined
130
+ ? DEFAULT_STEP_SUBAGENT.agent
131
+ : (readString(value.agent, `${path}.agent`, errors, {
132
+ pattern: SUBAGENT_RUNTIME_NAME_PATTERN,
133
+ }) ?? DEFAULT_STEP_SUBAGENT.agent);
134
+ const contextValue =
135
+ value.context === undefined
136
+ ? DEFAULT_STEP_SUBAGENT.context
137
+ : readString(value.context, `${path}.context`, errors);
138
+ const context: SubagentContext = DEFAULT_STEP_SUBAGENT.context;
139
+ if (contextValue !== 'fresh') {
140
+ errors.push(`${path}.context: expected fresh`);
141
+ }
142
+ const model =
143
+ value.model === undefined
144
+ ? undefined
145
+ : readString(value.model, `${path}.model`, errors, {
146
+ pattern: RESOURCE_SELECTOR_PATTERN,
147
+ });
148
+ const timeoutMs = readInteger(
149
+ value.timeoutMs,
150
+ DEFAULT_STEP_SUBAGENT.timeoutMs,
151
+ `${path}.timeoutMs`,
152
+ errors,
153
+ { min: 1_000, max: 86_400_000 },
154
+ );
155
+ const turnBudget = parseSubagentTurnBudget(
156
+ value.turnBudget,
157
+ `${path}.turnBudget`,
158
+ errors,
159
+ );
160
+ const toolBudget = parseSubagentToolBudget(
161
+ value.toolBudget,
162
+ `${path}.toolBudget`,
163
+ errors,
164
+ );
165
+ const artifacts = readBoolean(
166
+ value.artifacts,
167
+ DEFAULT_STEP_SUBAGENT.artifacts,
168
+ `${path}.artifacts`,
169
+ errors,
170
+ );
171
+ const retryToolFailures = readBoolean(
172
+ value.retryToolFailures,
173
+ DEFAULT_STEP_SUBAGENT.retryToolFailures,
174
+ `${path}.retryToolFailures`,
175
+ errors,
176
+ );
177
+ return {
178
+ agent,
179
+ context,
180
+ ...(model ? { model } : {}),
181
+ timeoutMs,
182
+ ...(turnBudget ? { turnBudget } : {}),
183
+ ...(toolBudget ? { toolBudget } : {}),
184
+ artifacts,
185
+ retryToolFailures,
186
+ };
187
+ }
188
+
189
+ /** Parse the subagent limits inside the user permission ceiling. */
190
+ export function parseSubagentPermissionCeiling(
191
+ value: unknown,
192
+ path: string,
193
+ errors: ValidationErrors,
194
+ ): SubagentPermissionCeiling | undefined {
195
+ if (!isJsonObject(value)) {
196
+ errors.push(`${path}: expected an object`);
197
+ return undefined;
198
+ }
199
+ rejectUnknownKeys(
200
+ value,
201
+ [
202
+ 'agents',
203
+ 'contexts',
204
+ 'models',
205
+ 'maxTimeoutMs',
206
+ 'maxTurns',
207
+ 'maxGraceTurns',
208
+ 'maxToolCalls',
209
+ 'artifacts',
210
+ 'retryToolFailures',
211
+ ],
212
+ path,
213
+ errors,
214
+ );
215
+ const agents = readStringList(
216
+ value.agents,
217
+ `${path}.agents`,
218
+ errors,
219
+ SUBAGENT_RUNTIME_NAME_PATTERN,
220
+ );
221
+ const contexts = readStringList(
222
+ value.contexts,
223
+ `${path}.contexts`,
224
+ errors,
225
+ /^fresh$/,
226
+ ).filter((context): context is SubagentContext => context === 'fresh');
227
+ const models = readStringList(
228
+ value.models,
229
+ `${path}.models`,
230
+ errors,
231
+ RESOURCE_SELECTOR_PATTERN,
232
+ );
233
+ if (agents.length === 0) {
234
+ errors.push(`${path}.agents: at least one subagent is required`);
235
+ }
236
+ if (contexts.length === 0) {
237
+ errors.push(`${path}.contexts: at least one context mode is required`);
238
+ }
239
+ (
240
+ [
241
+ 'maxTimeoutMs',
242
+ 'maxTurns',
243
+ 'maxGraceTurns',
244
+ 'maxToolCalls',
245
+ 'artifacts',
246
+ ] as const
247
+ )
248
+ .filter((field) => value[field] === undefined)
249
+ .forEach((field) => errors.push(`${path}.${field}: required`));
250
+
251
+ return {
252
+ agents,
253
+ contexts,
254
+ models,
255
+ maxTimeoutMs: readInteger(
256
+ value.maxTimeoutMs,
257
+ 0,
258
+ `${path}.maxTimeoutMs`,
259
+ errors,
260
+ { min: 1_000, max: 86_400_000 },
261
+ ),
262
+ maxTurns: readInteger(value.maxTurns, 0, `${path}.maxTurns`, errors, {
263
+ min: 1,
264
+ max: 1_000,
265
+ }),
266
+ maxGraceTurns: readInteger(
267
+ value.maxGraceTurns,
268
+ 0,
269
+ `${path}.maxGraceTurns`,
270
+ errors,
271
+ { min: 0, max: 100 },
272
+ ),
273
+ maxToolCalls: readInteger(
274
+ value.maxToolCalls,
275
+ 0,
276
+ `${path}.maxToolCalls`,
277
+ errors,
278
+ { min: 1, max: 100_000 },
279
+ ),
280
+ artifacts: readBoolean(value.artifacts, false, `${path}.artifacts`, errors),
281
+ retryToolFailures: readBoolean(
282
+ value.retryToolFailures,
283
+ false,
284
+ `${path}.retryToolFailures`,
285
+ errors,
286
+ ),
287
+ };
288
+ }
@@ -0,0 +1,156 @@
1
+ import { RESERVED_COMMAND_NAMES } from '../../command-names.ts';
2
+ import {
3
+ WORKFLOW_SCHEMA_VERSION,
4
+ type WorkflowDefinition,
5
+ type WorkflowStep,
6
+ } from '../types.ts';
7
+ import {
8
+ IDENTIFIER_PATTERN,
9
+ isJsonObject,
10
+ readInteger,
11
+ readString,
12
+ rejectUnknownKeys,
13
+ type ValidationErrors,
14
+ type ValidationResult,
15
+ } from './shared.ts';
16
+ import { parseWorkflowStep } from './step.ts';
17
+
18
+ function parseSteps(
19
+ value: unknown,
20
+ errors: ValidationErrors,
21
+ ): Record<string, WorkflowStep> {
22
+ if (!isJsonObject(value)) {
23
+ errors.push('workflow.steps: expected an object');
24
+ return {};
25
+ }
26
+
27
+ const steps = Object.entries(value).reduce<Record<string, WorkflowStep>>(
28
+ (result, [stepId, stepValue]) => {
29
+ if (!IDENTIFIER_PATTERN.test(stepId)) {
30
+ errors.push(`workflow.steps: invalid step id "${stepId}"`);
31
+ return result;
32
+ }
33
+ const step = parseWorkflowStep(
34
+ stepValue,
35
+ stepId,
36
+ `workflow.steps.${stepId}`,
37
+ errors,
38
+ );
39
+ return step ? { ...result, [stepId]: step } : result;
40
+ },
41
+ {},
42
+ );
43
+ if (Object.keys(steps).length === 0) {
44
+ errors.push('workflow.steps: at least one step is required');
45
+ }
46
+ return steps;
47
+ }
48
+
49
+ function validateGraph(
50
+ steps: Readonly<Record<string, WorkflowStep>>,
51
+ start: string | undefined,
52
+ errors: ValidationErrors,
53
+ ): void {
54
+ if (start && !Object.hasOwn(steps, start)) {
55
+ errors.push(`workflow.start: unknown step "${start}"`);
56
+ }
57
+ Object.entries(steps).forEach(([stepId, step]) => {
58
+ Object.entries(step.transitions).forEach(([outcome, target]) => {
59
+ if (
60
+ target !== '$done' &&
61
+ target !== '$pause' &&
62
+ !Object.hasOwn(steps, target)
63
+ ) {
64
+ errors.push(
65
+ `workflow.steps.${stepId}.transitions.${outcome}: unknown target "${target}"`,
66
+ );
67
+ }
68
+ });
69
+ });
70
+ }
71
+
72
+ /** Validate and normalize an untrusted workflow definition. */
73
+ export function validateWorkflow(
74
+ value: unknown,
75
+ ): ValidationResult<WorkflowDefinition> {
76
+ const errors: ValidationErrors = [];
77
+ if (!isJsonObject(value)) {
78
+ return { errors: ['workflow: expected an object'] };
79
+ }
80
+ rejectUnknownKeys(
81
+ value,
82
+ [
83
+ '$schema',
84
+ 'version',
85
+ 'id',
86
+ 'command',
87
+ 'description',
88
+ 'start',
89
+ 'maxStepVisits',
90
+ 'summaryMaxChars',
91
+ 'steps',
92
+ ],
93
+ 'workflow',
94
+ errors,
95
+ );
96
+ if (value.$schema !== undefined && typeof value.$schema !== 'string') {
97
+ errors.push('workflow.$schema: expected a string');
98
+ }
99
+ if (value.version !== WORKFLOW_SCHEMA_VERSION) {
100
+ errors.push(`workflow.version: expected ${WORKFLOW_SCHEMA_VERSION}`);
101
+ }
102
+
103
+ const id = readString(value.id, 'workflow.id', errors, {
104
+ pattern: IDENTIFIER_PATTERN,
105
+ });
106
+ const command = readString(value.command, 'workflow.command', errors, {
107
+ pattern: IDENTIFIER_PATTERN,
108
+ });
109
+ const description = readString(
110
+ value.description,
111
+ 'workflow.description',
112
+ errors,
113
+ );
114
+ const start = readString(value.start, 'workflow.start', errors, {
115
+ pattern: IDENTIFIER_PATTERN,
116
+ });
117
+ const maxStepVisits = readInteger(
118
+ value.maxStepVisits,
119
+ 5,
120
+ 'workflow.maxStepVisits',
121
+ errors,
122
+ { min: 1, max: 100 },
123
+ );
124
+ const summaryMaxChars = readInteger(
125
+ value.summaryMaxChars,
126
+ 4_000,
127
+ 'workflow.summaryMaxChars',
128
+ errors,
129
+ { min: 100, max: 50_000 },
130
+ );
131
+
132
+ if (command && RESERVED_COMMAND_NAMES.has(command)) {
133
+ errors.push(
134
+ `workflow.command: "${command}" is reserved by Pi or the harness`,
135
+ );
136
+ }
137
+
138
+ const steps = parseSteps(value.steps, errors);
139
+ validateGraph(steps, start, errors);
140
+ if (errors.length > 0 || !id || !command || !description || !start) {
141
+ return { errors };
142
+ }
143
+ return {
144
+ value: {
145
+ version: WORKFLOW_SCHEMA_VERSION,
146
+ id,
147
+ command,
148
+ description,
149
+ start,
150
+ maxStepVisits,
151
+ summaryMaxChars,
152
+ steps,
153
+ },
154
+ errors,
155
+ };
156
+ }
@@ -0,0 +1,29 @@
1
+ import { parseDocument } from 'yaml';
2
+
3
+ export type YamlDocumentKind = 'settings' | 'workflow';
4
+
5
+ /** Parse one strict, single-document YAML 1.2 configuration value. */
6
+ export function parseYaml(text: string, kind: YamlDocumentKind): unknown {
7
+ const document = parseDocument(text, {
8
+ customTags: [],
9
+ merge: false,
10
+ prettyErrors: true,
11
+ resolveKnownTags: false,
12
+ schema: 'core',
13
+ strict: true,
14
+ stringKeys: true,
15
+ uniqueKeys: true,
16
+ version: '1.2',
17
+ });
18
+ const issues = [...document.errors, ...document.warnings];
19
+ if (issues.length > 0) {
20
+ throw new Error(issues.map((issue) => issue.message).join('\n'));
21
+ }
22
+ if (
23
+ document.directives.yaml.explicit &&
24
+ document.directives.yaml.version !== '1.2'
25
+ ) {
26
+ throw new Error(`${kind} YAML must use version 1.2`);
27
+ }
28
+ return document.toJS({ maxAliasCount: 100 }) as unknown;
29
+ }