@fitlab-ai/agent-infra 0.9.5 → 0.9.7
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 +8 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +171 -20
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +244 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +370 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +68 -8
- package/dist/lib/agent-clients/adapters/codex.js +1 -1
- package/dist/lib/defaults.json +10 -2
- package/dist/lib/git/workflow.js +30 -4
- package/dist/lib/internal/codex-lifecycle.js +169 -3
- package/dist/lib/internal/codex-sandbox-controller.js +106 -0
- 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 +40 -7
- package/dist/lib/platform/context.js +6 -4
- package/dist/lib/platform/github-client.js +55 -2
- package/dist/lib/platform/issue-comments.js +2 -1
- package/dist/lib/process-data/checkpoint.js +319 -0
- package/dist/lib/process-data/index.js +128 -60
- package/dist/lib/process-data/materialize.js +84 -0
- package/dist/lib/process-data/normalize.js +34 -1
- package/dist/lib/process-data/reconcile.js +55 -1
- package/dist/lib/process-data/sources.js +422 -1
- package/dist/lib/process-data/store.js +341 -11
- package/dist/lib/release/workflow.js +29 -21
- package/dist/lib/sandbox/commands/create.js +111 -60
- package/dist/lib/sandbox/commands/prune.js +40 -16
- package/dist/lib/sandbox/commands/rm.js +372 -94
- package/dist/lib/sandbox/commands/start.js +4 -3
- package/dist/lib/sandbox/control/client.js +138 -22
- package/dist/lib/sandbox/control/container-identity.js +64 -0
- package/dist/lib/sandbox/control/executor.js +142 -0
- package/dist/lib/sandbox/control/lifecycle.js +557 -0
- package/dist/lib/sandbox/control/protocol.js +69 -26
- package/dist/lib/sandbox/control/server.js +395 -99
- package/dist/lib/sandbox/control/state.js +191 -0
- package/dist/lib/sandbox/control/timing.js +8 -0
- package/dist/lib/sandbox/index.js +2 -2
- package/dist/lib/sandbox/managed-fs.js +14 -2
- package/dist/lib/sandbox/mounts.js +22 -19
- package/dist/lib/sandbox/recovery.js +284 -50
- package/dist/lib/sandbox/workspace-view.js +90 -10
- package/dist/lib/sandbox/worktree-safety.js +196 -0
- package/dist/lib/server/daemon.js +1 -1
- package/dist/lib/server/process-control.js +17 -1
- package/dist/lib/server/process-state.js +57 -20
- package/dist/lib/task/artifact-lifecycle.js +87 -15
- package/dist/lib/task/artifact-receipts.js +128 -0
- package/dist/lib/task/codex-orchestration.js +236 -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/create.js +10 -2
- package/dist/lib/task/delegation-receipts.js +109 -2
- package/dist/lib/task/events.js +149 -6
- 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/ledger.js +13 -1
- package/dist/lib/task/orchestration.js +365 -15
- package/dist/lib/task/sections.js +5 -1
- package/dist/lib/task/task-execution-lock.js +5 -4
- package/dist/lib/task/verification-engine.js +78 -14
- package/dist/lib/task/verification.js +1 -0
- package/dist/lifecycle-build-manifest.json +6 -0
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +202 -18
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -3
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +83 -6
- package/lib/agent-clients/adapters/codex.ts +1 -1
- package/lib/defaults.json +10 -2
- package/lib/git/workflow.ts +25 -3
- package/lib/internal/codex-lifecycle.ts +175 -4
- package/lib/internal/codex-sandbox-controller.ts +109 -0
- 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 +34 -7
- package/lib/platform/context.ts +8 -5
- package/lib/platform/github-client.ts +61 -3
- package/lib/platform/issue-comments.ts +2 -1
- package/lib/process-data/checkpoint.ts +335 -0
- package/lib/process-data/index.ts +112 -60
- package/lib/process-data/materialize.ts +83 -0
- package/lib/process-data/normalize.ts +29 -1
- package/lib/process-data/reconcile.ts +64 -2
- package/lib/process-data/sources.ts +449 -2
- package/lib/process-data/store.ts +304 -11
- package/lib/process-data/types.ts +124 -2
- package/lib/release/workflow.ts +69 -20
- package/lib/sandbox/commands/create.ts +70 -25
- package/lib/sandbox/commands/prune.ts +53 -15
- package/lib/sandbox/commands/rm.ts +423 -108
- package/lib/sandbox/commands/start.ts +4 -3
- package/lib/sandbox/control/client.ts +152 -28
- package/lib/sandbox/control/container-identity.ts +93 -0
- package/lib/sandbox/control/executor.ts +167 -0
- package/lib/sandbox/control/lifecycle.ts +573 -0
- package/lib/sandbox/control/protocol.ts +121 -68
- package/lib/sandbox/control/server.ts +398 -109
- package/lib/sandbox/control/state.ts +213 -0
- package/lib/sandbox/control/timing.ts +12 -0
- package/lib/sandbox/index.ts +2 -2
- package/lib/sandbox/managed-fs.ts +17 -2
- package/lib/sandbox/mounts.ts +16 -8
- package/lib/sandbox/recovery.ts +335 -65
- package/lib/sandbox/workspace-view.ts +102 -11
- package/lib/sandbox/worktree-safety.ts +234 -0
- package/lib/server/daemon.ts +1 -1
- package/lib/server/process-control.ts +24 -3
- package/lib/server/process-state.ts +82 -27
- package/lib/task/artifact-lifecycle.ts +78 -14
- package/lib/task/artifact-receipts.ts +161 -0
- package/lib/task/codex-orchestration.ts +319 -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/create.ts +11 -2
- package/lib/task/delegation-receipts.ts +208 -7
- package/lib/task/events.ts +131 -5
- 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/ledger.ts +16 -1
- package/lib/task/orchestration.ts +552 -18
- package/lib/task/sections.ts +6 -2
- package/lib/task/task-execution-lock.ts +11 -9
- package/lib/task/verification-engine.ts +93 -24
- package/lib/task/verification.ts +2 -1
- package/package.json +1 -1
- package/templates/.agents/README.en.md +7 -0
- package/templates/.agents/README.zh-CN.md +5 -0
- package/templates/.agents/hooks/lifecycle-delegation.js +72 -12
- package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
- 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/rules/pr-checks-commands.github.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
- package/templates/.agents/rules/review-handshake.en.md +3 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -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 +25 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +25 -0
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +69 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +69 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -3
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -3
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -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/templates/.agents/workspace/README.en.md +0 -26
- package/templates/.agents/workspace/README.zh-CN.md +0 -26
|
@@ -151,7 +151,7 @@ function checkOrchestrationState({ taskDir }) {
|
|
|
151
151
|
if (isFailClosedLegacyPause(run)) {
|
|
152
152
|
return passResult('orchestration-state', 'Legacy orchestration run is safely paused');
|
|
153
153
|
}
|
|
154
|
-
if (run.schemaVersion
|
|
154
|
+
if (![2, 3].includes(run.schemaVersion) || !['paused', 'completed'].includes(run.status)) {
|
|
155
155
|
return failResult('orchestration-state', `Expected paused or completed run, received '${run.status}'`);
|
|
156
156
|
}
|
|
157
157
|
if (run.status === 'paused' && (!run.pause?.code || !run.pause?.message)) {
|
|
@@ -239,12 +239,29 @@ function checkOrchestrationEvidence({ taskDir }) {
|
|
|
239
239
|
return failResult('orchestration-evidence', 'Run model policy source or recovery history is invalid');
|
|
240
240
|
}
|
|
241
241
|
for (const recovery of run.recoveryHistory) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
242
|
+
const validModelRecovery = recovery.code === 'MODEL_POLICY_SUPPLEMENTED'
|
|
243
|
+
&& recovery.previousSchemaVersion === 1
|
|
244
|
+
&& recovery.receiptCount === 0
|
|
245
|
+
&& recovery.pendingDelegation === false
|
|
246
|
+
&& exactText(recovery.recoveredAt)
|
|
247
|
+
&& recovery.policySource;
|
|
248
|
+
const guards = recovery.guards;
|
|
249
|
+
const validCapabilityRecovery = recovery.code === 'CLIENT_CAPABILITY_ENABLED'
|
|
250
|
+
&& recovery.previousSchemaVersion === 2
|
|
251
|
+
&& recovery.previousStatus === 'paused'
|
|
252
|
+
&& recovery.previousPause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
|
|
253
|
+
&& recovery.client === 'codex'
|
|
254
|
+
&& recovery.resultingStatus === 'running'
|
|
255
|
+
&& exactText(recovery.recoveredAt)
|
|
256
|
+
&& guards?.stepCount === 0
|
|
257
|
+
&& guards?.nextStage === null
|
|
258
|
+
&& guards?.baselineEmpty === true
|
|
259
|
+
&& guards?.receiptCount === 0
|
|
260
|
+
&& guards?.pendingDelegation === false
|
|
261
|
+
&& guards?.commitAuthorizationUnused === true
|
|
262
|
+
&& guards?.completionEvidenceAbsent === true
|
|
263
|
+
&& guards?.commitIntentAbsent === true;
|
|
264
|
+
if (!validModelRecovery && !validCapabilityRecovery) {
|
|
248
265
|
return failResult('orchestration-evidence', 'Run recovery history contains invalid provenance');
|
|
249
266
|
}
|
|
250
267
|
}
|
|
@@ -298,6 +315,21 @@ function checkOrchestrationEvidence({ taskDir }) {
|
|
|
298
315
|
&& receipt.reasoningEffortFallbackReason !== null) {
|
|
299
316
|
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has an unrelated reasoning-effort fallback reason`);
|
|
300
317
|
}
|
|
318
|
+
if (receipt.client === 'codex') {
|
|
319
|
+
const host = receipt.hostEvidence;
|
|
320
|
+
if (host?.kind !== 'codex-lifecycle-v1'
|
|
321
|
+
|| !exactText(host.hookDefinitionHash)
|
|
322
|
+
|| !Number.isSafeInteger(host.startRevision)
|
|
323
|
+
|| host.startRevision < 1) {
|
|
324
|
+
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has invalid Codex start evidence`);
|
|
325
|
+
}
|
|
326
|
+
if (['sealed', 'consumed'].includes(receipt.status) && (!Number.isSafeInteger(host.stopRevision)
|
|
327
|
+
|| host.stopRevision <= host.startRevision
|
|
328
|
+
|| host.consumer !== receipt.id
|
|
329
|
+
|| !exactText(host.consumedAt))) {
|
|
330
|
+
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has invalid Codex consumed stop evidence`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
301
333
|
}
|
|
302
334
|
}
|
|
303
335
|
if (run.receipts.some((receipt) => receipt.status !== 'consumed')) {
|
|
@@ -884,7 +916,13 @@ function checkPostReviewCommit({ taskDir, config }) {
|
|
|
884
916
|
return blockedResult("post-review-commit", relation.message, relation.code);
|
|
885
917
|
}
|
|
886
918
|
if (relation.status === "strict") {
|
|
887
|
-
return
|
|
919
|
+
return resolvePostReviewIdentityFailure(task.content, {
|
|
920
|
+
code: "PR_MERGE_IDENTITY_INVALID",
|
|
921
|
+
message: "Merged PR evidence did not establish squash merge equivalence"
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
if (relation.code === "PR_MERGE_IDENTITY_INVALID") {
|
|
925
|
+
return resolvePostReviewIdentityFailure(task.content, relation);
|
|
888
926
|
}
|
|
889
927
|
return failResult("post-review-commit", relation.message, relation.code);
|
|
890
928
|
}
|
|
@@ -949,21 +987,47 @@ function checkPostReviewCommit({ taskDir, config }) {
|
|
|
949
987
|
}
|
|
950
988
|
const exemption = resolvePostReviewExemption(task.content);
|
|
951
989
|
if (!exemption.ok)
|
|
952
|
-
return failResult("post-review-commit", exemption.message);
|
|
953
|
-
if (exemption.
|
|
954
|
-
return passResult("post-review-commit", `${commits.length} post-review commit(s) covered by a human-decided exemption`);
|
|
990
|
+
return failResult("post-review-commit", exemption.message, exemption.code);
|
|
991
|
+
if (exemption.exemptions.length > 0) {
|
|
992
|
+
return passResult("post-review-commit", `${commits.length} post-review commit(s) covered by a human-decided exemption: ${formatPostReviewExemptions(exemption.exemptions)}`);
|
|
955
993
|
}
|
|
956
994
|
return failResult("post-review-commit", `${commits.length} commit(s) to code/rule paths after reviewed commit ${sha.slice(0, 8)}; re-run review-code or record a human-decided exemption`);
|
|
957
995
|
}
|
|
996
|
+
function resolvePostReviewIdentityFailure(content, failure) {
|
|
997
|
+
const exemption = resolvePostReviewExemption(content);
|
|
998
|
+
if (!exemption.ok) {
|
|
999
|
+
return failResult("post-review-commit", exemption.message, exemption.code);
|
|
1000
|
+
}
|
|
1001
|
+
if (exemption.exemptions.length === 0) {
|
|
1002
|
+
return failResult("post-review-commit", failure.message, failure.code);
|
|
1003
|
+
}
|
|
1004
|
+
return passResult("post-review-commit", `Human-decided post-review exemption overrode ${failure.code}: ${failure.message}; ${formatPostReviewExemptions(exemption.exemptions)}`);
|
|
1005
|
+
}
|
|
1006
|
+
function formatPostReviewExemptions(exemptions) {
|
|
1007
|
+
return exemptions.map((row) => `${row.id}: ${row.evidence}`).join("; ");
|
|
1008
|
+
}
|
|
958
1009
|
function resolvePostReviewExemption(content) {
|
|
959
1010
|
try {
|
|
1011
|
+
const rows = parseLedger(content);
|
|
1012
|
+
const invalid = validateLedgerRows(rows);
|
|
1013
|
+
if (invalid) {
|
|
1014
|
+
return {
|
|
1015
|
+
ok: false,
|
|
1016
|
+
code: "POST_REVIEW_EXEMPTION_INVALID",
|
|
1017
|
+
message: `${invalid.code}: ${invalid.message}`
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
960
1020
|
return {
|
|
961
1021
|
ok: true,
|
|
962
|
-
|
|
1022
|
+
exemptions: rows.filter((row) => row.stage === POST_REVIEW_COMMIT_STAGE)
|
|
963
1023
|
};
|
|
964
1024
|
}
|
|
965
|
-
catch {
|
|
966
|
-
return {
|
|
1025
|
+
catch (error) {
|
|
1026
|
+
return {
|
|
1027
|
+
ok: false,
|
|
1028
|
+
code: "POST_REVIEW_EXEMPTION_INVALID",
|
|
1029
|
+
message: `Invalid disagreement ledger: ${error instanceof Error ? error.message : String(error)}`
|
|
1030
|
+
};
|
|
967
1031
|
}
|
|
968
1032
|
}
|
|
969
1033
|
function checkReviewFact({ taskDir, artifactFile }) {
|
|
@@ -16,6 +16,7 @@ const VERIFICATION_CATALOG = {
|
|
|
16
16
|
'code.completed': gate('code-task', 'active', 'code'),
|
|
17
17
|
'review-code.completed': gate('review-code', 'active', 'review-code'),
|
|
18
18
|
'manual-validation.completed': gate('complete-manual-validation', 'active', 'manual-validation'),
|
|
19
|
+
'validation-run.completed': gate('run-manual-validation', 'active', 'validation-run'),
|
|
19
20
|
'block-task.completed': gate('block-task', 'blocked'),
|
|
20
21
|
'cancel-task.completed': gate('cancel-task', 'completed'),
|
|
21
22
|
'commit.completed': gate('commit', 'active'),
|
|
@@ -19,6 +19,12 @@ type DiscoveredHook = Readonly<{
|
|
|
19
19
|
matcher: string | null;
|
|
20
20
|
command: string;
|
|
21
21
|
enabled: boolean;
|
|
22
|
+
source: string;
|
|
23
|
+
sourcePathDigest: string;
|
|
24
|
+
trustStatus: string;
|
|
25
|
+
currentHash: string;
|
|
26
|
+
isManaged: boolean;
|
|
27
|
+
pluginId: string | null;
|
|
22
28
|
}>;
|
|
23
29
|
|
|
24
30
|
type AppServerTransportOptions = Readonly<{
|
|
@@ -27,6 +33,8 @@ type AppServerTransportOptions = Readonly<{
|
|
|
27
33
|
cwd?: string;
|
|
28
34
|
env?: NodeJS.ProcessEnv;
|
|
29
35
|
timeoutMs?: number;
|
|
36
|
+
rolloutReadAttempts?: number;
|
|
37
|
+
rolloutRetryMs?: number;
|
|
30
38
|
}>;
|
|
31
39
|
|
|
32
40
|
type CodexRuntimeIdentity = Readonly<{
|
|
@@ -37,10 +45,12 @@ type CodexRuntimeIdentity = Readonly<{
|
|
|
37
45
|
|
|
38
46
|
const LIFECYCLE_HOOKS = Object.freeze([
|
|
39
47
|
Object.freeze({ event: 'PreToolUse', matcher: '^collaborationspawn_agent$', phase: 'pre-tool' }),
|
|
40
|
-
Object.freeze({ event: 'PostToolUse', matcher: '
|
|
41
|
-
Object.freeze({ event: 'SubagentStart', matcher: '
|
|
42
|
-
Object.freeze({ event: 'SubagentStop', matcher: '
|
|
48
|
+
Object.freeze({ event: 'PostToolUse', matcher: '', phase: 'post-tool' }),
|
|
49
|
+
Object.freeze({ event: 'SubagentStart', matcher: '', phase: 'subagent-start' }),
|
|
50
|
+
Object.freeze({ event: 'SubagentStop', matcher: '', phase: 'subagent-stop' })
|
|
43
51
|
]);
|
|
52
|
+
const DEFAULT_ROLLOUT_READ_ATTEMPTS = 81;
|
|
53
|
+
const DEFAULT_ROLLOUT_RETRY_MS = 100;
|
|
44
54
|
|
|
45
55
|
function object(value: unknown): JsonObject | null {
|
|
46
56
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
@@ -52,6 +62,65 @@ function nonEmpty(value: unknown): string | null {
|
|
|
52
62
|
return typeof value === 'string' && value.trim() ? value : null;
|
|
53
63
|
}
|
|
54
64
|
|
|
65
|
+
function resolveCodexSpawnedChild(
|
|
66
|
+
transcriptPath: string,
|
|
67
|
+
expected: Readonly<{
|
|
68
|
+
sessionId: string;
|
|
69
|
+
toolUseId: string;
|
|
70
|
+
nativeAgent: string;
|
|
71
|
+
taskName: string;
|
|
72
|
+
requestedModel?: string;
|
|
73
|
+
requestedReasoningEffort?: string;
|
|
74
|
+
}>
|
|
75
|
+
): string {
|
|
76
|
+
const name = path.basename(transcriptPath);
|
|
77
|
+
if (!transcriptPath || (!name.endsWith(`-${expected.sessionId}.jsonl`) && name !== `${expected.sessionId}.jsonl`)) {
|
|
78
|
+
throw new Error('Codex parent rollout path is invalid');
|
|
79
|
+
}
|
|
80
|
+
const stat = fs.lstatSync(transcriptPath);
|
|
81
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('Codex parent rollout is not a regular file');
|
|
82
|
+
const maxBytes = 8 * 1024 * 1024;
|
|
83
|
+
const offset = Math.max(0, stat.size - maxBytes);
|
|
84
|
+
const buffer = Buffer.alloc(stat.size - offset);
|
|
85
|
+
const descriptor = fs.openSync(transcriptPath, 'r');
|
|
86
|
+
try {
|
|
87
|
+
fs.readSync(descriptor, buffer, 0, buffer.length, offset);
|
|
88
|
+
} finally {
|
|
89
|
+
fs.closeSync(descriptor);
|
|
90
|
+
}
|
|
91
|
+
let text = buffer.toString('utf8');
|
|
92
|
+
if (offset) text = text.slice(text.indexOf('\n') + 1);
|
|
93
|
+
const records = text.split(/\r?\n/).filter(Boolean).map((line) => object(JSON.parse(line))).filter(Boolean);
|
|
94
|
+
const calls = records.filter((record) => {
|
|
95
|
+
const payload = object(record?.payload);
|
|
96
|
+
return record?.type === 'response_item'
|
|
97
|
+
&& payload?.type === 'function_call'
|
|
98
|
+
&& payload.call_id === expected.toolUseId
|
|
99
|
+
&& payload.namespace === 'collaboration'
|
|
100
|
+
&& payload.name === 'spawn_agent';
|
|
101
|
+
});
|
|
102
|
+
if (calls.length !== 1) throw new Error('Codex parent rollout spawn call was not found uniquely');
|
|
103
|
+
const call = object(calls[0]?.payload);
|
|
104
|
+
const args = object(JSON.parse(nonEmpty(call?.arguments) ?? '{}'));
|
|
105
|
+
if (
|
|
106
|
+
args?.agent_type !== expected.nativeAgent
|
|
107
|
+
|| args.task_name !== expected.taskName
|
|
108
|
+
|| (expected.requestedModel && args.model !== expected.requestedModel)
|
|
109
|
+
|| (expected.requestedReasoningEffort && args.reasoning_effort !== expected.requestedReasoningEffort)
|
|
110
|
+
) throw new Error('Codex parent rollout spawn identity does not match the hook event');
|
|
111
|
+
const activities = records.filter((record) => {
|
|
112
|
+
const payload = object(record?.payload);
|
|
113
|
+
return record?.type === 'event_msg'
|
|
114
|
+
&& payload?.type === 'sub_agent_activity'
|
|
115
|
+
&& payload.event_id === expected.toolUseId
|
|
116
|
+
&& payload.kind === 'started'
|
|
117
|
+
&& payload.agent_path === `/root/${expected.taskName}`
|
|
118
|
+
&& nonEmpty(payload.agent_thread_id);
|
|
119
|
+
});
|
|
120
|
+
if (activities.length !== 1) throw new Error('Codex parent rollout child activity was not found uniquely');
|
|
121
|
+
return nonEmpty(object(activities[0]?.payload)?.agent_thread_id)!;
|
|
122
|
+
}
|
|
123
|
+
|
|
55
124
|
function validateCodexLifecycleHookConfig(value: unknown): void {
|
|
56
125
|
const hooks = object(object(value)?.hooks);
|
|
57
126
|
const valid = hooks && LIFECYCLE_HOOKS.every(({ event, matcher, phase }) => {
|
|
@@ -63,7 +132,7 @@ function validateCodexLifecycleHookConfig(value: unknown): void {
|
|
|
63
132
|
return entry.hooks.some((hookValue) => {
|
|
64
133
|
const hook = object(hookValue);
|
|
65
134
|
return hook?.type === 'command'
|
|
66
|
-
&& hook.timeout ===
|
|
135
|
+
&& hook.timeout === 15
|
|
67
136
|
&& hook.command === `node "$(git rev-parse --show-toplevel)/.agents/hooks/lifecycle-delegation.js" --client codex --event ${phase}`;
|
|
68
137
|
});
|
|
69
138
|
});
|
|
@@ -71,7 +140,11 @@ function validateCodexLifecycleHookConfig(value: unknown): void {
|
|
|
71
140
|
if (!valid) throw new Error('CODEX_PREFLIGHT_HOOKS_INVALID: lifecycle hooks are invalid');
|
|
72
141
|
}
|
|
73
142
|
|
|
74
|
-
function parseCodexHooksList(
|
|
143
|
+
function parseCodexHooksList(
|
|
144
|
+
value: unknown,
|
|
145
|
+
repoRoot: string,
|
|
146
|
+
expectedSource: 'direct-host' | 'isolated-user' = 'direct-host'
|
|
147
|
+
): readonly DiscoveredHook[] {
|
|
75
148
|
const root = object(value);
|
|
76
149
|
const entries = Array.isArray(root?.data) ? root.data : [];
|
|
77
150
|
const entry = entries.map(object).find((candidate) => candidate?.cwd === repoRoot);
|
|
@@ -84,13 +157,28 @@ function parseCodexHooksList(value: unknown, repoRoot: string): readonly Discove
|
|
|
84
157
|
eventName: String(hook.eventName ?? ''),
|
|
85
158
|
matcher: typeof hook.matcher === 'string' ? hook.matcher : null,
|
|
86
159
|
command: String(hook.command ?? ''),
|
|
87
|
-
enabled: hook.enabled === true
|
|
160
|
+
enabled: hook.enabled === true,
|
|
161
|
+
source: String(hook.source ?? ''),
|
|
162
|
+
sourcePathDigest: typeof hook.sourcePath === 'string' && path.isAbsolute(hook.sourcePath)
|
|
163
|
+
? crypto.createHash('sha256').update(hook.sourcePath).digest('hex')
|
|
164
|
+
: '',
|
|
165
|
+
trustStatus: String(hook.trustStatus ?? ''),
|
|
166
|
+
currentHash: String(hook.currentHash ?? ''),
|
|
167
|
+
isManaged: hook.isManaged === true,
|
|
168
|
+
pluginId: typeof hook.pluginId === 'string' ? hook.pluginId : null
|
|
88
169
|
}));
|
|
89
170
|
const valid = LIFECYCLE_HOOKS.every(({ event, matcher, phase }) => hooks.some((hook) =>
|
|
90
171
|
hook.eventName === event.replace(/^./, (character) => character.toLowerCase())
|
|
91
172
|
&& hook.matcher === matcher
|
|
92
173
|
&& hook.command === `node "$(git rev-parse --show-toplevel)/.agents/hooks/lifecycle-delegation.js" --client codex --event ${phase}`
|
|
93
174
|
&& hook.enabled
|
|
175
|
+
&& hook.currentHash.length > 0
|
|
176
|
+
&& hook.sourcePathDigest.length === 64
|
|
177
|
+
&& hook.pluginId === null
|
|
178
|
+
&& (expectedSource === 'isolated-user'
|
|
179
|
+
? hook.source === 'user' && hook.trustStatus !== 'modified'
|
|
180
|
+
: (hook.source === 'project' && hook.trustStatus === 'trusted')
|
|
181
|
+
|| hook.isManaged && hook.trustStatus === 'managed')
|
|
94
182
|
));
|
|
95
183
|
if (!valid) throw new Error('CODEX_PREFLIGHT_HOOKS_NOT_LOADED: lifecycle hooks are not loaded for this workspace');
|
|
96
184
|
return Object.freeze(hooks);
|
|
@@ -120,26 +208,38 @@ function hasCodexRuntimeLiveness(
|
|
|
120
208
|
});
|
|
121
209
|
}
|
|
122
210
|
|
|
123
|
-
function parseCodexThreadResolution(readValue: unknown,
|
|
211
|
+
function parseCodexThreadResolution(readValue: unknown, rolloutRecords: readonly unknown[]): ThreadResolution {
|
|
124
212
|
const readRoot = object(readValue);
|
|
125
|
-
const resumeRoot = object(resumeValue);
|
|
126
213
|
const thread = object(readRoot?.thread);
|
|
127
|
-
const resumedThread = object(resumeRoot?.thread);
|
|
128
214
|
const source = object(thread?.source);
|
|
129
215
|
const subAgent = object(source?.subAgent);
|
|
130
216
|
const threadSpawn = object(subAgent?.thread_spawn);
|
|
217
|
+
const sessionMeta = rolloutRecords
|
|
218
|
+
.map(object)
|
|
219
|
+
.find((record) => record?.type === 'session_meta');
|
|
220
|
+
const sessionPayload = object(sessionMeta?.payload);
|
|
221
|
+
const turnContext = rolloutRecords
|
|
222
|
+
.map(object)
|
|
223
|
+
.filter((record) => record?.type === 'turn_context')
|
|
224
|
+
.at(-1);
|
|
225
|
+
const turnPayload = object(turnContext?.payload);
|
|
131
226
|
const childThreadId = nonEmpty(thread?.id);
|
|
132
227
|
const parentThreadId = nonEmpty(thread?.parentThreadId);
|
|
133
228
|
const sourceParentThreadId = nonEmpty(threadSpawn?.parent_thread_id);
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
const
|
|
229
|
+
const rolloutChildThreadId = nonEmpty(sessionPayload?.id);
|
|
230
|
+
const rolloutParentThreadId = nonEmpty(sessionPayload?.parent_thread_id);
|
|
231
|
+
const agentRole = nonEmpty(sessionPayload?.agent_role);
|
|
232
|
+
const model = nonEmpty(turnPayload?.model);
|
|
233
|
+
const reasoningEffort = nonEmpty(turnPayload?.effort);
|
|
137
234
|
const forkedFromId = thread?.forkedFromId;
|
|
138
235
|
if (
|
|
139
236
|
!childThreadId
|
|
140
|
-
||
|
|
237
|
+
|| rolloutChildThreadId !== childThreadId
|
|
141
238
|
|| !parentThreadId
|
|
142
239
|
|| !sourceParentThreadId
|
|
240
|
+
|| rolloutParentThreadId !== parentThreadId
|
|
241
|
+
|| rolloutParentThreadId !== sourceParentThreadId
|
|
242
|
+
|| !/^agent-infra-lifecycle-(executor|reviewer)$/.test(agentRole ?? '')
|
|
143
243
|
|| !model
|
|
144
244
|
|| !reasoningEffort
|
|
145
245
|
|| (forkedFromId !== null && typeof forkedFromId !== 'string')
|
|
@@ -152,7 +252,8 @@ function parseCodexThreadResolution(readValue: unknown, resumeValue: unknown): T
|
|
|
152
252
|
childThreadId,
|
|
153
253
|
parentThreadId,
|
|
154
254
|
forkedFromId,
|
|
155
|
-
sourceParentThreadId
|
|
255
|
+
sourceParentThreadId,
|
|
256
|
+
nativeAgent: agentRole!
|
|
156
257
|
}),
|
|
157
258
|
settings: Object.freeze({
|
|
158
259
|
type: 'app-settings',
|
|
@@ -163,6 +264,49 @@ function parseCodexThreadResolution(readValue: unknown, resumeValue: unknown): T
|
|
|
163
264
|
});
|
|
164
265
|
}
|
|
165
266
|
|
|
267
|
+
function readCodexRolloutRecords(readValue: unknown, childThreadId: string): readonly unknown[] {
|
|
268
|
+
const thread = object(object(readValue)?.thread);
|
|
269
|
+
const rolloutPath = nonEmpty(thread?.path);
|
|
270
|
+
const rolloutName = rolloutPath ? path.basename(rolloutPath) : '';
|
|
271
|
+
if (!rolloutPath || (rolloutName !== `${childThreadId}.jsonl` && !rolloutName.endsWith(`-${childThreadId}.jsonl`))) {
|
|
272
|
+
throw new Error('Codex App Server rollout path is invalid');
|
|
273
|
+
}
|
|
274
|
+
const stat = fs.statSync(rolloutPath);
|
|
275
|
+
if (!stat.isFile()) throw new Error('Codex App Server rollout path is not a regular file');
|
|
276
|
+
const maxBytes = 1024 * 1024;
|
|
277
|
+
const bytes = Math.min(stat.size, maxBytes);
|
|
278
|
+
const descriptor = fs.openSync(rolloutPath, 'r');
|
|
279
|
+
try {
|
|
280
|
+
const buffer = Buffer.alloc(bytes);
|
|
281
|
+
fs.readSync(descriptor, buffer, 0, bytes, 0);
|
|
282
|
+
const text = buffer.toString('utf8');
|
|
283
|
+
const completeText = stat.size <= maxBytes ? text : text.slice(0, text.lastIndexOf('\n'));
|
|
284
|
+
return Object.freeze(completeText.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line)));
|
|
285
|
+
} finally {
|
|
286
|
+
fs.closeSync(descriptor);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function resolveCodexRolloutRecords(
|
|
291
|
+
readValue: unknown,
|
|
292
|
+
childThreadId: string,
|
|
293
|
+
attempts: number,
|
|
294
|
+
retryMs: number
|
|
295
|
+
): Promise<readonly unknown[]> {
|
|
296
|
+
let lastError: unknown;
|
|
297
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
298
|
+
try {
|
|
299
|
+
const records = readCodexRolloutRecords(readValue, childThreadId);
|
|
300
|
+
parseCodexThreadResolution(readValue, records);
|
|
301
|
+
return records;
|
|
302
|
+
} catch (error) {
|
|
303
|
+
lastError = error;
|
|
304
|
+
if (attempt + 1 < attempts) await new Promise((resolve) => setTimeout(resolve, retryMs));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
throw lastError;
|
|
308
|
+
}
|
|
309
|
+
|
|
166
310
|
function parseCodexTurnCompleted(value: unknown): Extract<CodexLifecycleEvent, { type: 'app-terminal' }> {
|
|
167
311
|
const root = object(value);
|
|
168
312
|
const turn = object(root?.turn);
|
|
@@ -333,12 +477,17 @@ async function resolveCodexThread(
|
|
|
333
477
|
const readResult = object(await transport.request('thread/read', { threadId: childThreadId, includeTurns: false }));
|
|
334
478
|
const readThread = object(readResult?.thread);
|
|
335
479
|
if (nonEmpty(readThread?.id) !== childThreadId) throw new Error('Codex App Server returned the wrong child thread');
|
|
336
|
-
const resumeResult = await transport.request('thread/resume', { threadId: childThreadId });
|
|
337
480
|
let resolution: ThreadResolution;
|
|
338
481
|
try {
|
|
339
|
-
|
|
482
|
+
const rolloutRecords = await resolveCodexRolloutRecords(
|
|
483
|
+
readResult,
|
|
484
|
+
childThreadId,
|
|
485
|
+
options.rolloutReadAttempts ?? DEFAULT_ROLLOUT_READ_ATTEMPTS,
|
|
486
|
+
options.rolloutRetryMs ?? DEFAULT_ROLLOUT_RETRY_MS
|
|
487
|
+
);
|
|
488
|
+
resolution = parseCodexThreadResolution(readResult, rolloutRecords);
|
|
340
489
|
} catch {
|
|
341
|
-
throw new Error('Codex App Server
|
|
490
|
+
throw new Error('Codex App Server rollout metadata is unavailable');
|
|
342
491
|
}
|
|
343
492
|
const reroutes = transport.notifications
|
|
344
493
|
.filter((entry) => entry.method === 'model/rerouted')
|
|
@@ -368,6 +517,8 @@ async function resolveCodexTerminal(
|
|
|
368
517
|
if (nonEmpty(thread?.id) !== childThreadId) throw new Error('Codex App Server returned the wrong child thread');
|
|
369
518
|
const turns = Array.isArray(thread?.turns) ? thread.turns : [];
|
|
370
519
|
const turn = turns.at(-1);
|
|
520
|
+
const turnValue = object(turn);
|
|
521
|
+
if (!turnValue || turnValue.status === 'inProgress') throw new Error('CODEX_TURN_NOT_TERMINAL');
|
|
371
522
|
return parseCodexTurnCompleted({ threadId: childThreadId, turn });
|
|
372
523
|
} finally {
|
|
373
524
|
transport.close();
|
|
@@ -415,13 +566,45 @@ async function preflightCodexLifecycleEvidence(
|
|
|
415
566
|
let discoveredHooks: readonly DiscoveredHook[];
|
|
416
567
|
try {
|
|
417
568
|
await transport.initialize();
|
|
418
|
-
discoveredHooks = parseCodexHooksList(
|
|
569
|
+
discoveredHooks = parseCodexHooksList(
|
|
570
|
+
await transport.request('hooks/list', { cwds: [repoRoot] }),
|
|
571
|
+
repoRoot,
|
|
572
|
+
process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT ? 'isolated-user' : 'direct-host'
|
|
573
|
+
);
|
|
419
574
|
} finally { transport.close(); }
|
|
575
|
+
const lifecycleHooks = LIFECYCLE_HOOKS.map(({ event, matcher, phase }) => discoveredHooks.filter((hook) =>
|
|
576
|
+
hook.eventName === event.replace(/^./, (character) => character.toLowerCase())
|
|
577
|
+
&& hook.matcher === matcher
|
|
578
|
+
&& hook.command === `node "$(git rev-parse --show-toplevel)/.agents/hooks/lifecycle-delegation.js" --client codex --event ${phase}`
|
|
579
|
+
));
|
|
580
|
+
if (lifecycleHooks.some((matches) => matches.length !== 1)) {
|
|
581
|
+
throw new Error('CODEX_PREFLIGHT_HOOK_SOURCE_AMBIGUOUS: lifecycle hook source is not unique');
|
|
582
|
+
}
|
|
583
|
+
const matchedHooks = lifecycleHooks.flat();
|
|
584
|
+
const sources = new Set(matchedHooks.map((hook) => hook.isManaged ? 'managed' : hook.source));
|
|
585
|
+
const sourcePathDigests = new Set(matchedHooks.map((hook) => hook.sourcePathDigest));
|
|
586
|
+
if (sources.size !== 1 || sourcePathDigests.size !== 1) {
|
|
587
|
+
throw new Error('CODEX_PREFLIGHT_HOOK_SOURCE_AMBIGUOUS: lifecycle hooks do not share one exact source');
|
|
588
|
+
}
|
|
589
|
+
const source = [...sources][0];
|
|
590
|
+
const hookSource = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT
|
|
591
|
+
? 'isolated-user'
|
|
592
|
+
: source === 'managed' ? 'managed' : 'project';
|
|
420
593
|
return Object.freeze({
|
|
421
594
|
cliVersion: match[1],
|
|
422
595
|
hookDefinitionHash,
|
|
423
596
|
staticReady: true,
|
|
424
597
|
discoveredHooks,
|
|
598
|
+
hookProvenance: Object.freeze({
|
|
599
|
+
hookSource,
|
|
600
|
+
hookSourcePathDigest: [...sourcePathDigests][0]!,
|
|
601
|
+
hookSourceHash: crypto.createHash('sha256').update(JSON.stringify(matchedHooks.map((hook) => ({
|
|
602
|
+
eventName: hook.eventName,
|
|
603
|
+
matcher: hook.matcher,
|
|
604
|
+
command: hook.command,
|
|
605
|
+
currentHash: hook.currentHash
|
|
606
|
+
})))).digest('hex')
|
|
607
|
+
}),
|
|
425
608
|
runtimeLiveness,
|
|
426
609
|
diagnostics: transport.diagnostics
|
|
427
610
|
});
|
|
@@ -434,6 +617,7 @@ export {
|
|
|
434
617
|
parseCodexHooksList,
|
|
435
618
|
parseCodexThreadResolution,
|
|
436
619
|
parseCodexTurnCompleted,
|
|
620
|
+
resolveCodexSpawnedChild,
|
|
437
621
|
preflightCodexLifecycleEvidence,
|
|
438
622
|
resolveCodexTerminal,
|
|
439
623
|
resolveCodexThread,
|