@fitlab-ai/agent-infra 0.11.1 → 0.11.3

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 (131) hide show
  1. package/dist/bin/internal-cli.js +7 -2
  2. package/dist/lib/agent-clients/adapter.js +3 -0
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +234 -0
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +8 -35
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +1 -34
  6. package/dist/lib/agent-clients/adapters/codex.js +10 -0
  7. package/dist/lib/internal/cli-route-inventory.js +1 -1
  8. package/dist/lib/internal/manual-validation.js +22 -27
  9. package/dist/lib/internal/platform-comment.js +59 -0
  10. package/dist/lib/internal/sandbox-control.js +2 -171
  11. package/dist/lib/internal/task-artifact.js +1 -1
  12. package/dist/lib/internal/task-lifecycle.js +1 -1
  13. package/dist/lib/internal/task-operation-registry.js +7 -2
  14. package/dist/lib/internal/task-review.js +5 -5
  15. package/dist/lib/internal/task-validate.js +0 -6
  16. package/dist/lib/platform/comment-safety.js +7 -1
  17. package/dist/lib/platform/github-provider.js +13 -3
  18. package/dist/lib/platform/issue-comments.js +93 -15
  19. package/dist/lib/platform/pr-summary.js +7 -2
  20. package/dist/lib/platform/provider-contract.d.ts +2 -0
  21. package/dist/lib/platform/provider-validation.js +7 -2
  22. package/dist/lib/platform/verification-sync.js +19 -2
  23. package/dist/lib/sandbox/control/client.js +3 -44
  24. package/dist/lib/sandbox/control/executor.js +14 -117
  25. package/dist/lib/sandbox/control/finalization-response.js +8 -2
  26. package/dist/lib/sandbox/control/protocol.js +5 -64
  27. package/dist/lib/sandbox/control/server.js +29 -206
  28. package/dist/lib/sandbox/control/state.js +1 -7
  29. package/dist/lib/sandbox/control/task-view.js +2 -1
  30. package/dist/lib/task/artifact-command.js +6 -6
  31. package/dist/lib/task/codex-orchestration.js +10 -208
  32. package/dist/lib/task/commit-operation.js +3 -18
  33. package/dist/lib/task/control-authority.js +35 -44
  34. package/dist/lib/task/control-recovery.js +3 -42
  35. package/dist/lib/task/delegation-receipts.js +16 -78
  36. package/dist/lib/task/events.js +89 -237
  37. package/dist/lib/task/finalization-artifacts.js +1 -3
  38. package/dist/lib/task/finalization-state.js +2 -1
  39. package/dist/lib/task/finalization.js +208 -57
  40. package/dist/lib/task/lifecycle-execution.js +2 -11
  41. package/dist/lib/task/lifecycle.js +1 -1
  42. package/dist/lib/task/local-artifact-finalization.js +25 -242
  43. package/dist/lib/task/manual-validation-receipt.js +8 -3
  44. package/dist/lib/task/orchestration.js +12 -24
  45. package/dist/lib/task/review-command.js +0 -1
  46. package/dist/lib/task/review-finalization.js +46 -211
  47. package/dist/lifecycle-build-manifest.json +3 -3
  48. package/lib/agent-clients/adapter.ts +46 -0
  49. package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +297 -0
  50. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +13 -33
  51. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +1 -44
  52. package/lib/agent-clients/adapters/codex.ts +10 -0
  53. package/lib/internal/cli-route-inventory.ts +1 -1
  54. package/lib/internal/manual-validation.ts +22 -26
  55. package/lib/internal/platform-comment.ts +38 -0
  56. package/lib/internal/sandbox-control.ts +1 -185
  57. package/lib/internal/task-artifact.ts +1 -1
  58. package/lib/internal/task-lifecycle.ts +2 -2
  59. package/lib/internal/task-operation-registry.ts +8 -4
  60. package/lib/internal/task-review.ts +5 -5
  61. package/lib/internal/task-validate.ts +0 -5
  62. package/lib/platform/comment-safety.ts +8 -0
  63. package/lib/platform/github-provider.ts +11 -2
  64. package/lib/platform/issue-comments.ts +102 -17
  65. package/lib/platform/pr-summary.ts +10 -2
  66. package/lib/platform/provider-contract.ts +2 -0
  67. package/lib/platform/provider-validation.ts +6 -2
  68. package/lib/platform/verification-sync.ts +19 -2
  69. package/lib/sandbox/control/client.ts +5 -68
  70. package/lib/sandbox/control/executor.ts +14 -125
  71. package/lib/sandbox/control/finalization-response.ts +8 -2
  72. package/lib/sandbox/control/protocol.ts +7 -83
  73. package/lib/sandbox/control/server.ts +30 -220
  74. package/lib/sandbox/control/state.ts +1 -9
  75. package/lib/sandbox/control/task-view.ts +2 -1
  76. package/lib/task/artifact-command.ts +6 -7
  77. package/lib/task/codex-orchestration.ts +8 -241
  78. package/lib/task/commit-operation.ts +3 -23
  79. package/lib/task/control-authority.ts +39 -55
  80. package/lib/task/control-recovery.ts +3 -43
  81. package/lib/task/delegation-receipts.ts +14 -81
  82. package/lib/task/events.ts +106 -255
  83. package/lib/task/finalization-artifacts.ts +1 -3
  84. package/lib/task/finalization-state.ts +2 -1
  85. package/lib/task/finalization.ts +221 -65
  86. package/lib/task/lifecycle-execution.ts +10 -21
  87. package/lib/task/lifecycle.ts +2 -2
  88. package/lib/task/local-artifact-finalization.ts +23 -281
  89. package/lib/task/manual-validation-receipt.ts +8 -2
  90. package/lib/task/orchestration.ts +30 -53
  91. package/lib/task/review-command.ts +0 -1
  92. package/lib/task/review-finalization.ts +45 -193
  93. package/package.json +2 -2
  94. package/templates/.agents/rules/lifecycle-orchestration.en.md +23 -9
  95. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +23 -9
  96. package/templates/.agents/rules/local-artifact-repair.en.md +14 -60
  97. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +13 -60
  98. package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -3
  99. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
  100. package/templates/.agents/skills/code-task/SKILL.en.md +3 -3
  101. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
  102. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -2
  103. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -2
  104. package/templates/.agents/skills/complete-task/SKILL.en.md +7 -8
  105. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +7 -8
  106. package/templates/.agents/skills/plan-task/SKILL.en.md +3 -3
  107. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
  108. package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
  109. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
  110. package/templates/.agents/skills/review-code/SKILL.en.md +3 -3
  111. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -3
  112. package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
  113. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
  114. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +2 -2
  115. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +2 -2
  116. package/templates/.agents/skills/run-task/SKILL.en.md +10 -9
  117. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +10 -9
  118. package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
  119. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
  120. package/dist/lib/sandbox/control/task-workflow.js +0 -74
  121. package/dist/lib/sandbox/control/workflow-executor.js +0 -157
  122. package/dist/lib/task/lifecycle-recovery.js +0 -549
  123. package/dist/lib/task/recovery-warning.js +0 -19
  124. package/dist/lib/task/workflow-command.js +0 -18
  125. package/dist/lib/task/workflow-dispatch.js +0 -161
  126. package/lib/sandbox/control/task-workflow.ts +0 -95
  127. package/lib/sandbox/control/workflow-executor.ts +0 -183
  128. package/lib/task/lifecycle-recovery.ts +0 -691
  129. package/lib/task/recovery-warning.ts +0 -21
  130. package/lib/task/workflow-command.ts +0 -19
  131. package/lib/task/workflow-dispatch.ts +0 -171
@@ -49,7 +49,9 @@ function taskControlTransportFailure(message, code = 'TASK_CONTROL_TRANSPORT_INV
49
49
  }
50
50
  let controlRouted = false;
51
51
  if (!taskViewGuardFailed && !markerlessHelp && (taskControlCommand || taskWorkflowCommand || manualValidationWorkflowCommand)) {
52
- const transport = resolveSandboxControlTransport(process.env);
52
+ const transport = resolveSandboxControlTransport(process.env, {
53
+ localWorkflow: command === 'task-orchestration' || taskWorkflowCommand || manualValidationWorkflowCommand
54
+ });
53
55
  switch (transport.kind) {
54
56
  case 'fail-closed': {
55
57
  const reason = transport.reasonCode ?? 'TASK_CONTROL_TRANSPORT_INVALID';
@@ -60,9 +62,12 @@ if (!taskViewGuardFailed && !markerlessHelp && (taskControlCommand || taskWorkfl
60
62
  // The host CLI is the direct-host authority. Domain handlers retain their
61
63
  // task locks, atomic writes, and operation-specific recovery facts.
62
64
  break;
65
+ case 'sandbox-local': {
66
+ break;
67
+ }
63
68
  case 'broker-client': {
64
69
  const { sandboxControl } = await import('../lib/internal/sandbox-control.js');
65
- await sandboxControl(['client', ...(taskControlCommand ? [command] : ['task-workflow', command]), ...process.argv.slice(3)]);
70
+ await sandboxControl(['client', command, ...process.argv.slice(3)]);
66
71
  break;
67
72
  }
68
73
  }
@@ -408,6 +408,9 @@ function defineAgentClientAdapter(candidate) {
408
408
  capabilities: Object.freeze(capabilities),
409
409
  modelSelection: frozenModelSelection,
410
410
  delegationEvidence: Object.freeze({ ...evidence }),
411
+ ...(candidate.orchestrationAdapter === undefined
412
+ ? {}
413
+ : { orchestrationAdapter: Object.freeze({ ...candidate.orchestrationAdapter }) }),
411
414
  project: Object.freeze({
412
415
  ownedPathPrefixes: Object.freeze(paths),
413
416
  ...projectAssets,
@@ -0,0 +1,234 @@
1
+ import fs from 'node:fs';
2
+ import { createCodexLifecycleStore } from './store.js';
3
+ import { managedDelegationRole } from '../../../task/delegation-receipts.js';
4
+ import { normalizeAgentToken } from '../../tokens.js';
5
+ import { locateActivityLog, pairEntries, appendActivityEntry } from '../../../task/activity-log.js';
6
+ import { captureTaskWriteMetadata, writeTask } from '../../../task/write.js';
7
+ import { parseTypedTaskFrontmatter } from '../../../task/frontmatter.js';
8
+ import { resolveTaskRef } from '../../../task/resolve-ref.js';
9
+ import { resolveAgentRuntimeStoreRoot } from '../../../runtime/agent-runtime.js';
10
+ import { recoverActivatedOrchestrationDelegationUnderLock, readRun } from '../../../task/orchestration.js';
11
+ const ACTIONS = {
12
+ analysis: 'Analyze Task',
13
+ 'review-analysis': 'Review Analysis',
14
+ plan: 'Plan Task',
15
+ 'review-plan': 'Review Plan',
16
+ code: 'Code Task',
17
+ 'review-code': 'Review Code'
18
+ };
19
+ function result(request, status, extra = {}) {
20
+ return {
21
+ status,
22
+ changed: status === 'applied',
23
+ targetState: 'active',
24
+ requestRef: request.taskRef,
25
+ intent: 'recover-started',
26
+ taskId: null,
27
+ receiptId: null,
28
+ childId: null,
29
+ error: null,
30
+ ...extra
31
+ };
32
+ }
33
+ function failure(request, status, code, message, extra = {}) {
34
+ return result(request, status, { error: { code, message }, ...extra });
35
+ }
36
+ function actionPattern(receipt) {
37
+ const action = ACTIONS[receipt.stage];
38
+ if (!action)
39
+ return null;
40
+ const escaped = action.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
41
+ return new RegExp(`^${escaped} \\(Round ${receipt.round}(?:, (?:fix for review-code(?:-r[2-9]|-r[1-9]\\d+)?.md|decision II-[1-9]\\d*))?\\)$`, 'u');
42
+ }
43
+ function openRows(section) {
44
+ return pairEntries([...section.entries]).filter((row) => row.started !== '' && row.done === '');
45
+ }
46
+ function rowForReceipt(rows, receipt) {
47
+ const pattern = actionPattern(receipt);
48
+ return pattern ? rows.filter((row) => pattern.test(row.step)) : [];
49
+ }
50
+ function readLifecycleStore(options, repoRoot) {
51
+ return options.lifecycleStore ?? createCodexLifecycleStore({
52
+ root: resolveAgentRuntimeStoreRoot({ repoRoot, store: 'lifecycle' }),
53
+ cliVersion: 'recovery',
54
+ now: options.now
55
+ });
56
+ }
57
+ function readStoredEvidence(store, childId) {
58
+ try {
59
+ return store.read(childId);
60
+ }
61
+ catch (error) {
62
+ const message = error instanceof Error ? error.message : String(error);
63
+ if (message.includes('was not found uniquely'))
64
+ return { missing: true };
65
+ return { error: error instanceof Error ? error : new Error(message) };
66
+ }
67
+ }
68
+ function validateStopRecord(record, receipt) {
69
+ const start = record.state.startEvidence;
70
+ const stop = record.state.stopEvidence;
71
+ const provenance = receipt.lifecycleProvenance;
72
+ const host = receipt.hostEvidence;
73
+ if (receipt.client !== 'codex'
74
+ || !provenance
75
+ || host?.kind !== 'codex-lifecycle-v2'
76
+ || !start
77
+ || !stop
78
+ || record.state.status !== 'stop-ready'
79
+ || stop.terminalStatus !== 'completed'
80
+ || stop.hookStopObserved !== true
81
+ || start.childThreadId !== receipt.childId
82
+ || start.parentThreadId !== receipt.parentId
83
+ || managedDelegationRole(start.nativeAgent) !== receipt.role
84
+ || start.hookDefinitionHash !== provenance.hookDefinitionHash
85
+ || host.hookDefinitionHash !== provenance.hookDefinitionHash
86
+ || host.capabilitySessionId !== provenance.capabilitySessionId
87
+ || host.capabilityTurnId !== provenance.capabilityTurnId
88
+ || host.capabilityToolUseId !== provenance.capabilityToolUseId
89
+ || host.spawnToolUseId !== start.spawnToolUseId
90
+ || !host.spawnObservedAt
91
+ || host.spawnObservedAt !== record.spawnObservedAt
92
+ || host.controllerInstanceDigest !== provenance.controllerInstanceDigest
93
+ || host.controlGeneration !== provenance.controlGeneration
94
+ || typeof host.controllerInstanceDigest !== 'string'
95
+ || typeof host.controlGeneration !== 'string'
96
+ || host.startRevision < 1
97
+ || record.revision <= host.startRevision
98
+ || record.state.child?.childThreadId !== receipt.childId
99
+ || record.state.stop?.childThreadId !== receipt.childId) {
100
+ return { ok: false, code: 'RECOVERY_STOP_EVIDENCE_INVALID', message: 'Codex stop evidence does not match the activated receipt' };
101
+ }
102
+ if (record.consumer !== null && record.consumer !== receipt.id) {
103
+ return { ok: false, code: 'RECOVERY_CONSUMER_CONFLICT', message: `Codex lifecycle evidence was consumed by '${record.consumer}'` };
104
+ }
105
+ return { ok: true };
106
+ }
107
+ function abortedCandidates(run, rows) {
108
+ const candidates = run.receipts.flatMap((receipt) => {
109
+ if (receipt.status !== 'aborted' || receipt.activatedAt === null || receipt.client !== 'codex')
110
+ return [];
111
+ const matches = rowForReceipt(rows, receipt);
112
+ return matches.length === 1 ? [{ receipt, row: matches[0] }] : [];
113
+ });
114
+ return candidates;
115
+ }
116
+ function appendRecoveryLog(request, taskId, section, candidate, repoRoot, writer) {
117
+ const metadata = captureTaskWriteMetadata();
118
+ const body = appendActivityEntry(section, {
119
+ time: metadata.timestamp,
120
+ step: `${candidate.row.step} [aborted]`,
121
+ agent: request.agent,
122
+ note: `terminated Codex delegation; receipt=${candidate.receipt.id}; child=${candidate.receipt.childId}`
123
+ });
124
+ const written = writer({
125
+ taskRef: taskId,
126
+ expectedState: 'active',
127
+ mutations: [{ kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: section.heading, body }]
128
+ }, { repoRoot, metadataProvider: () => metadata });
129
+ if (written.status === 'failed') {
130
+ return failure(request, 'owner-unknown', 'RECOVERY_LOG_WRITE_FAILED', written.error.message, {
131
+ taskId, receiptId: candidate.receipt.id, childId: candidate.receipt.childId
132
+ });
133
+ }
134
+ return result(request, 'applied', {
135
+ taskId, receiptId: candidate.receipt.id, childId: candidate.receipt.childId
136
+ });
137
+ }
138
+ function recoverStartedLifecycleUnderLock(request, options = {}) {
139
+ if (!request || request.intent !== 'recover-started' || request.auto !== true || normalizeAgentToken(request.agent) !== 'codex') {
140
+ return failure(request, 'conflict', 'RECOVERY_PAYLOAD_INVALID', 'automatic recovery requires a task and the Codex agent');
141
+ }
142
+ const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
143
+ if (!resolved.ok)
144
+ return failure(request, 'conflict', resolved.code, resolved.message);
145
+ const taskId = resolved.taskId;
146
+ let content;
147
+ try {
148
+ content = fs.readFileSync(resolved.taskMdPath, 'utf8');
149
+ parseTypedTaskFrontmatter(content);
150
+ }
151
+ catch (error) {
152
+ return failure(request, 'conflict', 'RECOVERY_TASK_INVALID', error instanceof Error ? error.message : String(error), { taskId });
153
+ }
154
+ const section = locateActivityLog(content);
155
+ if (!section)
156
+ return failure(request, 'conflict', 'RECOVERY_LOG_INVALID', 'task has no unique Activity Log section', { taskId });
157
+ let run;
158
+ try {
159
+ run = readRun(resolved.taskDir, options.orchestration);
160
+ }
161
+ catch (error) {
162
+ return failure(request, 'owner-unknown', 'RECOVERY_ORCHESTRATION_UNKNOWN', error instanceof Error ? error.message : String(error), { taskId });
163
+ }
164
+ if (!run)
165
+ return result(request, 'no-op', { taskId });
166
+ const rows = openRows(section);
167
+ const pending = run.pendingDelegation;
168
+ if (!pending) {
169
+ const candidates = abortedCandidates(run, rows);
170
+ if (candidates.length === 0)
171
+ return result(request, 'no-op', { taskId });
172
+ if (candidates.length > 1) {
173
+ return failure(request, 'conflict', 'RECOVERY_CANDIDATE_AMBIGUOUS', 'more than one aborted delegation has an open lifecycle row', { taskId });
174
+ }
175
+ return appendRecoveryLog(request, taskId, section, candidates[0], resolved.repoRoot, options.writeTask ?? writeTask);
176
+ }
177
+ if (pending.status !== 'activated')
178
+ return result(request, 'no-op', { taskId });
179
+ if (pending.client !== 'codex' || run.status !== 'running' || run.pause !== null) {
180
+ return failure(request, 'conflict', 'RECOVERY_ORCHESTRATION_INVALID', 'activated recovery requires one running Codex delegation', {
181
+ taskId, receiptId: pending.id, childId: pending.childId
182
+ });
183
+ }
184
+ const matchingRows = rowForReceipt(rows, pending);
185
+ if (matchingRows.length !== 1 || normalizeAgentToken(matchingRows[0].agent) !== 'codex' || matchingRows[0].note !== 'started') {
186
+ return failure(request, 'conflict', 'RECOVERY_LOG_CONFLICT', 'activated delegation does not have one matching open lifecycle row', {
187
+ taskId, receiptId: pending.id, childId: pending.childId
188
+ });
189
+ }
190
+ if (!pending.childId) {
191
+ return failure(request, 'conflict', 'RECOVERY_DELEGATION_INVALID', 'activated delegation has no child identity', { taskId, receiptId: pending.id });
192
+ }
193
+ const store = readLifecycleStore(options, resolved.repoRoot);
194
+ const stored = readStoredEvidence(store, pending.childId);
195
+ if ('error' in stored)
196
+ return failure(request, 'owner-unknown', 'RECOVERY_STORE_UNKNOWN', stored.error.message, { taskId, receiptId: pending.id, childId: pending.childId });
197
+ if ('missing' in stored)
198
+ return failure(request, 'owner-unknown', 'RECOVERY_STOP_EVIDENCE_MISSING', 'matching Codex lifecycle stop evidence is missing', { taskId, receiptId: pending.id, childId: pending.childId });
199
+ const evidence = validateStopRecord(stored, pending);
200
+ if (!evidence.ok)
201
+ return failure(request, evidence.code === 'RECOVERY_CONSUMER_CONFLICT' ? 'conflict' : 'owner-unknown', evidence.code, evidence.message, { taskId, receiptId: pending.id, childId: pending.childId });
202
+ let consumed = stored;
203
+ if (stored.consumer === null) {
204
+ try {
205
+ consumed = store.consume(pending.childId, pending.id, pending.hostEvidence?.hookDefinitionHash);
206
+ }
207
+ catch (error) {
208
+ return failure(request, 'owner-unknown', 'RECOVERY_EVIDENCE_CONSUME_FAILED', error instanceof Error ? error.message : String(error), { taskId, receiptId: pending.id, childId: pending.childId });
209
+ }
210
+ }
211
+ if (!consumed.consumedAt) {
212
+ return failure(request, 'owner-unknown', 'RECOVERY_EVIDENCE_INVALID', 'consumed stop evidence has no timestamp', { taskId, receiptId: pending.id, childId: pending.childId });
213
+ }
214
+ const recovered = recoverActivatedOrchestrationDelegationUnderLock(taskId, {
215
+ receiptId: pending.id,
216
+ stage: pending.stage,
217
+ round: pending.round,
218
+ artifact: pending.artifact,
219
+ startedAgent: request.agent,
220
+ childId: pending.childId,
221
+ stopRevision: consumed.revision,
222
+ consumer: pending.id,
223
+ consumedAt: consumed.consumedAt
224
+ }, { ...options.orchestration, repoRoot: resolved.repoRoot, now: options.now });
225
+ if (recovered.status === 'failed' || !recovered.run) {
226
+ return failure(request, 'owner-unknown', recovered.error?.code ?? 'RECOVERY_ORCHESTRATION_FAILED', recovered.error?.message ?? 'orchestration recovery failed', { taskId, receiptId: pending.id, childId: pending.childId });
227
+ }
228
+ return appendRecoveryLog(request, taskId, section, { receipt: recovered.run.receipts.at(-1), row: matchingRows[0] }, resolved.repoRoot, options.writeTask ?? writeTask);
229
+ }
230
+ function recoverStartedLifecycleFromAdapter(request, options = {}) {
231
+ return recoverStartedLifecycleUnderLock(request, options);
232
+ }
233
+ export { recoverStartedLifecycleFromAdapter, recoverStartedLifecycleUnderLock };
234
+ //# sourceMappingURL=recovery.js.map
@@ -5,8 +5,7 @@ import os from 'node:os';
5
5
  import path from 'node:path';
6
6
  import semver from 'semver';
7
7
  import * as toml from 'smol-toml';
8
- import { requestCodexControllerClose, requestCodexControllerOpen, requestCodexControllerVerify, requestSandboxControl } from '../../../sandbox/control/client.js';
9
- import { readSandboxControlStatus } from '../../../sandbox/control/state.js';
8
+ import { requestCodexControllerClose, requestCodexControllerOpen, requestCodexControllerVerify } from '../../../sandbox/control/client.js';
10
9
  import { getProcessStartTime } from '../../../server/process-state.js';
11
10
  import { LIFECYCLE_PROTOCOL_VERSION } from './build-identity.js';
12
11
  import { contextFromControllerLease, controllerProofFromContext, verifyCodexSandboxControllerContextWithWarnings as verifyContextFileWithWarnings, writeCodexSandboxControllerContext } from './controller-context.js';
@@ -56,44 +55,18 @@ function verifyRuntime(runtimeDir) {
56
55
  fs.rmSync(probe, { force: true });
57
56
  }
58
57
  }
59
- function verifyControl(taskId, control) {
58
+ function verifyControllerBinding(context, control, requestControllerVerify) {
60
59
  verifyRuntime(control.runtimeDir);
61
- let status = null;
62
- let lastError = null;
63
- for (let attempt = 0; attempt < 5; attempt += 1) {
64
- try {
65
- status = readSandboxControlStatus(control.statusDir);
66
- break;
67
- }
68
- catch (error) {
69
- lastError = error;
70
- if (attempt < 4)
71
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
72
- }
73
- }
74
- if (!status) {
75
- throw new Error(`CODEX_SANDBOX_CONTROLLER_CONTROL_UNAVAILABLE: ${String(lastError)}`);
76
- }
77
- if (status.generation !== control.generation
78
- || status.state !== 'healthy'
79
- || Date.now() - status.updatedAt > 1_500) {
80
- throw new Error('CODEX_SANDBOX_CONTROLLER_CONTROL_UNAVAILABLE');
81
- }
82
- const response = requestSandboxControl({
83
- family: 'task-orchestration',
84
- args: [taskId, 'status'],
60
+ const verified = requestControllerVerify({
61
+ controllerProof: controllerProofFromContext(context),
85
62
  ...control,
86
63
  timeoutMs: 30_000
87
64
  });
88
- let payload = {};
89
- try {
90
- payload = JSON.parse(response.stdout);
91
- }
92
- catch {
65
+ if (verified.binding.taskId !== context.taskId
66
+ || verified.binding.controlGeneration !== context.controlGeneration
67
+ || verified.binding.controllerInstanceDigest !== context.controllerInstanceDigest) {
93
68
  throw new Error('CODEX_SANDBOX_CONTROLLER_TASK_BINDING_INVALID');
94
69
  }
95
- if (payload.taskId !== taskId)
96
- throw new Error('CODEX_SANDBOX_CONTROLLER_TASK_BINDING_INVALID');
97
70
  }
98
71
  function verifyCodexSandboxControllerContextWithWarnings(contextPath, options = {}) {
99
72
  const control = options.control ?? controlFromEnvironment();
@@ -272,7 +245,7 @@ function prepareCodexSandboxController(input, options = {}) {
272
245
  || opened.lease.controllerProcess.startTime !== parentStartTime) {
273
246
  throw new Error('SANDBOX_CONTROL_RESULT_INVALID');
274
247
  }
275
- (options.verifyTaskBinding ?? verifyControl)(taskId, control);
248
+ verifyControllerBinding(context, control, options.verifyController ?? requestCodexControllerVerify);
276
249
  const contextPath = path.join(home, 'controller-context.json');
277
250
  writeCodexSandboxControllerContext(contextPath, context);
278
251
  const shimDir = path.join(home, 'bin');
@@ -6,19 +6,9 @@ import { resolveAgentRuntimeStoreRoot } from '../../../runtime/agent-runtime.js'
6
6
  const LOCK_RETRY_MS = 10;
7
7
  const LOCK_TIMEOUT_MS = 1_000;
8
8
  const LOCK_STALE_MS = 30_000;
9
- const RECOVERY_CONSUMER_RE = /^lifecycle-recovery:([^:\r\n]+):([^:\r\n]+)$/u;
10
9
  function digest(value) {
11
10
  return crypto.createHash('sha256').update(value).digest('hex');
12
11
  }
13
- function isRecoveryConsumer(value) {
14
- return RECOVERY_CONSUMER_RE.test(value);
15
- }
16
- function recoveryConsumer(taskId, receiptId) {
17
- if (!/^[^:\r\n]+$/u.test(taskId) || !/^[^:\r\n]+$/u.test(receiptId)) {
18
- throw new Error('Codex lifecycle recovery claim identity is invalid');
19
- }
20
- return `lifecycle-recovery:${taskId}:${receiptId}`;
21
- }
22
12
  function readRecord(file) {
23
13
  const value = JSON.parse(fs.readFileSync(file, 'utf8'));
24
14
  if (value.schemaVersion !== 1
@@ -255,29 +245,8 @@ function createCodexLifecycleStore(options) {
255
245
  function consume(childThreadId, consumer, expectedHookDefinitionHash) {
256
246
  if (!consumer.trim())
257
247
  throw new Error('Codex lifecycle consumer is required');
258
- if (consumer.startsWith('lifecycle-recovery:')) {
259
- throw new Error('Codex lifecycle recovery claims require claimRecovery');
260
- }
261
248
  return consumeInternal(childThreadId, consumer, expectedHookDefinitionHash);
262
249
  }
263
- function claimRecovery(childThreadId, taskId, receiptId, expectedHookDefinitionHash) {
264
- return consumeInternal(childThreadId, recoveryConsumer(taskId, receiptId), expectedHookDefinitionHash);
265
- }
266
- function releaseRecovery(childThreadId, consumer) {
267
- if (!/^lifecycle-recovery:[^:\r\n]+:[^:\r\n]+$/.test(consumer))
268
- return false;
269
- return withWriteLock(() => {
270
- const matches = findByChild(childThreadId);
271
- if (matches.length !== 1)
272
- return false;
273
- const file = matches[0];
274
- const current = readRecord(file);
275
- if (current.state.status !== 'stop-ready' || current.consumer !== consumer || !current.consumedAt)
276
- return false;
277
- fs.unlinkSync(file);
278
- return true;
279
- });
280
- }
281
250
  function expireBefore(cutoff) {
282
251
  return withWriteLock(() => {
283
252
  let changed = 0;
@@ -285,8 +254,6 @@ function createCodexLifecycleStore(options) {
285
254
  const current = readRecord(file);
286
255
  if (current.updatedAt >= cutoff)
287
256
  continue;
288
- if (current.consumer && isRecoveryConsumer(current.consumer))
289
- continue;
290
257
  if (current.consumer || ['invalid', 'expired', 'stop-ready'].includes(current.state.status)) {
291
258
  fs.unlinkSync(file);
292
259
  changed += 1;
@@ -306,7 +273,7 @@ function createCodexLifecycleStore(options) {
306
273
  return changed;
307
274
  });
308
275
  }
309
- return Object.freeze({ root, apply, applyToSpawn, consume, claimRecovery, releaseRecovery, expireBefore, findByParent, read });
276
+ return Object.freeze({ root, apply, applyToSpawn, consume, expireBefore, findByParent, read });
310
277
  }
311
278
  export { createCodexLifecycleStore, hasActiveCodexLifecycleEvidence };
312
279
  //# sourceMappingURL=store.js.map
@@ -24,6 +24,16 @@ const codexAdapter = defineAgentClientAdapter({
24
24
  actualModel: 'app-server',
25
25
  actualReasoningEffort: 'app-server'
26
26
  },
27
+ orchestrationAdapter: {
28
+ prepareDelegation: async (...args) => {
29
+ const { prepareCodexOrchestrationDelegation } = await import('../../task/codex-orchestration.js');
30
+ return prepareCodexOrchestrationDelegation(...args);
31
+ },
32
+ recoverStarted: async (...args) => {
33
+ const { recoverStartedLifecycleFromAdapter } = await import('./codex-lifecycle/recovery.js');
34
+ return recoverStartedLifecycleFromAdapter(...args);
35
+ }
36
+ },
27
37
  project: {
28
38
  ownedPathPrefixes: ['.codex/'],
29
39
  managed: ['.codex/hooks.json', '.codex/agents/'],
@@ -43,7 +43,7 @@ export const INTERNAL_HANDLER_ROUTE_SELECTORS = Object.freeze({
43
43
  'platform-security': ['read', 'dismiss'],
44
44
  'platform-metadata': ['init-labels', 'init-milestones'],
45
45
  'platform-context': ['resolve'],
46
- 'platform-comment': ['list', 'recover', 'owner', 'backfill', 'sync'],
46
+ 'platform-comment': ['list', 'recover', 'owner', 'backfill', 'stage-summary', 'sync'],
47
47
  'platform-issue': ['inspect', 'create', 'bind', 'sync'],
48
48
  'platform-pr': ['inspect', 'summary-context', 'resolve-external', 'create', 'bind', 'skip', 'sync', 'change-report', 'summary-sync', 'sync-in-labels'],
49
49
  'platform-pr-review': ['inspect', 'list', 'publish-pr', 'publish-task'],
@@ -5,7 +5,7 @@ import { resolveTaskRef } from '../task/resolve-ref.js';
5
5
  import { applyTaskEvent } from '../task/events.js';
6
6
  import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.js';
7
7
  import { normalizeAgentToken } from '../agent-clients/tokens.js';
8
- import { createManualValidationReceipt, manualValidationFinalSummaryDigest, manualValidationFinalSummaryProjectionMatches, readManualValidationReceipt, writeManualValidationReceiptAtomic } from '../task/manual-validation-receipt.js';
8
+ import { MANUAL_VALIDATION_RECEIPT_PLACEHOLDER, createManualValidationReceipt, manualValidationFinalSummaryDigest, manualValidationFinalSummaryProjectionMatches, readManualValidationReceipt, writeManualValidationReceiptAtomic } from '../task/manual-validation-receipt.js';
9
9
  import { archiveManualValidationGeneration, createManualValidationTransaction, readManualValidationTransaction, retryManualValidationTransaction, summaryPreimageDigest, transitionManualValidationTransaction, validateManualValidationGenerationArchive, writeManualValidationTransactionAtomic } from '../task/manual-validation-transaction.js';
10
10
  import { sha256File } from '../task/artifact-receipts.js';
11
11
  import { summaryCommentState, syncPullRequestSummary } from '../platform/pr-summary.js';
@@ -22,28 +22,22 @@ function printFailure(format, error) {
22
22
  process.stderr.write(`${error.code}: ${error.message}\n`);
23
23
  process.exitCode = 1;
24
24
  }
25
- const MANUAL_VALIDATION_STATUS_SECTION = /^###\s+(?:⚠️\s+(?:需人工校验|Manual Validation Required)|✅\s+(?:人工验证已通过|无需人工校验|Manual Validation Passed|No Manual Validation Required)|⏳\s+(?:人工验证待收尾|Manual Validation Pending))\s*$[\s\S]*?(?=^#{1,3}\s|(?![\s\S]))/gmu;
26
- const TRAILING_CANONICAL_REPORT_PLACEHOLDER = /\n*<!--\s*canonical-pr-change-report\s*-->\s*$/u;
27
- function manualSummaryBody(body, phase, transactionId, receiptDigest = '', evidenceDigest = '', prHeadSha = '') {
25
+ const MANUAL_VALIDATION_STATUS_HEADING = /^###\s+(?:⚠️\s+(?:需人工校验|Manual Validation Required)|✅\s+(?:人工验证已通过|无需人工校验|Manual Validation Passed|No Manual Validation Required)|⏳\s+(?:人工验证待收尾|Manual Validation Pending))\s*$/mu;
26
+ function manualSummaryBody(body, phase) {
28
27
  const chinese = /###\s+(?:⚠️\s+需人工校验|✅\s+(?:人工验证已通过|无需人工校验)|⏳\s+人工验证待收尾)\s*$/mu.test(body);
29
- const section = phase === 'pending'
30
- ? chinese
31
- ? `### ⏳ 人工验证待收尾\n\n人工验证证据已暂存;事务 ${transactionId} 正在等待最终完成。`
32
- : `### ⏳ Manual Validation Pending\n\nManual validation evidence is staged; transaction ${transactionId} is awaiting final promotion.`
33
- : chinese
34
- ? `### ✅ 人工验证已通过\n\n人工验证已通过;transaction=${transactionId}; receipt=${receiptDigest}; evidence=${evidenceDigest}; head=${prHeadSha}.`
35
- : `### ✅ Manual Validation Passed\n\nManual validation passed; transaction=${transactionId}; receipt=${receiptDigest}; evidence=${evidenceDigest}; head=${prHeadSha}.`;
36
- let inserted = false;
37
- const updated = body.replace(MANUAL_VALIDATION_STATUS_SECTION, (matched) => {
38
- if (inserted)
39
- return '';
40
- inserted = true;
41
- const trailingPlaceholder = TRAILING_CANONICAL_REPORT_PLACEHOLDER.exec(matched)?.[0];
42
- return trailingPlaceholder
43
- ? `${trailingPlaceholder.trim()}\n\n${section}\n\n`
44
- : `${section}\n\n`;
45
- });
46
- return inserted ? `${updated.replace(/\s+$/u, '')}\n` : `${body.replace(/\s+$/u, '')}\n\n${section}\n`;
28
+ const heading = phase === 'pending'
29
+ ? chinese ? '### ⏳ 人工验证待收尾' : '### ⏳ Manual Validation Pending'
30
+ : chinese ? '### ✅ 人工验证已通过' : '### ✅ Manual Validation Passed';
31
+ const marker = phase === 'final'
32
+ ? `\n\n${MANUAL_VALIDATION_RECEIPT_PLACEHOLDER}`
33
+ : '';
34
+ if (MANUAL_VALIDATION_STATUS_HEADING.test(body)) {
35
+ return `${body.replace(MANUAL_VALIDATION_STATUS_HEADING, `${heading}${marker}`).replace(/\s+$/u, '')}\n`;
36
+ }
37
+ const fallback = phase === 'pending'
38
+ ? chinese ? '人工验证正在等待最终完成。' : 'Manual validation is awaiting final promotion.'
39
+ : chinese ? '人工验证已通过。' : 'Manual validation passed.';
40
+ return `${body.replace(/\s+$/u, '')}\n\n${heading}${marker}\n\n${fallback}\n`;
47
41
  }
48
42
  function openManualValidationStarted(taskMdPath) {
49
43
  let content;
@@ -102,7 +96,9 @@ async function executeManualValidationTransactionLocked(taskRef, values, cwd, re
102
96
  const pullRequest = currentState.pullRequest;
103
97
  const preimageBody = currentState.comment?.body ?? '';
104
98
  const preimage = { commentId: currentState.comment?.id ?? null, body: preimageBody, digest: summaryPreimageDigest(preimageBody) };
105
- const evidenceDigest = sha256File(path.resolve(cwd, values.summaryFile));
99
+ const summaryPath = path.resolve(cwd, values.summaryFile);
100
+ const finalBodyWithoutReceipt = fs.readFileSync(summaryPath, 'utf8');
101
+ const evidenceDigest = sha256File(summaryPath);
106
102
  const archiveGeneration = options.archiveGeneration ?? archiveManualValidationGeneration;
107
103
  let transactionResult = readManualValidationTransaction(resolved.taskDir, { taskId: resolved.taskId, prNumber: pullRequest.number, prHeadSha: pullRequest.head.sha, evidenceDigest, artifact: values.artifact });
108
104
  let previousTransaction = null;
@@ -135,8 +131,7 @@ async function executeManualValidationTransactionLocked(taskRef, values, cwd, re
135
131
  const transactionId = transactionResult.ok
136
132
  ? transactionResult.value.transactionId
137
133
  : (openStarted.transactionId ?? values.transactionId ?? `mv-${Date.now()}`);
138
- const finalBodyWithoutReceipt = fs.readFileSync(path.resolve(cwd, values.summaryFile), 'utf8');
139
- const pendingBody = manualSummaryBody(finalBodyWithoutReceipt, 'pending', transactionId);
134
+ const pendingBody = manualSummaryBody(finalBodyWithoutReceipt, 'pending');
140
135
  const createTransaction = () => createManualValidationTransaction({
141
136
  transactionId,
142
137
  taskId: resolved.taskId,
@@ -145,7 +140,7 @@ async function executeManualValidationTransactionLocked(taskRef, values, cwd, re
145
140
  evidenceDigest,
146
141
  summaryPreimage: preimage,
147
142
  pendingSummaryDigest: summaryPreimageDigest(pendingBody),
148
- finalSummaryDigest: manualValidationFinalSummaryDigest(manualSummaryBody(finalBodyWithoutReceipt, 'final', transactionId, '<receipt>', evidenceDigest, pullRequest.head.sha)),
143
+ finalSummaryDigest: manualValidationFinalSummaryDigest(manualSummaryBody(finalBodyWithoutReceipt, 'final')),
149
144
  artifact: values.artifact,
150
145
  attempt: 1,
151
146
  createdAt: new Date().toISOString(),
@@ -249,7 +244,7 @@ async function executeManualValidationTransactionLocked(taskRef, values, cwd, re
249
244
  transitionAndPersist('final-promotion-in-progress');
250
245
  }
251
246
  const promoted = await syncPullRequestSummary(taskRef, {
252
- cwd, client: options.client, agent, body: manualSummaryBody(finalBodyWithoutReceipt, 'final', transaction.transactionId, receipt.receiptDigest, receipt.evidenceDigest, receipt.prHeadSha), changeReportFile: path.resolve(cwd, values.changeReportFile), primaryResult, strict: true,
247
+ cwd, client: options.client, agent, body: manualSummaryBody(finalBodyWithoutReceipt, 'final'), changeReportFile: path.resolve(cwd, values.changeReportFile), primaryResult, strict: true,
253
248
  manualValidation: { phase: 'final', transactionId: transaction.transactionId, receiptDigest: receipt.receiptDigest, prHeadSha: receipt.prHeadSha, authority: 'coordinator' }, lockAlreadyHeld: true
254
249
  });
255
250
  if (!['applied', 'no-op'].includes(promoted.status))
@@ -1,14 +1,19 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { createHash, randomUUID } from 'node:crypto';
3
4
  import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.js';
4
5
  import { checkPlatformCommentOwner, listPlatformComments, syncPlatformComment } from '../platform/issue-comments.js';
6
+ import { canonicalizeSummaryBody } from '../platform/comment-safety.js';
7
+ import { platformResult } from '../platform/types.js';
5
8
  import { recoverTaskFromComments } from '../platform/task-recovery.js';
6
9
  import { backfillCompletionComments } from '../platform/completion-backfill.js';
7
10
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.js';
11
+ import { resolveTaskRef } from '../task/resolve-ref.js';
8
12
  const USAGE = `Usage: agent-infra-internal platform-comment list --issue <token> [--cwd <path>]
9
13
  agent-infra-internal platform-comment recover --issue <token> --task-id <TASK-id> --output <staging-task.md> [--cwd <path>]
10
14
  agent-infra-internal platform-comment owner <task-ref> [--cwd <path>]
11
15
  agent-infra-internal platform-comment backfill <task-ref> --agent <agent> [--cwd <path>]
16
+ agent-infra-internal platform-comment stage-summary <task-ref> --body-file <path|-> [--cwd <path>]
12
17
  agent-infra-internal platform-comment sync <task-ref> --kind <kind> --agent <agent> [--artifact <file>] [--body-file <path|->] [--status-label <label>] [--backfill] [--cwd <path>]
13
18
  `;
14
19
  function fail(message) {
@@ -63,6 +68,7 @@ async function platformComment(args = []) {
63
68
  internalHandlerRoute('platform-comment', 'recover', operation),
64
69
  internalHandlerRoute('platform-comment', 'owner', operation),
65
70
  internalHandlerRoute('platform-comment', 'backfill', operation),
71
+ internalHandlerRoute('platform-comment', 'stage-summary', operation),
66
72
  internalHandlerRoute('platform-comment', 'sync', operation)
67
73
  ].some(Boolean)) {
68
74
  fail('a valid operation is required');
@@ -177,6 +183,59 @@ async function platformComment(args = []) {
177
183
  finish(await backfillCompletionComments(taskRef, { cwd, agent: normalizedAgent }));
178
184
  return;
179
185
  }
186
+ if (internalHandlerRoute('platform-comment', 'stage-summary', operation)) {
187
+ const unexpected = Object.keys(parsed.values).find((key) => !['cwd', 'bodyFile'].includes(key));
188
+ if (unexpected) {
189
+ fail(`stage-summary does not accept '--${unexpected}'`);
190
+ return;
191
+ }
192
+ if (typeof parsed.values.bodyFile !== 'string') {
193
+ fail('stage-summary requires --body-file');
194
+ return;
195
+ }
196
+ let body;
197
+ try {
198
+ body = readBodyFile(parsed.values.bodyFile, cwd);
199
+ }
200
+ catch (error) {
201
+ fail(`unable to read body file: ${error instanceof Error ? error.message : String(error)}`);
202
+ return;
203
+ }
204
+ const canonical = canonicalizeSummaryBody(body);
205
+ if (!canonical.ok) {
206
+ fail(`unable to stage summary: ${canonical.error.code}: ${canonical.error.message}`);
207
+ return;
208
+ }
209
+ body = canonical.value;
210
+ const resolved = resolveTaskRef(taskRef, { repoRoot: cwd });
211
+ if (!resolved.ok || resolved.state !== 'active') {
212
+ fail('stage-summary requires an active task');
213
+ return;
214
+ }
215
+ const target = path.join(resolved.taskDir, '.delivery-summary.json');
216
+ const value = { taskId: resolved.taskId, body, sha256: createHash('sha256').update(body).digest('hex') };
217
+ const previous = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : null;
218
+ const next = `${JSON.stringify(value)}\n`;
219
+ if (previous === next) {
220
+ finish(platformResult('no-op'));
221
+ return;
222
+ }
223
+ const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
224
+ try {
225
+ fs.writeFileSync(temporary, next, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
226
+ fs.renameSync(temporary, target);
227
+ }
228
+ catch (error) {
229
+ try {
230
+ fs.unlinkSync(temporary);
231
+ }
232
+ catch { /* preserve primary error */ }
233
+ fail(`unable to stage summary: ${error instanceof Error ? error.message : String(error)}`);
234
+ return;
235
+ }
236
+ finish(platformResult('applied', { operations: [{ name: 'delivery-summary', status: 'applied', reasonCode: null }] }));
237
+ return;
238
+ }
180
239
  if (!internalHandlerRoute('platform-comment', 'sync', operation)) {
181
240
  fail('operation is not registered');
182
241
  return;