@wichayutdew/pi-workflows 2.5.1 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +34 -1
  2. package/dist/index.js +1315 -2467
  3. package/examples/starter-kit/agents/planner.md +11 -0
  4. package/examples/starter-kit/agents/reviewer.md +10 -0
  5. package/examples/starter-kit/agents/scout.md +10 -0
  6. package/examples/starter-kit/agents/worker.md +11 -0
  7. package/examples/starter-kit/agents/workspace-preparer.md +10 -0
  8. package/examples/starter-kit/investigate.workflow.yaml +84 -0
  9. package/examples/starter-kit/jira.workflow.yaml +75 -0
  10. package/examples/starter-kit/mr-comment.workflow.yaml +86 -93
  11. package/examples/starter-kit/mr-review.workflow.yaml +52 -88
  12. package/examples/starter-kit/settings.yaml +4 -0
  13. package/examples/starter-kit/steps/investigate/investigate.md +40 -0
  14. package/examples/starter-kit/steps/investigate/retrieve.md +28 -0
  15. package/examples/starter-kit/steps/investigate/validate.md +20 -0
  16. package/examples/starter-kit/steps/jira/create.md +25 -0
  17. package/examples/starter-kit/steps/jira/draft.md +18 -0
  18. package/examples/starter-kit/steps/jira/plan.md +30 -0
  19. package/examples/starter-kit/steps/mr-comment/checkout-source.md +14 -0
  20. package/examples/starter-kit/steps/mr-comment/fetch.md +11 -28
  21. package/examples/starter-kit/steps/mr-comment/implement.md +9 -26
  22. package/examples/starter-kit/steps/mr-comment/plan.md +46 -55
  23. package/examples/starter-kit/steps/mr-comment/publish.md +11 -31
  24. package/examples/starter-kit/steps/mr-comment/verify.md +7 -34
  25. package/examples/starter-kit/steps/mr-review/fetch.md +13 -21
  26. package/examples/starter-kit/steps/mr-review/publish-approved.md +18 -0
  27. package/examples/starter-kit/steps/mr-review/review-for-approval.md +53 -0
  28. package/examples/starter-kit/steps/mr-review/verify-published.md +16 -0
  29. package/examples/starter-kit/steps/shared/prepare-workspace.md +11 -89
  30. package/examples/starter-kit/steps/shared/publish-remote.md +16 -0
  31. package/examples/starter-kit/steps/ticket/implement.md +10 -23
  32. package/examples/starter-kit/steps/ticket/plan.md +49 -98
  33. package/examples/starter-kit/steps/ticket/verify.md +14 -65
  34. package/examples/starter-kit/steps/work/implement.md +11 -22
  35. package/examples/starter-kit/steps/work/plan.md +43 -59
  36. package/examples/starter-kit/steps/work/verify.md +14 -25
  37. package/examples/starter-kit/ticket.workflow.yaml +57 -61
  38. package/examples/starter-kit/work.workflow.yaml +49 -57
  39. package/package.json +9 -19
  40. package/schemas/workflow.schema.json +63 -15
  41. package/scripts/patch-herdr-agent-state.mjs +36 -0
  42. package/src/agents/profile.ts +98 -0
  43. package/src/config/ceiling.ts +0 -82
  44. package/src/config/types.ts +15 -53
  45. package/src/config/validation/settings.ts +2 -14
  46. package/src/config/validation/step.ts +129 -8
  47. package/src/config/validation/workflow.ts +1 -10
  48. package/src/engine/run-workflow-validation.ts +0 -3
  49. package/src/engine/state-types.ts +1 -1
  50. package/src/harness/action-context.ts +1 -13
  51. package/src/harness/artifact-contract.ts +46 -0
  52. package/src/harness/delegation-control-actions.ts +4 -63
  53. package/src/harness/delegation-plan.ts +26 -80
  54. package/src/harness/delegation-response-actions.ts +22 -135
  55. package/src/harness/dependencies.ts +1 -12
  56. package/src/harness/gate-submission-action.ts +28 -0
  57. package/src/harness/status-actions.ts +20 -0
  58. package/src/harness/step-execution-actions.ts +2 -9
  59. package/src/harness/types.ts +2 -40
  60. package/src/harness.ts +2 -17
  61. package/src/herdr-workflow-state.ts +65 -0
  62. package/src/index.ts +4 -9
  63. package/src/integrations/subagents/child-policy-validation.ts +6 -7
  64. package/src/integrations/subagents/child-runtime-dependencies.ts +1 -1
  65. package/src/integrations/subagents/child-runtime-policy.ts +1 -7
  66. package/src/integrations/subagents/child-runtime.ts +18 -9
  67. package/src/integrations/subagents/client.ts +240 -98
  68. package/src/integrations/subagents/protocol-events.ts +32 -15
  69. package/src/integrations/subagents/protocol.ts +1 -1
  70. package/src/preflight.ts +0 -8
  71. package/src/prompt/main-workflow-notice.ts +8 -15
  72. package/src/prompt/step-task.ts +7 -6
  73. package/src/workflow-status/format-status.ts +5 -1
  74. package/src/workflow-status/render-step-detail.ts +94 -0
  75. package/src/workflow-status/types.ts +1 -0
  76. package/src/workflow-status/view.ts +49 -14
  77. package/agents/step.md +0 -32
  78. package/examples/mr-comments.workflow.yaml +0 -125
  79. package/examples/prompts/mr-comments/implement.md +0 -17
  80. package/examples/prompts/mr-comments/inspect.md +0 -5
  81. package/examples/prompts/mr-comments/plan.md +0 -54
  82. package/examples/prompts/mr-comments/verify.md +0 -9
  83. package/examples/settings.yaml +0 -27
  84. package/examples/starter-kit/steps/mr-review/publish.md +0 -48
  85. package/examples/starter-kit/steps/mr-review/review.md +0 -76
  86. package/examples/starter-kit/steps/mr-review/verify.md +0 -35
  87. package/src/config/validation/subagent.ts +0 -288
  88. package/src/harness/delegation-failure.ts +0 -248
  89. package/src/harness/delegation-recovery-validation.ts +0 -161
  90. package/src/harness/delegation-retry-policy.ts +0 -120
  91. package/src/integrations/subagents/client-delegation.ts +0 -181
  92. package/src/integrations/subagents/client-messages.ts +0 -66
  93. package/src/integrations/subagents/client-types.ts +0 -36
  94. package/src/integrations/subagents/diagnostic-format.ts +0 -45
  95. package/src/integrations/subagents/diagnostic-text.ts +0 -114
  96. package/src/integrations/subagents/diagnostic-types.ts +0 -83
  97. package/src/integrations/subagents/diagnostics.ts +0 -26
  98. package/src/integrations/subagents/failure-correlation.ts +0 -285
  99. package/src/integrations/subagents/failure-transcript.ts +0 -252
  100. package/src/integrations/subagents/hidden-bash-failure.ts +0 -98
  101. package/src/integrations/subagents/replay-audit.ts +0 -146
  102. package/src/integrations/subagents/replay-safety.ts +0 -67
  103. package/src/integrations/subagents/session-diagnostics.ts +0 -357
@@ -1,285 +0,0 @@
1
- import { comparableDiagnosticFragments } from './diagnostic-text.ts';
2
- import type {
3
- RecordedCompletion,
4
- RecordedToolCall,
5
- RecordedToolFailure,
6
- RecordedTranscriptWarning,
7
- ToolFailureDiagnostic,
8
- } from './diagnostic-types.ts';
9
- import { parseFailureTranscript } from './failure-transcript.ts';
10
- import { reproduceHiddenBashFalsePositive } from './hidden-bash-failure.ts';
11
- import { isFailureTranscriptReplaySafe } from './replay-safety.ts';
12
-
13
- const diagnosticMatchesTerminalError = (
14
- diagnostic: ToolFailureDiagnostic,
15
- terminalError: string,
16
- ): boolean => {
17
- if (!diagnostic.output) return false;
18
- const detail =
19
- terminalError.match(
20
- /\b[a-z][\w-]* failed(?:\s*\([^)]*\))?\s*:\s*([\s\S]+)/i,
21
- )?.[1] ?? terminalError;
22
- const outputFragments = comparableDiagnosticFragments(diagnostic.output);
23
- const errorFragments = comparableDiagnosticFragments(
24
- `${terminalError}\n${detail}`,
25
- );
26
- return outputFragments.some((output) =>
27
- errorFragments.some(
28
- (error) => output.includes(error) || error.includes(output),
29
- ),
30
- );
31
- };
32
-
33
- const latestMatching = (
34
- diagnostics: ReadonlyArray<RecordedToolFailure>,
35
- predicate: (diagnostic: RecordedToolFailure) => boolean,
36
- ): RecordedToolFailure | undefined => {
37
- for (let index = diagnostics.length - 1; index >= 0; index -= 1) {
38
- const diagnostic = diagnostics[index];
39
- if (diagnostic && predicate(diagnostic)) return diagnostic;
40
- }
41
- return undefined;
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
-
55
- const finalCompletion = (
56
- completions: ReadonlyArray<RecordedCompletion>,
57
- latestFailureOrder: number,
58
- lastInteractionOrder: number,
59
- allowCompletionProof: boolean,
60
- ): RecordedCompletion | undefined => {
61
- if (!allowCompletionProof || completions.length !== 1) return undefined;
62
- const completion = completions[0];
63
- return completion &&
64
- completion.order > latestFailureOrder &&
65
- completion.order === lastInteractionOrder
66
- ? completion
67
- : undefined;
68
- };
69
-
70
- const publicDiagnostic = (
71
- diagnostic: RecordedToolFailure,
72
- diagnostics: ReadonlyArray<RecordedToolFailure>,
73
- recordedCalls: ReadonlyArray<RecordedToolCall>,
74
- successfulCompletions: ReadonlyArray<RecordedCompletion>,
75
- postCompletionWarning: RecordedTranscriptWarning | undefined,
76
- lastInteractionOrder: number,
77
- allowCompletionProof: boolean,
78
- correlation?: ToolFailureDiagnostic['correlation'],
79
- ): ToolFailureDiagnostic => {
80
- const result: ToolFailureDiagnostic = {
81
- tool: diagnostic.tool,
82
- ...(diagnostic.call ? { call: diagnostic.call } : {}),
83
- ...(diagnostic.output ? { output: diagnostic.output } : {}),
84
- };
85
- const latestFailureOrder = diagnostics.at(-1)?.order ?? diagnostic.order;
86
- const completion = finalCompletion(
87
- successfulCompletions,
88
- latestFailureOrder,
89
- lastInteractionOrder,
90
- allowCompletionProof,
91
- );
92
- return {
93
- ...result,
94
- ...(postCompletionWarning
95
- ? { postCompletionWarning: postCompletionWarning.content }
96
- : {}),
97
- ...(isFailureTranscriptReplaySafe({
98
- calls: recordedCalls,
99
- diagnostics,
100
- isCompleteTranscript: allowCompletionProof,
101
- })
102
- ? { replaySafe: true as const }
103
- : {}),
104
- ...(completion
105
- ? {
106
- completionAfterFailure: true as const,
107
- completionValue: completion.value,
108
- }
109
- : {}),
110
- ...(correlation ? { correlation } : {}),
111
- };
112
- };
113
-
114
- const hiddenFalsePositiveDiagnostic = (
115
- expectedTool: string | undefined,
116
- terminalError: string,
117
- allowCompletionProof: boolean,
118
- transcript: ReturnType<typeof parseFailureTranscript>,
119
- ): ToolFailureDiagnostic | undefined => {
120
- const {
121
- diagnostics,
122
- hasValidFalsePositiveProof,
123
- lastInteractionOrder,
124
- recordedCalls,
125
- recordedMessages,
126
- resultCallIds,
127
- successfulCompletions,
128
- successfulResults,
129
- transcriptWarnings,
130
- } = transcript;
131
- const completionOrder = successfulCompletions.at(-1)?.order ?? 0;
132
- const postCompletionWarning = latestWarningAfter(
133
- transcriptWarnings,
134
- completionOrder,
135
- );
136
-
137
- const falsePositive = reproduceHiddenBashFalsePositive(
138
- recordedMessages,
139
- successfulResults,
140
- );
141
- const hasLaterFailure =
142
- falsePositive !== undefined &&
143
- diagnostics.some(
144
- (diagnostic) => diagnostic.order > falsePositive.result.order,
145
- );
146
- const completion = falsePositive
147
- ? finalCompletion(
148
- successfulCompletions,
149
- Math.max(
150
- falsePositive.result.order,
151
- diagnostics.at(-1)?.order ?? falsePositive.result.order,
152
- ),
153
- lastInteractionOrder,
154
- allowCompletionProof,
155
- )
156
- : undefined;
157
- if (
158
- !hasValidFalsePositiveProof ||
159
- recordedCalls.length !== resultCallIds.size ||
160
- !recordedCalls.every((call) => resultCallIds.has(call.id)) ||
161
- recordedCalls.filter((call) => call.tool === 'structured_output').length !==
162
- 1 ||
163
- expectedTool?.toLowerCase() !== 'bash' ||
164
- !falsePositive ||
165
- hasLaterFailure ||
166
- postCompletionWarning !== undefined ||
167
- terminalError !== falsePositive.terminalError ||
168
- !completion
169
- ) {
170
- return undefined;
171
- }
172
-
173
- const successfulOutput = falsePositive.result;
174
- return {
175
- tool: successfulOutput.tool,
176
- ...(successfulOutput.call ? { call: successfulOutput.call } : {}),
177
- ...(successfulOutput.output ? { output: successfulOutput.output } : {}),
178
- completionAfterFailure: true,
179
- completionValue: completion.value,
180
- transcriptToolCount: recordedCalls.length,
181
- transcriptTurnCount: recordedMessages.filter(
182
- ({ value }) => value.role === 'assistant',
183
- ).length,
184
- correlation: 'successful-output-before-completion',
185
- };
186
- };
187
-
188
- /**
189
- * Correlates a terminal tool failure with trusted evidence from a child
190
- * transcript.
191
- */
192
- export const parseToolFailureDiagnostic = (
193
- transcript: string,
194
- expectedTool?: string,
195
- terminalError?: string,
196
- allowCompletionProof = true,
197
- ): ToolFailureDiagnostic | undefined => {
198
- const parsed = parseFailureTranscript(transcript);
199
- const {
200
- diagnostics,
201
- lastInteractionOrder,
202
- recordedCalls,
203
- successfulCompletions,
204
- transcriptWarnings,
205
- } = parsed;
206
- const completionOrder = successfulCompletions.at(-1)?.order ?? 0;
207
- const postCompletionWarning = latestWarningAfter(
208
- transcriptWarnings,
209
- completionOrder,
210
- );
211
- const matchesExpectedTool = (
212
- diagnostic: Pick<RecordedToolFailure, 'tool'>,
213
- ): boolean =>
214
- expectedTool === undefined ||
215
- diagnostic.tool.toLowerCase() === expectedTool.toLowerCase();
216
-
217
- let selected: RecordedToolFailure | undefined;
218
- if (terminalError) {
219
- selected = latestMatching(
220
- diagnostics,
221
- (diagnostic) =>
222
- matchesExpectedTool(diagnostic) &&
223
- diagnosticMatchesTerminalError(diagnostic, terminalError),
224
- );
225
- if (!selected) {
226
- const fallback = latestMatching(diagnostics, matchesExpectedTool);
227
- const latestFailureOrder = diagnostics.at(-1)?.order;
228
- if (
229
- fallback &&
230
- latestFailureOrder !== undefined &&
231
- finalCompletion(
232
- successfulCompletions,
233
- latestFailureOrder,
234
- lastInteractionOrder,
235
- allowCompletionProof,
236
- )
237
- ) {
238
- return publicDiagnostic(
239
- fallback,
240
- diagnostics,
241
- recordedCalls,
242
- successfulCompletions,
243
- postCompletionWarning,
244
- lastInteractionOrder,
245
- allowCompletionProof,
246
- 'latest-before-completion',
247
- );
248
- }
249
- }
250
- if (!selected) {
251
- const falsePositive = hiddenFalsePositiveDiagnostic(
252
- expectedTool,
253
- terminalError,
254
- allowCompletionProof,
255
- parsed,
256
- );
257
- if (falsePositive) return falsePositive;
258
- }
259
- } else {
260
- selected = latestMatching(diagnostics, matchesExpectedTool);
261
- }
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
-
274
- return selected
275
- ? publicDiagnostic(
276
- selected,
277
- diagnostics,
278
- recordedCalls,
279
- successfulCompletions,
280
- postCompletionWarning,
281
- lastInteractionOrder,
282
- allowCompletionProof,
283
- )
284
- : undefined;
285
- };
@@ -1,252 +0,0 @@
1
- import {
2
- diagnosticTextContent,
3
- diagnosticToolCallText,
4
- firstDiagnosticTextContent,
5
- isDiagnosticRecord,
6
- structuredCompletionValue,
7
- } from './diagnostic-text.ts';
8
- import type {
9
- ParsedFailureTranscript,
10
- RecordedCompletion,
11
- RecordedMessage,
12
- RecordedToolCall,
13
- RecordedToolFailure,
14
- RecordedToolSuccess,
15
- RecordedTranscriptWarning,
16
- } from './diagnostic-types.ts';
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
-
64
- /**
65
- * Parses the tool calls, results, completions, and structural evidence needed
66
- * to correlate a terminal failure.
67
- */
68
- export const parseFailureTranscript = (
69
- transcript: string,
70
- ): ParsedFailureTranscript => {
71
- const calls = new Map<string, RecordedToolCall>();
72
- const recordedCalls: Array<RecordedToolCall> = [];
73
- const diagnostics: Array<RecordedToolFailure> = [];
74
- const successfulResults: Array<RecordedToolSuccess> = [];
75
- const successfulCompletions: Array<RecordedCompletion> = [];
76
- const transcriptWarnings: Array<RecordedTranscriptWarning> = [];
77
- const recordedMessages: Array<RecordedMessage> = [];
78
- const resultCallIds = new Set<string>();
79
- let hasValidFalsePositiveProof = true;
80
- let lastInteractionOrder = 0;
81
- let order = 0;
82
-
83
- for (const line of transcript.split('\n')) {
84
- order += 1;
85
- if (!line.trim()) continue;
86
-
87
- let entry: unknown;
88
- try {
89
- entry = JSON.parse(line);
90
- } catch {
91
- hasValidFalsePositiveProof = false;
92
- continue;
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
- }
102
- if (!isDiagnosticRecord(entry) || entry.type !== 'message') continue;
103
- const message = entry.message;
104
- if (!isDiagnosticRecord(message)) {
105
- hasValidFalsePositiveProof = false;
106
- continue;
107
- }
108
- recordedMessages.push({ order, value: message });
109
- if (
110
- message.role === 'assistant' &&
111
- ((typeof message.errorMessage === 'string' &&
112
- message.errorMessage.trim().length > 0) ||
113
- message.stopReason === 'error' ||
114
- message.stopReason === 'aborted')
115
- ) {
116
- hasValidFalsePositiveProof = false;
117
- }
118
-
119
- if (message.role === 'assistant') {
120
- if (!Array.isArray(message.content)) {
121
- hasValidFalsePositiveProof = false;
122
- continue;
123
- }
124
- if (!isBenignTerminalAssistant(message)) {
125
- lastInteractionOrder = order;
126
- }
127
- const toolCalls = message.content.filter(
128
- (item): item is Record<string, unknown> =>
129
- isDiagnosticRecord(item) &&
130
- item.type === 'toolCall' &&
131
- typeof item.id === 'string' &&
132
- typeof item.name === 'string',
133
- );
134
- for (const item of message.content) {
135
- if (
136
- !isDiagnosticRecord(item) ||
137
- item.type !== 'toolCall' ||
138
- typeof item.id !== 'string' ||
139
- typeof item.name !== 'string'
140
- ) {
141
- if (isDiagnosticRecord(item) && item.type === 'toolCall') {
142
- hasValidFalsePositiveProof = false;
143
- }
144
- continue;
145
- }
146
- if (calls.has(item.id)) hasValidFalsePositiveProof = false;
147
-
148
- const call = diagnosticToolCallText({
149
- tool: item.name,
150
- argumentsValue: item.arguments,
151
- });
152
- const isExclusiveCompletion =
153
- toolCalls.length === 1 &&
154
- message.content.every(
155
- (contentItem) =>
156
- isDiagnosticRecord(contentItem) &&
157
- (contentItem.type === 'thinking' ||
158
- contentItem.type === 'text' ||
159
- contentItem.type === 'toolCall'),
160
- );
161
- const completionValue =
162
- item.name === 'structured_output' && isExclusiveCompletion
163
- ? structuredCompletionValue(item.arguments)
164
- : undefined;
165
- const recordedCall: RecordedToolCall = {
166
- id: item.id,
167
- order,
168
- tool: item.name,
169
- ...(call ? { call } : {}),
170
- ...(completionValue ? { completionValue } : {}),
171
- };
172
- calls.set(item.id, recordedCall);
173
- recordedCalls.push(recordedCall);
174
- }
175
- continue;
176
- }
177
-
178
- if (message.role !== 'toolResult' || typeof message.toolName !== 'string') {
179
- if (message.role === 'toolResult') hasValidFalsePositiveProof = false;
180
- continue;
181
- }
182
- lastInteractionOrder = order;
183
- if (
184
- typeof message.toolCallId !== 'string' ||
185
- typeof message.isError !== 'boolean' ||
186
- !Array.isArray(message.content) ||
187
- resultCallIds.has(message.toolCallId)
188
- ) {
189
- hasValidFalsePositiveProof = false;
190
- }
191
- if (typeof message.toolCallId === 'string') {
192
- resultCallIds.add(message.toolCallId);
193
- }
194
- const recorded =
195
- typeof message.toolCallId === 'string'
196
- ? calls.get(message.toolCallId)
197
- : undefined;
198
- const doesCallMatchResult = recorded?.tool === message.toolName;
199
- if (!doesCallMatchResult) hasValidFalsePositiveProof = false;
200
-
201
- if (
202
- message.toolName === 'structured_output' &&
203
- message.isError === false &&
204
- doesCallMatchResult &&
205
- recorded.completionValue
206
- ) {
207
- successfulCompletions.push({
208
- order,
209
- value: recorded.completionValue,
210
- });
211
- }
212
- if (
213
- message.isError === false &&
214
- message.toolName !== 'structured_output' &&
215
- doesCallMatchResult
216
- ) {
217
- const output = diagnosticTextContent(message.content);
218
- const detectorOutput = firstDiagnosticTextContent(message.content);
219
- successfulResults.push({
220
- order,
221
- tool: message.toolName,
222
- ...(recorded.call ? { call: recorded.call } : {}),
223
- ...(output ? { output } : {}),
224
- ...(detectorOutput !== undefined ? { detectorOutput } : {}),
225
- });
226
- }
227
- if (message.isError !== true) continue;
228
-
229
- const output = diagnosticTextContent(message.content);
230
- diagnostics.push({
231
- tool: message.toolName,
232
- ...(doesCallMatchResult && recorded.call ? { call: recorded.call } : {}),
233
- ...(output ? { output } : {}),
234
- ...(doesCallMatchResult && typeof message.toolCallId === 'string'
235
- ? { callId: message.toolCallId }
236
- : {}),
237
- order,
238
- });
239
- }
240
-
241
- return {
242
- recordedCalls,
243
- diagnostics,
244
- successfulResults,
245
- successfulCompletions,
246
- transcriptWarnings,
247
- recordedMessages,
248
- resultCallIds,
249
- hasValidFalsePositiveProof,
250
- lastInteractionOrder,
251
- };
252
- };
@@ -1,98 +0,0 @@
1
- import {
2
- firstDiagnosticTextContent,
3
- isDiagnosticRecord,
4
- } from './diagnostic-text.ts';
5
- import type {
6
- RecordedMessage,
7
- RecordedToolSuccess,
8
- } from './diagnostic-types.ts';
9
-
10
- const HIDDEN_BASH_FATAL_PATTERNS = [
11
- /command not found/i,
12
- /permission denied/i,
13
- /no such file or directory/i,
14
- /segmentation fault/i,
15
- /killed|terminated/i,
16
- /out of memory/i,
17
- /connection refused/i,
18
- /timeout/i,
19
- ] as const;
20
- const HIDDEN_BASH_EXIT_PATTERN =
21
- /exit(?:ed)?\s*(?:with\s*)?(?:code|status)?\s*[:\s]?\s*(\d+)/i;
22
-
23
- export type HiddenBashFalsePositive = {
24
- readonly result: RecordedToolSuccess;
25
- readonly terminalError: string;
26
- };
27
-
28
- const lastAssistantTextIndex = (
29
- messages: ReadonlyArray<RecordedMessage>,
30
- ): number => {
31
- for (let index = messages.length - 1; index >= 0; index -= 1) {
32
- const message = messages[index]?.value;
33
- const hasAssistantText =
34
- message?.role === 'assistant' &&
35
- Array.isArray(message.content) &&
36
- message.content.some(
37
- (item) =>
38
- isDiagnosticRecord(item) &&
39
- item.type === 'text' &&
40
- typeof item.text === 'string' &&
41
- item.text.trim().length > 0,
42
- );
43
- if (hasAssistantText) return index;
44
- }
45
- return -1;
46
- };
47
-
48
- const hiddenExitCode = (output: string): number | undefined => {
49
- const exitCodeText = output.match(HIDDEN_BASH_EXIT_PATTERN)?.[1];
50
- const exitCode =
51
- exitCodeText === undefined ? undefined : Number.parseInt(exitCodeText, 10);
52
- if (exitCode !== undefined && exitCode !== 0) return exitCode;
53
- return HIDDEN_BASH_FATAL_PATTERNS.some((pattern) => pattern.test(output))
54
- ? 1
55
- : undefined;
56
- };
57
-
58
- /**
59
- * Reproduces pi-subagents' hidden Bash failure detection from successful tool
60
- * output.
61
- */
62
- export const reproduceHiddenBashFalsePositive = (
63
- messages: ReadonlyArray<RecordedMessage>,
64
- successfulResults: ReadonlyArray<RecordedToolSuccess>,
65
- ): HiddenBashFalsePositive | undefined => {
66
- const assistantTextIndex = lastAssistantTextIndex(messages);
67
- const scanStart = assistantTextIndex >= 0 ? assistantTextIndex + 1 : 0;
68
-
69
- for (let index = messages.length - 1; index >= scanStart; index -= 1) {
70
- const recordedMessage = messages[index];
71
- const message = recordedMessage?.value;
72
- if (
73
- !recordedMessage ||
74
- message?.role !== 'toolResult' ||
75
- message.toolName !== 'bash' ||
76
- message.isError !== false
77
- ) {
78
- continue;
79
- }
80
- const output = firstDiagnosticTextContent(message.content);
81
- if (output === undefined) continue;
82
-
83
- const detectedExitCode = hiddenExitCode(output);
84
- if (detectedExitCode === undefined) continue;
85
- const result = successfulResults.find(
86
- (candidate) =>
87
- candidate.order === recordedMessage.order &&
88
- candidate.tool === 'bash' &&
89
- candidate.detectorOutput === output,
90
- );
91
- if (!result) return undefined;
92
- return {
93
- result,
94
- terminalError: `bash failed (exit ${detectedExitCode}): ${output.slice(0, 200)}`,
95
- };
96
- }
97
- return undefined;
98
- };