@fitlab-ai/agent-infra 0.9.5 → 0.9.6
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/dist/bin/internal-cli.js +3 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +126 -16
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +48 -4
- package/dist/lib/agent-clients/adapters/codex.js +1 -1
- package/dist/lib/git/workflow.js +0 -1
- package/dist/lib/internal/codex-lifecycle.js +100 -2
- package/dist/lib/internal/release-workflow.js +108 -11
- package/dist/lib/internal/sandbox-control.js +43 -6
- package/dist/lib/internal/task-create.js +12 -4
- package/dist/lib/internal/task-orchestration.js +5 -4
- package/dist/lib/platform/issue-comments.js +2 -1
- package/dist/lib/release/workflow.js +29 -21
- package/dist/lib/sandbox/commands/create.js +15 -16
- package/dist/lib/sandbox/commands/prune.js +40 -16
- package/dist/lib/sandbox/commands/rm.js +318 -89
- package/dist/lib/sandbox/control/client.js +138 -22
- package/dist/lib/sandbox/control/executor.js +136 -0
- package/dist/lib/sandbox/control/lifecycle.js +348 -0
- package/dist/lib/sandbox/control/protocol.js +62 -26
- package/dist/lib/sandbox/control/server.js +222 -102
- package/dist/lib/sandbox/control/state.js +181 -0
- package/dist/lib/sandbox/index.js +2 -2
- package/dist/lib/sandbox/managed-fs.js +14 -2
- package/dist/lib/sandbox/mounts.js +35 -24
- package/dist/lib/sandbox/recovery.js +262 -45
- package/dist/lib/sandbox/workspace-view.js +73 -10
- package/dist/lib/sandbox/worktree-safety.js +196 -0
- package/dist/lib/server/process-control.js +6 -0
- package/dist/lib/task/artifact-lifecycle.js +5 -4
- package/dist/lib/task/codex-orchestration.js +161 -0
- package/dist/lib/task/commands/status.js +3 -0
- package/dist/lib/task/commands/validate.js +289 -0
- package/dist/lib/task/commit-finalization.js +36 -9
- package/dist/lib/task/delegation-receipts.js +25 -0
- package/dist/lib/task/events.js +10 -4
- package/dist/lib/task/finalization-artifacts.js +2 -1
- package/dist/lib/task/implementation-inputs.js +1 -7
- package/dist/lib/task/index.js +9 -0
- package/dist/lib/task/orchestration.js +133 -4
- package/dist/lib/task/verification-engine.js +38 -6
- package/dist/lib/task/verification.js +1 -0
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +142 -15
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -3
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +51 -3
- package/lib/agent-clients/adapters/codex.ts +1 -1
- package/lib/git/workflow.ts +0 -1
- package/lib/internal/codex-lifecycle.ts +107 -3
- package/lib/internal/release-workflow.ts +107 -11
- package/lib/internal/sandbox-control.ts +38 -6
- package/lib/internal/task-create.ts +16 -4
- package/lib/internal/task-orchestration.ts +4 -4
- package/lib/platform/issue-comments.ts +2 -1
- package/lib/release/workflow.ts +69 -20
- package/lib/sandbox/commands/create.ts +17 -21
- package/lib/sandbox/commands/prune.ts +53 -15
- package/lib/sandbox/commands/rm.ts +368 -103
- package/lib/sandbox/control/client.ts +152 -28
- package/lib/sandbox/control/executor.ts +163 -0
- package/lib/sandbox/control/lifecycle.ts +342 -0
- package/lib/sandbox/control/protocol.ts +96 -69
- package/lib/sandbox/control/server.ts +239 -110
- package/lib/sandbox/control/state.ts +196 -0
- package/lib/sandbox/index.ts +2 -2
- package/lib/sandbox/managed-fs.ts +17 -2
- package/lib/sandbox/mounts.ts +31 -13
- package/lib/sandbox/recovery.ts +301 -60
- package/lib/sandbox/workspace-view.ts +81 -11
- package/lib/sandbox/worktree-safety.ts +234 -0
- package/lib/server/process-control.ts +11 -0
- package/lib/task/artifact-lifecycle.ts +5 -4
- package/lib/task/codex-orchestration.ts +233 -0
- package/lib/task/commands/status.ts +2 -0
- package/lib/task/commands/validate.ts +282 -0
- package/lib/task/commit-finalization.ts +52 -9
- package/lib/task/delegation-receipts.ts +52 -3
- package/lib/task/events.ts +9 -4
- package/lib/task/finalization-artifacts.ts +2 -1
- package/lib/task/implementation-inputs.ts +1 -5
- package/lib/task/index.ts +8 -0
- package/lib/task/orchestration.ts +199 -4
- package/lib/task/verification-engine.ts +42 -8
- package/lib/task/verification.ts +2 -1
- package/package.json +1 -1
- package/templates/.agents/hooks/lifecycle-delegation.js +48 -12
- package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
- package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
- package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
- package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +44 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +44 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -3
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -3
- package/templates/.claude/commands/run-manual-validation.en.md +9 -0
- package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
- package/templates/.codex/hooks.json +8 -8
- package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
- package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
package/dist/bin/internal-cli.js
CHANGED
|
@@ -8,7 +8,7 @@ const command = process.argv[2] || '';
|
|
|
8
8
|
if (process.env.AGENT_INFRA_CONTROL_TOKEN
|
|
9
9
|
&& (command === 'task-lifecycle' || command === 'task-orchestration')) {
|
|
10
10
|
const { sandboxControl } = await import('../lib/internal/sandbox-control.js');
|
|
11
|
-
sandboxControl(['client', command, ...process.argv.slice(3)]);
|
|
11
|
+
await sandboxControl(['client', command, ...process.argv.slice(3)]);
|
|
12
12
|
}
|
|
13
13
|
else
|
|
14
14
|
switch (command) {
|
|
@@ -19,7 +19,7 @@ else
|
|
|
19
19
|
}
|
|
20
20
|
case 'sandbox-control': {
|
|
21
21
|
const { sandboxControl } = await import('../lib/internal/sandbox-control.js');
|
|
22
|
-
sandboxControl(process.argv.slice(3));
|
|
22
|
+
await sandboxControl(process.argv.slice(3));
|
|
23
23
|
break;
|
|
24
24
|
}
|
|
25
25
|
case 'agent-client': {
|
|
@@ -109,7 +109,7 @@ else
|
|
|
109
109
|
}
|
|
110
110
|
case 'task-orchestration': {
|
|
111
111
|
const { taskOrchestration } = await import('../lib/internal/task-orchestration.js');
|
|
112
|
-
taskOrchestration(process.argv.slice(3));
|
|
112
|
+
await taskOrchestration(process.argv.slice(3));
|
|
113
113
|
break;
|
|
114
114
|
}
|
|
115
115
|
case 'task-review': {
|
|
@@ -6,10 +6,12 @@ import path from 'node:path';
|
|
|
6
6
|
import semver from 'semver';
|
|
7
7
|
const LIFECYCLE_HOOKS = Object.freeze([
|
|
8
8
|
Object.freeze({ event: 'PreToolUse', matcher: '^collaborationspawn_agent$', phase: 'pre-tool' }),
|
|
9
|
-
Object.freeze({ event: 'PostToolUse', matcher: '
|
|
10
|
-
Object.freeze({ event: 'SubagentStart', matcher: '
|
|
11
|
-
Object.freeze({ event: 'SubagentStop', matcher: '
|
|
9
|
+
Object.freeze({ event: 'PostToolUse', matcher: '', phase: 'post-tool' }),
|
|
10
|
+
Object.freeze({ event: 'SubagentStart', matcher: '', phase: 'subagent-start' }),
|
|
11
|
+
Object.freeze({ event: 'SubagentStop', matcher: '', phase: 'subagent-stop' })
|
|
12
12
|
]);
|
|
13
|
+
const DEFAULT_ROLLOUT_READ_ATTEMPTS = 81;
|
|
14
|
+
const DEFAULT_ROLLOUT_RETRY_MS = 100;
|
|
13
15
|
function object(value) {
|
|
14
16
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
15
17
|
? value
|
|
@@ -18,6 +20,58 @@ function object(value) {
|
|
|
18
20
|
function nonEmpty(value) {
|
|
19
21
|
return typeof value === 'string' && value.trim() ? value : null;
|
|
20
22
|
}
|
|
23
|
+
function resolveCodexSpawnedChild(transcriptPath, expected) {
|
|
24
|
+
const name = path.basename(transcriptPath);
|
|
25
|
+
if (!transcriptPath || (!name.endsWith(`-${expected.sessionId}.jsonl`) && name !== `${expected.sessionId}.jsonl`)) {
|
|
26
|
+
throw new Error('Codex parent rollout path is invalid');
|
|
27
|
+
}
|
|
28
|
+
const stat = fs.lstatSync(transcriptPath);
|
|
29
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
30
|
+
throw new Error('Codex parent rollout is not a regular file');
|
|
31
|
+
const maxBytes = 8 * 1024 * 1024;
|
|
32
|
+
const offset = Math.max(0, stat.size - maxBytes);
|
|
33
|
+
const buffer = Buffer.alloc(stat.size - offset);
|
|
34
|
+
const descriptor = fs.openSync(transcriptPath, 'r');
|
|
35
|
+
try {
|
|
36
|
+
fs.readSync(descriptor, buffer, 0, buffer.length, offset);
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
fs.closeSync(descriptor);
|
|
40
|
+
}
|
|
41
|
+
let text = buffer.toString('utf8');
|
|
42
|
+
if (offset)
|
|
43
|
+
text = text.slice(text.indexOf('\n') + 1);
|
|
44
|
+
const records = text.split(/\r?\n/).filter(Boolean).map((line) => object(JSON.parse(line))).filter(Boolean);
|
|
45
|
+
const calls = records.filter((record) => {
|
|
46
|
+
const payload = object(record?.payload);
|
|
47
|
+
return record?.type === 'response_item'
|
|
48
|
+
&& payload?.type === 'function_call'
|
|
49
|
+
&& payload.call_id === expected.toolUseId
|
|
50
|
+
&& payload.namespace === 'collaboration'
|
|
51
|
+
&& payload.name === 'spawn_agent';
|
|
52
|
+
});
|
|
53
|
+
if (calls.length !== 1)
|
|
54
|
+
throw new Error('Codex parent rollout spawn call was not found uniquely');
|
|
55
|
+
const call = object(calls[0]?.payload);
|
|
56
|
+
const args = object(JSON.parse(nonEmpty(call?.arguments) ?? '{}'));
|
|
57
|
+
if (args?.agent_type !== expected.nativeAgent
|
|
58
|
+
|| args.task_name !== expected.taskName
|
|
59
|
+
|| (expected.requestedModel && args.model !== expected.requestedModel)
|
|
60
|
+
|| (expected.requestedReasoningEffort && args.reasoning_effort !== expected.requestedReasoningEffort))
|
|
61
|
+
throw new Error('Codex parent rollout spawn identity does not match the hook event');
|
|
62
|
+
const activities = records.filter((record) => {
|
|
63
|
+
const payload = object(record?.payload);
|
|
64
|
+
return record?.type === 'event_msg'
|
|
65
|
+
&& payload?.type === 'sub_agent_activity'
|
|
66
|
+
&& payload.event_id === expected.toolUseId
|
|
67
|
+
&& payload.kind === 'started'
|
|
68
|
+
&& payload.agent_path === `/root/${expected.taskName}`
|
|
69
|
+
&& nonEmpty(payload.agent_thread_id);
|
|
70
|
+
});
|
|
71
|
+
if (activities.length !== 1)
|
|
72
|
+
throw new Error('Codex parent rollout child activity was not found uniquely');
|
|
73
|
+
return nonEmpty(object(activities[0]?.payload)?.agent_thread_id);
|
|
74
|
+
}
|
|
21
75
|
function validateCodexLifecycleHookConfig(value) {
|
|
22
76
|
const hooks = object(object(value)?.hooks);
|
|
23
77
|
const valid = hooks && LIFECYCLE_HOOKS.every(({ event, matcher, phase }) => {
|
|
@@ -31,7 +85,7 @@ function validateCodexLifecycleHookConfig(value) {
|
|
|
31
85
|
return entry.hooks.some((hookValue) => {
|
|
32
86
|
const hook = object(hookValue);
|
|
33
87
|
return hook?.type === 'command'
|
|
34
|
-
&& hook.timeout ===
|
|
88
|
+
&& hook.timeout === 15
|
|
35
89
|
&& hook.command === `node "$(git rev-parse --show-toplevel)/.agents/hooks/lifecycle-delegation.js" --client codex --event ${phase}`;
|
|
36
90
|
});
|
|
37
91
|
});
|
|
@@ -84,25 +138,37 @@ function hasCodexRuntimeLiveness(runtimeRoot, hookDefinitionHash, identity) {
|
|
|
84
138
|
}
|
|
85
139
|
});
|
|
86
140
|
}
|
|
87
|
-
function parseCodexThreadResolution(readValue,
|
|
141
|
+
function parseCodexThreadResolution(readValue, rolloutRecords) {
|
|
88
142
|
const readRoot = object(readValue);
|
|
89
|
-
const resumeRoot = object(resumeValue);
|
|
90
143
|
const thread = object(readRoot?.thread);
|
|
91
|
-
const resumedThread = object(resumeRoot?.thread);
|
|
92
144
|
const source = object(thread?.source);
|
|
93
145
|
const subAgent = object(source?.subAgent);
|
|
94
146
|
const threadSpawn = object(subAgent?.thread_spawn);
|
|
147
|
+
const sessionMeta = rolloutRecords
|
|
148
|
+
.map(object)
|
|
149
|
+
.find((record) => record?.type === 'session_meta');
|
|
150
|
+
const sessionPayload = object(sessionMeta?.payload);
|
|
151
|
+
const turnContext = rolloutRecords
|
|
152
|
+
.map(object)
|
|
153
|
+
.filter((record) => record?.type === 'turn_context')
|
|
154
|
+
.at(-1);
|
|
155
|
+
const turnPayload = object(turnContext?.payload);
|
|
95
156
|
const childThreadId = nonEmpty(thread?.id);
|
|
96
157
|
const parentThreadId = nonEmpty(thread?.parentThreadId);
|
|
97
158
|
const sourceParentThreadId = nonEmpty(threadSpawn?.parent_thread_id);
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
const
|
|
159
|
+
const rolloutChildThreadId = nonEmpty(sessionPayload?.id);
|
|
160
|
+
const rolloutParentThreadId = nonEmpty(sessionPayload?.parent_thread_id);
|
|
161
|
+
const agentRole = nonEmpty(sessionPayload?.agent_role);
|
|
162
|
+
const model = nonEmpty(turnPayload?.model);
|
|
163
|
+
const reasoningEffort = nonEmpty(turnPayload?.effort);
|
|
101
164
|
const forkedFromId = thread?.forkedFromId;
|
|
102
165
|
if (!childThreadId
|
|
103
|
-
||
|
|
166
|
+
|| rolloutChildThreadId !== childThreadId
|
|
104
167
|
|| !parentThreadId
|
|
105
168
|
|| !sourceParentThreadId
|
|
169
|
+
|| rolloutParentThreadId !== parentThreadId
|
|
170
|
+
|| rolloutParentThreadId !== sourceParentThreadId
|
|
171
|
+
|| !/^agent-infra-lifecycle-(executor|reviewer)$/.test(agentRole ?? '')
|
|
106
172
|
|| !model
|
|
107
173
|
|| !reasoningEffort
|
|
108
174
|
|| (forkedFromId !== null && typeof forkedFromId !== 'string')) {
|
|
@@ -114,7 +180,8 @@ function parseCodexThreadResolution(readValue, resumeValue) {
|
|
|
114
180
|
childThreadId,
|
|
115
181
|
parentThreadId,
|
|
116
182
|
forkedFromId,
|
|
117
|
-
sourceParentThreadId
|
|
183
|
+
sourceParentThreadId,
|
|
184
|
+
nativeAgent: agentRole
|
|
118
185
|
}),
|
|
119
186
|
settings: Object.freeze({
|
|
120
187
|
type: 'app-settings',
|
|
@@ -124,6 +191,46 @@ function parseCodexThreadResolution(readValue, resumeValue) {
|
|
|
124
191
|
})
|
|
125
192
|
});
|
|
126
193
|
}
|
|
194
|
+
function readCodexRolloutRecords(readValue, childThreadId) {
|
|
195
|
+
const thread = object(object(readValue)?.thread);
|
|
196
|
+
const rolloutPath = nonEmpty(thread?.path);
|
|
197
|
+
const rolloutName = rolloutPath ? path.basename(rolloutPath) : '';
|
|
198
|
+
if (!rolloutPath || (rolloutName !== `${childThreadId}.jsonl` && !rolloutName.endsWith(`-${childThreadId}.jsonl`))) {
|
|
199
|
+
throw new Error('Codex App Server rollout path is invalid');
|
|
200
|
+
}
|
|
201
|
+
const stat = fs.statSync(rolloutPath);
|
|
202
|
+
if (!stat.isFile())
|
|
203
|
+
throw new Error('Codex App Server rollout path is not a regular file');
|
|
204
|
+
const maxBytes = 1024 * 1024;
|
|
205
|
+
const bytes = Math.min(stat.size, maxBytes);
|
|
206
|
+
const descriptor = fs.openSync(rolloutPath, 'r');
|
|
207
|
+
try {
|
|
208
|
+
const buffer = Buffer.alloc(bytes);
|
|
209
|
+
fs.readSync(descriptor, buffer, 0, bytes, 0);
|
|
210
|
+
const text = buffer.toString('utf8');
|
|
211
|
+
const completeText = stat.size <= maxBytes ? text : text.slice(0, text.lastIndexOf('\n'));
|
|
212
|
+
return Object.freeze(completeText.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line)));
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
fs.closeSync(descriptor);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function resolveCodexRolloutRecords(readValue, childThreadId, attempts, retryMs) {
|
|
219
|
+
let lastError;
|
|
220
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
221
|
+
try {
|
|
222
|
+
const records = readCodexRolloutRecords(readValue, childThreadId);
|
|
223
|
+
parseCodexThreadResolution(readValue, records);
|
|
224
|
+
return records;
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
lastError = error;
|
|
228
|
+
if (attempt + 1 < attempts)
|
|
229
|
+
await new Promise((resolve) => setTimeout(resolve, retryMs));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
throw lastError;
|
|
233
|
+
}
|
|
127
234
|
function parseCodexTurnCompleted(value) {
|
|
128
235
|
const root = object(value);
|
|
129
236
|
const turn = object(root?.turn);
|
|
@@ -287,13 +394,13 @@ async function resolveCodexThread(childThreadId, options = {}) {
|
|
|
287
394
|
const readThread = object(readResult?.thread);
|
|
288
395
|
if (nonEmpty(readThread?.id) !== childThreadId)
|
|
289
396
|
throw new Error('Codex App Server returned the wrong child thread');
|
|
290
|
-
const resumeResult = await transport.request('thread/resume', { threadId: childThreadId });
|
|
291
397
|
let resolution;
|
|
292
398
|
try {
|
|
293
|
-
|
|
399
|
+
const rolloutRecords = await resolveCodexRolloutRecords(readResult, childThreadId, options.rolloutReadAttempts ?? DEFAULT_ROLLOUT_READ_ATTEMPTS, options.rolloutRetryMs ?? DEFAULT_ROLLOUT_RETRY_MS);
|
|
400
|
+
resolution = parseCodexThreadResolution(readResult, rolloutRecords);
|
|
294
401
|
}
|
|
295
402
|
catch {
|
|
296
|
-
throw new Error('Codex App Server
|
|
403
|
+
throw new Error('Codex App Server rollout metadata is unavailable');
|
|
297
404
|
}
|
|
298
405
|
const reroutes = transport.notifications
|
|
299
406
|
.filter((entry) => entry.method === 'model/rerouted')
|
|
@@ -322,6 +429,9 @@ async function resolveCodexTerminal(childThreadId, options = {}) {
|
|
|
322
429
|
throw new Error('Codex App Server returned the wrong child thread');
|
|
323
430
|
const turns = Array.isArray(thread?.turns) ? thread.turns : [];
|
|
324
431
|
const turn = turns.at(-1);
|
|
432
|
+
const turnValue = object(turn);
|
|
433
|
+
if (!turnValue || turnValue.status === 'inProgress')
|
|
434
|
+
throw new Error('CODEX_TURN_NOT_TERMINAL');
|
|
325
435
|
return parseCodexTurnCompleted({ threadId: childThreadId, turn });
|
|
326
436
|
}
|
|
327
437
|
finally {
|
|
@@ -378,5 +488,5 @@ async function preflightCodexLifecycleEvidence(repoRoot = process.cwd(), runtime
|
|
|
378
488
|
diagnostics: transport.diagnostics
|
|
379
489
|
});
|
|
380
490
|
}
|
|
381
|
-
export { CodexAppServerTransport, hasCodexRuntimeLiveness, parseCodexModelReroute, parseCodexHooksList, parseCodexThreadResolution, parseCodexTurnCompleted, preflightCodexLifecycleEvidence, resolveCodexTerminal, resolveCodexThread, validateCodexLifecycleHookConfig };
|
|
491
|
+
export { CodexAppServerTransport, hasCodexRuntimeLiveness, parseCodexModelReroute, parseCodexHooksList, parseCodexThreadResolution, parseCodexTurnCompleted, resolveCodexSpawnedChild, preflightCodexLifecycleEvidence, resolveCodexTerminal, resolveCodexThread, validateCodexLifecycleHookConfig };
|
|
382
492
|
//# sourceMappingURL=app-server.js.map
|
|
@@ -35,17 +35,29 @@ function requiredStrings(event) {
|
|
|
35
35
|
}
|
|
36
36
|
function derive(state) {
|
|
37
37
|
const { spawn, child, thread, settings, reroute, terminal, stop } = state;
|
|
38
|
-
if (spawn && child && (spawn.sessionId !== child.
|
|
38
|
+
if (spawn && child && (spawn.sessionId !== child.parentThreadId
|
|
39
39
|
|| spawn.nativeAgent !== child.nativeAgent)) {
|
|
40
40
|
return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'hook spawn and child identity do not match');
|
|
41
41
|
}
|
|
42
|
+
if (child && child.sessionId !== child.parentThreadId) {
|
|
43
|
+
return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'child event session does not match the parent thread');
|
|
44
|
+
}
|
|
42
45
|
if (child && thread && child.childThreadId !== thread.childThreadId) {
|
|
43
46
|
return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'hook and App Server child identity do not match');
|
|
44
47
|
}
|
|
48
|
+
if (child && thread && child.nativeAgent !== thread.nativeAgent) {
|
|
49
|
+
return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'hook and App Server agent roles do not match');
|
|
50
|
+
}
|
|
51
|
+
if (child && thread && child.parentThreadId !== thread.parentThreadId) {
|
|
52
|
+
return invalid(state, 'CODEX_EVIDENCE_PARENT_MISMATCH', 'hook and App Server parent identity do not match');
|
|
53
|
+
}
|
|
45
54
|
if (spawn && thread && (thread.parentThreadId !== spawn.sessionId
|
|
46
55
|
|| thread.sourceParentThreadId !== spawn.sessionId)) {
|
|
47
56
|
return invalid(state, 'CODEX_EVIDENCE_PARENT_MISMATCH', 'App Server parent identity does not match the spawning session');
|
|
48
57
|
}
|
|
58
|
+
if (spawn && thread && spawn.nativeAgent !== thread.nativeAgent) {
|
|
59
|
+
return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'spawn and App Server agent roles do not match');
|
|
60
|
+
}
|
|
49
61
|
if (thread?.forkedFromId !== null && thread?.forkedFromId !== undefined) {
|
|
50
62
|
return invalid(state, 'CODEX_EVIDENCE_FORK_FORBIDDEN', 'forked child threads are not fresh lifecycle evidence');
|
|
51
63
|
}
|
|
@@ -105,7 +117,7 @@ function derive(state) {
|
|
|
105
117
|
childThreadId: startEvidence.childThreadId,
|
|
106
118
|
turnId: terminal.turnId,
|
|
107
119
|
terminalStatus: 'completed',
|
|
108
|
-
hookStopObserved:
|
|
120
|
+
hookStopObserved: stop.source !== 'parent-rollout'
|
|
109
121
|
});
|
|
110
122
|
}
|
|
111
123
|
const status = stopEvidence
|
|
@@ -99,7 +99,7 @@ function createCodexLifecycleStore(options) {
|
|
|
99
99
|
if (event.type === 'hook-child') {
|
|
100
100
|
const matches = recordFiles(root).filter((file) => {
|
|
101
101
|
const state = readRecord(file).state;
|
|
102
|
-
return state.spawn?.sessionId === event.
|
|
102
|
+
return state.spawn?.sessionId === event.parentThreadId
|
|
103
103
|
&& state.spawn.nativeAgent === event.nativeAgent
|
|
104
104
|
&& (!state.child || state.child.childThreadId === event.childThreadId);
|
|
105
105
|
});
|
|
@@ -146,6 +146,43 @@ function createCodexLifecycleStore(options) {
|
|
|
146
146
|
return Object.freeze({ path: file, revision: next.revision, state: next.state });
|
|
147
147
|
});
|
|
148
148
|
}
|
|
149
|
+
function applyToSpawn(identity, event) {
|
|
150
|
+
return withWriteLock(() => {
|
|
151
|
+
const file = path.join(root, `${digest(`${identity.sessionId}\0${identity.turnId}\0${identity.toolUseId}`)}.json`);
|
|
152
|
+
if (!fs.existsSync(file))
|
|
153
|
+
throw new Error('Codex lifecycle spawn identity was not found');
|
|
154
|
+
const current = readRecord(file);
|
|
155
|
+
if (current.consumer)
|
|
156
|
+
throw new Error(`Codex lifecycle evidence was already consumed by '${current.consumer}'`);
|
|
157
|
+
if (current.state.spawn?.sessionId !== identity.sessionId
|
|
158
|
+
|| current.state.spawn.turnId !== identity.turnId
|
|
159
|
+
|| current.state.spawn.toolUseId !== identity.toolUseId)
|
|
160
|
+
throw new Error('Codex lifecycle spawn identity does not match the stored event');
|
|
161
|
+
const nextState = reduceCodexLifecycleEvent(current.state, event);
|
|
162
|
+
const next = Object.freeze({
|
|
163
|
+
...current,
|
|
164
|
+
revision: current.revision + 1,
|
|
165
|
+
state: nextState,
|
|
166
|
+
updatedAt: now()
|
|
167
|
+
});
|
|
168
|
+
writeRecord(file, next, current.revision);
|
|
169
|
+
return Object.freeze({ path: file, revision: next.revision, state: next.state });
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
function findByParent(parentThreadId) {
|
|
173
|
+
const matches = recordFiles(root)
|
|
174
|
+
.map(readRecord)
|
|
175
|
+
.filter((record) => record.state.startEvidence?.parentThreadId === parentThreadId
|
|
176
|
+
&& ['start-ready', 'observed-terminal', 'stop-ready'].includes(record.state.status));
|
|
177
|
+
const unconsumed = matches.filter((record) => !record.consumer);
|
|
178
|
+
if (unconsumed.length > 1)
|
|
179
|
+
throw new Error(`Codex lifecycle parent '${parentThreadId}' has ambiguous active children`);
|
|
180
|
+
if (unconsumed.length === 1)
|
|
181
|
+
return Object.freeze(unconsumed);
|
|
182
|
+
return Object.freeze(matches
|
|
183
|
+
.filter((record) => record.consumer && record.state.status === 'stop-ready')
|
|
184
|
+
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)));
|
|
185
|
+
}
|
|
149
186
|
function read(childThreadId) {
|
|
150
187
|
const matches = findByChild(childThreadId);
|
|
151
188
|
if (matches.length !== 1)
|
|
@@ -161,8 +198,15 @@ function createCodexLifecycleStore(options) {
|
|
|
161
198
|
throw new Error(`Codex lifecycle child '${childThreadId}' was not found uniquely`);
|
|
162
199
|
const file = matches[0];
|
|
163
200
|
const current = readRecord(file);
|
|
164
|
-
if (current.consumer)
|
|
165
|
-
|
|
201
|
+
if (current.consumer) {
|
|
202
|
+
if (current.consumer !== consumer) {
|
|
203
|
+
throw new Error(`Codex lifecycle evidence was already consumed by '${current.consumer}'`);
|
|
204
|
+
}
|
|
205
|
+
if (expectedHookDefinitionHash
|
|
206
|
+
&& current.state.startEvidence?.hookDefinitionHash !== expectedHookDefinitionHash)
|
|
207
|
+
throw new Error('Codex lifecycle hook definition hash is stale');
|
|
208
|
+
return current;
|
|
209
|
+
}
|
|
166
210
|
if (current.state.status !== 'stop-ready')
|
|
167
211
|
throw new Error('Codex lifecycle evidence is not stop-ready');
|
|
168
212
|
if (expectedHookDefinitionHash
|
|
@@ -205,7 +249,7 @@ function createCodexLifecycleStore(options) {
|
|
|
205
249
|
return changed;
|
|
206
250
|
});
|
|
207
251
|
}
|
|
208
|
-
return Object.freeze({ root, apply, consume, expireBefore, read });
|
|
252
|
+
return Object.freeze({ root, apply, applyToSpawn, consume, expireBefore, findByParent, read });
|
|
209
253
|
}
|
|
210
254
|
export { createCodexLifecycleStore };
|
|
211
255
|
//# sourceMappingURL=store.js.map
|
|
@@ -11,7 +11,7 @@ const codexAdapter = defineAgentClientAdapter({
|
|
|
11
11
|
commands: { level: 'integrated' },
|
|
12
12
|
hooks: { level: 'integrated' },
|
|
13
13
|
subagents: { level: 'experimental' },
|
|
14
|
-
orchestration: { level: '
|
|
14
|
+
orchestration: { level: 'experimental' },
|
|
15
15
|
sandbox: { level: 'integrated' },
|
|
16
16
|
verification: { level: 'compatible' }
|
|
17
17
|
},
|
package/dist/lib/git/workflow.js
CHANGED
|
@@ -119,7 +119,6 @@ function pushRebasedBranch(input, runner = defaultRunner) {
|
|
|
119
119
|
.map((name) => value(runner, input.cwd, ['rev-parse', '--git-path', name]))
|
|
120
120
|
.filter((candidate) => Boolean(candidate));
|
|
121
121
|
if (snapshot.worktree.length > 0 || snapshot.staged.length > 0 ||
|
|
122
|
-
run(runner, input.cwd, ['rev-parse', '-q', '--verify', 'REBASE_HEAD']).status === 0 ||
|
|
123
122
|
rebasePaths.some((candidate) => fs.existsSync(path.isAbsolute(candidate) ? candidate : path.join(input.cwd, candidate)))) {
|
|
124
123
|
return { status: 'failed', changed: false, snapshot, operations: [], error: { code: 'GIT_LOCAL_STATE_UNSAFE', message: 'Working tree must be clean with no rebase in progress' } };
|
|
125
124
|
}
|
|
@@ -4,6 +4,7 @@ import fs from 'node:fs';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { preflightCodexLifecycleEvidence, resolveCodexTerminal, resolveCodexThread } from '../agent-clients/adapters/codex-lifecycle/app-server.js';
|
|
6
6
|
import { createCodexLifecycleStore } from '../agent-clients/adapters/codex-lifecycle/store.js';
|
|
7
|
+
import { activateCodexOrchestrationDelegation, activateCodexSpawnDelegation, reconcileCodexOrchestrationDelegation, sealCodexOrchestrationDelegation, sealCodexParentDelegation } from '../task/codex-orchestration.js';
|
|
7
8
|
const USAGE = 'Usage: agent-infra-internal codex-lifecycle <hook-event|resolve-start|resolve-stop|preflight|consume> [options]\n';
|
|
8
9
|
const MANAGED_AGENT = /^agent-infra-lifecycle-(executor|reviewer)$/;
|
|
9
10
|
function output(value) {
|
|
@@ -13,6 +14,15 @@ function failure(code, message) {
|
|
|
13
14
|
output({ status: 'failed', changed: false, evidence: null, diagnostics: [], error: { code, message } });
|
|
14
15
|
process.exitCode = 1;
|
|
15
16
|
}
|
|
17
|
+
function outputBridgeResult(result) {
|
|
18
|
+
if (result.error?.code === 'ORCHESTRATION_DELEGATION_MISSING') {
|
|
19
|
+
output({ status: 'ignored', changed: false, evidence: null, diagnostics: [], error: null });
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
output(result);
|
|
23
|
+
if (result.status !== 'running')
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
}
|
|
16
26
|
function parse(args) {
|
|
17
27
|
if (args[0] === '--help' || args[0] === '-h') {
|
|
18
28
|
process.stdout.write(USAGE);
|
|
@@ -24,7 +34,7 @@ function parse(args) {
|
|
|
24
34
|
return null;
|
|
25
35
|
}
|
|
26
36
|
const allowed = {
|
|
27
|
-
'hook-event': ['--event'],
|
|
37
|
+
'hook-event': ['--event', '--bridge'],
|
|
28
38
|
'resolve-start': ['--child-id'],
|
|
29
39
|
'resolve-stop': ['--child-id'],
|
|
30
40
|
preflight: ['--format', '--session-id', '--turn-id', '--tool-use-id'],
|
|
@@ -110,6 +120,12 @@ function recordFromPayload(phase, payload) {
|
|
|
110
120
|
return null;
|
|
111
121
|
throw new Error(`unknown Codex lifecycle hook phase '${phase}'`);
|
|
112
122
|
}
|
|
123
|
+
function payloadText(payload, key) {
|
|
124
|
+
return payload && typeof payload === 'object' && !Array.isArray(payload)
|
|
125
|
+
&& typeof payload[key] === 'string'
|
|
126
|
+
? payload[key]
|
|
127
|
+
: '';
|
|
128
|
+
}
|
|
113
129
|
async function codexLifecycle(args = []) {
|
|
114
130
|
const parsed = parse(args);
|
|
115
131
|
if (!parsed || process.exitCode)
|
|
@@ -146,12 +162,94 @@ async function codexLifecycle(args = []) {
|
|
|
146
162
|
if (!phase || !['pre-tool', 'subagent-start', 'post-tool', 'subagent-stop'].includes(phase)) {
|
|
147
163
|
throw new Error('hook-event requires a known --event');
|
|
148
164
|
}
|
|
149
|
-
|
|
165
|
+
if (parsed.values['--bridge'] !== undefined && parsed.values['--bridge'] !== 'true') {
|
|
166
|
+
throw new Error("hook-event --bridge must be 'true'");
|
|
167
|
+
}
|
|
168
|
+
const payload = await readStdin();
|
|
169
|
+
if (phase === 'post-tool' && parsed.values['--bridge'] === 'true') {
|
|
170
|
+
const toolName = payloadText(payload, 'toolName');
|
|
171
|
+
if (toolName === 'collaborationspawn_agent') {
|
|
172
|
+
const nativeAgent = payloadText(payload, 'nativeAgent');
|
|
173
|
+
if (!MANAGED_AGENT.test(nativeAgent)) {
|
|
174
|
+
output({ status: 'ignored', changed: false, evidence: null, diagnostics: [], error: null });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const requestedModel = payloadText(payload, 'requestedModel');
|
|
178
|
+
const requestedReasoningEffort = payloadText(payload, 'requestedReasoningEffort');
|
|
179
|
+
const bridged = await activateCodexSpawnDelegation({
|
|
180
|
+
sessionId: payloadText(payload, 'sessionId'),
|
|
181
|
+
turnId: payloadText(payload, 'turnId'),
|
|
182
|
+
toolUseId: payloadText(payload, 'toolUseId'),
|
|
183
|
+
transcriptPath: payloadText(payload, 'transcriptPath'),
|
|
184
|
+
nativeAgent,
|
|
185
|
+
taskName: payloadText(payload, 'taskName'),
|
|
186
|
+
...(requestedModel ? { requestedModel } : {}),
|
|
187
|
+
...(requestedReasoningEffort ? { requestedReasoningEffort } : {})
|
|
188
|
+
}, { store });
|
|
189
|
+
outputBridgeResult(bridged);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (toolName === 'collaborationwait_agent') {
|
|
193
|
+
outputBridgeResult(await sealCodexParentDelegation(payloadText(payload, 'sessionId'), { store }));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
output({ status: 'ignored', changed: false, evidence: null, diagnostics: [], error: null });
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const event = recordFromPayload(phase, payload);
|
|
150
200
|
if (!event) {
|
|
201
|
+
if (phase === 'post-tool' && parsed.values['--bridge'] === 'true') {
|
|
202
|
+
const value = payload;
|
|
203
|
+
const childThreadId = String(value.childThreadId ?? '');
|
|
204
|
+
const reconciled = reconcileCodexOrchestrationDelegation(childThreadId);
|
|
205
|
+
outputBridgeResult(reconciled);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
151
208
|
output({ status: 'ignored', changed: false, evidence: null, diagnostics: [], error: null });
|
|
152
209
|
return;
|
|
153
210
|
}
|
|
211
|
+
if (parsed.values['--bridge'] === 'true' && event.type === 'hook-stop') {
|
|
212
|
+
try {
|
|
213
|
+
if (store.read(event.childThreadId).consumer) {
|
|
214
|
+
const bridged = await sealCodexOrchestrationDelegation(event.childThreadId, { store });
|
|
215
|
+
outputBridgeResult(bridged);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
// The normal apply path below owns missing or invalid evidence errors.
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (parsed.values['--bridge'] === 'true' && event.type === 'hook-child') {
|
|
224
|
+
const resolved = await resolveCodexThread(event.childThreadId);
|
|
225
|
+
store.apply({
|
|
226
|
+
...event,
|
|
227
|
+
parentThreadId: resolved.resolution.thread.parentThreadId
|
|
228
|
+
});
|
|
229
|
+
const bridged = await activateCodexOrchestrationDelegation(event.childThreadId, {
|
|
230
|
+
store,
|
|
231
|
+
resolveThread: async () => resolved
|
|
232
|
+
});
|
|
233
|
+
outputBridgeResult(bridged);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (event.type === 'hook-child') {
|
|
237
|
+
const resolved = await resolveCodexThread(event.childThreadId);
|
|
238
|
+
const result = store.apply({
|
|
239
|
+
...event,
|
|
240
|
+
parentThreadId: resolved.resolution.thread.parentThreadId
|
|
241
|
+
});
|
|
242
|
+
output({ status: result.state.status, changed: true, evidence: result.state, diagnostics: resolved.diagnostics, error: result.state.error });
|
|
243
|
+
if (result.state.status === 'invalid')
|
|
244
|
+
process.exitCode = 1;
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
154
247
|
const result = store.apply(event);
|
|
248
|
+
if (parsed.values['--bridge'] === 'true' && event.type === 'hook-stop') {
|
|
249
|
+
const bridged = await sealCodexOrchestrationDelegation(event.childThreadId, { store });
|
|
250
|
+
outputBridgeResult(bridged);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
155
253
|
output({ status: result.state.status, changed: true, evidence: result.state, diagnostics: [], error: result.state.error });
|
|
156
254
|
if (result.state.status === 'invalid')
|
|
157
255
|
process.exitCode = 1;
|