@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.
Files changed (172) hide show
  1. package/dist/bin/internal-cli.js +8 -3
  2. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +171 -20
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +244 -0
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -2
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +370 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +68 -8
  8. package/dist/lib/agent-clients/adapters/codex.js +1 -1
  9. package/dist/lib/defaults.json +10 -2
  10. package/dist/lib/git/workflow.js +30 -4
  11. package/dist/lib/internal/codex-lifecycle.js +169 -3
  12. package/dist/lib/internal/codex-sandbox-controller.js +106 -0
  13. package/dist/lib/internal/release-workflow.js +108 -11
  14. package/dist/lib/internal/sandbox-control.js +43 -6
  15. package/dist/lib/internal/task-create.js +12 -4
  16. package/dist/lib/internal/task-orchestration.js +40 -7
  17. package/dist/lib/platform/context.js +6 -4
  18. package/dist/lib/platform/github-client.js +55 -2
  19. package/dist/lib/platform/issue-comments.js +2 -1
  20. package/dist/lib/process-data/checkpoint.js +319 -0
  21. package/dist/lib/process-data/index.js +128 -60
  22. package/dist/lib/process-data/materialize.js +84 -0
  23. package/dist/lib/process-data/normalize.js +34 -1
  24. package/dist/lib/process-data/reconcile.js +55 -1
  25. package/dist/lib/process-data/sources.js +422 -1
  26. package/dist/lib/process-data/store.js +341 -11
  27. package/dist/lib/release/workflow.js +29 -21
  28. package/dist/lib/sandbox/commands/create.js +111 -60
  29. package/dist/lib/sandbox/commands/prune.js +40 -16
  30. package/dist/lib/sandbox/commands/rm.js +372 -94
  31. package/dist/lib/sandbox/commands/start.js +4 -3
  32. package/dist/lib/sandbox/control/client.js +138 -22
  33. package/dist/lib/sandbox/control/container-identity.js +64 -0
  34. package/dist/lib/sandbox/control/executor.js +142 -0
  35. package/dist/lib/sandbox/control/lifecycle.js +557 -0
  36. package/dist/lib/sandbox/control/protocol.js +69 -26
  37. package/dist/lib/sandbox/control/server.js +395 -99
  38. package/dist/lib/sandbox/control/state.js +191 -0
  39. package/dist/lib/sandbox/control/timing.js +8 -0
  40. package/dist/lib/sandbox/index.js +2 -2
  41. package/dist/lib/sandbox/managed-fs.js +14 -2
  42. package/dist/lib/sandbox/mounts.js +22 -19
  43. package/dist/lib/sandbox/recovery.js +284 -50
  44. package/dist/lib/sandbox/workspace-view.js +90 -10
  45. package/dist/lib/sandbox/worktree-safety.js +196 -0
  46. package/dist/lib/server/daemon.js +1 -1
  47. package/dist/lib/server/process-control.js +17 -1
  48. package/dist/lib/server/process-state.js +57 -20
  49. package/dist/lib/task/artifact-lifecycle.js +87 -15
  50. package/dist/lib/task/artifact-receipts.js +128 -0
  51. package/dist/lib/task/codex-orchestration.js +236 -0
  52. package/dist/lib/task/commands/status.js +3 -0
  53. package/dist/lib/task/commands/validate.js +289 -0
  54. package/dist/lib/task/commit-finalization.js +36 -9
  55. package/dist/lib/task/create.js +10 -2
  56. package/dist/lib/task/delegation-receipts.js +109 -2
  57. package/dist/lib/task/events.js +149 -6
  58. package/dist/lib/task/finalization-artifacts.js +2 -1
  59. package/dist/lib/task/implementation-inputs.js +1 -7
  60. package/dist/lib/task/index.js +9 -0
  61. package/dist/lib/task/ledger.js +13 -1
  62. package/dist/lib/task/orchestration.js +365 -15
  63. package/dist/lib/task/sections.js +5 -1
  64. package/dist/lib/task/task-execution-lock.js +5 -4
  65. package/dist/lib/task/verification-engine.js +78 -14
  66. package/dist/lib/task/verification.js +1 -0
  67. package/dist/lifecycle-build-manifest.json +6 -0
  68. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +202 -18
  69. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
  70. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
  71. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -3
  72. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
  73. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
  74. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +83 -6
  75. package/lib/agent-clients/adapters/codex.ts +1 -1
  76. package/lib/defaults.json +10 -2
  77. package/lib/git/workflow.ts +25 -3
  78. package/lib/internal/codex-lifecycle.ts +175 -4
  79. package/lib/internal/codex-sandbox-controller.ts +109 -0
  80. package/lib/internal/release-workflow.ts +107 -11
  81. package/lib/internal/sandbox-control.ts +38 -6
  82. package/lib/internal/task-create.ts +16 -4
  83. package/lib/internal/task-orchestration.ts +34 -7
  84. package/lib/platform/context.ts +8 -5
  85. package/lib/platform/github-client.ts +61 -3
  86. package/lib/platform/issue-comments.ts +2 -1
  87. package/lib/process-data/checkpoint.ts +335 -0
  88. package/lib/process-data/index.ts +112 -60
  89. package/lib/process-data/materialize.ts +83 -0
  90. package/lib/process-data/normalize.ts +29 -1
  91. package/lib/process-data/reconcile.ts +64 -2
  92. package/lib/process-data/sources.ts +449 -2
  93. package/lib/process-data/store.ts +304 -11
  94. package/lib/process-data/types.ts +124 -2
  95. package/lib/release/workflow.ts +69 -20
  96. package/lib/sandbox/commands/create.ts +70 -25
  97. package/lib/sandbox/commands/prune.ts +53 -15
  98. package/lib/sandbox/commands/rm.ts +423 -108
  99. package/lib/sandbox/commands/start.ts +4 -3
  100. package/lib/sandbox/control/client.ts +152 -28
  101. package/lib/sandbox/control/container-identity.ts +93 -0
  102. package/lib/sandbox/control/executor.ts +167 -0
  103. package/lib/sandbox/control/lifecycle.ts +573 -0
  104. package/lib/sandbox/control/protocol.ts +121 -68
  105. package/lib/sandbox/control/server.ts +398 -109
  106. package/lib/sandbox/control/state.ts +213 -0
  107. package/lib/sandbox/control/timing.ts +12 -0
  108. package/lib/sandbox/index.ts +2 -2
  109. package/lib/sandbox/managed-fs.ts +17 -2
  110. package/lib/sandbox/mounts.ts +16 -8
  111. package/lib/sandbox/recovery.ts +335 -65
  112. package/lib/sandbox/workspace-view.ts +102 -11
  113. package/lib/sandbox/worktree-safety.ts +234 -0
  114. package/lib/server/daemon.ts +1 -1
  115. package/lib/server/process-control.ts +24 -3
  116. package/lib/server/process-state.ts +82 -27
  117. package/lib/task/artifact-lifecycle.ts +78 -14
  118. package/lib/task/artifact-receipts.ts +161 -0
  119. package/lib/task/codex-orchestration.ts +319 -0
  120. package/lib/task/commands/status.ts +2 -0
  121. package/lib/task/commands/validate.ts +282 -0
  122. package/lib/task/commit-finalization.ts +52 -9
  123. package/lib/task/create.ts +11 -2
  124. package/lib/task/delegation-receipts.ts +208 -7
  125. package/lib/task/events.ts +131 -5
  126. package/lib/task/finalization-artifacts.ts +2 -1
  127. package/lib/task/implementation-inputs.ts +1 -5
  128. package/lib/task/index.ts +8 -0
  129. package/lib/task/ledger.ts +16 -1
  130. package/lib/task/orchestration.ts +552 -18
  131. package/lib/task/sections.ts +6 -2
  132. package/lib/task/task-execution-lock.ts +11 -9
  133. package/lib/task/verification-engine.ts +93 -24
  134. package/lib/task/verification.ts +2 -1
  135. package/package.json +1 -1
  136. package/templates/.agents/README.en.md +7 -0
  137. package/templates/.agents/README.zh-CN.md +5 -0
  138. package/templates/.agents/hooks/lifecycle-delegation.js +72 -12
  139. package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
  140. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
  141. package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
  142. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
  143. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  144. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
  145. package/templates/.agents/rules/review-handshake.en.md +3 -1
  146. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
  147. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
  148. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
  149. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  150. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  151. package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
  152. package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
  153. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
  154. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
  155. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +25 -0
  156. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +25 -0
  157. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
  158. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
  159. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +69 -0
  160. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +69 -0
  161. package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
  162. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
  163. package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -3
  164. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -3
  165. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
  166. package/templates/.claude/commands/run-manual-validation.en.md +9 -0
  167. package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
  168. package/templates/.codex/hooks.json +8 -8
  169. package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
  170. package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
  171. package/templates/.agents/workspace/README.en.md +0 -26
  172. package/templates/.agents/workspace/README.zh-CN.md +0 -26
@@ -0,0 +1,128 @@
1
+ import fs from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+ import { extractSection, findSectionHeading, parseTable } from './sections.js';
4
+ const RECEIPT_SECTION_ALIASES = ['产物生命周期收据', 'Artifact Lifecycle Receipts'];
5
+ const RECEIPT_COLUMNS = ['event', 'output', 'input', 'input_sha256', 'completed_at'];
6
+ const RECEIPT_EVENTS = new Set([
7
+ 'review-analysis.completed',
8
+ 'review-plan.completed',
9
+ 'review-code.completed',
10
+ 'code.completed'
11
+ ]);
12
+ const SHA256_RE = /^[a-f0-9]{64}$/;
13
+ const ARTIFACT_NAME_RE = /^(analysis|review-analysis|plan|review-plan|code|review-code|manual-validation|validation-run|pr-review)(?:-r([2-9]|[1-9]\d+))?\.md$/;
14
+ const RECEIPT_SHAPES = {
15
+ 'review-analysis.completed': { output: 'review-analysis', input: 'analysis' },
16
+ 'review-plan.completed': { output: 'review-plan', input: 'plan' },
17
+ 'review-code.completed': { output: 'review-code', input: 'code' },
18
+ 'code.completed': { output: 'code', input: 'plan' }
19
+ };
20
+ class ArtifactReceiptError extends Error {
21
+ code = 'ARTIFACT_RECEIPT_INVALID';
22
+ constructor(message) {
23
+ super(message);
24
+ this.name = 'ArtifactReceiptError';
25
+ }
26
+ }
27
+ function sha256Bytes(value) {
28
+ return createHash('sha256').update(value).digest('hex');
29
+ }
30
+ function sha256File(filePath) {
31
+ return sha256Bytes(fs.readFileSync(filePath));
32
+ }
33
+ function parseReceiptArtifactName(name) {
34
+ const match = ARTIFACT_NAME_RE.exec(name);
35
+ if (!match)
36
+ return null;
37
+ const roundText = match[2];
38
+ if (roundText !== undefined) {
39
+ const round = Number(roundText);
40
+ if (!Number.isSafeInteger(round) || String(round) !== roundText)
41
+ return null;
42
+ }
43
+ return { family: match[1] };
44
+ }
45
+ function validateReceiptShape(event, output, input) {
46
+ const outputIdentity = parseReceiptArtifactName(output);
47
+ const inputIdentity = parseReceiptArtifactName(input);
48
+ if (!outputIdentity || !inputIdentity)
49
+ throw new ArtifactReceiptError(`receipt artifact identity is invalid: ${output} -> ${input}`);
50
+ const shape = RECEIPT_SHAPES[event];
51
+ if (outputIdentity.family !== shape.output || inputIdentity.family !== shape.input) {
52
+ throw new ArtifactReceiptError(`receipt event '${event}' does not match ${output} -> ${input}`);
53
+ }
54
+ }
55
+ function parseArtifactReceipts(content) {
56
+ const section = extractSection(content, [...RECEIPT_SECTION_ALIASES]);
57
+ if (!section)
58
+ return { present: false, rows: [] };
59
+ let table;
60
+ try {
61
+ table = parseTable(content, {
62
+ sectionAliases: [...RECEIPT_SECTION_ALIASES],
63
+ columns: [...RECEIPT_COLUMNS],
64
+ keyColumn: 'output'
65
+ });
66
+ }
67
+ catch (error) {
68
+ throw new ArtifactReceiptError(error instanceof Error ? error.message : String(error));
69
+ }
70
+ if (!table)
71
+ throw new ArtifactReceiptError('receipt section has no receipt table');
72
+ const rows = table.rows.map((row) => {
73
+ const event = row.values.event ?? '';
74
+ const output = row.values.output ?? '';
75
+ const input = row.values.input ?? '';
76
+ const inputSha256 = row.values.input_sha256 ?? '';
77
+ const completedAt = row.values.completed_at ?? '';
78
+ if (!RECEIPT_EVENTS.has(event))
79
+ throw new ArtifactReceiptError(`unknown receipt event '${event}'`);
80
+ if (!output || !input)
81
+ throw new ArtifactReceiptError('receipt output and input are required');
82
+ validateReceiptShape(event, output, input);
83
+ if (!SHA256_RE.test(inputSha256))
84
+ throw new ArtifactReceiptError(`receipt digest for '${output}' is invalid`);
85
+ if (!completedAt || Number.isNaN(Date.parse(completedAt.replace(' ', 'T')))) {
86
+ throw new ArtifactReceiptError(`receipt completion time for '${output}' is invalid`);
87
+ }
88
+ return {
89
+ event: event,
90
+ output,
91
+ input,
92
+ inputSha256,
93
+ completedAt
94
+ };
95
+ });
96
+ return { present: true, rows };
97
+ }
98
+ function receiptForOutput(content, output) {
99
+ const parsed = parseArtifactReceipts(content);
100
+ return parsed.rows.find((row) => row.output === output) ?? null;
101
+ }
102
+ function upsertArtifactReceipt(content, receipt) {
103
+ if (!RECEIPT_EVENTS.has(receipt.event))
104
+ throw new ArtifactReceiptError(`unknown receipt event '${receipt.event}'`);
105
+ if (!receipt.output || !receipt.input)
106
+ throw new ArtifactReceiptError('receipt output and input are required');
107
+ validateReceiptShape(receipt.event, receipt.output, receipt.input);
108
+ if (!SHA256_RE.test(receipt.inputSha256))
109
+ throw new ArtifactReceiptError(`receipt digest for '${receipt.output}' is invalid`);
110
+ if (!receipt.completedAt || Number.isNaN(Date.parse(receipt.completedAt.replace(' ', 'T')))) {
111
+ throw new ArtifactReceiptError(`receipt completion time for '${receipt.output}' is invalid`);
112
+ }
113
+ const existing = parseArtifactReceipts(content).rows;
114
+ const previous = existing.find((row) => row.output === receipt.output);
115
+ if (previous && JSON.stringify(previous) !== JSON.stringify(receipt)) {
116
+ throw new ArtifactReceiptError(`receipt for '${receipt.output}' already exists with different evidence`);
117
+ }
118
+ const rows = previous ? existing : [...existing, receipt];
119
+ const heading = findSectionHeading(content, [...RECEIPT_SECTION_ALIASES]);
120
+ const body = [
121
+ `| ${RECEIPT_COLUMNS.join(' | ')} |`,
122
+ `| ${RECEIPT_COLUMNS.map(() => '---').join(' | ')} |`,
123
+ ...rows.map((row) => `| ${row.event} | ${row.output} | ${row.input} | ${row.inputSha256} | ${row.completedAt} |`)
124
+ ].join('\n');
125
+ return { aliases: [...RECEIPT_SECTION_ALIASES], heading, body };
126
+ }
127
+ export { ArtifactReceiptError, RECEIPT_SECTION_ALIASES, parseArtifactReceipts, receiptForOutput, sha256Bytes, sha256File, upsertArtifactReceipt };
128
+ //# sourceMappingURL=artifact-receipts.js.map
@@ -0,0 +1,236 @@
1
+ import fs from 'node:fs';
2
+ import { preflightCodexLifecycleEvidence, resolveCodexSpawnedChild, resolveCodexTerminal, resolveCodexThread } from '../agent-clients/adapters/codex-lifecycle/app-server.js';
3
+ import { createCodexLifecycleStore } from '../agent-clients/adapters/codex-lifecycle/store.js';
4
+ import { createCodexCapabilityStore } from '../agent-clients/adapters/codex-lifecycle/capability-store.js';
5
+ import { computeLifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.js';
6
+ import { verifyCodexSandboxControllerContext } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.js';
7
+ import { resolveTaskRef } from './resolve-ref.js';
8
+ import { activateMatchingOrchestrationDelegation, hasActivatableOrchestrationDelegation, hasSealableOrchestrationDelegation, pauseMatchingOrchestrationDelegation, prepareOrchestrationDelegation, reconcileMatchingOrchestrationDelegation, sealMatchingOrchestrationDelegationWithHostEvidence } from './orchestration.js';
9
+ function coreOptions(options) {
10
+ return { ...options.orchestrationOptions, repoRoot: options.repoRoot ?? options.orchestrationOptions?.repoRoot };
11
+ }
12
+ function bridgeFailure(code, message) {
13
+ return {
14
+ status: 'failed', changed: false, taskId: null, run: null, next: null,
15
+ error: { code, message }
16
+ };
17
+ }
18
+ function pauseBridge(code, message, options) {
19
+ const paused = pauseMatchingOrchestrationDelegation('codex', code, message, coreOptions(options));
20
+ return paused.error?.code === 'ORCHESTRATION_DELEGATION_MISSING'
21
+ ? bridgeFailure(code, message)
22
+ : paused;
23
+ }
24
+ function requiredStore(options) {
25
+ if (!options.store)
26
+ throw new Error('Codex lifecycle store is required for orchestration bridge events');
27
+ return options.store;
28
+ }
29
+ function controllerContext(taskId, repoRoot) {
30
+ const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
31
+ return contextPath
32
+ ? verifyCodexSandboxControllerContext(contextPath, taskId, { repoRoot })
33
+ : null;
34
+ }
35
+ async function prepareCodexOrchestrationDelegation(taskRef, input, options = {}) {
36
+ if (input.client !== 'codex')
37
+ return prepareOrchestrationDelegation(taskRef, input, coreOptions(options));
38
+ try {
39
+ const repoRoot = options.repoRoot ?? process.cwd();
40
+ const resolved = resolveTaskRef(taskRef, { repoRoot });
41
+ if (!resolved.ok)
42
+ return bridgeFailure(resolved.code, resolved.message);
43
+ const preflight = await (options.preflight ?? preflightCodexLifecycleEvidence)(repoRoot);
44
+ if (!input.capabilityToken) {
45
+ return bridgeFailure('ORCHESTRATION_CODEX_CAPABILITY_REQUIRED', 'Codex prepare requires a current-session capability token');
46
+ }
47
+ const buildIdentity = options.buildIdentity ?? computeLifecycleBuildIdentity(repoRoot);
48
+ const controller = controllerContext(resolved.taskId, repoRoot);
49
+ const capabilityStore = options.capabilityStore ?? createCodexCapabilityStore();
50
+ const consumed = capabilityStore.consume(input.capabilityToken, {
51
+ taskId: resolved.taskId,
52
+ hookDefinitionHash: preflight.hookDefinitionHash,
53
+ buildIdentity,
54
+ ...(controller ? { controller: {
55
+ instanceDigest: controller.controllerInstanceDigest,
56
+ controlGeneration: controller.controlGeneration
57
+ } } : {})
58
+ });
59
+ return prepareOrchestrationDelegation(taskRef, {
60
+ ...input,
61
+ lifecycleProvenance: {
62
+ ...consumed.buildIdentity,
63
+ hookDefinitionHash: preflight.hookDefinitionHash,
64
+ ...preflight.hookProvenance,
65
+ capabilitySessionId: consumed.sessionId,
66
+ capabilityTurnId: consumed.turnId,
67
+ capabilityToolUseId: consumed.toolUseId,
68
+ controllerInstanceDigest: controller?.controllerInstanceDigest ?? null,
69
+ controlGeneration: controller?.controlGeneration ?? null
70
+ }
71
+ }, coreOptions(options));
72
+ }
73
+ catch (error) {
74
+ return bridgeFailure(error instanceof Error && error.name.startsWith('CODEX_CAPABILITY_')
75
+ ? error.name
76
+ : 'ORCHESTRATION_CLIENT_PREFLIGHT_FAILED', error instanceof Error ? error.message : String(error));
77
+ }
78
+ }
79
+ async function activateCodexOrchestrationDelegation(childThreadId, options = {}) {
80
+ try {
81
+ if (!hasActivatableOrchestrationDelegation('codex', childThreadId, coreOptions(options))) {
82
+ return bridgeFailure('ORCHESTRATION_DELEGATION_MISSING', 'No matching Codex delegation is active');
83
+ }
84
+ const store = requiredStore(options);
85
+ const resolved = await (options.resolveThread ?? resolveCodexThread)(childThreadId);
86
+ store.apply(resolved.resolution.thread);
87
+ for (const reroute of resolved.reroutes)
88
+ store.apply(reroute);
89
+ store.apply(resolved.resolution.settings);
90
+ const record = store.read(childThreadId);
91
+ const evidence = record.state.startEvidence;
92
+ if (record.state.status !== 'start-ready' || !evidence) {
93
+ return pauseBridge('ORCHESTRATION_CODEX_START_EVIDENCE_INVALID', 'Codex lifecycle start evidence is not ready', options);
94
+ }
95
+ const repoRoot = options.repoRoot ?? process.cwd();
96
+ const preflight = await (options.preflight ?? preflightCodexLifecycleEvidence)(repoRoot, {
97
+ sessionId: evidence.parentThreadId,
98
+ turnId: evidence.parentTurnId,
99
+ toolUseId: evidence.spawnToolUseId
100
+ });
101
+ const buildIdentity = options.buildIdentity ?? computeLifecycleBuildIdentity(repoRoot);
102
+ const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
103
+ const contextTaskId = contextPath
104
+ ? JSON.parse(fs.readFileSync(contextPath, 'utf8')).taskId
105
+ : null;
106
+ if (contextPath && typeof contextTaskId !== 'string') {
107
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
108
+ }
109
+ const controller = contextPath
110
+ ? verifyCodexSandboxControllerContext(contextPath, contextTaskId, { repoRoot })
111
+ : null;
112
+ return activateMatchingOrchestrationDelegation('codex', {
113
+ nativeAgent: evidence.nativeAgent,
114
+ childId: evidence.childThreadId,
115
+ parentId: evidence.parentThreadId,
116
+ spawnMode: evidence.spawnMode,
117
+ actualModel: evidence.actualModel.value,
118
+ actualReasoningEffort: evidence.actualReasoningEffort.value,
119
+ ...(evidence.modelFallbackReason ? { modelFallbackReason: evidence.modelFallbackReason } : {}),
120
+ ...(evidence.reasoningEffortFallbackReason
121
+ ? { reasoningEffortFallbackReason: evidence.reasoningEffortFallbackReason }
122
+ : {}),
123
+ hostEvidence: {
124
+ kind: 'codex-lifecycle-v2',
125
+ hookDefinitionHash: evidence.hookDefinitionHash,
126
+ startRevision: record.revision,
127
+ ...buildIdentity,
128
+ ...preflight.hookProvenance,
129
+ capabilitySessionId: evidence.parentThreadId,
130
+ capabilityTurnId: evidence.parentTurnId,
131
+ spawnToolUseId: evidence.spawnToolUseId,
132
+ spawnObservedAt: record.spawnObservedAt ?? undefined,
133
+ controllerInstanceDigest: controller?.controllerInstanceDigest ?? null,
134
+ controlGeneration: controller?.controlGeneration ?? null
135
+ }
136
+ }, coreOptions(options));
137
+ }
138
+ catch (error) {
139
+ return pauseBridge('ORCHESTRATION_CODEX_START_FAILED', error instanceof Error ? error.message : String(error), options);
140
+ }
141
+ }
142
+ async function activateCodexSpawnDelegation(spawn, options = {}) {
143
+ try {
144
+ const store = requiredStore(options);
145
+ const childThreadId = resolveCodexSpawnedChild(spawn.transcriptPath, spawn);
146
+ const resolved = await (options.resolveThread ?? resolveCodexThread)(childThreadId);
147
+ store.applyToSpawn(spawn, {
148
+ type: 'hook-child',
149
+ sessionId: spawn.sessionId,
150
+ turnId: spawn.turnId,
151
+ childThreadId,
152
+ parentThreadId: resolved.resolution.thread.parentThreadId,
153
+ nativeAgent: spawn.nativeAgent,
154
+ source: 'parent-rollout'
155
+ });
156
+ return activateCodexOrchestrationDelegation(childThreadId, {
157
+ ...options,
158
+ store,
159
+ resolveThread: async () => resolved
160
+ });
161
+ }
162
+ catch (error) {
163
+ return pauseBridge('ORCHESTRATION_CODEX_START_FAILED', error instanceof Error ? error.message : String(error), options);
164
+ }
165
+ }
166
+ async function sealCodexOrchestrationDelegation(childThreadId, options = {}) {
167
+ try {
168
+ if (!hasSealableOrchestrationDelegation('codex', childThreadId, coreOptions(options))) {
169
+ return bridgeFailure('ORCHESTRATION_DELEGATION_MISSING', 'No matching Codex delegation is active');
170
+ }
171
+ const store = requiredStore(options);
172
+ const existing = store.read(childThreadId);
173
+ if (!existing.consumer) {
174
+ store.apply(await (options.resolveTerminal ?? resolveCodexTerminal)(childThreadId));
175
+ }
176
+ const record = store.read(childThreadId);
177
+ const evidence = record.state.stopEvidence;
178
+ if (record.state.status !== 'stop-ready' || !evidence || !record.state.startEvidence) {
179
+ return pauseBridge('ORCHESTRATION_CODEX_STOP_EVIDENCE_INVALID', 'Codex lifecycle stop evidence is not ready', options);
180
+ }
181
+ return sealMatchingOrchestrationDelegationWithHostEvidence('codex', { nativeAgent: record.state.startEvidence.nativeAgent, childId: childThreadId }, (receipt) => {
182
+ const consumed = store.consume(childThreadId, receipt.id, receipt.hostEvidence?.hookDefinitionHash);
183
+ return {
184
+ stopRevision: consumed.revision,
185
+ consumer: consumed.consumer,
186
+ consumedAt: consumed.consumedAt
187
+ };
188
+ }, coreOptions(options));
189
+ }
190
+ catch (error) {
191
+ return pauseBridge('ORCHESTRATION_CODEX_STOP_FAILED', error instanceof Error ? error.message : String(error), options);
192
+ }
193
+ }
194
+ async function sealCodexParentDelegation(parentThreadId, options = {}) {
195
+ try {
196
+ const store = requiredStore(options);
197
+ const candidates = store.findByParent(parentThreadId);
198
+ if (!candidates.length)
199
+ return bridgeFailure('ORCHESTRATION_DELEGATION_MISSING', 'No matching Codex delegation is active');
200
+ for (const consumed of candidates.filter((record) => record.consumer)) {
201
+ const replayed = await sealCodexOrchestrationDelegation(consumed.state.startEvidence.childThreadId, options);
202
+ if (replayed.error?.code !== 'ORCHESTRATION_DELEGATION_MISSING')
203
+ return replayed;
204
+ }
205
+ const active = candidates.find((record) => !record.consumer);
206
+ if (!active)
207
+ return bridgeFailure('ORCHESTRATION_DELEGATION_MISSING', 'No matching Codex delegation is active');
208
+ const start = active.state.startEvidence;
209
+ const child = active.state.child;
210
+ if (!active.state.terminal) {
211
+ store.apply(await (options.resolveTerminal ?? resolveCodexTerminal)(start.childThreadId));
212
+ }
213
+ if (!store.read(start.childThreadId).state.stop) {
214
+ store.apply({
215
+ type: 'hook-stop',
216
+ sessionId: parentThreadId,
217
+ turnId: child.turnId,
218
+ childThreadId: start.childThreadId,
219
+ nativeAgent: start.nativeAgent,
220
+ source: 'parent-rollout'
221
+ });
222
+ }
223
+ return sealCodexOrchestrationDelegation(start.childThreadId, options);
224
+ }
225
+ catch (error) {
226
+ if (error instanceof Error && error.message === 'CODEX_TURN_NOT_TERMINAL') {
227
+ return bridgeFailure('ORCHESTRATION_DELEGATION_MISSING', 'The matching Codex child has not completed');
228
+ }
229
+ return pauseBridge('ORCHESTRATION_CODEX_STOP_FAILED', error instanceof Error ? error.message : String(error), options);
230
+ }
231
+ }
232
+ function reconcileCodexOrchestrationDelegation(childThreadId, options = {}) {
233
+ return reconcileMatchingOrchestrationDelegation('codex', childThreadId, coreOptions(options));
234
+ }
235
+ export { activateCodexOrchestrationDelegation, activateCodexSpawnDelegation, prepareCodexOrchestrationDelegation, reconcileCodexOrchestrationDelegation, sealCodexParentDelegation, sealCodexOrchestrationDelegation };
236
+ //# sourceMappingURL=codex-orchestration.js.map
@@ -59,6 +59,7 @@ const STAGE_ORDER = [
59
59
  'review-plan',
60
60
  'code',
61
61
  'review-code',
62
+ 'validation-run',
62
63
  'task',
63
64
  'other'
64
65
  ];
@@ -72,6 +73,8 @@ function stageOf(name) {
72
73
  return 'review-plan';
73
74
  if (stem.startsWith('review-code'))
74
75
  return 'review-code';
76
+ if (stem.startsWith('validation-run'))
77
+ return 'validation-run';
75
78
  if (stem.startsWith('analysis'))
76
79
  return 'analysis';
77
80
  if (stem.startsWith('plan'))
@@ -0,0 +1,289 @@
1
+ import { execFileSync, spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { randomUUID } from 'node:crypto';
6
+ import { loadConfig } from '../../sandbox/config.js';
7
+ import { detectEngine } from '../../sandbox/engine.js';
8
+ import { runEngine } from '../../sandbox/shell.js';
9
+ import { sandboxBranchLabel, sandboxLabel } from '../../sandbox/constants.js';
10
+ import { fetchSandboxRows, selectSandboxContainer } from '../../sandbox/commands/list-running.js';
11
+ import { containerNameCandidates } from '../../sandbox/constants.js';
12
+ import { resolveSandboxTarget } from '../../sandbox/workspace-identity.js';
13
+ import { sandboxControlPaths } from '../../sandbox/workspace-view.js';
14
+ import { appendSandboxControlAudit, readActiveLease, readSandboxControlStatus } from '../../sandbox/control/state.js';
15
+ import { SANDBOX_CONTROL_FUTURE_SKEW_MS, SANDBOX_CONTROL_STATUS_STALE_MS } from '../../sandbox/control/protocol.js';
16
+ import { getProcessStartTime } from '../../server/process-state.js';
17
+ import { assertGitWorktreeBinding } from '../../git/worktree-identity.js';
18
+ const USAGE = `Usage: ai task validate <branch | TASK-id | N> [--scope snapshot|inplace] [--timeout <ms>] [--format text|json] -- <command> [args...]`;
19
+ const MAX_TIMEOUT_MS = 60 * 60 * 1000;
20
+ const RECOVERY_GRACE_MS = 30_000;
21
+ export function parseValidateArgs(args) {
22
+ if (args.includes('--help') || args.includes('-h')) {
23
+ return { target: '', scope: 'snapshot', timeoutMs: 300_000, format: 'text', command: [], help: true };
24
+ }
25
+ const separator = args.indexOf('--');
26
+ if (separator < 0)
27
+ throw new Error(`${USAGE}\nA literal -- must separate validation options from the command.`);
28
+ const optionArgs = args.slice(0, separator);
29
+ const command = args.slice(separator + 1);
30
+ let scope = 'snapshot';
31
+ let timeoutMs = 300_000;
32
+ let format = 'text';
33
+ const positionals = [];
34
+ for (let index = 0; index < optionArgs.length; index += 1) {
35
+ const arg = optionArgs[index];
36
+ if (arg === '--scope') {
37
+ const value = optionArgs[++index];
38
+ if (value !== 'snapshot' && value !== 'inplace')
39
+ throw new Error('validate --scope must be snapshot or inplace');
40
+ scope = value;
41
+ }
42
+ else if (arg === '--timeout') {
43
+ const value = Number(optionArgs[++index]);
44
+ if (!Number.isSafeInteger(value) || value <= 0 || value > MAX_TIMEOUT_MS)
45
+ throw new Error(`validate --timeout must be 1..${MAX_TIMEOUT_MS}`);
46
+ timeoutMs = value;
47
+ }
48
+ else if (arg === '--format') {
49
+ const value = optionArgs[++index];
50
+ if (value !== 'text' && value !== 'json')
51
+ throw new Error('validate --format must be text or json');
52
+ format = value;
53
+ }
54
+ else if (arg.startsWith('-')) {
55
+ throw new Error(`Unknown option: ${arg}`);
56
+ }
57
+ else {
58
+ positionals.push(arg);
59
+ }
60
+ }
61
+ if (positionals.length !== 1 || command.length === 0)
62
+ throw new Error(USAGE);
63
+ return { target: positionals[0], scope, timeoutMs, format, command, help: false };
64
+ }
65
+ function runValidationCommand(command, cwd, scope, timeoutMs, json) {
66
+ const [file, ...args] = command;
67
+ const result = spawnSync(file, args, {
68
+ cwd,
69
+ env: { ...process.env, AGENT_INFRA_VALIDATION_SCOPE: scope },
70
+ encoding: 'utf8',
71
+ timeout: timeoutMs,
72
+ stdio: ['inherit', 'pipe', 'pipe'],
73
+ shell: process.platform === 'win32' && /\.(?:bat|cmd)$/i.test(file)
74
+ });
75
+ if (json) {
76
+ if (result.stdout)
77
+ process.stderr.write(result.stdout);
78
+ if (result.stderr)
79
+ process.stderr.write(result.stderr);
80
+ }
81
+ else {
82
+ if (result.stdout)
83
+ process.stdout.write(result.stdout);
84
+ if (result.stderr)
85
+ process.stderr.write(result.stderr);
86
+ }
87
+ return { exitCode: result.status ?? 1, signal: result.signal ?? null };
88
+ }
89
+ function git(root, args) {
90
+ return execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
91
+ }
92
+ function waitForState(statusDir, predicate, timeoutMs = 5_000) {
93
+ const deadline = Date.now() + timeoutMs;
94
+ while (Date.now() < deadline) {
95
+ try {
96
+ const status = readSandboxControlStatus(statusDir);
97
+ if (predicate(status))
98
+ return;
99
+ }
100
+ catch {
101
+ // Atomic status publication may be between renames.
102
+ }
103
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
104
+ }
105
+ throw new Error('SANDBOX_VALIDATION_BROKER_STATE_TIMEOUT');
106
+ }
107
+ function snapshotValidation(repoRoot, commit, options) {
108
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-infra-validation-'));
109
+ const worktree = path.join(tempRoot, 'worktree');
110
+ let cleanup = 'not-created';
111
+ try {
112
+ git(repoRoot, ['worktree', 'add', '--detach', worktree, commit]);
113
+ cleanup = 'pending';
114
+ return { ...runValidationCommand(options.command, worktree, 'snapshot', options.timeoutMs, options.format === 'json'), cleanup: () => cleanup };
115
+ }
116
+ finally {
117
+ let cleanupError = null;
118
+ if (fs.existsSync(worktree)) {
119
+ try {
120
+ git(repoRoot, ['worktree', 'remove', '--force', worktree]);
121
+ cleanup = 'completed';
122
+ }
123
+ catch (error) {
124
+ cleanupError = error;
125
+ }
126
+ }
127
+ try {
128
+ git(repoRoot, ['worktree', 'prune']);
129
+ }
130
+ catch (error) {
131
+ cleanupError ??= error;
132
+ }
133
+ try {
134
+ fs.rmSync(tempRoot, { recursive: true, force: true });
135
+ }
136
+ catch (error) {
137
+ cleanupError ??= error;
138
+ }
139
+ if (cleanupError)
140
+ throw cleanupError;
141
+ }
142
+ }
143
+ function inplaceValidation(config, target, options) {
144
+ const engine = detectEngine(config);
145
+ const { running, nonRunning } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
146
+ const row = selectSandboxContainer([...running, ...nonRunning], containerNameCandidates(config, target.branch));
147
+ if (!row)
148
+ throw new Error('SANDBOX_VALIDATION_CONTAINER_NOT_FOUND');
149
+ const control = sandboxControlPaths({
150
+ base: config.controlBase, project: config.project,
151
+ container: row.name, identity: target.workspace
152
+ });
153
+ const manifest = JSON.parse(fs.readFileSync(control.manifestPath, 'utf8'));
154
+ const initial = readSandboxControlStatus(control.statusDir);
155
+ const now = Date.now();
156
+ if (initial.generation !== manifest.generation
157
+ || now - initial.updatedAt > SANDBOX_CONTROL_STATUS_STALE_MS
158
+ || initial.updatedAt > now + SANDBOX_CONTROL_FUTURE_SKEW_MS) {
159
+ throw new Error('SANDBOX_VALIDATION_BROKER_STATUS_STALE');
160
+ }
161
+ if (initial.state !== 'healthy' || initial.activeRequestId !== null) {
162
+ throw new Error('SANDBOX_VALIDATION_BROKER_NOT_IDLE');
163
+ }
164
+ const startTime = getProcessStartTime(process.pid);
165
+ if (!startTime)
166
+ throw new Error('SANDBOX_VALIDATION_OWNER_IDENTITY_UNAVAILABLE');
167
+ const leasePath = path.join(control.root, 'lease.json');
168
+ const nonce = randomUUID();
169
+ const issuedAt = Date.now();
170
+ const lease = {
171
+ version: 2, generation: manifest.generation, nonce,
172
+ owner: { pid: process.pid, startTime }, issuedAt,
173
+ expiresAt: issuedAt + Math.min(options.timeoutMs + RECOVERY_GRACE_MS, MAX_TIMEOUT_MS + RECOVERY_GRACE_MS),
174
+ taskId: target.workspace.mode === 'task-bound' ? target.workspace.taskId : null,
175
+ branch: target.branch, reason: 'manual-validation'
176
+ };
177
+ const originalBranch = git(manifest.worktreeRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
178
+ const originalHead = git(manifest.worktreeRoot, ['rev-parse', 'HEAD']);
179
+ fs.writeFileSync(leasePath, `${JSON.stringify(lease)}\n`, { flag: 'wx', mode: 0o600 });
180
+ let stopped = false;
181
+ let restored = false;
182
+ try {
183
+ appendSandboxControlAudit(manifest, 'lease-acquire', { ownerPid: process.pid });
184
+ waitForState(control.statusDir, (status) => status.state === 'parked' && status.reasonCode === 'SANDBOX_CONTROL_HANDOFF_ACTIVE');
185
+ if (row.running) {
186
+ runEngine(engine, 'docker', ['stop', row.name]);
187
+ stopped = true;
188
+ }
189
+ const refreshedRows = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
190
+ const refreshed = selectSandboxContainer([...refreshedRows.running, ...refreshedRows.nonRunning], containerNameCandidates(config, target.branch));
191
+ if (!refreshed || refreshed.running)
192
+ throw new Error('SANDBOX_VALIDATION_CONTAINER_STILL_RUNNING');
193
+ const activeLease = readActiveLease(manifest);
194
+ if (!activeLease || activeLease.nonce !== nonce)
195
+ throw new Error('SANDBOX_VALIDATION_LEASE_OWNERSHIP_LOST');
196
+ assertGitWorktreeBinding(config.repoRoot, manifest.worktreeRoot, target.branch);
197
+ const result = runValidationCommand(options.command, manifest.worktreeRoot, 'inplace', options.timeoutMs, options.format === 'json');
198
+ return { ...result, cleanup: () => restored ? 'completed' : 'pending' };
199
+ }
200
+ finally {
201
+ const recoveryErrors = [];
202
+ try {
203
+ const currentBranch = git(manifest.worktreeRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
204
+ if (currentBranch !== originalBranch)
205
+ git(manifest.worktreeRoot, ['switch', originalBranch]);
206
+ }
207
+ catch {
208
+ try {
209
+ git(manifest.worktreeRoot, ['checkout', '--detach', originalHead]);
210
+ git(manifest.worktreeRoot, ['switch', originalBranch]);
211
+ }
212
+ catch (fallbackError) {
213
+ recoveryErrors.push(`git binding restore failed: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`);
214
+ }
215
+ }
216
+ if (stopped) {
217
+ try {
218
+ runEngine(engine, 'docker', ['start', row.name]);
219
+ }
220
+ catch (error) {
221
+ recoveryErrors.push(`container start failed: ${error instanceof Error ? error.message : String(error)}`);
222
+ }
223
+ }
224
+ try {
225
+ const currentLease = JSON.parse(fs.readFileSync(leasePath, 'utf8'));
226
+ if (currentLease.nonce !== nonce)
227
+ throw new Error('SANDBOX_VALIDATION_LEASE_OWNERSHIP_LOST');
228
+ }
229
+ catch (error) {
230
+ recoveryErrors.push(error instanceof Error ? error.message : String(error));
231
+ }
232
+ if (recoveryErrors.length === 0) {
233
+ try {
234
+ fs.unlinkSync(leasePath);
235
+ appendSandboxControlAudit(manifest, 'lease-release', { ownerPid: process.pid });
236
+ waitForState(control.statusDir, (status) => status.state === 'healthy');
237
+ restored = true;
238
+ }
239
+ catch (error) {
240
+ try {
241
+ if (!fs.existsSync(leasePath)) {
242
+ fs.writeFileSync(leasePath, `${JSON.stringify(lease)}\n`, { flag: 'wx', mode: 0o600 });
243
+ }
244
+ appendSandboxControlAudit(manifest, 'lease-release-failed', { ownerPid: process.pid });
245
+ }
246
+ catch (leaseError) {
247
+ recoveryErrors.push(`lease preservation failed: ${leaseError instanceof Error ? leaseError.message : String(leaseError)}`);
248
+ }
249
+ recoveryErrors.push(error instanceof Error ? error.message : String(error));
250
+ }
251
+ }
252
+ if (recoveryErrors.length > 0) {
253
+ throw new Error(`SANDBOX_VALIDATION_RECOVERY_INCOMPLETE: ${recoveryErrors.join('; ')}; run 'ai sandbox start ${target.branch}'`);
254
+ }
255
+ }
256
+ }
257
+ export async function validate(args) {
258
+ const options = parseValidateArgs(args);
259
+ if (options.help) {
260
+ process.stdout.write(`${USAGE}\n`);
261
+ return 0;
262
+ }
263
+ const config = loadConfig();
264
+ const target = resolveSandboxTarget(options.target, config.repoRoot);
265
+ const commit = git(config.repoRoot, ['rev-parse', target.branch]);
266
+ const startedAt = new Date().toISOString();
267
+ const result = options.scope === 'snapshot'
268
+ ? snapshotValidation(config.repoRoot, commit, options)
269
+ : inplaceValidation(config, target, options);
270
+ const evidence = {
271
+ version: 1,
272
+ taskId: target.workspace.mode === 'task-bound' ? target.workspace.taskId : null,
273
+ branch: target.branch,
274
+ scope: options.scope,
275
+ commit,
276
+ command: path.basename(options.command[0]),
277
+ startedAt,
278
+ completedAt: new Date().toISOString(),
279
+ exitCode: result.exitCode,
280
+ signal: result.signal,
281
+ cleanup: result.cleanup()
282
+ };
283
+ if (options.format === 'json')
284
+ process.stdout.write(`${JSON.stringify(evidence)}\n`);
285
+ else
286
+ process.stdout.write(`Validation ${result.exitCode === 0 ? 'passed' : 'failed'} (${options.scope}, ${evidence.command}, cleanup=${evidence.cleanup}).\n`);
287
+ return result.exitCode;
288
+ }
289
+ //# sourceMappingURL=validate.js.map