@wichayutdew/pi-workflows 0.2.2 → 0.2.3
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/LICENSE +21 -201
- package/README.md +191 -104
- package/agents/step.md +15 -1
- package/dist/index.js +1803 -463
- package/examples/mr-comments.workflow.yaml +4 -4
- package/examples/prompts/mr-comments/implement.md +10 -5
- package/examples/prompts/mr-comments/plan.md +10 -5
- package/examples/prompts/mr-comments/verify.md +5 -4
- package/examples/settings.yaml +3 -1
- package/package.json +10 -6
- package/schemas/settings.schema.json +8 -0
- package/schemas/workflow.schema.json +10 -2
- package/src/command-names.ts +0 -1
- package/src/commands.ts +0 -6
- package/src/config/ceiling.ts +8 -0
- package/src/config/load.ts +3 -9
- package/src/config/types.ts +15 -3
- package/src/config/validate.ts +147 -22
- package/src/engine/state.ts +7 -0
- package/src/engine/transitions.ts +52 -7
- package/src/harness.ts +701 -74
- package/src/index.ts +6 -2
- package/src/integrations/prompt-gate.ts +14 -13
- package/src/integrations/subagents/child-runtime.ts +187 -69
- package/src/integrations/subagents/client.ts +4 -3
- package/src/integrations/subagents/diagnostics.ts +799 -0
- package/src/integrations/subagents/protocol.ts +86 -15
- package/src/policy/approved-commands.ts +212 -5
- package/src/policy/bash.ts +0 -9
- package/src/prompt.ts +106 -7
- package/src/runtime/serial-task-queue.ts +5 -1
- package/src/workflow-status.ts +244 -35
|
@@ -12,7 +12,7 @@ steps:
|
|
|
12
12
|
prompt:
|
|
13
13
|
file: prompts/mr-comments/inspect.md
|
|
14
14
|
subagent:
|
|
15
|
-
agent:
|
|
15
|
+
agent: scout
|
|
16
16
|
context: fresh
|
|
17
17
|
timeoutMs: 600000
|
|
18
18
|
turnBudget:
|
|
@@ -33,7 +33,7 @@ steps:
|
|
|
33
33
|
prompt:
|
|
34
34
|
file: prompts/mr-comments/plan.md
|
|
35
35
|
subagent:
|
|
36
|
-
agent:
|
|
36
|
+
agent: planner
|
|
37
37
|
context: fresh
|
|
38
38
|
timeoutMs: 600000
|
|
39
39
|
turnBudget:
|
|
@@ -65,7 +65,7 @@ steps:
|
|
|
65
65
|
prompt:
|
|
66
66
|
file: prompts/mr-comments/implement.md
|
|
67
67
|
subagent:
|
|
68
|
-
agent:
|
|
68
|
+
agent: worker
|
|
69
69
|
context: fresh
|
|
70
70
|
timeoutMs: 1200000
|
|
71
71
|
artifacts: true
|
|
@@ -91,7 +91,7 @@ steps:
|
|
|
91
91
|
prompt:
|
|
92
92
|
file: prompts/mr-comments/verify.md
|
|
93
93
|
subagent:
|
|
94
|
-
agent:
|
|
94
|
+
agent: reviewer
|
|
95
95
|
context: fresh
|
|
96
96
|
timeoutMs: 600000
|
|
97
97
|
turnBudget:
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
Implement the approved plan with a small, coherent diff.
|
|
2
2
|
|
|
3
3
|
Use repository conventions and the allowed skill. Add or update focused tests
|
|
4
|
-
where practical.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
contract
|
|
8
|
-
|
|
4
|
+
where practical. Treat the approved plan as the final contract: do not ask a
|
|
5
|
+
terminal question or block solely because a profile or skill prefers an
|
|
6
|
+
unapproved ceremony or command. Apply TDD with the authorized commands when the
|
|
7
|
+
reviewed contract supports it; otherwise implement and record that constraint.
|
|
8
|
+
Run each exact reviewed worker command with its enclosing
|
|
9
|
+
`repositories[].cwd`, plus the static allow-list. Do not perform remote actions.
|
|
10
|
+
Complete with `ready` when implementation and focused checks are complete;
|
|
11
|
+
include the exact fenced JSON contract in the summary for verification.
|
|
12
|
+
Complete with `blocked` only if the harness, configuration, repository, or
|
|
13
|
+
approved contract must be repaired first.
|
|
@@ -5,9 +5,14 @@ Include scope, exact files or symbols, risks, and verification. Account for prio
|
|
|
5
5
|
{{gate.feedback}}
|
|
6
6
|
|
|
7
7
|
Include one fenced `json` verification contract with a top-level
|
|
8
|
-
`repositories` array.
|
|
9
|
-
`
|
|
8
|
+
`repositories` array. Every repository entry must contain its exact absolute
|
|
9
|
+
`cwd`; this workflow requires one distinct repository directory. Put exact
|
|
10
|
+
standalone implementation commands under `worker[].command` and independent
|
|
11
|
+
commands under `reviewer[].command`. Resolve any TDD setup, test selection, or
|
|
12
|
+
command limitation in this reviewed contract; later steps must not invent
|
|
13
|
+
another prerequisite or ask for clarification.
|
|
10
14
|
|
|
11
|
-
When the plan is ready, call `
|
|
12
|
-
|
|
13
|
-
next transition; its reviewed artifact becomes the next step's
|
|
15
|
+
When the plan is ready, call `structured_output` with a `value` whose outcome
|
|
16
|
+
is `submit` and whose `artifact` is the full Markdown plan. The Plannotator gate
|
|
17
|
+
decides the next transition; its reviewed artifact becomes the next step's
|
|
18
|
+
handoff.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
Review the implementation against the requested merge-request feedback and approved plan.
|
|
2
2
|
|
|
3
|
-
Inspect the diff and run
|
|
4
|
-
allow-list.
|
|
3
|
+
Inspect the diff and run each exact reviewed reviewer command with its enclosing
|
|
4
|
+
`repositories[].cwd`, plus the static allow-list. Do not ask a terminal
|
|
5
|
+
question. Complete with `passed` only when evidence supports completion.
|
|
5
6
|
Complete with `failed` to return to implementation and preserve the exact
|
|
6
|
-
fenced JSON contract in the summary, or `blocked` when
|
|
7
|
-
|
|
7
|
+
fenced JSON contract in the summary, or `blocked` when the workflow or
|
|
8
|
+
environment must be repaired.
|
package/examples/settings.yaml
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# yaml-language-server: $schema=../schemas/settings.schema.json
|
|
2
2
|
version: 1
|
|
3
3
|
allowProjectWorkflows: false
|
|
4
|
+
statusShortcut: ctrl+alt+w
|
|
4
5
|
permissionCeiling:
|
|
5
6
|
tools: [read, grep, bash]
|
|
6
7
|
mcp: []
|
|
@@ -9,7 +10,7 @@ permissionCeiling:
|
|
|
9
10
|
bash:
|
|
10
11
|
mode: read-only
|
|
11
12
|
subagent:
|
|
12
|
-
agents: [
|
|
13
|
+
agents: [scout, planner, worker, reviewer]
|
|
13
14
|
contexts: [fresh]
|
|
14
15
|
models: []
|
|
15
16
|
maxTimeoutMs: 900000
|
|
@@ -17,3 +18,4 @@ permissionCeiling:
|
|
|
17
18
|
maxGraceTurns: 3
|
|
18
19
|
maxToolCalls: 100
|
|
19
20
|
artifacts: false
|
|
21
|
+
retryToolFailures: false
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wichayutdew/pi-workflows",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "A declarative, pauseable workflow harness for Pi",
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/wichayutdew/pi-workflows.git"
|
|
@@ -43,9 +43,12 @@
|
|
|
43
43
|
"format:check": "bunx --bun prettier . --check",
|
|
44
44
|
"lint": "bun --bun eslint .",
|
|
45
45
|
"lint:fix": "bun --bun eslint . --fix",
|
|
46
|
-
"test": "bun test",
|
|
46
|
+
"test": "bun test --no-orphans --reporter=dots",
|
|
47
|
+
"test:coverage": "bun run test && bun run coverage:check",
|
|
48
|
+
"coverage:check": "bun .github/scripts/check-coverage.mjs coverage/lcov.info",
|
|
49
|
+
"test:e2e": "bun --config=bunfig.e2e.toml test --no-orphans test/e2e/workflow-runtime.test.ts --reporter=dots",
|
|
47
50
|
"typecheck": "tsc --noEmit",
|
|
48
|
-
"check": "bun run lint && bun run format:check && bun run typecheck && bun test && bun run build"
|
|
51
|
+
"check": "bun run lint && bun run format:check && bun run typecheck && bun run test:coverage && bun run test:e2e && bun run build"
|
|
49
52
|
},
|
|
50
53
|
"dependencies": {
|
|
51
54
|
"yaml": "^2.8.3"
|
|
@@ -53,7 +56,7 @@
|
|
|
53
56
|
"peerDependencies": {
|
|
54
57
|
"@earendil-works/pi-coding-agent": "*",
|
|
55
58
|
"@earendil-works/pi-tui": "*",
|
|
56
|
-
"pi-subagents": ">=0.
|
|
59
|
+
"pi-subagents": ">=0.36.0",
|
|
57
60
|
"typebox": "*"
|
|
58
61
|
},
|
|
59
62
|
"peerDependenciesMeta": {
|
|
@@ -65,10 +68,11 @@
|
|
|
65
68
|
"@earendil-works/pi-coding-agent": "^0.81.1",
|
|
66
69
|
"@earendil-works/pi-tui": "^0.81.1",
|
|
67
70
|
"@eslint/js": "^10.0.1",
|
|
71
|
+
"@types/bun": "^1.3.14",
|
|
68
72
|
"@types/node": "^24.0.0",
|
|
69
73
|
"eslint": "^10.7.0",
|
|
70
74
|
"globals": "^17.7.0",
|
|
71
|
-
"pi-subagents": "
|
|
75
|
+
"pi-subagents": "0.36.0",
|
|
72
76
|
"prettier": "^3.9.5",
|
|
73
77
|
"typebox": "^1.1.38",
|
|
74
78
|
"typescript": "^5.9.3",
|
|
@@ -15,6 +15,14 @@
|
|
|
15
15
|
"type": "boolean",
|
|
16
16
|
"default": false
|
|
17
17
|
},
|
|
18
|
+
"statusShortcut": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"default": "ctrl+alt+w",
|
|
21
|
+
"minLength": 1,
|
|
22
|
+
"maxLength": 64,
|
|
23
|
+
"pattern": "^\\S+$",
|
|
24
|
+
"description": "Pi key identifier used to toggle workflow status. Restart extensions with Pi /reload after changing it."
|
|
25
|
+
},
|
|
18
26
|
"permissionCeiling": {
|
|
19
27
|
"$ref": "workflow.schema.json#/$defs/permissionCeiling"
|
|
20
28
|
}
|
|
@@ -360,7 +360,7 @@
|
|
|
360
360
|
"default": "pi-workflows.step"
|
|
361
361
|
},
|
|
362
362
|
"context": {
|
|
363
|
-
"enum": ["fresh"
|
|
363
|
+
"enum": ["fresh"],
|
|
364
364
|
"default": "fresh"
|
|
365
365
|
},
|
|
366
366
|
"model": {
|
|
@@ -381,6 +381,10 @@
|
|
|
381
381
|
"artifacts": {
|
|
382
382
|
"type": "boolean",
|
|
383
383
|
"default": false
|
|
384
|
+
},
|
|
385
|
+
"retryToolFailures": {
|
|
386
|
+
"type": "boolean",
|
|
387
|
+
"default": false
|
|
384
388
|
}
|
|
385
389
|
}
|
|
386
390
|
}
|
|
@@ -413,7 +417,7 @@
|
|
|
413
417
|
"minItems": 1,
|
|
414
418
|
"uniqueItems": true,
|
|
415
419
|
"items": {
|
|
416
|
-
"enum": ["fresh"
|
|
420
|
+
"enum": ["fresh"]
|
|
417
421
|
}
|
|
418
422
|
},
|
|
419
423
|
"models": {
|
|
@@ -445,6 +449,10 @@
|
|
|
445
449
|
},
|
|
446
450
|
"artifacts": {
|
|
447
451
|
"type": "boolean"
|
|
452
|
+
},
|
|
453
|
+
"retryToolFailures": {
|
|
454
|
+
"type": "boolean",
|
|
455
|
+
"default": false
|
|
448
456
|
}
|
|
449
457
|
}
|
|
450
458
|
},
|
package/src/command-names.ts
CHANGED
package/src/commands.ts
CHANGED
|
@@ -15,7 +15,6 @@ export interface WorkflowCommandController {
|
|
|
15
15
|
resume(ctx: ExtensionCommandContext): Promise<void>;
|
|
16
16
|
abort(reason: string, ctx: ExtensionCommandContext): Promise<void>;
|
|
17
17
|
reload(ctx: ExtensionCommandContext): Promise<void>;
|
|
18
|
-
status(ctx: ExtensionCommandContext): Promise<void>;
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
function splitFirst(value: string): [string, string] {
|
|
@@ -72,9 +71,4 @@ export function registerHarnessCommands(
|
|
|
72
71
|
description: 'Reload workflow files while no workflow is running',
|
|
73
72
|
handler: async (_args, ctx) => controller.reload(ctx),
|
|
74
73
|
});
|
|
75
|
-
|
|
76
|
-
pi.registerCommand('workflow-status', {
|
|
77
|
-
description: 'Open the active workflow status board',
|
|
78
|
-
handler: async (_args, ctx) => controller.status(ctx),
|
|
79
|
-
});
|
|
80
74
|
}
|
package/src/config/ceiling.ts
CHANGED
|
@@ -113,6 +113,14 @@ export function checkWorkflowAgainstCeiling(
|
|
|
113
113
|
`${subagentPath}.artifacts: exceeds the user permission ceiling`,
|
|
114
114
|
);
|
|
115
115
|
}
|
|
116
|
+
if (
|
|
117
|
+
step.subagent.retryToolFailures &&
|
|
118
|
+
!ceiling.subagent.retryToolFailures
|
|
119
|
+
) {
|
|
120
|
+
errors.push(
|
|
121
|
+
`${subagentPath}.retryToolFailures: exceeds the user permission ceiling`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
116
124
|
if (!step.subagent.turnBudget) {
|
|
117
125
|
errors.push(
|
|
118
126
|
`${subagentPath}.turnBudget: required for a project workflow`,
|
package/src/config/load.ts
CHANGED
|
@@ -184,7 +184,7 @@ async function loadWorkflowDirectory(
|
|
|
184
184
|
return { workflows, diagnostics };
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
async function loadSettings(userDirectory: string): Promise<{
|
|
187
|
+
export async function loadSettings(userDirectory: string): Promise<{
|
|
188
188
|
settings: WorkflowSettings;
|
|
189
189
|
diagnostics: ConfigDiagnostic[];
|
|
190
190
|
}> {
|
|
@@ -284,14 +284,8 @@ export async function loadCatalog(
|
|
|
284
284
|
'warning',
|
|
285
285
|
),
|
|
286
286
|
);
|
|
287
|
-
} else if (!settingsResult.settings.permissionCeiling) {
|
|
288
|
-
diagnostics.push(
|
|
289
|
-
diagnostic(
|
|
290
|
-
projectDirectory,
|
|
291
|
-
'project workflows were skipped because no user permission ceiling is configured',
|
|
292
|
-
),
|
|
293
|
-
);
|
|
294
287
|
} else {
|
|
288
|
+
const permissionCeiling = settingsResult.settings.permissionCeiling!;
|
|
295
289
|
const projectResult = await loadWorkflowDirectory(
|
|
296
290
|
projectDirectory,
|
|
297
291
|
'project',
|
|
@@ -300,7 +294,7 @@ export async function loadCatalog(
|
|
|
300
294
|
for (const workflow of projectResult.workflows) {
|
|
301
295
|
const ceilingErrors = checkWorkflowAgainstCeiling(
|
|
302
296
|
workflow.definition,
|
|
303
|
-
|
|
297
|
+
permissionCeiling,
|
|
304
298
|
);
|
|
305
299
|
if (ceilingErrors.length > 0) {
|
|
306
300
|
diagnostics.push(
|
package/src/config/types.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import type { KeyId } from '@earendil-works/pi-tui';
|
|
2
|
+
|
|
1
3
|
export const WORKFLOW_SCHEMA_VERSION = 1 as const;
|
|
4
|
+
export const DEFAULT_STATUS_SHORTCUT = 'ctrl+alt+w' as const satisfies KeyId;
|
|
2
5
|
export const SUBAGENT_RUNTIME_NAME_PATTERN =
|
|
3
6
|
/^[a-z0-9][a-z0-9-]*(?:\.[a-z0-9][a-z0-9-]*)*$/;
|
|
4
7
|
|
|
@@ -43,7 +46,7 @@ export interface StepRequirements {
|
|
|
43
46
|
skills: string[];
|
|
44
47
|
}
|
|
45
48
|
|
|
46
|
-
export type SubagentContext = 'fresh'
|
|
49
|
+
export type SubagentContext = 'fresh';
|
|
47
50
|
|
|
48
51
|
export interface SubagentTurnBudget {
|
|
49
52
|
maxTurns: number;
|
|
@@ -57,15 +60,20 @@ export interface SubagentToolBudget {
|
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
export interface StepSubagent {
|
|
60
|
-
/**
|
|
63
|
+
/** Pi Subagents agent profile launched for this isolated step. */
|
|
61
64
|
agent: string;
|
|
62
|
-
/**
|
|
65
|
+
/** Workflow steps always use a fresh context. */
|
|
63
66
|
context: SubagentContext;
|
|
64
67
|
model?: string;
|
|
65
68
|
timeoutMs: number;
|
|
66
69
|
turnBudget?: SubagentTurnBudget;
|
|
67
70
|
toolBudget?: SubagentToolBudget;
|
|
68
71
|
artifacts: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Authorizes one fresh-context continuation after a tool failure when the
|
|
74
|
+
* trusted complete transcript proves every recorded call was replay-safe.
|
|
75
|
+
*/
|
|
76
|
+
retryToolFailures: boolean;
|
|
69
77
|
}
|
|
70
78
|
|
|
71
79
|
export type PromptSpec = { inline: string } | { file: string };
|
|
@@ -138,11 +146,13 @@ export interface SubagentPermissionCeiling {
|
|
|
138
146
|
maxGraceTurns: number;
|
|
139
147
|
maxToolCalls: number;
|
|
140
148
|
artifacts: boolean;
|
|
149
|
+
retryToolFailures: boolean;
|
|
141
150
|
}
|
|
142
151
|
|
|
143
152
|
export interface WorkflowSettings {
|
|
144
153
|
version: typeof WORKFLOW_SCHEMA_VERSION;
|
|
145
154
|
allowProjectWorkflows: boolean;
|
|
155
|
+
statusShortcut: KeyId;
|
|
146
156
|
permissionCeiling?: PermissionCeiling;
|
|
147
157
|
}
|
|
148
158
|
|
|
@@ -179,9 +189,11 @@ export const DEFAULT_STEP_SUBAGENT: StepSubagent = {
|
|
|
179
189
|
context: 'fresh',
|
|
180
190
|
timeoutMs: 900_000,
|
|
181
191
|
artifacts: false,
|
|
192
|
+
retryToolFailures: false,
|
|
182
193
|
};
|
|
183
194
|
|
|
184
195
|
export const DEFAULT_SETTINGS: WorkflowSettings = {
|
|
185
196
|
version: WORKFLOW_SCHEMA_VERSION,
|
|
186
197
|
allowProjectWorkflows: false,
|
|
198
|
+
statusShortcut: DEFAULT_STATUS_SHORTCUT,
|
|
187
199
|
};
|
package/src/config/validate.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DEFAULT_SETTINGS,
|
|
3
|
+
DEFAULT_STATUS_SHORTCUT,
|
|
3
4
|
DEFAULT_STEP_SUBAGENT,
|
|
4
5
|
EMPTY_PERMISSIONS,
|
|
5
6
|
EMPTY_REQUIREMENTS,
|
|
6
7
|
SUBAGENT_RUNTIME_NAME_PATTERN,
|
|
7
8
|
WORKFLOW_SCHEMA_VERSION,
|
|
8
|
-
type BashMode,
|
|
9
9
|
type BashApprovalSource,
|
|
10
|
+
type BashMode,
|
|
10
11
|
type BashPermission,
|
|
11
12
|
type BashRule,
|
|
12
13
|
type PermissionCeiling,
|
|
@@ -38,6 +39,60 @@ const MCP_SELECTOR_PATTERN = /^[A-Za-z0-9_.:-]+(?:\/[A-Za-z0-9_.:-]+)?$/;
|
|
|
38
39
|
const EXECUTABLE_PATTERN = /^[A-Za-z0-9_./+-]+$/;
|
|
39
40
|
const BASH_APPROVAL_SOURCE_PATTERN =
|
|
40
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;
|
|
41
96
|
const PROMPT_VARIABLES = new Set([
|
|
42
97
|
'workflow.input',
|
|
43
98
|
'workflow.id',
|
|
@@ -91,6 +146,54 @@ function readString(
|
|
|
91
146
|
return result;
|
|
92
147
|
}
|
|
93
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
|
+
|
|
94
197
|
function readInteger(
|
|
95
198
|
value: unknown,
|
|
96
199
|
fallback: number,
|
|
@@ -268,18 +371,7 @@ function parseBashPermission(
|
|
|
268
371
|
errors,
|
|
269
372
|
BASH_APPROVAL_SOURCE_PATTERN,
|
|
270
373
|
)
|
|
271
|
-
)
|
|
272
|
-
const valid =
|
|
273
|
-
source === 'verification-worker' ||
|
|
274
|
-
source === 'verification-reviewer' ||
|
|
275
|
-
source === 'remote-actions';
|
|
276
|
-
if (!valid) {
|
|
277
|
-
errors.push(
|
|
278
|
-
`${path}.approvedSources: expected verification-worker, verification-reviewer, or remote-actions`,
|
|
279
|
-
);
|
|
280
|
-
}
|
|
281
|
-
return valid;
|
|
282
|
-
});
|
|
374
|
+
) as BashApprovalSource[];
|
|
283
375
|
|
|
284
376
|
const normalizedMode = validMode ? mode : 'deny';
|
|
285
377
|
if (normalizedMode !== 'allow-list' && allow.length > 0) {
|
|
@@ -511,7 +603,7 @@ function parseStepSubagent(
|
|
|
511
603
|
return { ...DEFAULT_STEP_SUBAGENT, agent };
|
|
512
604
|
}
|
|
513
605
|
if (!isObject(value)) {
|
|
514
|
-
errors.push(`${path}: expected
|
|
606
|
+
errors.push(`${path}: expected an agent profile name or object`);
|
|
515
607
|
return undefined;
|
|
516
608
|
}
|
|
517
609
|
rejectUnknownKeys(
|
|
@@ -524,6 +616,7 @@ function parseStepSubagent(
|
|
|
524
616
|
'turnBudget',
|
|
525
617
|
'toolBudget',
|
|
526
618
|
'artifacts',
|
|
619
|
+
'retryToolFailures',
|
|
527
620
|
],
|
|
528
621
|
path,
|
|
529
622
|
errors,
|
|
@@ -539,12 +632,9 @@ function parseStepSubagent(
|
|
|
539
632
|
value.context === undefined
|
|
540
633
|
? DEFAULT_STEP_SUBAGENT.context
|
|
541
634
|
: readString(value.context, `${path}.context`, errors);
|
|
542
|
-
const context: SubagentContext =
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
: DEFAULT_STEP_SUBAGENT.context;
|
|
546
|
-
if (contextValue !== 'fork' && contextValue !== 'fresh') {
|
|
547
|
-
errors.push(`${path}.context: expected fresh or fork`);
|
|
635
|
+
const context: SubagentContext = DEFAULT_STEP_SUBAGENT.context;
|
|
636
|
+
if (contextValue !== 'fresh') {
|
|
637
|
+
errors.push(`${path}.context: expected fresh`);
|
|
548
638
|
}
|
|
549
639
|
const model =
|
|
550
640
|
value.model === undefined
|
|
@@ -575,6 +665,12 @@ function parseStepSubagent(
|
|
|
575
665
|
`${path}.artifacts`,
|
|
576
666
|
errors,
|
|
577
667
|
);
|
|
668
|
+
const retryToolFailures = readBoolean(
|
|
669
|
+
value.retryToolFailures,
|
|
670
|
+
DEFAULT_STEP_SUBAGENT.retryToolFailures,
|
|
671
|
+
`${path}.retryToolFailures`,
|
|
672
|
+
errors,
|
|
673
|
+
);
|
|
578
674
|
return {
|
|
579
675
|
agent,
|
|
580
676
|
context,
|
|
@@ -583,6 +679,7 @@ function parseStepSubagent(
|
|
|
583
679
|
...(turnBudget ? { turnBudget } : {}),
|
|
584
680
|
...(toolBudget ? { toolBudget } : {}),
|
|
585
681
|
artifacts,
|
|
682
|
+
retryToolFailures,
|
|
586
683
|
};
|
|
587
684
|
}
|
|
588
685
|
|
|
@@ -766,6 +863,14 @@ function parseStep(
|
|
|
766
863
|
`${path}.permissions`,
|
|
767
864
|
errors,
|
|
768
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
|
+
}
|
|
769
874
|
const requires = parseRequirements(
|
|
770
875
|
value.requires,
|
|
771
876
|
permissions,
|
|
@@ -1008,6 +1113,7 @@ function parseSubagentPermissionCeiling(
|
|
|
1008
1113
|
'maxGraceTurns',
|
|
1009
1114
|
'maxToolCalls',
|
|
1010
1115
|
'artifacts',
|
|
1116
|
+
'retryToolFailures',
|
|
1011
1117
|
],
|
|
1012
1118
|
path,
|
|
1013
1119
|
errors,
|
|
@@ -1022,7 +1128,7 @@ function parseSubagentPermissionCeiling(
|
|
|
1022
1128
|
value.contexts,
|
|
1023
1129
|
`${path}.contexts`,
|
|
1024
1130
|
errors,
|
|
1025
|
-
/^
|
|
1131
|
+
/^fresh$/,
|
|
1026
1132
|
) as SubagentContext[];
|
|
1027
1133
|
const models = readStringList(
|
|
1028
1134
|
value.models,
|
|
@@ -1078,6 +1184,12 @@ function parseSubagentPermissionCeiling(
|
|
|
1078
1184
|
`${path}.artifacts`,
|
|
1079
1185
|
errors,
|
|
1080
1186
|
);
|
|
1187
|
+
const retryToolFailures = readBoolean(
|
|
1188
|
+
value.retryToolFailures,
|
|
1189
|
+
false,
|
|
1190
|
+
`${path}.retryToolFailures`,
|
|
1191
|
+
errors,
|
|
1192
|
+
);
|
|
1081
1193
|
return {
|
|
1082
1194
|
agents,
|
|
1083
1195
|
contexts,
|
|
@@ -1087,6 +1199,7 @@ function parseSubagentPermissionCeiling(
|
|
|
1087
1199
|
maxGraceTurns,
|
|
1088
1200
|
maxToolCalls,
|
|
1089
1201
|
artifacts,
|
|
1202
|
+
retryToolFailures,
|
|
1090
1203
|
};
|
|
1091
1204
|
}
|
|
1092
1205
|
|
|
@@ -1099,7 +1212,13 @@ export function validateSettings(
|
|
|
1099
1212
|
}
|
|
1100
1213
|
rejectUnknownKeys(
|
|
1101
1214
|
value,
|
|
1102
|
-
[
|
|
1215
|
+
[
|
|
1216
|
+
'$schema',
|
|
1217
|
+
'version',
|
|
1218
|
+
'allowProjectWorkflows',
|
|
1219
|
+
'statusShortcut',
|
|
1220
|
+
'permissionCeiling',
|
|
1221
|
+
],
|
|
1103
1222
|
'settings',
|
|
1104
1223
|
errors,
|
|
1105
1224
|
);
|
|
@@ -1115,6 +1234,11 @@ export function validateSettings(
|
|
|
1115
1234
|
} else if (value.allowProjectWorkflows !== undefined) {
|
|
1116
1235
|
errors.push('settings.allowProjectWorkflows: expected a boolean');
|
|
1117
1236
|
}
|
|
1237
|
+
const statusShortcut = readStatusShortcut(
|
|
1238
|
+
value.statusShortcut,
|
|
1239
|
+
'settings.statusShortcut',
|
|
1240
|
+
errors,
|
|
1241
|
+
);
|
|
1118
1242
|
const permissionCeiling = parsePermissionCeiling(
|
|
1119
1243
|
value.permissionCeiling,
|
|
1120
1244
|
'settings.permissionCeiling',
|
|
@@ -1130,6 +1254,7 @@ export function validateSettings(
|
|
|
1130
1254
|
value: {
|
|
1131
1255
|
...DEFAULT_SETTINGS,
|
|
1132
1256
|
allowProjectWorkflows,
|
|
1257
|
+
statusShortcut,
|
|
1133
1258
|
...(permissionCeiling ? { permissionCeiling } : {}),
|
|
1134
1259
|
},
|
|
1135
1260
|
errors,
|
package/src/engine/state.ts
CHANGED
|
@@ -60,6 +60,8 @@ export interface WorkflowRun {
|
|
|
60
60
|
gateFeedback: string;
|
|
61
61
|
pauseReason?: string | undefined;
|
|
62
62
|
pausedFrom?: 'running' | 'awaiting-gate' | undefined;
|
|
63
|
+
/** Current step when execution failed and was paused for a resumable retry. */
|
|
64
|
+
failedStepId?: string | undefined;
|
|
63
65
|
pendingGate?: PendingGate | undefined;
|
|
64
66
|
}
|
|
65
67
|
|
|
@@ -141,6 +143,7 @@ export function isWorkflowRun(value: unknown): value is WorkflowRun {
|
|
|
141
143
|
typeof run.reviewedArtifact === 'string') &&
|
|
142
144
|
(run.stepHandoff === undefined || typeof run.stepHandoff === 'string') &&
|
|
143
145
|
(run.pauseReason === undefined || typeof run.pauseReason === 'string') &&
|
|
146
|
+
(run.failedStepId === undefined || typeof run.failedStepId === 'string') &&
|
|
144
147
|
(run.pausedFrom === undefined ||
|
|
145
148
|
run.pausedFrom === 'running' ||
|
|
146
149
|
run.pausedFrom === 'awaiting-gate');
|
|
@@ -154,6 +157,9 @@ export function isWorkflowRun(value: unknown): value is WorkflowRun {
|
|
|
154
157
|
run.status === 'paused'
|
|
155
158
|
? run.pausedFrom === 'running' || run.pausedFrom === 'awaiting-gate'
|
|
156
159
|
: run.pausedFrom === undefined;
|
|
160
|
+
const failureStateIsValid =
|
|
161
|
+
run.failedStepId === undefined ||
|
|
162
|
+
(run.status === 'paused' && run.failedStepId === run.currentStepId);
|
|
157
163
|
const gateStateIsValid = !gateIsValid
|
|
158
164
|
? false
|
|
159
165
|
: run.pendingGate === undefined
|
|
@@ -177,6 +183,7 @@ export function isWorkflowRun(value: unknown): value is WorkflowRun {
|
|
|
177
183
|
optionalsAreValid &&
|
|
178
184
|
statusIsValid &&
|
|
179
185
|
pauseStateIsValid &&
|
|
186
|
+
failureStateIsValid &&
|
|
180
187
|
gateStateIsValid &&
|
|
181
188
|
typeof run.startedAt === 'number' &&
|
|
182
189
|
typeof run.updatedAt === 'number' &&
|