@wichayutdew/pi-workflows 1.0.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +312 -223
- package/agents/step.md +4 -11
- package/dist/index.js +2554 -976
- package/examples/mr-comments.workflow.yaml +16 -8
- package/examples/prompts/mr-comments/implement.md +13 -9
- package/examples/prompts/mr-comments/plan.md +44 -15
- package/examples/prompts/mr-comments/verify.md +8 -7
- package/examples/settings.yaml +7 -1
- package/examples/starter-kit/mr-comment.workflow.yaml +149 -0
- package/examples/starter-kit/mr-review.workflow.yaml +130 -0
- package/examples/starter-kit/steps/mr-comment/fetch.md +33 -0
- package/examples/starter-kit/steps/mr-comment/implement.md +38 -0
- package/examples/starter-kit/steps/mr-comment/plan.md +62 -0
- package/examples/starter-kit/steps/mr-comment/publish.md +36 -0
- package/examples/starter-kit/steps/mr-comment/verify.md +35 -0
- package/examples/starter-kit/steps/mr-review/fetch.md +25 -0
- package/examples/starter-kit/steps/mr-review/publish.md +30 -0
- package/examples/starter-kit/steps/mr-review/review.md +69 -0
- package/examples/starter-kit/steps/mr-review/verify.md +24 -0
- package/examples/starter-kit/steps/shared/prepare-workspace.md +57 -0
- package/examples/starter-kit/steps/ticket/implement.md +35 -0
- package/examples/starter-kit/steps/ticket/plan.md +40 -0
- package/examples/starter-kit/steps/ticket/verify.md +29 -0
- package/examples/starter-kit/steps/work/implement.md +34 -0
- package/examples/starter-kit/steps/work/plan.md +38 -0
- package/examples/starter-kit/steps/work/verify.md +30 -0
- package/examples/starter-kit/ticket.workflow.yaml +100 -0
- package/examples/starter-kit/work.workflow.yaml +97 -0
- package/package.json +3 -3
- package/schemas/workflow.schema.json +78 -44
- package/src/command-names.ts +1 -0
- package/src/commands.ts +33 -4
- package/src/config/ceiling.ts +6 -12
- package/src/config/load-workflows.ts +12 -1
- package/src/config/step-digests.ts +19 -0
- package/src/config/types.ts +17 -22
- package/src/config/validation/permissions.ts +5 -66
- package/src/config/validation/prompt.ts +1 -0
- package/src/config/validation/shared.ts +0 -2
- package/src/config/validation/step.ts +106 -2
- package/src/config/validation/workflow.ts +76 -0
- package/src/engine/create-run.ts +3 -0
- package/src/engine/gate-transitions.ts +58 -9
- package/src/engine/reconciliation-history.ts +61 -24
- package/src/engine/run-advance.ts +37 -4
- package/src/engine/run-lifecycle.ts +11 -0
- package/src/engine/run-reconciliation.ts +51 -21
- package/src/engine/run-validation.ts +235 -0
- package/src/engine/run-workflow-validation.ts +191 -0
- package/src/engine/state-types.ts +108 -5
- package/src/engine/state.ts +15 -1
- package/src/engine/step-trace.ts +495 -0
- package/src/engine/transitions.ts +1 -0
- package/src/harness/action-context.ts +31 -6
- package/src/harness/core-actions.ts +30 -1
- package/src/harness/delegation-control-actions.ts +27 -2
- package/src/harness/delegation-failure.ts +42 -9
- package/src/harness/delegation-plan.ts +96 -40
- package/src/harness/delegation-recovery-validation.ts +3 -1
- package/src/harness/delegation-response-actions.ts +73 -3
- package/src/harness/delegation-retry-policy.ts +5 -3
- package/src/harness/dependencies.ts +11 -2
- package/src/harness/gate-submission-action.ts +5 -31
- package/src/harness/lifecycle-actions.ts +4 -5
- package/src/harness/pause-actions.ts +10 -1
- package/src/harness/plannotator-result-actions.ts +20 -11
- package/src/harness/prompt-gate-actions.ts +40 -2
- package/src/harness/resume-action.ts +75 -1
- package/src/harness/start-actions.ts +77 -3
- package/src/harness/status-actions.ts +7 -17
- package/src/harness/step-effects.ts +61 -0
- package/src/harness/step-execution-actions.ts +181 -25
- package/src/harness/step-reporting.ts +168 -0
- package/src/harness/types.ts +1 -0
- package/src/harness/workspace-directory.ts +67 -0
- package/src/harness.ts +37 -9
- package/src/integrations/plannotator-requests.ts +40 -5
- package/src/integrations/subagents/child-policy-sections.ts +71 -84
- package/src/integrations/subagents/child-policy-types.ts +7 -7
- package/src/integrations/subagents/child-policy-validation.ts +10 -4
- package/src/integrations/subagents/child-runtime-completion.ts +1 -0
- package/src/integrations/subagents/child-runtime-files.ts +29 -86
- package/src/integrations/subagents/child-runtime-policy.ts +8 -33
- package/src/integrations/subagents/child-runtime.ts +3 -15
- package/src/integrations/subagents/delegated-result.ts +1 -0
- package/src/integrations/subagents/diagnostic-format.ts +5 -0
- package/src/integrations/subagents/diagnostic-types.ts +7 -1
- package/src/integrations/subagents/diagnostics.ts +2 -0
- package/src/integrations/subagents/failure-correlation.ts +52 -2
- package/src/integrations/subagents/failure-transcript.ts +60 -1
- package/src/integrations/subagents/replay-audit.ts +0 -1
- package/src/integrations/subagents/replay-safety.ts +12 -27
- package/src/integrations/subagents/session-diagnostics.ts +102 -3
- package/src/policy/bash-authorization.ts +2 -21
- package/src/policy/bash-types.ts +0 -5
- package/src/policy/bash.ts +1 -2
- package/src/policy/tool-call-authorization.ts +1 -7
- package/src/preflight.ts +0 -5
- package/src/prompt/retry-task.ts +11 -3
- package/src/prompt/step-contract.ts +10 -56
- package/src/prompt/step-sections.ts +8 -10
- package/src/prompt/step-task.ts +41 -10
- package/src/prompt/template.ts +3 -2
- package/src/runtime/completion-tool.ts +14 -0
- package/src/runtime/main-step-completion.ts +3 -0
- package/src/runtime/main-step-policy.ts +2 -1
- package/src/runtime/main-step-runtime-types.ts +9 -1
- package/src/runtime/main-step-runtime.ts +2 -0
- package/src/runtime/main-step-state.ts +6 -0
- package/src/runtime/main-step-trace.ts +45 -0
- package/src/runtime/step-result.ts +57 -0
- package/src/step-log.ts +178 -0
- package/src/workflow-doctor.ts +254 -0
- package/src/workflow-status/format-status.ts +32 -3
- package/src/workflow-status/render-board.ts +7 -4
- package/src/workflow-status/render-path.ts +51 -24
- package/src/workflow-status/render-step-detail.ts +379 -0
- package/src/workflow-status/render-summary.ts +22 -0
- package/src/workflow-status/transcript-reader.ts +231 -0
- package/src/workflow-status/types.ts +8 -0
- package/src/workflow-status/view.ts +161 -14
- package/src/workflow-status.ts +6 -0
- package/src/policy/approved-command-extraction.ts +0 -71
- package/src/policy/approved-commands.ts +0 -15
- package/src/policy/bash-read-only.ts +0 -161
- package/src/policy/restricted-git.ts +0 -30
- package/src/policy/reviewed-artifact.ts +0 -97
- package/src/policy/reviewed-command-safety.ts +0 -150
- package/src/policy/reviewed-command-shape.ts +0 -49
- package/src/policy/reviewed-repository-contract.ts +0 -138
- package/src/policy/reviewed-repository-cwd.ts +0 -102
|
@@ -18,6 +18,7 @@ export const childPolicyStep = (policy: ChildStepPolicy): WorkflowStep => ({
|
|
|
18
18
|
permissions: policy.permissions,
|
|
19
19
|
requires: { tools: [], extensions: [], skills: [] },
|
|
20
20
|
transitions: {},
|
|
21
|
+
...(policy.workspace ? { workspace: policy.workspace } : {}),
|
|
21
22
|
});
|
|
22
23
|
|
|
23
24
|
/**
|
|
@@ -25,7 +26,6 @@ export const childPolicyStep = (policy: ChildStepPolicy): WorkflowStep => ({
|
|
|
25
26
|
* policy.
|
|
26
27
|
*/
|
|
27
28
|
export const childSystemPrompt = (policy: ChildStepPolicy): string => {
|
|
28
|
-
const hasPauseOutcome = policy.pauseOutcomes.length > 0;
|
|
29
29
|
return [
|
|
30
30
|
'# Pi Workflows delegated step',
|
|
31
31
|
'',
|
|
@@ -36,7 +36,7 @@ export const childSystemPrompt = (policy: ChildStepPolicy): string => {
|
|
|
36
36
|
'The parent workflow harness owns orchestration and state transitions.',
|
|
37
37
|
'Perform only this delegated step. Its child-side tool policy is enforced.',
|
|
38
38
|
'When finished, call `structured_output` exactly once and as the only tool call in that message.',
|
|
39
|
-
'Pass the workflow result as its `value`: outcome, summary, and
|
|
39
|
+
'Pass the workflow result as its `value`: outcome, summary, optional artifact, and workspace only when required below.',
|
|
40
40
|
`Valid outcomes: ${policy.outcomes.join(', ')}`,
|
|
41
41
|
`Pause outcomes: ${policy.pauseOutcomes.join(', ') || '(none)'}`,
|
|
42
42
|
`Summary limit: ${policy.summaryMaxChars} characters`,
|
|
@@ -45,39 +45,14 @@ export const childSystemPrompt = (policy: ChildStepPolicy): string => {
|
|
|
45
45
|
`Outcome "${policy.gateSubmitOutcome}" requires the complete gate artifact.`,
|
|
46
46
|
]
|
|
47
47
|
: []),
|
|
48
|
-
...(
|
|
48
|
+
...(policy.workspace
|
|
49
49
|
? [
|
|
50
|
-
`
|
|
50
|
+
`Workspace-binding outcomes: ${policy.workspace.bindOn.join(', ')}`,
|
|
51
|
+
`For those outcomes, include workspace.cwd as an absolute directory under one allowed root relative to the run-start directory: ${policy.workspace.allowedRoots.join(', ')}`,
|
|
52
|
+
'For every other outcome, omit workspace.',
|
|
51
53
|
]
|
|
52
|
-
: [
|
|
53
|
-
'If the workflow definition or environment is wrong, do not fabricate success or call the completion tool; end with a concise declarative error so the parent pauses the step.',
|
|
54
|
-
]),
|
|
54
|
+
: ['This step cannot bind a workspace; omit workspace.']),
|
|
55
55
|
'This is a non-interactive workflow child. Never call contact_supervisor, subagent_supervisor, or intercom.',
|
|
56
|
-
|
|
57
|
-
? [
|
|
58
|
-
'Put every unresolved decision in the gate artifact with evidence, options, a recommendation, and an adopted default; do not ask a terminal question.',
|
|
59
|
-
]
|
|
60
|
-
: hasPauseOutcome
|
|
61
|
-
? [
|
|
62
|
-
'Treat the step instructions and incoming handoff as the final execution contract.',
|
|
63
|
-
'If that contract is missing, stale, or contradictory, finish with a pause outcome and describe the unresolved contract and evidence declaratively in the summary; do not ask a terminal question.',
|
|
64
|
-
]
|
|
65
|
-
: [
|
|
66
|
-
'Treat the step instructions and incoming handoff as the final execution contract.',
|
|
67
|
-
'If that contract is missing, stale, or contradictory, do not fabricate success or call the completion tool; end with a concise declarative error so the parent pauses the step. Do not ask a terminal question.',
|
|
68
|
-
]),
|
|
69
|
-
...(policy.repositoryCwd
|
|
70
|
-
? [
|
|
71
|
-
`Reviewed repository root: ${policy.repositoryCwd}`,
|
|
72
|
-
...(policy.bootstrapCwd
|
|
73
|
-
? [
|
|
74
|
-
`Bootstrap directory: ${policy.bootstrapCwd}`,
|
|
75
|
-
'The reviewed repository root does not exist yet. Run only its exact approved setup command first, then use absolute paths under the reviewed repository root for every edit and write. Never mutate the bootstrap directory.',
|
|
76
|
-
]
|
|
77
|
-
: [
|
|
78
|
-
'Keep every edit and write inside the reviewed repository root.',
|
|
79
|
-
]),
|
|
80
|
-
]
|
|
81
|
-
: []),
|
|
56
|
+
'Follow the supplied step instructions when choosing one valid outcome; outcome names have no built-in domain meaning.',
|
|
82
57
|
].join('\n');
|
|
83
58
|
};
|
|
@@ -13,8 +13,8 @@ import {
|
|
|
13
13
|
} from './child-runtime-completion.ts';
|
|
14
14
|
import { DEFAULT_CHILD_RUNTIME_DEPENDENCIES } from './child-runtime-dependencies.ts';
|
|
15
15
|
import {
|
|
16
|
-
repositoryMutationError,
|
|
17
16
|
verifyChildCapability,
|
|
17
|
+
verifyChildWorkingDirectory,
|
|
18
18
|
writeChildResult,
|
|
19
19
|
} from './child-runtime-files.ts';
|
|
20
20
|
import { childPolicyStep, childSystemPrompt } from './child-runtime-policy.ts';
|
|
@@ -109,6 +109,7 @@ export const registerSubagentChildRuntime = (
|
|
|
109
109
|
'child agent does not match the delegated workflow policy',
|
|
110
110
|
);
|
|
111
111
|
}
|
|
112
|
+
verifyChildWorkingDirectory(extracted.policy, dependencies);
|
|
112
113
|
verifyChildCapability({
|
|
113
114
|
policy: extracted.policy,
|
|
114
115
|
childAgent,
|
|
@@ -221,7 +222,7 @@ export const registerSubagentChildRuntime = (
|
|
|
221
222
|
return {
|
|
222
223
|
block: true,
|
|
223
224
|
reason:
|
|
224
|
-
'workflow children are non-interactive;
|
|
225
|
+
'workflow children are non-interactive; follow the step prompt and use structured_output with one configured valid outcome',
|
|
225
226
|
};
|
|
226
227
|
}
|
|
227
228
|
|
|
@@ -231,7 +232,6 @@ export const registerSubagentChildRuntime = (
|
|
|
231
232
|
input,
|
|
232
233
|
childPolicyStep(activePolicy),
|
|
233
234
|
pi.getAllTools(),
|
|
234
|
-
activePolicy.approvedBashCommands ?? [],
|
|
235
235
|
);
|
|
236
236
|
if (!authorization.allowed) {
|
|
237
237
|
return {
|
|
@@ -246,18 +246,6 @@ export const registerSubagentChildRuntime = (
|
|
|
246
246
|
};
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
const mutationError = repositoryMutationError({
|
|
250
|
-
toolName: event.toolName,
|
|
251
|
-
input,
|
|
252
|
-
policy: activePolicy,
|
|
253
|
-
dependencies,
|
|
254
|
-
});
|
|
255
|
-
if (mutationError) {
|
|
256
|
-
return {
|
|
257
|
-
block: true,
|
|
258
|
-
reason: mutationError,
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
249
|
freezeToolInput(event.input);
|
|
262
250
|
});
|
|
263
251
|
};
|
|
@@ -21,6 +21,7 @@ export const parseDelegatedStepResult = (
|
|
|
21
21
|
...(policy.gateSubmitOutcome
|
|
22
22
|
? { gateSubmitOutcome: policy.gateSubmitOutcome }
|
|
23
23
|
: {}),
|
|
24
|
+
...(policy.workspace ? { workspace: policy.workspace } : {}),
|
|
24
25
|
});
|
|
25
26
|
} catch (error) {
|
|
26
27
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -26,6 +26,11 @@ export const formatToolFailureDiagnostic = (
|
|
|
26
26
|
`${hasSuccessfulOutputCorrelation ? 'Successful tool output' : 'Tool error'}: ${diagnostic.output}`,
|
|
27
27
|
]
|
|
28
28
|
: []),
|
|
29
|
+
...(diagnostic.postCompletionWarning
|
|
30
|
+
? [
|
|
31
|
+
`Post-completion watchdog warning: ${diagnostic.postCompletionWarning}`,
|
|
32
|
+
]
|
|
33
|
+
: []),
|
|
29
34
|
...(diagnostic.correlation === 'latest-before-completion'
|
|
30
35
|
? [
|
|
31
36
|
'Correlation: latest failed tool call before successful structured_output; terminal text did not identify the call',
|
|
@@ -4,6 +4,7 @@ export type ToolFailureDiagnostic = {
|
|
|
4
4
|
readonly tool: string;
|
|
5
5
|
readonly call?: string;
|
|
6
6
|
readonly output?: string;
|
|
7
|
+
readonly postCompletionWarning?: string;
|
|
7
8
|
readonly replaySafe?: true;
|
|
8
9
|
readonly completionAfterFailure?: true;
|
|
9
10
|
readonly completionValue?: Readonly<Record<string, unknown>>;
|
|
@@ -21,7 +22,6 @@ export type DelegationReplayAudit = {
|
|
|
21
22
|
export type DelegationReplayExpectation = {
|
|
22
23
|
readonly task: string;
|
|
23
24
|
readonly bashPermission: BashPermission;
|
|
24
|
-
readonly approvedBashCommands: ReadonlyArray<string>;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
export type SubagentSessionIdentity = {
|
|
@@ -55,6 +55,11 @@ export type RecordedCompletion = {
|
|
|
55
55
|
readonly value: Readonly<Record<string, unknown>>;
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
+
export type RecordedTranscriptWarning = {
|
|
59
|
+
readonly order: number;
|
|
60
|
+
readonly content: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
58
63
|
export type RecordedMessage = {
|
|
59
64
|
readonly order: number;
|
|
60
65
|
readonly value: Readonly<Record<string, unknown>>;
|
|
@@ -65,6 +70,7 @@ export type ParsedFailureTranscript = {
|
|
|
65
70
|
readonly diagnostics: ReadonlyArray<RecordedToolFailure>;
|
|
66
71
|
readonly successfulResults: ReadonlyArray<RecordedToolSuccess>;
|
|
67
72
|
readonly successfulCompletions: ReadonlyArray<RecordedCompletion>;
|
|
73
|
+
readonly transcriptWarnings: ReadonlyArray<RecordedTranscriptWarning>;
|
|
68
74
|
readonly recordedMessages: ReadonlyArray<RecordedMessage>;
|
|
69
75
|
readonly resultCallIds: ReadonlySet<string>;
|
|
70
76
|
readonly hasValidFalsePositiveProof: boolean;
|
|
@@ -11,11 +11,13 @@ export type {
|
|
|
11
11
|
export { parseToolFailureDiagnostic } from './failure-correlation.ts';
|
|
12
12
|
export { parseDelegationReplayAudit } from './replay-audit.ts';
|
|
13
13
|
export {
|
|
14
|
+
auditCompletedDelegationTranscript,
|
|
14
15
|
deriveSubagentSessionRoot,
|
|
15
16
|
readDelegationReplayAudit,
|
|
16
17
|
readToolFailureDiagnostic,
|
|
17
18
|
} from './session-diagnostics.ts';
|
|
18
19
|
export type {
|
|
20
|
+
CompletedDelegationTranscriptAudit,
|
|
19
21
|
SubagentDiagnosticDependencies,
|
|
20
22
|
SubagentDiagnosticFileHandle,
|
|
21
23
|
SubagentDiagnosticFileSnapshot,
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
RecordedCompletion,
|
|
4
4
|
RecordedToolCall,
|
|
5
5
|
RecordedToolFailure,
|
|
6
|
+
RecordedTranscriptWarning,
|
|
6
7
|
ToolFailureDiagnostic,
|
|
7
8
|
} from './diagnostic-types.ts';
|
|
8
9
|
import { parseFailureTranscript } from './failure-transcript.ts';
|
|
@@ -40,6 +41,17 @@ const latestMatching = (
|
|
|
40
41
|
return undefined;
|
|
41
42
|
};
|
|
42
43
|
|
|
44
|
+
const latestWarningAfter = (
|
|
45
|
+
warnings: ReadonlyArray<RecordedTranscriptWarning>,
|
|
46
|
+
order: number,
|
|
47
|
+
): RecordedTranscriptWarning | undefined => {
|
|
48
|
+
for (let index = warnings.length - 1; index >= 0; index -= 1) {
|
|
49
|
+
const warning = warnings[index];
|
|
50
|
+
if (warning && warning.order > order) return warning;
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
};
|
|
54
|
+
|
|
43
55
|
const finalCompletion = (
|
|
44
56
|
completions: ReadonlyArray<RecordedCompletion>,
|
|
45
57
|
latestFailureOrder: number,
|
|
@@ -60,6 +72,7 @@ const publicDiagnostic = (
|
|
|
60
72
|
diagnostics: ReadonlyArray<RecordedToolFailure>,
|
|
61
73
|
recordedCalls: ReadonlyArray<RecordedToolCall>,
|
|
62
74
|
successfulCompletions: ReadonlyArray<RecordedCompletion>,
|
|
75
|
+
postCompletionWarning: RecordedTranscriptWarning | undefined,
|
|
63
76
|
lastInteractionOrder: number,
|
|
64
77
|
allowCompletionProof: boolean,
|
|
65
78
|
correlation?: ToolFailureDiagnostic['correlation'],
|
|
@@ -78,6 +91,9 @@ const publicDiagnostic = (
|
|
|
78
91
|
);
|
|
79
92
|
return {
|
|
80
93
|
...result,
|
|
94
|
+
...(postCompletionWarning
|
|
95
|
+
? { postCompletionWarning: postCompletionWarning.content }
|
|
96
|
+
: {}),
|
|
81
97
|
...(isFailureTranscriptReplaySafe({
|
|
82
98
|
calls: recordedCalls,
|
|
83
99
|
diagnostics,
|
|
@@ -110,17 +126,30 @@ const hiddenFalsePositiveDiagnostic = (
|
|
|
110
126
|
resultCallIds,
|
|
111
127
|
successfulCompletions,
|
|
112
128
|
successfulResults,
|
|
129
|
+
transcriptWarnings,
|
|
113
130
|
} = transcript;
|
|
114
|
-
|
|
131
|
+
const completionOrder = successfulCompletions.at(-1)?.order ?? 0;
|
|
132
|
+
const postCompletionWarning = latestWarningAfter(
|
|
133
|
+
transcriptWarnings,
|
|
134
|
+
completionOrder,
|
|
135
|
+
);
|
|
115
136
|
|
|
116
137
|
const falsePositive = reproduceHiddenBashFalsePositive(
|
|
117
138
|
recordedMessages,
|
|
118
139
|
successfulResults,
|
|
119
140
|
);
|
|
141
|
+
const hasLaterFailure =
|
|
142
|
+
falsePositive !== undefined &&
|
|
143
|
+
diagnostics.some(
|
|
144
|
+
(diagnostic) => diagnostic.order > falsePositive.result.order,
|
|
145
|
+
);
|
|
120
146
|
const completion = falsePositive
|
|
121
147
|
? finalCompletion(
|
|
122
148
|
successfulCompletions,
|
|
123
|
-
|
|
149
|
+
Math.max(
|
|
150
|
+
falsePositive.result.order,
|
|
151
|
+
diagnostics.at(-1)?.order ?? falsePositive.result.order,
|
|
152
|
+
),
|
|
124
153
|
lastInteractionOrder,
|
|
125
154
|
allowCompletionProof,
|
|
126
155
|
)
|
|
@@ -133,6 +162,8 @@ const hiddenFalsePositiveDiagnostic = (
|
|
|
133
162
|
1 ||
|
|
134
163
|
expectedTool?.toLowerCase() !== 'bash' ||
|
|
135
164
|
!falsePositive ||
|
|
165
|
+
hasLaterFailure ||
|
|
166
|
+
postCompletionWarning !== undefined ||
|
|
136
167
|
terminalError !== falsePositive.terminalError ||
|
|
137
168
|
!completion
|
|
138
169
|
) {
|
|
@@ -170,7 +201,13 @@ export const parseToolFailureDiagnostic = (
|
|
|
170
201
|
lastInteractionOrder,
|
|
171
202
|
recordedCalls,
|
|
172
203
|
successfulCompletions,
|
|
204
|
+
transcriptWarnings,
|
|
173
205
|
} = parsed;
|
|
206
|
+
const completionOrder = successfulCompletions.at(-1)?.order ?? 0;
|
|
207
|
+
const postCompletionWarning = latestWarningAfter(
|
|
208
|
+
transcriptWarnings,
|
|
209
|
+
completionOrder,
|
|
210
|
+
);
|
|
174
211
|
const matchesExpectedTool = (
|
|
175
212
|
diagnostic: Pick<RecordedToolFailure, 'tool'>,
|
|
176
213
|
): boolean =>
|
|
@@ -203,6 +240,7 @@ export const parseToolFailureDiagnostic = (
|
|
|
203
240
|
diagnostics,
|
|
204
241
|
recordedCalls,
|
|
205
242
|
successfulCompletions,
|
|
243
|
+
postCompletionWarning,
|
|
206
244
|
lastInteractionOrder,
|
|
207
245
|
allowCompletionProof,
|
|
208
246
|
'latest-before-completion',
|
|
@@ -222,12 +260,24 @@ export const parseToolFailureDiagnostic = (
|
|
|
222
260
|
selected = latestMatching(diagnostics, matchesExpectedTool);
|
|
223
261
|
}
|
|
224
262
|
|
|
263
|
+
if (!selected && postCompletionWarning) {
|
|
264
|
+
selected =
|
|
265
|
+
latestMatching(diagnostics, matchesExpectedTool) ?? diagnostics.at(-1);
|
|
266
|
+
if (!selected) {
|
|
267
|
+
return {
|
|
268
|
+
tool: expectedTool ?? 'subagent',
|
|
269
|
+
postCompletionWarning: postCompletionWarning.content,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
225
274
|
return selected
|
|
226
275
|
? publicDiagnostic(
|
|
227
276
|
selected,
|
|
228
277
|
diagnostics,
|
|
229
278
|
recordedCalls,
|
|
230
279
|
successfulCompletions,
|
|
280
|
+
postCompletionWarning,
|
|
231
281
|
lastInteractionOrder,
|
|
232
282
|
allowCompletionProof,
|
|
233
283
|
)
|
|
@@ -12,8 +12,55 @@ import type {
|
|
|
12
12
|
RecordedToolCall,
|
|
13
13
|
RecordedToolFailure,
|
|
14
14
|
RecordedToolSuccess,
|
|
15
|
+
RecordedTranscriptWarning,
|
|
15
16
|
} from './diagnostic-types.ts';
|
|
16
17
|
|
|
18
|
+
const WATCHDOG_WARNING_TYPE = 'subagent_watchdog_warning';
|
|
19
|
+
const MAX_WARNING_FIELD_CHARS = 600;
|
|
20
|
+
|
|
21
|
+
const boundedWarningField = (value: string): string => {
|
|
22
|
+
const normalized = value.trim().replaceAll(/\s+/g, ' ');
|
|
23
|
+
return normalized.length <= MAX_WARNING_FIELD_CHARS
|
|
24
|
+
? normalized
|
|
25
|
+
: `${normalized.slice(0, MAX_WARNING_FIELD_CHARS - 1)}…`;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const transcriptWarningContent = (
|
|
29
|
+
entry: Readonly<Record<string, unknown>>,
|
|
30
|
+
): string | undefined => {
|
|
31
|
+
if (
|
|
32
|
+
entry.type !== 'custom_message' ||
|
|
33
|
+
entry.customType !== WATCHDOG_WARNING_TYPE
|
|
34
|
+
) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
const details = entry.details;
|
|
38
|
+
if (isDiagnosticRecord(details)) {
|
|
39
|
+
const fields = [
|
|
40
|
+
typeof details.summary === 'string' ? details.summary : undefined,
|
|
41
|
+
typeof details.evidence === 'string' ? details.evidence : undefined,
|
|
42
|
+
typeof details.recommendedAction === 'string'
|
|
43
|
+
? `Recommended action: ${details.recommendedAction}`
|
|
44
|
+
: undefined,
|
|
45
|
+
].filter((field): field is string => Boolean(field?.trim()));
|
|
46
|
+
if (fields.length > 0) return boundedWarningField(fields.join(' '));
|
|
47
|
+
}
|
|
48
|
+
return typeof entry.content === 'string' && entry.content.trim()
|
|
49
|
+
? boundedWarningField(entry.content)
|
|
50
|
+
: 'The child emitted an unresolved watchdog warning after completion.';
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const isBenignTerminalAssistant = (
|
|
54
|
+
message: Readonly<Record<string, unknown>>,
|
|
55
|
+
): boolean =>
|
|
56
|
+
message.stopReason === 'stop' &&
|
|
57
|
+
message.errorMessage === undefined &&
|
|
58
|
+
Array.isArray(message.content) &&
|
|
59
|
+
message.content.length === 1 &&
|
|
60
|
+
isDiagnosticRecord(message.content[0]) &&
|
|
61
|
+
message.content[0].type === 'text' &&
|
|
62
|
+
message.content[0].text === '';
|
|
63
|
+
|
|
17
64
|
/**
|
|
18
65
|
* Parses the tool calls, results, completions, and structural evidence needed
|
|
19
66
|
* to correlate a terminal failure.
|
|
@@ -26,6 +73,7 @@ export const parseFailureTranscript = (
|
|
|
26
73
|
const diagnostics: Array<RecordedToolFailure> = [];
|
|
27
74
|
const successfulResults: Array<RecordedToolSuccess> = [];
|
|
28
75
|
const successfulCompletions: Array<RecordedCompletion> = [];
|
|
76
|
+
const transcriptWarnings: Array<RecordedTranscriptWarning> = [];
|
|
29
77
|
const recordedMessages: Array<RecordedMessage> = [];
|
|
30
78
|
const resultCallIds = new Set<string>();
|
|
31
79
|
let hasValidFalsePositiveProof = true;
|
|
@@ -43,6 +91,14 @@ export const parseFailureTranscript = (
|
|
|
43
91
|
hasValidFalsePositiveProof = false;
|
|
44
92
|
continue;
|
|
45
93
|
}
|
|
94
|
+
if (isDiagnosticRecord(entry)) {
|
|
95
|
+
const warning = transcriptWarningContent(entry);
|
|
96
|
+
if (warning) {
|
|
97
|
+
transcriptWarnings.push({ order, content: warning });
|
|
98
|
+
lastInteractionOrder = order;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
46
102
|
if (!isDiagnosticRecord(entry) || entry.type !== 'message') continue;
|
|
47
103
|
const message = entry.message;
|
|
48
104
|
if (!isDiagnosticRecord(message)) {
|
|
@@ -65,7 +121,9 @@ export const parseFailureTranscript = (
|
|
|
65
121
|
hasValidFalsePositiveProof = false;
|
|
66
122
|
continue;
|
|
67
123
|
}
|
|
68
|
-
|
|
124
|
+
if (!isBenignTerminalAssistant(message)) {
|
|
125
|
+
lastInteractionOrder = order;
|
|
126
|
+
}
|
|
69
127
|
const toolCalls = message.content.filter(
|
|
70
128
|
(item): item is Record<string, unknown> =>
|
|
71
129
|
isDiagnosticRecord(item) &&
|
|
@@ -184,6 +242,7 @@ export const parseFailureTranscript = (
|
|
|
184
242
|
diagnostics,
|
|
185
243
|
successfulResults,
|
|
186
244
|
successfulCompletions,
|
|
245
|
+
transcriptWarnings,
|
|
187
246
|
recordedMessages,
|
|
188
247
|
resultCallIds,
|
|
189
248
|
hasValidFalsePositiveProof,
|
|
@@ -12,25 +12,16 @@ const REPLAY_SAFE_TOOLS: ReadonlySet<string> = new Set([
|
|
|
12
12
|
'read',
|
|
13
13
|
'structured_output',
|
|
14
14
|
]);
|
|
15
|
-
const PRE_EXECUTION_BASH_FAILURES = [
|
|
16
|
-
'command does not match this step',
|
|
17
|
-
'environment assignments are not allowed',
|
|
18
|
-
'not enabled by subagent',
|
|
19
|
-
'shell operators, substitutions, expansions, and comments are not allowed',
|
|
20
|
-
'shell wrapper',
|
|
21
|
-
'substitutions and escapes are not allowed inside double quotes',
|
|
22
|
-
'trailing bash escape is not allowed',
|
|
23
|
-
'unterminated bash quote',
|
|
24
|
-
'unquoted pathname and tilde expansion are not allowed',
|
|
25
|
-
] as const;
|
|
26
15
|
|
|
27
|
-
const isPreExecutionBashFailure = (
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
16
|
+
const isPreExecutionBashFailure = (
|
|
17
|
+
output: string | undefined,
|
|
18
|
+
rejectionReason: string | undefined,
|
|
19
|
+
): boolean =>
|
|
20
|
+
Boolean(
|
|
21
|
+
output &&
|
|
22
|
+
rejectionReason &&
|
|
23
|
+
output.toLowerCase().includes(rejectionReason.toLowerCase()),
|
|
32
24
|
);
|
|
33
|
-
};
|
|
34
25
|
|
|
35
26
|
/**
|
|
36
27
|
* Returns whether replaying a recorded call cannot repeat a mutation.
|
|
@@ -39,29 +30,23 @@ export const isReplaySafeToolCall = ({
|
|
|
39
30
|
call,
|
|
40
31
|
diagnostics,
|
|
41
32
|
bashPermission,
|
|
42
|
-
approvedBashCommands = [],
|
|
43
33
|
}: {
|
|
44
34
|
readonly call: RecordedToolCall;
|
|
45
35
|
readonly diagnostics: ReadonlyArray<RecordedToolFailure>;
|
|
46
36
|
readonly bashPermission?: BashPermission;
|
|
47
|
-
readonly approvedBashCommands?: ReadonlyArray<string>;
|
|
48
37
|
}): boolean => {
|
|
49
38
|
const tool = call.tool.toLowerCase();
|
|
50
39
|
if (REPLAY_SAFE_TOOLS.has(tool)) return true;
|
|
51
40
|
if (tool !== 'bash' || !call.call) return false;
|
|
52
|
-
if (
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
|
-
if (
|
|
56
|
-
!bashPermission ||
|
|
57
|
-
authorizeBash(call.call, bashPermission, approvedBashCommands).allowed
|
|
58
|
-
) {
|
|
41
|
+
if (!bashPermission) {
|
|
59
42
|
return false;
|
|
60
43
|
}
|
|
44
|
+
const authorization = authorizeBash(call.call, bashPermission);
|
|
45
|
+
if (authorization.allowed) return false;
|
|
61
46
|
const failure = diagnostics.find(
|
|
62
47
|
(diagnostic) => diagnostic.callId === call.id,
|
|
63
48
|
);
|
|
64
|
-
return isPreExecutionBashFailure(failure?.output);
|
|
49
|
+
return isPreExecutionBashFailure(failure?.output, authorization.reason);
|
|
65
50
|
};
|
|
66
51
|
|
|
67
52
|
/**
|
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
SubagentSessionIdentity,
|
|
17
17
|
ToolFailureDiagnostic,
|
|
18
18
|
} from './diagnostic-types.ts';
|
|
19
|
+
import { parseFailureTranscript } from './failure-transcript.ts';
|
|
19
20
|
import { parseToolFailureDiagnostic } from './failure-correlation.ts';
|
|
20
21
|
import { parseDelegationReplayAudit } from './replay-audit.ts';
|
|
21
22
|
|
|
@@ -25,6 +26,7 @@ const SESSION_FILE_SUFFIX = '.jsonl';
|
|
|
25
26
|
const MAX_SESSION_TAIL_BYTES = 1024 * 1024;
|
|
26
27
|
|
|
27
28
|
export type SubagentDiagnosticPathInspection = {
|
|
29
|
+
readonly isDirectory: () => boolean;
|
|
28
30
|
readonly isFile: () => boolean;
|
|
29
31
|
readonly isSymbolicLink: () => boolean;
|
|
30
32
|
};
|
|
@@ -60,6 +62,16 @@ export type SubagentDiagnosticDependencies = {
|
|
|
60
62
|
readonly fileSystem: SubagentDiagnosticFileSystem;
|
|
61
63
|
};
|
|
62
64
|
|
|
65
|
+
export type CompletedDelegationTranscriptAudit =
|
|
66
|
+
| {
|
|
67
|
+
readonly verified: true;
|
|
68
|
+
readonly warning?: string;
|
|
69
|
+
}
|
|
70
|
+
| {
|
|
71
|
+
readonly verified: false;
|
|
72
|
+
readonly reason: string;
|
|
73
|
+
};
|
|
74
|
+
|
|
63
75
|
const DEFAULT_DIAGNOSTIC_DEPENDENCIES = {
|
|
64
76
|
fileSystem: {
|
|
65
77
|
inspect: lstat,
|
|
@@ -152,15 +164,41 @@ const readContainedSessionTail = async ({
|
|
|
152
164
|
return undefined;
|
|
153
165
|
}
|
|
154
166
|
|
|
167
|
+
const runDirectory = resolve(trustedRoot, identity.runId);
|
|
168
|
+
const childDirectory = resolve(runDirectory, `run-${identity.childIndex}`);
|
|
155
169
|
const resolvedSessionFile = resolve(sessionFile);
|
|
156
|
-
const inspected = await
|
|
157
|
-
|
|
170
|
+
const [runDirectoryInfo, childDirectoryInfo, inspected] = await Promise.all([
|
|
171
|
+
dependencies.fileSystem.inspect(runDirectory),
|
|
172
|
+
dependencies.fileSystem.inspect(childDirectory),
|
|
173
|
+
dependencies.fileSystem.inspect(resolvedSessionFile),
|
|
174
|
+
]);
|
|
175
|
+
if (
|
|
176
|
+
runDirectoryInfo.isSymbolicLink() ||
|
|
177
|
+
!runDirectoryInfo.isDirectory() ||
|
|
178
|
+
childDirectoryInfo.isSymbolicLink() ||
|
|
179
|
+
!childDirectoryInfo.isDirectory() ||
|
|
180
|
+
inspected.isSymbolicLink() ||
|
|
181
|
+
!inspected.isFile()
|
|
182
|
+
) {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
158
185
|
|
|
159
186
|
const [canonicalRoot, canonicalSessionFile] = await Promise.all([
|
|
160
187
|
dependencies.fileSystem.realPath(trustedRoot),
|
|
161
188
|
dependencies.fileSystem.realPath(resolvedSessionFile),
|
|
162
189
|
]);
|
|
163
|
-
|
|
190
|
+
const canonicalExpectedSessionFile = resolve(
|
|
191
|
+
canonicalRoot,
|
|
192
|
+
identity.runId,
|
|
193
|
+
`run-${identity.childIndex}`,
|
|
194
|
+
SESSION_FILE_NAME,
|
|
195
|
+
);
|
|
196
|
+
if (
|
|
197
|
+
!pathIsWithin(canonicalRoot, canonicalSessionFile) ||
|
|
198
|
+
canonicalSessionFile !== canonicalExpectedSessionFile
|
|
199
|
+
) {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
164
202
|
|
|
165
203
|
const handle =
|
|
166
204
|
await dependencies.fileSystem.openReadOnlyNoFollow(canonicalSessionFile);
|
|
@@ -229,6 +267,67 @@ export const readToolFailureDiagnostic = async (
|
|
|
229
267
|
}
|
|
230
268
|
};
|
|
231
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Proves that a completed child transcript ends at one successful structured
|
|
272
|
+
* result and contains no later watchdog blocker.
|
|
273
|
+
*/
|
|
274
|
+
export const auditCompletedDelegationTranscript = async (
|
|
275
|
+
sessionFile: string | undefined,
|
|
276
|
+
trustedRoot: string | undefined,
|
|
277
|
+
identity: SubagentSessionIdentity | undefined,
|
|
278
|
+
dependencies: SubagentDiagnosticDependencies = DEFAULT_DIAGNOSTIC_DEPENDENCIES,
|
|
279
|
+
): Promise<CompletedDelegationTranscriptAudit> => {
|
|
280
|
+
if (!sessionFile || !trustedRoot || !identity) {
|
|
281
|
+
return {
|
|
282
|
+
verified: false,
|
|
283
|
+
reason: 'completed response has no trusted child transcript identity',
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
const tail = await readContainedSessionTail({
|
|
288
|
+
sessionFile,
|
|
289
|
+
trustedRoot,
|
|
290
|
+
identity,
|
|
291
|
+
dependencies,
|
|
292
|
+
});
|
|
293
|
+
if (!tail) {
|
|
294
|
+
return {
|
|
295
|
+
verified: false,
|
|
296
|
+
reason: 'completed child transcript is missing, unstable, or untrusted',
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
const parsed = parseFailureTranscript(tail.content);
|
|
300
|
+
const completion = parsed.successfulCompletions.at(-1);
|
|
301
|
+
if (!completion) {
|
|
302
|
+
return {
|
|
303
|
+
verified: false,
|
|
304
|
+
reason:
|
|
305
|
+
'completed child transcript does not contain a successful structured_output result',
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
const warning = parsed.transcriptWarnings.find(
|
|
309
|
+
(candidate) => candidate.order > completion.order,
|
|
310
|
+
);
|
|
311
|
+
if (warning) return { verified: true, warning: warning.content };
|
|
312
|
+
if (
|
|
313
|
+
!parsed.hasValidFalsePositiveProof ||
|
|
314
|
+
completion.order !== parsed.lastInteractionOrder
|
|
315
|
+
) {
|
|
316
|
+
return {
|
|
317
|
+
verified: false,
|
|
318
|
+
reason:
|
|
319
|
+
'completed child transcript has malformed or later terminal interactions',
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
return { verified: true };
|
|
323
|
+
} catch {
|
|
324
|
+
return {
|
|
325
|
+
verified: false,
|
|
326
|
+
reason: 'completed child transcript could not be read safely',
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
232
331
|
/**
|
|
233
332
|
* Reads and audits replay safety from a confined child session file.
|
|
234
333
|
*
|