@wichayutdew/pi-workflows 0.2.2 → 0.3.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/LICENSE +21 -201
- package/README.md +207 -106
- package/agents/step.md +15 -1
- package/dist/index.js +1964 -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 +804 -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 +977 -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 +115 -7
- package/src/runtime/serial-task-queue.ts +5 -1
- package/src/workflow-status.ts +244 -35
package/src/harness.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomBytes, randomUUID } from 'node:crypto';
|
|
2
|
-
import { mkdtempSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import {
|
|
2
|
+
import { constants, mkdtempSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { lstat, open, rm } from 'node:fs/promises';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import type {
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
ExtensionCommandContext,
|
|
9
9
|
ExtensionContext,
|
|
10
10
|
} from '@earendil-works/pi-coding-agent';
|
|
11
|
+
import type { KeyId } from '@earendil-works/pi-tui';
|
|
11
12
|
import {
|
|
12
13
|
registerHarnessCommands,
|
|
13
14
|
type WorkflowCommandController,
|
|
@@ -16,6 +17,7 @@ import { loadCatalog } from './config/load.ts';
|
|
|
16
17
|
import { hasRuntimeCommandConflict } from './config/command-conflicts.ts';
|
|
17
18
|
import {
|
|
18
19
|
DEFAULT_SETTINGS,
|
|
20
|
+
DEFAULT_STATUS_SHORTCUT,
|
|
19
21
|
type LoadedWorkflow,
|
|
20
22
|
type WorkflowCatalog,
|
|
21
23
|
type WorkflowStep,
|
|
@@ -28,6 +30,7 @@ import {
|
|
|
28
30
|
attachGateReviewId,
|
|
29
31
|
beginGate,
|
|
30
32
|
failGate,
|
|
33
|
+
failRun,
|
|
31
34
|
pauseRun,
|
|
32
35
|
reconcileRun,
|
|
33
36
|
resolveGate,
|
|
@@ -61,31 +64,128 @@ import {
|
|
|
61
64
|
type ChildStepPolicy,
|
|
62
65
|
type SubagentDelegationRequest,
|
|
63
66
|
type SubagentDelegationResponse,
|
|
67
|
+
type SubagentDelegationStatus,
|
|
64
68
|
type SubagentDelegationUpdate,
|
|
65
69
|
} from './integrations/subagents/protocol.ts';
|
|
70
|
+
import {
|
|
71
|
+
deriveSubagentSessionRoot,
|
|
72
|
+
failedToolName,
|
|
73
|
+
formatToolFailureDiagnostic,
|
|
74
|
+
readDelegationReplayAudit,
|
|
75
|
+
readToolFailureDiagnostic,
|
|
76
|
+
type DelegationReplayAudit,
|
|
77
|
+
} from './integrations/subagents/diagnostics.ts';
|
|
66
78
|
import { preflightStep } from './preflight.ts';
|
|
67
79
|
import {
|
|
68
80
|
buildDelegatedStepTask,
|
|
69
81
|
buildMainStepTask,
|
|
70
82
|
buildMainWorkflowNotice,
|
|
83
|
+
reinforcementRetryTask,
|
|
71
84
|
} from './prompt.ts';
|
|
72
|
-
import {
|
|
85
|
+
import {
|
|
86
|
+
narrowApprovedBashCommands,
|
|
87
|
+
resolveReviewedRepositoryCwd,
|
|
88
|
+
reviewedCommandShapeError,
|
|
89
|
+
} from './policy/approved-commands.ts';
|
|
73
90
|
import {
|
|
74
91
|
MainStepRuntime,
|
|
75
92
|
type MainStepExecution,
|
|
76
93
|
} from './runtime/main-step-runtime.ts';
|
|
94
|
+
import { WORKFLOW_COMPLETION_PARAMETERS } from './runtime/completion-tool.ts';
|
|
77
95
|
import { SerialTaskQueue } from './runtime/serial-task-queue.ts';
|
|
78
96
|
import type { WorkflowStepResult } from './runtime/step-result.ts';
|
|
79
97
|
import { formatWorkflowList } from './workflow-list.ts';
|
|
80
98
|
import {
|
|
81
|
-
|
|
82
|
-
showWorkflowStatus,
|
|
99
|
+
formatShortcutLabel,
|
|
100
|
+
showWorkflowStatus as showWorkflowStatusOverlay,
|
|
101
|
+
workflowStatusIcon,
|
|
83
102
|
type WorkflowStatusExecution,
|
|
84
103
|
type WorkflowStatusSnapshot,
|
|
85
104
|
} from './workflow-status.ts';
|
|
86
105
|
|
|
87
106
|
const STATE_ENTRY_TYPE = 'pi-workflows-state-v1';
|
|
88
107
|
const STATUS_KEY = 'pi-workflows';
|
|
108
|
+
const LEGACY_PROGRESS_WIDGET_KEY = 'pi-workflows-progress';
|
|
109
|
+
const STATUS_REFRESH_INTERVAL_MS = 250;
|
|
110
|
+
const MAX_REINFORCEMENT_RETRIES = 1;
|
|
111
|
+
|
|
112
|
+
function delegationTranscriptBinding(
|
|
113
|
+
requestId: string,
|
|
114
|
+
policyDigest: string,
|
|
115
|
+
): string {
|
|
116
|
+
return `<pi-workflows-delegation-binding-v1>${requestId}:${policyDigest}</pi-workflows-delegation-binding-v1>`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function nonEmptyTerminalError(
|
|
120
|
+
response: SubagentDelegationResponse,
|
|
121
|
+
): string | undefined {
|
|
122
|
+
return [response.error, response.execution?.error].find(
|
|
123
|
+
(error): error is string =>
|
|
124
|
+
typeof error === 'string' && error.trim().length > 0,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function nonzeroTerminalExitCode(
|
|
129
|
+
response: SubagentDelegationResponse,
|
|
130
|
+
): number | undefined {
|
|
131
|
+
return [response.exitCode, response.execution?.exitCode].find(
|
|
132
|
+
(exitCode): exitCode is number =>
|
|
133
|
+
typeof exitCode === 'number' &&
|
|
134
|
+
Number.isSafeInteger(exitCode) &&
|
|
135
|
+
exitCode !== 0,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function hasContradictoryCompletion(
|
|
140
|
+
response: SubagentDelegationResponse,
|
|
141
|
+
): boolean {
|
|
142
|
+
return (
|
|
143
|
+
response.status === 'completed' &&
|
|
144
|
+
(nonEmptyTerminalError(response) !== undefined ||
|
|
145
|
+
nonzeroTerminalExitCode(response) !== undefined)
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function isRetryableTerminalFailure(
|
|
150
|
+
failure: DelegationFailureDetails,
|
|
151
|
+
): boolean {
|
|
152
|
+
return (
|
|
153
|
+
(failure.status === 'failed' ||
|
|
154
|
+
failure.status === 'structured_output_failed') &&
|
|
155
|
+
(failure.error !== undefined ||
|
|
156
|
+
(Number.isSafeInteger(failure.exitCode) && failure.exitCode !== 0))
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function validateReplayAudit(
|
|
161
|
+
response: SubagentDelegationResponse,
|
|
162
|
+
replayAudit: DelegationReplayAudit | undefined,
|
|
163
|
+
): DelegationReplayAudit | undefined {
|
|
164
|
+
if (!replayAudit) return undefined;
|
|
165
|
+
if (
|
|
166
|
+
response.toolCount !== undefined &&
|
|
167
|
+
response.toolCount !== replayAudit.toolCount
|
|
168
|
+
) {
|
|
169
|
+
return { ...replayAudit, replaySafe: false };
|
|
170
|
+
}
|
|
171
|
+
return replayAudit;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function isSafeToRetryDelegation(
|
|
175
|
+
policy: ChildStepPolicy,
|
|
176
|
+
replayExplicitlyAuthorized: boolean,
|
|
177
|
+
replayAudit: DelegationReplayAudit | undefined,
|
|
178
|
+
): boolean {
|
|
179
|
+
const tools = new Set(policy.permissions.tools);
|
|
180
|
+
return (
|
|
181
|
+
replayAudit?.replaySafe === true &&
|
|
182
|
+
!tools.has('edit') &&
|
|
183
|
+
!tools.has('write') &&
|
|
184
|
+
(replayExplicitlyAuthorized ||
|
|
185
|
+
policy.permissions.bash.mode === 'deny' ||
|
|
186
|
+
policy.permissions.bash.mode === 'read-only')
|
|
187
|
+
);
|
|
188
|
+
}
|
|
89
189
|
|
|
90
190
|
interface ActiveDelegation {
|
|
91
191
|
requestId: string;
|
|
@@ -95,11 +195,275 @@ interface ActiveDelegation {
|
|
|
95
195
|
sessionEpoch: number;
|
|
96
196
|
resultDirectory: string;
|
|
97
197
|
policy: ChildStepPolicy;
|
|
198
|
+
transcriptTask: string;
|
|
98
199
|
agent: string;
|
|
200
|
+
trustedSessionRoot?: string;
|
|
201
|
+
reinforcementReplayAuthorized: boolean;
|
|
202
|
+
reinforcementRetryCount: number;
|
|
99
203
|
progress?: string;
|
|
100
204
|
cancelling?: boolean;
|
|
101
205
|
}
|
|
102
206
|
|
|
207
|
+
const MAX_FAILURE_FIELD_CHARS = 1_600;
|
|
208
|
+
const MAX_DELEGATED_RESULT_BYTES = 1024 * 1024;
|
|
209
|
+
|
|
210
|
+
function boundedFailureField(value: string): string {
|
|
211
|
+
if (value.length <= MAX_FAILURE_FIELD_CHARS) return value;
|
|
212
|
+
const marker = '… [truncated] …';
|
|
213
|
+
const available = MAX_FAILURE_FIELD_CHARS - marker.length - 2;
|
|
214
|
+
const startLength = Math.ceil(available / 2);
|
|
215
|
+
const endLength = Math.floor(available / 2);
|
|
216
|
+
return `${value.slice(0, startLength)}\n${marker}\n${value.slice(-endLength)}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function readStableDelegatedResult(
|
|
220
|
+
active: ActiveDelegation,
|
|
221
|
+
): Promise<string> {
|
|
222
|
+
const expectedPath = join(active.resultDirectory, 'result.json');
|
|
223
|
+
if (active.policy.resultPath !== expectedPath) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
'delegated result path does not match its private directory',
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
const inspected = await lstat(expectedPath);
|
|
229
|
+
if (inspected.isSymbolicLink() || !inspected.isFile()) {
|
|
230
|
+
throw new Error('delegated result is not a regular file');
|
|
231
|
+
}
|
|
232
|
+
const handle = await open(
|
|
233
|
+
expectedPath,
|
|
234
|
+
constants.O_RDONLY | constants.O_NOFOLLOW,
|
|
235
|
+
);
|
|
236
|
+
try {
|
|
237
|
+
const beforeRead = await handle.stat();
|
|
238
|
+
if (!beforeRead.isFile() || beforeRead.size > MAX_DELEGATED_RESULT_BYTES) {
|
|
239
|
+
throw new Error('delegated result is not a bounded regular file');
|
|
240
|
+
}
|
|
241
|
+
const value = await handle.readFile({ encoding: 'utf8' });
|
|
242
|
+
const afterRead = await handle.stat();
|
|
243
|
+
if (
|
|
244
|
+
afterRead.dev !== beforeRead.dev ||
|
|
245
|
+
afterRead.ino !== beforeRead.ino ||
|
|
246
|
+
afterRead.size !== beforeRead.size ||
|
|
247
|
+
afterRead.mtimeMs !== beforeRead.mtimeMs
|
|
248
|
+
) {
|
|
249
|
+
throw new Error('delegated result changed while it was being read');
|
|
250
|
+
}
|
|
251
|
+
return value;
|
|
252
|
+
} finally {
|
|
253
|
+
await handle.close();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function rejectedRecoveryReason(
|
|
258
|
+
failure: DelegationFailureDetails,
|
|
259
|
+
error: unknown,
|
|
260
|
+
): string {
|
|
261
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
262
|
+
return `${failure.reason}\nRecovery rejected: ${boundedFailureField(detail)}`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function completionMatchesResult(
|
|
266
|
+
diagnostic: NonNullable<DelegationFailureDetails['diagnostic']>,
|
|
267
|
+
result: WorkflowStepResult,
|
|
268
|
+
policy: ChildStepPolicy,
|
|
269
|
+
): boolean {
|
|
270
|
+
const value = diagnostic.completionValue;
|
|
271
|
+
if (!value) return false;
|
|
272
|
+
const expectedKeys = [
|
|
273
|
+
'outcome',
|
|
274
|
+
'summary',
|
|
275
|
+
...(result.artifact === undefined ? [] : ['artifact']),
|
|
276
|
+
].sort();
|
|
277
|
+
const actualKeys = Object.keys(value).sort();
|
|
278
|
+
if (
|
|
279
|
+
actualKeys.length !== expectedKeys.length ||
|
|
280
|
+
!actualKeys.every((key, index) => key === expectedKeys[index])
|
|
281
|
+
) {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
const completion = parseDelegatedStepResult(
|
|
286
|
+
{
|
|
287
|
+
...value,
|
|
288
|
+
version: 1,
|
|
289
|
+
policyDigest: policy.policyDigest,
|
|
290
|
+
},
|
|
291
|
+
policy,
|
|
292
|
+
);
|
|
293
|
+
return (
|
|
294
|
+
completion.outcome === result.outcome &&
|
|
295
|
+
completion.summary === result.summary &&
|
|
296
|
+
completion.artifact === result.artifact
|
|
297
|
+
);
|
|
298
|
+
} catch {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function recoveredProjectionError(
|
|
304
|
+
active: ActiveDelegation,
|
|
305
|
+
response: SubagentDelegationResponse,
|
|
306
|
+
diagnostic: NonNullable<DelegationFailureDetails['diagnostic']>,
|
|
307
|
+
): string | undefined {
|
|
308
|
+
if (response.agent !== active.agent) {
|
|
309
|
+
return `terminal agent identity is ${JSON.stringify(response.agent)}; expected ${JSON.stringify(active.agent)}`;
|
|
310
|
+
}
|
|
311
|
+
if (response.childIndex !== 0) {
|
|
312
|
+
return `terminal child index is ${JSON.stringify(response.childIndex)}; expected 0`;
|
|
313
|
+
}
|
|
314
|
+
if (
|
|
315
|
+
typeof response.exitCode !== 'number' ||
|
|
316
|
+
!Number.isSafeInteger(response.exitCode) ||
|
|
317
|
+
response.exitCode <= 0
|
|
318
|
+
) {
|
|
319
|
+
return `terminal exit code is ${JSON.stringify(response.exitCode)}; expected a positive safe integer`;
|
|
320
|
+
}
|
|
321
|
+
const execution = response.execution;
|
|
322
|
+
if (!execution) return 'terminal response has no execution projection';
|
|
323
|
+
if (execution.status !== 'failed' || execution.success !== false) {
|
|
324
|
+
return `execution projection is ${JSON.stringify({
|
|
325
|
+
status: execution.status,
|
|
326
|
+
success: execution.success,
|
|
327
|
+
})}; expected failed/false`;
|
|
328
|
+
}
|
|
329
|
+
if (execution.exitCode !== response.exitCode) {
|
|
330
|
+
return `execution exit code ${JSON.stringify(execution.exitCode)} does not match terminal exit code ${JSON.stringify(response.exitCode)}`;
|
|
331
|
+
}
|
|
332
|
+
if (
|
|
333
|
+
typeof response.error !== 'string' ||
|
|
334
|
+
!response.error ||
|
|
335
|
+
typeof execution.error !== 'string' ||
|
|
336
|
+
execution.error !== response.error
|
|
337
|
+
) {
|
|
338
|
+
return 'terminal and execution errors are missing or do not match exactly';
|
|
339
|
+
}
|
|
340
|
+
const warnings = response.warnings as unknown;
|
|
341
|
+
if (
|
|
342
|
+
warnings !== undefined &&
|
|
343
|
+
(!Array.isArray(warnings) ||
|
|
344
|
+
warnings.some(
|
|
345
|
+
(warning) => typeof warning !== 'string' || warning.trim().length > 0,
|
|
346
|
+
))
|
|
347
|
+
) {
|
|
348
|
+
return `terminal response contains warning evidence: ${JSON.stringify(warnings)}`;
|
|
349
|
+
}
|
|
350
|
+
if (
|
|
351
|
+
diagnostic.transcriptToolCount !== undefined &&
|
|
352
|
+
response.toolCount !== undefined &&
|
|
353
|
+
response.toolCount !== diagnostic.transcriptToolCount
|
|
354
|
+
) {
|
|
355
|
+
return `terminal tool count ${response.toolCount} does not match transcript tool count ${diagnostic.transcriptToolCount}`;
|
|
356
|
+
}
|
|
357
|
+
if (
|
|
358
|
+
diagnostic.transcriptTurnCount !== undefined &&
|
|
359
|
+
response.turns !== undefined &&
|
|
360
|
+
response.turns !== diagnostic.transcriptTurnCount
|
|
361
|
+
) {
|
|
362
|
+
return `terminal turn count ${response.turns} does not match transcript turn count ${diagnostic.transcriptTurnCount}`;
|
|
363
|
+
}
|
|
364
|
+
const toolFailure = response.error.match(
|
|
365
|
+
/^\s*([a-z][\w-]*) failed\s*\(exit\s+(\d+)\)\s*:/i,
|
|
366
|
+
);
|
|
367
|
+
if (!toolFailure) {
|
|
368
|
+
return 'terminal error is not a recognized "<tool> failed (exit N): <detail>" failure';
|
|
369
|
+
}
|
|
370
|
+
const terminalTool = toolFailure[1]!;
|
|
371
|
+
const terminalExitCode = Number(toolFailure[2]);
|
|
372
|
+
if (
|
|
373
|
+
terminalTool.toLowerCase() !== diagnostic.tool.toLowerCase() ||
|
|
374
|
+
terminalExitCode !== response.exitCode
|
|
375
|
+
) {
|
|
376
|
+
return `terminal tool/exit ${JSON.stringify({
|
|
377
|
+
tool: terminalTool,
|
|
378
|
+
exitCode: terminalExitCode,
|
|
379
|
+
})} does not match the correlated failure ${JSON.stringify({
|
|
380
|
+
tool: diagnostic.tool,
|
|
381
|
+
exitCode: response.exitCode,
|
|
382
|
+
})}`;
|
|
383
|
+
}
|
|
384
|
+
const unsafeFlag = (
|
|
385
|
+
[
|
|
386
|
+
['interrupted', execution.interrupted],
|
|
387
|
+
['timedOut', execution.timedOut],
|
|
388
|
+
['stopped', execution.stopped],
|
|
389
|
+
['detached', execution.detached],
|
|
390
|
+
] as const
|
|
391
|
+
).find(([, enabled]) => enabled === true)?.[0];
|
|
392
|
+
return unsafeFlag
|
|
393
|
+
? `execution projection reports ${unsafeFlag}=true`
|
|
394
|
+
: undefined;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
interface DelegationFailureDetails {
|
|
398
|
+
reason: string;
|
|
399
|
+
status: SubagentDelegationStatus;
|
|
400
|
+
error?: string;
|
|
401
|
+
exitCode?: number;
|
|
402
|
+
diagnostic?: Awaited<ReturnType<typeof readToolFailureDiagnostic>>;
|
|
403
|
+
replayAudit?: DelegationReplayAudit;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function delegationFailureDetails(
|
|
407
|
+
active: ActiveDelegation,
|
|
408
|
+
response: SubagentDelegationResponse,
|
|
409
|
+
): Promise<DelegationFailureDetails> {
|
|
410
|
+
const terminalError = nonEmptyTerminalError(response);
|
|
411
|
+
const error =
|
|
412
|
+
terminalError ?? 'The subagent returned no terminal error details.';
|
|
413
|
+
const responseIdentityMatches =
|
|
414
|
+
response.childIndex === 0 &&
|
|
415
|
+
(response.agent === undefined || response.agent === active.agent);
|
|
416
|
+
const identity =
|
|
417
|
+
responseIdentityMatches && response.runId !== undefined
|
|
418
|
+
? { runId: response.runId, childIndex: 0 }
|
|
419
|
+
: undefined;
|
|
420
|
+
const [diagnostic, replayAudit] = await Promise.all([
|
|
421
|
+
readToolFailureDiagnostic(
|
|
422
|
+
response.sessionFile,
|
|
423
|
+
active.trustedSessionRoot,
|
|
424
|
+
identity,
|
|
425
|
+
failedToolName(terminalError),
|
|
426
|
+
terminalError,
|
|
427
|
+
),
|
|
428
|
+
readDelegationReplayAudit(
|
|
429
|
+
response.sessionFile,
|
|
430
|
+
active.trustedSessionRoot,
|
|
431
|
+
identity,
|
|
432
|
+
{
|
|
433
|
+
task: active.transcriptTask,
|
|
434
|
+
bashPermission: active.policy.permissions.bash,
|
|
435
|
+
approvedBashCommands: active.policy.approvedBashCommands ?? [],
|
|
436
|
+
},
|
|
437
|
+
),
|
|
438
|
+
]);
|
|
439
|
+
const validatedReplayAudit = validateReplayAudit(response, replayAudit);
|
|
440
|
+
const exitCode =
|
|
441
|
+
nonzeroTerminalExitCode(response) ??
|
|
442
|
+
response.exitCode ??
|
|
443
|
+
response.execution?.exitCode;
|
|
444
|
+
const reason = [
|
|
445
|
+
hasContradictoryCompletion(response)
|
|
446
|
+
? `Subagent "${active.agent}" reported terminal failure signals with completed status.`
|
|
447
|
+
: `Subagent "${active.agent}" ${response.status.replaceAll('_', ' ')}.`,
|
|
448
|
+
...(diagnostic ? formatToolFailureDiagnostic(diagnostic) : []),
|
|
449
|
+
...(exitCode !== undefined ? [`Subagent exit code: ${exitCode}`] : []),
|
|
450
|
+
`Terminal error: ${boundedFailureField(error)}`,
|
|
451
|
+
...(diagnostic && response.sessionFile
|
|
452
|
+
? [
|
|
453
|
+
`Diagnostic session: ${boundedFailureField(response.sessionFile.replaceAll(/\s+/g, ' '))}`,
|
|
454
|
+
]
|
|
455
|
+
: []),
|
|
456
|
+
].join('\n');
|
|
457
|
+
return {
|
|
458
|
+
reason,
|
|
459
|
+
status: response.status,
|
|
460
|
+
...(terminalError ? { error: terminalError } : {}),
|
|
461
|
+
...(exitCode !== undefined ? { exitCode } : {}),
|
|
462
|
+
...(diagnostic ? { diagnostic } : {}),
|
|
463
|
+
...(validatedReplayAudit ? { replayAudit: validatedReplayAudit } : {}),
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
103
467
|
interface MainStepIdentity {
|
|
104
468
|
runId: string;
|
|
105
469
|
stepId: string;
|
|
@@ -115,6 +479,12 @@ interface ActivePromptReview {
|
|
|
115
479
|
abortController: AbortController;
|
|
116
480
|
}
|
|
117
481
|
|
|
482
|
+
interface WorkflowStartContext {
|
|
483
|
+
context: ExtensionContext;
|
|
484
|
+
skills: () => readonly { name: string }[] | undefined;
|
|
485
|
+
waitForIdle: () => Promise<void>;
|
|
486
|
+
}
|
|
487
|
+
|
|
118
488
|
function emptyCatalog(): WorkflowCatalog {
|
|
119
489
|
return {
|
|
120
490
|
workflows: new Map(),
|
|
@@ -135,6 +505,30 @@ function formatDiagnostics(catalog: WorkflowCatalog): string {
|
|
|
135
505
|
].join('\n');
|
|
136
506
|
}
|
|
137
507
|
|
|
508
|
+
function skillNamesFromSystemPrompt(
|
|
509
|
+
systemPrompt: string,
|
|
510
|
+
): Array<{ name: string }> {
|
|
511
|
+
const sections = [
|
|
512
|
+
...systemPrompt.matchAll(
|
|
513
|
+
/<available_skills>([\s\S]*?)<\/available_skills>/g,
|
|
514
|
+
),
|
|
515
|
+
];
|
|
516
|
+
const section = sections.at(-1)?.[1] ?? '';
|
|
517
|
+
return [...section.matchAll(/<name>([^<]+)<\/name>/g)].map((match) => ({
|
|
518
|
+
name: match[1]!.trim(),
|
|
519
|
+
}));
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async function waitForEventContextIdle(ctx: ExtensionContext): Promise<void> {
|
|
523
|
+
const deadline = Date.now() + 30_000;
|
|
524
|
+
while (!ctx.isIdle()) {
|
|
525
|
+
if (Date.now() >= deadline) {
|
|
526
|
+
throw new Error('Timed out waiting for the interrupted Pi turn to stop');
|
|
527
|
+
}
|
|
528
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
138
532
|
export class WorkflowHarness implements WorkflowCommandController {
|
|
139
533
|
private readonly pi: ExtensionAPI;
|
|
140
534
|
private readonly subagents: SubagentDelegationClient;
|
|
@@ -150,12 +544,24 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
150
544
|
private registeredWorkflowCommands = new Set<string>();
|
|
151
545
|
private catalogLoadSequence = 0;
|
|
152
546
|
private readonly mutationQueue = new SerialTaskQueue();
|
|
153
|
-
|
|
154
|
-
|
|
547
|
+
private readonly statusShortcut: KeyId;
|
|
548
|
+
private readonly statusShortcutLabel: string;
|
|
549
|
+
private statusRefreshTimer: ReturnType<typeof setInterval> | undefined;
|
|
550
|
+
private statusOverlayOpen = false;
|
|
551
|
+
private legacyProgressWidgetContext: ExtensionContext | undefined;
|
|
552
|
+
|
|
553
|
+
constructor(
|
|
554
|
+
pi: ExtensionAPI,
|
|
555
|
+
statusShortcut: KeyId = DEFAULT_STATUS_SHORTCUT,
|
|
556
|
+
) {
|
|
155
557
|
this.pi = pi;
|
|
558
|
+
this.statusShortcut = statusShortcut;
|
|
559
|
+
this.statusShortcutLabel = formatShortcutLabel(statusShortcut);
|
|
156
560
|
this.subagents = new SubagentDelegationClient(pi.events);
|
|
157
561
|
this.mainSteps = new MainStepRuntime(pi);
|
|
158
562
|
registerHarnessCommands(pi, this);
|
|
563
|
+
this.registerWorkflowStatusShortcut();
|
|
564
|
+
this.registerMultilineCommandInput();
|
|
159
565
|
this.registerLifecycle();
|
|
160
566
|
this.registerPolicy();
|
|
161
567
|
this.registerPlannotatorResults();
|
|
@@ -165,6 +571,49 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
165
571
|
return [...this.catalog.workflows.keys()].sort();
|
|
166
572
|
}
|
|
167
573
|
|
|
574
|
+
private registerMultilineCommandInput(): void {
|
|
575
|
+
this.pi.on('input', async (event, ctx) => {
|
|
576
|
+
if (
|
|
577
|
+
event.source === 'extension' ||
|
|
578
|
+
event.images?.length ||
|
|
579
|
+
!event.text.startsWith('/')
|
|
580
|
+
) {
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
const newline = event.text.indexOf('\n');
|
|
584
|
+
if (newline === -1) return;
|
|
585
|
+
const command = event.text.slice(1, newline).replace(/\r$/, '');
|
|
586
|
+
if (!this.registeredWorkflowCommands.has(command)) return;
|
|
587
|
+
const workflow = [...this.catalog.workflows.values()].find(
|
|
588
|
+
(candidate) => candidate.definition.command === command,
|
|
589
|
+
);
|
|
590
|
+
if (!workflow) return;
|
|
591
|
+
|
|
592
|
+
const input = event.text.slice(newline + 1);
|
|
593
|
+
const skills = skillNamesFromSystemPrompt(ctx.getSystemPrompt());
|
|
594
|
+
try {
|
|
595
|
+
await this.enqueueMutation(ctx, (sessionEpoch) =>
|
|
596
|
+
this.startNow(
|
|
597
|
+
workflow.definition.id,
|
|
598
|
+
input,
|
|
599
|
+
{
|
|
600
|
+
context: ctx,
|
|
601
|
+
skills: () => skills,
|
|
602
|
+
waitForIdle: () => waitForEventContextIdle(ctx),
|
|
603
|
+
},
|
|
604
|
+
sessionEpoch,
|
|
605
|
+
),
|
|
606
|
+
);
|
|
607
|
+
} catch (error) {
|
|
608
|
+
ctx.ui.notify(
|
|
609
|
+
`Cannot start workflow: ${error instanceof Error ? error.message : String(error)}`,
|
|
610
|
+
'error',
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
return { action: 'handled' as const };
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
|
|
168
617
|
async list(ctx: ExtensionCommandContext): Promise<void> {
|
|
169
618
|
const workflows = [...this.catalog.workflows.values()].sort((left, right) =>
|
|
170
619
|
left.definition.id.localeCompare(right.definition.id),
|
|
@@ -191,16 +640,26 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
191
640
|
ctx: ExtensionCommandContext,
|
|
192
641
|
): Promise<void> {
|
|
193
642
|
return this.enqueueMutation(ctx, (sessionEpoch) =>
|
|
194
|
-
this.startNow(
|
|
643
|
+
this.startNow(
|
|
644
|
+
workflowId,
|
|
645
|
+
input,
|
|
646
|
+
{
|
|
647
|
+
context: ctx,
|
|
648
|
+
skills: () => ctx.getSystemPromptOptions().skills,
|
|
649
|
+
waitForIdle: () => ctx.waitForIdle(),
|
|
650
|
+
},
|
|
651
|
+
sessionEpoch,
|
|
652
|
+
),
|
|
195
653
|
);
|
|
196
654
|
}
|
|
197
655
|
|
|
198
656
|
private async startNow(
|
|
199
657
|
workflowId: string,
|
|
200
658
|
input: string,
|
|
201
|
-
|
|
659
|
+
startContext: WorkflowStartContext,
|
|
202
660
|
sessionEpoch: number,
|
|
203
661
|
): Promise<void> {
|
|
662
|
+
const { context: ctx } = startContext;
|
|
204
663
|
if (this.activeDelegation) {
|
|
205
664
|
ctx.ui.notify(
|
|
206
665
|
`Cannot start a workflow while subagent "${this.activeDelegation.agent}" is still cancelling`,
|
|
@@ -221,7 +680,7 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
221
680
|
}
|
|
222
681
|
if (!ctx.isIdle()) {
|
|
223
682
|
ctx.abort();
|
|
224
|
-
await
|
|
683
|
+
await startContext.waitForIdle();
|
|
225
684
|
}
|
|
226
685
|
if (!this.sessionActive || this.sessionEpoch !== sessionEpoch) {
|
|
227
686
|
ctx.ui.notify(
|
|
@@ -231,7 +690,7 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
231
690
|
return;
|
|
232
691
|
}
|
|
233
692
|
|
|
234
|
-
this.captureSkills(
|
|
693
|
+
this.captureSkills(startContext.skills());
|
|
235
694
|
if (!(await this.reloadCatalog(ctx, false))) {
|
|
236
695
|
ctx.ui.notify(
|
|
237
696
|
'Workflow start was superseded by a newer configuration load',
|
|
@@ -271,6 +730,7 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
271
730
|
this.persist();
|
|
272
731
|
this.isolateMainSessionTools();
|
|
273
732
|
this.updateStatus();
|
|
733
|
+
this.openWorkflowStatus(ctx);
|
|
274
734
|
this.launchCurrentStep(workflow);
|
|
275
735
|
}
|
|
276
736
|
|
|
@@ -510,7 +970,7 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
510
970
|
|
|
511
971
|
const preflightErrors = this.preflight(workflow, this.run.currentStepId);
|
|
512
972
|
if (preflightErrors.length > 0) {
|
|
513
|
-
this.run =
|
|
973
|
+
this.run = failRun(
|
|
514
974
|
this.run,
|
|
515
975
|
`Step preflight failed: ${preflightErrors.join('; ')}`,
|
|
516
976
|
Date.now(),
|
|
@@ -591,19 +1051,6 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
591
1051
|
await this.reloadCatalog(ctx, true);
|
|
592
1052
|
}
|
|
593
1053
|
|
|
594
|
-
async status(ctx: ExtensionCommandContext): Promise<void> {
|
|
595
|
-
const snapshot = this.workflowStatusSnapshot();
|
|
596
|
-
if (!snapshot) {
|
|
597
|
-
ctx.ui.notify('No workflow checkpoint in this session', 'info');
|
|
598
|
-
return;
|
|
599
|
-
}
|
|
600
|
-
if (ctx.hasUI && ctx.mode === 'tui') {
|
|
601
|
-
await showWorkflowStatus(ctx, () => this.workflowStatusSnapshot());
|
|
602
|
-
return;
|
|
603
|
-
}
|
|
604
|
-
ctx.ui.notify(formatWorkflowStatusText(snapshot), 'info');
|
|
605
|
-
}
|
|
606
|
-
|
|
607
1054
|
private workflowStatusSnapshot(): WorkflowStatusSnapshot | undefined {
|
|
608
1055
|
if (!this.run) return undefined;
|
|
609
1056
|
const workflow = this.catalog.workflows.get(this.run.workflowId);
|
|
@@ -662,6 +1109,7 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
662
1109
|
if (this.run) this.restoreBaselineTools();
|
|
663
1110
|
this.run = undefined;
|
|
664
1111
|
this.latestContext = undefined;
|
|
1112
|
+
this.stopStatusRefresh();
|
|
665
1113
|
});
|
|
666
1114
|
}
|
|
667
1115
|
|
|
@@ -672,7 +1120,7 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
672
1120
|
if (!this.run || this.run.status !== 'running') return;
|
|
673
1121
|
const workflow = this.catalog.workflows.get(this.run.workflowId);
|
|
674
1122
|
if (!workflow) {
|
|
675
|
-
this.run =
|
|
1123
|
+
this.run = failRun(
|
|
676
1124
|
this.run,
|
|
677
1125
|
'Workflow configuration disappeared; reload or restore it',
|
|
678
1126
|
Date.now(),
|
|
@@ -683,12 +1131,15 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
683
1131
|
return;
|
|
684
1132
|
}
|
|
685
1133
|
return {
|
|
686
|
-
systemPrompt: `${event.systemPrompt}\n\n${buildMainWorkflowNotice(workflow, this.run)}`,
|
|
1134
|
+
systemPrompt: `${event.systemPrompt}\n\n${buildMainWorkflowNotice(workflow, this.run, this.statusShortcutLabel)}`,
|
|
687
1135
|
};
|
|
688
1136
|
});
|
|
689
1137
|
}
|
|
690
1138
|
|
|
691
|
-
private launchCurrentStep(
|
|
1139
|
+
private launchCurrentStep(
|
|
1140
|
+
workflow: LoadedWorkflow,
|
|
1141
|
+
reinforcementRetry?: { count: number; reason: string },
|
|
1142
|
+
): void {
|
|
692
1143
|
const run = this.run;
|
|
693
1144
|
if (
|
|
694
1145
|
!run ||
|
|
@@ -712,6 +1163,18 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
712
1163
|
return;
|
|
713
1164
|
}
|
|
714
1165
|
|
|
1166
|
+
const reviewedRepository = resolveReviewedRepositoryCwd(
|
|
1167
|
+
run.reviewedArtifact ?? '',
|
|
1168
|
+
);
|
|
1169
|
+
if (reviewedRepository.kind === 'invalid') {
|
|
1170
|
+
this.pauseForExecutionFailure('Subagent step', reviewedRepository.reason);
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
const delegationCwd =
|
|
1174
|
+
reviewedRepository.kind === 'resolved'
|
|
1175
|
+
? reviewedRepository.cwd
|
|
1176
|
+
: (this.latestContext?.cwd ?? process.cwd());
|
|
1177
|
+
const runtimeAgent = subagent.agent;
|
|
715
1178
|
const requestId = `${run.runId}:${run.currentStepId}:${randomUUID()}`;
|
|
716
1179
|
const resultDirectory = mkdtempSync(join(tmpdir(), 'pi-workflows-step-'));
|
|
717
1180
|
const capabilityPath = join(resultDirectory, 'capability');
|
|
@@ -722,25 +1185,38 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
722
1185
|
flag: 'wx',
|
|
723
1186
|
mode: 0o600,
|
|
724
1187
|
});
|
|
725
|
-
const
|
|
1188
|
+
const outcomes = allowedOutcomes(workflow, run);
|
|
1189
|
+
const outcomeSet = new Set(outcomes);
|
|
1190
|
+
const approvedBashCommands = narrowApprovedBashCommands(
|
|
726
1191
|
run.reviewedArtifact ?? '',
|
|
1192
|
+
run.stepHandoff ?? '',
|
|
727
1193
|
step.permissions.bash.approvedSources ?? [],
|
|
728
1194
|
);
|
|
1195
|
+
const repositoryPolicy =
|
|
1196
|
+
reviewedRepository.kind === 'resolved'
|
|
1197
|
+
? {
|
|
1198
|
+
repositoryCwd: reviewedRepository.repositoryCwd,
|
|
1199
|
+
...(reviewedRepository.bootstrapping
|
|
1200
|
+
? { bootstrapCwd: reviewedRepository.cwd }
|
|
1201
|
+
: {}),
|
|
1202
|
+
}
|
|
1203
|
+
: {};
|
|
729
1204
|
const policyDigest = digest({
|
|
730
1205
|
version: 1,
|
|
731
1206
|
requestId,
|
|
732
|
-
agent:
|
|
1207
|
+
agent: runtimeAgent,
|
|
733
1208
|
runId: run.runId,
|
|
734
1209
|
stepId: run.currentStepId,
|
|
735
1210
|
stepDigest: run.currentStepDigest,
|
|
736
1211
|
capabilityPath,
|
|
737
1212
|
resultPath,
|
|
738
1213
|
approvedBashCommands,
|
|
1214
|
+
...repositoryPolicy,
|
|
739
1215
|
});
|
|
740
1216
|
const policy: ChildStepPolicy = {
|
|
741
1217
|
version: 1,
|
|
742
1218
|
requestId,
|
|
743
|
-
agent:
|
|
1219
|
+
agent: runtimeAgent,
|
|
744
1220
|
workflowId: workflow.definition.id,
|
|
745
1221
|
runId: run.runId,
|
|
746
1222
|
stepId: run.currentStepId,
|
|
@@ -751,10 +1227,32 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
751
1227
|
resultPath,
|
|
752
1228
|
permissions: structuredClone(step.permissions),
|
|
753
1229
|
...(approvedBashCommands.length > 0 ? { approvedBashCommands } : {}),
|
|
754
|
-
|
|
1230
|
+
...repositoryPolicy,
|
|
1231
|
+
outcomes,
|
|
1232
|
+
pauseOutcomes: Object.entries(step.transitions)
|
|
1233
|
+
.filter(
|
|
1234
|
+
([outcome, target]) => target === '$pause' && outcomeSet.has(outcome),
|
|
1235
|
+
)
|
|
1236
|
+
.map(([outcome]) => outcome),
|
|
755
1237
|
summaryMaxChars: workflow.definition.summaryMaxChars,
|
|
756
1238
|
...(step.gate ? { gateSubmitOutcome: step.gate.submitOutcome } : {}),
|
|
757
1239
|
};
|
|
1240
|
+
const trustedSessionRoot = deriveSubagentSessionRoot(
|
|
1241
|
+
this.latestContext?.sessionManager.getSessionFile(),
|
|
1242
|
+
);
|
|
1243
|
+
const transcriptTask = [
|
|
1244
|
+
buildDelegatedStepTask(workflow, run, ''),
|
|
1245
|
+
delegationTranscriptBinding(requestId, policyDigest),
|
|
1246
|
+
...(reinforcementRetry
|
|
1247
|
+
? [
|
|
1248
|
+
reinforcementRetryTask(
|
|
1249
|
+
reinforcementRetry.reason,
|
|
1250
|
+
reinforcementRetry.count,
|
|
1251
|
+
MAX_REINFORCEMENT_RETRIES,
|
|
1252
|
+
),
|
|
1253
|
+
]
|
|
1254
|
+
: []),
|
|
1255
|
+
].join('\n\n');
|
|
758
1256
|
const active: ActiveDelegation = {
|
|
759
1257
|
requestId,
|
|
760
1258
|
runId: run.runId,
|
|
@@ -763,25 +1261,32 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
763
1261
|
sessionEpoch: this.sessionEpoch,
|
|
764
1262
|
resultDirectory,
|
|
765
1263
|
policy,
|
|
1264
|
+
transcriptTask,
|
|
766
1265
|
agent: subagent.agent,
|
|
1266
|
+
...(trustedSessionRoot ? { trustedSessionRoot } : {}),
|
|
1267
|
+
reinforcementReplayAuthorized: subagent.retryToolFailures,
|
|
1268
|
+
reinforcementRetryCount: reinforcementRetry?.count ?? 0,
|
|
767
1269
|
};
|
|
768
1270
|
const request: SubagentDelegationRequest = {
|
|
769
1271
|
version: 1,
|
|
770
1272
|
requestId,
|
|
771
|
-
agent:
|
|
772
|
-
task:
|
|
773
|
-
|
|
774
|
-
|
|
1273
|
+
agent: runtimeAgent,
|
|
1274
|
+
task: `${encodeChildPolicy(policy)}\n\n${transcriptTask}`,
|
|
1275
|
+
// Workflow steps are isolation boundaries. Never fork the parent or a
|
|
1276
|
+
// sibling step's transcript; pass only the explicit workflow handoff.
|
|
1277
|
+
context: 'fresh',
|
|
1278
|
+
cwd: delegationCwd,
|
|
775
1279
|
timeoutMs: subagent.timeoutMs,
|
|
776
1280
|
skill:
|
|
777
1281
|
step.permissions.skills.length > 0
|
|
778
1282
|
? [...step.permissions.skills]
|
|
779
1283
|
: false,
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
1284
|
+
output: false,
|
|
1285
|
+
outputSchema: WORKFLOW_COMPLETION_PARAMETERS as unknown as Record<
|
|
1286
|
+
string,
|
|
1287
|
+
unknown
|
|
1288
|
+
>,
|
|
1289
|
+
agentContract: { version: 1 },
|
|
785
1290
|
artifacts: subagent.artifacts,
|
|
786
1291
|
...(subagent.model ? { model: subagent.model } : {}),
|
|
787
1292
|
...(subagent.turnBudget
|
|
@@ -819,8 +1324,9 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
819
1324
|
run: WorkflowRun,
|
|
820
1325
|
step: WorkflowStep,
|
|
821
1326
|
): void {
|
|
822
|
-
const approvedBashCommands =
|
|
1327
|
+
const approvedBashCommands = narrowApprovedBashCommands(
|
|
823
1328
|
run.reviewedArtifact ?? '',
|
|
1329
|
+
run.stepHandoff ?? '',
|
|
824
1330
|
step.permissions.bash.approvedSources ?? [],
|
|
825
1331
|
);
|
|
826
1332
|
const identity: MainStepIdentity = {
|
|
@@ -998,6 +1504,7 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
998
1504
|
return;
|
|
999
1505
|
}
|
|
1000
1506
|
this.activeDelegation = undefined;
|
|
1507
|
+
let terminalFailure: DelegationFailureDetails | undefined;
|
|
1001
1508
|
|
|
1002
1509
|
try {
|
|
1003
1510
|
if (
|
|
@@ -1011,19 +1518,34 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
1011
1518
|
) {
|
|
1012
1519
|
return;
|
|
1013
1520
|
}
|
|
1014
|
-
if (response.status !== 'completed') {
|
|
1015
|
-
throw new Error(
|
|
1016
|
-
`Subagent "${active.agent}" ${response.status.replaceAll('_', ' ')}${
|
|
1017
|
-
response.error ? `: ${response.error}` : ''
|
|
1018
|
-
}`,
|
|
1019
|
-
);
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
1521
|
const workflow = this.catalog.workflows.get(this.run.workflowId);
|
|
1023
1522
|
const step = workflow?.definition.steps[this.run.currentStepId];
|
|
1024
1523
|
if (!workflow || !step) {
|
|
1025
1524
|
throw new Error('Active workflow configuration is unavailable');
|
|
1026
1525
|
}
|
|
1526
|
+
let recoveredTerminalFailure: DelegationFailureDetails | undefined;
|
|
1527
|
+
if (
|
|
1528
|
+
response.status !== 'completed' ||
|
|
1529
|
+
hasContradictoryCompletion(response)
|
|
1530
|
+
) {
|
|
1531
|
+
const failure = await delegationFailureDetails(active, response);
|
|
1532
|
+
terminalFailure = failure;
|
|
1533
|
+
if (
|
|
1534
|
+
response.status !== 'failed' ||
|
|
1535
|
+
failure.diagnostic?.completionAfterFailure !== true
|
|
1536
|
+
) {
|
|
1537
|
+
throw new Error(failure.reason);
|
|
1538
|
+
}
|
|
1539
|
+
const projectionError = recoveredProjectionError(
|
|
1540
|
+
active,
|
|
1541
|
+
response,
|
|
1542
|
+
failure.diagnostic,
|
|
1543
|
+
);
|
|
1544
|
+
if (projectionError) {
|
|
1545
|
+
throw new Error(rejectedRecoveryReason(failure, projectionError));
|
|
1546
|
+
}
|
|
1547
|
+
recoveredTerminalFailure = failure;
|
|
1548
|
+
}
|
|
1027
1549
|
const requiredSkillWarning =
|
|
1028
1550
|
step.requires.skills.length > 0
|
|
1029
1551
|
? response.warnings?.find((warning) => /skill/i.test(warning))
|
|
@@ -1034,10 +1556,65 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
1034
1556
|
);
|
|
1035
1557
|
}
|
|
1036
1558
|
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1559
|
+
let serializedResult: string;
|
|
1560
|
+
try {
|
|
1561
|
+
serializedResult = await readStableDelegatedResult(active);
|
|
1562
|
+
} catch (error) {
|
|
1563
|
+
if (recoveredTerminalFailure) {
|
|
1564
|
+
throw new Error(
|
|
1565
|
+
rejectedRecoveryReason(recoveredTerminalFailure, error),
|
|
1566
|
+
{ cause: error },
|
|
1567
|
+
);
|
|
1568
|
+
}
|
|
1569
|
+
if (
|
|
1570
|
+
(error as { code?: unknown } | null | undefined)?.code === 'ENOENT'
|
|
1571
|
+
) {
|
|
1572
|
+
throw new Error(
|
|
1573
|
+
`Subagent "${active.agent}" completed without producing the required correlated structured_output result`,
|
|
1574
|
+
{ cause: error },
|
|
1575
|
+
);
|
|
1576
|
+
}
|
|
1577
|
+
throw error;
|
|
1578
|
+
}
|
|
1579
|
+
let result: WorkflowStepResult;
|
|
1580
|
+
try {
|
|
1581
|
+
const rawResult = JSON.parse(serializedResult) as unknown;
|
|
1582
|
+
result = parseDelegatedStepResult(rawResult, active.policy);
|
|
1583
|
+
} catch (error) {
|
|
1584
|
+
if (recoveredTerminalFailure) {
|
|
1585
|
+
throw new Error(
|
|
1586
|
+
rejectedRecoveryReason(recoveredTerminalFailure, error),
|
|
1587
|
+
{ cause: error },
|
|
1588
|
+
);
|
|
1589
|
+
}
|
|
1590
|
+
throw error;
|
|
1591
|
+
}
|
|
1592
|
+
if (
|
|
1593
|
+
recoveredTerminalFailure?.diagnostic &&
|
|
1594
|
+
!completionMatchesResult(
|
|
1595
|
+
recoveredTerminalFailure.diagnostic,
|
|
1596
|
+
result,
|
|
1597
|
+
active.policy,
|
|
1598
|
+
)
|
|
1599
|
+
) {
|
|
1600
|
+
throw new Error(
|
|
1601
|
+
rejectedRecoveryReason(
|
|
1602
|
+
recoveredTerminalFailure,
|
|
1603
|
+
'structured_output transcript value does not match the correlated result',
|
|
1604
|
+
),
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
1607
|
+
if (recoveredTerminalFailure) {
|
|
1608
|
+
const falsePositive =
|
|
1609
|
+
recoveredTerminalFailure.diagnostic?.correlation ===
|
|
1610
|
+
'successful-output-before-completion';
|
|
1611
|
+
this.latestContext?.ui.notify(
|
|
1612
|
+
falsePositive
|
|
1613
|
+
? `Accepted "${active.stepId}" because the trusted child transcript proved the terminal tool error was a false positive and produced a matching structured result`
|
|
1614
|
+
: `Accepted "${active.stepId}" because the child resolved an earlier tool failure and produced a valid structured result`,
|
|
1615
|
+
'warning',
|
|
1616
|
+
);
|
|
1617
|
+
}
|
|
1041
1618
|
if (step.gate?.submitOutcome === result.outcome) {
|
|
1042
1619
|
await this.submitGate(
|
|
1043
1620
|
workflow,
|
|
@@ -1057,9 +1634,10 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
1057
1634
|
);
|
|
1058
1635
|
this.settleAfterTransition(workflow);
|
|
1059
1636
|
} catch (error) {
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1637
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1638
|
+
if (!this.retryDelegationAfterFailure(active, terminalFailure, reason)) {
|
|
1639
|
+
this.pauseForDelegationFailure(reason);
|
|
1640
|
+
}
|
|
1063
1641
|
} finally {
|
|
1064
1642
|
await this.cleanupDelegation(active);
|
|
1065
1643
|
if (active.cancelling) this.releaseMainAfterCancellation(active);
|
|
@@ -1098,6 +1676,45 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
1098
1676
|
await rm(active.resultDirectory, { recursive: true, force: true });
|
|
1099
1677
|
}
|
|
1100
1678
|
|
|
1679
|
+
private retryDelegationAfterFailure(
|
|
1680
|
+
active: ActiveDelegation,
|
|
1681
|
+
failure: DelegationFailureDetails | undefined,
|
|
1682
|
+
reason: string,
|
|
1683
|
+
): boolean {
|
|
1684
|
+
if (
|
|
1685
|
+
!failure ||
|
|
1686
|
+
active.reinforcementRetryCount >= MAX_REINFORCEMENT_RETRIES ||
|
|
1687
|
+
!isRetryableTerminalFailure(failure) ||
|
|
1688
|
+
!isSafeToRetryDelegation(
|
|
1689
|
+
active.policy,
|
|
1690
|
+
active.reinforcementReplayAuthorized,
|
|
1691
|
+
failure.replayAudit,
|
|
1692
|
+
) ||
|
|
1693
|
+
!this.sessionActive ||
|
|
1694
|
+
this.sessionEpoch !== active.sessionEpoch ||
|
|
1695
|
+
!this.run ||
|
|
1696
|
+
this.run.status !== 'running' ||
|
|
1697
|
+
this.run.runId !== active.runId ||
|
|
1698
|
+
this.run.currentStepId !== active.stepId ||
|
|
1699
|
+
this.run.currentStepDigest !== active.stepDigest ||
|
|
1700
|
+
this.activeDelegation
|
|
1701
|
+
) {
|
|
1702
|
+
return false;
|
|
1703
|
+
}
|
|
1704
|
+
const workflow = this.catalog.workflows.get(this.run.workflowId);
|
|
1705
|
+
if (!workflow) return false;
|
|
1706
|
+
|
|
1707
|
+
this.latestContext?.ui.notify(
|
|
1708
|
+
`Reinforcement retry for "${active.stepId}" after a subagent failure (${active.reinforcementRetryCount + 1}/${MAX_REINFORCEMENT_RETRIES})`,
|
|
1709
|
+
'warning',
|
|
1710
|
+
);
|
|
1711
|
+
this.launchCurrentStep(workflow, {
|
|
1712
|
+
count: active.reinforcementRetryCount + 1,
|
|
1713
|
+
reason,
|
|
1714
|
+
});
|
|
1715
|
+
return true;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1101
1718
|
private pauseForDelegationFailure(reason: string): void {
|
|
1102
1719
|
this.pauseForExecutionFailure('Subagent step', reason);
|
|
1103
1720
|
}
|
|
@@ -1105,7 +1722,7 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
1105
1722
|
private pauseForExecutionFailure(label: string, reason: string): void {
|
|
1106
1723
|
if (!this.run || this.run.status !== 'running') return;
|
|
1107
1724
|
this.mainSteps.deactivate();
|
|
1108
|
-
this.run =
|
|
1725
|
+
this.run = failRun(this.run, `${label} failed: ${reason}`, Date.now());
|
|
1109
1726
|
this.persist();
|
|
1110
1727
|
if (this.activeDelegation) {
|
|
1111
1728
|
this.isolateMainSessionTools();
|
|
@@ -1126,7 +1743,7 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
1126
1743
|
active.cancelling = true;
|
|
1127
1744
|
active.progress = 'cancellation unconfirmed';
|
|
1128
1745
|
if (this.run?.status === 'running') {
|
|
1129
|
-
this.run =
|
|
1746
|
+
this.run = failRun(
|
|
1130
1747
|
this.run,
|
|
1131
1748
|
`Subagent step failed: ${reason}`,
|
|
1132
1749
|
Date.now(),
|
|
@@ -1169,6 +1786,34 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
1169
1786
|
const step = workflow.definition.steps[originalRun.currentStepId];
|
|
1170
1787
|
if (!step?.gate) throw new Error('Current step has no gate');
|
|
1171
1788
|
|
|
1789
|
+
const commandShapeError = reviewedCommandShapeError(artifact);
|
|
1790
|
+
if (commandShapeError) {
|
|
1791
|
+
const awaitingReview = beginGate(
|
|
1792
|
+
workflow,
|
|
1793
|
+
originalRun,
|
|
1794
|
+
outcome,
|
|
1795
|
+
artifact,
|
|
1796
|
+
requestId,
|
|
1797
|
+
Date.now(),
|
|
1798
|
+
);
|
|
1799
|
+
this.run = resolveGate(
|
|
1800
|
+
workflow,
|
|
1801
|
+
awaitingReview,
|
|
1802
|
+
{
|
|
1803
|
+
approved: false,
|
|
1804
|
+
feedback: commandShapeError,
|
|
1805
|
+
resolvedAt: Date.now(),
|
|
1806
|
+
},
|
|
1807
|
+
Date.now(),
|
|
1808
|
+
);
|
|
1809
|
+
this.latestContext?.ui.notify(
|
|
1810
|
+
`Plan contract needs repair before review: ${commandShapeError}`,
|
|
1811
|
+
'warning',
|
|
1812
|
+
);
|
|
1813
|
+
this.settleAfterTransition(workflow);
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1172
1817
|
this.run = beginGate(
|
|
1173
1818
|
workflow,
|
|
1174
1819
|
originalRun,
|
|
@@ -1213,10 +1858,7 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
1213
1858
|
if (response.status !== 'handled') {
|
|
1214
1859
|
const reason = response.error ?? 'Plannotator is unavailable';
|
|
1215
1860
|
const gateFailed = failGate(this.run, reason, Date.now());
|
|
1216
|
-
this.run =
|
|
1217
|
-
this.run.status === 'paused'
|
|
1218
|
-
? pauseRun(gateFailed, reason, Date.now())
|
|
1219
|
-
: gateFailed;
|
|
1861
|
+
this.run = failRun(gateFailed, reason, Date.now());
|
|
1220
1862
|
this.persist();
|
|
1221
1863
|
if (this.run.status === 'running') {
|
|
1222
1864
|
this.isolateMainSessionTools();
|
|
@@ -1250,6 +1892,7 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
1250
1892
|
this.pausePromptGate(
|
|
1251
1893
|
pendingGate.requestId,
|
|
1252
1894
|
'Built-in review requires Pi TUI or RPC mode; resume there to continue',
|
|
1895
|
+
false,
|
|
1253
1896
|
);
|
|
1254
1897
|
return;
|
|
1255
1898
|
}
|
|
@@ -1304,6 +1947,7 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
1304
1947
|
this.pausePromptGate(
|
|
1305
1948
|
active.requestId,
|
|
1306
1949
|
`Built-in review failed: ${reason}`,
|
|
1950
|
+
true,
|
|
1307
1951
|
);
|
|
1308
1952
|
})
|
|
1309
1953
|
.catch((error: unknown) => {
|
|
@@ -1335,6 +1979,7 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
1335
1979
|
this.pausePromptGate(
|
|
1336
1980
|
active.requestId,
|
|
1337
1981
|
'Built-in review was dismissed; resume to reopen it',
|
|
1982
|
+
false,
|
|
1338
1983
|
);
|
|
1339
1984
|
return;
|
|
1340
1985
|
}
|
|
@@ -1360,6 +2005,7 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
1360
2005
|
this.pausePromptGate(
|
|
1361
2006
|
active.requestId,
|
|
1362
2007
|
'Built-in review finished, but workflow configuration is unavailable',
|
|
2008
|
+
true,
|
|
1363
2009
|
);
|
|
1364
2010
|
return;
|
|
1365
2011
|
}
|
|
@@ -1370,11 +2016,16 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
1370
2016
|
this.pausePromptGate(
|
|
1371
2017
|
active.requestId,
|
|
1372
2018
|
`Cannot apply built-in review: ${error instanceof Error ? error.message : String(error)}`,
|
|
2019
|
+
true,
|
|
1373
2020
|
);
|
|
1374
2021
|
}
|
|
1375
2022
|
}
|
|
1376
2023
|
|
|
1377
|
-
private pausePromptGate(
|
|
2024
|
+
private pausePromptGate(
|
|
2025
|
+
requestId: string,
|
|
2026
|
+
reason: string,
|
|
2027
|
+
failed: boolean,
|
|
2028
|
+
): void {
|
|
1378
2029
|
if (
|
|
1379
2030
|
!this.run ||
|
|
1380
2031
|
this.run.pendingGate?.provider !== 'prompt' ||
|
|
@@ -1383,7 +2034,9 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
1383
2034
|
return;
|
|
1384
2035
|
}
|
|
1385
2036
|
if (this.run.status === 'awaiting-gate') {
|
|
1386
|
-
this.run =
|
|
2037
|
+
this.run = failed
|
|
2038
|
+
? failRun(this.run, reason, Date.now())
|
|
2039
|
+
: pauseRun(this.run, reason, Date.now());
|
|
1387
2040
|
}
|
|
1388
2041
|
this.persist();
|
|
1389
2042
|
this.restoreBaselineTools();
|
|
@@ -1445,7 +2098,7 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
1445
2098
|
|
|
1446
2099
|
const workflow = this.catalog.workflows.get(this.run.workflowId);
|
|
1447
2100
|
if (!workflow) {
|
|
1448
|
-
this.run =
|
|
2101
|
+
this.run = failRun(
|
|
1449
2102
|
this.run,
|
|
1450
2103
|
'Gate result arrived, but workflow configuration is unavailable',
|
|
1451
2104
|
Date.now(),
|
|
@@ -1459,7 +2112,7 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
1459
2112
|
this.run = resolveGate(workflow, this.run, resolution, Date.now());
|
|
1460
2113
|
this.settleAfterTransition(workflow);
|
|
1461
2114
|
} catch (error) {
|
|
1462
|
-
this.run =
|
|
2115
|
+
this.run = failRun(
|
|
1463
2116
|
this.run,
|
|
1464
2117
|
`Cannot apply gate result: ${error instanceof Error ? error.message : String(error)}`,
|
|
1465
2118
|
Date.now(),
|
|
@@ -1475,7 +2128,7 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
1475
2128
|
if (this.run.status === 'running') {
|
|
1476
2129
|
const preflightErrors = this.preflight(workflow, this.run.currentStepId);
|
|
1477
2130
|
if (preflightErrors.length > 0) {
|
|
1478
|
-
this.run =
|
|
2131
|
+
this.run = failRun(
|
|
1479
2132
|
this.run,
|
|
1480
2133
|
`Step preflight failed: ${preflightErrors.join('; ')}`,
|
|
1481
2134
|
Date.now(),
|
|
@@ -1610,6 +2263,15 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
1610
2263
|
return false;
|
|
1611
2264
|
}
|
|
1612
2265
|
this.latestContext = ctx;
|
|
2266
|
+
if (catalog.settings.statusShortcut !== this.statusShortcut) {
|
|
2267
|
+
catalog.diagnostics.push({
|
|
2268
|
+
level: 'warning',
|
|
2269
|
+
path: join(catalog.userDirectory, 'settings.yaml'),
|
|
2270
|
+
message:
|
|
2271
|
+
`settings.statusShortcut is "${catalog.settings.statusShortcut}", ` +
|
|
2272
|
+
`but the active shortcut is "${this.statusShortcut}"; run Pi /reload to apply shortcut changes`,
|
|
2273
|
+
});
|
|
2274
|
+
}
|
|
1613
2275
|
const availableCommands = this.pi.getCommands();
|
|
1614
2276
|
for (const [workflowId, workflow] of catalog.workflows) {
|
|
1615
2277
|
const command = workflow.definition.command;
|
|
@@ -1658,19 +2320,87 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
1658
2320
|
}
|
|
1659
2321
|
|
|
1660
2322
|
private updateStatus(): void {
|
|
2323
|
+
this.refreshStatusWhileRunning();
|
|
1661
2324
|
if (!this.latestContext) return;
|
|
2325
|
+
if (this.legacyProgressWidgetContext !== this.latestContext) {
|
|
2326
|
+
this.latestContext.ui.setWidget(LEGACY_PROGRESS_WIDGET_KEY, undefined);
|
|
2327
|
+
this.legacyProgressWidgetContext = this.latestContext;
|
|
2328
|
+
}
|
|
1662
2329
|
if (!this.run) {
|
|
1663
2330
|
this.latestContext.ui.setStatus(STATUS_KEY, undefined);
|
|
1664
2331
|
return;
|
|
1665
2332
|
}
|
|
1666
|
-
const
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
2333
|
+
const snapshot = this.workflowStatusSnapshot();
|
|
2334
|
+
if (this.run.status !== 'running') {
|
|
2335
|
+
this.latestContext.ui.setStatus(STATUS_KEY, undefined);
|
|
2336
|
+
return;
|
|
2337
|
+
}
|
|
1671
2338
|
this.latestContext.ui.setStatus(
|
|
1672
2339
|
STATUS_KEY,
|
|
1673
|
-
`${this.run
|
|
2340
|
+
`${workflowStatusIcon(this.run, snapshot?.now)} ${this.run.workflowId}: working · ${this.statusShortcutLabel}`,
|
|
1674
2341
|
);
|
|
1675
2342
|
}
|
|
2343
|
+
|
|
2344
|
+
private refreshStatusWhileRunning(): void {
|
|
2345
|
+
if (this.run?.status === 'running' && this.latestContext) {
|
|
2346
|
+
if (this.statusRefreshTimer) return;
|
|
2347
|
+
this.statusRefreshTimer = setInterval(
|
|
2348
|
+
() => this.updateStatus(),
|
|
2349
|
+
STATUS_REFRESH_INTERVAL_MS,
|
|
2350
|
+
);
|
|
2351
|
+
this.statusRefreshTimer.unref?.();
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2354
|
+
this.stopStatusRefresh();
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
private stopStatusRefresh(): void {
|
|
2358
|
+
if (this.statusRefreshTimer) clearInterval(this.statusRefreshTimer);
|
|
2359
|
+
this.statusRefreshTimer = undefined;
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
private registerWorkflowStatusShortcut(): void {
|
|
2363
|
+
this.pi.registerShortcut(this.statusShortcut, {
|
|
2364
|
+
description: 'Toggle workflow status',
|
|
2365
|
+
handler: async (ctx) => {
|
|
2366
|
+
this.latestContext = ctx;
|
|
2367
|
+
if (this.statusOverlayOpen) return;
|
|
2368
|
+
if (!this.run) {
|
|
2369
|
+
ctx.ui.notify('No workflow checkpoint in this session', 'info');
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
await this.showWorkflowStatus(ctx);
|
|
2373
|
+
},
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
private openWorkflowStatus(ctx: ExtensionContext): void {
|
|
2378
|
+
void this.showWorkflowStatus(ctx).catch((error: unknown) => {
|
|
2379
|
+
ctx.ui.notify(
|
|
2380
|
+
`Cannot open workflow status: ${error instanceof Error ? error.message : String(error)}`,
|
|
2381
|
+
'error',
|
|
2382
|
+
);
|
|
2383
|
+
});
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
private async showWorkflowStatus(ctx: ExtensionContext): Promise<void> {
|
|
2387
|
+
if (
|
|
2388
|
+
this.statusOverlayOpen ||
|
|
2389
|
+
!this.run ||
|
|
2390
|
+
!ctx.hasUI ||
|
|
2391
|
+
ctx.mode !== 'tui'
|
|
2392
|
+
) {
|
|
2393
|
+
return;
|
|
2394
|
+
}
|
|
2395
|
+
this.statusOverlayOpen = true;
|
|
2396
|
+
try {
|
|
2397
|
+
await showWorkflowStatusOverlay(
|
|
2398
|
+
ctx,
|
|
2399
|
+
() => this.workflowStatusSnapshot(),
|
|
2400
|
+
this.statusShortcut,
|
|
2401
|
+
);
|
|
2402
|
+
} finally {
|
|
2403
|
+
this.statusOverlayOpen = false;
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
1676
2406
|
}
|