@wichayutdew/pi-workflows 0.3.0 → 1.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 (142) hide show
  1. package/README.md +53 -39
  2. package/dist/index.js +5754 -4794
  3. package/package.json +1 -1
  4. package/src/command-names.ts +7 -1
  5. package/src/commands.ts +126 -52
  6. package/src/config/catalog.ts +162 -0
  7. package/src/config/ceiling.ts +116 -116
  8. package/src/config/command-conflicts.ts +4 -4
  9. package/src/config/diagnostics.ts +28 -0
  10. package/src/config/load-settings.ts +37 -0
  11. package/src/config/load-types.ts +53 -0
  12. package/src/config/load-workflows.ts +135 -0
  13. package/src/config/load.ts +43 -311
  14. package/src/config/types.ts +137 -135
  15. package/src/config/validate.ts +12 -1261
  16. package/src/config/validation/permissions.ts +291 -0
  17. package/src/config/validation/prompt.ts +19 -0
  18. package/src/config/validation/settings.ts +122 -0
  19. package/src/config/validation/shared.ts +129 -0
  20. package/src/config/validation/shortcut.ts +118 -0
  21. package/src/config/validation/step.ts +228 -0
  22. package/src/config/validation/subagent.ts +288 -0
  23. package/src/config/validation/workflow.ts +156 -0
  24. package/src/config/yaml.ts +29 -0
  25. package/src/digest.ts +38 -6
  26. package/src/engine/checkpoint.ts +13 -9
  27. package/src/engine/create-run.ts +42 -0
  28. package/src/engine/gate-transitions.ts +184 -0
  29. package/src/engine/reconciliation-history.ts +88 -0
  30. package/src/engine/resume.ts +19 -7
  31. package/src/engine/run-advance.ts +122 -0
  32. package/src/engine/run-lifecycle.ts +124 -0
  33. package/src/engine/run-reconciliation.ts +116 -0
  34. package/src/engine/run-validation.ts +128 -0
  35. package/src/engine/state-types.ts +64 -0
  36. package/src/engine/state.ts +10 -193
  37. package/src/engine/transition-helpers.ts +28 -0
  38. package/src/engine/transition-types.ts +8 -0
  39. package/src/engine/transitions.ts +17 -471
  40. package/src/harness/action-context.ts +181 -0
  41. package/src/harness/catalog.ts +45 -0
  42. package/src/harness/context-idle.ts +21 -0
  43. package/src/harness/core-actions.ts +299 -0
  44. package/src/harness/delegation-control-actions.ts +250 -0
  45. package/src/harness/delegation-failure.ts +215 -0
  46. package/src/harness/delegation-plan.ts +219 -0
  47. package/src/harness/delegation-recovery-validation.ts +159 -0
  48. package/src/harness/delegation-response-actions.ts +295 -0
  49. package/src/harness/delegation-retry-policy.ts +118 -0
  50. package/src/harness/dependencies.ts +165 -0
  51. package/src/harness/gate-submission-action.ts +154 -0
  52. package/src/harness/lifecycle-actions.ts +154 -0
  53. package/src/harness/pause-actions.ts +120 -0
  54. package/src/harness/plannotator-result-actions.ts +123 -0
  55. package/src/harness/prompt-gate-actions.ts +270 -0
  56. package/src/harness/resume-action.ts +261 -0
  57. package/src/harness/start-actions.ts +183 -0
  58. package/src/harness/status-actions.ts +191 -0
  59. package/src/harness/step-execution-actions.ts +283 -0
  60. package/src/harness/types.ts +76 -0
  61. package/src/harness.ts +236 -2303
  62. package/src/index.ts +73 -11
  63. package/src/integrations/plannotator-requests.ts +127 -0
  64. package/src/integrations/plannotator-responses.ts +141 -0
  65. package/src/integrations/plannotator-types.ts +44 -0
  66. package/src/integrations/plannotator.ts +16 -235
  67. package/src/integrations/prompt-gate.ts +17 -10
  68. package/src/integrations/subagents/child-policy-envelope.ts +119 -0
  69. package/src/integrations/subagents/child-policy-paths.ts +66 -0
  70. package/src/integrations/subagents/child-policy-sections.ts +174 -0
  71. package/src/integrations/subagents/child-policy-types.ts +32 -0
  72. package/src/integrations/subagents/child-policy-validation.ts +163 -0
  73. package/src/integrations/subagents/child-runtime-completion.ts +56 -0
  74. package/src/integrations/subagents/child-runtime-dependencies.ts +47 -0
  75. package/src/integrations/subagents/child-runtime-files.ts +156 -0
  76. package/src/integrations/subagents/child-runtime-policy.ts +83 -0
  77. package/src/integrations/subagents/child-runtime-types.ts +30 -0
  78. package/src/integrations/subagents/child-runtime.ts +140 -301
  79. package/src/integrations/subagents/client-delegation.ts +181 -0
  80. package/src/integrations/subagents/client-messages.ts +66 -0
  81. package/src/integrations/subagents/client-types.ts +36 -0
  82. package/src/integrations/subagents/client.ts +127 -219
  83. package/src/integrations/subagents/delegated-result.ts +31 -0
  84. package/src/integrations/subagents/diagnostic-format.ts +40 -0
  85. package/src/integrations/subagents/diagnostic-text.ts +114 -0
  86. package/src/integrations/subagents/diagnostic-types.ts +77 -0
  87. package/src/integrations/subagents/diagnostics.ts +24 -977
  88. package/src/integrations/subagents/failure-correlation.ts +235 -0
  89. package/src/integrations/subagents/failure-transcript.ts +192 -0
  90. package/src/integrations/subagents/hidden-bash-failure.ts +98 -0
  91. package/src/integrations/subagents/protocol-events.ts +27 -0
  92. package/src/integrations/subagents/protocol.ts +30 -375
  93. package/src/integrations/subagents/replay-audit.ts +147 -0
  94. package/src/integrations/subagents/replay-safety.ts +82 -0
  95. package/src/integrations/subagents/session-diagnostics.ts +258 -0
  96. package/src/policy/approved-command-extraction.ts +71 -0
  97. package/src/policy/approved-commands.ts +15 -432
  98. package/src/policy/bash-authorization.ts +86 -0
  99. package/src/policy/bash-read-only.ts +161 -0
  100. package/src/policy/bash-types.ts +20 -0
  101. package/src/policy/bash.ts +4 -346
  102. package/src/policy/completion-batch.ts +21 -16
  103. package/src/policy/immutable-input.ts +5 -2
  104. package/src/policy/mcp-authorization.ts +71 -0
  105. package/src/policy/restricted-command.ts +141 -0
  106. package/src/policy/restricted-git.ts +30 -0
  107. package/src/policy/reviewed-artifact.ts +97 -0
  108. package/src/policy/reviewed-command-safety.ts +150 -0
  109. package/src/policy/reviewed-command-shape.ts +49 -0
  110. package/src/policy/reviewed-repository-contract.ts +138 -0
  111. package/src/policy/reviewed-repository-cwd.ts +102 -0
  112. package/src/policy/tool-call-authorization.ts +58 -0
  113. package/src/policy/tool-selection.ts +72 -0
  114. package/src/policy/tool-types.ts +14 -0
  115. package/src/policy/tools.ts +11 -150
  116. package/src/preflight.ts +96 -61
  117. package/src/prompt/main-workflow-notice.ts +41 -0
  118. package/src/prompt/retry-task.ts +83 -0
  119. package/src/prompt/step-contract.ts +101 -0
  120. package/src/prompt/step-sections.ts +75 -0
  121. package/src/prompt/step-task.ts +156 -0
  122. package/src/prompt/template.ts +81 -0
  123. package/src/prompt.ts +10 -254
  124. package/src/runtime/completion-tool.ts +3 -0
  125. package/src/runtime/main-step-completion.ts +78 -0
  126. package/src/runtime/main-step-lifecycle.ts +37 -0
  127. package/src/runtime/main-step-policy.ts +84 -0
  128. package/src/runtime/main-step-runtime-types.ts +64 -0
  129. package/src/runtime/main-step-runtime.ts +129 -200
  130. package/src/runtime/main-step-state.ts +85 -0
  131. package/src/runtime/serial-task-queue.ts +48 -13
  132. package/src/runtime/step-result.ts +48 -27
  133. package/src/workflow-list.ts +15 -6
  134. package/src/workflow-status/format-status.ts +52 -0
  135. package/src/workflow-status/formatting.ts +178 -0
  136. package/src/workflow-status/layout.ts +109 -0
  137. package/src/workflow-status/render-board.ts +101 -0
  138. package/src/workflow-status/render-path.ts +123 -0
  139. package/src/workflow-status/render-summary.ts +144 -0
  140. package/src/workflow-status/types.ts +40 -0
  141. package/src/workflow-status/view.ts +273 -0
  142. package/src/workflow-status.ts +20 -820
@@ -1,1267 +1,18 @@
1
- import {
2
- DEFAULT_SETTINGS,
3
- DEFAULT_STATUS_SHORTCUT,
4
- DEFAULT_STEP_SUBAGENT,
5
- EMPTY_PERMISSIONS,
6
- EMPTY_REQUIREMENTS,
7
- SUBAGENT_RUNTIME_NAME_PATTERN,
8
- WORKFLOW_SCHEMA_VERSION,
9
- type BashApprovalSource,
10
- type BashMode,
11
- type BashPermission,
12
- type BashRule,
13
- type PermissionCeiling,
14
- type PromptSpec,
15
- type StepPermissions,
16
- type StepRequirements,
17
- type StepSubagent,
18
- type SubagentContext,
19
- type SubagentPermissionCeiling,
20
- type SubagentToolBudget,
21
- type SubagentTurnBudget,
22
- type WorkflowDefinition,
23
- type WorkflowGate,
24
- type WorkflowSettings,
25
- type WorkflowStep,
26
- } from './types.ts';
27
- import { RESERVED_COMMAND_NAMES } from '../command-names.ts';
1
+ import { EMPTY_REQUIREMENTS } from './types.ts';
28
2
 
29
- export interface ValidationResult<T> {
30
- value?: T;
31
- errors: string[];
32
- }
33
-
34
- const IDENTIFIER_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
35
- const OUTCOME_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
36
- const TOOL_PATTERN = /^[A-Za-z0-9_.:-]+$/;
37
- const RESOURCE_SELECTOR_PATTERN = /^[A-Za-z0-9_@./:+-]+$/;
38
- const MCP_SELECTOR_PATTERN = /^[A-Za-z0-9_.:-]+(?:\/[A-Za-z0-9_.:-]+)?$/;
39
- const EXECUTABLE_PATTERN = /^[A-Za-z0-9_./+-]+$/;
40
- const BASH_APPROVAL_SOURCE_PATTERN =
41
- /^(verification-worker|verification-reviewer|remote-actions)$/;
42
- const SHORTCUT_MODIFIERS = new Set(['ctrl', 'shift', 'alt', 'super']);
43
- const SHORTCUT_NAMED_KEYS = new Map<string, string>([
44
- ['escape', 'escape'],
45
- ['esc', 'esc'],
46
- ['enter', 'enter'],
47
- ['return', 'return'],
48
- ['tab', 'tab'],
49
- ['space', 'space'],
50
- ['backspace', 'backspace'],
51
- ['delete', 'delete'],
52
- ['insert', 'insert'],
53
- ['clear', 'clear'],
54
- ['home', 'home'],
55
- ['end', 'end'],
56
- ['pageup', 'pageUp'],
57
- ['pagedown', 'pageDown'],
58
- ['up', 'up'],
59
- ['down', 'down'],
60
- ['left', 'left'],
61
- ['right', 'right'],
62
- ]);
63
- const SHORTCUT_SYMBOL_KEYS = new Set([
64
- '`',
65
- '-',
66
- '=',
67
- '[',
68
- ']',
69
- '\\',
70
- ';',
71
- "'",
72
- ',',
73
- '.',
74
- '/',
75
- '!',
76
- '@',
77
- '#',
78
- '$',
79
- '%',
80
- '^',
81
- '&',
82
- '*',
83
- '(',
84
- ')',
85
- '_',
86
- '|',
87
- '~',
88
- '{',
89
- '}',
90
- ':',
91
- '<',
92
- '>',
93
- '?',
94
- ]);
95
- const MAX_SHORTCUT_CHARS = 64;
96
- const PROMPT_VARIABLES = new Set([
97
- 'workflow.input',
98
- 'workflow.id',
99
- 'run.id',
100
- 'step.id',
101
- 'step.title',
102
- 'last.summary',
103
- 'gate.feedback',
104
- ]);
105
-
106
- type JsonObject = Record<string, unknown>;
107
-
108
- function isObject(value: unknown): value is JsonObject {
109
- return value !== null && typeof value === 'object' && !Array.isArray(value);
110
- }
111
-
112
- function rejectUnknownKeys(
113
- value: JsonObject,
114
- allowed: readonly string[],
115
- path: string,
116
- errors: string[],
117
- ): void {
118
- const allowedSet = new Set(allowed);
119
- for (const key of Object.keys(value)) {
120
- if (!allowedSet.has(key)) {
121
- errors.push(`${path}: unknown property "${key}"`);
122
- }
123
- }
124
- }
125
-
126
- function readString(
127
- value: unknown,
128
- path: string,
129
- errors: string[],
130
- options: { pattern?: RegExp; nonEmpty?: boolean } = {},
131
- ): string | undefined {
132
- if (typeof value !== 'string') {
133
- errors.push(`${path}: expected a string`);
134
- return undefined;
135
- }
136
-
137
- const result = value.trim();
138
- if ((options.nonEmpty ?? true) && !result) {
139
- errors.push(`${path}: must not be empty`);
140
- return undefined;
141
- }
142
- if (options.pattern && !options.pattern.test(result)) {
143
- errors.push(`${path}: invalid value "${result}"`);
144
- return undefined;
145
- }
146
- return result;
147
- }
148
-
149
- function canonicalShortcutKey(value: string): string | undefined {
150
- if (/^[a-z0-9]$/.test(value) || SHORTCUT_SYMBOL_KEYS.has(value)) {
151
- return value;
152
- }
153
- if (/^f(?:[1-9]|1[0-2])$/.test(value)) return value;
154
- return SHORTCUT_NAMED_KEYS.get(value);
155
- }
156
-
157
- function readStatusShortcut(
158
- value: unknown,
159
- path: string,
160
- errors: string[],
161
- ): WorkflowSettings['statusShortcut'] {
162
- if (value === undefined) return DEFAULT_STATUS_SHORTCUT;
163
- const shortcut = readString(value, path, errors);
164
- if (!shortcut) return DEFAULT_STATUS_SHORTCUT;
165
- if (shortcut.length > MAX_SHORTCUT_CHARS) {
166
- errors.push(`${path}: must be at most ${MAX_SHORTCUT_CHARS} characters`);
167
- return DEFAULT_STATUS_SHORTCUT;
168
- }
169
-
170
- const parts = shortcut.toLowerCase().split('+');
171
- const key = parts.pop() ?? '';
172
- const modifiers = parts;
173
- const uniqueModifiers = new Set(modifiers);
174
- const canonicalKey = canonicalShortcutKey(key);
175
- const isPlainTypingKey =
176
- modifiers.length === 0 && (key.length === 1 || key === 'space');
177
- const isUnmatchableModifiedKey =
178
- modifiers.length > 0 &&
179
- (key === 'escape' || key === 'esc' || /^f(?:[1-9]|1[0-2])$/.test(key));
180
- if (
181
- !canonicalKey ||
182
- modifiers.length > SHORTCUT_MODIFIERS.size ||
183
- uniqueModifiers.size !== modifiers.length ||
184
- modifiers.some((modifier) => !SHORTCUT_MODIFIERS.has(modifier)) ||
185
- isPlainTypingKey ||
186
- isUnmatchableModifiedKey
187
- ) {
188
- errors.push(`${path}: expected a supported Pi key id such as "ctrl+alt+w"`);
189
- return DEFAULT_STATUS_SHORTCUT;
190
- }
191
-
192
- return [...modifiers, canonicalKey].join(
193
- '+',
194
- ) as WorkflowSettings['statusShortcut'];
195
- }
196
-
197
- function readInteger(
198
- value: unknown,
199
- fallback: number,
200
- path: string,
201
- errors: string[],
202
- limits: { min: number; max: number },
203
- ): number {
204
- if (value === undefined) return fallback;
205
- if (
206
- !Number.isInteger(value) ||
207
- (value as number) < limits.min ||
208
- (value as number) > limits.max
209
- ) {
210
- errors.push(
211
- `${path}: expected an integer from ${limits.min} to ${limits.max}`,
212
- );
213
- return fallback;
214
- }
215
- return value as number;
216
- }
217
-
218
- function readBoolean(
219
- value: unknown,
220
- fallback: boolean,
221
- path: string,
222
- errors: string[],
223
- ): boolean {
224
- if (value === undefined) return fallback;
225
- if (typeof value !== 'boolean') {
226
- errors.push(`${path}: expected a boolean`);
227
- return fallback;
228
- }
229
- return value;
230
- }
231
-
232
- function readStringList(
233
- value: unknown,
234
- path: string,
235
- errors: string[],
236
- pattern: RegExp,
237
- ): string[] {
238
- if (value === undefined) return [];
239
- if (!Array.isArray(value)) {
240
- errors.push(`${path}: expected an array of strings`);
241
- return [];
242
- }
243
-
244
- const result: string[] = [];
245
- const seen = new Set<string>();
246
- value.forEach((item, index) => {
247
- const parsed = readString(item, `${path}[${index}]`, errors, { pattern });
248
- if (!parsed) return;
249
- if (seen.has(parsed)) {
250
- errors.push(`${path}[${index}]: duplicate value "${parsed}"`);
251
- return;
252
- }
253
- seen.add(parsed);
254
- result.push(parsed);
255
- });
256
- return result;
257
- }
258
-
259
- function parseBashRule(
260
- value: unknown,
261
- path: string,
262
- errors: string[],
263
- ): BashRule[] {
264
- if (!isObject(value)) {
265
- errors.push(`${path}: expected an object`);
266
- return [];
267
- }
268
- rejectUnknownKeys(
269
- value,
270
- ['executable', 'argsPrefix', 'argsPrefixes'],
271
- path,
272
- errors,
273
- );
274
-
275
- const executable = readString(
276
- value.executable,
277
- `${path}.executable`,
278
- errors,
279
- {
280
- pattern: EXECUTABLE_PATTERN,
281
- },
282
- );
283
- if (value.argsPrefix !== undefined && value.argsPrefixes !== undefined) {
284
- errors.push(`${path}: argsPrefix and argsPrefixes are mutually exclusive`);
285
- }
286
-
287
- if (value.argsPrefixes !== undefined) {
288
- if (!Array.isArray(value.argsPrefixes)) {
289
- errors.push(`${path}.argsPrefixes: expected an array of argument arrays`);
290
- return [];
291
- }
292
- if (value.argsPrefixes.length === 0) {
293
- errors.push(`${path}.argsPrefixes: at least one prefix is required`);
294
- }
295
- const prefixes: string[][] = [];
296
- const seen = new Set<string>();
297
- value.argsPrefixes.forEach((candidate, index) => {
298
- const prefixPath = `${path}.argsPrefixes[${index}]`;
299
- const prefix = readStringList(candidate, prefixPath, errors, /^[^\s]+$/);
300
- if (Array.isArray(candidate) && candidate.length === 0) {
301
- errors.push(`${prefixPath}: at least one argument is required`);
302
- return;
303
- }
304
- if (prefix.length === 0) return;
305
- const key = JSON.stringify(prefix);
306
- if (seen.has(key)) {
307
- errors.push(`${prefixPath}: duplicate argument prefix`);
308
- return;
309
- }
310
- seen.add(key);
311
- prefixes.push(prefix);
312
- });
313
- return executable
314
- ? prefixes.map((argsPrefix) => ({ executable, argsPrefix }))
315
- : [];
316
- }
317
-
318
- const argsPrefix = readStringList(
319
- value.argsPrefix,
320
- `${path}.argsPrefix`,
321
- errors,
322
- /^[^\s]+$/,
323
- );
324
- return executable ? [{ executable, argsPrefix }] : [];
325
- }
326
-
327
- function parseBashPermission(
328
- value: unknown,
329
- path: string,
330
- errors: string[],
331
- ): BashPermission {
332
- if (value === undefined) {
333
- return { ...EMPTY_PERMISSIONS.bash, allow: [] };
334
- }
335
- if (!isObject(value)) {
336
- errors.push(`${path}: expected an object`);
337
- return { ...EMPTY_PERMISSIONS.bash, allow: [] };
338
- }
339
- rejectUnknownKeys(value, ['mode', 'allow', 'approvedSources'], path, errors);
340
-
341
- const mode = readString(value.mode, `${path}.mode`, errors) as
342
- BashMode | undefined;
343
- const validMode =
344
- mode === 'deny' ||
345
- mode === 'read-only' ||
346
- mode === 'allow-list' ||
347
- mode === 'unrestricted';
348
- if (!validMode) {
349
- errors.push(
350
- `${path}.mode: expected deny, read-only, allow-list, or unrestricted`,
351
- );
352
- }
353
-
354
- const allow: BashRule[] = [];
355
- if (value.allow !== undefined) {
356
- if (!Array.isArray(value.allow)) {
357
- errors.push(`${path}.allow: expected an array`);
358
- } else {
359
- value.allow.forEach((rule, index) => {
360
- allow.push(...parseBashRule(rule, `${path}.allow[${index}]`, errors));
361
- });
362
- }
363
- }
364
-
365
- const approvedSources = (
366
- value.approvedSources === undefined
367
- ? []
368
- : readStringList(
369
- value.approvedSources,
370
- `${path}.approvedSources`,
371
- errors,
372
- BASH_APPROVAL_SOURCE_PATTERN,
373
- )
374
- ) as BashApprovalSource[];
375
-
376
- const normalizedMode = validMode ? mode : 'deny';
377
- if (normalizedMode !== 'allow-list' && allow.length > 0) {
378
- errors.push(`${path}.allow: only valid when mode is "allow-list"`);
379
- }
380
- if (normalizedMode === 'allow-list' && allow.length === 0) {
381
- if (approvedSources.length === 0) {
382
- errors.push(
383
- `${path}: allow-list mode requires an allow rule or an approved command source`,
384
- );
385
- }
386
- }
387
- if (normalizedMode !== 'allow-list' && approvedSources.length > 0) {
388
- errors.push(
389
- `${path}.approvedSources: only valid when mode is "allow-list"`,
390
- );
391
- }
392
- return {
393
- mode: normalizedMode,
394
- allow,
395
- ...(approvedSources.length > 0 ? { approvedSources } : {}),
396
- };
397
- }
398
-
399
- function parsePermissions(
400
- value: unknown,
401
- path: string,
402
- errors: string[],
403
- ): StepPermissions {
404
- if (value === undefined) {
405
- return {
406
- tools: [],
407
- mcp: [],
408
- extensions: [],
409
- skills: [],
410
- bash: { mode: 'deny', allow: [] },
411
- };
412
- }
413
- if (!isObject(value)) {
414
- errors.push(`${path}: expected an object`);
415
- return {
416
- tools: [],
417
- mcp: [],
418
- extensions: [],
419
- skills: [],
420
- bash: { mode: 'deny', allow: [] },
421
- };
422
- }
423
- rejectUnknownKeys(
424
- value,
425
- ['tools', 'mcp', 'extensions', 'skills', 'bash'],
426
- path,
427
- errors,
428
- );
429
-
430
- const permissions: StepPermissions = {
431
- tools: readStringList(value.tools, `${path}.tools`, errors, TOOL_PATTERN),
432
- mcp: readStringList(value.mcp, `${path}.mcp`, errors, MCP_SELECTOR_PATTERN),
433
- extensions: readStringList(
434
- value.extensions,
435
- `${path}.extensions`,
436
- errors,
437
- RESOURCE_SELECTOR_PATTERN,
438
- ),
439
- skills: readStringList(
440
- value.skills,
441
- `${path}.skills`,
442
- errors,
443
- RESOURCE_SELECTOR_PATTERN,
444
- ),
445
- bash: parseBashPermission(value.bash, `${path}.bash`, errors),
446
- };
447
-
448
- if (permissions.bash.mode !== 'deny' && !permissions.tools.includes('bash')) {
449
- errors.push(`${path}.tools: must include "bash" when Bash is enabled`);
450
- }
451
- return permissions;
452
- }
453
-
454
- function parseRequirements(
455
- value: unknown,
456
- permissions: StepPermissions,
457
- path: string,
458
- errors: string[],
459
- ): StepRequirements {
460
- if (value === undefined) {
461
- return {
462
- tools: [],
463
- extensions: [],
464
- skills: [],
465
- };
466
- }
467
- if (!isObject(value)) {
468
- errors.push(`${path}: expected an object`);
469
- return {
470
- tools: [],
471
- extensions: [],
472
- skills: [],
473
- };
474
- }
475
- rejectUnknownKeys(value, ['tools', 'extensions', 'skills'], path, errors);
476
-
477
- const requirements: StepRequirements = {
478
- tools: readStringList(value.tools, `${path}.tools`, errors, TOOL_PATTERN),
479
- extensions: readStringList(
480
- value.extensions,
481
- `${path}.extensions`,
482
- errors,
483
- RESOURCE_SELECTOR_PATTERN,
484
- ),
485
- skills: readStringList(
486
- value.skills,
487
- `${path}.skills`,
488
- errors,
489
- RESOURCE_SELECTOR_PATTERN,
490
- ),
491
- };
492
-
493
- for (const tool of requirements.tools) {
494
- if (
495
- !permissions.tools.includes(tool) &&
496
- !(tool === 'mcp' && permissions.mcp.length > 0)
497
- ) {
498
- errors.push(
499
- `${path}.tools: required tool "${tool}" is not allowed by this step`,
500
- );
501
- }
502
- }
503
- for (const extension of requirements.extensions) {
504
- if (!permissions.extensions.includes(extension)) {
505
- errors.push(
506
- `${path}.extensions: required extension "${extension}" is not allowed by this step`,
507
- );
508
- }
509
- }
510
- for (const skill of requirements.skills) {
511
- if (!permissions.skills.includes(skill)) {
512
- errors.push(
513
- `${path}.skills: required skill "${skill}" is not allowed by this step`,
514
- );
515
- }
516
- }
517
- return requirements;
518
- }
519
-
520
- function parseSubagentTurnBudget(
521
- value: unknown,
522
- path: string,
523
- errors: string[],
524
- ): SubagentTurnBudget | undefined {
525
- if (value === undefined) return undefined;
526
- if (!isObject(value)) {
527
- errors.push(`${path}: expected an object`);
528
- return undefined;
529
- }
530
- rejectUnknownKeys(value, ['maxTurns', 'graceTurns'], path, errors);
531
- const maxTurns = readInteger(value.maxTurns, 0, `${path}.maxTurns`, errors, {
532
- min: 1,
533
- max: 1_000,
534
- });
535
- const graceTurns =
536
- value.graceTurns === undefined
537
- ? undefined
538
- : readInteger(value.graceTurns, 0, `${path}.graceTurns`, errors, {
539
- min: 0,
540
- max: 100,
541
- });
542
- if (maxTurns === 0) return undefined;
543
- return {
544
- maxTurns,
545
- ...(graceTurns !== undefined ? { graceTurns } : {}),
546
- };
547
- }
548
-
549
- function parseSubagentToolBudget(
550
- value: unknown,
551
- path: string,
552
- errors: string[],
553
- ): SubagentToolBudget | undefined {
554
- if (value === undefined) return undefined;
555
- if (!isObject(value)) {
556
- errors.push(`${path}: expected an object`);
557
- return undefined;
558
- }
559
- rejectUnknownKeys(value, ['soft', 'hard', 'block'], path, errors);
560
- const hard = readInteger(value.hard, 0, `${path}.hard`, errors, {
561
- min: 1,
562
- max: 100_000,
563
- });
564
- const soft =
565
- value.soft === undefined
566
- ? undefined
567
- : readInteger(value.soft, 0, `${path}.soft`, errors, {
568
- min: 1,
569
- max: 100_000,
570
- });
571
-
572
- let block: string[] | '*' | undefined;
573
- if (value.block === '*') {
574
- block = '*';
575
- } else if (value.block !== undefined) {
576
- block = readStringList(value.block, `${path}.block`, errors, TOOL_PATTERN);
577
- if (block.length === 0) {
578
- errors.push(`${path}.block: expected "*" or at least one tool name`);
579
- }
580
- }
581
- if (soft !== undefined && hard > 0 && soft > hard) {
582
- errors.push(`${path}.soft: must not exceed hard`);
583
- }
584
- if (hard === 0) return undefined;
585
- return {
586
- hard,
587
- ...(soft !== undefined ? { soft } : {}),
588
- ...(block !== undefined ? { block } : {}),
589
- };
590
- }
591
-
592
- function parseStepSubagent(
593
- value: unknown,
594
- path: string,
595
- errors: string[],
596
- ): StepSubagent | undefined {
597
- if (value === undefined) return undefined;
598
- if (typeof value === 'string') {
599
- const agent =
600
- readString(value, path, errors, {
601
- pattern: SUBAGENT_RUNTIME_NAME_PATTERN,
602
- }) ?? DEFAULT_STEP_SUBAGENT.agent;
603
- return { ...DEFAULT_STEP_SUBAGENT, agent };
604
- }
605
- if (!isObject(value)) {
606
- errors.push(`${path}: expected an agent profile name or object`);
607
- return undefined;
608
- }
609
- rejectUnknownKeys(
610
- value,
611
- [
612
- 'agent',
613
- 'context',
614
- 'model',
615
- 'timeoutMs',
616
- 'turnBudget',
617
- 'toolBudget',
618
- 'artifacts',
619
- 'retryToolFailures',
620
- ],
621
- path,
622
- errors,
623
- );
624
-
625
- const agent =
626
- value.agent === undefined
627
- ? DEFAULT_STEP_SUBAGENT.agent
628
- : (readString(value.agent, `${path}.agent`, errors, {
629
- pattern: SUBAGENT_RUNTIME_NAME_PATTERN,
630
- }) ?? DEFAULT_STEP_SUBAGENT.agent);
631
- const contextValue =
632
- value.context === undefined
633
- ? DEFAULT_STEP_SUBAGENT.context
634
- : readString(value.context, `${path}.context`, errors);
635
- const context: SubagentContext = DEFAULT_STEP_SUBAGENT.context;
636
- if (contextValue !== 'fresh') {
637
- errors.push(`${path}.context: expected fresh`);
638
- }
639
- const model =
640
- value.model === undefined
641
- ? undefined
642
- : readString(value.model, `${path}.model`, errors, {
643
- pattern: RESOURCE_SELECTOR_PATTERN,
644
- });
645
- const timeoutMs = readInteger(
646
- value.timeoutMs,
647
- DEFAULT_STEP_SUBAGENT.timeoutMs,
648
- `${path}.timeoutMs`,
649
- errors,
650
- { min: 1_000, max: 86_400_000 },
651
- );
652
- const turnBudget = parseSubagentTurnBudget(
653
- value.turnBudget,
654
- `${path}.turnBudget`,
655
- errors,
656
- );
657
- const toolBudget = parseSubagentToolBudget(
658
- value.toolBudget,
659
- `${path}.toolBudget`,
660
- errors,
661
- );
662
- const artifacts = readBoolean(
663
- value.artifacts,
664
- DEFAULT_STEP_SUBAGENT.artifacts,
665
- `${path}.artifacts`,
666
- errors,
667
- );
668
- const retryToolFailures = readBoolean(
669
- value.retryToolFailures,
670
- DEFAULT_STEP_SUBAGENT.retryToolFailures,
671
- `${path}.retryToolFailures`,
672
- errors,
673
- );
674
- return {
675
- agent,
676
- context,
677
- ...(model ? { model } : {}),
678
- timeoutMs,
679
- ...(turnBudget ? { turnBudget } : {}),
680
- ...(toolBudget ? { toolBudget } : {}),
681
- artifacts,
682
- retryToolFailures,
683
- };
684
- }
685
-
686
- function parsePrompt(
687
- value: unknown,
688
- path: string,
689
- errors: string[],
690
- ): PromptSpec | undefined {
691
- if (typeof value === 'string') {
692
- const inline = readString(value, path, errors);
693
- return inline ? { inline } : undefined;
694
- }
695
- if (!isObject(value)) {
696
- errors.push(`${path}: expected a string or an object`);
697
- return undefined;
698
- }
699
- rejectUnknownKeys(value, ['file'], path, errors);
700
- const file = readString(value.file, `${path}.file`, errors);
701
- if (!file) return undefined;
702
- if (file.startsWith('/') || file.includes('\0')) {
703
- errors.push(`${path}.file: expected a safe relative path`);
704
- return undefined;
705
- }
706
- return { file };
707
- }
3
+ export type { ValidationResult } from './validation/shared.ts';
4
+ export { validatePromptText } from './validation/prompt.ts';
5
+ export { validateSettings } from './validation/settings.ts';
6
+ export { validateWorkflow } from './validation/workflow.ts';
708
7
 
709
- function parseTransitions(
710
- value: unknown,
711
- path: string,
712
- errors: string[],
713
- ): Record<string, string> {
714
- if (!isObject(value)) {
715
- errors.push(`${path}: expected an object`);
716
- return {};
717
- }
718
- const transitions: Record<string, string> = {};
719
- for (const [outcome, targetValue] of Object.entries(value)) {
720
- if (!OUTCOME_PATTERN.test(outcome)) {
721
- errors.push(`${path}: invalid outcome "${outcome}"`);
722
- continue;
723
- }
724
- const target = readString(targetValue, `${path}.${outcome}`, errors);
725
- if (target) transitions[outcome] = target;
726
- }
727
- if (Object.keys(transitions).length === 0) {
728
- errors.push(`${path}: at least one transition is required`);
729
- }
730
- return transitions;
731
- }
732
-
733
- function parseGate(
734
- value: unknown,
735
- path: string,
736
- errors: string[],
737
- ): WorkflowGate | undefined {
738
- if (value === undefined) return undefined;
739
- if (!isObject(value)) {
740
- errors.push(`${path}: expected an object`);
741
- return undefined;
742
- }
743
- rejectUnknownKeys(
744
- value,
745
- [
746
- 'provider',
747
- 'submitOutcome',
748
- 'approvedOutcome',
749
- 'rejectedOutcome',
750
- 'timeoutMs',
751
- ],
752
- path,
753
- errors,
754
- );
755
-
756
- const providerValue =
757
- value.provider === undefined
758
- ? 'prompt'
759
- : readString(value.provider, `${path}.provider`, errors);
760
- const provider =
761
- providerValue === 'prompt' || providerValue === 'plannotator'
762
- ? providerValue
763
- : undefined;
764
- if (!provider) {
765
- errors.push(`${path}.provider: expected prompt or plannotator`);
766
- }
767
- const submitOutcome = readString(
768
- value.submitOutcome,
769
- `${path}.submitOutcome`,
770
- errors,
771
- {
772
- pattern: OUTCOME_PATTERN,
773
- },
774
- );
775
- const approvedOutcome = readString(
776
- value.approvedOutcome,
777
- `${path}.approvedOutcome`,
778
- errors,
779
- {
780
- pattern: OUTCOME_PATTERN,
781
- },
782
- );
783
- const rejectedOutcome = readString(
784
- value.rejectedOutcome,
785
- `${path}.rejectedOutcome`,
786
- errors,
787
- {
788
- pattern: OUTCOME_PATTERN,
789
- },
790
- );
791
- if (provider === 'prompt' && value.timeoutMs !== undefined) {
792
- errors.push(`${path}.timeoutMs: only valid with provider "plannotator"`);
793
- }
794
- if (!submitOutcome || !approvedOutcome || !rejectedOutcome || !provider) {
795
- return undefined;
796
- }
797
- if (approvedOutcome === rejectedOutcome) {
798
- errors.push(`${path}: approvedOutcome and rejectedOutcome must differ`);
799
- }
800
- if (provider === 'prompt') {
801
- return {
802
- provider,
803
- submitOutcome,
804
- approvedOutcome,
805
- rejectedOutcome,
806
- };
807
- }
808
- return {
809
- provider,
810
- submitOutcome,
811
- approvedOutcome,
812
- rejectedOutcome,
813
- timeoutMs: readInteger(
814
- value.timeoutMs,
815
- 5_000,
816
- `${path}.timeoutMs`,
817
- errors,
818
- {
819
- min: 1_000,
820
- max: 30_000,
821
- },
822
- ),
823
- };
824
- }
825
-
826
- function parseStep(
827
- value: unknown,
828
- stepId: string,
829
- path: string,
830
- errors: string[],
831
- ): WorkflowStep | undefined {
832
- if (!isObject(value)) {
833
- errors.push(`${path}: expected an object`);
834
- return undefined;
835
- }
836
- rejectUnknownKeys(
837
- value,
838
- [
839
- 'title',
840
- 'prompt',
841
- 'subagent',
842
- 'permissions',
843
- 'requires',
844
- 'transitions',
845
- 'gate',
846
- ],
847
- path,
848
- errors,
849
- );
850
-
851
- const title =
852
- value.title === undefined
853
- ? stepId
854
- : readString(value.title, `${path}.title`, errors);
855
- const prompt = parsePrompt(value.prompt, `${path}.prompt`, errors);
856
- const subagent = parseStepSubagent(
857
- value.subagent,
858
- `${path}.subagent`,
859
- errors,
860
- );
861
- const permissions = parsePermissions(
862
- value.permissions,
863
- `${path}.permissions`,
864
- errors,
865
- );
866
- if (
867
- subagent?.retryToolFailures &&
868
- permissions.tools.some((tool) => tool === 'edit' || tool === 'write')
869
- ) {
870
- errors.push(
871
- `${path}.subagent.retryToolFailures: requires a step without edit or write tools`,
872
- );
873
- }
874
- const requires = parseRequirements(
875
- value.requires,
876
- permissions,
877
- `${path}.requires`,
878
- errors,
879
- );
880
- const transitions = parseTransitions(
881
- value.transitions,
882
- `${path}.transitions`,
883
- errors,
884
- );
885
- const gate = parseGate(value.gate, `${path}.gate`, errors);
886
-
887
- if (gate) {
888
- if (!Object.hasOwn(transitions, gate.approvedOutcome)) {
889
- errors.push(
890
- `${path}.transitions: missing gate outcome "${gate.approvedOutcome}"`,
891
- );
892
- }
893
- if (!Object.hasOwn(transitions, gate.rejectedOutcome)) {
894
- errors.push(
895
- `${path}.transitions: missing gate outcome "${gate.rejectedOutcome}"`,
896
- );
897
- }
898
- if (Object.hasOwn(transitions, gate.submitOutcome)) {
899
- errors.push(
900
- `${path}.transitions: submitOutcome is handled by the gate and must not be a transition`,
901
- );
902
- }
903
- }
904
-
905
- if (!title || !prompt) return undefined;
906
- return {
907
- title,
908
- prompt,
909
- ...(subagent ? { subagent } : {}),
910
- permissions,
911
- requires,
912
- transitions,
913
- ...(gate ? { gate } : {}),
914
- };
915
- }
916
-
917
- export function validatePromptText(text: string, path: string): string[] {
918
- const errors: string[] = [];
919
- for (const match of text.matchAll(/\{\{([^{}]+)\}\}/g)) {
920
- const variable = match[1]?.trim() ?? '';
921
- if (!PROMPT_VARIABLES.has(variable)) {
922
- errors.push(`${path}: unknown prompt variable "{{${variable}}}"`);
923
- }
924
- }
925
- return errors;
926
- }
927
-
928
- export function validateWorkflow(
929
- value: unknown,
930
- ): ValidationResult<WorkflowDefinition> {
931
- const errors: string[] = [];
932
- if (!isObject(value)) {
933
- return { errors: ['workflow: expected an object'] };
934
- }
935
- rejectUnknownKeys(
936
- value,
937
- [
938
- '$schema',
939
- 'version',
940
- 'id',
941
- 'command',
942
- 'description',
943
- 'start',
944
- 'maxStepVisits',
945
- 'summaryMaxChars',
946
- 'steps',
947
- ],
948
- 'workflow',
949
- errors,
950
- );
951
- if (value.$schema !== undefined && typeof value.$schema !== 'string') {
952
- errors.push('workflow.$schema: expected a string');
953
- }
954
-
955
- if (value.version !== WORKFLOW_SCHEMA_VERSION) {
956
- errors.push(`workflow.version: expected ${WORKFLOW_SCHEMA_VERSION}`);
957
- }
958
- const id = readString(value.id, 'workflow.id', errors, {
959
- pattern: IDENTIFIER_PATTERN,
960
- });
961
- const command = readString(value.command, 'workflow.command', errors, {
962
- pattern: IDENTIFIER_PATTERN,
963
- });
964
- const description = readString(
965
- value.description,
966
- 'workflow.description',
967
- errors,
968
- );
969
- const start = readString(value.start, 'workflow.start', errors, {
970
- pattern: IDENTIFIER_PATTERN,
971
- });
972
- const maxStepVisits = readInteger(
973
- value.maxStepVisits,
974
- 5,
975
- 'workflow.maxStepVisits',
976
- errors,
977
- {
978
- min: 1,
979
- max: 100,
980
- },
981
- );
982
- const summaryMaxChars = readInteger(
983
- value.summaryMaxChars,
984
- 4_000,
985
- 'workflow.summaryMaxChars',
986
- errors,
987
- { min: 100, max: 50_000 },
988
- );
989
-
990
- if (command && RESERVED_COMMAND_NAMES.has(command)) {
991
- errors.push(
992
- `workflow.command: "${command}" is reserved by Pi or the harness`,
993
- );
994
- }
995
-
996
- const steps: Record<string, WorkflowStep> = {};
997
- if (!isObject(value.steps)) {
998
- errors.push('workflow.steps: expected an object');
999
- } else {
1000
- for (const [stepId, stepValue] of Object.entries(value.steps)) {
1001
- if (!IDENTIFIER_PATTERN.test(stepId)) {
1002
- errors.push(`workflow.steps: invalid step id "${stepId}"`);
1003
- continue;
1004
- }
1005
- const step = parseStep(
1006
- stepValue,
1007
- stepId,
1008
- `workflow.steps.${stepId}`,
1009
- errors,
1010
- );
1011
- if (step) steps[stepId] = step;
1012
- }
1013
- }
1014
-
1015
- if (Object.keys(steps).length === 0) {
1016
- errors.push('workflow.steps: at least one step is required');
1017
- }
1018
- if (start && !Object.hasOwn(steps, start)) {
1019
- errors.push(`workflow.start: unknown step "${start}"`);
1020
- }
1021
- for (const [stepId, step] of Object.entries(steps)) {
1022
- for (const [outcome, target] of Object.entries(step.transitions)) {
1023
- if (
1024
- target !== '$done' &&
1025
- target !== '$pause' &&
1026
- !Object.hasOwn(steps, target)
1027
- ) {
1028
- errors.push(
1029
- `workflow.steps.${stepId}.transitions.${outcome}: unknown target "${target}"`,
1030
- );
1031
- }
1032
- }
1033
- }
1034
-
1035
- if (errors.length > 0 || !id || !command || !description || !start) {
1036
- return { errors };
1037
- }
1038
- return {
1039
- value: {
1040
- version: WORKFLOW_SCHEMA_VERSION,
1041
- id,
1042
- command,
1043
- description,
1044
- start,
1045
- maxStepVisits,
1046
- summaryMaxChars,
1047
- steps,
1048
- },
1049
- errors,
1050
- };
1051
- }
1052
-
1053
- function parsePermissionCeiling(
1054
- value: unknown,
1055
- path: string,
1056
- errors: string[],
1057
- ): PermissionCeiling | undefined {
1058
- if (value === undefined) return undefined;
1059
- if (!isObject(value)) {
1060
- errors.push(`${path}: expected an object`);
1061
- return undefined;
1062
- }
1063
- rejectUnknownKeys(
1064
- value,
1065
- ['tools', 'mcp', 'extensions', 'skills', 'bash', 'subagent'],
1066
- path,
1067
- errors,
1068
- );
1069
- const permissions = parsePermissions(
1070
- {
1071
- ...(value.tools !== undefined ? { tools: value.tools } : {}),
1072
- ...(value.mcp !== undefined ? { mcp: value.mcp } : {}),
1073
- ...(value.extensions !== undefined
1074
- ? { extensions: value.extensions }
1075
- : {}),
1076
- ...(value.skills !== undefined ? { skills: value.skills } : {}),
1077
- ...(value.bash !== undefined ? { bash: value.bash } : {}),
1078
- },
1079
- path,
1080
- errors,
1081
- );
1082
- const subagent =
1083
- value.subagent === undefined
1084
- ? undefined
1085
- : parseSubagentPermissionCeiling(
1086
- value.subagent,
1087
- `${path}.subagent`,
1088
- errors,
1089
- );
1090
- return {
1091
- ...permissions,
1092
- ...(subagent ? { subagent } : {}),
1093
- };
1094
- }
1095
-
1096
- function parseSubagentPermissionCeiling(
1097
- value: unknown,
1098
- path: string,
1099
- errors: string[],
1100
- ): SubagentPermissionCeiling | undefined {
1101
- if (!isObject(value)) {
1102
- errors.push(`${path}: expected an object`);
1103
- return undefined;
1104
- }
1105
- rejectUnknownKeys(
1106
- value,
1107
- [
1108
- 'agents',
1109
- 'contexts',
1110
- 'models',
1111
- 'maxTimeoutMs',
1112
- 'maxTurns',
1113
- 'maxGraceTurns',
1114
- 'maxToolCalls',
1115
- 'artifacts',
1116
- 'retryToolFailures',
1117
- ],
1118
- path,
1119
- errors,
1120
- );
1121
- const agents = readStringList(
1122
- value.agents,
1123
- `${path}.agents`,
1124
- errors,
1125
- SUBAGENT_RUNTIME_NAME_PATTERN,
1126
- );
1127
- const contexts = readStringList(
1128
- value.contexts,
1129
- `${path}.contexts`,
1130
- errors,
1131
- /^fresh$/,
1132
- ) as SubagentContext[];
1133
- const models = readStringList(
1134
- value.models,
1135
- `${path}.models`,
1136
- errors,
1137
- RESOURCE_SELECTOR_PATTERN,
1138
- );
1139
- if (agents.length === 0) {
1140
- errors.push(`${path}.agents: at least one subagent is required`);
1141
- }
1142
- if (contexts.length === 0) {
1143
- errors.push(`${path}.contexts: at least one context mode is required`);
1144
- }
1145
- for (const field of [
1146
- 'maxTimeoutMs',
1147
- 'maxTurns',
1148
- 'maxGraceTurns',
1149
- 'maxToolCalls',
1150
- 'artifacts',
1151
- ] as const) {
1152
- if (value[field] === undefined) {
1153
- errors.push(`${path}.${field}: required`);
1154
- }
1155
- }
1156
- const maxTimeoutMs = readInteger(
1157
- value.maxTimeoutMs,
1158
- 0,
1159
- `${path}.maxTimeoutMs`,
1160
- errors,
1161
- { min: 1_000, max: 86_400_000 },
1162
- );
1163
- const maxTurns = readInteger(value.maxTurns, 0, `${path}.maxTurns`, errors, {
1164
- min: 1,
1165
- max: 1_000,
1166
- });
1167
- const maxGraceTurns = readInteger(
1168
- value.maxGraceTurns,
1169
- 0,
1170
- `${path}.maxGraceTurns`,
1171
- errors,
1172
- { min: 0, max: 100 },
1173
- );
1174
- const maxToolCalls = readInteger(
1175
- value.maxToolCalls,
1176
- 0,
1177
- `${path}.maxToolCalls`,
1178
- errors,
1179
- { min: 1, max: 100_000 },
1180
- );
1181
- const artifacts = readBoolean(
1182
- value.artifacts,
1183
- false,
1184
- `${path}.artifacts`,
1185
- errors,
1186
- );
1187
- const retryToolFailures = readBoolean(
1188
- value.retryToolFailures,
1189
- false,
1190
- `${path}.retryToolFailures`,
1191
- errors,
1192
- );
1193
- return {
1194
- agents,
1195
- contexts,
1196
- models,
1197
- maxTimeoutMs,
1198
- maxTurns,
1199
- maxGraceTurns,
1200
- maxToolCalls,
1201
- artifacts,
1202
- retryToolFailures,
1203
- };
1204
- }
1205
-
1206
- export function validateSettings(
1207
- value: unknown,
1208
- ): ValidationResult<WorkflowSettings> {
1209
- const errors: string[] = [];
1210
- if (!isObject(value)) {
1211
- return { errors: ['settings: expected an object'] };
1212
- }
1213
- rejectUnknownKeys(
1214
- value,
1215
- [
1216
- '$schema',
1217
- 'version',
1218
- 'allowProjectWorkflows',
1219
- 'statusShortcut',
1220
- 'permissionCeiling',
1221
- ],
1222
- 'settings',
1223
- errors,
1224
- );
1225
- if (value.$schema !== undefined && typeof value.$schema !== 'string') {
1226
- errors.push('settings.$schema: expected a string');
1227
- }
1228
- if (value.version !== WORKFLOW_SCHEMA_VERSION) {
1229
- errors.push(`settings.version: expected ${WORKFLOW_SCHEMA_VERSION}`);
1230
- }
1231
- let allowProjectWorkflows = false;
1232
- if (typeof value.allowProjectWorkflows === 'boolean') {
1233
- allowProjectWorkflows = value.allowProjectWorkflows;
1234
- } else if (value.allowProjectWorkflows !== undefined) {
1235
- errors.push('settings.allowProjectWorkflows: expected a boolean');
1236
- }
1237
- const statusShortcut = readStatusShortcut(
1238
- value.statusShortcut,
1239
- 'settings.statusShortcut',
1240
- errors,
1241
- );
1242
- const permissionCeiling = parsePermissionCeiling(
1243
- value.permissionCeiling,
1244
- 'settings.permissionCeiling',
1245
- errors,
1246
- );
1247
- if (allowProjectWorkflows && !permissionCeiling) {
1248
- errors.push(
1249
- 'settings.permissionCeiling: required when project workflows are enabled',
1250
- );
1251
- }
1252
- if (errors.length > 0) return { errors };
1253
- return {
1254
- value: {
1255
- ...DEFAULT_SETTINGS,
1256
- allowProjectWorkflows,
1257
- statusShortcut,
1258
- ...(permissionCeiling ? { permissionCeiling } : {}),
1259
- },
1260
- errors,
1261
- };
1262
- }
8
+ type MutableStepRequirements = {
9
+ readonly tools: Array<string>;
10
+ readonly extensions: Array<string>;
11
+ readonly skills: Array<string>;
12
+ };
1263
13
 
1264
- export function cloneEmptyRequirements(): StepRequirements {
14
+ /** Create an independent empty requirements value for mutable consumers. */
15
+ export function cloneEmptyRequirements(): MutableStepRequirements {
1265
16
  return {
1266
17
  tools: [...EMPTY_REQUIREMENTS.tools],
1267
18
  extensions: [...EMPTY_REQUIREMENTS.extensions],