@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.
- package/README.md +34 -1
- package/dist/index.js +1315 -2467
- package/examples/starter-kit/agents/planner.md +11 -0
- package/examples/starter-kit/agents/reviewer.md +10 -0
- package/examples/starter-kit/agents/scout.md +10 -0
- package/examples/starter-kit/agents/worker.md +11 -0
- package/examples/starter-kit/agents/workspace-preparer.md +10 -0
- package/examples/starter-kit/investigate.workflow.yaml +84 -0
- package/examples/starter-kit/jira.workflow.yaml +75 -0
- package/examples/starter-kit/mr-comment.workflow.yaml +86 -93
- package/examples/starter-kit/mr-review.workflow.yaml +52 -88
- package/examples/starter-kit/settings.yaml +4 -0
- package/examples/starter-kit/steps/investigate/investigate.md +40 -0
- package/examples/starter-kit/steps/investigate/retrieve.md +28 -0
- package/examples/starter-kit/steps/investigate/validate.md +20 -0
- package/examples/starter-kit/steps/jira/create.md +25 -0
- package/examples/starter-kit/steps/jira/draft.md +18 -0
- package/examples/starter-kit/steps/jira/plan.md +30 -0
- package/examples/starter-kit/steps/mr-comment/checkout-source.md +14 -0
- package/examples/starter-kit/steps/mr-comment/fetch.md +11 -28
- package/examples/starter-kit/steps/mr-comment/implement.md +9 -26
- package/examples/starter-kit/steps/mr-comment/plan.md +46 -55
- package/examples/starter-kit/steps/mr-comment/publish.md +11 -31
- package/examples/starter-kit/steps/mr-comment/verify.md +7 -34
- package/examples/starter-kit/steps/mr-review/fetch.md +13 -21
- package/examples/starter-kit/steps/mr-review/publish-approved.md +18 -0
- package/examples/starter-kit/steps/mr-review/review-for-approval.md +53 -0
- package/examples/starter-kit/steps/mr-review/verify-published.md +16 -0
- package/examples/starter-kit/steps/shared/prepare-workspace.md +11 -89
- package/examples/starter-kit/steps/shared/publish-remote.md +16 -0
- package/examples/starter-kit/steps/ticket/implement.md +10 -23
- package/examples/starter-kit/steps/ticket/plan.md +49 -98
- package/examples/starter-kit/steps/ticket/verify.md +14 -65
- package/examples/starter-kit/steps/work/implement.md +11 -22
- package/examples/starter-kit/steps/work/plan.md +43 -59
- package/examples/starter-kit/steps/work/verify.md +14 -25
- package/examples/starter-kit/ticket.workflow.yaml +57 -61
- package/examples/starter-kit/work.workflow.yaml +49 -57
- package/package.json +9 -19
- package/schemas/workflow.schema.json +63 -15
- package/scripts/patch-herdr-agent-state.mjs +36 -0
- package/src/agents/profile.ts +98 -0
- package/src/config/ceiling.ts +0 -82
- package/src/config/types.ts +15 -53
- package/src/config/validation/settings.ts +2 -14
- package/src/config/validation/step.ts +129 -8
- package/src/config/validation/workflow.ts +1 -10
- package/src/engine/run-workflow-validation.ts +0 -3
- package/src/engine/state-types.ts +1 -1
- package/src/harness/action-context.ts +1 -13
- package/src/harness/artifact-contract.ts +46 -0
- package/src/harness/delegation-control-actions.ts +4 -63
- package/src/harness/delegation-plan.ts +26 -80
- package/src/harness/delegation-response-actions.ts +22 -135
- package/src/harness/dependencies.ts +1 -12
- package/src/harness/gate-submission-action.ts +28 -0
- package/src/harness/status-actions.ts +20 -0
- package/src/harness/step-execution-actions.ts +2 -9
- package/src/harness/types.ts +2 -40
- package/src/harness.ts +2 -17
- package/src/herdr-workflow-state.ts +65 -0
- package/src/index.ts +4 -9
- package/src/integrations/subagents/child-policy-validation.ts +6 -7
- package/src/integrations/subagents/child-runtime-dependencies.ts +1 -1
- package/src/integrations/subagents/child-runtime-policy.ts +1 -7
- package/src/integrations/subagents/child-runtime.ts +18 -9
- package/src/integrations/subagents/client.ts +240 -98
- package/src/integrations/subagents/protocol-events.ts +32 -15
- package/src/integrations/subagents/protocol.ts +1 -1
- package/src/preflight.ts +0 -8
- package/src/prompt/main-workflow-notice.ts +8 -15
- package/src/prompt/step-task.ts +7 -6
- package/src/workflow-status/format-status.ts +5 -1
- package/src/workflow-status/render-step-detail.ts +94 -0
- package/src/workflow-status/types.ts +1 -0
- package/src/workflow-status/view.ts +49 -14
- package/agents/step.md +0 -32
- package/examples/mr-comments.workflow.yaml +0 -125
- package/examples/prompts/mr-comments/implement.md +0 -17
- package/examples/prompts/mr-comments/inspect.md +0 -5
- package/examples/prompts/mr-comments/plan.md +0 -54
- package/examples/prompts/mr-comments/verify.md +0 -9
- package/examples/settings.yaml +0 -27
- package/examples/starter-kit/steps/mr-review/publish.md +0 -48
- package/examples/starter-kit/steps/mr-review/review.md +0 -76
- package/examples/starter-kit/steps/mr-review/verify.md +0 -35
- package/src/config/validation/subagent.ts +0 -288
- package/src/harness/delegation-failure.ts +0 -248
- package/src/harness/delegation-recovery-validation.ts +0 -161
- package/src/harness/delegation-retry-policy.ts +0 -120
- package/src/integrations/subagents/client-delegation.ts +0 -181
- package/src/integrations/subagents/client-messages.ts +0 -66
- package/src/integrations/subagents/client-types.ts +0 -36
- package/src/integrations/subagents/diagnostic-format.ts +0 -45
- package/src/integrations/subagents/diagnostic-text.ts +0 -114
- package/src/integrations/subagents/diagnostic-types.ts +0 -83
- package/src/integrations/subagents/diagnostics.ts +0 -26
- package/src/integrations/subagents/failure-correlation.ts +0 -285
- package/src/integrations/subagents/failure-transcript.ts +0 -252
- package/src/integrations/subagents/hidden-bash-failure.ts +0 -98
- package/src/integrations/subagents/replay-audit.ts +0 -146
- package/src/integrations/subagents/replay-safety.ts +0 -67
- 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
|
+
}
|
package/src/config/ceiling.ts
CHANGED
|
@@ -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
|
}
|
package/src/config/types.ts
CHANGED
|
@@ -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
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
/**
|
|
106
|
-
readonly
|
|
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'
|
|
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
|
-
|
|
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 {
|
|
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
|
-
'
|
|
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
|
|
268
|
-
value.
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
...(
|
|
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
|
|
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
|
-
/**
|
|
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,
|