@wichayutdew/pi-workflows 2.5.1 → 2.7.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 (103) hide show
  1. package/README.md +34 -1
  2. package/dist/index.js +1315 -2467
  3. package/examples/starter-kit/agents/planner.md +11 -0
  4. package/examples/starter-kit/agents/reviewer.md +10 -0
  5. package/examples/starter-kit/agents/scout.md +10 -0
  6. package/examples/starter-kit/agents/worker.md +11 -0
  7. package/examples/starter-kit/agents/workspace-preparer.md +10 -0
  8. package/examples/starter-kit/investigate.workflow.yaml +84 -0
  9. package/examples/starter-kit/jira.workflow.yaml +75 -0
  10. package/examples/starter-kit/mr-comment.workflow.yaml +86 -93
  11. package/examples/starter-kit/mr-review.workflow.yaml +52 -88
  12. package/examples/starter-kit/settings.yaml +4 -0
  13. package/examples/starter-kit/steps/investigate/investigate.md +40 -0
  14. package/examples/starter-kit/steps/investigate/retrieve.md +28 -0
  15. package/examples/starter-kit/steps/investigate/validate.md +20 -0
  16. package/examples/starter-kit/steps/jira/create.md +25 -0
  17. package/examples/starter-kit/steps/jira/draft.md +18 -0
  18. package/examples/starter-kit/steps/jira/plan.md +30 -0
  19. package/examples/starter-kit/steps/mr-comment/checkout-source.md +14 -0
  20. package/examples/starter-kit/steps/mr-comment/fetch.md +11 -28
  21. package/examples/starter-kit/steps/mr-comment/implement.md +9 -26
  22. package/examples/starter-kit/steps/mr-comment/plan.md +46 -55
  23. package/examples/starter-kit/steps/mr-comment/publish.md +11 -31
  24. package/examples/starter-kit/steps/mr-comment/verify.md +7 -34
  25. package/examples/starter-kit/steps/mr-review/fetch.md +13 -21
  26. package/examples/starter-kit/steps/mr-review/publish-approved.md +18 -0
  27. package/examples/starter-kit/steps/mr-review/review-for-approval.md +53 -0
  28. package/examples/starter-kit/steps/mr-review/verify-published.md +16 -0
  29. package/examples/starter-kit/steps/shared/prepare-workspace.md +11 -89
  30. package/examples/starter-kit/steps/shared/publish-remote.md +16 -0
  31. package/examples/starter-kit/steps/ticket/implement.md +10 -23
  32. package/examples/starter-kit/steps/ticket/plan.md +49 -98
  33. package/examples/starter-kit/steps/ticket/verify.md +14 -65
  34. package/examples/starter-kit/steps/work/implement.md +11 -22
  35. package/examples/starter-kit/steps/work/plan.md +43 -59
  36. package/examples/starter-kit/steps/work/verify.md +14 -25
  37. package/examples/starter-kit/ticket.workflow.yaml +57 -61
  38. package/examples/starter-kit/work.workflow.yaml +49 -57
  39. package/package.json +9 -19
  40. package/schemas/workflow.schema.json +63 -15
  41. package/scripts/patch-herdr-agent-state.mjs +36 -0
  42. package/src/agents/profile.ts +98 -0
  43. package/src/config/ceiling.ts +0 -82
  44. package/src/config/types.ts +15 -53
  45. package/src/config/validation/settings.ts +2 -14
  46. package/src/config/validation/step.ts +129 -8
  47. package/src/config/validation/workflow.ts +1 -10
  48. package/src/engine/run-workflow-validation.ts +0 -3
  49. package/src/engine/state-types.ts +1 -1
  50. package/src/harness/action-context.ts +1 -13
  51. package/src/harness/artifact-contract.ts +46 -0
  52. package/src/harness/delegation-control-actions.ts +4 -63
  53. package/src/harness/delegation-plan.ts +26 -80
  54. package/src/harness/delegation-response-actions.ts +22 -135
  55. package/src/harness/dependencies.ts +1 -12
  56. package/src/harness/gate-submission-action.ts +28 -0
  57. package/src/harness/status-actions.ts +20 -0
  58. package/src/harness/step-execution-actions.ts +2 -9
  59. package/src/harness/types.ts +2 -40
  60. package/src/harness.ts +2 -17
  61. package/src/herdr-workflow-state.ts +65 -0
  62. package/src/index.ts +4 -9
  63. package/src/integrations/subagents/child-policy-validation.ts +6 -7
  64. package/src/integrations/subagents/child-runtime-dependencies.ts +1 -1
  65. package/src/integrations/subagents/child-runtime-policy.ts +1 -7
  66. package/src/integrations/subagents/child-runtime.ts +18 -9
  67. package/src/integrations/subagents/client.ts +240 -98
  68. package/src/integrations/subagents/protocol-events.ts +32 -15
  69. package/src/integrations/subagents/protocol.ts +1 -1
  70. package/src/preflight.ts +0 -8
  71. package/src/prompt/main-workflow-notice.ts +8 -15
  72. package/src/prompt/step-task.ts +7 -6
  73. package/src/workflow-status/format-status.ts +5 -1
  74. package/src/workflow-status/render-step-detail.ts +94 -0
  75. package/src/workflow-status/types.ts +1 -0
  76. package/src/workflow-status/view.ts +49 -14
  77. package/agents/step.md +0 -32
  78. package/examples/mr-comments.workflow.yaml +0 -125
  79. package/examples/prompts/mr-comments/implement.md +0 -17
  80. package/examples/prompts/mr-comments/inspect.md +0 -5
  81. package/examples/prompts/mr-comments/plan.md +0 -54
  82. package/examples/prompts/mr-comments/verify.md +0 -9
  83. package/examples/settings.yaml +0 -27
  84. package/examples/starter-kit/steps/mr-review/publish.md +0 -48
  85. package/examples/starter-kit/steps/mr-review/review.md +0 -76
  86. package/examples/starter-kit/steps/mr-review/verify.md +0 -35
  87. package/src/config/validation/subagent.ts +0 -288
  88. package/src/harness/delegation-failure.ts +0 -248
  89. package/src/harness/delegation-recovery-validation.ts +0 -161
  90. package/src/harness/delegation-retry-policy.ts +0 -120
  91. package/src/integrations/subagents/client-delegation.ts +0 -181
  92. package/src/integrations/subagents/client-messages.ts +0 -66
  93. package/src/integrations/subagents/client-types.ts +0 -36
  94. package/src/integrations/subagents/diagnostic-format.ts +0 -45
  95. package/src/integrations/subagents/diagnostic-text.ts +0 -114
  96. package/src/integrations/subagents/diagnostic-types.ts +0 -83
  97. package/src/integrations/subagents/diagnostics.ts +0 -26
  98. package/src/integrations/subagents/failure-correlation.ts +0 -285
  99. package/src/integrations/subagents/failure-transcript.ts +0 -252
  100. package/src/integrations/subagents/hidden-bash-failure.ts +0 -98
  101. package/src/integrations/subagents/replay-audit.ts +0 -146
  102. package/src/integrations/subagents/replay-safety.ts +0 -67
  103. package/src/integrations/subagents/session-diagnostics.ts +0 -357
@@ -0,0 +1,98 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { parse } from 'yaml';
5
+ import { defaultUserWorkflowDirectory } from '../config/load.ts';
6
+
7
+ export const THINKING_LEVELS = [
8
+ 'off',
9
+ 'minimal',
10
+ 'low',
11
+ 'medium',
12
+ 'high',
13
+ 'xhigh',
14
+ 'max',
15
+ ] as const;
16
+
17
+ export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
18
+
19
+ export type AgentProfile = {
20
+ readonly prompt: string;
21
+ readonly model?: string;
22
+ readonly thinking?: ThinkingLevel;
23
+ };
24
+
25
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
26
+ value !== null && typeof value === 'object' && !Array.isArray(value);
27
+
28
+ /** Parses the optional YAML frontmatter and role prompt in one agent profile. */
29
+ export function parseAgentProfile(source: string, name: string): AgentProfile {
30
+ if (!source.startsWith('---\n')) return { prompt: source.trim() };
31
+ const end = source.indexOf('\n---\n', 4);
32
+ if (end === -1) {
33
+ throw new Error(`workflow agent profile is invalid: ${name}`);
34
+ }
35
+ const metadata: unknown = parse(source.slice(4, end));
36
+ if (!isRecord(metadata)) {
37
+ throw new Error(
38
+ `workflow agent profile metadata must be an object: ${name}`,
39
+ );
40
+ }
41
+ const unknownKey = Object.keys(metadata).find(
42
+ (key) => key !== 'model' && key !== 'thinking',
43
+ );
44
+ if (unknownKey) {
45
+ throw new Error(
46
+ `workflow agent profile has unknown metadata "${unknownKey}": ${name}`,
47
+ );
48
+ }
49
+ const model = metadata.model;
50
+ if (model !== undefined && (typeof model !== 'string' || !model.trim())) {
51
+ throw new Error(
52
+ `workflow agent profile model must be a non-empty string: ${name}`,
53
+ );
54
+ }
55
+ const thinking = metadata.thinking;
56
+ if (
57
+ thinking !== undefined &&
58
+ (typeof thinking !== 'string' ||
59
+ !THINKING_LEVELS.includes(thinking as ThinkingLevel))
60
+ ) {
61
+ throw new Error(
62
+ `workflow agent profile thinking must be one of ${THINKING_LEVELS.join(', ')}: ${name}`,
63
+ );
64
+ }
65
+ const prompt = source.slice(end + 5).trim();
66
+ if (!prompt)
67
+ throw new Error(`workflow agent profile prompt is empty: ${name}`);
68
+ return {
69
+ prompt,
70
+ ...(typeof model === 'string' ? { model: model.trim() } : {}),
71
+ ...(typeof thinking === 'string'
72
+ ? { thinking: thinking as ThinkingLevel }
73
+ : {}),
74
+ };
75
+ }
76
+
77
+ /** Loads a user-owned agent profile, with a bundled prompt-only fallback. */
78
+ export function loadAgentProfile(
79
+ name: string,
80
+ userDirectory = defaultUserWorkflowDirectory(),
81
+ ): AgentProfile {
82
+ const userPath = join(userDirectory, 'agents', `${name}.md`);
83
+ const bundledUrl = new URL(
84
+ `../../examples/starter-kit/agents/${name}.md`,
85
+ import.meta.url,
86
+ );
87
+ const path = existsSync(userPath) ? userPath : fileURLToPath(bundledUrl);
88
+ try {
89
+ return parseAgentProfile(readFileSync(path, 'utf8'), name);
90
+ } catch (error) {
91
+ if (error instanceof Error && error.message.startsWith('workflow agent')) {
92
+ throw error;
93
+ }
94
+ throw new Error(`workflow agent profile is unavailable: ${name}`, {
95
+ cause: error,
96
+ });
97
+ }
98
+ }
@@ -2,8 +2,6 @@ import type {
2
2
  BashPermission,
3
3
  BashRule,
4
4
  PermissionCeiling,
5
- StepSubagent,
6
- SubagentPermissionCeiling,
7
5
  WorkflowDefinition,
8
6
  } from './types.ts';
9
7
 
@@ -34,81 +32,6 @@ function bashWithinCeiling(
34
32
  return requested.allow.every((rule) => allowedRules.has(ruleKey(rule)));
35
33
  }
36
34
 
37
- function turnBudgetErrors(
38
- subagent: StepSubagent,
39
- ceiling: SubagentPermissionCeiling,
40
- path: string,
41
- ): Array<string> {
42
- if (!subagent.turnBudget) {
43
- return [`${path}.turnBudget: required for a project workflow`];
44
- }
45
- return [
46
- ...(subagent.turnBudget.maxTurns > ceiling.maxTurns
47
- ? [`${path}.turnBudget.maxTurns: exceeds the user permission ceiling`]
48
- : []),
49
- ...((subagent.turnBudget.graceTurns ?? 0) > ceiling.maxGraceTurns
50
- ? [`${path}.turnBudget.graceTurns: exceeds the user permission ceiling`]
51
- : []),
52
- ];
53
- }
54
-
55
- function toolBudgetErrors(
56
- subagent: StepSubagent,
57
- ceiling: SubagentPermissionCeiling,
58
- path: string,
59
- ): Array<string> {
60
- if (!subagent.toolBudget) {
61
- return [`${path}.toolBudget: required for a project workflow`];
62
- }
63
- return [
64
- ...(subagent.toolBudget.hard > ceiling.maxToolCalls
65
- ? [`${path}.toolBudget.hard: exceeds the user permission ceiling`]
66
- : []),
67
- ...(subagent.toolBudget.block !== '*'
68
- ? [`${path}.toolBudget.block: must be "*" for a project workflow`]
69
- : []),
70
- ];
71
- }
72
-
73
- function subagentErrors(
74
- subagent: StepSubagent | undefined,
75
- ceiling: SubagentPermissionCeiling | undefined,
76
- path: string,
77
- ): Array<string> {
78
- if (!subagent) return [];
79
- if (!ceiling) {
80
- return [`${path}: subagent execution exceeds the user permission ceiling`];
81
- }
82
- return [
83
- ...(!ceiling.agents.includes(subagent.agent)
84
- ? [
85
- `${path}.agent: "${subagent.agent}" exceeds the user permission ceiling`,
86
- ]
87
- : []),
88
- ...(!ceiling.contexts.includes(subagent.context)
89
- ? [
90
- `${path}.context: "${subagent.context}" exceeds the user permission ceiling`,
91
- ]
92
- : []),
93
- ...(subagent.model && !ceiling.models.includes(subagent.model)
94
- ? [
95
- `${path}.model: "${subagent.model}" exceeds the user permission ceiling`,
96
- ]
97
- : []),
98
- ...(subagent.timeoutMs > ceiling.maxTimeoutMs
99
- ? [`${path}.timeoutMs: exceeds the user permission ceiling`]
100
- : []),
101
- ...(subagent.artifacts && !ceiling.artifacts
102
- ? [`${path}.artifacts: exceeds the user permission ceiling`]
103
- : []),
104
- ...(subagent.retryToolFailures && !ceiling.retryToolFailures
105
- ? [`${path}.retryToolFailures: exceeds the user permission ceiling`]
106
- : []),
107
- ...turnBudgetErrors(subagent, ceiling, path),
108
- ...toolBudgetErrors(subagent, ceiling, path),
109
- ];
110
- }
111
-
112
35
  /** Return every project-workflow permission that exceeds the user ceiling. */
113
36
  export function checkWorkflowAgainstCeiling(
114
37
  workflow: WorkflowDefinition,
@@ -149,11 +72,6 @@ export function checkWorkflowAgainstCeiling(
149
72
  ...(!bashWithinCeiling(step.permissions.bash, ceiling.bash)
150
73
  ? [`${path}.bash: exceeds the user permission ceiling`]
151
74
  : []),
152
- ...subagentErrors(
153
- step.subagent,
154
- ceiling.subagent,
155
- `workflow.steps.${stepId}.subagent`,
156
- ),
157
75
  ];
158
76
  });
159
77
  }
@@ -2,7 +2,7 @@ import type { KeyId } from '@earendil-works/pi-tui';
2
2
 
3
3
  export const WORKFLOW_SCHEMA_VERSION = 1 as const;
4
4
  export const DEFAULT_STATUS_SHORTCUT = 'ctrl+alt+w' as const satisfies KeyId;
5
- export const SUBAGENT_RUNTIME_NAME_PATTERN =
5
+ export const AGENT_PROFILE_NAME_PATTERN =
6
6
  /^[a-z0-9][a-z0-9-]*(?:\.[a-z0-9][a-z0-9-]*)*$/;
7
7
  export const MAX_WORKSPACE_PATH_CHARS = 4_096;
8
8
  export const MAX_WORKSPACE_ALLOWED_ROOTS = 32;
@@ -41,44 +41,27 @@ export type StepRequirements = {
41
41
  readonly skills: ReadonlyArray<string>;
42
42
  };
43
43
 
44
- export type SubagentContext = 'fresh';
45
-
46
- export type SubagentTurnBudget = {
47
- readonly maxTurns: number;
48
- readonly graceTurns?: number;
49
- };
50
-
51
- export type SubagentToolBudget = {
52
- readonly hard: number;
53
- readonly soft?: number;
54
- readonly block?: ReadonlyArray<string> | '*';
55
- };
56
-
57
- export type StepSubagent = {
58
- /** Pi Subagents agent profile launched for this isolated step. */
59
- readonly agent: string;
60
- /** Workflow steps always use a fresh context. */
61
- readonly context: SubagentContext;
62
- readonly model?: string;
63
- readonly timeoutMs: number;
64
- readonly turnBudget?: SubagentTurnBudget;
65
- readonly toolBudget?: SubagentToolBudget;
66
- readonly artifacts: boolean;
67
- /**
68
- * Authorizes bounded automatic recovery when Bash can execute commands.
69
- * Every failed attempt must still have a complete, mutation-safe replay
70
- * audit.
71
- */
72
- readonly retryToolFailures: boolean;
44
+ export type StepAgent = {
45
+ /** Workflow-owned role prompt applied to this main-agent step. */
46
+ readonly name: string;
73
47
  };
74
48
 
75
49
  export type PromptSpec =
76
50
  { readonly inline: string } | { readonly file: string };
77
51
 
52
+ export type ArtifactContract = {
53
+ readonly maxChars: number;
54
+ readonly requiredSubstrings: ReadonlyArray<string>;
55
+ readonly forbiddenSubstrings: ReadonlyArray<string>;
56
+ readonly equalOccurrenceGroups: ReadonlyArray<ReadonlyArray<string>>;
57
+ readonly onValidationFailure?: 'retry';
58
+ };
59
+
78
60
  type GateDefinition = {
79
61
  readonly submitOutcome: string;
80
62
  readonly approvedOutcome: string;
81
63
  readonly rejectedOutcome: string;
64
+ readonly artifactContract?: ArtifactContract;
82
65
  };
83
66
 
84
67
  export type PromptGate = GateDefinition & {
@@ -102,8 +85,8 @@ export type StepWorkspaceBinding = {
102
85
  export type WorkflowStep = {
103
86
  readonly title: string;
104
87
  readonly prompt: PromptSpec;
105
- /** Omit to execute this step in the main Pi agent. */
106
- readonly subagent?: StepSubagent;
88
+ /** Optional workflow-owned role prompt for this main-agent step. */
89
+ readonly agent?: StepAgent;
107
90
  readonly permissions: StepPermissions;
108
91
  readonly requires: StepRequirements;
109
92
  readonly transitions: Readonly<Record<string, StepTarget>>;
@@ -142,19 +125,6 @@ export type PermissionCeiling = {
142
125
  readonly extensions: ReadonlyArray<string>;
143
126
  readonly skills: ReadonlyArray<string>;
144
127
  readonly bash: BashPermission;
145
- readonly subagent?: SubagentPermissionCeiling;
146
- };
147
-
148
- export type SubagentPermissionCeiling = {
149
- readonly agents: ReadonlyArray<string>;
150
- readonly contexts: ReadonlyArray<SubagentContext>;
151
- readonly models: ReadonlyArray<string>;
152
- readonly maxTimeoutMs: number;
153
- readonly maxTurns: number;
154
- readonly maxGraceTurns: number;
155
- readonly maxToolCalls: number;
156
- readonly artifacts: boolean;
157
- readonly retryToolFailures: boolean;
158
128
  };
159
129
 
160
130
  export type WorkflowSettings = {
@@ -192,14 +162,6 @@ export const EMPTY_REQUIREMENTS = {
192
162
  skills: [],
193
163
  } as const satisfies StepRequirements;
194
164
 
195
- export const DEFAULT_STEP_SUBAGENT = {
196
- agent: 'pi-workflows.step',
197
- context: 'fresh',
198
- timeoutMs: 900_000,
199
- artifacts: false,
200
- retryToolFailures: false,
201
- } as const satisfies StepSubagent;
202
-
203
165
  export const DEFAULT_SETTINGS = {
204
166
  version: WORKFLOW_SCHEMA_VERSION,
205
167
  allowProjectWorkflows: false,
@@ -12,7 +12,6 @@ import {
12
12
  type ValidationResult,
13
13
  } from './shared.ts';
14
14
  import { readStatusShortcut } from './shortcut.ts';
15
- import { parseSubagentPermissionCeiling } from './subagent.ts';
16
15
 
17
16
  function parsePermissionCeiling(
18
17
  value: unknown,
@@ -26,7 +25,7 @@ function parsePermissionCeiling(
26
25
  }
27
26
  rejectUnknownKeys(
28
27
  value,
29
- ['tools', 'mcp', 'extensions', 'skills', 'bash', 'subagent'],
28
+ ['tools', 'mcp', 'extensions', 'skills', 'bash'],
30
29
  path,
31
30
  errors,
32
31
  );
@@ -43,18 +42,7 @@ function parsePermissionCeiling(
43
42
  path,
44
43
  errors,
45
44
  );
46
- const subagent =
47
- value.subagent === undefined
48
- ? undefined
49
- : parseSubagentPermissionCeiling(
50
- value.subagent,
51
- `${path}.subagent`,
52
- errors,
53
- );
54
- return {
55
- ...permissions,
56
- ...(subagent ? { subagent } : {}),
57
- };
45
+ return permissions;
58
46
  }
59
47
 
60
48
  /** Validate and normalize untrusted user workflow settings. */
@@ -2,6 +2,7 @@ import { isAbsolute, win32 } from 'node:path';
2
2
  import {
3
3
  MAX_WORKSPACE_ALLOWED_ROOTS,
4
4
  MAX_WORKSPACE_PATH_CHARS,
5
+ type ArtifactContract,
5
6
  type PromptSpec,
6
7
  type StepWorkspaceBinding,
7
8
  type WorkflowGate,
@@ -17,7 +18,7 @@ import {
17
18
  rejectUnknownKeys,
18
19
  type ValidationErrors,
19
20
  } from './shared.ts';
20
- import { parseStepSubagent } from './subagent.ts';
21
+ import { AGENT_PROFILE_NAME_PATTERN, type StepAgent } from '../types.ts';
21
22
 
22
23
  function parsePrompt(
23
24
  value: unknown,
@@ -68,6 +69,106 @@ function parseTransitions(
68
69
  return transitions;
69
70
  }
70
71
 
72
+ function parseArtifactContract(
73
+ value: unknown,
74
+ path: string,
75
+ errors: ValidationErrors,
76
+ ): ArtifactContract | undefined {
77
+ if (value === undefined) return undefined;
78
+ if (!isJsonObject(value)) {
79
+ errors.push(`${path}: expected an object`);
80
+ return undefined;
81
+ }
82
+ rejectUnknownKeys(
83
+ value,
84
+ [
85
+ 'maxChars',
86
+ 'requiredSubstrings',
87
+ 'forbiddenSubstrings',
88
+ 'equalOccurrenceGroups',
89
+ 'onValidationFailure',
90
+ ],
91
+ path,
92
+ errors,
93
+ );
94
+ if (value.maxChars === undefined) {
95
+ errors.push(`${path}.maxChars: expected an integer from 1 to 200000`);
96
+ }
97
+ const maxChars = readInteger(
98
+ value.maxChars,
99
+ 200_000,
100
+ `${path}.maxChars`,
101
+ errors,
102
+ { min: 1, max: 200_000 },
103
+ );
104
+ const parseSubstrings = (field: string): Array<string> => {
105
+ const values = readStringList(
106
+ value[field],
107
+ `${path}.${field}`,
108
+ errors,
109
+ /.+/,
110
+ );
111
+ if (values.length > 32) {
112
+ errors.push(`${path}.${field}: at most 32 values are allowed`);
113
+ }
114
+ values.forEach((substring, index) => {
115
+ if (substring.length > 1_024) {
116
+ errors.push(`${path}.${field}[${index}]: exceeds 1024 characters`);
117
+ }
118
+ });
119
+ return values;
120
+ };
121
+ const equalOccurrenceGroups = (() => {
122
+ if (value.equalOccurrenceGroups === undefined) return [];
123
+ if (!Array.isArray(value.equalOccurrenceGroups)) {
124
+ errors.push(`${path}.equalOccurrenceGroups: expected an array`);
125
+ return [];
126
+ }
127
+ if (value.equalOccurrenceGroups.length > 32) {
128
+ errors.push(
129
+ `${path}.equalOccurrenceGroups: at most 32 groups are allowed`,
130
+ );
131
+ }
132
+ return value.equalOccurrenceGroups.reduce<Array<Array<string>>>(
133
+ (groups, group, index) => {
134
+ const groupPath = `${path}.equalOccurrenceGroups[${index}]`;
135
+ const values = readStringList(group, groupPath, errors, /.+/);
136
+ if (values.length < 2) {
137
+ errors.push(`${groupPath}: at least two values are required`);
138
+ }
139
+ if (values.length > 32) {
140
+ errors.push(`${groupPath}: at most 32 values are allowed`);
141
+ }
142
+ values.forEach((substring, valueIndex) => {
143
+ if (substring.length > 1_024) {
144
+ errors.push(`${groupPath}[${valueIndex}]: exceeds 1024 characters`);
145
+ }
146
+ });
147
+ return [...groups, values];
148
+ },
149
+ [],
150
+ );
151
+ })();
152
+ const onValidationFailure =
153
+ value.onValidationFailure === undefined
154
+ ? undefined
155
+ : readString(
156
+ value.onValidationFailure,
157
+ `${path}.onValidationFailure`,
158
+ errors,
159
+ );
160
+ if (onValidationFailure !== undefined && onValidationFailure !== 'retry') {
161
+ errors.push(`${path}.onValidationFailure: expected retry`);
162
+ }
163
+ return {
164
+ maxChars,
165
+ requiredSubstrings: parseSubstrings('requiredSubstrings'),
166
+ forbiddenSubstrings: parseSubstrings('forbiddenSubstrings'),
167
+ equalOccurrenceGroups,
168
+ ...(onValidationFailure === 'retry' ? { onValidationFailure } : {}),
169
+ };
170
+ }
171
+
71
172
  function parseGate(
72
173
  value: unknown,
73
174
  path: string,
@@ -86,6 +187,7 @@ function parseGate(
86
187
  'approvedOutcome',
87
188
  'rejectedOutcome',
88
189
  'timeoutMs',
190
+ 'artifactContract',
89
191
  ],
90
192
  path,
91
193
  errors,
@@ -120,6 +222,11 @@ function parseGate(
120
222
  errors,
121
223
  { pattern: OUTCOME_PATTERN },
122
224
  );
225
+ const artifactContract = parseArtifactContract(
226
+ value.artifactContract,
227
+ `${path}.artifactContract`,
228
+ errors,
229
+ );
123
230
  if (provider === 'prompt' && value.timeoutMs !== undefined) {
124
231
  errors.push(`${path}.timeoutMs: only valid with provider "plannotator"`);
125
232
  }
@@ -136,12 +243,14 @@ function parseGate(
136
243
  submitOutcome,
137
244
  approvedOutcome,
138
245
  rejectedOutcome,
246
+ ...(artifactContract ? { artifactContract } : {}),
139
247
  }
140
248
  : {
141
249
  provider,
142
250
  submitOutcome,
143
251
  approvedOutcome,
144
252
  rejectedOutcome,
253
+ ...(artifactContract ? { artifactContract } : {}),
145
254
  timeoutMs: readInteger(
146
255
  value.timeoutMs,
147
256
  30_000,
@@ -248,7 +357,7 @@ export function parseWorkflowStep(
248
357
  [
249
358
  'title',
250
359
  'prompt',
251
- 'subagent',
360
+ 'agent',
252
361
  'permissions',
253
362
  'requires',
254
363
  'transitions',
@@ -264,11 +373,15 @@ export function parseWorkflowStep(
264
373
  ? stepId
265
374
  : readString(value.title, `${path}.title`, errors);
266
375
  const prompt = parsePrompt(value.prompt, `${path}.prompt`, errors);
267
- const subagent = parseStepSubagent(
268
- value.subagent,
269
- `${path}.subagent`,
270
- errors,
271
- );
376
+ const agentName =
377
+ value.agent === undefined
378
+ ? undefined
379
+ : readString(value.agent, `${path}.agent`, errors, {
380
+ pattern: AGENT_PROFILE_NAME_PATTERN,
381
+ });
382
+ const agent: StepAgent | undefined = agentName
383
+ ? { name: agentName }
384
+ : undefined;
272
385
  const permissions = parsePermissions(
273
386
  value.permissions,
274
387
  `${path}.permissions`,
@@ -308,6 +421,14 @@ export function parseWorkflowStep(
308
421
  `${path}.transitions: submitOutcome is handled by the gate and must not be a transition`,
309
422
  );
310
423
  }
424
+ if (
425
+ gate.artifactContract?.onValidationFailure === 'retry' &&
426
+ !Object.hasOwn(transitions, 'retry')
427
+ ) {
428
+ errors.push(
429
+ `${path}.transitions: artifact-contract retry requires a "retry" transition`,
430
+ );
431
+ }
311
432
  }
312
433
  if (workspace) {
313
434
  workspace.bindOn.forEach((outcome) => {
@@ -323,7 +444,7 @@ export function parseWorkflowStep(
323
444
  return {
324
445
  title,
325
446
  prompt,
326
- ...(subagent ? { subagent } : {}),
447
+ ...(agent ? { agent } : {}),
327
448
  permissions,
328
449
  requires,
329
450
  transitions,
@@ -91,9 +91,6 @@ function validateWorkspaceGraph(
91
91
  `${workspacePath}: only one workspace-binding step is allowed; "${firstBinderId}" also configures workspace binding`,
92
92
  );
93
93
  }
94
- if (!step.subagent) {
95
- errors.push(`${workspacePath}: workspace binding requires a subagent`);
96
- }
97
94
  if (step.gate) {
98
95
  errors.push(
99
96
  `${workspacePath}: workspace binding is not allowed on a gated step`,
@@ -110,14 +107,13 @@ function validateWorkspaceGraph(
110
107
  }
111
108
  return target && Object.hasOwn(steps, target) ? [target] : [];
112
109
  });
113
- validateWorkspaceDescendants(steps, downstream, errors);
110
+ validateWorkspaceDescendants(steps, downstream);
114
111
  });
115
112
  }
116
113
 
117
114
  function validateWorkspaceDescendants(
118
115
  steps: Readonly<Record<string, WorkflowStep>>,
119
116
  initialStepIds: ReadonlyArray<string>,
120
- errors: ValidationErrors,
121
117
  ): void {
122
118
  const pending = [...initialStepIds];
123
119
  const visited = new Set<string>();
@@ -127,11 +123,6 @@ function validateWorkspaceDescendants(
127
123
  visited.add(stepId);
128
124
  const step = steps[stepId];
129
125
  if (!step) continue;
130
- if (!step.subagent) {
131
- errors.push(
132
- `workflow.steps.${stepId}.subagent: every nonterminal step reachable after workspace binding must use a subagent`,
133
- );
134
- }
135
126
  Object.values(step.transitions).forEach((target) => {
136
127
  if (
137
128
  target !== '$done' &&
@@ -183,9 +183,6 @@ export function validateRunWorkflowSemantics(
183
183
  if (sameWorkflowDigest && currentStepChanged) {
184
184
  return `current step "${run.currentStepId}" does not match the active workflow digest`;
185
185
  }
186
- if (boundWorkspaceCwd && !currentStep.subagent) {
187
- return `bound workflow current step "${run.currentStepId}" must use a subagent`;
188
- }
189
186
  if (currentStepChanged) return undefined;
190
187
  return validatePendingGate(run, currentStep);
191
188
  }
@@ -46,7 +46,7 @@ export type SubagentTranscriptReference = {
46
46
  readonly trustedRoot: string;
47
47
  /** Exact child session returned by the delegation protocol. */
48
48
  readonly sessionFile: string;
49
- /** Pi-subagents run identity used to confine the session path. */
49
+ /** Legacy child-run identity used to confine the session path. */
50
50
  readonly runId: string;
51
51
  readonly childIndex: number;
52
52
  };
@@ -20,14 +20,11 @@ import type { MainStepRuntimeController } from '../runtime/main-step-runtime.ts'
20
20
  import type { SerialTaskQueueController } from '../runtime/serial-task-queue.ts';
21
21
  import type { WorkflowStepResult } from '../runtime/step-result.ts';
22
22
  import type { WorkflowStatusSnapshot } from '../workflow-status.ts';
23
- import type { DelegationFailureActions } from './delegation-failure.ts';
24
23
  import type { WorkflowHarnessDependencies } from './dependencies.ts';
25
24
  import type { SettledStepReport } from './step-reporting.ts';
26
25
  import type {
27
26
  ActiveDelegation,
28
27
  ActivePromptReview,
29
- DelegationRecovery,
30
- DelegationFailureDetails,
31
28
  MainStepIdentity,
32
29
  WorkflowStartContext,
33
30
  } from './types.ts';
@@ -41,7 +38,6 @@ import type {
41
38
  export type HarnessActionContext = {
42
39
  pi: ExtensionAPI;
43
40
  dependencies: WorkflowHarnessDependencies;
44
- delegationFailures: DelegationFailureActions;
45
41
  subagents: SubagentDelegationClientController;
46
42
  mainSteps: MainStepRuntimeController;
47
43
  catalog: WorkflowCatalog;
@@ -98,10 +94,7 @@ export type HarnessActionContext = {
98
94
  registerLifecycle: () => void;
99
95
  registerPolicy: () => void;
100
96
  registerMultilineCommandInput: () => void;
101
- launchCurrentStep: (
102
- workflow: LoadedWorkflow,
103
- recovery?: DelegationRecovery,
104
- ) => void;
97
+ launchCurrentStep: (workflow: LoadedWorkflow) => void;
105
98
  launchMainStep: (
106
99
  workflow: LoadedWorkflow,
107
100
  run: WorkflowRun,
@@ -142,11 +135,6 @@ export type HarnessActionContext = {
142
135
  ) => Promise<void>;
143
136
  cancelActiveDelegation: (reason: string) => Promise<boolean>;
144
137
  cleanupDelegation: (active: ActiveDelegation) => Promise<void>;
145
- retryDelegationAfterFailure: (
146
- active: ActiveDelegation,
147
- failure: DelegationFailureDetails | undefined,
148
- reason: string,
149
- ) => boolean;
150
138
  pauseForDelegationFailure: (reason: string, failureSummary?: string) => void;
151
139
  pauseForExecutionFailure: (
152
140
  label: string,