@wichayutdew/pi-workflows 0.2.1 → 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.
@@ -12,7 +12,7 @@ steps:
12
12
  prompt:
13
13
  file: prompts/mr-comments/inspect.md
14
14
  subagent:
15
- agent: pi-workflows.step
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: pi-workflows.step
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: pi-workflows.step
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: pi-workflows.step
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. Run only exact reviewed worker commands plus the static
5
- allow-list. Do not perform remote actions. Complete with `ready` when
6
- implementation and focused checks are complete; include the exact fenced JSON
7
- contract in the summary for verification. Complete with `blocked` if the
8
- harness, configuration, or repository must be fixed first.
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. Put exact standalone implementation commands under
9
- `worker[].command` and independent commands under `reviewer[].command`.
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 `workflow_complete_step` with outcome `submit` and
12
- place the full Markdown plan in `artifact`. The Plannotator gate decides the
13
- next transition; its reviewed artifact becomes the next step's handoff.
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 the exact reviewed reviewer commands plus the static
4
- allow-list. Complete with `passed` only when evidence supports completion.
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 a human must repair the
7
- workflow or environment.
7
+ fenced JSON contract in the summary, or `blocked` when the workflow or
8
+ environment must be repaired.
@@ -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: [pi-workflows.step, scout, worker, reviewer]
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.1",
3
+ "version": "0.2.3",
4
4
  "description": "A declarative, pauseable workflow harness for Pi",
5
- "license": "Apache-2.0",
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.35.1",
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": "^0.35.1",
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", "fork"],
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", "fork"]
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
  },
@@ -5,7 +5,6 @@ export const HARNESS_COMMAND_NAMES = [
5
5
  'workflow-reload',
6
6
  'workflow-resume',
7
7
  'workflow-start',
8
- 'workflow-status',
9
8
  ] as const;
10
9
 
11
10
  /**
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
  }
@@ -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`,
@@ -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
- settingsResult.settings.permissionCeiling,
297
+ permissionCeiling,
304
298
  );
305
299
  if (ceilingErrors.length > 0) {
306
300
  diagnostics.push(
@@ -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' | 'fork';
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
- /** Configured pi-subagents agent name. */
63
+ /** Pi Subagents agent profile launched for this isolated step. */
61
64
  agent: string;
62
- /** Fresh keeps workflow work out of the parent transcript. */
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
  };
@@ -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
- ).filter((source): source is BashApprovalSource => {
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 a workflow subagent name or object`);
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
- contextValue === 'fork' || contextValue === 'fresh'
544
- ? contextValue
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
- /^(?:fresh|fork)$/,
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
- ['$schema', 'version', 'allowProjectWorkflows', 'permissionCeiling'],
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,
@@ -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' &&