@fitlab-ai/agent-infra 0.9.7 → 0.9.9

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 (195) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +11 -1
  4. package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +60 -22
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +8 -1
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +175 -11
  8. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +147 -0
  9. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
  10. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +86 -69
  11. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +2 -1
  12. package/dist/lib/decide.js +68 -14
  13. package/dist/lib/git/workflow.js +10 -1
  14. package/dist/lib/init.js +1 -1
  15. package/dist/lib/internal/codex-lifecycle.js +19 -5
  16. package/dist/lib/internal/codex-sandbox-controller.js +6 -13
  17. package/dist/lib/internal/git-workflow.js +5 -7
  18. package/dist/lib/internal/platform-pr.js +20 -7
  19. package/dist/lib/internal/release-workflow.js +3 -4
  20. package/dist/lib/internal/sandbox-control.js +79 -2
  21. package/dist/lib/internal/task-activity.js +57 -7
  22. package/dist/lib/internal/task-event.js +57 -4
  23. package/dist/lib/internal/task-finalization.js +54 -0
  24. package/dist/lib/internal/task-ledger.js +59 -5
  25. package/dist/lib/internal/task-lifecycle.js +59 -8
  26. package/dist/lib/internal/task-orchestration.js +161 -288
  27. package/dist/lib/internal/task-override.js +131 -0
  28. package/dist/lib/internal/task-review.js +64 -6
  29. package/dist/lib/internal/task-verify.js +65 -3
  30. package/dist/lib/internal/task-warning.js +57 -4
  31. package/dist/lib/platform/pr-summary.js +53 -13
  32. package/dist/lib/platform/pull-requests.js +205 -41
  33. package/dist/lib/runtime/agent-runtime.js +56 -0
  34. package/dist/lib/sandbox/commands/create.js +389 -311
  35. package/dist/lib/sandbox/commands/rm.js +316 -55
  36. package/dist/lib/sandbox/control/client.js +190 -6
  37. package/dist/lib/sandbox/control/container-identity.js +1 -1
  38. package/dist/lib/sandbox/control/controller-registration.js +273 -0
  39. package/dist/lib/sandbox/control/executor.js +145 -5
  40. package/dist/lib/sandbox/control/lifecycle.js +395 -73
  41. package/dist/lib/sandbox/control/protocol.js +67 -6
  42. package/dist/lib/sandbox/control/state.js +27 -11
  43. package/dist/lib/sandbox/engine.js +14 -7
  44. package/dist/lib/sandbox/index.js +5 -3
  45. package/dist/lib/sandbox/managed-fs.js +5 -0
  46. package/dist/lib/sandbox/mounts.js +20 -5
  47. package/dist/lib/sandbox/recovery.js +78 -10
  48. package/dist/lib/sandbox/task-resolver.js +50 -6
  49. package/dist/lib/sandbox/workspace-identity.js +27 -0
  50. package/dist/lib/sandbox/workspace-view.js +62 -40
  51. package/dist/lib/sandbox/worktree-safety.js +215 -2
  52. package/dist/lib/server/process-state.js +51 -1
  53. package/dist/lib/task/activity-intent.js +4 -1
  54. package/dist/lib/task/codex-orchestration.js +158 -33
  55. package/dist/lib/task/commands/status.js +59 -1
  56. package/dist/lib/task/commit-operation.js +335 -0
  57. package/dist/lib/task/commit-policy.js +18 -0
  58. package/dist/lib/task/decision-intents.js +4 -2
  59. package/dist/lib/task/delegation-receipts.js +279 -17
  60. package/dist/lib/task/events.js +9 -5
  61. package/dist/lib/task/finalization.js +567 -0
  62. package/dist/lib/task/guard-override.js +31 -0
  63. package/dist/lib/task/human-override.js +1240 -0
  64. package/dist/lib/task/ledger-intents.js +6 -3
  65. package/dist/lib/task/lifecycle.js +69 -14
  66. package/dist/lib/task/operation-outcome.js +33 -0
  67. package/dist/lib/task/orchestration.js +199 -719
  68. package/dist/lib/task/review-finalization.js +3 -2
  69. package/dist/lib/task/short-id.js +59 -1
  70. package/dist/lib/task/task-execution-lock.js +6 -1
  71. package/dist/lib/task/verification-engine.js +138 -59
  72. package/dist/lib/task/verification.js +15 -2
  73. package/dist/lib/task/workflow-warning-intents.js +54 -3
  74. package/dist/lib/task/write.js +2 -1
  75. package/dist/lifecycle-build-manifest.json +3 -3
  76. package/lib/agent-clients/adapters/claude-code.ts +3 -3
  77. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +63 -20
  78. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +12 -0
  79. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +278 -13
  80. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +180 -0
  81. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
  82. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
  83. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +112 -93
  84. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +2 -1
  85. package/lib/decide.ts +59 -14
  86. package/lib/git/workflow.ts +11 -2
  87. package/lib/init.ts +1 -1
  88. package/lib/internal/codex-lifecycle.ts +18 -5
  89. package/lib/internal/codex-sandbox-controller.ts +6 -12
  90. package/lib/internal/git-workflow.ts +5 -6
  91. package/lib/internal/platform-pr.ts +14 -7
  92. package/lib/internal/release-workflow.ts +3 -4
  93. package/lib/internal/sandbox-control.ts +92 -2
  94. package/lib/internal/task-activity.ts +46 -6
  95. package/lib/internal/task-event.ts +53 -4
  96. package/lib/internal/task-finalization.ts +68 -0
  97. package/lib/internal/task-ledger.ts +48 -4
  98. package/lib/internal/task-lifecycle.ts +56 -12
  99. package/lib/internal/task-orchestration.ts +32 -107
  100. package/lib/internal/task-override.ts +138 -0
  101. package/lib/internal/task-review.ts +51 -6
  102. package/lib/internal/task-verify.ts +52 -3
  103. package/lib/internal/task-warning.ts +44 -3
  104. package/lib/platform/pr-summary.ts +65 -14
  105. package/lib/platform/pull-requests.ts +211 -41
  106. package/lib/runtime/agent-runtime.ts +76 -0
  107. package/lib/sandbox/commands/create.ts +88 -4
  108. package/lib/sandbox/commands/rm.ts +413 -61
  109. package/lib/sandbox/config.ts +2 -1
  110. package/lib/sandbox/control/client.ts +255 -6
  111. package/lib/sandbox/control/container-identity.ts +3 -3
  112. package/lib/sandbox/control/controller-registration.ts +347 -0
  113. package/lib/sandbox/control/executor.ts +172 -6
  114. package/lib/sandbox/control/lifecycle.ts +465 -77
  115. package/lib/sandbox/control/protocol.ts +85 -9
  116. package/lib/sandbox/control/state.ts +35 -18
  117. package/lib/sandbox/engine.ts +20 -9
  118. package/lib/sandbox/index.ts +5 -3
  119. package/lib/sandbox/managed-fs.ts +5 -0
  120. package/lib/sandbox/mounts.ts +23 -5
  121. package/lib/sandbox/recovery.ts +90 -9
  122. package/lib/sandbox/task-resolver.ts +58 -7
  123. package/lib/sandbox/workspace-identity.ts +50 -2
  124. package/lib/sandbox/workspace-view.ts +73 -43
  125. package/lib/sandbox/worktree-safety.ts +236 -2
  126. package/lib/server/process-state.ts +48 -1
  127. package/lib/task/activity-intent.ts +4 -2
  128. package/lib/task/codex-orchestration.ts +167 -33
  129. package/lib/task/commands/status.ts +78 -1
  130. package/lib/task/commit-operation.ts +430 -0
  131. package/lib/task/commit-policy.ts +30 -0
  132. package/lib/task/decision-intents.ts +4 -2
  133. package/lib/task/delegation-receipts.ts +304 -19
  134. package/lib/task/events.ts +10 -5
  135. package/lib/task/finalization.ts +719 -0
  136. package/lib/task/guard-override.ts +50 -0
  137. package/lib/task/human-override.ts +1503 -0
  138. package/lib/task/ledger-intents.ts +6 -3
  139. package/lib/task/lifecycle.ts +71 -14
  140. package/lib/task/operation-outcome.ts +48 -0
  141. package/lib/task/orchestration.ts +239 -922
  142. package/lib/task/review-finalization.ts +6 -2
  143. package/lib/task/short-id.ts +65 -1
  144. package/lib/task/task-execution-lock.ts +10 -1
  145. package/lib/task/verification-engine.ts +145 -64
  146. package/lib/task/verification.ts +18 -3
  147. package/lib/task/workflow-warning-intents.ts +61 -4
  148. package/lib/task/write.ts +4 -1
  149. package/package.json +2 -2
  150. package/templates/.agents/QUICKSTART.en.md +2 -2
  151. package/templates/.agents/QUICKSTART.zh-CN.md +2 -2
  152. package/templates/.agents/hooks/lifecycle-delegation.js +9 -24
  153. package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -3
  154. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -3
  155. package/templates/.agents/rules/task-management.en.md +2 -2
  156. package/templates/.agents/rules/task-management.zh-CN.md +2 -2
  157. package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
  158. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
  159. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  160. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
  161. package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
  162. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
  163. package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
  164. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
  165. package/templates/.agents/skills/commit/SKILL.en.md +41 -125
  166. package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
  167. package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
  168. package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
  169. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
  170. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
  171. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  172. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  173. package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
  174. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
  175. package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
  176. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
  177. package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
  178. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
  179. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
  180. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
  181. package/templates/.agents/skills/create-pr/SKILL.en.md +6 -4
  182. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -4
  183. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
  184. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
  185. package/templates/.agents/skills/run-task/SKILL.en.md +5 -5
  186. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +5 -5
  187. package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -1
  188. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -1
  189. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
  190. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
  191. package/templates/.codex/hooks.json +4 -4
  192. package/dist/lib/task/commit-finalization.js +0 -305
  193. package/dist/lib/task/commit-intent.js +0 -165
  194. package/lib/task/commit-finalization.ts +0 -419
  195. package/lib/task/commit-intent.ts +0 -230
@@ -9,6 +9,9 @@ export type ProcessIdentity = Readonly<{
9
9
  startTime: ProcessStartTime;
10
10
  }>;
11
11
 
12
+ export type ProcessIdentityState = 'alive' | 'dead' | 'unknown';
13
+ export type ProcessIdentityProbe = (identity: ProcessIdentity) => ProcessIdentityState;
14
+
12
15
  export type PidRecord = ProcessIdentity & {
13
16
  version: 2;
14
17
  };
@@ -127,6 +130,50 @@ export function getProcessStartTime(
127
130
  }
128
131
  }
129
132
 
133
+ export function getProcessIdentityState(
134
+ identity: ProcessIdentity,
135
+ platform: NodeJS.Platform = process.platform
136
+ ): ProcessIdentityState {
137
+ if (!Number.isInteger(identity.pid) || identity.pid <= 0 || !Number.isSafeInteger(identity.startTime)) {
138
+ return 'dead';
139
+ }
140
+ if (platform === 'linux') {
141
+ try {
142
+ const stat = parseLinuxProcessStat(fs.readFileSync(`/proc/${identity.pid}/stat`, 'utf8'));
143
+ if (stat === null) return 'unknown';
144
+ if (stat.state === 'Z') return 'dead';
145
+ return stat.startTime === identity.startTime ? 'alive' : 'dead';
146
+ } catch (error) {
147
+ return (error as NodeJS.ErrnoException).code === 'ENOENT' ? 'dead' : 'unknown';
148
+ }
149
+ }
150
+ try {
151
+ process.kill(identity.pid, 0);
152
+ } catch (error) {
153
+ const code = (error as NodeJS.ErrnoException).code;
154
+ if (code === 'ESRCH') return 'dead';
155
+ if (code !== 'EPERM') return 'unknown';
156
+ }
157
+ const query = buildProcessStartTimeQuery(identity.pid, platform);
158
+ if (query === null) return 'unknown';
159
+ try {
160
+ const output = execFileSync(query.command, query.args, {
161
+ encoding: 'utf8',
162
+ env: platform === 'darwin' ? { ...process.env, LC_ALL: 'C', LANG: 'C', TZ: 'UTC' } : process.env
163
+ }).trim();
164
+ if (output.length === 0) return 'unknown';
165
+ const startTime = platform === 'win32'
166
+ ? new Date(output).getTime()
167
+ : platform === 'darwin'
168
+ ? parseDarwinStartTime(output)
169
+ : null;
170
+ if (startTime === null || Number.isNaN(startTime)) return 'unknown';
171
+ return startTime === identity.startTime ? 'alive' : 'dead';
172
+ } catch {
173
+ return 'unknown';
174
+ }
175
+ }
176
+
130
177
  function parsePidRecord(
131
178
  raw: string
132
179
  ):
@@ -234,5 +281,5 @@ export function removePidRecordIfMatches(pidFile: string, expected: PidRecord):
234
281
  }
235
282
 
236
283
  export function processIdentityMatches(identity: ProcessIdentity): boolean {
237
- return getProcessStartTime(identity.pid) === identity.startTime;
284
+ return getProcessIdentityState(identity) === 'alive';
238
285
  }
@@ -14,6 +14,7 @@ import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-
14
14
  import type { TaskExecutionLockOptions } from './task-execution-lock.ts';
15
15
  import { captureTaskWriteMetadata, writeTask } from './write.ts';
16
16
  import type { TaskMutation, TaskOperationSummary, TaskWriteOptions } from './write.ts';
17
+ import { allowsManualOverride } from './guard-override.ts';
17
18
 
18
19
  type PrReviewVerdict = 'approved' | 'changes-requested' | 'commented';
19
20
  type PrReviewOutcome = 'aborted' | 'superseded';
@@ -95,7 +96,7 @@ export type PrReviewInspectionResult = {
95
96
  error: { code: string; message: string } | null;
96
97
  };
97
98
 
98
- type ActivityIntentOptions = TaskWriteOptions & { lockOptions?: TaskExecutionLockOptions };
99
+ type ActivityIntentOptions = TaskWriteOptions & { lockOptions?: TaskExecutionLockOptions; lockAlreadyHeld?: boolean };
99
100
  type ArtifactRecord = { identity: ArtifactIdentity; head: string; status: PrReviewArtifactStatus };
100
101
  type RoundState = ArtifactRecord & {
101
102
  startedAgent: string | null;
@@ -397,9 +398,10 @@ export function applyPrReviewActivityIntent(intent: PrReviewActivityIntent, opti
397
398
  if ('code' in validated) return failed(intent, validated.code, validated.message);
398
399
  const resolved = resolveTaskRef(intent.taskRef, { repoRoot: options.repoRoot });
399
400
  if (!resolved.ok) return failed(intent, resolved.code, resolved.message, resolved.taskId);
400
- if (resolved.state !== 'active') {
401
+ if (resolved.state !== 'active' && !allowsManualOverride(options.manualOverride, 'activity-intent', 'TASK_STATE_MISMATCH')) {
401
402
  return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
402
403
  }
404
+ if (options.lockAlreadyHeld) return applyLocked(intent, validated.agent, { ...options, repoRoot: resolved.repoRoot });
403
405
  try {
404
406
  return withTaskExecutionLock(
405
407
  resolved.repoRoot,
@@ -1,5 +1,3 @@
1
- import fs from 'node:fs';
2
-
3
1
  import {
4
2
  preflightCodexLifecycleEvidence,
5
3
  resolveCodexSpawnedChild,
@@ -7,16 +5,22 @@ import {
7
5
  resolveCodexThread
8
6
  } from '../agent-clients/adapters/codex-lifecycle/app-server.ts';
9
7
  import { createCodexLifecycleStore } from '../agent-clients/adapters/codex-lifecycle/store.ts';
10
- import { createCodexCapabilityStore } from '../agent-clients/adapters/codex-lifecycle/capability-store.ts';
8
+ import {
9
+ createCodexCapabilityStore,
10
+ isCodexCapabilityProvenanceDetail
11
+ } from '../agent-clients/adapters/codex-lifecycle/capability-store.ts';
12
+ import type { CodexCapabilityProvenanceDetail } from '../agent-clients/adapters/codex-lifecycle/capability-store.ts';
11
13
  import { computeLifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.ts';
12
14
  import type { LifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.ts';
13
15
  import { verifyCodexSandboxControllerContext } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.ts';
16
+ import { verifyCodexSandboxControllerContext as verifyControllerContextFile } from '../agent-clients/adapters/codex-lifecycle/controller-context.ts';
14
17
  import type { AgentClientId } from '../agent-clients/types.ts';
15
18
  import { resolveTaskRef } from './resolve-ref.ts';
16
19
  import {
17
20
  activateMatchingOrchestrationDelegation,
18
21
  hasActivatableOrchestrationDelegation,
19
22
  hasSealableOrchestrationDelegation,
23
+ OrchestrationStateError,
20
24
  pauseMatchingOrchestrationDelegation,
21
25
  prepareOrchestrationDelegation,
22
26
  reconcileMatchingOrchestrationDelegation,
@@ -52,10 +56,27 @@ function coreOptions(options: CodexBridgeOptions): OrchestrationOptions {
52
56
  return { ...options.orchestrationOptions, repoRoot: options.repoRoot ?? options.orchestrationOptions?.repoRoot };
53
57
  }
54
58
 
55
- function bridgeFailure(code: string, message: string): OrchestrationResult {
59
+ function bridgeFailure(code: string, message: string, detail?: CodexCapabilityProvenanceDetail): OrchestrationResult {
56
60
  return {
57
61
  status: 'failed', changed: false, taskId: null, run: null, next: null,
58
- error: { code, message }
62
+ error: { code, message, ...(detail ? { detail } : {}) }
63
+ };
64
+ }
65
+
66
+ function capabilityFailure(error: unknown): NonNullable<OrchestrationResult['error']> {
67
+ const detailValue = error instanceof Error
68
+ ? (error as Error & { detail?: unknown }).detail
69
+ : undefined;
70
+ return {
71
+ code: error instanceof Error && error.name.startsWith('CODEX_CAPABILITY_')
72
+ ? error.name
73
+ : 'ORCHESTRATION_CLIENT_PREFLIGHT_FAILED',
74
+ message: error instanceof Error ? error.message : String(error),
75
+ ...(error instanceof Error
76
+ && error.name === 'CODEX_CAPABILITY_PROVENANCE_MISMATCH'
77
+ && isCodexCapabilityProvenanceDetail(detailValue)
78
+ ? { detail: detailValue }
79
+ : {})
59
80
  };
60
81
  }
61
82
 
@@ -71,11 +92,54 @@ function requiredStore(options: CodexBridgeOptions): LifecycleStore {
71
92
  return options.store;
72
93
  }
73
94
 
74
- function controllerContext(taskId: string, repoRoot: string) {
95
+ function controllerContext(
96
+ repoRoot: string,
97
+ brokerController: Readonly<{ instanceDigest: string; controlGeneration: string }> | null
98
+ ) {
75
99
  const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
76
- return contextPath
77
- ? verifyCodexSandboxControllerContext(contextPath, taskId, { repoRoot })
78
- : null;
100
+ if (!contextPath) return null;
101
+ if (brokerController) {
102
+ return verifyControllerContextFile(contextPath, {
103
+ repoRoot,
104
+ generation: process.env.AGENT_INFRA_CONTROL_GENERATION ?? brokerController.controlGeneration
105
+ });
106
+ }
107
+ return verifyCodexSandboxControllerContext(contextPath, { repoRoot });
108
+ }
109
+
110
+ function brokerBindingConflictsWithContext(
111
+ repoRoot: string,
112
+ brokerController: Readonly<{ instanceDigest: string; controlGeneration: string }> | null
113
+ ): boolean {
114
+ const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
115
+ if (!contextPath || !brokerController) return false;
116
+ const context = verifyControllerContextFile(contextPath, {
117
+ repoRoot,
118
+ generation: process.env.AGENT_INFRA_CONTROL_GENERATION ?? brokerController.controlGeneration
119
+ });
120
+ return context.controllerInstanceDigest !== brokerController.instanceDigest
121
+ || context.controlGeneration !== brokerController.controlGeneration;
122
+ }
123
+
124
+ function brokerControllerBinding(): Readonly<{ instanceDigest: string; controlGeneration: string }> | null {
125
+ const raw = process.env.AGENT_INFRA_CONTROL_CONTROLLER_BINDING;
126
+ if (!raw) return null;
127
+ let value: unknown;
128
+ try {
129
+ value = JSON.parse(raw);
130
+ } catch {
131
+ throw new Error('CODEX_SANDBOX_CONTROLLER_BINDING_INVALID');
132
+ }
133
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
134
+ throw new Error('CODEX_SANDBOX_CONTROLLER_BINDING_INVALID');
135
+ }
136
+ const binding = value as Record<string, unknown>;
137
+ if (Object.keys(binding).sort().join(',') !== 'controlGeneration,instanceDigest'
138
+ || typeof binding.instanceDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(binding.instanceDigest)
139
+ || typeof binding.controlGeneration !== 'string' || binding.controlGeneration.length === 0) {
140
+ throw new Error('CODEX_SANDBOX_CONTROLLER_BINDING_INVALID');
141
+ }
142
+ return binding as { instanceDigest: string; controlGeneration: string };
79
143
  }
80
144
 
81
145
  async function prepareCodexOrchestrationDelegation(
@@ -101,36 +165,101 @@ async function prepareCodexOrchestrationDelegation(
101
165
  );
102
166
  }
103
167
  const buildIdentity = options.buildIdentity ?? computeLifecycleBuildIdentity(repoRoot);
104
- const controller = controllerContext(resolved.taskId, repoRoot);
168
+ const brokerController = brokerControllerBinding();
169
+ if (brokerBindingConflictsWithContext(repoRoot, brokerController)) {
170
+ return bridgeFailure(
171
+ 'CODEX_SANDBOX_CONTROLLER_BINDING_MISMATCH',
172
+ 'broker and local controller bindings do not match'
173
+ );
174
+ }
175
+ const localController = controllerContext(repoRoot, brokerController);
176
+ if (localController && brokerController
177
+ && (localController.controllerInstanceDigest !== brokerController.instanceDigest
178
+ || localController.controlGeneration !== brokerController.controlGeneration)) {
179
+ return bridgeFailure(
180
+ 'CODEX_SANDBOX_CONTROLLER_BINDING_MISMATCH',
181
+ 'broker and local controller bindings do not match'
182
+ );
183
+ }
184
+ const controller = brokerController ?? (localController ? {
185
+ instanceDigest: localController.controllerInstanceDigest,
186
+ controlGeneration: localController.controlGeneration
187
+ } : null);
105
188
  const capabilityStore = options.capabilityStore ?? createCodexCapabilityStore();
106
- const consumed = capabilityStore.consume(input.capabilityToken, {
189
+ const attested = capabilityStore.inspect(input.capabilityToken);
190
+ const lifecycleProvenance = {
191
+ ...attested.buildIdentity,
192
+ hookDefinitionHash: preflight.hookDefinitionHash,
193
+ ...preflight.hookProvenance,
194
+ capabilitySessionId: attested.sessionId!,
195
+ capabilityTurnId: attested.turnId!,
196
+ capabilityToolUseId: attested.toolUseId!,
197
+ controllerInstanceDigest: controller?.instanceDigest ?? null,
198
+ controlGeneration: controller?.controlGeneration ?? null
199
+ };
200
+ const capabilityExpected = {
107
201
  taskId: resolved.taskId,
108
202
  hookDefinitionHash: preflight.hookDefinitionHash,
109
203
  buildIdentity,
110
204
  ...(controller ? { controller: {
111
- instanceDigest: controller.controllerInstanceDigest,
205
+ instanceDigest: controller.instanceDigest,
112
206
  controlGeneration: controller.controlGeneration
113
207
  } } : {})
114
- });
208
+ };
115
209
  return prepareOrchestrationDelegation(taskRef, {
116
210
  ...input,
117
- lifecycleProvenance: {
118
- ...consumed.buildIdentity,
119
- hookDefinitionHash: preflight.hookDefinitionHash,
120
- ...preflight.hookProvenance,
121
- capabilitySessionId: consumed.sessionId!,
122
- capabilityTurnId: consumed.turnId!,
123
- capabilityToolUseId: consumed.toolUseId!,
124
- controllerInstanceDigest: controller?.controllerInstanceDigest ?? null,
125
- controlGeneration: controller?.controlGeneration ?? null
211
+ lifecycleProvenance
212
+ }, {
213
+ ...coreOptions(options),
214
+ validateLifecycleCapability: () => {
215
+ try {
216
+ const validated = capabilityStore.validate(input.capabilityToken!, capabilityExpected);
217
+ if (validated.sessionId !== attested.sessionId
218
+ || validated.turnId !== attested.turnId
219
+ || validated.toolUseId !== attested.toolUseId) {
220
+ return {
221
+ code: 'CODEX_CAPABILITY_IDENTITY_CHANGED',
222
+ message: 'capability identity changed during validation'
223
+ };
224
+ }
225
+ return null;
226
+ } catch (error) {
227
+ return capabilityFailure(error);
228
+ }
229
+ },
230
+ consumeLifecycleCapability: () => {
231
+ try {
232
+ const consumed = capabilityStore.consume(input.capabilityToken!, capabilityExpected);
233
+ if (consumed.sessionId !== attested.sessionId
234
+ || consumed.turnId !== attested.turnId
235
+ || consumed.toolUseId !== attested.toolUseId) {
236
+ return {
237
+ code: 'CODEX_CAPABILITY_IDENTITY_CHANGED',
238
+ message: 'capability identity changed during consumption'
239
+ };
240
+ }
241
+ return null;
242
+ } catch (error) {
243
+ return capabilityFailure(error);
244
+ }
126
245
  }
127
- }, coreOptions(options));
246
+ });
128
247
  } catch (error) {
248
+ if (error instanceof OrchestrationStateError) return bridgeFailure(error.code, error.message);
249
+ const detailValue = error instanceof Error
250
+ ? (error as Error & { detail?: unknown }).detail
251
+ : undefined;
252
+ const detail = error instanceof Error
253
+ && error.name === 'CODEX_CAPABILITY_PROVENANCE_MISMATCH'
254
+ && isCodexCapabilityProvenanceDetail(detailValue)
255
+ ? detailValue
256
+ : undefined;
129
257
  return bridgeFailure(
130
258
  error instanceof Error && error.name.startsWith('CODEX_CAPABILITY_')
131
259
  ? error.name
132
260
  : 'ORCHESTRATION_CLIENT_PREFLIGHT_FAILED',
133
- error instanceof Error ? error.message : String(error)
261
+ error instanceof Error ? error.message : String(error),
262
+ detail
134
263
  );
135
264
  }
136
265
  }
@@ -161,14 +290,8 @@ async function activateCodexOrchestrationDelegation(
161
290
  });
162
291
  const buildIdentity = options.buildIdentity ?? computeLifecycleBuildIdentity(repoRoot);
163
292
  const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
164
- const contextTaskId = contextPath
165
- ? (JSON.parse(fs.readFileSync(contextPath, 'utf8')) as { taskId?: unknown }).taskId
166
- : null;
167
- if (contextPath && typeof contextTaskId !== 'string') {
168
- throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
169
- }
170
293
  const controller = contextPath
171
- ? verifyCodexSandboxControllerContext(contextPath, contextTaskId as string, { repoRoot })
294
+ ? verifyCodexSandboxControllerContext(contextPath, { repoRoot })
172
295
  : null;
173
296
  return activateMatchingOrchestrationDelegation('codex', {
174
297
  nativeAgent: evidence.nativeAgent,
@@ -196,6 +319,7 @@ async function activateCodexOrchestrationDelegation(
196
319
  }
197
320
  }, coreOptions(options));
198
321
  } catch (error) {
322
+ if (error instanceof OrchestrationStateError) return bridgeFailure(error.code, error.message);
199
323
  return pauseBridge('ORCHESTRATION_CODEX_START_FAILED', error instanceof Error ? error.message : String(error), options);
200
324
  }
201
325
  }
@@ -223,6 +347,7 @@ async function activateCodexSpawnDelegation(
223
347
  resolveThread: async () => resolved
224
348
  });
225
349
  } catch (error) {
350
+ if (error instanceof OrchestrationStateError) return bridgeFailure(error.code, error.message);
226
351
  return pauseBridge('ORCHESTRATION_CODEX_START_FAILED', error instanceof Error ? error.message : String(error), options);
227
352
  }
228
353
  }
@@ -238,7 +363,11 @@ async function sealCodexOrchestrationDelegation(
238
363
  const store = requiredStore(options);
239
364
  const existing = store.read(childThreadId);
240
365
  if (!existing.consumer) {
241
- store.apply(await (options.resolveTerminal ?? resolveCodexTerminal)(childThreadId));
366
+ const stopTurnId = existing.state.stop?.turnId;
367
+ if (!stopTurnId) {
368
+ return pauseBridge('ORCHESTRATION_CODEX_STOP_EVIDENCE_INVALID', 'Codex lifecycle stop hook is not available', options);
369
+ }
370
+ store.apply(await (options.resolveTerminal ?? resolveCodexTerminal)(childThreadId, stopTurnId));
242
371
  }
243
372
  const record = store.read(childThreadId);
244
373
  const evidence = record.state.stopEvidence;
@@ -259,6 +388,7 @@ async function sealCodexOrchestrationDelegation(
259
388
  coreOptions(options)
260
389
  );
261
390
  } catch (error) {
391
+ if (error instanceof OrchestrationStateError) return bridgeFailure(error.code, error.message);
262
392
  return pauseBridge('ORCHESTRATION_CODEX_STOP_FAILED', error instanceof Error ? error.message : String(error), options);
263
393
  }
264
394
  }
@@ -280,7 +410,10 @@ async function sealCodexParentDelegation(
280
410
  const start = active.state.startEvidence!;
281
411
  const child = active.state.child!;
282
412
  if (!active.state.terminal) {
283
- store.apply(await (options.resolveTerminal ?? resolveCodexTerminal)(start.childThreadId));
413
+ const resolveTerminal = options.resolveTerminal ?? resolveCodexTerminal;
414
+ store.apply(active.state.stop
415
+ ? await resolveTerminal(start.childThreadId, active.state.stop.turnId)
416
+ : await resolveTerminal(start.childThreadId));
284
417
  }
285
418
  if (!store.read(start.childThreadId).state.stop) {
286
419
  store.apply({
@@ -294,6 +427,7 @@ async function sealCodexParentDelegation(
294
427
  }
295
428
  return sealCodexOrchestrationDelegation(start.childThreadId, options);
296
429
  } catch (error) {
430
+ if (error instanceof OrchestrationStateError) return bridgeFailure(error.code, error.message);
297
431
  if (error instanceof Error && error.message === 'CODEX_TURN_NOT_TERMINAL') {
298
432
  return bridgeFailure('ORCHESTRATION_DELEGATION_MISSING', 'The matching Codex child has not completed');
299
433
  }
@@ -9,6 +9,8 @@ import { parseTaskFrontmatter, extractTitle, type Frontmatter } from '../frontma
9
9
  import { loadShortIdByTaskId } from '../short-id.ts';
10
10
  import { getOpenWorkflowWarnings, formatWorkflowWarningSummary, type WorkflowWarning } from '../workflow-warnings.ts';
11
11
  import { parseActivityLog, pairEntries } from '../activity-log.ts';
12
+ import { readRun, type OrchestrationRun } from '../orchestration.ts';
13
+ import type { DelegationReceipt } from '../delegation-receipts.ts';
12
14
  import { statusCard, type DisplayMessage } from '../../server/display.ts';
13
15
 
14
16
  const USAGE = `Usage: ai task status [<N | TASK-id> | --task <ref> | -t <ref>]
@@ -219,6 +221,19 @@ type RuntimeInfo = {
219
221
  log: string;
220
222
  };
221
223
 
224
+ type PendingDelegationInfo = Readonly<Pick<
225
+ DelegationReceipt,
226
+ 'id' | 'role' | 'stage' | 'round' | 'artifact' | 'client' | 'status'
227
+ >>;
228
+
229
+ type OrchestrationInfo = Readonly<{
230
+ status: 'absent' | OrchestrationRun['status'];
231
+ runId: string;
232
+ nextStage: string;
233
+ pause: Readonly<{ code: string; message: string; recoverable: boolean }> | null;
234
+ pendingDelegation: PendingDelegationInfo | null;
235
+ }>;
236
+
222
237
  type ManagedRunRecord = {
223
238
  run_id: string;
224
239
  engine: string;
@@ -323,6 +338,39 @@ function collectRuntime(taskDir: string, workflow: WorkflowInfo, run: Runner): R
323
338
  };
324
339
  }
325
340
 
341
+ function pendingDelegationInfo(receipt: DelegationReceipt | null): PendingDelegationInfo | null {
342
+ if (!receipt) return null;
343
+ return {
344
+ id: receipt.id,
345
+ role: receipt.role,
346
+ stage: receipt.stage,
347
+ round: receipt.round,
348
+ artifact: receipt.artifact,
349
+ client: receipt.client,
350
+ status: receipt.status
351
+ };
352
+ }
353
+
354
+ function collectOrchestration(taskDir: string): OrchestrationInfo {
355
+ const run = readRun(taskDir);
356
+ if (!run) {
357
+ return {
358
+ status: 'absent',
359
+ runId: DASH,
360
+ nextStage: DASH,
361
+ pause: null,
362
+ pendingDelegation: null
363
+ };
364
+ }
365
+ return {
366
+ status: run.status,
367
+ runId: run.runId,
368
+ nextStage: run.nextStage ?? DASH,
369
+ pause: run.pause,
370
+ pendingDelegation: pendingDelegationInfo(run.pendingDelegation)
371
+ };
372
+ }
373
+
326
374
  type StatusModel = {
327
375
  taskId: string;
328
376
  shortId: string;
@@ -332,6 +380,7 @@ type StatusModel = {
332
380
  artifacts: { count: number; groups: { stage: string; files: string[] }[] };
333
381
  workflow: WorkflowInfo;
334
382
  runtime: RuntimeInfo;
383
+ orchestration: OrchestrationInfo;
335
384
  git: GitInfo;
336
385
  };
337
386
 
@@ -380,6 +429,26 @@ function renderStatus(model: StatusModel): string[] {
380
429
  lines.push(...renderPairs(model.artifacts.groups.map((group) => [group.stage, group.files.join(', ')])));
381
430
  }
382
431
 
432
+ lines.push(
433
+ '',
434
+ 'Orchestration',
435
+ ...renderPairs([
436
+ ['status', model.orchestration.status],
437
+ ['run_id', model.orchestration.runId],
438
+ ['next_stage', model.orchestration.nextStage],
439
+ ['pause_code', model.orchestration.pause?.code ?? DASH],
440
+ ['pause_message', model.orchestration.pause?.message ?? DASH],
441
+ ['recoverable', model.orchestration.pause ? String(model.orchestration.pause.recoverable) : DASH],
442
+ ['pending_id', model.orchestration.pendingDelegation?.id ?? DASH],
443
+ ['pending_role', model.orchestration.pendingDelegation?.role ?? DASH],
444
+ ['pending_stage', model.orchestration.pendingDelegation?.stage ?? DASH],
445
+ ['pending_round', model.orchestration.pendingDelegation ? String(model.orchestration.pendingDelegation.round) : DASH],
446
+ ['pending_artifact', model.orchestration.pendingDelegation?.artifact ?? DASH],
447
+ ['pending_client', model.orchestration.pendingDelegation?.client ?? DASH],
448
+ ['pending_status', model.orchestration.pendingDelegation?.status ?? DASH]
449
+ ])
450
+ );
451
+
383
452
  lines.push(
384
453
  '',
385
454
  'Workflow',
@@ -425,6 +494,9 @@ function renderStatus(model: StatusModel): string[] {
425
494
  }
426
495
 
427
496
  function modelTone(model: StatusModel): 'info' | 'success' | 'warning' | 'danger' | 'running' {
497
+ if (model.orchestration.status === 'paused') return 'warning';
498
+ if (model.orchestration.status === 'running') return 'running';
499
+ if (model.orchestration.status === 'completed') return 'success';
428
500
  if (model.workflow.state === 'in-progress') return model.workflow.stale === 'yes' ? 'warning' : 'running';
429
501
  if (model.metadata.some(([key, value]) => key === 'status' && value === 'completed')) return 'success';
430
502
  if (model.metadata.some(([key, value]) => key === 'status' && (value === 'blocked' || value === 'cancelled'))) {
@@ -449,6 +521,7 @@ function buildFromResolved(input: BuildStatusModelInput): StatusModel {
449
521
  artifacts: { count: artifacts.length, groups: groupArtifacts(artifacts) },
450
522
  workflow,
451
523
  runtime: collectRuntime(input.taskDir, workflow, run),
524
+ orchestration: collectOrchestration(input.taskDir),
452
525
  git: collectGit(fm.branch ?? '', run)
453
526
  };
454
527
  }
@@ -483,6 +556,9 @@ function statusModelToDisplay(model: StatusModel): DisplayMessage {
483
556
  `Task ${model.taskId} (${model.shortId})`,
484
557
  modelTone(model),
485
558
  [
559
+ ['orchestration', model.orchestration.status],
560
+ ['run', model.orchestration.runId],
561
+ ['next', model.orchestration.nextStage],
486
562
  ['workflow', model.workflow.state],
487
563
  ['step', model.workflow.step],
488
564
  ['runtime', model.runtime.status],
@@ -530,8 +606,9 @@ export {
530
606
  collectGit,
531
607
  collectWorkflow,
532
608
  collectRuntime,
609
+ collectOrchestration,
533
610
  renderStatus,
534
611
  statusModelToDisplay,
535
612
  METADATA_KEYS
536
613
  };
537
- export type { Runner, GitInfo, WorkflowInfo, RuntimeInfo, StatusModel, BuildStatusModelInput };
614
+ export type { Runner, GitInfo, WorkflowInfo, RuntimeInfo, OrchestrationInfo, PendingDelegationInfo, StatusModel, BuildStatusModelInput };