@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
@@ -48,6 +48,7 @@
48
48
  "tree",
49
49
  "trust",
50
50
  "workflow-abort",
51
+ "workflow-doctor",
51
52
  "workflow-list",
52
53
  "workflow-pause",
53
54
  "workflow-reload",
@@ -94,6 +95,34 @@
94
95
  "type": "string",
95
96
  "pattern": "^[a-z][a-z0-9-]{0,63}$"
96
97
  },
98
+ "relativePath": {
99
+ "type": "string",
100
+ "minLength": 1,
101
+ "maxLength": 4096,
102
+ "pattern": "^\\S(?:.*\\S)?$",
103
+ "allOf": [
104
+ {
105
+ "not": {
106
+ "pattern": "^/"
107
+ }
108
+ },
109
+ {
110
+ "not": {
111
+ "pattern": "^[A-Za-z]:"
112
+ }
113
+ },
114
+ {
115
+ "not": {
116
+ "pattern": "^\\\\"
117
+ }
118
+ },
119
+ {
120
+ "not": {
121
+ "pattern": "\\u0000"
122
+ }
123
+ }
124
+ ]
125
+ },
97
126
  "resourceName": {
98
127
  "type": "string",
99
128
  "pattern": "^[A-Za-z0-9_@./:+-]+$"
@@ -114,8 +143,7 @@
114
143
  "required": ["file"],
115
144
  "properties": {
116
145
  "file": {
117
- "type": "string",
118
- "minLength": 1
146
+ "$ref": "#/$defs/relativePath"
119
147
  }
120
148
  }
121
149
  }
@@ -175,32 +203,17 @@
175
203
  "required": ["mode"]
176
204
  },
177
205
  "then": {
178
- "anyOf": [
179
- {
180
- "required": ["allow"],
181
- "properties": {
182
- "allow": {
183
- "minItems": 1
184
- }
185
- }
186
- },
187
- {
188
- "required": ["approvedSources"],
189
- "properties": {
190
- "approvedSources": {
191
- "minItems": 1
192
- }
193
- }
206
+ "required": ["allow"],
207
+ "properties": {
208
+ "allow": {
209
+ "minItems": 1
194
210
  }
195
- ]
211
+ }
196
212
  },
197
213
  "else": {
198
214
  "properties": {
199
215
  "allow": {
200
216
  "maxItems": 0
201
- },
202
- "approvedSources": {
203
- "maxItems": 0
204
217
  }
205
218
  }
206
219
  }
@@ -208,24 +221,13 @@
208
221
  ],
209
222
  "properties": {
210
223
  "mode": {
211
- "enum": ["deny", "read-only", "allow-list", "unrestricted"]
224
+ "enum": ["deny", "allow-list", "unrestricted"]
212
225
  },
213
226
  "allow": {
214
227
  "type": "array",
215
228
  "items": {
216
229
  "$ref": "#/$defs/bashRule"
217
230
  }
218
- },
219
- "approvedSources": {
220
- "type": "array",
221
- "uniqueItems": true,
222
- "items": {
223
- "enum": [
224
- "verification-worker",
225
- "verification-reviewer",
226
- "remote-actions"
227
- ]
228
- }
229
231
  }
230
232
  }
231
233
  },
@@ -546,16 +548,54 @@
546
548
  "type": "integer",
547
549
  "minimum": 1000,
548
550
  "maximum": 30000,
549
- "default": 5000
551
+ "default": 30000
550
552
  }
551
553
  }
552
554
  }
553
555
  ]
554
556
  },
557
+ "workspace": {
558
+ "type": "object",
559
+ "additionalProperties": false,
560
+ "required": ["bindOn"],
561
+ "properties": {
562
+ "bindOn": {
563
+ "type": "array",
564
+ "minItems": 1,
565
+ "uniqueItems": true,
566
+ "items": {
567
+ "$ref": "#/$defs/identifier"
568
+ }
569
+ },
570
+ "allowedRoots": {
571
+ "type": "array",
572
+ "minItems": 1,
573
+ "maxItems": 32,
574
+ "uniqueItems": true,
575
+ "default": ["."],
576
+ "items": {
577
+ "$ref": "#/$defs/relativePath"
578
+ }
579
+ }
580
+ }
581
+ },
555
582
  "step": {
556
583
  "type": "object",
557
584
  "additionalProperties": false,
558
585
  "required": ["prompt", "transitions"],
586
+ "allOf": [
587
+ {
588
+ "if": {
589
+ "required": ["workspace"]
590
+ },
591
+ "then": {
592
+ "required": ["subagent"],
593
+ "not": {
594
+ "required": ["gate"]
595
+ }
596
+ }
597
+ }
598
+ ],
559
599
  "properties": {
560
600
  "title": {
561
601
  "type": "string",
@@ -586,6 +626,9 @@
586
626
  },
587
627
  "gate": {
588
628
  "$ref": "#/$defs/gate"
629
+ },
630
+ "workspace": {
631
+ "$ref": "#/$defs/workspace"
589
632
  }
590
633
  }
591
634
  }
@@ -3,6 +3,7 @@
3
3
  */
4
4
  export const HARNESS_COMMAND_NAMES = [
5
5
  'workflow-abort',
6
+ 'workflow-doctor',
6
7
  'workflow-list',
7
8
  'workflow-pause',
8
9
  'workflow-reload',
package/src/commands.ts CHANGED
@@ -12,6 +12,11 @@ export type WorkflowCommandController = {
12
12
  readonly workflowIds: () => ReadonlyArray<string>;
13
13
  /** Lists loaded workflows. */
14
14
  readonly list: (context: ExtensionCommandContext) => Promise<void>;
15
+ /** Diagnoses workflow graph liveness. */
16
+ readonly doctor: (
17
+ workflowId: string,
18
+ context: ExtensionCommandContext,
19
+ ) => Promise<void>;
15
20
  /** Starts a workflow. */
16
21
  readonly start: (
17
22
  workflowId: string,
@@ -23,8 +28,11 @@ export type WorkflowCommandController = {
23
28
  reason: string,
24
29
  context: ExtensionCommandContext,
25
30
  ) => Promise<void>;
26
- /** Resumes the paused workflow. */
27
- readonly resume: (context: ExtensionCommandContext) => Promise<void>;
31
+ /** Resumes the paused workflow with optional user guidance. */
32
+ readonly resume: (
33
+ input: string,
34
+ context: ExtensionCommandContext,
35
+ ) => Promise<void>;
28
36
  /** Aborts the active workflow. */
29
37
  readonly abort: (
30
38
  reason: string,
@@ -98,6 +106,25 @@ export function createHarnessCommands(
98
106
  handler: async (_args, context) => controller.list(context),
99
107
  },
100
108
  },
109
+ {
110
+ name: 'workflow-doctor',
111
+ options: {
112
+ description:
113
+ 'Check workflow completion paths, unreachable steps, and cycles',
114
+ getArgumentCompletions: (prefix) => {
115
+ const completions = controller
116
+ .workflowIds()
117
+ .filter((workflowId) => workflowId.startsWith(prefix))
118
+ .map((workflowId) => ({
119
+ value: workflowId,
120
+ label: workflowId,
121
+ }));
122
+ return completions.length > 0 ? completions : null;
123
+ },
124
+ handler: async (args, context) =>
125
+ controller.doctor(args.trim(), context),
126
+ },
127
+ },
101
128
  createStartCommand(controller),
102
129
  {
103
130
  name: 'workflow-pause',
@@ -110,8 +137,10 @@ export function createHarnessCommands(
110
137
  {
111
138
  name: 'workflow-resume',
112
139
  options: {
113
- description: 'Reload configuration and resume the paused workflow',
114
- handler: async (_args, context) => controller.resume(context),
140
+ description:
141
+ 'Reload configuration and resume: /workflow-resume [guidance]',
142
+ handler: async (input, context) =>
143
+ controller.resume(input.trim(), context),
115
144
  },
116
145
  },
117
146
  {
@@ -28,17 +28,10 @@ function bashWithinCeiling(
28
28
  if (ceiling.mode === 'unrestricted') return true;
29
29
  if (requested.mode === 'deny') return true;
30
30
  if (ceiling.mode === 'deny') return false;
31
- if (ceiling.mode === 'read-only') return requested.mode === 'read-only';
32
31
  if (requested.mode !== 'allow-list') return false;
33
32
 
34
33
  const allowedRules = new Set(ceiling.allow.map(ruleKey));
35
- const allowedSources = new Set(ceiling.approvedSources ?? []);
36
- return (
37
- requested.allow.every((rule) => allowedRules.has(ruleKey(rule))) &&
38
- (requested.approvedSources ?? []).every((source) =>
39
- allowedSources.has(source),
40
- )
41
- );
34
+ return requested.allow.every((rule) => allowedRules.has(ruleKey(rule)));
42
35
  }
43
36
 
44
37
  function turnBudgetErrors(
@@ -124,6 +117,11 @@ export function checkWorkflowAgainstCeiling(
124
117
  return Object.entries(workflow.steps).flatMap(([stepId, step]) => {
125
118
  const path = `workflow.steps.${stepId}.permissions`;
126
119
  return [
120
+ ...(Object.hasOwn(step, 'workspace')
121
+ ? [
122
+ `workflow.steps.${stepId}.workspace: workspace binding is unavailable to project workflows`,
123
+ ]
124
+ : []),
127
125
  ...step.permissions.tools
128
126
  .filter((tool) => !ceiling.tools.includes(tool))
129
127
  .map(
@@ -2,6 +2,10 @@ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
2
2
  import { digest } from '../digest.ts';
3
3
  import { createDiagnostic, errorCode, errorMessage } from './diagnostics.ts';
4
4
  import type { ConfigFileSystem, LoadedDirectory } from './load-types.ts';
5
+ import {
6
+ digestWorkflowStep,
7
+ digestWorkflowStepStructure,
8
+ } from './step-digests.ts';
5
9
  import type {
6
10
  LoadedWorkflow,
7
11
  WorkflowDefinition,
@@ -81,7 +85,13 @@ async function loadWorkflowFile(
81
85
  const stepDigests = Object.fromEntries(
82
86
  Object.entries(definition.steps).map(([stepId, step]) => [
83
87
  stepId,
84
- digest({ step, prompt: prompts[stepId] }),
88
+ digestWorkflowStep(step, prompts[stepId] ?? ''),
89
+ ]),
90
+ );
91
+ const stepStructuralDigests = Object.fromEntries(
92
+ Object.entries(definition.steps).map(([stepId, step]) => [
93
+ stepId,
94
+ digestWorkflowStepStructure(step),
85
95
  ]),
86
96
  );
87
97
  return {
@@ -89,6 +99,7 @@ async function loadWorkflowFile(
89
99
  prompts,
90
100
  digest: digest({ definition, prompts }),
91
101
  stepDigests,
102
+ stepStructuralDigests,
92
103
  sourcePath,
93
104
  sourceKind,
94
105
  };
@@ -0,0 +1,19 @@
1
+ import { digest } from '../digest.ts';
2
+ import type { WorkflowStep } from './types.ts';
3
+
4
+ /**
5
+ * Digests a normalized workflow step together with its resolved prompt text.
6
+ */
7
+ export function digestWorkflowStep(step: WorkflowStep, prompt: string): string {
8
+ return digest({ step, prompt });
9
+ }
10
+
11
+ /**
12
+ * Digests every normalized workflow-step field except the prompt specification.
13
+ */
14
+ export function digestWorkflowStepStructure(step: WorkflowStep): string {
15
+ const structure = Object.fromEntries(
16
+ Object.entries(step).filter(([key]) => key !== 'prompt'),
17
+ );
18
+ return digest(structure);
19
+ }
@@ -4,14 +4,14 @@ export const WORKFLOW_SCHEMA_VERSION = 1 as const;
4
4
  export const DEFAULT_STATUS_SHORTCUT = 'ctrl+alt+w' as const satisfies KeyId;
5
5
  export const SUBAGENT_RUNTIME_NAME_PATTERN =
6
6
  /^[a-z0-9][a-z0-9-]*(?:\.[a-z0-9][a-z0-9-]*)*$/;
7
+ export const MAX_WORKSPACE_PATH_CHARS = 4_096;
8
+ export const MAX_WORKSPACE_ALLOWED_ROOTS = 32;
7
9
 
8
10
  export const TERMINAL_TARGETS = ['$done', '$pause'] as const;
9
11
  export type TerminalTarget = (typeof TERMINAL_TARGETS)[number];
10
12
  export type StepTarget = string;
11
13
 
12
- export type BashMode = 'deny' | 'read-only' | 'allow-list' | 'unrestricted';
13
- export type BashApprovalSource =
14
- 'verification-worker' | 'verification-reviewer' | 'remote-actions';
14
+ export type BashMode = 'deny' | 'allow-list' | 'unrestricted';
15
15
 
16
16
  export type BashRule = {
17
17
  readonly executable: string;
@@ -21,11 +21,6 @@ export type BashRule = {
21
21
  export type BashPermission = {
22
22
  readonly mode: BashMode;
23
23
  readonly allow: ReadonlyArray<BashRule>;
24
- /**
25
- * Exact commands extracted from the most recent human-approved artifact.
26
- * They supplement `allow` only inside the correlated step execution.
27
- */
28
- readonly approvedSources?: ReadonlyArray<BashApprovalSource>;
29
24
  };
30
25
 
31
26
  export type StepPermissions = {
@@ -70,9 +65,9 @@ export type StepSubagent = {
70
65
  readonly toolBudget?: SubagentToolBudget;
71
66
  readonly artifacts: boolean;
72
67
  /**
73
- * Authorizes the bounded automatic recovery sequence in Bash modes broader
74
- * than read-only. Every failed attempt must still have a complete,
75
- * mutation-safe replay audit.
68
+ * Authorizes bounded automatic recovery when Bash can execute commands.
69
+ * Every failed attempt must still have a complete, mutation-safe replay
70
+ * audit.
76
71
  */
77
72
  readonly retryToolFailures: boolean;
78
73
  };
@@ -97,6 +92,13 @@ export type PlannotatorGate = GateDefinition & {
97
92
 
98
93
  export type WorkflowGate = PromptGate | PlannotatorGate;
99
94
 
95
+ export type StepWorkspaceBinding = {
96
+ /** Outcomes whose result establishes the workspace for later steps. */
97
+ readonly bindOn: ReadonlyArray<string>;
98
+ /** Paths relative to the run-start directory that may contain the workspace. */
99
+ readonly allowedRoots: ReadonlyArray<string>;
100
+ };
101
+
100
102
  export type WorkflowStep = {
101
103
  readonly title: string;
102
104
  readonly prompt: PromptSpec;
@@ -106,6 +108,8 @@ export type WorkflowStep = {
106
108
  readonly requires: StepRequirements;
107
109
  readonly transitions: Readonly<Record<string, StepTarget>>;
108
110
  readonly gate?: WorkflowGate;
111
+ /** Optional one-time workspace binding produced by this delegated step. */
112
+ readonly workspace?: StepWorkspaceBinding;
109
113
  };
110
114
 
111
115
  export type WorkflowDefinition = {
@@ -126,6 +130,8 @@ export type LoadedWorkflow = {
126
130
  readonly prompts: Readonly<Record<string, string>>;
127
131
  readonly digest: string;
128
132
  readonly stepDigests: Readonly<Record<string, string>>;
133
+ /** Per-step digest excluding only the prompt specification and resolved text. */
134
+ readonly stepStructuralDigests: Readonly<Record<string, string>>;
129
135
  readonly sourcePath: string;
130
136
  readonly sourceKind: WorkflowSourceKind;
131
137
  };
@@ -1,6 +1,5 @@
1
1
  import {
2
2
  EMPTY_PERMISSIONS,
3
- type BashApprovalSource,
4
3
  type BashMode,
5
4
  type BashPermission,
6
5
  type BashRule,
@@ -8,7 +7,6 @@ import {
8
7
  type StepRequirements,
9
8
  } from '../types.ts';
10
9
  import {
11
- BASH_APPROVAL_SOURCE_PATTERN,
12
10
  EXECUTABLE_PATTERN,
13
11
  isJsonObject,
14
12
  MCP_SELECTOR_PATTERN,
@@ -20,16 +18,6 @@ import {
20
18
  type ValidationErrors,
21
19
  } from './shared.ts';
22
20
 
23
- const BASH_APPROVAL_SOURCES = [
24
- 'verification-worker',
25
- 'verification-reviewer',
26
- 'remote-actions',
27
- ] as const satisfies ReadonlyArray<BashApprovalSource>;
28
-
29
- function isBashApprovalSource(value: string): value is BashApprovalSource {
30
- return BASH_APPROVAL_SOURCES.some((source) => source === value);
31
- }
32
-
33
21
  function emptyPermissions(): StepPermissions {
34
22
  return {
35
23
  tools: [],
@@ -129,18 +117,13 @@ function parseBashPermission(
129
117
  errors.push(`${path}: expected an object`);
130
118
  return { ...EMPTY_PERMISSIONS.bash, allow: [] };
131
119
  }
132
- rejectUnknownKeys(value, ['mode', 'allow', 'approvedSources'], path, errors);
120
+ rejectUnknownKeys(value, ['mode', 'allow'], path, errors);
133
121
 
134
122
  const mode = readString(value.mode, `${path}.mode`, errors);
135
123
  const isValidMode =
136
- mode === 'deny' ||
137
- mode === 'read-only' ||
138
- mode === 'allow-list' ||
139
- mode === 'unrestricted';
124
+ mode === 'deny' || mode === 'allow-list' || mode === 'unrestricted';
140
125
  if (!isValidMode) {
141
- errors.push(
142
- `${path}.mode: expected deny, read-only, allow-list, or unrestricted`,
143
- );
126
+ errors.push(`${path}.mode: expected deny, allow-list, or unrestricted`);
144
127
  }
145
128
 
146
129
  const allow = Array.isArray(value.allow)
@@ -152,40 +135,17 @@ function parseBashPermission(
152
135
  errors.push(`${path}.allow: expected an array`);
153
136
  }
154
137
 
155
- const approvedSources = (
156
- value.approvedSources === undefined
157
- ? []
158
- : readStringList(
159
- value.approvedSources,
160
- `${path}.approvedSources`,
161
- errors,
162
- BASH_APPROVAL_SOURCE_PATTERN,
163
- )
164
- ).filter(isBashApprovalSource);
165
-
166
138
  const normalizedMode: BashMode = isValidMode ? mode : 'deny';
167
139
  if (normalizedMode !== 'allow-list' && allow.length > 0) {
168
140
  errors.push(`${path}.allow: only valid when mode is "allow-list"`);
169
141
  }
170
- if (
171
- normalizedMode === 'allow-list' &&
172
- allow.length === 0 &&
173
- approvedSources.length === 0
174
- ) {
175
- errors.push(
176
- `${path}: allow-list mode requires an allow rule or an approved command source`,
177
- );
178
- }
179
- if (normalizedMode !== 'allow-list' && approvedSources.length > 0) {
180
- errors.push(
181
- `${path}.approvedSources: only valid when mode is "allow-list"`,
182
- );
142
+ if (normalizedMode === 'allow-list' && allow.length === 0) {
143
+ errors.push(`${path}: allow-list mode requires an allow rule`);
183
144
  }
184
145
 
185
146
  return {
186
147
  mode: normalizedMode,
187
148
  allow,
188
- ...(approvedSources.length > 0 ? { approvedSources } : {}),
189
149
  };
190
150
  }
191
151
 
@@ -5,7 +5,10 @@ const PROMPT_VARIABLES = new Set([
5
5
  'step.id',
6
6
  'step.title',
7
7
  'last.summary',
8
+ 'reviewed.artifact',
9
+ 'reviewed.feedback',
8
10
  'gate.feedback',
11
+ 'resume.input',
9
12
  ]);
10
13
 
11
14
  /** Validate template variables embedded in resolved prompt text. */
@@ -12,8 +12,6 @@ export const TOOL_PATTERN = /^[A-Za-z0-9_.:-]+$/;
12
12
  export const RESOURCE_SELECTOR_PATTERN = /^[A-Za-z0-9_@./:+-]+$/;
13
13
  export const MCP_SELECTOR_PATTERN = /^[A-Za-z0-9_.:-]+(?:\/[A-Za-z0-9_.:-]+)?$/;
14
14
  export const EXECUTABLE_PATTERN = /^[A-Za-z0-9_./+-]+$/;
15
- export const BASH_APPROVAL_SOURCE_PATTERN =
16
- /^(verification-worker|verification-reviewer|remote-actions)$/;
17
15
 
18
16
  type StringOptions = {
19
17
  readonly pattern?: RegExp;
@@ -1,10 +1,19 @@
1
- import type { PromptSpec, WorkflowGate, WorkflowStep } from '../types.ts';
1
+ import { isAbsolute, win32 } from 'node:path';
2
+ import {
3
+ MAX_WORKSPACE_ALLOWED_ROOTS,
4
+ MAX_WORKSPACE_PATH_CHARS,
5
+ type PromptSpec,
6
+ type StepWorkspaceBinding,
7
+ type WorkflowGate,
8
+ type WorkflowStep,
9
+ } from '../types.ts';
2
10
  import { parsePermissions, parseRequirements } from './permissions.ts';
3
11
  import {
4
12
  isJsonObject,
5
13
  OUTCOME_PATTERN,
6
14
  readInteger,
7
15
  readString,
16
+ readStringList,
8
17
  rejectUnknownKeys,
9
18
  type ValidationErrors,
10
19
  } from './shared.ts';
@@ -135,7 +144,7 @@ function parseGate(
135
144
  rejectedOutcome,
136
145
  timeoutMs: readInteger(
137
146
  value.timeoutMs,
138
- 5_000,
147
+ 30_000,
139
148
  `${path}.timeoutMs`,
140
149
  errors,
141
150
  { min: 1_000, max: 30_000 },
@@ -143,6 +152,85 @@ function parseGate(
143
152
  };
144
153
  }
145
154
 
155
+ function parseWorkspaceRoots(
156
+ value: unknown,
157
+ path: string,
158
+ errors: ValidationErrors,
159
+ ): Array<string> {
160
+ if (value === undefined) return ['.'];
161
+ if (!Array.isArray(value)) {
162
+ errors.push(`${path}: expected an array of relative paths`);
163
+ return [];
164
+ }
165
+ if (value.length > MAX_WORKSPACE_ALLOWED_ROOTS) {
166
+ errors.push(
167
+ `${path}: at most ${MAX_WORKSPACE_ALLOWED_ROOTS} relative paths are allowed`,
168
+ );
169
+ }
170
+
171
+ const roots = value
172
+ .slice(0, MAX_WORKSPACE_ALLOWED_ROOTS)
173
+ .reduce<Array<string>>((result, item, index) => {
174
+ const itemPath = `${path}[${index}]`;
175
+ if (typeof item !== 'string' || !item || item.trim() !== item) {
176
+ errors.push(`${itemPath}: expected a non-empty relative path`);
177
+ return result;
178
+ }
179
+ const root = item;
180
+ if (root.length > MAX_WORKSPACE_PATH_CHARS) {
181
+ errors.push(
182
+ `${itemPath}: path exceeds ${MAX_WORKSPACE_PATH_CHARS} characters`,
183
+ );
184
+ return result;
185
+ }
186
+ if (
187
+ root.includes('\0') ||
188
+ isAbsolute(root) ||
189
+ win32.parse(root).root !== ''
190
+ ) {
191
+ errors.push(`${itemPath}: expected a relative path`);
192
+ return result;
193
+ }
194
+ if (result.includes(root)) {
195
+ errors.push(`${itemPath}: duplicate value "${root}"`);
196
+ return result;
197
+ }
198
+ return [...result, root];
199
+ }, []);
200
+ if (roots.length === 0) {
201
+ errors.push(`${path}: at least one relative path is required`);
202
+ }
203
+ return roots;
204
+ }
205
+
206
+ function parseWorkspace(
207
+ value: unknown,
208
+ path: string,
209
+ errors: ValidationErrors,
210
+ ): StepWorkspaceBinding | undefined {
211
+ if (value === undefined) return undefined;
212
+ if (!isJsonObject(value)) {
213
+ errors.push(`${path}: expected an object`);
214
+ return undefined;
215
+ }
216
+ rejectUnknownKeys(value, ['bindOn', 'allowedRoots'], path, errors);
217
+ const bindOn = readStringList(
218
+ value.bindOn,
219
+ `${path}.bindOn`,
220
+ errors,
221
+ OUTCOME_PATTERN,
222
+ );
223
+ if (bindOn.length === 0) {
224
+ errors.push(`${path}.bindOn: at least one outcome is required`);
225
+ }
226
+ const allowedRoots = parseWorkspaceRoots(
227
+ value.allowedRoots,
228
+ `${path}.allowedRoots`,
229
+ errors,
230
+ );
231
+ return { bindOn, allowedRoots };
232
+ }
233
+
146
234
  /** Parse one workflow step and validate relationships within that step. */
147
235
  export function parseWorkflowStep(
148
236
  value: unknown,
@@ -164,6 +252,7 @@ export function parseWorkflowStep(
164
252
  'requires',
165
253
  'transitions',
166
254
  'gate',
255
+ 'workspace',
167
256
  ],
168
257
  path,
169
258
  errors,
@@ -196,6 +285,11 @@ export function parseWorkflowStep(
196
285
  errors,
197
286
  );
198
287
  const gate = parseGate(value.gate, `${path}.gate`, errors);
288
+ const workspace = parseWorkspace(
289
+ value.workspace,
290
+ `${path}.workspace`,
291
+ errors,
292
+ );
199
293
 
200
294
  if (gate) {
201
295
  if (!Object.hasOwn(transitions, gate.approvedOutcome)) {
@@ -214,6 +308,15 @@ export function parseWorkflowStep(
214
308
  );
215
309
  }
216
310
  }
311
+ if (workspace) {
312
+ workspace.bindOn.forEach((outcome) => {
313
+ if (!Object.hasOwn(transitions, outcome)) {
314
+ errors.push(
315
+ `${path}.workspace.bindOn: unknown transition outcome "${outcome}"`,
316
+ );
317
+ }
318
+ });
319
+ }
217
320
 
218
321
  if (!title || !prompt) return undefined;
219
322
  return {
@@ -224,5 +327,6 @@ export function parseWorkflowStep(
224
327
  requires,
225
328
  transitions,
226
329
  ...(gate ? { gate } : {}),
330
+ ...(workspace ? { workspace } : {}),
227
331
  };
228
332
  }