@wichayutdew/pi-workflows 2.5.0 → 2.6.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 +4 -1
- package/dist/index.js +1133 -2409
- package/examples/starter-kit/agents/planner.md +7 -0
- package/examples/starter-kit/agents/reviewer.md +6 -0
- package/examples/starter-kit/agents/scout.md +6 -0
- package/examples/starter-kit/agents/worker.md +7 -0
- package/examples/starter-kit/agents/workspace-preparer.md +6 -0
- package/examples/starter-kit/investigate.workflow.yaml +114 -0
- package/examples/starter-kit/mr-comment.workflow.yaml +114 -54
- package/examples/starter-kit/mr-review.workflow.yaml +77 -56
- package/examples/starter-kit/settings.yaml +3 -0
- package/examples/starter-kit/steps/investigate/investigate.md +77 -0
- package/examples/starter-kit/steps/investigate/retrieve.md +63 -0
- package/examples/starter-kit/steps/investigate/validate.md +46 -0
- package/examples/starter-kit/steps/mr-comment/checkout-source.md +67 -0
- package/examples/starter-kit/steps/mr-comment/fetch.md +34 -26
- package/examples/starter-kit/steps/mr-comment/implement.md +34 -27
- package/examples/starter-kit/steps/mr-comment/plan.md +54 -40
- package/examples/starter-kit/steps/mr-comment/publish.md +28 -27
- package/examples/starter-kit/steps/mr-comment/verify.md +42 -35
- package/examples/starter-kit/steps/mr-review/fetch.md +52 -22
- package/examples/starter-kit/steps/mr-review/publish-approved.md +49 -0
- package/examples/starter-kit/steps/mr-review/review-for-approval.md +142 -0
- package/examples/starter-kit/steps/mr-review/verify-published.md +37 -0
- package/examples/starter-kit/steps/shared/prepare-workspace.md +91 -73
- package/examples/starter-kit/steps/shared/publish-remote.md +45 -0
- package/examples/starter-kit/steps/ticket/implement.md +59 -25
- package/examples/starter-kit/steps/ticket/plan.md +145 -72
- package/examples/starter-kit/steps/ticket/verify.md +80 -45
- package/examples/starter-kit/steps/work/implement.md +60 -24
- package/examples/starter-kit/steps/work/plan.md +129 -57
- package/examples/starter-kit/steps/work/verify.md +58 -22
- package/examples/starter-kit/ticket.workflow.yaml +75 -36
- package/examples/starter-kit/work.workflow.yaml +63 -33
- package/package.json +3 -16
- package/schemas/workflow.schema.json +2 -15
- package/src/agents/profile.ts +98 -0
- package/src/config/ceiling.ts +0 -82
- package/src/config/types.ts +6 -53
- package/src/config/validation/settings.ts +2 -14
- package/src/config/validation/step.ts +12 -8
- package/src/config/validation/workflow.ts +1 -10
- package/src/engine/run-workflow-validation.ts +0 -3
- package/src/engine/state-types.ts +1 -1
- package/src/harness/action-context.ts +1 -13
- package/src/harness/delegation-control-actions.ts +4 -63
- package/src/harness/delegation-plan.ts +26 -80
- package/src/harness/delegation-response-actions.ts +22 -135
- package/src/harness/dependencies.ts +1 -12
- package/src/harness/status-actions.ts +1 -0
- package/src/harness/step-execution-actions.ts +2 -9
- package/src/harness/types.ts +2 -40
- package/src/harness.ts +2 -17
- package/src/index.ts +4 -9
- package/src/integrations/subagents/child-policy-validation.ts +6 -7
- package/src/integrations/subagents/child-runtime-dependencies.ts +1 -1
- package/src/integrations/subagents/child-runtime-policy.ts +1 -7
- package/src/integrations/subagents/child-runtime.ts +18 -9
- package/src/integrations/subagents/client.ts +240 -98
- package/src/integrations/subagents/protocol-events.ts +32 -15
- package/src/integrations/subagents/protocol.ts +1 -1
- package/src/preflight.ts +0 -8
- package/src/prompt/main-workflow-notice.ts +8 -15
- package/src/prompt/step-task.ts +7 -6
- package/src/workflow-status/format-status.ts +5 -1
- package/src/workflow-status/render-step-detail.ts +94 -0
- package/src/workflow-status/types.ts +1 -0
- package/src/workflow-status/view.ts +49 -14
- package/agents/step.md +0 -32
- package/examples/mr-comments.workflow.yaml +0 -125
- package/examples/prompts/mr-comments/implement.md +0 -17
- package/examples/prompts/mr-comments/inspect.md +0 -5
- package/examples/prompts/mr-comments/plan.md +0 -54
- package/examples/prompts/mr-comments/verify.md +0 -9
- package/examples/settings.yaml +0 -27
- package/examples/starter-kit/steps/mr-review/publish.md +0 -48
- package/examples/starter-kit/steps/mr-review/review.md +0 -76
- package/examples/starter-kit/steps/mr-review/verify.md +0 -35
- package/src/config/validation/subagent.ts +0 -288
- package/src/harness/delegation-failure.ts +0 -248
- package/src/harness/delegation-recovery-validation.ts +0 -161
- package/src/harness/delegation-retry-policy.ts +0 -120
- package/src/integrations/subagents/client-delegation.ts +0 -181
- package/src/integrations/subagents/client-messages.ts +0 -66
- package/src/integrations/subagents/client-types.ts +0 -36
- package/src/integrations/subagents/diagnostic-format.ts +0 -45
- package/src/integrations/subagents/diagnostic-text.ts +0 -114
- package/src/integrations/subagents/diagnostic-types.ts +0 -83
- package/src/integrations/subagents/diagnostics.ts +0 -26
- package/src/integrations/subagents/failure-correlation.ts +0 -285
- package/src/integrations/subagents/failure-transcript.ts +0 -251
- package/src/integrations/subagents/hidden-bash-failure.ts +0 -98
- package/src/integrations/subagents/replay-audit.ts +0 -146
- package/src/integrations/subagents/replay-safety.ts +0 -67
- package/src/integrations/subagents/session-diagnostics.ts +0 -357
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
diagnosticTextContent,
|
|
3
|
-
diagnosticToolCallText,
|
|
4
|
-
isDiagnosticRecord,
|
|
5
|
-
} from './diagnostic-text.ts';
|
|
6
|
-
import type {
|
|
7
|
-
DelegationReplayAudit,
|
|
8
|
-
DelegationReplayExpectation,
|
|
9
|
-
RecordedToolCall,
|
|
10
|
-
RecordedToolFailure,
|
|
11
|
-
} from './diagnostic-types.ts';
|
|
12
|
-
import { isReplaySafeToolCall } from './replay-safety.ts';
|
|
13
|
-
|
|
14
|
-
const initialDelegationTask = (transcript: string): string | undefined => {
|
|
15
|
-
for (const line of transcript.split('\n')) {
|
|
16
|
-
if (!line.trim()) continue;
|
|
17
|
-
let entry: unknown;
|
|
18
|
-
try {
|
|
19
|
-
entry = JSON.parse(line);
|
|
20
|
-
} catch {
|
|
21
|
-
continue;
|
|
22
|
-
}
|
|
23
|
-
if (!isDiagnosticRecord(entry) || entry.type !== 'message') continue;
|
|
24
|
-
const message = entry.message;
|
|
25
|
-
if (!isDiagnosticRecord(message) || message.role !== 'user') continue;
|
|
26
|
-
if (!Array.isArray(message.content)) return undefined;
|
|
27
|
-
const textParts = message.content.flatMap((item) =>
|
|
28
|
-
isDiagnosticRecord(item) &&
|
|
29
|
-
item.type === 'text' &&
|
|
30
|
-
typeof item.text === 'string'
|
|
31
|
-
? [item.text]
|
|
32
|
-
: [],
|
|
33
|
-
);
|
|
34
|
-
return textParts.length === 1 ? textParts[0] : undefined;
|
|
35
|
-
}
|
|
36
|
-
return undefined;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Audits whether a complete transcript can be safely replayed under the
|
|
41
|
-
* expected delegated policy.
|
|
42
|
-
*/
|
|
43
|
-
export const parseDelegationReplayAudit = (
|
|
44
|
-
transcript: string,
|
|
45
|
-
expectation: DelegationReplayExpectation,
|
|
46
|
-
isCompleteTranscript = true,
|
|
47
|
-
): DelegationReplayAudit => {
|
|
48
|
-
const calls = new Map<string, RecordedToolCall>();
|
|
49
|
-
const recordedCalls: Array<RecordedToolCall> = [];
|
|
50
|
-
const diagnostics: Array<RecordedToolFailure> = [];
|
|
51
|
-
const resultCallIds = new Set<string>();
|
|
52
|
-
let isStructurallyValid = true;
|
|
53
|
-
let order = 0;
|
|
54
|
-
|
|
55
|
-
for (const line of transcript.split('\n')) {
|
|
56
|
-
order += 1;
|
|
57
|
-
if (!line.trim()) continue;
|
|
58
|
-
let entry: unknown;
|
|
59
|
-
try {
|
|
60
|
-
entry = JSON.parse(line);
|
|
61
|
-
} catch {
|
|
62
|
-
isStructurallyValid = false;
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
if (!isDiagnosticRecord(entry) || entry.type !== 'message') continue;
|
|
66
|
-
const message = entry.message;
|
|
67
|
-
if (!isDiagnosticRecord(message)) {
|
|
68
|
-
isStructurallyValid = false;
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (message.role === 'assistant') {
|
|
73
|
-
if (!Array.isArray(message.content)) {
|
|
74
|
-
isStructurallyValid = false;
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
for (const item of message.content) {
|
|
78
|
-
if (!isDiagnosticRecord(item) || item.type !== 'toolCall') continue;
|
|
79
|
-
if (
|
|
80
|
-
typeof item.id !== 'string' ||
|
|
81
|
-
typeof item.name !== 'string' ||
|
|
82
|
-
calls.has(item.id)
|
|
83
|
-
) {
|
|
84
|
-
isStructurallyValid = false;
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
const call = diagnosticToolCallText({
|
|
88
|
-
tool: item.name,
|
|
89
|
-
argumentsValue: item.arguments,
|
|
90
|
-
});
|
|
91
|
-
const recordedCall: RecordedToolCall = {
|
|
92
|
-
id: item.id,
|
|
93
|
-
order,
|
|
94
|
-
tool: item.name,
|
|
95
|
-
...(call ? { call } : {}),
|
|
96
|
-
};
|
|
97
|
-
calls.set(item.id, recordedCall);
|
|
98
|
-
recordedCalls.push(recordedCall);
|
|
99
|
-
}
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (message.role !== 'toolResult') continue;
|
|
104
|
-
if (
|
|
105
|
-
typeof message.toolCallId !== 'string' ||
|
|
106
|
-
typeof message.toolName !== 'string' ||
|
|
107
|
-
typeof message.isError !== 'boolean' ||
|
|
108
|
-
!Array.isArray(message.content) ||
|
|
109
|
-
resultCallIds.has(message.toolCallId)
|
|
110
|
-
) {
|
|
111
|
-
isStructurallyValid = false;
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
resultCallIds.add(message.toolCallId);
|
|
115
|
-
const recorded = calls.get(message.toolCallId);
|
|
116
|
-
if (recorded?.tool !== message.toolName) {
|
|
117
|
-
isStructurallyValid = false;
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
if (message.isError) {
|
|
121
|
-
const output = diagnosticTextContent(message.content);
|
|
122
|
-
diagnostics.push({
|
|
123
|
-
tool: message.toolName,
|
|
124
|
-
callId: message.toolCallId,
|
|
125
|
-
order,
|
|
126
|
-
...(recorded.call ? { call: recorded.call } : {}),
|
|
127
|
-
...(output ? { output } : {}),
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
replaySafe:
|
|
134
|
-
isCompleteTranscript &&
|
|
135
|
-
isStructurallyValid &&
|
|
136
|
-
initialDelegationTask(transcript) === expectation.task &&
|
|
137
|
-
recordedCalls.every((call) =>
|
|
138
|
-
isReplaySafeToolCall({
|
|
139
|
-
call,
|
|
140
|
-
diagnostics,
|
|
141
|
-
bashPermission: expectation.bashPermission,
|
|
142
|
-
}),
|
|
143
|
-
),
|
|
144
|
-
toolCount: recordedCalls.length,
|
|
145
|
-
};
|
|
146
|
-
};
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import type { BashPermission } from '../../config/types.ts';
|
|
2
|
-
import { authorizeBash } from '../../policy/bash.ts';
|
|
3
|
-
import type {
|
|
4
|
-
RecordedToolCall,
|
|
5
|
-
RecordedToolFailure,
|
|
6
|
-
} from './diagnostic-types.ts';
|
|
7
|
-
|
|
8
|
-
const REPLAY_SAFE_TOOLS: ReadonlySet<string> = new Set([
|
|
9
|
-
'find',
|
|
10
|
-
'grep',
|
|
11
|
-
'ls',
|
|
12
|
-
'read',
|
|
13
|
-
'structured_output',
|
|
14
|
-
]);
|
|
15
|
-
|
|
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()),
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Returns whether replaying a recorded call cannot repeat a mutation.
|
|
28
|
-
*/
|
|
29
|
-
export const isReplaySafeToolCall = ({
|
|
30
|
-
call,
|
|
31
|
-
diagnostics,
|
|
32
|
-
bashPermission,
|
|
33
|
-
}: {
|
|
34
|
-
readonly call: RecordedToolCall;
|
|
35
|
-
readonly diagnostics: ReadonlyArray<RecordedToolFailure>;
|
|
36
|
-
readonly bashPermission?: BashPermission;
|
|
37
|
-
}): boolean => {
|
|
38
|
-
const tool = call.tool.toLowerCase();
|
|
39
|
-
if (REPLAY_SAFE_TOOLS.has(tool)) return true;
|
|
40
|
-
if (tool !== 'bash' || !call.call) return false;
|
|
41
|
-
if (!bashPermission) {
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
const authorization = authorizeBash(call.call, bashPermission);
|
|
45
|
-
if (authorization.allowed) return false;
|
|
46
|
-
const failure = diagnostics.find(
|
|
47
|
-
(diagnostic) => diagnostic.callId === call.id,
|
|
48
|
-
);
|
|
49
|
-
return isPreExecutionBashFailure(failure?.output, authorization.reason);
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Returns whether a complete failure transcript contains only replay-safe
|
|
54
|
-
* calls.
|
|
55
|
-
*/
|
|
56
|
-
export const isFailureTranscriptReplaySafe = ({
|
|
57
|
-
calls,
|
|
58
|
-
diagnostics,
|
|
59
|
-
isCompleteTranscript,
|
|
60
|
-
}: {
|
|
61
|
-
readonly calls: ReadonlyArray<RecordedToolCall>;
|
|
62
|
-
readonly diagnostics: ReadonlyArray<RecordedToolFailure>;
|
|
63
|
-
readonly isCompleteTranscript: boolean;
|
|
64
|
-
}): boolean =>
|
|
65
|
-
isCompleteTranscript &&
|
|
66
|
-
calls.length > 0 &&
|
|
67
|
-
calls.every((call) => isReplaySafeToolCall({ call, diagnostics }));
|
|
@@ -1,357 +0,0 @@
|
|
|
1
|
-
import { constants } from 'node:fs';
|
|
2
|
-
import { lstat, open, realpath } from 'node:fs/promises';
|
|
3
|
-
import {
|
|
4
|
-
basename,
|
|
5
|
-
dirname,
|
|
6
|
-
isAbsolute,
|
|
7
|
-
join,
|
|
8
|
-
relative,
|
|
9
|
-
resolve,
|
|
10
|
-
sep,
|
|
11
|
-
} from 'node:path';
|
|
12
|
-
import type {
|
|
13
|
-
DelegationReplayAudit,
|
|
14
|
-
DelegationReplayExpectation,
|
|
15
|
-
SessionTail,
|
|
16
|
-
SubagentSessionIdentity,
|
|
17
|
-
ToolFailureDiagnostic,
|
|
18
|
-
} from './diagnostic-types.ts';
|
|
19
|
-
import { parseFailureTranscript } from './failure-transcript.ts';
|
|
20
|
-
import { parseToolFailureDiagnostic } from './failure-correlation.ts';
|
|
21
|
-
import { parseDelegationReplayAudit } from './replay-audit.ts';
|
|
22
|
-
|
|
23
|
-
const SESSION_FILE_NAME = 'session.jsonl';
|
|
24
|
-
const SESSION_RUN_DIRECTORY = /^run-\d+$/;
|
|
25
|
-
const SESSION_FILE_SUFFIX = '.jsonl';
|
|
26
|
-
const MAX_SESSION_TAIL_BYTES = 1024 * 1024;
|
|
27
|
-
|
|
28
|
-
export type SubagentDiagnosticPathInspection = {
|
|
29
|
-
readonly isDirectory: () => boolean;
|
|
30
|
-
readonly isFile: () => boolean;
|
|
31
|
-
readonly isSymbolicLink: () => boolean;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export type SubagentDiagnosticFileSnapshot =
|
|
35
|
-
SubagentDiagnosticPathInspection & {
|
|
36
|
-
readonly dev: number | bigint;
|
|
37
|
-
readonly ino: number | bigint;
|
|
38
|
-
readonly size: number;
|
|
39
|
-
readonly mtimeMs: number;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export type SubagentDiagnosticFileHandle = {
|
|
43
|
-
readonly stat: () => Promise<SubagentDiagnosticFileSnapshot>;
|
|
44
|
-
readonly read: (
|
|
45
|
-
buffer: Uint8Array,
|
|
46
|
-
offset: number,
|
|
47
|
-
length: number,
|
|
48
|
-
position: number,
|
|
49
|
-
) => Promise<{ readonly bytesRead: number }>;
|
|
50
|
-
readonly close: () => Promise<void>;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
export type SubagentDiagnosticFileSystem = {
|
|
54
|
-
readonly inspect: (path: string) => Promise<SubagentDiagnosticPathInspection>;
|
|
55
|
-
readonly realPath: (path: string) => Promise<string>;
|
|
56
|
-
readonly openReadOnlyNoFollow: (
|
|
57
|
-
path: string,
|
|
58
|
-
) => Promise<SubagentDiagnosticFileHandle>;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
export type SubagentDiagnosticDependencies = {
|
|
62
|
-
readonly fileSystem: SubagentDiagnosticFileSystem;
|
|
63
|
-
};
|
|
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
|
-
|
|
75
|
-
const DEFAULT_DIAGNOSTIC_DEPENDENCIES = {
|
|
76
|
-
fileSystem: {
|
|
77
|
-
inspect: lstat,
|
|
78
|
-
realPath: realpath,
|
|
79
|
-
openReadOnlyNoFollow: (path: string) =>
|
|
80
|
-
open(path, constants.O_RDONLY | constants.O_NOFOLLOW),
|
|
81
|
-
},
|
|
82
|
-
} as const satisfies SubagentDiagnosticDependencies;
|
|
83
|
-
|
|
84
|
-
const isSessionFilePath = (path: string): boolean =>
|
|
85
|
-
isAbsolute(path) &&
|
|
86
|
-
!path.includes('\0') &&
|
|
87
|
-
basename(path) === SESSION_FILE_NAME &&
|
|
88
|
-
SESSION_RUN_DIRECTORY.test(basename(dirname(path)));
|
|
89
|
-
|
|
90
|
-
const pathIsWithin = (root: string, candidate: string): boolean => {
|
|
91
|
-
const relativePath = relative(resolve(root), resolve(candidate));
|
|
92
|
-
return (
|
|
93
|
-
relativePath !== '' &&
|
|
94
|
-
relativePath !== '..' &&
|
|
95
|
-
!relativePath.startsWith(`..${sep}`) &&
|
|
96
|
-
!isAbsolute(relativePath)
|
|
97
|
-
);
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
const isValidSessionIdentity = (identity: SubagentSessionIdentity): boolean =>
|
|
101
|
-
identity.runId.length > 0 &&
|
|
102
|
-
!identity.runId.includes('\0') &&
|
|
103
|
-
basename(identity.runId) === identity.runId &&
|
|
104
|
-
identity.runId !== '.' &&
|
|
105
|
-
identity.runId !== '..' &&
|
|
106
|
-
Number.isSafeInteger(identity.childIndex) &&
|
|
107
|
-
identity.childIndex >= 0;
|
|
108
|
-
|
|
109
|
-
const readStableTail = async (
|
|
110
|
-
handle: SubagentDiagnosticFileHandle,
|
|
111
|
-
): Promise<SessionTail | undefined> => {
|
|
112
|
-
const opened = await handle.stat();
|
|
113
|
-
if (!opened.isFile()) return undefined;
|
|
114
|
-
|
|
115
|
-
const bytesToRead = Math.min(opened.size, MAX_SESSION_TAIL_BYTES);
|
|
116
|
-
if (bytesToRead === 0) return { content: '', truncated: false };
|
|
117
|
-
|
|
118
|
-
const start = opened.size - bytesToRead;
|
|
119
|
-
const buffer = Buffer.alloc(bytesToRead);
|
|
120
|
-
const { bytesRead } = await handle.read(buffer, 0, bytesToRead, start);
|
|
121
|
-
const afterRead = await handle.stat();
|
|
122
|
-
const didFileChange =
|
|
123
|
-
afterRead.dev !== opened.dev ||
|
|
124
|
-
afterRead.ino !== opened.ino ||
|
|
125
|
-
afterRead.size !== opened.size ||
|
|
126
|
-
afterRead.mtimeMs !== opened.mtimeMs;
|
|
127
|
-
if (didFileChange) return undefined;
|
|
128
|
-
|
|
129
|
-
let content = buffer.subarray(0, bytesRead).toString('utf8');
|
|
130
|
-
if (start > 0) {
|
|
131
|
-
const firstNewline = content.indexOf('\n');
|
|
132
|
-
content = firstNewline === -1 ? '' : content.slice(firstNewline + 1);
|
|
133
|
-
}
|
|
134
|
-
return { content, truncated: start > 0 };
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
type ReadContainedSessionTailOptions = {
|
|
138
|
-
readonly sessionFile: string;
|
|
139
|
-
readonly trustedRoot: string;
|
|
140
|
-
readonly identity: SubagentSessionIdentity;
|
|
141
|
-
readonly dependencies: SubagentDiagnosticDependencies;
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
const readContainedSessionTail = async ({
|
|
145
|
-
sessionFile,
|
|
146
|
-
trustedRoot,
|
|
147
|
-
identity,
|
|
148
|
-
dependencies,
|
|
149
|
-
}: ReadContainedSessionTailOptions): Promise<SessionTail | undefined> => {
|
|
150
|
-
const expectedSessionFile = resolve(
|
|
151
|
-
trustedRoot,
|
|
152
|
-
identity.runId,
|
|
153
|
-
`run-${identity.childIndex}`,
|
|
154
|
-
SESSION_FILE_NAME,
|
|
155
|
-
);
|
|
156
|
-
if (
|
|
157
|
-
!isSessionFilePath(sessionFile) ||
|
|
158
|
-
!isAbsolute(trustedRoot) ||
|
|
159
|
-
trustedRoot.includes('\0') ||
|
|
160
|
-
!pathIsWithin(trustedRoot, sessionFile) ||
|
|
161
|
-
!isValidSessionIdentity(identity) ||
|
|
162
|
-
resolve(sessionFile) !== expectedSessionFile
|
|
163
|
-
) {
|
|
164
|
-
return undefined;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const runDirectory = resolve(trustedRoot, identity.runId);
|
|
168
|
-
const childDirectory = resolve(runDirectory, `run-${identity.childIndex}`);
|
|
169
|
-
const resolvedSessionFile = resolve(sessionFile);
|
|
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
|
-
}
|
|
185
|
-
|
|
186
|
-
const [canonicalRoot, canonicalSessionFile] = await Promise.all([
|
|
187
|
-
dependencies.fileSystem.realPath(trustedRoot),
|
|
188
|
-
dependencies.fileSystem.realPath(resolvedSessionFile),
|
|
189
|
-
]);
|
|
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
|
-
}
|
|
202
|
-
|
|
203
|
-
const handle =
|
|
204
|
-
await dependencies.fileSystem.openReadOnlyNoFollow(canonicalSessionFile);
|
|
205
|
-
try {
|
|
206
|
-
return await readStableTail(handle);
|
|
207
|
-
} finally {
|
|
208
|
-
await handle.close();
|
|
209
|
-
}
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Derives the trusted pi-subagents session root beside a parent session file.
|
|
214
|
-
*/
|
|
215
|
-
export const deriveSubagentSessionRoot = (
|
|
216
|
-
parentSessionFile: string | undefined,
|
|
217
|
-
): string | undefined => {
|
|
218
|
-
if (
|
|
219
|
-
!parentSessionFile ||
|
|
220
|
-
!isAbsolute(parentSessionFile) ||
|
|
221
|
-
parentSessionFile.includes('\0')
|
|
222
|
-
) {
|
|
223
|
-
return undefined;
|
|
224
|
-
}
|
|
225
|
-
const parentName = basename(parentSessionFile);
|
|
226
|
-
if (
|
|
227
|
-
!parentName.endsWith(SESSION_FILE_SUFFIX) ||
|
|
228
|
-
parentName === SESSION_FILE_SUFFIX
|
|
229
|
-
) {
|
|
230
|
-
return undefined;
|
|
231
|
-
}
|
|
232
|
-
return join(
|
|
233
|
-
dirname(parentSessionFile),
|
|
234
|
-
parentName.slice(0, -SESSION_FILE_SUFFIX.length),
|
|
235
|
-
);
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Reads and correlates a tool failure from a confined child session file.
|
|
240
|
-
*
|
|
241
|
-
* File-system operations are dependency-injected for deterministic use.
|
|
242
|
-
*/
|
|
243
|
-
export const readToolFailureDiagnostic = async (
|
|
244
|
-
sessionFile: string | undefined,
|
|
245
|
-
trustedRoot: string | undefined,
|
|
246
|
-
identity: SubagentSessionIdentity | undefined,
|
|
247
|
-
expectedTool?: string,
|
|
248
|
-
terminalError?: string,
|
|
249
|
-
dependencies: SubagentDiagnosticDependencies = DEFAULT_DIAGNOSTIC_DEPENDENCIES,
|
|
250
|
-
): Promise<ToolFailureDiagnostic | undefined> => {
|
|
251
|
-
if (!sessionFile || !trustedRoot || !identity) return undefined;
|
|
252
|
-
try {
|
|
253
|
-
const tail = await readContainedSessionTail({
|
|
254
|
-
sessionFile,
|
|
255
|
-
trustedRoot,
|
|
256
|
-
identity,
|
|
257
|
-
dependencies,
|
|
258
|
-
});
|
|
259
|
-
return parseToolFailureDiagnostic(
|
|
260
|
-
tail?.content ?? '',
|
|
261
|
-
expectedTool,
|
|
262
|
-
terminalError,
|
|
263
|
-
tail?.truncated !== true,
|
|
264
|
-
);
|
|
265
|
-
} catch {
|
|
266
|
-
return undefined;
|
|
267
|
-
}
|
|
268
|
-
};
|
|
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
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Reads and audits replay safety from a confined child session file.
|
|
333
|
-
*
|
|
334
|
-
* File-system operations are dependency-injected for deterministic use.
|
|
335
|
-
*/
|
|
336
|
-
export const readDelegationReplayAudit = async (
|
|
337
|
-
sessionFile: string | undefined,
|
|
338
|
-
trustedRoot: string | undefined,
|
|
339
|
-
identity: SubagentSessionIdentity | undefined,
|
|
340
|
-
expectation: DelegationReplayExpectation,
|
|
341
|
-
dependencies: SubagentDiagnosticDependencies = DEFAULT_DIAGNOSTIC_DEPENDENCIES,
|
|
342
|
-
): Promise<DelegationReplayAudit | undefined> => {
|
|
343
|
-
if (!sessionFile || !trustedRoot || !identity) return undefined;
|
|
344
|
-
try {
|
|
345
|
-
const tail = await readContainedSessionTail({
|
|
346
|
-
sessionFile,
|
|
347
|
-
trustedRoot,
|
|
348
|
-
identity,
|
|
349
|
-
dependencies,
|
|
350
|
-
});
|
|
351
|
-
return tail
|
|
352
|
-
? parseDelegationReplayAudit(tail.content, expectation, !tail.truncated)
|
|
353
|
-
: undefined;
|
|
354
|
-
} catch {
|
|
355
|
-
return undefined;
|
|
356
|
-
}
|
|
357
|
-
};
|