@fitlab-ai/agent-infra 0.10.0 → 0.11.0

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 (95) hide show
  1. package/README.md +2 -6
  2. package/README.zh-CN.md +2 -6
  3. package/bin/internal-cli.sh +0 -13
  4. package/dist/bin/internal-cli.js +11 -49
  5. package/dist/lib/internal/cli-route-inventory.js +3 -4
  6. package/dist/lib/internal/platform-comment.js +60 -2
  7. package/dist/lib/internal/task-artifact.js +1 -1
  8. package/dist/lib/internal/task-finalization.js +2 -2
  9. package/dist/lib/internal/task-lifecycle.js +2 -2
  10. package/dist/lib/internal/task-operation-registry.js +22 -12
  11. package/dist/lib/internal/task-orchestration.js +2 -2
  12. package/dist/lib/internal/task-review.js +10 -5
  13. package/dist/lib/platform/activity-recovery.js +159 -0
  14. package/dist/lib/platform/issue-comments.js +84 -32
  15. package/dist/lib/platform/task-comment-projection.js +70 -0
  16. package/dist/lib/platform/task-recovery.js +55 -0
  17. package/dist/lib/platform/verification-sync.js +43 -9
  18. package/dist/lib/sandbox/control/executor.js +4 -2
  19. package/dist/lib/sandbox/control/task-workflow.js +1 -1
  20. package/dist/lib/sandbox/control/workflow-executor.js +83 -21
  21. package/dist/lib/server/process-state.js +17 -5
  22. package/dist/lib/task/artifact-command.js +6 -3
  23. package/dist/lib/task/artifact-operations.js +1 -1
  24. package/dist/lib/task/artifact-recovery.js +114 -68
  25. package/dist/lib/task/artifact-repair-intent.js +25 -8
  26. package/dist/lib/task/control-authority.js +1 -1
  27. package/dist/lib/task/events.js +6 -6
  28. package/dist/lib/task/intermediate-cleanup.js +28 -3
  29. package/dist/lib/task/local-artifact-finalization.js +78 -14
  30. package/dist/lib/task/review-command.js +2 -2
  31. package/dist/lib/task/review-finalization.js +203 -10
  32. package/dist/lib/{host-control → task}/secure-fs.js +8 -19
  33. package/dist/lib/task/workflow-command.js +2 -0
  34. package/dist/lib/task/workflow-dispatch.js +161 -0
  35. package/dist/lifecycle-build-manifest.json +3 -3
  36. package/lib/internal/cli-route-inventory.ts +3 -4
  37. package/lib/internal/platform-comment.ts +53 -2
  38. package/lib/internal/task-artifact.ts +1 -1
  39. package/lib/internal/task-finalization.ts +1 -1
  40. package/lib/internal/task-lifecycle.ts +1 -1
  41. package/lib/internal/task-operation-registry.ts +22 -12
  42. package/lib/internal/task-orchestration.ts +1 -1
  43. package/lib/internal/task-review.ts +9 -5
  44. package/lib/platform/activity-recovery.ts +179 -0
  45. package/lib/platform/issue-comments.ts +98 -31
  46. package/lib/platform/task-comment-projection.ts +79 -0
  47. package/lib/platform/task-recovery.ts +54 -0
  48. package/lib/platform/verification-sync.ts +46 -13
  49. package/lib/sandbox/control/executor.ts +4 -2
  50. package/lib/sandbox/control/protocol.ts +1 -1
  51. package/lib/sandbox/control/task-workflow.ts +1 -1
  52. package/lib/sandbox/control/workflow-executor.ts +102 -21
  53. package/lib/server/process-state.ts +26 -5
  54. package/lib/task/artifact-command.ts +6 -4
  55. package/lib/task/artifact-operations.ts +1 -1
  56. package/lib/task/artifact-recovery.ts +120 -67
  57. package/lib/task/artifact-repair-intent.ts +38 -11
  58. package/lib/task/control-authority.ts +1 -1
  59. package/lib/task/events.ts +6 -6
  60. package/lib/task/intermediate-cleanup.ts +18 -2
  61. package/lib/task/local-artifact-finalization.ts +85 -11
  62. package/lib/task/review-command.ts +1 -1
  63. package/lib/task/review-finalization.ts +203 -8
  64. package/lib/{host-control → task}/secure-fs.ts +10 -32
  65. package/lib/task/workflow-command.ts +2 -0
  66. package/lib/task/workflow-dispatch.ts +171 -0
  67. package/package.json +1 -1
  68. package/templates/.agents/rules/issue-sync.en.md +9 -1
  69. package/templates/.agents/rules/issue-sync.zh-CN.md +10 -3
  70. package/templates/.agents/rules/local-artifact-repair.en.md +7 -6
  71. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +10 -8
  72. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  73. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -2
  74. package/templates/.agents/skills/restore-task/SKILL.en.md +9 -4
  75. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +9 -4
  76. package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
  77. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
  78. package/templates/.agents/skills/review-code/SKILL.en.md +3 -2
  79. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -2
  80. package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
  81. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
  82. package/dist/lib/host-control/audit.js +0 -19
  83. package/dist/lib/host-control/client.js +0 -142
  84. package/dist/lib/host-control/command.js +0 -49
  85. package/dist/lib/host-control/path.js +0 -160
  86. package/dist/lib/host-control/server.js +0 -159
  87. package/dist/lib/host-control/service.js +0 -112
  88. package/dist/lib/internal/host-control.js +0 -28
  89. package/lib/host-control/audit.ts +0 -22
  90. package/lib/host-control/client.ts +0 -172
  91. package/lib/host-control/command.ts +0 -60
  92. package/lib/host-control/path.ts +0 -167
  93. package/lib/host-control/server.ts +0 -180
  94. package/lib/host-control/service.ts +0 -133
  95. package/lib/internal/host-control.ts +0 -28
package/README.md CHANGED
@@ -95,13 +95,9 @@ Every command above works the same way in Claude Code, Codex, Antigravity CLI, a
95
95
 
96
96
  ### Sandbox control boundary
97
97
 
98
- Task-bound sandboxes use a host-only control service for lifecycle and workflow writes. On Linux its fixed endpoint is `/run/user/<uid>/agent-infra/host-control.sock`; on macOS it is `/Users/<login>/Library/Application Support/agent-infra/run/host-control.sock`, resolved from the account identity rather than `HOME` or `TMPDIR`. The service directory is private (`0700`) and the socket is private (`0600`). If the endpoint or sandbox identity is unavailable, task-control commands fail closed.
98
+ Direct-host lifecycle and workflow commands execute in the current CLI process. Task-bound sandboxes route requests to their broker; the broker verifies the request, manifest, generation, owner, lease, and controller before starting a one-request executor. There is no host socket, worker token, or user-level task-control service.
99
99
 
100
- The shell installer creates and starts the user-scoped service through systemd user units on Linux or launchd user agents on macOS. After a manual npm install, run `agent-infra-internal host-control install`, then enable the generated user service; `agent-infra-internal host-control status` reports endpoint health. No root or setuid service is required.
101
-
102
- On macOS, the installer retries service registration during reload. If registration or startup still fails, it exits with an error and a recovery command.
103
-
104
- The task directory exposed inside a task-bound sandbox is a writable projection. Artifact commands accept only typed workflow operations and canonical artifact basenames. The host opens candidates with `O_NOFOLLOW`, reads and hashes them through one file descriptor, checks observable metadata changes, and atomically lands validated bytes into the authoritative task directory. Changes to the projection's protected topology fail closed and never turn the projection into task state.
100
+ The task directory exposed inside a task-bound sandbox is writable only for its bound task. Artifact commands accept typed workflow operations and canonical artifact basenames. The executor opens candidates with `O_NOFOLLOW`, reads and hashes them through one file descriptor, checks observable metadata changes, and atomically lands validated bytes into the authoritative task directory.
105
101
 
106
102
  ## Quick Start
107
103
 
package/README.zh-CN.md CHANGED
@@ -94,13 +94,9 @@ agent-infra 的目标就是把这层共享基础设施标准化。它为所有
94
94
 
95
95
  ### 沙箱控制边界
96
96
 
97
- 任务沙箱的生命周期和工作流写操作由宿主专用控制服务执行。Linux 固定端点为 `/run/user/<uid>/agent-infra/host-control.sock`;macOS 固定端点为 `/Users/<login>/Library/Application Support/agent-infra/run/host-control.sock`,路径根据账户身份解析,不读取 `HOME` 或 `TMPDIR`。服务目录权限为 `0700`,socket 权限为 `0600`。端点或沙箱身份不可用时,task-control 命令会失败关闭。
97
+ direct-host 生命周期和工作流命令在当前 CLI 进程中执行。任务沙箱把请求发送给所属 broker;broker 会先校验请求、manifest、generation、owner、lease 和 controller,再启动一次性 executor。系统不再使用宿主 socket、worker token 或用户级 task-control 服务。
98
98
 
99
- Shell 安装脚本会在 Linux 通过 systemd 用户单元、在 macOS 通过 launchd 用户代理创建并启动该服务。手动 npm 安装后,可运行 `agent-infra-internal host-control install` 创建服务配置,再启用对应的用户服务;`agent-infra-internal host-control status` 可检查端点健康状态。整个服务不需要 root 或 setuid。
100
-
101
- macOS 重载期间,安装脚本会重试服务注册;若最终仍无法注册或启动,则以错误退出并提供恢复命令。
102
-
103
- 任务沙箱内的 task 目录是可写投影。产物命令只接受类型化 workflow 操作和规范的产物 basename。宿主使用 `O_NOFOLLOW` 打开候选文件,通过同一个文件描述符读取并计算摘要,检查可观察的元数据变化,再把已校验字节原子落入权威 task 目录。投影的受保护拓扑发生变化时会失败关闭,投影修改不会变成任务状态。
99
+ 任务沙箱内的 task 目录只允许其绑定任务读写。产物命令只接受类型化 workflow 操作和规范的产物 basename。executor 使用 `O_NOFOLLOW` 打开候选文件,通过同一个文件描述符读取并计算摘要,检查可观察的元数据变化,再把已校验字节原子落入权威 task 目录。
104
100
 
105
101
  ## 快速开始
106
102
 
@@ -26,19 +26,6 @@ else
26
26
  internal_cli="$script_dir/../dist/bin/internal-cli.js"
27
27
  fi
28
28
 
29
- command=${1-}
30
- task_control_command=false
31
- case "$command" in
32
- task-lifecycle|task-orchestration|task-finalization) task_control_command=true ;;
33
- esac
34
-
35
- status_mount_present=false
36
- if [ -d /run/agent-infra/control-status ]; then status_mount_present=true; fi
37
- launch_config=${AGENT_INFRA_TASK_ID-}${AGENT_INFRA_CONTROL_TOKEN-}${AGENT_INFRA_CONTROL_GENERATION-}${AGENT_INFRA_CONTROL_DIR-}${AGENT_INFRA_CONTROL_STATUS_DIR-}${AGENT_INFRA_RUNTIME_DIR-}${AGENT_INFRA_EXECUTOR_MANIFEST-}
38
- if [ "$task_control_command" = true ] && [ "$status_mount_present" = true ] && [ -z "$launch_config" ] && [ -z "${AGENT_INFRA_TEST_HOST_CONTROL_ENDPOINT-}" ]; then
39
- printf '%s\n' '{"status":"failed","changed":false,"error":{"code":"SANDBOX_CONTROL_IDENTITY_MISSING","message":"sandbox control identity is present but its launch configuration is missing"}}'
40
- exit 1
41
- fi
42
29
  # The outer launcher owns the Node startup boundary. A task-bound process must
43
30
  # not pass preload, import, or loader injection into the control router.
44
31
  unset NODE_OPTIONS NODE_PATH
@@ -1,8 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { formatTaskViewDiagnostic, guardTaskOperation, resolveSandboxControlTransport, TaskViewOperationError } from '../lib/internal/task-operation-registry.js';
3
3
  import { INTERNAL_HANDLER_ROUTE_SELECTORS } from '../lib/internal/cli-route-inventory.js';
4
- import { hostControlRequestForCommand, requestHostControl, HostControlClientError, HOST_CONTROL_TEST_ENVIRONMENT_KEYS } from '../lib/host-control/client.js';
5
- import { resolveHostControlEndpoint, readHostControlWorkerToken } from '../lib/host-control/path.js';
6
4
  const [major = 0, minor = 0] = process.versions.node.split('.').map((part) => parseInt(part, 10));
7
5
  if (major < 22 || (major === 22 && minor < 9)) {
8
6
  process.stderr.write(`agent-infra-internal requires Node.js >= 22.9.0 (current: ${process.version})\n`);
@@ -18,9 +16,13 @@ const taskWorkflowCommand = command === 'task-artifact'
18
16
  || command === 'task-invalidation'
19
17
  || command === 'task-warning';
20
18
  const manualValidationWorkflowCommand = command === 'manual-validation';
21
- const localTaskControlHelp = taskControlCommand
22
- && (process.argv[3] === '--help' || process.argv[3] === '-h')
23
- && !process.env.AGENT_INFRA_TEST_HOST_CONTROL_ENDPOINT;
19
+ const taskControlMarkers = [
20
+ 'AGENT_INFRA_TASK_ID', 'AGENT_INFRA_CONTROL_TOKEN', 'AGENT_INFRA_CONTROL_GENERATION',
21
+ 'AGENT_INFRA_CONTROL_DIR', 'AGENT_INFRA_CONTROL_STATUS_DIR', 'AGENT_INFRA_RUNTIME_DIR',
22
+ 'AGENT_INFRA_CONTROL_CONTROLLER_BINDING', 'AGENT_INFRA_EXECUTOR_MANIFEST'
23
+ ];
24
+ const markerlessHelp = ['--help', '-h'].includes(process.argv[3] ?? '')
25
+ && taskControlMarkers.every((key) => !process.env[key]);
24
26
  let taskViewGuardFailed = false;
25
27
  try {
26
28
  const guard = guardTaskOperation('internal', command, process.argv.slice(3));
@@ -45,44 +47,8 @@ function taskControlTransportFailure(message, code = 'TASK_CONTROL_TRANSPORT_INV
45
47
  })}\n`);
46
48
  process.exit(1);
47
49
  }
48
- async function runHostControlCommand(commandName, args) {
49
- let response;
50
- try {
51
- const testEndpoint = process.env.AGENT_INFRA_TEST_HOST_CONTROL_ENDPOINT;
52
- const forwardedEnvironment = testEndpoint
53
- ? Object.fromEntries(HOST_CONTROL_TEST_ENVIRONMENT_KEYS.flatMap((key) => process.env[key] === undefined ? [] : [[key, process.env[key]]]))
54
- : {};
55
- response = await requestHostControl({
56
- request: hostControlRequestForCommand(commandName, args, process.cwd(), forwardedEnvironment),
57
- endpoint: process.env.AGENT_INFRA_TEST_HOST_CONTROL_ENDPOINT ?? resolveHostControlEndpoint()
58
- });
59
- }
60
- catch (error) {
61
- if (error instanceof HostControlClientError) {
62
- taskControlTransportFailure(error.message, error.changed === false
63
- ? 'SANDBOX_CONTROL_HOST_AUTHORITY_UNAVAILABLE' : error.code, error.changed);
64
- }
65
- throw error;
66
- }
67
- process.stdout.write(response.stdout);
68
- process.stderr.write(response.stderr);
69
- process.exitCode = response.exitCode;
70
- }
71
- const hostWorkerRequested = process.env.AGENT_INFRA_HOST_CONTROL_WORKER === '1';
72
- const hostWorker = hostWorkerRequested && (() => {
73
- try {
74
- const endpoint = process.env.AGENT_INFRA_TEST_HOST_CONTROL_ENDPOINT ?? resolveHostControlEndpoint();
75
- return process.env.AGENT_INFRA_HOST_CONTROL_WORKER_TOKEN === readHostControlWorkerToken(endpoint);
76
- }
77
- catch {
78
- return false;
79
- }
80
- })();
81
- if (hostWorkerRequested && !hostWorker && (taskControlCommand || taskWorkflowCommand || manualValidationWorkflowCommand)) {
82
- taskControlTransportFailure('host-control worker authorization is invalid', 'SANDBOX_CONTROL_HOST_AUTHORITY_UNAVAILABLE');
83
- }
84
50
  let controlRouted = false;
85
- if (!taskViewGuardFailed && (taskControlCommand || taskWorkflowCommand || manualValidationWorkflowCommand) && !hostWorker && !localTaskControlHelp) {
51
+ if (!taskViewGuardFailed && !markerlessHelp && (taskControlCommand || taskWorkflowCommand || manualValidationWorkflowCommand)) {
86
52
  const transport = resolveSandboxControlTransport(process.env);
87
53
  switch (transport.kind) {
88
54
  case 'fail-closed': {
@@ -91,7 +57,8 @@ if (!taskViewGuardFailed && (taskControlCommand || taskWorkflowCommand || manual
91
57
  break;
92
58
  }
93
59
  case 'direct-host':
94
- await runHostControlCommand(command, process.argv.slice(3));
60
+ // The host CLI is the direct-host authority. Domain handlers retain their
61
+ // task locks, atomic writes, and operation-specific recovery facts.
95
62
  break;
96
63
  case 'broker-client': {
97
64
  const { sandboxControl } = await import('../lib/internal/sandbox-control.js');
@@ -99,7 +66,7 @@ if (!taskViewGuardFailed && (taskControlCommand || taskWorkflowCommand || manual
99
66
  break;
100
67
  }
101
68
  }
102
- controlRouted = true;
69
+ controlRouted = transport.kind === 'broker-client';
103
70
  }
104
71
  if (!controlRouted && !taskViewGuardFailed && internalRouteRegistered)
105
72
  switch (command) {
@@ -128,11 +95,6 @@ if (!controlRouted && !taskViewGuardFailed && internalRouteRegistered)
128
95
  await sandboxControl(process.argv.slice(3));
129
96
  break;
130
97
  }
131
- case 'host-control': {
132
- const { hostControl } = await import('../lib/internal/host-control.js');
133
- await hostControl(process.argv.slice(3));
134
- break;
135
- }
136
98
  case 'agent-client': {
137
99
  const { agentClient } = await import('../lib/internal/agent-client.js');
138
100
  agentClient(process.argv.slice(3));
@@ -33,7 +33,6 @@ export const INTERNAL_HANDLER_ROUTE_SELECTORS = Object.freeze({
33
33
  'task-create': ['input'],
34
34
  'task-qualification': ['proposal', 'confirm', 'supersede', 'revoke'],
35
35
  'sandbox-control': ['serve', 'execute', 'recover', 'client'],
36
- 'host-control': ['serve', 'status', 'install', 'uninstall'],
37
36
  'agent-client': ['next-steps', 'model-selection'],
38
37
  'codex-lifecycle': ['preflight', 'capability-arm', 'hook-event', 'resolve-start', 'resolve-stop', 'consume'],
39
38
  'codex-sandbox-controller': ['verify-context', 'run'],
@@ -44,7 +43,7 @@ export const INTERNAL_HANDLER_ROUTE_SELECTORS = Object.freeze({
44
43
  'platform-security': ['read', 'dismiss'],
45
44
  'platform-metadata': ['init-labels', 'init-milestones'],
46
45
  'platform-context': ['resolve'],
47
- 'platform-comment': ['list', 'owner', 'backfill', 'sync'],
46
+ 'platform-comment': ['list', 'recover', 'owner', 'backfill', 'sync'],
48
47
  'platform-issue': ['inspect', 'create', 'bind', 'sync'],
49
48
  'platform-pr': ['inspect', 'summary-context', 'resolve-external', 'create', 'bind', 'skip', 'sync', 'change-report', 'summary-sync', 'sync-in-labels'],
50
49
  'platform-pr-review': ['inspect', 'list', 'publish-pr', 'publish-task'],
@@ -54,9 +53,9 @@ export const INTERNAL_HANDLER_ROUTE_SELECTORS = Object.freeze({
54
53
  'task-ledger': ['decision-next-id', 'stage-status', 'finding-upsert', 'finding-respond', 'finding-review', 'decision-upsert', 'rework-intent-upsert'],
55
54
  'task-warning': ['list', 'add', 'set-status'],
56
55
  'task-activity': ['pr-review-inspect', 'pr-review-start', 'pr-review-complete', 'pr-review-terminate'],
57
- 'task-artifact': ['inspect', 'init', 'finalize-local'],
56
+ 'task-artifact': ['inspect', 'init', 'preflight', 'finalize-local'],
58
57
  'task-orchestration': ['status', 'progress'],
59
- 'task-review': ['finalize-summary'],
58
+ 'task-review': ['preflight', 'finalize-summary'],
60
59
  'task-event': ['event'],
61
60
  'task-invalidation': ['reconcile'],
62
61
  'task-override': ['diagnose', 'issue', 'consume'],
@@ -2,9 +2,11 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.js';
4
4
  import { checkPlatformCommentOwner, listPlatformComments, syncPlatformComment } from '../platform/issue-comments.js';
5
+ import { recoverTaskFromComments } from '../platform/task-recovery.js';
5
6
  import { backfillCompletionComments } from '../platform/completion-backfill.js';
6
7
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.js';
7
8
  const USAGE = `Usage: agent-infra-internal platform-comment list --issue <token> [--cwd <path>]
9
+ agent-infra-internal platform-comment recover --issue <token> --task-id <TASK-id> --output <staging-task.md> [--cwd <path>]
8
10
  agent-infra-internal platform-comment owner <task-ref> [--cwd <path>]
9
11
  agent-infra-internal platform-comment backfill <task-ref> --agent <agent> [--cwd <path>]
10
12
  agent-infra-internal platform-comment sync <task-ref> --kind <kind> --agent <agent> [--artifact <file>] [--body-file <path|->] [--status-label <label>] [--backfill] [--cwd <path>]
@@ -26,7 +28,7 @@ function parseFlags(args, start) {
26
28
  const seen = new Set();
27
29
  for (let index = start; index < args.length; index += 1) {
28
30
  const flag = args[index];
29
- if (!['--issue', '--cwd', '--kind', '--agent', '--artifact', '--body-file', '--status-label', '--backfill'].includes(flag)) {
31
+ if (!['--issue', '--task-id', '--output', '--cwd', '--kind', '--agent', '--artifact', '--body-file', '--status-label', '--backfill'].includes(flag)) {
30
32
  return { values, error: `unknown option '${flag}'` };
31
33
  }
32
34
  if (seen.has(flag))
@@ -58,6 +60,7 @@ async function platformComment(args = []) {
58
60
  const operation = args[0];
59
61
  if (!operation || ![
60
62
  internalHandlerRoute('platform-comment', 'list', operation),
63
+ internalHandlerRoute('platform-comment', 'recover', operation),
61
64
  internalHandlerRoute('platform-comment', 'owner', operation),
62
65
  internalHandlerRoute('platform-comment', 'backfill', operation),
63
66
  internalHandlerRoute('platform-comment', 'sync', operation)
@@ -65,7 +68,7 @@ async function platformComment(args = []) {
65
68
  fail('a valid operation is required');
66
69
  return;
67
70
  }
68
- const hasTaskRef = operation !== 'list';
71
+ const hasTaskRef = operation !== 'list' && operation !== 'recover';
69
72
  const taskRef = hasTaskRef ? args[1] : undefined;
70
73
  if (hasTaskRef && (!taskRef || taskRef.startsWith('--'))) {
71
74
  fail(`${operation} requires a task ref`);
@@ -91,6 +94,61 @@ async function platformComment(args = []) {
91
94
  finish(await listPlatformComments(issue, cwd));
92
95
  return;
93
96
  }
97
+ if (internalHandlerRoute('platform-comment', 'recover', operation)) {
98
+ const issue = parsed.values.issue;
99
+ const taskId = parsed.values.taskId;
100
+ const output = parsed.values.output;
101
+ if (typeof issue !== 'string' || !issue || typeof taskId !== 'string' || !/^TASK-\d{8}-\d{6}$/.test(taskId)
102
+ || typeof output !== 'string' || !output) {
103
+ fail('recover requires --issue <token> --task-id <TASK-id> --output <staging-task.md>');
104
+ return;
105
+ }
106
+ const unexpected = Object.keys(parsed.values).find((key) => !['issue', 'taskId', 'output', 'cwd'].includes(key));
107
+ if (unexpected) {
108
+ fail(`recover does not accept '--${unexpected}'`);
109
+ return;
110
+ }
111
+ const outputPath = path.resolve(cwd, output);
112
+ const workspaceDir = path.resolve(cwd, '.agents', 'workspace');
113
+ const stagingDir = path.dirname(outputPath);
114
+ const stagingParent = fs.existsSync(stagingDir) ? path.dirname(fs.realpathSync(stagingDir)) : '';
115
+ const realWorkspace = fs.existsSync(workspaceDir) ? fs.realpathSync(workspaceDir) : '';
116
+ if (path.basename(outputPath) !== 'task.md' || stagingParent !== realWorkspace
117
+ || !path.basename(stagingDir).startsWith('.restore-staging-') || !fs.existsSync(stagingDir)
118
+ || !fs.lstatSync(stagingDir).isDirectory() || fs.existsSync(outputPath)) {
119
+ fail('recover output must be a new task.md inside an existing .agents/workspace/.restore-staging-* directory');
120
+ return;
121
+ }
122
+ const listed = await listPlatformComments(issue, cwd);
123
+ if (listed.status === 'failed' || listed.status === 'blocked' || !listed.comments) {
124
+ finish(listed);
125
+ return;
126
+ }
127
+ try {
128
+ const taskContent = recoverTaskFromComments({ taskId, comments: listed.comments });
129
+ fs.writeFileSync(outputPath, taskContent, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
130
+ finish({
131
+ ...listed,
132
+ status: 'applied',
133
+ changed: true,
134
+ operations: [{ name: `recovery:task:${taskId}`, status: 'applied', reasonCode: null }],
135
+ error: null
136
+ });
137
+ }
138
+ catch (error) {
139
+ finish({
140
+ ...listed,
141
+ status: 'failed',
142
+ changed: false,
143
+ error: {
144
+ code: 'RECOVERY_EVIDENCE_MISSING',
145
+ message: error instanceof Error ? error.message : String(error),
146
+ retryable: false
147
+ }
148
+ });
149
+ }
150
+ return;
151
+ }
94
152
  if (internalHandlerRoute('platform-comment', 'owner', operation)) {
95
153
  const unexpected = Object.keys(parsed.values).find((key) => key !== 'cwd');
96
154
  if (unexpected) {
@@ -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>\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> finalize-local --family <analysis|plan|code> --artifact <artifact> [--recovery-id <id>]\n\nInspect, initialize, or finalize a workflow artifact without changing task state.\n`;
3
+ const USAGE = `Usage: agent-infra-internal task-artifact <N | TASK-id> inspect --family <family>\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> [--recovery-id <id>]\n\nInspect, initialize, preflight, or finalize a workflow artifact without changing task state.\n`;
4
4
  function taskArtifact(args = []) {
5
5
  if (!ensureInternalHandlerRoute('task-artifact', args))
6
6
  return;
@@ -20,12 +20,12 @@ function parseFailure(error) {
20
20
  return message.replace(/^TASK_CONTROL_OPERATION_INVALID: /u, '');
21
21
  }
22
22
  async function taskFinalization(args = []) {
23
- if (!ensureInternalHandlerRoute('task-finalization', args))
24
- return;
25
23
  if (args[0] === '--help' || args[0] === '-h') {
26
24
  process.stdout.write(USAGE);
27
25
  return;
28
26
  }
27
+ if (!ensureInternalHandlerRoute('task-finalization', args))
28
+ return;
29
29
  let operation;
30
30
  try {
31
31
  operation = parseTaskControlOperation('task-finalization', args);
@@ -13,12 +13,12 @@ function parseFailure(error) {
13
13
  return message.replace(/^TASK_CONTROL_OPERATION_INVALID: /u, '');
14
14
  }
15
15
  async function taskLifecycle(args = []) {
16
- if (!ensureInternalHandlerRoute('task-lifecycle', args))
17
- return;
18
16
  if (args[0] === '--help' || args[0] === '-h') {
19
17
  process.stdout.write(USAGE);
20
18
  return;
21
19
  }
20
+ if (!ensureInternalHandlerRoute('task-lifecycle', args))
21
+ return;
22
22
  let operation;
23
23
  try {
24
24
  operation = parseTaskControlOperation('task-lifecycle', args);
@@ -16,10 +16,6 @@ function internalTaskRoutes() {
16
16
  descriptor('internal', 'sandbox-control', 'execute', 'non-task', 'progress', 'none'),
17
17
  descriptor('internal', 'sandbox-control', 'recover', 'conditional', 'recovery', 'delegated'),
18
18
  descriptor('internal', 'sandbox-control', 'client', 'conditional', 'progress', 'delegated'),
19
- descriptor('internal', 'host-control', 'serve', 'non-task', 'progress', 'none'),
20
- descriptor('internal', 'host-control', 'status', 'non-task', 'diagnostic', 'none'),
21
- descriptor('internal', 'host-control', 'install', 'non-task', 'progress', 'none'),
22
- descriptor('internal', 'host-control', 'uninstall', 'non-task', 'progress', 'none'),
23
19
  descriptor('internal', 'agent-client', 'next-steps', 'non-task', 'diagnostic', 'none'),
24
20
  descriptor('internal', 'agent-client', 'model-selection', 'non-task', 'diagnostic', 'none'),
25
21
  descriptor('internal', 'codex-lifecycle', 'preflight', 'non-task', 'diagnostic', 'none'),
@@ -51,6 +47,7 @@ function internalTaskRoutes() {
51
47
  descriptor('internal', 'platform-metadata', 'init-milestones', 'non-task', 'remote-write', 'none'),
52
48
  descriptor('internal', 'platform-context', 'resolve', 'non-task', 'diagnostic', 'none'),
53
49
  descriptor('internal', 'platform-comment', 'list', 'non-task', 'diagnostic', 'none'),
50
+ descriptor('internal', 'platform-comment', 'recover', 'non-task', 'recovery', 'none'),
54
51
  descriptor('internal', 'platform-comment', 'owner', 'task-bound', 'diagnostic'),
55
52
  descriptor('internal', 'platform-comment', 'backfill', 'task-bound', 'remote-write'),
56
53
  descriptor('internal', 'platform-comment', 'sync', 'task-bound', 'remote-write'),
@@ -96,9 +93,11 @@ function internalTaskRoutes() {
96
93
  descriptor('internal', 'task-activity', 'pr-review-terminate', 'task-bound', 'progress'),
97
94
  descriptor('internal', 'task-artifact', 'inspect', 'task-bound', 'diagnostic'),
98
95
  descriptor('internal', 'task-artifact', 'init', 'task-bound', 'artifact-write'),
96
+ descriptor('internal', 'task-artifact', 'preflight', 'task-bound', 'artifact-write'),
99
97
  descriptor('internal', 'task-artifact', 'finalize-local', 'task-bound', 'artifact-write'),
100
98
  descriptor('internal', 'task-orchestration', 'status', 'task-bound', 'diagnostic'),
101
99
  descriptor('internal', 'task-orchestration', 'progress', 'task-bound', 'progress'),
100
+ descriptor('internal', 'task-review', 'preflight', 'task-bound', 'artifact-write'),
102
101
  descriptor('internal', 'task-review', 'finalize-summary', 'task-bound', 'progress'),
103
102
  descriptor('internal', 'task-event', 'event', 'task-bound', 'progress'),
104
103
  descriptor('internal', 'task-invalidation', 'reconcile', 'task-bound', 'progress'),
@@ -175,7 +174,7 @@ export const TASK_OPERATION_DESCRIPTORS = Object.freeze([
175
174
  ...PUBLIC_OPERATION_DESCRIPTORS
176
175
  ]);
177
176
  export const INTERNAL_DISPATCHER_ROUTES = Object.freeze([
178
- 'task-create', 'task-qualification', 'sandbox-control', 'host-control', 'agent-client', 'codex-lifecycle', 'codex-sandbox-controller',
177
+ 'task-create', 'task-qualification', 'sandbox-control', 'agent-client', 'codex-lifecycle', 'codex-sandbox-controller',
179
178
  'git-workflow', 'task-delivery', 'release-workflow', 'platform-release-notes', 'platform-security', 'platform-metadata', 'platform-context',
180
179
  'platform-comment', 'platform-issue', 'platform-pr', 'platform-pr-review', 'pr-review-grade',
181
180
  'platform-checks', 'task-context', 'task-ledger', 'task-warning', 'task-activity', 'task-artifact',
@@ -287,28 +286,39 @@ function taskMarkerState(env) {
287
286
  return 'incomplete';
288
287
  }
289
288
  export function resolveSandboxControlTransport(env = process.env, options = {}) {
290
- const configuredStatusDir = env.AGENT_INFRA_CONTROL_STATUS_DIR;
291
- const fixedStatusDir = options.statusMountPath
292
- ?? SANDBOX_CONTROL_STATUS_MOUNT;
293
289
  const hasAnyMarker = TASK_MARKER_KEYS.some((key) => Boolean(env[key]))
294
290
  || Boolean(env.AGENT_INFRA_CONTROL_CONTROLLER_BINDING)
295
291
  || Boolean(env.AGENT_INFRA_EXECUTOR_MANIFEST);
296
- const fixedStatusProbe = !env.AGENT_INFRA_TEST_HOST_CONTROL_ENDPOINT && path.isAbsolute(fixedStatusDir)
292
+ const fixedStatusDir = options.statusMountPath
293
+ ?? SANDBOX_CONTROL_STATUS_MOUNT;
294
+ const fixedStatusProbe = path.isAbsolute(fixedStatusDir)
297
295
  ? nativeDirectoryProbe(fixedStatusDir) : 'absent';
298
296
  if (fixedStatusProbe === 'unknown') {
299
297
  return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_UNAVAILABLE' };
300
298
  }
301
299
  const fixedStatusMounted = fixedStatusProbe === 'present';
300
+ // A fixed status mount is installed only in a task-bound sandbox. Its
301
+ // presence is not mutable by the sandbox process, so marker removal must
302
+ // never make that process look like a direct host invocation.
303
+ if (!hasAnyMarker) {
304
+ return fixedStatusMounted
305
+ ? { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' }
306
+ : { kind: 'direct-host', reasonCode: null };
307
+ }
308
+ const hasCompleteConfig = TASK_CONTROL_CONFIG_KEYS.every((key) => Boolean(env[key]));
309
+ if (!hasCompleteConfig) {
310
+ return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
311
+ }
312
+ const configuredStatusDir = env.AGENT_INFRA_CONTROL_STATUS_DIR;
302
313
  const configuredStatusProbe = configuredStatusDir && path.isAbsolute(configuredStatusDir)
303
314
  ? nativeDirectoryProbe(configuredStatusDir) : 'absent';
304
315
  if (configuredStatusProbe === 'unknown') {
305
316
  return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_UNAVAILABLE' };
306
317
  }
307
- const statusDir = configuredStatusDir && path.isAbsolute(configuredStatusDir) && configuredStatusProbe === 'present'
308
- ? configuredStatusDir
318
+ const statusDir = configuredStatusDir && path.isAbsolute(configuredStatusDir)
319
+ ? configuredStatusProbe === 'present' ? configuredStatusDir : null
309
320
  : fixedStatusMounted ? fixedStatusDir : null;
310
321
  const statusMounted = statusDir !== null;
311
- const hasCompleteConfig = TASK_CONTROL_CONFIG_KEYS.every((key) => Boolean(env[key]));
312
322
  if (env.AGENT_INFRA_EXECUTOR_MANIFEST || env.AGENT_INFRA_CONTROL_CONTROLLER_BINDING) {
313
323
  return { kind: 'fail-closed', reasonCode: 'TASK_CONTROL_TRANSPORT_INVALID' };
314
324
  }
@@ -16,12 +16,12 @@ function parseFailure(error) {
16
16
  return message.replace(/^TASK_CONTROL_OPERATION_INVALID: /u, '');
17
17
  }
18
18
  async function taskOrchestration(args = []) {
19
- if (!ensureInternalHandlerRoute('task-orchestration', args))
20
- return;
21
19
  if (args[0] === '--help' || args[0] === '-h') {
22
20
  process.stdout.write(USAGE);
23
21
  return;
24
22
  }
23
+ if (!ensureInternalHandlerRoute('task-orchestration', args))
24
+ return;
25
25
  const route = internalHandlerRoute('task-orchestration', 'status', args[1] ?? '')
26
26
  ? 'status'
27
27
  : internalHandlerRoute('task-orchestration', 'progress', args[1] ? 'progress' : '')
@@ -1,15 +1,15 @@
1
1
  import { parseReviewCommand } from '../task/review-command.js';
2
- import { finalizeReviewSummary } from '../task/review-finalization.js';
2
+ import { finalizeReviewSummary, preflightReviewSummary } from '../task/review-finalization.js';
3
3
  import { consumeHumanOverride, failureId, overrideDryRunConflict } from '../task/human-override.js';
4
4
  import { resolveTaskRef } from '../task/resolve-ref.js';
5
5
  import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.js';
6
6
  import { ensureInternalHandlerRoute } from './cli-route-inventory.js';
7
- const USAGE = 'Usage: agent-infra-internal task-review <task-ref> finalize-summary --stage <analysis|plan|code> --artifact <review-*.md> [--recovery-id <id>] [--orchestrated] [--dry-run] [--override-ticket <ticket> --override-target <target> --override-scope <scope>]\n';
7
+ const USAGE = 'Usage: agent-infra-internal task-review <task-ref> <preflight|finalize-summary> --stage <analysis|plan|code> --artifact <review-*.md> [--recovery-id <id>] [--orchestrated] [--dry-run] [--override-ticket <ticket> --override-target <target> --override-scope <scope>]\n';
8
8
  function failUsage(message) {
9
9
  process.stdout.write(`${JSON.stringify({
10
10
  status: 'failed',
11
11
  changed: false,
12
- intent: 'finalize-summary',
12
+ intent: 'review-operation',
13
13
  error: { code: 'REVIEW_PAYLOAD_INVALID', message }
14
14
  })}\n`);
15
15
  process.stderr.write(USAGE);
@@ -31,6 +31,7 @@ async function taskReview(args = []) {
31
31
  return;
32
32
  }
33
33
  const { stage, artifact, recoveryId, dryRun, orchestrated, overrideTicket, overrideTarget, overrideScope } = request;
34
+ const operation = args[1];
34
35
  const dryRunConflict = overrideDryRunConflict({ dryRun, overrideTicket, overrideTarget, overrideScope });
35
36
  if (dryRunConflict) {
36
37
  failUsage(dryRunConflict.message);
@@ -44,8 +45,12 @@ async function taskReview(args = []) {
44
45
  let result;
45
46
  let humanOverride = null;
46
47
  try {
47
- result = await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'task-review.finalize-summary', async () => {
48
- let current = finalizeReviewSummary({ taskRef: args[0], stage, artifact, recoveryId, orchestrated, dryRun }, { lockAlreadyHeld: true });
48
+ result = await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-review.${operation}`, async () => {
49
+ let current = operation === 'preflight'
50
+ ? preflightReviewSummary({ taskRef: args[0], stage, artifact, recoveryId, orchestrated, dryRun }, { lockAlreadyHeld: true })
51
+ : finalizeReviewSummary({ taskRef: args[0], stage, artifact, recoveryId, orchestrated, dryRun }, { lockAlreadyHeld: true });
52
+ if (operation === 'preflight')
53
+ return current;
49
54
  if (current.status !== 'failed' || !overrideTicket)
50
55
  return current;
51
56
  if (!overrideTarget || !overrideScope) {