@fitlab-ai/agent-infra 0.11.9 → 0.11.11

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 (219) hide show
  1. package/dist/bin/internal-cli.js +0 -6
  2. package/dist/lib/agent-clients/adapter.js +20 -0
  3. package/dist/lib/agent-clients/adapters/codex-sandbox.js +13 -0
  4. package/dist/lib/agent-clients/adapters/codex.js +4 -0
  5. package/dist/lib/git/workflow.js +1 -1
  6. package/dist/lib/internal/cli-route-inventory.js +0 -3
  7. package/dist/lib/internal/manual-validation.js +2 -2
  8. package/dist/lib/internal/platform-pr.js +4 -5
  9. package/dist/lib/internal/sandbox-control.js +30 -5
  10. package/dist/lib/internal/task-artifact.js +1 -1
  11. package/dist/lib/internal/task-create.js +15 -1
  12. package/dist/lib/internal/task-finalization.js +14 -25
  13. package/dist/lib/internal/task-operation-registry.js +6 -141
  14. package/dist/lib/internal/task-platform-recovery.js +9 -4
  15. package/dist/lib/platform/completion-backfill.js +2 -1
  16. package/dist/lib/platform/context.js +4 -2
  17. package/dist/lib/platform/github-client.js +20 -17
  18. package/dist/lib/platform/github-provider.js +42 -22
  19. package/dist/lib/platform/github-release-notes.js +4 -2
  20. package/dist/lib/platform/in-label-sync.js +2 -1
  21. package/dist/lib/platform/issue-comments.js +13 -9
  22. package/dist/lib/platform/operation-coordinator.js +1 -1
  23. package/dist/lib/platform/pr-review.js +2 -1
  24. package/dist/lib/platform/pr-summary.js +45 -8
  25. package/dist/lib/platform/provider-bridge.js +4 -5
  26. package/dist/lib/platform/provider-contract.d.ts +3 -6
  27. package/dist/lib/platform/provider-contract.js +3 -15
  28. package/dist/lib/platform/provider-validation.js +9 -4
  29. package/dist/lib/platform/pull-requests.js +83 -16
  30. package/dist/lib/platform/verification-sync.js +17 -14
  31. package/dist/lib/sandbox/commands/create.js +18 -2
  32. package/dist/lib/sandbox/commands/enter.js +47 -33
  33. package/dist/lib/sandbox/control/client.js +99 -17
  34. package/dist/lib/sandbox/control/executor.js +12 -14
  35. package/dist/lib/sandbox/control/host-recovery.js +218 -0
  36. package/dist/lib/sandbox/control/protocol.js +2 -3
  37. package/dist/lib/sandbox/control/server.js +42 -2
  38. package/dist/lib/sandbox/environment.js +150 -0
  39. package/dist/lib/sandbox/recovery.js +8 -0
  40. package/dist/lib/sandbox/removal.js +0 -1
  41. package/dist/lib/sandbox/workspace-identity.js +0 -14
  42. package/dist/lib/sandbox/workspace-view.js +33 -6
  43. package/dist/lib/task/artifact-command.js +18 -3
  44. package/dist/lib/task/artifact-lifecycle.js +121 -63
  45. package/dist/lib/task/artifact-selection.js +1 -1
  46. package/dist/lib/task/capabilities.js +59 -22
  47. package/dist/lib/task/commands/status.js +0 -27
  48. package/dist/lib/task/commit-identity.js +22 -7
  49. package/dist/lib/task/commit-intent.js +67 -26
  50. package/dist/lib/task/commit-operation.js +19 -8
  51. package/dist/lib/task/completion-facts.js +32 -10
  52. package/dist/lib/task/control-authority.js +2 -6
  53. package/dist/lib/task/current-contract.js +1 -5
  54. package/dist/lib/task/events.js +9 -275
  55. package/dist/lib/task/execution-inputs.js +14 -0
  56. package/dist/lib/task/finalization-envelope.js +4 -0
  57. package/dist/lib/task/finalization.js +43 -86
  58. package/dist/lib/task/frontmatter.js +25 -2
  59. package/dist/lib/task/ledger-intents.js +6 -8
  60. package/dist/lib/task/orchestration.js +11 -10
  61. package/dist/lib/task/platform-operation-journal.js +29 -10
  62. package/dist/lib/task/platform-operation-recovery.js +109 -35
  63. package/dist/lib/task/resolve-ref.js +8 -3
  64. package/dist/lib/task/review-fingerprint.js +4 -26
  65. package/dist/lib/task/verification-engine.js +95 -6
  66. package/dist/lib/task/write.js +0 -11
  67. package/dist/lifecycle-build-manifest.json +3 -3
  68. package/lib/agent-clients/adapter.ts +22 -0
  69. package/lib/agent-clients/adapters/codex-sandbox.ts +13 -0
  70. package/lib/agent-clients/adapters/codex.ts +4 -0
  71. package/lib/git/workflow.ts +1 -1
  72. package/lib/internal/cli-route-inventory.ts +0 -2
  73. package/lib/internal/manual-validation.ts +2 -2
  74. package/lib/internal/platform-pr.ts +3 -4
  75. package/lib/internal/sandbox-control.ts +39 -6
  76. package/lib/internal/task-artifact.ts +1 -1
  77. package/lib/internal/task-create.ts +15 -2
  78. package/lib/internal/task-finalization.ts +16 -26
  79. package/lib/internal/task-operation-registry.ts +6 -153
  80. package/lib/internal/task-platform-recovery.ts +9 -4
  81. package/lib/platform/completion-backfill.ts +2 -1
  82. package/lib/platform/context.ts +8 -4
  83. package/lib/platform/github-client.ts +24 -20
  84. package/lib/platform/github-provider.ts +47 -26
  85. package/lib/platform/github-release-notes.ts +5 -3
  86. package/lib/platform/in-label-sync.ts +2 -1
  87. package/lib/platform/issue-comments.ts +14 -9
  88. package/lib/platform/operation-coordinator.ts +1 -1
  89. package/lib/platform/pr-review.ts +2 -1
  90. package/lib/platform/pr-summary.ts +43 -7
  91. package/lib/platform/provider-bridge.ts +10 -9
  92. package/lib/platform/provider-contract.ts +6 -21
  93. package/lib/platform/provider-loader.ts +7 -6
  94. package/lib/platform/provider-validation.ts +30 -8
  95. package/lib/platform/pull-requests.ts +78 -15
  96. package/lib/platform/repository-metadata.ts +3 -2
  97. package/lib/platform/security-alerts.ts +1 -1
  98. package/lib/platform/types.ts +2 -0
  99. package/lib/platform/verification-sync.ts +18 -14
  100. package/lib/sandbox/commands/create.ts +24 -2
  101. package/lib/sandbox/commands/enter.ts +42 -39
  102. package/lib/sandbox/control/client.ts +107 -19
  103. package/lib/sandbox/control/executor.ts +13 -15
  104. package/lib/sandbox/control/host-recovery.ts +195 -0
  105. package/lib/sandbox/control/protocol.ts +4 -5
  106. package/lib/sandbox/control/server.ts +42 -1
  107. package/lib/sandbox/environment.ts +162 -0
  108. package/lib/sandbox/recovery.ts +10 -0
  109. package/lib/sandbox/removal.ts +2 -10
  110. package/lib/sandbox/tool-types.ts +1 -0
  111. package/lib/sandbox/workspace-identity.ts +0 -26
  112. package/lib/sandbox/workspace-view.ts +33 -8
  113. package/lib/task/artifact-command.ts +19 -3
  114. package/lib/task/artifact-lifecycle.ts +102 -53
  115. package/lib/task/artifact-selection.ts +1 -1
  116. package/lib/task/capabilities.ts +52 -26
  117. package/lib/task/commands/status.ts +0 -31
  118. package/lib/task/commit-identity.ts +24 -7
  119. package/lib/task/commit-intent.ts +58 -24
  120. package/lib/task/commit-operation.ts +17 -8
  121. package/lib/task/completion-facts.ts +31 -9
  122. package/lib/task/control-authority.ts +1 -8
  123. package/lib/task/current-contract.ts +1 -6
  124. package/lib/task/events.ts +9 -248
  125. package/lib/task/execution-inputs.ts +25 -0
  126. package/lib/task/finalization-envelope.ts +21 -0
  127. package/lib/task/finalization.ts +40 -82
  128. package/lib/task/frontmatter.ts +26 -2
  129. package/lib/task/ledger-intents.ts +6 -8
  130. package/lib/task/orchestration.ts +11 -17
  131. package/lib/task/platform-operation-journal.ts +47 -13
  132. package/lib/task/platform-operation-recovery.ts +102 -37
  133. package/lib/task/resolve-ref.ts +8 -3
  134. package/lib/task/review-fingerprint.ts +4 -26
  135. package/lib/task/verification-engine.ts +103 -9
  136. package/lib/task/write.ts +0 -19
  137. package/package.json +1 -1
  138. package/templates/.agents/rules/README.en.md +1 -1
  139. package/templates/.agents/rules/README.zh-CN.md +1 -1
  140. package/templates/.agents/rules/commit-and-pr.en.md +5 -5
  141. package/templates/.agents/rules/commit-and-pr.zh-CN.md +5 -5
  142. package/templates/.agents/rules/decision-qualification.en.md +1 -1
  143. package/templates/.agents/rules/decision-qualification.zh-CN.md +1 -1
  144. package/templates/.agents/rules/issue-sync.en.md +1 -1
  145. package/templates/.agents/rules/issue-sync.zh-CN.md +1 -1
  146. package/templates/.agents/rules/no-mid-flow-questions.en.md +19 -0
  147. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +19 -0
  148. package/templates/.agents/rules/review-handshake.en.md +3 -0
  149. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -0
  150. package/templates/.agents/rules/task-management.en.md +1 -1
  151. package/templates/.agents/rules/task-management.zh-CN.md +1 -1
  152. package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
  153. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -1
  154. package/templates/.agents/skills/block-task/SKILL.en.md +1 -1
  155. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
  156. package/templates/.agents/skills/cancel-task/SKILL.en.md +1 -1
  157. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +1 -1
  158. package/templates/.agents/skills/close-codescan/SKILL.en.md +3 -0
  159. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +3 -0
  160. package/templates/.agents/skills/close-dependabot/SKILL.en.md +3 -0
  161. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +3 -0
  162. package/templates/.agents/skills/code-task/SKILL.en.md +25 -9
  163. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +27 -10
  164. package/templates/.agents/skills/code-task/reference/branch-management.en.md +1 -1
  165. package/templates/.agents/skills/code-task/reference/branch-management.zh-CN.md +1 -1
  166. package/templates/.agents/skills/code-task/reference/code-rules.en.md +1 -1
  167. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +1 -1
  168. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +2 -0
  169. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +2 -0
  170. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +5 -16
  171. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +5 -16
  172. package/templates/.agents/skills/code-task/reference/report-template.en.md +1 -1
  173. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +1 -1
  174. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
  175. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
  176. package/templates/.agents/skills/complete-task/SKILL.en.md +15 -12
  177. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +11 -10
  178. package/templates/.agents/skills/create-pr/SKILL.en.md +1 -1
  179. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
  180. package/templates/.agents/skills/create-pr/reference/branch-strategy.en.md +2 -2
  181. package/templates/.agents/skills/create-pr/reference/branch-strategy.zh-CN.md +2 -2
  182. package/templates/.agents/skills/create-task/SKILL.en.md +3 -5
  183. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +3 -4
  184. package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
  185. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  186. package/templates/.agents/skills/import-dependabot/SKILL.en.md +1 -1
  187. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
  188. package/templates/.agents/skills/import-issue/SKILL.en.md +1 -1
  189. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
  190. package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
  191. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -1
  192. package/templates/.agents/skills/review-code/SKILL.en.md +4 -4
  193. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -4
  194. package/templates/.agents/skills/review-pr/SKILL.en.md +1 -1
  195. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +1 -1
  196. package/templates/.agents/skills/test/SKILL.en.md +4 -1
  197. package/templates/.agents/skills/test/SKILL.zh-CN.md +4 -1
  198. package/templates/.agents/skills/test-integration/SKILL.en.md +5 -2
  199. package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +5 -2
  200. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +5 -2
  201. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +5 -2
  202. package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +5 -2
  203. package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +5 -2
  204. package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
  205. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
  206. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
  207. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
  208. package/templates/.agents/templates/task.zh-CN.md +0 -12
  209. package/templates/.github/workflows/pr-label.yml +1 -2
  210. package/dist/lib/internal/task-invalidation.js +0 -63
  211. package/dist/lib/task/finalization-handoff.js +0 -93
  212. package/dist/lib/task/intermediate-cleanup.js +0 -327
  213. package/dist/lib/task/invalidation-command.js +0 -88
  214. package/dist/lib/task/invalidation.js +0 -242
  215. package/lib/internal/task-invalidation.ts +0 -41
  216. package/lib/task/finalization-handoff.ts +0 -105
  217. package/lib/task/intermediate-cleanup.ts +0 -393
  218. package/lib/task/invalidation-command.ts +0 -88
  219. package/lib/task/invalidation.ts +0 -313
@@ -13,7 +13,6 @@ const taskWorkflowCommand = command === 'task-artifact'
13
13
  || command === 'task-review'
14
14
  || command === 'task-event'
15
15
  || command === 'task-ledger'
16
- || command === 'task-invalidation'
17
16
  || command === 'task-warning';
18
17
  const taskPlatformRecoveryCommand = command === 'task-platform-recovery';
19
18
  const manualValidationWorkflowCommand = command === 'manual-validation';
@@ -231,11 +230,6 @@ if (!controlRouted && !taskViewGuardFailed && internalRouteRegistered)
231
230
  await taskEvent(process.argv.slice(3));
232
231
  break;
233
232
  }
234
- case 'task-invalidation': {
235
- const { taskInvalidation } = await import('../lib/internal/task-invalidation.js');
236
- taskInvalidation(process.argv.slice(3));
237
- break;
238
- }
239
233
  case 'task-lifecycle': {
240
234
  const { taskLifecycle } = await import('../lib/internal/task-lifecycle.js');
241
235
  await taskLifecycle(process.argv.slice(3));
@@ -1,3 +1,4 @@
1
+ import path from 'node:path';
1
2
  import { AGENT_CLIENT_CAPABILITY_IDS, AGENT_CLIENT_SUPPORT_LEVELS, isAgentClientId } from './types.js';
2
3
  import { SANDBOX_HOOK_PHASES } from '../sandbox/tool-types.js';
3
4
  const PROJECT_ASSET_CATEGORIES = ['managed', 'merged', 'ejected'];
@@ -65,6 +66,24 @@ function freezeTool(tool) {
65
66
  if (!tool.containerMount.startsWith('/')) {
66
67
  throw new Error(`Sandbox tool '${tool.id}' containerMount must be absolute`);
67
68
  }
69
+ if (tool.hostStateMounts !== undefined && !Array.isArray(tool.hostStateMounts)) {
70
+ throw new Error(`Agent Client '${tool.id}' hostStateMounts must be an array`);
71
+ }
72
+ const hostStateMounts = tool.hostStateMounts?.map((entry) => {
73
+ if (!entry || typeof entry.hostSubdir !== 'string' || typeof entry.containerSubpath !== 'string') {
74
+ throw new Error(`Agent Client '${tool.id}' hostStateMounts must contain relative path pairs`);
75
+ }
76
+ for (const value of [entry.hostSubdir, entry.containerSubpath]) {
77
+ const normalized = path.posix.normalize(value);
78
+ if (value === ''
79
+ || path.posix.isAbsolute(value)
80
+ || normalized === '..'
81
+ || normalized.startsWith('../')) {
82
+ throw new Error(`Agent Client '${tool.id}' hostStateMounts must stay within the tool directories`);
83
+ }
84
+ }
85
+ return Object.freeze({ ...entry });
86
+ });
68
87
  if (!tool.versionCmd) {
69
88
  throw new Error(`Sandbox tool '${tool.id}' requires versionCmd`);
70
89
  }
@@ -82,6 +101,7 @@ function freezeTool(tool) {
82
101
  ...(tool.hostLiveMounts ? {
83
102
  hostLiveMounts: Object.freeze(tool.hostLiveMounts.map((entry) => Object.freeze({ ...entry })))
84
103
  } : {}),
104
+ ...(hostStateMounts ? { hostStateMounts: Object.freeze(hostStateMounts) } : {}),
85
105
  ...(tool.postSetupCmds ? { postSetupCmds: Object.freeze([...tool.postSetupCmds]) } : {}),
86
106
  ...(tool.tmpfs ? {
87
107
  tmpfs: Object.freeze({
@@ -241,6 +241,19 @@ const codexRecoveryChecks = [
241
241
  path: '/home/devuser/.codex'
242
242
  }
243
243
  },
244
+ {
245
+ id: 'app-server-package-writable',
246
+ probe: {
247
+ script: 'probe="$1/.agent-infra-app-server-package-$$"; trap \'rm -f -- "$probe"\' EXIT; : > "$probe"',
248
+ args: ['/home/devuser/.codex/packages/app-server-daemon'],
249
+ user: 'devuser'
250
+ },
251
+ finding: {
252
+ repairKind: 'hard-failure',
253
+ message: 'Codex app-server package directory is not writable by devuser.',
254
+ path: '/home/devuser/.codex/packages/app-server-daemon'
255
+ }
256
+ },
244
257
  {
245
258
  id: 'prompts-link',
246
259
  when: {
@@ -51,6 +51,10 @@ const codexAdapter = defineAgentClientAdapter({
51
51
  versionCmd: 'codex --version',
52
52
  setupHint: 'Run codex once inside the container and choose Device Code login if needed.',
53
53
  tmpfs: { size: '512m', seed: ['config.toml', 'model-catalogs'], exec: true },
54
+ hostStateMounts: [{
55
+ hostSubdir: 'packages/app-server-daemon',
56
+ containerSubpath: 'packages/app-server-daemon'
57
+ }],
54
58
  hostLiveMounts: [
55
59
  {
56
60
  hostPath: hostJoin(home, '.codex', 'auth.json'),
@@ -103,7 +103,7 @@ function commitExplicitPaths(input, runner = defaultRunner) {
103
103
  if (tree !== input.expectedTree)
104
104
  return { status: 'failed', changed: false, snapshot: inspectGitWorkflow(input.cwd, runner).snapshot, operations: [{ name: 'stage', status: 'applied' }], error: { code: 'GIT_TREE_MISMATCH', message: `Expected staged tree ${input.expectedTree}, received ${tree ?? 'unavailable'}` } };
105
105
  }
106
- const commit = run(runner, input.cwd, ['commit', '-m', input.message]);
106
+ const commit = run(runner, input.cwd, ['commit', '--cleanup=verbatim', '-m', input.message]);
107
107
  const after = inspectGitWorkflow(input.cwd, runner);
108
108
  if (commit.status !== 0)
109
109
  return { status: 'failed', changed: false, snapshot: after.snapshot, operations: [{ name: 'commit', status: 'failed', message: commit.stderr.trim() }], error: { code: 'GIT_COMMIT_FAILED', message: commit.stderr.trim() } };
@@ -57,7 +57,6 @@ export const INTERNAL_HANDLER_ROUTE_SELECTORS = Object.freeze({
57
57
  'task-orchestration': ['status', 'progress'],
58
58
  'task-review': ['preflight', 'finalize-summary'],
59
59
  'task-event': ['event'],
60
- 'task-invalidation': ['reconcile'],
61
60
  'task-lifecycle': ['intent'],
62
61
  'task-finalization': ['complete'],
63
62
  'task-platform-recovery': ['inspect', 'recover'],
@@ -105,8 +104,6 @@ export function internalRouteSelector(command, args) {
105
104
  return args[1] === 'status' ? 'status' : args[1] ? 'progress' : '';
106
105
  if (command === 'task-review')
107
106
  return args[1] ?? '';
108
- if (command === 'task-invalidation')
109
- return args[1] ?? '';
110
107
  if (command === 'task-artifact')
111
108
  return args[1] ?? '';
112
109
  if (command === 'platform-pr-review' && first(args) === 'publish') {
@@ -196,7 +196,7 @@ async function executeManualValidationTransactionLocked(taskRef, values, cwd, re
196
196
  if (transaction.phase === 'prepared') {
197
197
  const staged = await syncPullRequestSummary(taskRef, {
198
198
  cwd, client: options.client, agent, body: pendingBody, changeReportFile: path.resolve(cwd, values.changeReportFile), primaryResult: primaryResult, strict: true,
199
- manualValidation: { phase: 'pending' }, lockAlreadyHeld: true
199
+ manualValidation: { phase: 'pending', transactionId: transaction.transactionId }, lockAlreadyHeld: true
200
200
  });
201
201
  if (!['applied', 'no-op'].includes(staged.status))
202
202
  transactionFailure(staged.error ?? { code: 'MANUAL_VALIDATION_TRANSACTION_FAILED', message: 'pending summary staging failed' });
@@ -261,7 +261,7 @@ async function executeManualValidationTransactionLocked(taskRef, values, cwd, re
261
261
  try {
262
262
  await syncPullRequestSummary(taskRef, {
263
263
  cwd, client: options.client, agent, body: pendingBody, changeReportFile: path.resolve(cwd, values.changeReportFile), primaryResult: primaryResult, strict: true,
264
- manualValidation: { phase: 'pending' }, lockAlreadyHeld: true
264
+ manualValidation: { phase: 'pending', transactionId: transaction.transactionId }, lockAlreadyHeld: true
265
265
  });
266
266
  }
267
267
  catch {
@@ -10,7 +10,7 @@ const USAGE = `Usage: agent-infra-internal platform-pr inspect <task-ref> [--cwd
10
10
  agent-infra-internal platform-pr bind <task-ref> --pr <token> --agent <agent> [--dry-run] [--cwd <path>]
11
11
  agent-infra-internal platform-pr skip <task-ref> --agent <agent> [--dry-run] [--cwd <path>]
12
12
  agent-infra-internal platform-pr sync <task-ref> --agent <agent> [--metadata] [--closing-issue] --result <pr_created|pr_reused|no_op> [--dry-run] [--cwd <path>]
13
- agent-infra-internal platform-pr sync-in-labels --pr <N> [--dry-run] [--cwd <path>]
13
+ agent-infra-internal platform-pr sync-in-labels --pr <token> [--dry-run] [--cwd <path>]
14
14
  agent-infra-internal platform-pr summary-context <task-ref> [--cwd <path>]
15
15
  agent-infra-internal platform-pr change-report <task-ref> --agent <agent> --mechanical-file <path> --precheck-file <path> [--dry-run] [--cwd <path>]
16
16
  agent-infra-internal platform-pr summary-sync <task-ref> --agent <agent> --body-file <path|-> --change-report-file <path> --result <pr_created|pr_reused|no_op> [--manual-phase pending|final ...] [--strict] [--dry-run] [--cwd <path>]
@@ -91,13 +91,12 @@ async function platformPr(args = []) {
91
91
  fail(`sync-in-labels does not accept --${unexpected}`);
92
92
  return;
93
93
  }
94
- const pr = Number(values.pr);
95
- if (!Number.isInteger(pr) || pr <= 0) {
96
- fail('sync-in-labels requires a positive --pr');
94
+ if (typeof values.pr !== 'string' || !values.pr || values.pr.trim() !== values.pr) {
95
+ fail('sync-in-labels requires a non-empty --pr token');
97
96
  return;
98
97
  }
99
98
  const cwd = path.resolve(typeof values.cwd === 'string' ? values.cwd : process.cwd());
100
- finish(await syncPlatformPullRequestInLabels(pr, { cwd, dryRun: values.dryRun === true }));
99
+ finish(await syncPlatformPullRequestInLabels(values.pr, { cwd, dryRun: values.dryRun === true }));
101
100
  return;
102
101
  }
103
102
  const taskRef = args[1];
@@ -4,8 +4,9 @@ import { serveSandboxControl } from '../sandbox/control/server.js';
4
4
  import { runSandboxControlExecutor } from '../sandbox/control/executor.js';
5
5
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.js';
6
6
  import { parseTaskCreateResult, taskCreateExitCode } from '../task/create-service.js';
7
- function writeFinalizationEnvelope(status, accepted, error, changed = false, result = null) {
8
- process.stdout.write(`${JSON.stringify({ version: 1, status, changed, accepted, result, error })}\n`);
7
+ import { serializeTaskFinalizationEnvelope } from '../task/finalization-envelope.js';
8
+ function writeFinalizationEnvelope(status, accepted, error, changed = false, result = null, requestId = null) {
9
+ process.stdout.write(serializeTaskFinalizationEnvelope({ status, changed, accepted, requestId, result, error }));
9
10
  }
10
11
  function finalizationErrorStatus(error) {
11
12
  if (error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN')
@@ -30,6 +31,28 @@ function recoveredExitCode(response) {
30
31
  }
31
32
  return response.exitCode ?? 1;
32
33
  }
34
+ function finalizationResponseOutput(response) {
35
+ try {
36
+ const value = JSON.parse(response.stdout);
37
+ if (value.version === 2
38
+ && value.status
39
+ && ['completed', 'failed', 'blocked', 'unknown'].includes(value.status)
40
+ && (typeof value.changed === 'boolean' || value.changed === null)) {
41
+ return serializeTaskFinalizationEnvelope({
42
+ status: value.status,
43
+ changed: value.changed,
44
+ accepted: true,
45
+ requestId: response.id,
46
+ result: value.result ?? null,
47
+ error: value.error ?? null
48
+ });
49
+ }
50
+ }
51
+ catch {
52
+ // Preserve non-envelope output for diagnostics from the executor.
53
+ }
54
+ return response.stdout;
55
+ }
33
56
  function sandboxFinalizationClient(args) {
34
57
  if (args.length !== 4 || !args[0] || args[1] !== 'complete' || args[2] !== '--agent' || !args[3]) {
35
58
  const error = { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'task ref, complete intent, and --agent are required', retryable: false };
@@ -52,7 +75,8 @@ function sandboxFinalizationClient(args) {
52
75
  catch (error) {
53
76
  if (!(error instanceof SandboxControlClientError))
54
77
  throw error;
55
- writeFinalizationEnvelope(finalizationErrorStatus(error.detail), error.accepted, error.detail);
78
+ const status = finalizationErrorStatus(error.detail);
79
+ writeFinalizationEnvelope(status, error.accepted, error.detail, status === 'unknown' ? null : false, null, error.requestId);
56
80
  writeClientError(error);
57
81
  process.exitCode = error.detail.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? 1 : error.detail.retryable ? 2 : 1;
58
82
  return;
@@ -63,12 +87,13 @@ function sandboxFinalizationClient(args) {
63
87
  message: 'sandbox control rejected the finalization request',
64
88
  retryable: false
65
89
  };
66
- writeFinalizationEnvelope(finalizationErrorStatus(error), error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN', error);
90
+ const status = finalizationErrorStatus(error);
91
+ writeFinalizationEnvelope(status, error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN', error, status === 'unknown' ? null : false, null, response.id);
67
92
  process.stderr.write(`${error.message}\n`);
68
93
  process.exitCode = error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? 1 : error.retryable ? 2 : 1;
69
94
  return;
70
95
  }
71
- process.stdout.write(response.stdout);
96
+ process.stdout.write(finalizationResponseOutput(response));
72
97
  process.stderr.write(response.stderr);
73
98
  process.exitCode = response.exitCode ?? 1;
74
99
  }
@@ -1,6 +1,6 @@
1
1
  import { parseArtifactCommand, executeArtifactCommand } from '../task/artifact-command.js';
2
2
  import { ensureInternalHandlerRoute } from './cli-route-inventory.js';
3
- const USAGE = `Usage: agent-infra-internal task-artifact <N | TASK-id> inspect --family <family> [--source-finding <id> --source-artifact <name> --source-sha256 <digest>]\n agent-infra-internal task-artifact <N | TASK-id> init --family <family> --artifact <artifact> [--locale <zh-CN|en>]\n agent-infra-internal task-artifact <N | TASK-id> preflight|finalize-local --family <analysis|plan|code> --artifact <artifact>\n\nInspect, initialize, preflight, or finalize workflow artifact facts.\n`;
3
+ const USAGE = `Usage: agent-infra-internal task-artifact <N | TASK-id> inspect --family <family> [--reason-code <reason>] [--source-finding <id> --source-artifact <name> --source-sha256 <digest>]\n agent-infra-internal task-artifact <N | TASK-id> init --family <family> --artifact <artifact> [--locale <zh-CN|en>]\n agent-infra-internal task-artifact <N | TASK-id> preflight|finalize-local --family <analysis|plan|code> --artifact <artifact>\n\nInspect, initialize, preflight, or finalize workflow artifact facts.\n`;
4
4
  function taskArtifact(args = []) {
5
5
  if (!ensureInternalHandlerRoute('task-artifact', args))
6
6
  return;
@@ -54,7 +54,21 @@ async function taskCreate(args) {
54
54
  throw new Error('TASK_CREATE_INPUT_INVALID: input must be a regular file');
55
55
  if (stat.size > SANDBOX_CONTROL_MAX_BYTES)
56
56
  throw new Error('TASK_CREATE_INPUT_TOO_LARGE: input exceeds the control limit');
57
- const candidate = validateTaskCreateCandidate(JSON.parse(fs.readFileSync(inputPath, 'utf8')));
57
+ const inputContent = fs.readFileSync(inputPath, 'utf8');
58
+ let candidate;
59
+ try {
60
+ candidate = validateTaskCreateCandidate(JSON.parse(inputContent));
61
+ }
62
+ catch (error) {
63
+ const message = error instanceof Error ? error.message : String(error);
64
+ const code = /^([A-Z][A-Z0-9_]+)/.exec(message)?.[1] ?? 'TASK_CREATE_INPUT_INVALID';
65
+ output(failed(code, message, false, {
66
+ requestId: null,
67
+ accepted: false,
68
+ recovery: 'new-request-id'
69
+ }));
70
+ return;
71
+ }
58
72
  const environment = resolveSandboxControlTransport();
59
73
  if (environment.kind === 'fail-closed') {
60
74
  output(failed(environment.reasonCode ?? 'TASK_CONTROL_TRANSPORT_INVALID', 'sandbox client control configuration is invalid'));
@@ -1,14 +1,13 @@
1
1
  import { createDirectHostExecutionContext, dispatchTaskControlOperation, parseTaskControlOperation } from '../task/control-authority.js';
2
- import { applyTaskFinalization, bindTaskFinalizationReceipt, prepareTaskFinalization } from '../task/finalization.js';
3
- import { publishTaskFinalizationHandoff } from '../task/finalization-handoff.js';
2
+ import { applyTaskFinalization } from '../task/finalization.js';
4
3
  import { detectRepoRoot, resolveTaskRef } from '../task/resolve-ref.js';
5
- import { verifyTaskEvent } from '../task/verification.js';
6
4
  import { resolveSandboxControlTransport } from './task-operation-registry.js';
7
5
  import { requestSandboxTaskFinalization, SandboxControlClientError } from '../sandbox/control/client.js';
6
+ import { serializeTaskFinalizationEnvelope } from '../task/finalization-envelope.js';
8
7
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.js';
9
8
  const USAGE = 'Usage: agent-infra-internal task-finalization <N | TASK-id> complete --agent <agent>\n';
10
- function envelope(status, changed, accepted, result, error) {
11
- return `${JSON.stringify({ version: 1, status, changed, accepted, result, error })}\n`;
9
+ function envelope(status, changed, accepted, result, error, requestId = null) {
10
+ return serializeTaskFinalizationEnvelope({ status, changed, accepted, requestId, result, error });
12
11
  }
13
12
  function exitCode(status) {
14
13
  return status === 'completed' ? 0 : status === 'blocked' ? 2 : 1;
@@ -65,34 +64,24 @@ async function taskFinalization(args = []) {
65
64
  };
66
65
  const transport = resolveSandboxControlTransport(process.env, { localWorkflow: true });
67
66
  if (transport.kind === 'sandbox-local') {
68
- const prepared = await prepareTaskFinalization(boundOperation.request, {
69
- repoRoot,
70
- preflight: (request, options) => verifyTaskEvent({ ...request, event: 'complete-task.hard-preflight' }, options)
71
- });
72
- if (prepared.status !== 'prepared') {
73
- process.stdout.write(envelope(prepared.status, prepared.changed, true, prepared, prepared.error));
74
- process.exitCode = exitCode(prepared.status);
75
- return;
76
- }
77
67
  try {
78
- const response = requestSandboxTaskFinalization({
79
- agent: boundOperation.request.agent,
80
- prepareHandoff: (binding) => {
81
- const receipt = bindTaskFinalizationReceipt(repoRoot, boundOperation.request.taskRef, binding);
82
- return publishTaskFinalizationHandoff('/share/branch', receipt, binding);
83
- }
84
- });
68
+ const response = requestSandboxTaskFinalization({ agent: boundOperation.request.agent });
85
69
  process.stdout.write(response.stdout);
86
70
  process.stderr.write(response.stderr);
87
71
  process.exitCode = response.exitCode ?? 1;
88
72
  return;
89
73
  }
90
74
  catch (error) {
91
- const detail = error instanceof SandboxControlClientError
92
- ? error.detail
75
+ const clientError = error instanceof SandboxControlClientError ? error : null;
76
+ const detail = clientError
77
+ ? clientError.detail
93
78
  : { code: 'SANDBOX_CONTROL_CLIENT_FAILED', message: error instanceof Error ? error.message : String(error), retryable: false };
94
- process.stdout.write(envelope(detail.retryable ? 'blocked' : 'failed', false, false, null, detail));
95
- process.exitCode = detail.retryable ? 2 : 1;
79
+ const status = detail.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN'
80
+ ? 'unknown'
81
+ : detail.retryable ? 'blocked' : 'failed';
82
+ const changed = status === 'unknown' ? null : false;
83
+ process.stdout.write(envelope(status, changed, clientError?.accepted ?? false, null, detail, clientError?.requestId ?? null));
84
+ process.exitCode = exitCode(status);
96
85
  return;
97
86
  }
98
87
  }
@@ -3,7 +3,8 @@ import path from 'node:path';
3
3
  import { accessSandboxTaskView, parseSandboxTaskView, taskViewFromStatus } from '../sandbox/control/task-view.js';
4
4
  import { resolveTaskRef } from '../task/resolve-ref.js';
5
5
  import { PUBLIC_CLI_COMMAND_ALIASES, PUBLIC_CLI_SELECTOR_ALIASES, internalRouteSelector } from './cli-route-inventory.js';
6
- import { readSandboxControlIdentitySentinel } from '../sandbox/control/identity-sentinel.js';
6
+ import { hasSandboxControlMarker, resolveSandboxControlTransport, sandboxTaskMarkerState } from '../sandbox/environment.js';
7
+ export { resolveSandboxControlTransport } from '../sandbox/environment.js';
7
8
  const descriptor = (dispatcher, command, selector, scope, effect, taskRefSource = scope === 'non-task' ? 'none' : 'argv') => ({ dispatcher, command, selector, scope, effect, taskRefSource, guardBeforeImport: true });
8
9
  function internalTaskRoutes() {
9
10
  return [
@@ -102,9 +103,8 @@ function internalTaskRoutes() {
102
103
  descriptor('internal', 'task-review', 'preflight', 'task-bound', 'artifact-write'),
103
104
  descriptor('internal', 'task-review', 'finalize-summary', 'task-bound', 'progress'),
104
105
  descriptor('internal', 'task-event', 'event', 'task-bound', 'progress'),
105
- descriptor('internal', 'task-invalidation', 'reconcile', 'task-bound', 'progress'),
106
106
  descriptor('internal', 'task-lifecycle', 'intent', 'task-bound', 'progress'),
107
- descriptor('internal', 'task-finalization', 'complete', 'task-bound', 'progress'),
107
+ descriptor('internal', 'task-finalization', 'complete', 'task-bound', 'recovery'),
108
108
  descriptor('internal', 'task-platform-recovery', 'inspect', 'task-bound', 'diagnostic'),
109
109
  descriptor('internal', 'task-platform-recovery', 'recover', 'task-bound', 'recovery'),
110
110
  descriptor('internal', 'task-short-id', 'list', 'conditional', 'diagnostic'),
@@ -179,7 +179,7 @@ export const INTERNAL_DISPATCHER_ROUTES = Object.freeze([
179
179
  'git-workflow', 'task-delivery', 'release-workflow', 'platform-release-notes', 'platform-security', 'platform-metadata', 'platform-context',
180
180
  'platform-comment', 'platform-issue', 'platform-pr', 'platform-pr-review', 'pr-review-grade',
181
181
  'platform-checks', 'task-context', 'task-ledger', 'task-warning', 'task-activity', 'task-artifact',
182
- 'task-orchestration', 'task-review', 'task-event', 'task-invalidation', 'task-lifecycle',
182
+ 'task-orchestration', 'task-review', 'task-event', 'task-lifecycle',
183
183
  'task-finalization', 'task-platform-recovery', 'task-short-id', 'task-snapshot', 'task-verify', 'task-validate', 'manual-validation'
184
184
  ]);
185
185
  export const PUBLIC_DISPATCHER_ROUTES = Object.freeze([
@@ -233,141 +233,6 @@ export function resolveDelegatedTaskOperation(args) {
233
233
  return null;
234
234
  return resolveTaskOperation('internal', rest[0], rest.slice(1));
235
235
  }
236
- const TASK_MARKER_KEYS = [
237
- 'AGENT_INFRA_TASK_ID',
238
- 'AGENT_INFRA_CONTROL_TOKEN',
239
- 'AGENT_INFRA_CONTROL_GENERATION',
240
- 'AGENT_INFRA_CONTROL_DIR',
241
- 'AGENT_INFRA_CONTROL_STATUS_DIR',
242
- 'AGENT_INFRA_RUNTIME_DIR'
243
- ];
244
- const TASK_CONTROL_MARKER_KEYS = [
245
- 'AGENT_INFRA_CONTROL_TOKEN',
246
- 'AGENT_INFRA_CONTROL_GENERATION',
247
- 'AGENT_INFRA_CONTROL_DIR',
248
- 'AGENT_INFRA_CONTROL_STATUS_DIR'
249
- ];
250
- const TASK_CONTROL_CONFIG_KEYS = [
251
- 'AGENT_INFRA_CONTROL_TOKEN',
252
- 'AGENT_INFRA_CONTROL_GENERATION',
253
- 'AGENT_INFRA_CONTROL_ROOT_ID',
254
- 'AGENT_INFRA_CONTROL_DIR',
255
- 'AGENT_INFRA_CONTROL_STATUS_DIR'
256
- ];
257
- export const SANDBOX_CONTROL_STATUS_MOUNT = '/run/agent-infra/control-status';
258
- function nativeDirectoryProbe(candidate) {
259
- try {
260
- // The fixed mount is a trust anchor. Do not use a mutable node:fs export:
261
- // a preload must not turn a mounted sandbox into the direct-host path by
262
- // replacing the ordinary filesystem probe.
263
- const binding = process.binding('fs');
264
- if (!Function.prototype.toString.call(binding.internalModuleStat).includes('[native code]'))
265
- return 'unknown';
266
- const result = binding.internalModuleStat(candidate);
267
- if (result === 1)
268
- return 'present';
269
- if (result === -2)
270
- return 'absent';
271
- return 'unknown';
272
- }
273
- catch {
274
- return 'unknown';
275
- }
276
- }
277
- function taskMarkerState(env) {
278
- const present = (key) => Boolean(env[key]);
279
- const taskId = present('AGENT_INFRA_TASK_ID');
280
- const controls = TASK_CONTROL_MARKER_KEYS.every(present);
281
- const runtime = present('AGENT_INFRA_RUNTIME_DIR');
282
- const any = TASK_MARKER_KEYS.some(present);
283
- if (!any)
284
- return 'none';
285
- if (taskId && controls && runtime)
286
- return 'task-bound';
287
- if (!taskId && controls && !runtime)
288
- return 'branch-only';
289
- return 'incomplete';
290
- }
291
- export function resolveSandboxControlTransport(env = process.env, options = {}) {
292
- const hasAnyMarker = TASK_MARKER_KEYS.some((key) => Boolean(env[key]))
293
- || Boolean(env.AGENT_INFRA_CONTROL_CONTROLLER_BINDING)
294
- || Boolean(env.AGENT_INFRA_EXECUTOR_MANIFEST);
295
- const fixedStatusDir = options.statusMountPath
296
- ?? SANDBOX_CONTROL_STATUS_MOUNT;
297
- const fixedStatusProbe = path.isAbsolute(fixedStatusDir)
298
- ? nativeDirectoryProbe(fixedStatusDir) : 'absent';
299
- if (fixedStatusProbe === 'unknown') {
300
- return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_UNAVAILABLE' };
301
- }
302
- const fixedStatusMounted = fixedStatusProbe === 'present';
303
- // A fixed status mount is installed only in a task-bound sandbox. Its
304
- // presence is not mutable by the sandbox process, so marker removal must
305
- // never make that process look like a direct host invocation.
306
- if (!hasAnyMarker) {
307
- return fixedStatusMounted
308
- ? { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' }
309
- : { kind: 'direct-host', reasonCode: null };
310
- }
311
- const hasCompleteConfig = TASK_CONTROL_CONFIG_KEYS.every((key) => Boolean(env[key]));
312
- if (!hasCompleteConfig) {
313
- return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
314
- }
315
- const configuredStatusDir = env.AGENT_INFRA_CONTROL_STATUS_DIR;
316
- const configuredStatusProbe = configuredStatusDir && path.isAbsolute(configuredStatusDir)
317
- ? nativeDirectoryProbe(configuredStatusDir) : 'absent';
318
- if (configuredStatusProbe === 'unknown') {
319
- return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_UNAVAILABLE' };
320
- }
321
- const statusDir = configuredStatusDir && path.isAbsolute(configuredStatusDir)
322
- ? configuredStatusProbe === 'present' ? configuredStatusDir : null
323
- : fixedStatusMounted ? fixedStatusDir : null;
324
- const statusMounted = statusDir !== null;
325
- if (env.AGENT_INFRA_EXECUTOR_MANIFEST || env.AGENT_INFRA_CONTROL_CONTROLLER_BINDING) {
326
- return { kind: 'fail-closed', reasonCode: 'TASK_CONTROL_TRANSPORT_INVALID' };
327
- }
328
- const taskBound = Boolean(env.AGENT_INFRA_TASK_ID);
329
- const runtime = Boolean(env.AGENT_INFRA_RUNTIME_DIR);
330
- if (statusMounted) {
331
- let sentinel;
332
- try {
333
- sentinel = readSandboxControlIdentitySentinel(statusDir);
334
- }
335
- catch (error) {
336
- const message = error instanceof Error ? error.message : '';
337
- return { kind: 'fail-closed', reasonCode: message.endsWith('MISSING')
338
- ? 'SANDBOX_CONTROL_IDENTITY_MISSING'
339
- : 'SANDBOX_CONTROL_IDENTITY_MALFORMED' };
340
- }
341
- if (sentinel.generation !== env.AGENT_INFRA_CONTROL_GENERATION) {
342
- return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_GENERATION_MISMATCH' };
343
- }
344
- if (env.AGENT_INFRA_CONTROL_ROOT_ID && sentinel.controlRootId !== env.AGENT_INFRA_CONTROL_ROOT_ID) {
345
- return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_ROOT_ID_MISMATCH' };
346
- }
347
- if (sentinel.mode === 'task-bound'
348
- && (sentinel.taskId !== env.AGENT_INFRA_TASK_ID || !runtime)) {
349
- return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_TOPOLOGY_MISMATCH' };
350
- }
351
- if (sentinel.mode === 'branch-only' && (taskBound || runtime)) {
352
- return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_TOPOLOGY_MISMATCH' };
353
- }
354
- if (!hasCompleteConfig) {
355
- return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
356
- }
357
- return { kind: options.localWorkflow ? 'sandbox-local' : 'broker-client', reasonCode: null };
358
- }
359
- if (!hasAnyMarker)
360
- return { kind: 'direct-host', reasonCode: null };
361
- if (!hasCompleteConfig || (taskBound && !runtime) || (!taskBound && runtime)) {
362
- return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
363
- }
364
- return options.localWorkflow
365
- ? { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_MISSING' }
366
- : { kind: 'broker-client', reasonCode: null };
367
- }
368
- export function hasTaskBoundMarker(env = process.env) {
369
- return taskMarkerState(env) === 'task-bound';
370
- }
371
236
  function explicitTaskRefForOperation(dispatcher, command, args, selected) {
372
237
  if (selected.taskRefSource === 'none' || selected.taskRefSource === 'environment')
373
238
  return null;
@@ -447,7 +312,7 @@ function isSandboxExecutorRoute(dispatcher, command, args, env) {
447
312
  && first(args) === 'execute'
448
313
  && Boolean(env.AGENT_INFRA_EXECUTOR_MANIFEST)
449
314
  && Boolean(env.AGENT_INFRA_RUNTIME_DIR)
450
- && !TASK_CONTROL_MARKER_KEYS.some((key) => Boolean(env[key]));
315
+ && !hasSandboxControlMarker(env);
451
316
  }
452
317
  function viewFromEnvironment(env) {
453
318
  const statusDir = env.AGENT_INFRA_CONTROL_STATUS_DIR;
@@ -493,7 +358,7 @@ export function guardTaskOperation(dispatcher, command, args = [], options = {})
493
358
  taskView: null
494
359
  };
495
360
  }
496
- const markerState = taskMarkerState(env);
361
+ const markerState = sandboxTaskMarkerState(env);
497
362
  if (markerState === 'incomplete' && !isSandboxExecutorRoute(dispatcher, command, args, env)) {
498
363
  throw new TaskViewOperationError('SANDBOX_TASK_VIEW_MARKER_INVALID', 'sandbox task markers are incomplete; refusing to infer host-direct authority');
499
364
  }
@@ -2,7 +2,7 @@ import { detectRepoRoot, resolveTaskRef } from '../task/resolve-ref.js';
2
2
  import { readPlatformOperationJournal } from '../task/platform-operation-journal.js';
3
3
  import { recoverPlatformOperations } from '../task/platform-operation-recovery.js';
4
4
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.js';
5
- const USAGE = 'Usage: agent-infra-internal task-platform-recovery <N | TASK-id> <inspect | recover> [--agent <agent>] [--selection required|deferred|all]\n';
5
+ const USAGE = 'Usage: agent-infra-internal task-platform-recovery <N | TASK-id> <inspect | recover> [--agent <agent>] [--selection required|deferred|all] [--attempts <positive-integer>]\n';
6
6
  function option(args, name) {
7
7
  const index = args.indexOf(name);
8
8
  return index >= 0 ? args[index + 1] : undefined;
@@ -41,14 +41,19 @@ async function taskPlatformRecovery(args = []) {
41
41
  }
42
42
  const agent = option(args, '--agent');
43
43
  const selection = option(args, '--selection') ?? 'all';
44
+ const attemptsInput = option(args, '--attempts');
45
+ const attempts = attemptsInput === undefined ? undefined : Number(attemptsInput);
46
+ const validAttempts = attemptsInput === undefined
47
+ || (attempts !== undefined && Number.isSafeInteger(attempts) && attempts > 0);
44
48
  if (!agent || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u.test(agent)
45
- || !['required', 'deferred', 'all'].includes(selection)) {
46
- process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'PLATFORM_OPERATION_RECOVERY_PAYLOAD_INVALID', message: 'recover requires a valid --agent and --selection' } })}\n`);
49
+ || !['required', 'deferred', 'all'].includes(selection)
50
+ || !validAttempts) {
51
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'PLATFORM_OPERATION_RECOVERY_PAYLOAD_INVALID', message: 'recover requires a valid --agent, --selection, and positive --attempts when supplied' } })}\n`);
47
52
  process.stderr.write(USAGE);
48
53
  process.exitCode = 1;
49
54
  return;
50
55
  }
51
- const result = await recoverPlatformOperations(task.taskId, selection, { agent, cwd: repoRoot });
56
+ const result = await recoverPlatformOperations(task.taskId, selection, { agent, cwd: repoRoot, ...(attempts === undefined ? {} : { attempts }) });
52
57
  process.stdout.write(`${JSON.stringify(result)}\n`);
53
58
  process.exitCode = result.status === 'applied' || result.status === 'no-op' ? 0 : result.status === 'blocked' ? 2 : 1;
54
59
  }
@@ -8,6 +8,7 @@ import { getOpenWorkflowWarnings } from '../task/workflow-warnings.js';
8
8
  import { applyWorkflowWarningIntent } from '../task/workflow-warning-intents.js';
9
9
  import { captureTaskWriteMetadata } from '../task/write.js';
10
10
  import { syncPlatformComment } from './issue-comments.js';
11
+ import { providerErrorRetryable } from './provider-validation.js';
11
12
  import { platformResult } from './types.js';
12
13
  import { taskIssueIdentity } from './task-identities.js';
13
14
  function result(base, artifacts, warnings) {
@@ -124,7 +125,7 @@ async function backfillCompletionComments(taskRef, options) {
124
125
  if (latest.status === 'failed' || latest.status === 'blocked')
125
126
  return result({ ...latest, operations }, artifacts, []);
126
127
  if (latest.error)
127
- return result(platformResult(latest.error.retryable ? 'blocked' : 'failed', {
128
+ return result(platformResult(latest.error.retryable || providerErrorRetryable(latest.error.code) ? 'blocked' : 'failed', {
128
129
  platform: latest.platform,
129
130
  resource: latest.resource,
130
131
  capabilities: latest.capabilities,
@@ -1,18 +1,20 @@
1
1
  import path from 'node:path';
2
2
  import { loadPlatformProvider } from './provider-loader.js';
3
3
  import { defaultGitRemote, parseGitHubRemote } from './github-provider.js';
4
+ import { providerErrorRetryable } from './provider-validation.js';
4
5
  import { platformResult } from './types.js';
5
6
  function errorStatus(error) {
6
- if (error.retryable || error.code === 'AUTH_REQUIRED' || error.code === 'PLATFORM_DEPENDENCY_MISSING')
7
+ if (error.retryable || providerErrorRetryable(error.code) || error.code === 'AUTH_REQUIRED' || error.code === 'PLATFORM_DEPENDENCY_MISSING')
7
8
  return 'blocked';
8
9
  if (error.code === 'REMOTE_MISSING' || error.code === 'PLATFORM_UNSUPPORTED')
9
10
  return 'no-op';
10
11
  return 'failed';
11
12
  }
12
13
  function contextError(providerType, error, repository = null) {
14
+ const retryable = error.retryable ?? providerErrorRetryable(error.code);
13
15
  return platformResult(errorStatus(error), {
14
16
  platform: { type: providerType || null, repository, currentUser: null },
15
- error: { code: error.code, message: error.message, retryable: error.retryable }
17
+ error: { code: error.code, message: error.message, retryable }
16
18
  });
17
19
  }
18
20
  function contextFromSnapshot(providerType, snapshot) {