@fitlab-ai/agent-infra 0.11.11 → 0.11.13

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 (154) hide show
  1. package/dist/bin/internal-cli.js +2 -2
  2. package/dist/lib/agent-clients/adapter.js +7 -1
  3. package/dist/lib/agent-clients/adapters/claude-code.js +5 -1
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +78 -63
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/binding.js +114 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/cli.js +447 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-client.js +130 -0
  8. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-operation.js +131 -0
  9. package/dist/lib/{sandbox/control → agent-clients/adapters/codex-lifecycle}/controller-registration.js +2 -2
  10. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +48 -1
  11. package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +124 -62
  12. package/dist/lib/{internal/codex-sandbox-controller.js → agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.js} +3 -3
  13. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +3 -5
  14. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +211 -213
  15. package/dist/lib/agent-clients/adapters/codex-orchestration.js +468 -0
  16. package/dist/lib/agent-clients/adapters/codex.js +40 -1
  17. package/dist/lib/agent-clients/adapters/index.js +2 -10
  18. package/dist/lib/agent-clients/tokens.js +3 -2
  19. package/dist/lib/internal/agent-client.js +132 -72
  20. package/dist/lib/internal/cli-route-inventory.js +2 -2
  21. package/dist/lib/internal/sandbox-control.js +1 -81
  22. package/dist/lib/internal/task-finalization.js +26 -19
  23. package/dist/lib/internal/task-lifecycle.js +1 -1
  24. package/dist/lib/internal/task-operation-registry.js +1 -1
  25. package/dist/lib/internal/task-validate.js +1 -1
  26. package/dist/lib/platform/github-client.js +33 -3
  27. package/dist/lib/platform/issues.js +18 -6
  28. package/dist/lib/runtime/agent-runtime.js +6 -29
  29. package/dist/lib/sandbox/commands/control-status.js +1 -0
  30. package/dist/lib/sandbox/commands/create.js +7 -2
  31. package/dist/lib/sandbox/commands/ls.js +4 -5
  32. package/dist/lib/sandbox/commands/rm.js +1 -1
  33. package/dist/lib/sandbox/control/agent-operation.js +34 -0
  34. package/dist/lib/sandbox/control/client.js +14 -201
  35. package/dist/lib/sandbox/control/executor.js +12 -125
  36. package/dist/lib/sandbox/control/lifecycle.js +66 -4
  37. package/dist/lib/sandbox/control/protocol.js +15 -54
  38. package/dist/lib/sandbox/control/server.js +216 -281
  39. package/dist/lib/sandbox/control/task-view.js +0 -13
  40. package/dist/lib/sandbox/mounts.js +9 -1
  41. package/dist/lib/sandbox/recovery.js +31 -10
  42. package/dist/lib/sandbox/removal.js +28 -5
  43. package/dist/lib/sandbox/workspace-identity.js +18 -6
  44. package/dist/lib/sandbox/workspace-view.js +56 -19
  45. package/dist/lib/task/commands/ls.js +3 -3
  46. package/dist/lib/task/control-authority.js +56 -264
  47. package/dist/lib/task/control-recovery.js +2 -3
  48. package/dist/lib/task/create.js +2 -2
  49. package/dist/lib/task/delegation-receipts.js +74 -161
  50. package/dist/lib/task/events.js +88 -44
  51. package/dist/lib/task/finalization.js +35 -6
  52. package/dist/lib/task/lifecycle.js +274 -115
  53. package/dist/lib/task/orchestration.js +201 -194
  54. package/dist/lib/task/runtime-paths.js +10 -0
  55. package/dist/lib/task/short-id.js +37 -27
  56. package/dist/lib/task/verification-engine.js +32 -26
  57. package/dist/lib/task/workspace-snapshot.js +1 -1
  58. package/dist/lifecycle-build-manifest.json +3 -3
  59. package/lib/agent-clients/adapter.ts +39 -0
  60. package/lib/agent-clients/adapters/claude-code.ts +5 -1
  61. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +105 -80
  62. package/lib/agent-clients/adapters/codex-lifecycle/binding.ts +147 -0
  63. package/lib/agent-clients/adapters/codex-lifecycle/cli.ts +510 -0
  64. package/lib/agent-clients/adapters/codex-lifecycle/controller-client.ts +158 -0
  65. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +1 -1
  66. package/lib/agent-clients/adapters/codex-lifecycle/controller-operation.ts +151 -0
  67. package/lib/{sandbox/control → agent-clients/adapters/codex-lifecycle}/controller-registration.ts +4 -4
  68. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +43 -0
  69. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +8 -5
  70. package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +190 -65
  71. package/lib/{internal/codex-sandbox-controller.ts → agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.ts} +2 -2
  72. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +3 -5
  73. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +270 -224
  74. package/lib/agent-clients/adapters/codex-orchestration.ts +632 -0
  75. package/lib/agent-clients/adapters/codex.ts +44 -1
  76. package/lib/agent-clients/adapters/index.ts +7 -20
  77. package/lib/agent-clients/tokens.ts +4 -3
  78. package/lib/internal/agent-client.ts +142 -73
  79. package/lib/internal/cli-route-inventory.ts +2 -2
  80. package/lib/internal/sandbox-control.ts +0 -97
  81. package/lib/internal/task-finalization.ts +30 -18
  82. package/lib/internal/task-lifecycle.ts +1 -1
  83. package/lib/internal/task-operation-registry.ts +1 -1
  84. package/lib/internal/task-validate.ts +1 -1
  85. package/lib/platform/github-client.ts +34 -3
  86. package/lib/platform/issues.ts +18 -6
  87. package/lib/runtime/agent-runtime.ts +6 -41
  88. package/lib/sandbox/commands/control-status.ts +1 -0
  89. package/lib/sandbox/commands/create.ts +7 -1
  90. package/lib/sandbox/commands/ls.ts +4 -5
  91. package/lib/sandbox/commands/rm.ts +1 -1
  92. package/lib/sandbox/control/agent-operation.ts +49 -0
  93. package/lib/sandbox/control/client.ts +20 -254
  94. package/lib/sandbox/control/executor.ts +13 -136
  95. package/lib/sandbox/control/lifecycle.ts +67 -4
  96. package/lib/sandbox/control/protocol.ts +22 -66
  97. package/lib/sandbox/control/server.ts +21 -95
  98. package/lib/sandbox/control/task-view.ts +0 -19
  99. package/lib/sandbox/mounts.ts +10 -1
  100. package/lib/sandbox/recovery.ts +38 -17
  101. package/lib/sandbox/removal.ts +33 -7
  102. package/lib/sandbox/workspace-identity.ts +16 -6
  103. package/lib/sandbox/workspace-view.ts +50 -18
  104. package/lib/task/commands/ls.ts +3 -3
  105. package/lib/task/control-authority.ts +66 -378
  106. package/lib/task/control-recovery.ts +3 -4
  107. package/lib/task/create.ts +3 -3
  108. package/lib/task/delegation-receipts.ts +87 -231
  109. package/lib/task/events.ts +100 -42
  110. package/lib/task/finalization.ts +37 -7
  111. package/lib/task/lifecycle.ts +257 -109
  112. package/lib/task/orchestration.ts +260 -237
  113. package/lib/task/runtime-paths.ts +12 -0
  114. package/lib/task/short-id.ts +33 -40
  115. package/lib/task/verification-engine.ts +30 -25
  116. package/lib/task/workspace-snapshot.ts +1 -1
  117. package/package.json +1 -1
  118. package/templates/.agents/hooks/lifecycle-delegation.js +1 -25
  119. package/templates/.agents/rules/README.en.md +2 -1
  120. package/templates/.agents/rules/README.zh-CN.md +2 -1
  121. package/templates/.agents/rules/next-step-output.en.md +6 -19
  122. package/templates/.agents/rules/next-step-output.zh-CN.md +6 -19
  123. package/templates/.agents/rules/strategy-pattern.en.md +25 -0
  124. package/templates/.agents/rules/strategy-pattern.zh-CN.md +25 -0
  125. package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
  126. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -1
  127. package/templates/.agents/skills/check-task/SKILL.en.md +1 -1
  128. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +1 -1
  129. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  130. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
  131. package/templates/.agents/skills/code-task/reference/code-rules.en.md +4 -0
  132. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +4 -0
  133. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +0 -1
  134. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +0 -1
  135. package/templates/.agents/skills/complete-task/SKILL.en.md +5 -29
  136. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +5 -26
  137. package/templates/.agents/skills/plan-task/SKILL.en.md +7 -4
  138. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +7 -4
  139. package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
  140. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
  141. package/templates/.agents/skills/review-code/SKILL.en.md +1 -1
  142. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +1 -1
  143. package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
  144. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
  145. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -1
  146. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -1
  147. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  148. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  149. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +0 -546
  150. package/dist/lib/internal/codex-lifecycle.js +0 -385
  151. package/dist/lib/task/codex-orchestration.js +0 -206
  152. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +0 -776
  153. package/lib/internal/codex-lifecycle.ts +0 -390
  154. package/lib/task/codex-orchestration.ts +0 -276
@@ -0,0 +1,10 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { resolveTaskRef } from './resolve-ref.js';
4
+ export function resolveTaskRuntimeRoot(taskRef, options = {}) {
5
+ const resolved = resolveTaskRef(taskRef, options);
6
+ if (!resolved.ok)
7
+ throw new Error(`${resolved.code}: ${resolved.message}`);
8
+ return path.join(fs.realpathSync.native(resolved.taskDir), '.runtime');
9
+ }
10
+ //# sourceMappingURL=runtime-paths.js.map
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { parseTypedTaskFrontmatter } from './frontmatter.js';
3
4
  const REGISTRY_NAME = '.short-ids.json';
4
5
  const LOCK_NAME = '.short-ids.json.lock';
5
6
  function normalizeShortIdInput(input, opts) {
@@ -90,20 +91,33 @@ function resolveShortIdReadOnly(input, repoRoot, opts) {
90
91
  const taskId = ids[key];
91
92
  if (!taskId) {
92
93
  const message = Object.keys(ids).length === 0
93
- ? `short id '${key}' not found; active task registry is empty.`
94
- : `short id '${key}' not found in active task registry (it may have been cleaned up after archival; check 'task-short-id.js list').`;
94
+ ? `short id '${key}' not found; task short-id registry is empty.`
95
+ : `short id '${key}' not found in task short-id registry.`;
95
96
  return shortIdFailure('SHORT_ID_NOT_FOUND', message);
96
97
  }
97
- const taskMdPath = path.join(repoRoot, '.agents', 'workspace', 'active', taskId, 'task.md');
98
- if (!fs.existsSync(taskMdPath)) {
99
- const remainingActiveEntries = Object.values(ids).filter((candidateTaskId) => fs.existsSync(path.join(repoRoot, '.agents', 'workspace', 'active', candidateTaskId, 'task.md')));
100
- const message = remainingActiveEntries.length === 0
101
- ? `short id '${key}' not found; active task registry is empty.`
102
- : `short id '${key}' not found in active task registry (it may have been cleaned up after archival; check 'task-short-id.js list').`;
103
- return shortIdFailure('SHORT_ID_STALE', message, taskId);
98
+ const activeTask = path.join(repoRoot, '.agents', 'workspace', 'active', taskId, 'task.md');
99
+ const completedTask = path.join(repoRoot, '.agents', 'workspace', 'completed', taskId, 'task.md');
100
+ let isValidCompletedTask = false;
101
+ try {
102
+ const task = parseTypedTaskFrontmatter(fs.readFileSync(completedTask, 'utf8'));
103
+ isValidCompletedTask = task.id === taskId && task.status === 'completed';
104
+ }
105
+ catch { /* a mapping is valid only while its task content is present */ }
106
+ if (!fs.existsSync(activeTask) && !isValidCompletedTask) {
107
+ return shortIdFailure('SHORT_ID_STALE', `short id '${key}' points to no active or completed task content.`, taskId);
104
108
  }
105
109
  return { ok: true, taskId };
106
110
  }
111
+ function hasRetainedCompletedTask(activeDir, taskId) {
112
+ try {
113
+ const taskPath = path.join(path.dirname(activeDir), 'completed', taskId, 'task.md');
114
+ const task = parseTypedTaskFrontmatter(fs.readFileSync(taskPath, 'utf8'));
115
+ return task.id === taskId && task.status === 'completed';
116
+ }
117
+ catch {
118
+ return false;
119
+ }
120
+ }
107
121
  function configuredShortIdLength(repoRoot) {
108
122
  try {
109
123
  const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8'));
@@ -206,7 +220,8 @@ function mutateShortIdRegistryAt(activeDir, width, taskId, requested, onlyReleas
206
220
  const original = JSON.stringify(registry.ids);
207
221
  if (pruneOrphans) {
208
222
  for (const [key, candidate] of Object.entries(registry.ids)) {
209
- if (candidate !== taskId && !fs.existsSync(path.join(activeDir, candidate, 'task.md')))
223
+ if (candidate !== taskId && !fs.existsSync(path.join(activeDir, candidate, 'task.md'))
224
+ && !hasRetainedCompletedTask(activeDir, candidate))
210
225
  delete registry.ids[key];
211
226
  }
212
227
  }
@@ -277,34 +292,29 @@ function executeShortIdCommand(request) {
277
292
  if (normalized.kind !== 'shortId') {
278
293
  throw Object.assign(new Error(`invalid short id format '${request.argument}'`), { code: 'SHORT_ID_FORMAT_INVALID' });
279
294
  }
280
- return withRegistryLock(request.activeDir, () => {
281
- const registry = readRegistryStrict(registryPath, request.shortIdLength);
282
- let changed = false;
283
- for (const [key, taskId] of Object.entries(registry.ids)) {
284
- if (!fs.existsSync(path.join(request.activeDir, taskId, 'task.md'))) {
285
- delete registry.ids[key];
286
- changed = true;
287
- }
288
- }
289
- if (changed)
290
- writeRegistry(registryPath, registry);
291
- const taskId = registry.ids[normalized.value];
292
- if (!taskId)
293
- throw Object.assign(new Error(`short id '${normalized.value}' not found in active task registry`), { code: 'SHORT_ID_NOT_FOUND' });
294
- return { status: changed ? 'applied' : 'no-op', changed, output: taskId, error: null };
295
- });
295
+ const registry = readRegistryStrict(registryPath, request.shortIdLength);
296
+ const taskId = registry.ids[normalized.value];
297
+ if (!taskId)
298
+ throw Object.assign(new Error(`short id '${normalized.value}' not found in task short-id registry`), { code: 'SHORT_ID_NOT_FOUND' });
299
+ if (!fs.existsSync(path.join(request.activeDir, taskId, 'task.md'))
300
+ && !hasRetainedCompletedTask(request.activeDir, taskId)) {
301
+ throw Object.assign(new Error(`short id '${normalized.value}' points to a task without active or completed task content`), { code: 'SHORT_ID_STALE' });
302
+ }
303
+ return { status: 'no-op', changed: false, output: taskId, error: null };
296
304
  }
297
305
  const registry = readRegistryStrict(registryPath, request.shortIdLength);
298
306
  if (request.verify) {
299
307
  const active = fs.existsSync(request.activeDir)
300
308
  ? fs.readdirSync(request.activeDir).filter((entry) => /^TASK-\d{8}-\d{6}$/.test(entry) && fs.existsSync(path.join(request.activeDir, entry, 'task.md')))
301
309
  : [];
310
+ const retained = Object.values(registry.ids).filter((taskId) => hasRetainedCompletedTask(request.activeDir, taskId));
311
+ const validTasks = new Set([...active, ...retained]);
302
312
  const registered = new Map();
303
313
  for (const [key, taskId] of Object.entries(registry.ids))
304
314
  registered.set(taskId, [...(registered.get(taskId) ?? []), key]);
305
315
  const diff = {
306
316
  missing_in_registry: active.filter((taskId) => !registered.has(taskId)).map((taskId) => ({ taskId })),
307
- orphans_in_registry: Object.entries(registry.ids).filter(([, taskId]) => !active.includes(taskId)).map(([key, taskId]) => ({ key, taskId })),
317
+ orphans_in_registry: Object.entries(registry.ids).filter(([, taskId]) => !validTasks.has(taskId)).map(([key, taskId]) => ({ key, taskId })),
308
318
  duplicate_registry_keys: [...registered.entries()].filter(([, keys]) => keys.length > 1).map(([taskId, keys]) => ({ taskId, keys }))
309
319
  };
310
320
  const clean = Object.values(diff).every((items) => items.length === 0);
@@ -31,6 +31,7 @@ import { manualValidationFinalSummaryProjectionMatches } from "./manual-validati
31
31
  import { readManualValidationCompletion } from "./manual-validation-completion.js";
32
32
  import { sha256File } from "./artifact-receipts.js";
33
33
  import { inspectArtifactDirectory } from "./artifact-lifecycle.js";
34
+ import { getAgentClientAdapter } from "../agent-clients/registry.js";
34
35
  import { parseArtifactName } from "./artifact-name.js";
35
36
  import { summaryCommentState } from "../platform/pr-summary.js";
36
37
  import { taskIssueIdentity } from "../platform/task-identities.js";
@@ -124,7 +125,7 @@ async function runCheck(type, context, shared) {
124
125
  }
125
126
  }
126
127
  function checkOrchestrationState({ taskDir }) {
127
- const file = path.join(taskDir, 'orchestration.json');
128
+ const file = path.join(taskDir, '.runtime', 'orchestration.json');
128
129
  const stat = safeStat(file);
129
130
  if (!stat?.isFile())
130
131
  return failResult('orchestration-state', 'orchestration.json is missing');
@@ -139,18 +140,16 @@ function checkOrchestrationState({ taskDir }) {
139
140
  if (!run || !['paused', 'completed'].includes(run.status)) {
140
141
  return failResult('orchestration-state', `Expected paused or completed run, received '${run?.status ?? 'missing'}'`);
141
142
  }
143
+ const adapterEvidenceError = validateAdapterReceiptEvidence(run);
144
+ if (adapterEvidenceError)
145
+ return failResult('orchestration-state', adapterEvidenceError);
142
146
  if (run.status === 'paused' && (!run.pause?.code || !run.pause?.message)) {
143
147
  return failResult('orchestration-state', 'Paused run requires a stable pause code and message');
144
148
  }
145
149
  if (run.status === 'completed') {
146
- if (run.completionEvidence != null) {
147
- const evidenceError = validateCleanCompletionEvidence(run);
148
- if (evidenceError)
149
- return failResult('orchestration-state', evidenceError);
150
- }
151
- else if (run.pendingDelegation !== null || !run.commitAuthorization?.consumedAt) {
152
- return failResult('orchestration-state', 'Completed run must consume commit authorization and clear pending delegation');
153
- }
150
+ const evidenceError = validateCleanCompletionEvidence(run);
151
+ if (evidenceError)
152
+ return failResult('orchestration-state', evidenceError);
154
153
  }
155
154
  else if (run.completionEvidence != null) {
156
155
  return failResult('orchestration-state', 'Clean completion evidence requires a completed run');
@@ -182,15 +181,23 @@ function validateCleanCompletionEvidence(run) {
182
181
  }
183
182
  if (run.status !== 'completed'
184
183
  || run.pendingDelegation !== null
185
- || run.commitAuthorization?.issuedAt !== null
186
- || run.commitAuthorization?.consumedAt !== null
187
184
  || run.receipts.some((receipt) => receipt.stage === 'commit')) {
188
- return 'Clean completion evidence conflicts with commit delegation state';
185
+ return 'Clean completion evidence conflicts with pending delegation or commit receipt state';
186
+ }
187
+ return null;
188
+ }
189
+ function validateAdapterReceiptEvidence(run) {
190
+ const receipts = [...run.receipts, ...(run.pendingDelegation ? [run.pendingDelegation] : [])];
191
+ for (const receipt of receipts) {
192
+ const validate = getAgentClientAdapter(receipt.client).orchestrationAdapter?.validateReceiptEvidence;
193
+ if (validate && !validate(receipt)) {
194
+ return `Receipt '${receipt.id}' has invalid evidence from its agent adapter`;
195
+ }
189
196
  }
190
197
  return null;
191
198
  }
192
199
  function checkOrchestrationEvidence({ taskDir }) {
193
- const file = path.join(taskDir, 'orchestration.json');
200
+ const file = path.join(taskDir, '.runtime', 'orchestration.json');
194
201
  const stat = safeStat(file);
195
202
  if (!stat?.isFile())
196
203
  return failResult('orchestration-evidence', 'orchestration.json is missing');
@@ -204,6 +211,9 @@ function checkOrchestrationEvidence({ taskDir }) {
204
211
  }
205
212
  if (!run)
206
213
  return failResult('orchestration-evidence', 'orchestration.json is missing');
214
+ const adapterEvidenceError = validateAdapterReceiptEvidence(run);
215
+ if (adapterEvidenceError)
216
+ return failResult('orchestration-evidence', adapterEvidenceError);
207
217
  // readRun validates the persisted schema, receipt identities and client provenance.
208
218
  const policy = run.modelPolicy;
209
219
  if (run.completionEvidence != null) {
@@ -228,20 +238,22 @@ function checkOrchestrationEvidence({ taskDir }) {
228
238
  }
229
239
  const activated = ['activated', 'stage-completed', 'sealed', 'consumed'].includes(receipt.status);
230
240
  if (activated) {
231
- const isClaudeCode = receipt.client === 'claude-code';
232
- if (!isClaudeCode) {
241
+ const evidencePolicy = getAgentClientAdapter(receipt.client).delegationEvidence;
242
+ if (evidencePolicy.actualModelRequired !== false) {
233
243
  if (!exactText(receipt.actualModel)) {
234
244
  return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has no host-observed actual model`);
235
245
  }
246
+ }
247
+ if (evidencePolicy.actualReasoningEffortRequired !== false) {
236
248
  if (!exactText(receipt.actualReasoningEffort)) {
237
249
  return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has no host-observed actual reasoning effort`);
238
250
  }
239
251
  }
240
- if (!isClaudeCode && receipt.spawnMode !== 'fresh') {
252
+ if (evidencePolicy.spawnModeRequired !== false && receipt.spawnMode !== 'fresh') {
241
253
  return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has invalid fresh delegation identity`);
242
254
  }
243
255
  if (receipt.actualModel != null && receipt.actualModel !== receipt.requestedModel) {
244
- if (!isClaudeCode && !exactText(receipt.modelFallbackReason)) {
256
+ if (evidencePolicy.fallbackReasonRequired !== false && !exactText(receipt.modelFallbackReason)) {
245
257
  return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' model fallback is not justified`);
246
258
  }
247
259
  }
@@ -250,7 +262,7 @@ function checkOrchestrationEvidence({ taskDir }) {
250
262
  return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has an unrelated model fallback reason`);
251
263
  }
252
264
  if (receipt.actualReasoningEffort != null && receipt.actualReasoningEffort !== receipt.requestedReasoningEffort) {
253
- if (!isClaudeCode && !exactText(receipt.reasoningEffortFallbackReason)) {
265
+ if (evidencePolicy.fallbackReasonRequired !== false && !exactText(receipt.reasoningEffortFallbackReason)) {
254
266
  return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' reasoning-effort fallback is not justified`);
255
267
  }
256
268
  }
@@ -700,7 +712,6 @@ function checkActivityLog({ taskDir, config, skillName, artifactFile }) {
700
712
  const entries = section.entries;
701
713
  if (entries.length === 0)
702
714
  return failResult("activity-log", "Activity Log has no entries");
703
- let previousTimestamp = "";
704
715
  let latestAction = "";
705
716
  let latestTimestamp = "";
706
717
  const doneActions = [];
@@ -708,13 +719,8 @@ function checkActivityLog({ taskDir, config, skillName, artifactFile }) {
708
719
  if (!entry.note.trim())
709
720
  return failResult("activity-log", "Activity Log entry note is required");
710
721
  const { time: timestamp, step: action } = entry;
711
- if (previousTimestamp && timestamp < previousTimestamp) {
712
- return failResult("activity-log", "Activity Log timestamps are not in ascending order");
713
- }
714
- previousTimestamp = timestamp;
715
- // Ascending order is checked over every entry, but a `[started]` marker is
716
- // not a terminal action: keep latestAction/latestTimestamp on the most
717
- // recent done entry so expected_action_pattern sees it.
722
+ // A `[started]` marker is not a terminal action: keep latestAction/latestTimestamp
723
+ // on the most recent done entry so expected_action_pattern sees it.
718
724
  if (!ACTIVITY_LOG_STARTED_RE.test(action)) {
719
725
  latestTimestamp = timestamp;
720
726
  latestAction = action;
@@ -45,7 +45,7 @@ function taskFileFingerprints(stateRoot, taskId) {
45
45
  }
46
46
  if (!stat.isFile() && !stat.isSymbolicLink())
47
47
  return;
48
- if (relative.startsWith('.agents/workspace/active/') && path.posix.basename(relative) === 'orchestration.json')
48
+ if (/^\.agents\/workspace\/(?:active|blocked|completed)\/TASK-[0-9]{8}-[0-9]{6}\/\.runtime\/orchestration\.json$/u.test(relative))
49
49
  return;
50
50
  const content = stat.isSymbolicLink()
51
51
  ? Buffer.from(fs.readlinkSync(absolute))
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "protocolVersion": 3,
3
- "packageVersion": "0.11.11",
4
- "sourceInputHash": "576098e3f88ceca11a0a0f94573a3ceca2b14c97b29a6b934ae07849e214c889",
5
- "internalExecutableBuildHash": "065375c31d3564a65236fc43bad9947395ea0873ae2531d9b05f42956ef30f1e"
3
+ "packageVersion": "0.11.13",
4
+ "sourceInputHash": "70e89f362895c1f8f976011715a86b47e06c105ca4285eacbb3fa51d0459a282",
5
+ "internalExecutableBuildHash": "f183e6d39ba985dbb2962eb951b4019af9594361ca3c46b88bf65fff325d7ad4"
6
6
  }
@@ -23,6 +23,27 @@ import type {
23
23
  SandboxTool,
24
24
  SandboxToolContext
25
25
  } from '../sandbox/tool-types.ts';
26
+ import type { SandboxControlManifest } from '../sandbox/control/protocol.ts';
27
+
28
+ type AgentClientSandboxControlOperation = Readonly<{
29
+ validate: (params: Readonly<{
30
+ operation: string;
31
+ payload: Record<string, unknown>;
32
+ manifest: SandboxControlManifest;
33
+ }>) => void;
34
+ execute: (params: Readonly<{
35
+ operation: string;
36
+ payload: Record<string, unknown>;
37
+ manifest: SandboxControlManifest;
38
+ manifestPath: string;
39
+ }>) => Promise<Readonly<{ exitCode: number; stdout: string; stderr: string }>>;
40
+ recover?: (params: Readonly<{
41
+ operation: string;
42
+ manifest: SandboxControlManifest;
43
+ manifestPath: string;
44
+ stdout: string | null;
45
+ }>) => Readonly<Record<string, unknown>>;
46
+ }>;
26
47
 
27
48
  type AgentClientCapabilities = AgentClientCapabilityMap;
28
49
 
@@ -46,6 +67,10 @@ type AgentClientModelSelectionContext =
46
67
  type AgentClientDelegationEvidence = Readonly<{
47
68
  actualModel: 'host-event' | 'app-server' | 'unavailable';
48
69
  actualReasoningEffort: 'host-event' | 'app-server' | 'spawn-ack' | 'unavailable';
70
+ spawnModeRequired?: boolean;
71
+ actualModelRequired?: boolean;
72
+ actualReasoningEffortRequired?: boolean;
73
+ fallbackReasonRequired?: boolean;
49
74
  }>;
50
75
 
51
76
  type AgentClientLifecycleRecoveryRequest = Readonly<{
@@ -68,6 +93,12 @@ type AgentClientLifecycleRecoveryResult = Readonly<{
68
93
  }>;
69
94
 
70
95
  type AgentClientOrchestrationAdapter = Readonly<{
96
+ validateActivationEvidence?: (evidence: unknown, parentId: string) => boolean;
97
+ validateReceiptEvidence?: (receipt: unknown) => boolean;
98
+ createLaunchCarrier?: (
99
+ adapterContext: string,
100
+ identity: Readonly<{ stage: string; round: number; role: string }>
101
+ ) => Readonly<Record<string, string>>;
71
102
  prepareDelegation?: (
72
103
  taskRef: string,
73
104
  input: Readonly<{
@@ -116,6 +147,7 @@ type AgentClientAdapter = Readonly<{
116
147
  modelSelection: AgentClientModelSelectionContext;
117
148
  delegationEvidence: AgentClientDelegationEvidence;
118
149
  orchestrationAdapter?: AgentClientOrchestrationAdapter;
150
+ sandboxControlOperation?: AgentClientSandboxControlOperation;
119
151
  project: AgentClientProjectDescriptor;
120
152
  sandbox: AgentClientSandboxDescriptor;
121
153
  }>;
@@ -395,6 +427,9 @@ function defineAgentClientAdapter(
395
427
  !evidence
396
428
  || !['host-event', 'app-server', 'unavailable'].includes(evidence.actualModel)
397
429
  || !['host-event', 'app-server', 'spawn-ack', 'unavailable'].includes(evidence.actualReasoningEffort)
430
+ || ['spawnModeRequired', 'actualModelRequired', 'actualReasoningEffortRequired', 'fallbackReasonRequired']
431
+ .some((key) => evidence[key as keyof AgentClientDelegationEvidence] !== undefined
432
+ && typeof evidence[key as keyof AgentClientDelegationEvidence] !== 'boolean')
398
433
  ) {
399
434
  throw new Error(`Agent Client '${candidate.id}' has invalid delegation evidence`);
400
435
  }
@@ -693,6 +728,9 @@ function defineAgentClientAdapter(
693
728
  ...(candidate.orchestrationAdapter === undefined
694
729
  ? {}
695
730
  : { orchestrationAdapter: Object.freeze({ ...candidate.orchestrationAdapter }) }),
731
+ ...(candidate.sandboxControlOperation === undefined
732
+ ? {}
733
+ : { sandboxControlOperation: Object.freeze({ ...candidate.sandboxControlOperation }) }),
696
734
  project: Object.freeze({
697
735
  ownedPathPrefixes: Object.freeze(paths),
698
736
  ...projectAssets,
@@ -715,6 +753,7 @@ export type {
715
753
  AgentClientCapabilities,
716
754
  AgentClientCustomCommandDescriptor,
717
755
  AgentClientDelegationEvidence,
756
+ AgentClientSandboxControlOperation,
718
757
  AgentClientLifecycleRecoveryRequest,
719
758
  AgentClientLifecycleRecoveryResult,
720
759
  AgentClientOrchestrationAdapter,
@@ -27,7 +27,11 @@ const claudeCodeAdapter = defineAgentClientAdapter({
27
27
  },
28
28
  delegationEvidence: {
29
29
  actualModel: 'host-event',
30
- actualReasoningEffort: 'spawn-ack'
30
+ actualReasoningEffort: 'spawn-ack',
31
+ spawnModeRequired: false,
32
+ actualModelRequired: false,
33
+ actualReasoningEffortRequired: false,
34
+ fallbackReasonRequired: false
31
35
  },
32
36
  project: {
33
37
  ownedPathPrefixes: ['.claude/'],
@@ -7,7 +7,9 @@ import semver from 'semver';
7
7
 
8
8
  import type { ChildProcessWithoutNullStreams } from 'node:child_process';
9
9
  import type { CodexLifecycleEvent } from './evidence.ts';
10
- import { resolveAgentRuntimeStoreRoot } from '../../../runtime/agent-runtime.ts';
10
+ import { parseCodexLifecycleBinding } from './binding.ts';
11
+ import { hasActiveCodexLifecycleEvidence } from './store.ts';
12
+ import { resolveTaskContext, resolveTaskRef } from '../../../task/resolve-ref.ts';
11
13
 
12
14
  type JsonObject = Record<string, unknown>;
13
15
  type ThreadResolution = Readonly<{
@@ -71,26 +73,14 @@ function nonEmpty(value: unknown): string | null {
71
73
  return typeof value === 'string' && value.trim() ? value : null;
72
74
  }
73
75
 
74
- function resolveCodexSpawnedChild(
75
- transcriptPath: string,
76
- expected: Readonly<{
77
- sessionId: string;
78
- turnId: string;
79
- toolUseId: string;
80
- nativeAgent: string;
81
- taskName: string;
82
- requestedModel?: string;
83
- requestedReasoningEffort?: string;
84
- }>
85
- ): string {
76
+ function readRolloutRecords(transcriptPath: string, sessionId: string): JsonObject[] {
86
77
  const name = path.basename(transcriptPath);
87
- if (!transcriptPath || (!name.endsWith(`-${expected.sessionId}.jsonl`) && name !== `${expected.sessionId}.jsonl`)) {
78
+ if (!transcriptPath || (!name.endsWith(`-${sessionId}.jsonl`) && name !== `${sessionId}.jsonl`)) {
88
79
  throw new Error('Codex parent rollout path is invalid');
89
80
  }
90
81
  const stat = fs.lstatSync(transcriptPath);
91
82
  if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('Codex parent rollout is not a regular file');
92
- const maxBytes = 8 * 1024 * 1024;
93
- const offset = Math.max(0, stat.size - maxBytes);
83
+ const offset = Math.max(0, stat.size - 8 * 1024 * 1024);
94
84
  const buffer = Buffer.alloc(stat.size - offset);
95
85
  const descriptor = fs.openSync(transcriptPath, 'r');
96
86
  try {
@@ -100,37 +90,61 @@ function resolveCodexSpawnedChild(
100
90
  }
101
91
  let text = buffer.toString('utf8');
102
92
  if (offset) text = text.slice(text.indexOf('\n') + 1);
103
- const records = text.split(/\r?\n/).filter(Boolean).map((line) => object(JSON.parse(line))).filter(Boolean);
104
- const calls = records.filter((record) => {
105
- const payload = object(record?.payload);
106
- return record?.type === 'response_item'
107
- && payload?.type === 'function_call'
108
- && payload.call_id === expected.toolUseId
109
- && payload.namespace === 'collaboration'
110
- && payload.name === 'spawn_agent';
111
- });
93
+ return text.split(/\r?\n/u).filter(Boolean).map((line) => object(JSON.parse(line))).filter((record): record is JsonObject => Boolean(record));
94
+ }
95
+
96
+ function isExpectedSpawnCall(record: JsonObject, expected: Parameters<typeof resolveCodexSpawnedChild>[1]): boolean {
97
+ const payload = object(record.payload);
98
+ return record.type === 'response_item'
99
+ && payload?.type === 'function_call'
100
+ && payload.call_id === expected.toolUseId
101
+ && payload.namespace === 'collaboration'
102
+ && payload.name === 'spawn_agent';
103
+ }
104
+
105
+ function spawnArgsMatch(args: JsonObject | null, expected: Parameters<typeof resolveCodexSpawnedChild>[1]): boolean {
106
+ return 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
+ }
111
+
112
+ function isExpectedChildActivity(record: JsonObject, expected: Parameters<typeof resolveCodexSpawnedChild>[1]): boolean {
113
+ const payload = object(record.payload);
114
+ const item = object(payload?.item);
115
+ return record.type === 'event_msg'
116
+ && payload?.type === 'item_completed'
117
+ && payload.thread_id === expected.sessionId
118
+ && payload.turn_id === expected.turnId
119
+ && item?.type === 'SubAgentActivity'
120
+ && item.id === expected.toolUseId
121
+ && item.kind === 'started'
122
+ && item.agent_path === `/root/${expected.taskName}`
123
+ && Boolean(nonEmpty(item.agent_thread_id));
124
+ }
125
+
126
+ function resolveCodexSpawnedChild(
127
+ transcriptPath: string,
128
+ expected: Readonly<{
129
+ sessionId: string;
130
+ turnId: string;
131
+ toolUseId: string;
132
+ nativeAgent: string;
133
+ taskName: string;
134
+ requestedModel?: string;
135
+ requestedReasoningEffort?: string;
136
+ }>
137
+ ): string {
138
+ if (!parseCodexLifecycleBinding(expected.taskName)) {
139
+ throw new Error('Codex spawn task_name does not carry a valid lifecycle binding');
140
+ }
141
+ const records = readRolloutRecords(transcriptPath, expected.sessionId);
142
+ const calls = records.filter((record) => isExpectedSpawnCall(record, expected));
112
143
  if (calls.length !== 1) throw new Error('Codex parent rollout spawn call was not found uniquely');
113
144
  const call = object(calls[0]?.payload);
114
145
  const args = object(JSON.parse(nonEmpty(call?.arguments) ?? '{}'));
115
- if (
116
- args?.agent_type !== expected.nativeAgent
117
- || args.task_name !== expected.taskName
118
- || (expected.requestedModel && args.model !== expected.requestedModel)
119
- || (expected.requestedReasoningEffort && args.reasoning_effort !== expected.requestedReasoningEffort)
120
- ) throw new Error('Codex parent rollout spawn identity does not match the hook event');
121
- const activities = records.filter((record) => {
122
- const payload = object(record?.payload);
123
- const item = object(payload?.item);
124
- return record?.type === 'event_msg'
125
- && payload?.type === 'item_completed'
126
- && payload.thread_id === expected.sessionId
127
- && payload.turn_id === expected.turnId
128
- && item?.type === 'SubAgentActivity'
129
- && item.id === expected.toolUseId
130
- && item.kind === 'started'
131
- && item.agent_path === `/root/${expected.taskName}`
132
- && nonEmpty(item.agent_thread_id);
133
- });
146
+ if (!spawnArgsMatch(args, expected)) throw new Error('Codex parent rollout spawn identity does not match the hook event');
147
+ const activities = records.filter((record) => isExpectedChildActivity(record, expected));
134
148
  if (activities.length !== 1) throw new Error('Codex parent rollout child activity was not found uniquely');
135
149
  return nonEmpty(object(object(activities[0]?.payload)?.item)?.agent_thread_id)!;
136
150
  }
@@ -198,30 +212,6 @@ function parseCodexHooksList(
198
212
  return Object.freeze(hooks);
199
213
  }
200
214
 
201
- function hasCodexRuntimeLiveness(
202
- runtimeRoot: string,
203
- hookDefinitionHash: string,
204
- identity?: CodexRuntimeIdentity
205
- ): boolean {
206
- if (!identity || !fs.existsSync(runtimeRoot)) return false;
207
- return fs.readdirSync(runtimeRoot)
208
- .filter((name) => /^[a-f0-9]{64}\.json$/.test(name))
209
- .some((name) => {
210
- try {
211
- const record = object(JSON.parse(fs.readFileSync(path.join(runtimeRoot, name), 'utf8')));
212
- const state = object(record?.state);
213
- const spawnEvent = object(state?.spawn);
214
- return !['invalid', 'expired'].includes(String(state?.status))
215
- && spawnEvent?.hookDefinitionHash === hookDefinitionHash
216
- && spawnEvent.sessionId === identity.sessionId
217
- && spawnEvent.turnId === identity.turnId
218
- && spawnEvent.toolUseId === identity.toolUseId;
219
- } catch {
220
- return false;
221
- }
222
- });
223
- }
224
-
225
215
  function parseCodexThreadResolution(readValue: unknown, rolloutRecords: readonly unknown[]): ThreadResolution {
226
216
  const readRoot = object(readValue);
227
217
  const thread = object(readRoot?.thread);
@@ -278,6 +268,20 @@ function parseCodexThreadResolution(readValue: unknown, rolloutRecords: readonly
278
268
  });
279
269
  }
280
270
 
271
+ function resolvePreflightTaskDir(
272
+ repoRoot: string,
273
+ taskId?: string
274
+ ): string | undefined {
275
+ try {
276
+ const context = taskId
277
+ ? resolveTaskRef(taskId, { repoRoot })
278
+ : resolveTaskContext(undefined, { repoRoot });
279
+ return context.ok ? context.taskDir : undefined;
280
+ } catch {
281
+ return undefined;
282
+ }
283
+ }
284
+
281
285
  function readCodexRolloutRecords(readValue: unknown, childThreadId: string): readonly unknown[] {
282
286
  const thread = object(object(readValue)?.thread);
283
287
  const rolloutPath = nonEmpty(thread?.path);
@@ -570,8 +574,27 @@ async function resolveCodexTerminal(
570
574
 
571
575
  async function preflightCodexLifecycleEvidence(
572
576
  repoRoot: string = process.cwd(),
573
- runtimeIdentity?: CodexRuntimeIdentity
577
+ runtimeIdentity?: CodexRuntimeIdentity,
578
+ taskId?: string
574
579
  ) {
580
+ const { cliVersion, hooksRaw } = validateStaticCodexPreflight(repoRoot);
581
+ const hookDefinitionHash = crypto.createHash('sha256').update(hooksRaw).digest('hex');
582
+ const taskDir = resolvePreflightTaskDir(repoRoot, taskId);
583
+ const runtimeLiveness = Boolean(taskDir && runtimeIdentity
584
+ && hasActiveCodexLifecycleEvidence(taskDir, { hookDefinitionHash, identity: runtimeIdentity }));
585
+ const host = await discoverLifecycleHooks(repoRoot);
586
+ return Object.freeze({
587
+ cliVersion,
588
+ hookDefinitionHash,
589
+ staticReady: true,
590
+ discoveredHooks: host.discoveredHooks,
591
+ hookProvenance: host.hookProvenance,
592
+ runtimeLiveness,
593
+ diagnostics: host.diagnostics
594
+ });
595
+ }
596
+
597
+ function validateStaticCodexPreflight(repoRoot: string): Readonly<{ cliVersion: string; hooksRaw: string }> {
575
598
  const versionRun = spawnSync('codex', ['--version'], { encoding: 'utf8' });
576
599
  const match = /codex-cli\s+(\d+\.\d+\.\d+)/.exec(versionRun.stdout ?? '');
577
600
  if (versionRun.status !== 0 || !match?.[1] || !semver.gte(match[1], '0.147.0')) {
@@ -599,11 +622,18 @@ async function preflightCodexLifecycleEvidence(
599
622
  } finally {
600
623
  fs.rmSync(schemaDir, { recursive: true, force: true });
601
624
  }
625
+ return { cliVersion: match[1]!, hooksRaw };
626
+ }
602
627
 
603
- const hookDefinitionHash = crypto.createHash('sha256').update(hooksRaw).digest('hex');
604
- const runtimeRoot = resolveAgentRuntimeStoreRoot({ repoRoot, store: 'lifecycle' });
605
- const runtimeLiveness = hasCodexRuntimeLiveness(runtimeRoot, hookDefinitionHash, runtimeIdentity);
606
-
628
+ async function discoverLifecycleHooks(repoRoot: string): Promise<Readonly<{
629
+ discoveredHooks: readonly DiscoveredHook[];
630
+ hookProvenance: Readonly<{
631
+ hookSource: 'isolated-user' | 'managed' | 'project';
632
+ hookSourcePathDigest: string;
633
+ hookSourceHash: string;
634
+ }>;
635
+ diagnostics: readonly unknown[];
636
+ }>> {
607
637
  const transport = new CodexAppServerTransport();
608
638
  transport.start();
609
639
  let discoveredHooks: readonly DiscoveredHook[];
@@ -633,10 +663,7 @@ async function preflightCodexLifecycleEvidence(
633
663
  const hookSource = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT
634
664
  ? 'isolated-user'
635
665
  : source === 'managed' ? 'managed' : 'project';
636
- return Object.freeze({
637
- cliVersion: match[1],
638
- hookDefinitionHash,
639
- staticReady: true,
666
+ return {
640
667
  discoveredHooks,
641
668
  hookProvenance: Object.freeze({
642
669
  hookSource,
@@ -648,14 +675,12 @@ async function preflightCodexLifecycleEvidence(
648
675
  currentHash: hook.currentHash
649
676
  })))).digest('hex')
650
677
  }),
651
- runtimeLiveness,
652
678
  diagnostics: transport.diagnostics
653
- });
679
+ };
654
680
  }
655
681
 
656
682
  export {
657
683
  CodexAppServerTransport,
658
- hasCodexRuntimeLiveness,
659
684
  parseCodexModelReroute,
660
685
  parseCodexHooksList,
661
686
  parseCodexThreadResolution,