@fitlab-ai/agent-infra 0.9.6 → 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 (127) hide show
  1. package/dist/bin/internal-cli.js +5 -0
  2. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +45 -4
  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/sandbox-controller.js +370 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +21 -5
  7. package/dist/lib/defaults.json +10 -2
  8. package/dist/lib/git/workflow.js +30 -3
  9. package/dist/lib/internal/codex-lifecycle.js +69 -1
  10. package/dist/lib/internal/codex-sandbox-controller.js +106 -0
  11. package/dist/lib/internal/task-orchestration.js +37 -5
  12. package/dist/lib/platform/context.js +6 -4
  13. package/dist/lib/platform/github-client.js +55 -2
  14. package/dist/lib/process-data/checkpoint.js +319 -0
  15. package/dist/lib/process-data/index.js +128 -60
  16. package/dist/lib/process-data/materialize.js +84 -0
  17. package/dist/lib/process-data/normalize.js +34 -1
  18. package/dist/lib/process-data/reconcile.js +55 -1
  19. package/dist/lib/process-data/sources.js +422 -1
  20. package/dist/lib/process-data/store.js +341 -11
  21. package/dist/lib/sandbox/commands/create.js +103 -51
  22. package/dist/lib/sandbox/commands/rm.js +64 -15
  23. package/dist/lib/sandbox/commands/start.js +4 -3
  24. package/dist/lib/sandbox/control/container-identity.js +64 -0
  25. package/dist/lib/sandbox/control/executor.js +13 -7
  26. package/dist/lib/sandbox/control/lifecycle.js +238 -29
  27. package/dist/lib/sandbox/control/protocol.js +7 -0
  28. package/dist/lib/sandbox/control/server.js +202 -26
  29. package/dist/lib/sandbox/control/state.js +25 -15
  30. package/dist/lib/sandbox/control/timing.js +8 -0
  31. package/dist/lib/sandbox/mounts.js +5 -13
  32. package/dist/lib/sandbox/recovery.js +59 -42
  33. package/dist/lib/sandbox/workspace-view.js +57 -40
  34. package/dist/lib/server/daemon.js +1 -1
  35. package/dist/lib/server/process-control.js +11 -1
  36. package/dist/lib/server/process-state.js +57 -20
  37. package/dist/lib/task/artifact-lifecycle.js +82 -11
  38. package/dist/lib/task/artifact-receipts.js +128 -0
  39. package/dist/lib/task/codex-orchestration.js +81 -6
  40. package/dist/lib/task/commands/validate.js +1 -1
  41. package/dist/lib/task/create.js +10 -2
  42. package/dist/lib/task/delegation-receipts.js +84 -2
  43. package/dist/lib/task/events.js +139 -2
  44. package/dist/lib/task/ledger.js +13 -1
  45. package/dist/lib/task/orchestration.js +235 -14
  46. package/dist/lib/task/sections.js +5 -1
  47. package/dist/lib/task/task-execution-lock.js +5 -4
  48. package/dist/lib/task/verification-engine.js +40 -8
  49. package/dist/lifecycle-build-manifest.json +6 -0
  50. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +60 -3
  51. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
  52. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
  53. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
  54. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
  55. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +33 -4
  56. package/lib/defaults.json +10 -2
  57. package/lib/git/workflow.ts +25 -2
  58. package/lib/internal/codex-lifecycle.ts +68 -1
  59. package/lib/internal/codex-sandbox-controller.ts +109 -0
  60. package/lib/internal/task-orchestration.ts +31 -4
  61. package/lib/platform/context.ts +8 -5
  62. package/lib/platform/github-client.ts +61 -3
  63. package/lib/process-data/checkpoint.ts +335 -0
  64. package/lib/process-data/index.ts +112 -60
  65. package/lib/process-data/materialize.ts +83 -0
  66. package/lib/process-data/normalize.ts +29 -1
  67. package/lib/process-data/reconcile.ts +64 -2
  68. package/lib/process-data/sources.ts +449 -2
  69. package/lib/process-data/store.ts +304 -11
  70. package/lib/process-data/types.ts +124 -2
  71. package/lib/sandbox/commands/create.ts +56 -7
  72. package/lib/sandbox/commands/rm.ts +64 -14
  73. package/lib/sandbox/commands/start.ts +4 -3
  74. package/lib/sandbox/control/container-identity.ts +93 -0
  75. package/lib/sandbox/control/executor.ts +14 -10
  76. package/lib/sandbox/control/lifecycle.ts +264 -33
  77. package/lib/sandbox/control/protocol.ts +31 -5
  78. package/lib/sandbox/control/server.ts +184 -24
  79. package/lib/sandbox/control/state.ts +34 -17
  80. package/lib/sandbox/control/timing.ts +12 -0
  81. package/lib/sandbox/mounts.ts +5 -15
  82. package/lib/sandbox/recovery.ts +71 -42
  83. package/lib/sandbox/workspace-view.ts +61 -40
  84. package/lib/server/daemon.ts +1 -1
  85. package/lib/server/process-control.ts +13 -3
  86. package/lib/server/process-state.ts +82 -27
  87. package/lib/task/artifact-lifecycle.ts +73 -10
  88. package/lib/task/artifact-receipts.ts +161 -0
  89. package/lib/task/codex-orchestration.ts +91 -5
  90. package/lib/task/commands/validate.ts +1 -1
  91. package/lib/task/create.ts +11 -2
  92. package/lib/task/delegation-receipts.ts +160 -8
  93. package/lib/task/events.ts +122 -1
  94. package/lib/task/ledger.ts +16 -1
  95. package/lib/task/orchestration.ts +356 -17
  96. package/lib/task/sections.ts +6 -2
  97. package/lib/task/task-execution-lock.ts +11 -9
  98. package/lib/task/verification-engine.ts +51 -16
  99. package/package.json +1 -1
  100. package/templates/.agents/README.en.md +7 -0
  101. package/templates/.agents/README.zh-CN.md +5 -0
  102. package/templates/.agents/hooks/lifecycle-delegation.js +25 -1
  103. package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
  104. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
  105. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  106. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
  107. package/templates/.agents/rules/review-handshake.en.md +3 -1
  108. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
  109. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
  110. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
  111. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  112. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  113. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +6 -6
  114. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +6 -6
  115. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -3
  116. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -3
  117. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
  118. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
  119. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +25 -0
  120. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +25 -0
  121. package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
  122. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
  123. package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -0
  124. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -0
  125. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
  126. package/templates/.agents/workspace/README.en.md +0 -26
  127. package/templates/.agents/workspace/README.zh-CN.md +0 -26
@@ -7,6 +7,7 @@ import { locateActivityLog } from './activity-log.ts';
7
7
  import { parseImplementationInputs, selectPendingImplementationInput } from './implementation-inputs.ts';
8
8
  import { parseVerdict } from './review-artifacts.ts';
9
9
  import { extractSection, findSectionHeading } from './sections.ts';
10
+ import { receiptForOutput, sha256File } from './artifact-receipts.ts';
10
11
 
11
12
  const artifactFamilyCatalog = [
12
13
  { family: 'analysis', sectionAliases: ['分析', 'Analysis'], heading: '分析', labels: ['需求分析报告', 'Requirements Analysis'] },
@@ -218,11 +219,7 @@ function resolveReviewedInput(
218
219
  diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name, String(error)));
219
220
  return null;
220
221
  }
221
- const lines = content.split(/\r?\n/);
222
- const header = lines.findIndex((line) => /\*\*(?:审查输入|Review Input)\*\*[::]/.test(line));
223
- const referenceBlock = header >= 0 ? lines.slice(header, header + 12).join('\n') : '';
224
- const candidates = [...referenceBlock.matchAll(/`([^`]+\.md)`/g)].map((match) => match[1]!);
225
- const parsed = candidates.map((name) => parseArtifactName(name)).find((item) => item?.family === expectedFamily);
222
+ const parsed = parseReviewedInputReference(content, expectedFamily);
226
223
  if (!parsed) {
227
224
  diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name, `${review.name} does not reference a canonical ${expectedFamily} artifact`));
228
225
  return null;
@@ -231,9 +228,16 @@ function resolveReviewedInput(
231
228
  try {
232
229
  const stat = fs.lstatSync(abs);
233
230
  if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('referenced input is not a regular file');
234
- if (stat.mtimeMs > review.mtimeMs) {
235
- diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name, `${parsed.name} is newer than ${review.name}`));
236
- return null;
231
+ const taskContent = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
232
+ const receipt = receiptForOutput(taskContent, review.name);
233
+ if (!receipt) throw new Error(`receipt for ${review.name} is missing`);
234
+ const expectedEvent = reviewEventName(review.family);
235
+ if (receipt.event !== expectedEvent || receipt.input !== parsed.name) {
236
+ throw new Error(`receipt for ${review.name} does not match ${parsed.name}`);
237
+ }
238
+ const actualSha256 = sha256File(abs);
239
+ if (actualSha256 !== receipt.inputSha256) {
240
+ throw new Error(`${parsed.name} content digest does not match receipt`);
237
241
  }
238
242
  return { ...parsed, path: abs, size: stat.size, mtimeMs: stat.mtimeMs };
239
243
  } catch (error) {
@@ -242,6 +246,54 @@ function resolveReviewedInput(
242
246
  }
243
247
  }
244
248
 
249
+ function parseReviewedInputReference(content: string, expectedFamily: ArtifactFamily): { family: ArtifactFamily; round: number; name: string } | null {
250
+ const lines = content.split(/\r?\n/);
251
+ const header = lines.findIndex((line) => /\*\*(?:审查输入|Review Input)\*\*[::]/.test(line));
252
+ const referenceBlock = header >= 0 ? lines.slice(header, header + 12).join('\n') : '';
253
+ const candidates = [...referenceBlock.matchAll(/`([^`]+\.md)`/g)].map((match) => match[1]!);
254
+ return candidates
255
+ .map((name) => parseArtifactName(name))
256
+ .find((item) => item?.family === expectedFamily) ?? null;
257
+ }
258
+
259
+ function parseCodePlanInputReference(content: string): { family: 'plan'; round: number; name: string } | null {
260
+ const lines = content.split(/\r?\n/);
261
+ const sectionStart = lines.findIndex((line) => /^##\s+(?:实现输入|Implementation Input)\s*$/.test(line.trim()));
262
+ if (sectionStart < 0) return null;
263
+ const bodyLines = lines.slice(sectionStart + 1);
264
+ const nextSection = bodyLines.findIndex((line) => /^##\s+/.test(line.trim()));
265
+ const body = (nextSection < 0 ? bodyLines : bodyLines.slice(0, nextSection)).join('\n');
266
+ const match = body.match(/\*\*(?:方案输入|Plan Input)\*\*[::]\s*`([^`]+\.md)`/);
267
+ if (!match) return null;
268
+ const parsed = parseArtifactName(match[1]!);
269
+ return parsed?.family === 'plan' ? { ...parsed, family: 'plan' } : null;
270
+ }
271
+
272
+ function reviewEventName(family: ArtifactFamily): 'review-analysis.completed' | 'review-plan.completed' | 'review-code.completed' {
273
+ if (family === 'review-analysis') return 'review-analysis.completed';
274
+ if (family === 'review-plan') return 'review-plan.completed';
275
+ if (family === 'review-code') return 'review-code.completed';
276
+ throw new Error(`unsupported review family '${family}'`);
277
+ }
278
+
279
+ function resolveCodeInputReceipt(taskDir: string, code: ArtifactIdentity): { input: ArtifactIdentity; inputSha256: string } | null {
280
+ let taskContent: string;
281
+ try { taskContent = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8'); }
282
+ catch { return null; }
283
+ let receipt;
284
+ try { receipt = receiptForOutput(taskContent, code.name); }
285
+ catch { return null; }
286
+ if (!receipt || receipt.event !== 'code.completed') return null;
287
+ const plan = inspectArtifactDirectory(taskDir, 'plan');
288
+ if (plan.status !== 'ready') return null;
289
+ const input = plan.artifacts.find((artifact) => artifact.name === receipt.input);
290
+ return input ? { input, inputSha256: receipt.inputSha256 } : null;
291
+ }
292
+
293
+ function resolveCodePlanInput(taskDir: string, code: ArtifactIdentity): { input: ArtifactIdentity; inputSha256: string } | null {
294
+ return resolveCodeInputReceipt(taskDir, code);
295
+ }
296
+
245
297
  function assertWritableInventory(inventory: ArtifactInventoryResult): ArtifactError | null {
246
298
  if (inventory.status === 'failed') return inventory.error;
247
299
  const blockers = inventory.diagnostics.filter((item) => BLOCKING_DIAGNOSTICS.has(item.code));
@@ -312,11 +364,21 @@ function resolveCodeContext(inventory: ArtifactInventoryResult, options: Inspect
312
364
  return withCodeMode(inventory, inputs, 'ready', 'init', codeMax, reviewMax, null, null,
313
365
  'No prior code artifact. Starting initial implementation (round 1 -> code.md).');
314
366
  }
367
+ const codePlanInput = resolveCodePlanInput(inventory.taskDir!, latestCode);
368
+ if (!codePlanInput) {
369
+ return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', `code completion receipt for ${latestCode.name} is missing or does not match the current plan`, latestCode.name);
370
+ }
371
+ if (reviewPlan.latest && !reviewPlan.reviewedInput) {
372
+ return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', `${reviewPlan.latest.name} has no valid reviewed input`, reviewPlan.latest.name);
373
+ }
315
374
  if (reviewPlan.latest && reviewPlan.reviewedInput?.name === plan.latest.name) {
316
375
  const verdict = parseVerdict(reviewPlan.latest.path);
317
- if (verdict.ok && verdict.verdict === 'Approved' && reviewPlan.latest.mtimeMs > latestCode.mtimeMs) {
376
+ const reviewedPlanSha256 = sha256File(reviewPlan.reviewedInput.path);
377
+ if (verdict.ok && verdict.verdict === 'Approved' && (
378
+ codePlanInput.input.name !== plan.latest.name || codePlanInput.inputSha256 !== reviewedPlanSha256
379
+ )) {
318
380
  return withCodeMode(inventory, inputs, 'ready', 'init', codeMax, reviewMax, verdict.verdict, reviewPlan.latest.name,
319
- `Latest ${reviewPlan.latest.name} is approved and newer than the latest code artifact. Entering replan-driven init.`);
381
+ `Latest ${reviewPlan.latest.name} approves plan content not captured by the latest code input receipt. Entering replan-driven init.`);
320
382
  }
321
383
  }
322
384
  if (reviewMax < codeMax) {
@@ -440,6 +502,7 @@ function buildArtifactLinkSection(content: string, artifact: ArtifactIdentity):
440
502
  export {
441
503
  artifactFamilyCatalog, artifactName, parseArtifactName, familySpec,
442
504
  inspectTaskArtifacts, inspectArtifactDirectory, resolveArtifactContext,
505
+ parseReviewedInputReference, parseCodePlanInputReference, resolveCodePlanInput, reviewEventName,
443
506
  assertWritableInventory, validateCompletedArtifact, buildArtifactLinkSection
444
507
  };
445
508
  export type {
@@ -0,0 +1,161 @@
1
+ import fs from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+
4
+ import { extractSection, findSectionHeading, parseTable } from './sections.ts';
5
+
6
+ const RECEIPT_SECTION_ALIASES = ['产物生命周期收据', 'Artifact Lifecycle Receipts'] as const;
7
+ const RECEIPT_COLUMNS = ['event', 'output', 'input', 'input_sha256', 'completed_at'] as const;
8
+ const RECEIPT_EVENTS = new Set([
9
+ 'review-analysis.completed',
10
+ 'review-plan.completed',
11
+ 'review-code.completed',
12
+ 'code.completed'
13
+ ]);
14
+ const SHA256_RE = /^[a-f0-9]{64}$/;
15
+ 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$/;
16
+ const RECEIPT_SHAPES = {
17
+ 'review-analysis.completed': { output: 'review-analysis', input: 'analysis' },
18
+ 'review-plan.completed': { output: 'review-plan', input: 'plan' },
19
+ 'review-code.completed': { output: 'review-code', input: 'code' },
20
+ 'code.completed': { output: 'code', input: 'plan' }
21
+ } as const;
22
+
23
+ type ArtifactReceiptEvent =
24
+ | 'review-analysis.completed'
25
+ | 'review-plan.completed'
26
+ | 'review-code.completed'
27
+ | 'code.completed';
28
+ type ArtifactReceipt = {
29
+ event: ArtifactReceiptEvent;
30
+ output: string;
31
+ input: string;
32
+ inputSha256: string;
33
+ completedAt: string;
34
+ };
35
+ type ArtifactReceiptParseResult = {
36
+ present: boolean;
37
+ rows: readonly ArtifactReceipt[];
38
+ };
39
+ type ReceiptSectionMutation = {
40
+ aliases: readonly string[];
41
+ heading: string;
42
+ body: string;
43
+ };
44
+
45
+ class ArtifactReceiptError extends Error {
46
+ readonly code = 'ARTIFACT_RECEIPT_INVALID';
47
+
48
+ constructor(message: string) {
49
+ super(message);
50
+ this.name = 'ArtifactReceiptError';
51
+ }
52
+ }
53
+
54
+ function sha256Bytes(value: Buffer): string {
55
+ return createHash('sha256').update(value).digest('hex');
56
+ }
57
+
58
+ function sha256File(filePath: string): string {
59
+ return sha256Bytes(fs.readFileSync(filePath));
60
+ }
61
+
62
+ function parseReceiptArtifactName(name: string): { family: string } | null {
63
+ const match = ARTIFACT_NAME_RE.exec(name);
64
+ if (!match) return null;
65
+ const roundText = match[2];
66
+ if (roundText !== undefined) {
67
+ const round = Number(roundText);
68
+ if (!Number.isSafeInteger(round) || String(round) !== roundText) return null;
69
+ }
70
+ return { family: match[1]! };
71
+ }
72
+
73
+ function validateReceiptShape(event: ArtifactReceiptEvent, output: string, input: string): void {
74
+ const outputIdentity = parseReceiptArtifactName(output);
75
+ const inputIdentity = parseReceiptArtifactName(input);
76
+ if (!outputIdentity || !inputIdentity) throw new ArtifactReceiptError(`receipt artifact identity is invalid: ${output} -> ${input}`);
77
+ const shape = RECEIPT_SHAPES[event];
78
+ if (outputIdentity.family !== shape.output || inputIdentity.family !== shape.input) {
79
+ throw new ArtifactReceiptError(`receipt event '${event}' does not match ${output} -> ${input}`);
80
+ }
81
+ }
82
+
83
+ function parseArtifactReceipts(content: string): ArtifactReceiptParseResult {
84
+ const section = extractSection(content, [...RECEIPT_SECTION_ALIASES]);
85
+ if (!section) return { present: false, rows: [] };
86
+
87
+ let table;
88
+ try {
89
+ table = parseTable(content, {
90
+ sectionAliases: [...RECEIPT_SECTION_ALIASES],
91
+ columns: [...RECEIPT_COLUMNS],
92
+ keyColumn: 'output'
93
+ });
94
+ } catch (error) {
95
+ throw new ArtifactReceiptError(error instanceof Error ? error.message : String(error));
96
+ }
97
+ if (!table) throw new ArtifactReceiptError('receipt section has no receipt table');
98
+
99
+ const rows = table.rows.map((row) => {
100
+ const event = row.values.event ?? '';
101
+ const output = row.values.output ?? '';
102
+ const input = row.values.input ?? '';
103
+ const inputSha256 = row.values.input_sha256 ?? '';
104
+ const completedAt = row.values.completed_at ?? '';
105
+ if (!RECEIPT_EVENTS.has(event)) throw new ArtifactReceiptError(`unknown receipt event '${event}'`);
106
+ if (!output || !input) throw new ArtifactReceiptError('receipt output and input are required');
107
+ validateReceiptShape(event as ArtifactReceiptEvent, output, input);
108
+ if (!SHA256_RE.test(inputSha256)) throw new ArtifactReceiptError(`receipt digest for '${output}' is invalid`);
109
+ if (!completedAt || Number.isNaN(Date.parse(completedAt.replace(' ', 'T')))) {
110
+ throw new ArtifactReceiptError(`receipt completion time for '${output}' is invalid`);
111
+ }
112
+ return {
113
+ event: event as ArtifactReceiptEvent,
114
+ output,
115
+ input,
116
+ inputSha256,
117
+ completedAt
118
+ };
119
+ });
120
+ return { present: true, rows };
121
+ }
122
+
123
+ function receiptForOutput(content: string, output: string): ArtifactReceipt | null {
124
+ const parsed = parseArtifactReceipts(content);
125
+ return parsed.rows.find((row) => row.output === output) ?? null;
126
+ }
127
+
128
+ function upsertArtifactReceipt(content: string, receipt: ArtifactReceipt): ReceiptSectionMutation {
129
+ if (!RECEIPT_EVENTS.has(receipt.event)) throw new ArtifactReceiptError(`unknown receipt event '${receipt.event}'`);
130
+ if (!receipt.output || !receipt.input) throw new ArtifactReceiptError('receipt output and input are required');
131
+ validateReceiptShape(receipt.event, receipt.output, receipt.input);
132
+ if (!SHA256_RE.test(receipt.inputSha256)) throw new ArtifactReceiptError(`receipt digest for '${receipt.output}' is invalid`);
133
+ if (!receipt.completedAt || Number.isNaN(Date.parse(receipt.completedAt.replace(' ', 'T')))) {
134
+ throw new ArtifactReceiptError(`receipt completion time for '${receipt.output}' is invalid`);
135
+ }
136
+
137
+ const existing = parseArtifactReceipts(content).rows;
138
+ const previous = existing.find((row) => row.output === receipt.output);
139
+ if (previous && JSON.stringify(previous) !== JSON.stringify(receipt)) {
140
+ throw new ArtifactReceiptError(`receipt for '${receipt.output}' already exists with different evidence`);
141
+ }
142
+ const rows = previous ? existing : [...existing, receipt];
143
+ const heading = findSectionHeading(content, [...RECEIPT_SECTION_ALIASES]);
144
+ const body = [
145
+ `| ${RECEIPT_COLUMNS.join(' | ')} |`,
146
+ `| ${RECEIPT_COLUMNS.map(() => '---').join(' | ')} |`,
147
+ ...rows.map((row) => `| ${row.event} | ${row.output} | ${row.input} | ${row.inputSha256} | ${row.completedAt} |`)
148
+ ].join('\n');
149
+ return { aliases: [...RECEIPT_SECTION_ALIASES], heading, body };
150
+ }
151
+
152
+ export {
153
+ ArtifactReceiptError,
154
+ RECEIPT_SECTION_ALIASES,
155
+ parseArtifactReceipts,
156
+ receiptForOutput,
157
+ sha256Bytes,
158
+ sha256File,
159
+ upsertArtifactReceipt
160
+ };
161
+ export type { ArtifactReceipt, ArtifactReceiptEvent, ArtifactReceiptParseResult, ReceiptSectionMutation };
@@ -1,3 +1,5 @@
1
+ import fs from 'node:fs';
2
+
1
3
  import {
2
4
  preflightCodexLifecycleEvidence,
3
5
  resolveCodexSpawnedChild,
@@ -5,9 +7,16 @@ import {
5
7
  resolveCodexThread
6
8
  } from '../agent-clients/adapters/codex-lifecycle/app-server.ts';
7
9
  import { createCodexLifecycleStore } from '../agent-clients/adapters/codex-lifecycle/store.ts';
10
+ import { createCodexCapabilityStore } from '../agent-clients/adapters/codex-lifecycle/capability-store.ts';
11
+ import { computeLifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.ts';
12
+ import type { LifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.ts';
13
+ import { verifyCodexSandboxControllerContext } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.ts';
8
14
  import type { AgentClientId } from '../agent-clients/types.ts';
15
+ import { resolveTaskRef } from './resolve-ref.ts';
9
16
  import {
10
17
  activateMatchingOrchestrationDelegation,
18
+ hasActivatableOrchestrationDelegation,
19
+ hasSealableOrchestrationDelegation,
11
20
  pauseMatchingOrchestrationDelegation,
12
21
  prepareOrchestrationDelegation,
13
22
  reconcileMatchingOrchestrationDelegation,
@@ -16,12 +25,15 @@ import {
16
25
  import type { OrchestrationOptions, OrchestrationResult } from './orchestration.ts';
17
26
 
18
27
  type LifecycleStore = ReturnType<typeof createCodexLifecycleStore>;
28
+ type CapabilityStore = ReturnType<typeof createCodexCapabilityStore>;
19
29
  type CodexBridgeOptions = Readonly<{
20
30
  repoRoot?: string;
21
31
  store?: LifecycleStore;
22
32
  preflight?: typeof preflightCodexLifecycleEvidence;
23
33
  resolveThread?: typeof resolveCodexThread;
24
34
  resolveTerminal?: typeof resolveCodexTerminal;
35
+ capabilityStore?: CapabilityStore;
36
+ buildIdentity?: LifecycleBuildIdentity;
25
37
  orchestrationOptions?: OrchestrationOptions;
26
38
  }>;
27
39
 
@@ -59,25 +71,68 @@ function requiredStore(options: CodexBridgeOptions): LifecycleStore {
59
71
  return options.store;
60
72
  }
61
73
 
74
+ function controllerContext(taskId: string, repoRoot: string) {
75
+ const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
76
+ return contextPath
77
+ ? verifyCodexSandboxControllerContext(contextPath, taskId, { repoRoot })
78
+ : null;
79
+ }
80
+
62
81
  async function prepareCodexOrchestrationDelegation(
63
82
  taskRef: string,
64
83
  input: Readonly<{
65
84
  client: AgentClientId;
66
85
  requestedModel?: string;
67
86
  requestedReasoningEffort?: string;
87
+ capabilityToken?: string;
68
88
  }>,
69
89
  options: CodexBridgeOptions = {}
70
90
  ): Promise<OrchestrationResult> {
71
91
  if (input.client !== 'codex') return prepareOrchestrationDelegation(taskRef, input, coreOptions(options));
72
92
  try {
73
- await (options.preflight ?? preflightCodexLifecycleEvidence)(options.repoRoot ?? process.cwd());
93
+ const repoRoot = options.repoRoot ?? process.cwd();
94
+ const resolved = resolveTaskRef(taskRef, { repoRoot });
95
+ if (!resolved.ok) return bridgeFailure(resolved.code, resolved.message);
96
+ const preflight = await (options.preflight ?? preflightCodexLifecycleEvidence)(repoRoot);
97
+ if (!input.capabilityToken) {
98
+ return bridgeFailure(
99
+ 'ORCHESTRATION_CODEX_CAPABILITY_REQUIRED',
100
+ 'Codex prepare requires a current-session capability token'
101
+ );
102
+ }
103
+ const buildIdentity = options.buildIdentity ?? computeLifecycleBuildIdentity(repoRoot);
104
+ const controller = controllerContext(resolved.taskId, repoRoot);
105
+ const capabilityStore = options.capabilityStore ?? createCodexCapabilityStore();
106
+ const consumed = capabilityStore.consume(input.capabilityToken, {
107
+ taskId: resolved.taskId,
108
+ hookDefinitionHash: preflight.hookDefinitionHash,
109
+ buildIdentity,
110
+ ...(controller ? { controller: {
111
+ instanceDigest: controller.controllerInstanceDigest,
112
+ controlGeneration: controller.controlGeneration
113
+ } } : {})
114
+ });
115
+ return prepareOrchestrationDelegation(taskRef, {
116
+ ...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
126
+ }
127
+ }, coreOptions(options));
74
128
  } catch (error) {
75
129
  return bridgeFailure(
76
- 'ORCHESTRATION_CLIENT_PREFLIGHT_FAILED',
130
+ error instanceof Error && error.name.startsWith('CODEX_CAPABILITY_')
131
+ ? error.name
132
+ : 'ORCHESTRATION_CLIENT_PREFLIGHT_FAILED',
77
133
  error instanceof Error ? error.message : String(error)
78
134
  );
79
135
  }
80
- return prepareOrchestrationDelegation(taskRef, input, coreOptions(options));
81
136
  }
82
137
 
83
138
  async function activateCodexOrchestrationDelegation(
@@ -85,6 +140,9 @@ async function activateCodexOrchestrationDelegation(
85
140
  options: CodexBridgeOptions = {}
86
141
  ): Promise<OrchestrationResult> {
87
142
  try {
143
+ if (!hasActivatableOrchestrationDelegation('codex', childThreadId, coreOptions(options))) {
144
+ return bridgeFailure('ORCHESTRATION_DELEGATION_MISSING', 'No matching Codex delegation is active');
145
+ }
88
146
  const store = requiredStore(options);
89
147
  const resolved = await (options.resolveThread ?? resolveCodexThread)(childThreadId);
90
148
  store.apply(resolved.resolution.thread);
@@ -95,6 +153,23 @@ async function activateCodexOrchestrationDelegation(
95
153
  if (record.state.status !== 'start-ready' || !evidence) {
96
154
  return pauseBridge('ORCHESTRATION_CODEX_START_EVIDENCE_INVALID', 'Codex lifecycle start evidence is not ready', options);
97
155
  }
156
+ const repoRoot = options.repoRoot ?? process.cwd();
157
+ const preflight = await (options.preflight ?? preflightCodexLifecycleEvidence)(repoRoot, {
158
+ sessionId: evidence.parentThreadId,
159
+ turnId: evidence.parentTurnId,
160
+ toolUseId: evidence.spawnToolUseId
161
+ });
162
+ const buildIdentity = options.buildIdentity ?? computeLifecycleBuildIdentity(repoRoot);
163
+ 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
+ const controller = contextPath
171
+ ? verifyCodexSandboxControllerContext(contextPath, contextTaskId as string, { repoRoot })
172
+ : null;
98
173
  return activateMatchingOrchestrationDelegation('codex', {
99
174
  nativeAgent: evidence.nativeAgent,
100
175
  childId: evidence.childThreadId,
@@ -107,9 +182,17 @@ async function activateCodexOrchestrationDelegation(
107
182
  ? { reasoningEffortFallbackReason: evidence.reasoningEffortFallbackReason }
108
183
  : {}),
109
184
  hostEvidence: {
110
- kind: 'codex-lifecycle-v1',
185
+ kind: 'codex-lifecycle-v2',
111
186
  hookDefinitionHash: evidence.hookDefinitionHash,
112
- startRevision: record.revision
187
+ startRevision: record.revision,
188
+ ...buildIdentity,
189
+ ...preflight.hookProvenance,
190
+ capabilitySessionId: evidence.parentThreadId,
191
+ capabilityTurnId: evidence.parentTurnId,
192
+ spawnToolUseId: evidence.spawnToolUseId,
193
+ spawnObservedAt: record.spawnObservedAt ?? undefined,
194
+ controllerInstanceDigest: controller?.controllerInstanceDigest ?? null,
195
+ controlGeneration: controller?.controlGeneration ?? null
113
196
  }
114
197
  }, coreOptions(options));
115
198
  } catch (error) {
@@ -149,6 +232,9 @@ async function sealCodexOrchestrationDelegation(
149
232
  options: CodexBridgeOptions = {}
150
233
  ): Promise<OrchestrationResult> {
151
234
  try {
235
+ if (!hasSealableOrchestrationDelegation('codex', childThreadId, coreOptions(options))) {
236
+ return bridgeFailure('ORCHESTRATION_DELEGATION_MISSING', 'No matching Codex delegation is active');
237
+ }
152
238
  const store = requiredStore(options);
153
239
  const existing = store.read(childThreadId);
154
240
  if (!existing.consumer) {
@@ -169,7 +169,7 @@ function inplaceValidation(config: ReturnType<typeof loadConfig>, target: Return
169
169
  const nonce = randomUUID();
170
170
  const issuedAt = Date.now();
171
171
  const lease: SandboxControlLease = {
172
- version: 1, generation: manifest.generation, nonce,
172
+ version: 2, generation: manifest.generation, nonce,
173
173
  owner: { pid: process.pid, startTime }, issuedAt,
174
174
  expiresAt: issuedAt + Math.min(options.timeoutMs + RECOVERY_GRACE_MS, MAX_TIMEOUT_MS + RECOVERY_GRACE_MS),
175
175
  taskId: target.workspace.mode === 'task-bound' ? target.workspace.taskId : null,
@@ -231,6 +231,14 @@ function assertRealDirectory(directory: string, code: string): void {
231
231
  if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(code);
232
232
  }
233
233
 
234
+ function ensureRealDirectory(directory: string, code: string): void {
235
+ if (fs.existsSync(directory)) {
236
+ assertRealDirectory(directory, code);
237
+ return;
238
+ }
239
+ fs.mkdirSync(directory, { mode: 0o700 });
240
+ }
241
+
234
242
  function withCreateLock<T>(repoRoot: string, workspaceRoot: string, operation: () => T): T {
235
243
  const lockRoot = path.join(workspaceRoot, '.task-create.lock');
236
244
  fs.mkdirSync(lockRoot, { recursive: true, mode: 0o700 });
@@ -289,8 +297,9 @@ function createLocalTask(value: unknown, options: LocalTaskCreateOptions): Local
289
297
  const repoRoot = fs.realpathSync.native(options.repoRoot);
290
298
  const workspaceRoot = path.join(repoRoot, '.agents', 'workspace');
291
299
  const activeRoot = path.join(workspaceRoot, 'active');
292
- assertRealDirectory(workspaceRoot, 'TASK_CREATE_WORKSPACE_INVALID');
293
- assertRealDirectory(activeRoot, 'TASK_CREATE_WORKSPACE_INVALID');
300
+ ensureRealDirectory(path.join(repoRoot, '.agents'), 'TASK_CREATE_WORKSPACE_INVALID');
301
+ ensureRealDirectory(workspaceRoot, 'TASK_CREATE_WORKSPACE_INVALID');
302
+ ensureRealDirectory(activeRoot, 'TASK_CREATE_WORKSPACE_INVALID');
294
303
  const project = readProject(repoRoot);
295
304
  const canonical = canonicalTaskCreateCandidate(candidate);
296
305
  const candidateDigest = sha256(canonical);