@fitlab-ai/agent-infra 0.11.12 → 0.11.14

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 (120) hide show
  1. package/dist/bin/internal-cli.js +2 -7
  2. package/dist/lib/agent-clients/adapter.js +0 -3
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +5 -9
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/cli.js +1 -5
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +1 -5
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +11 -25
  7. package/dist/lib/agent-clients/adapters/codex-orchestration.js +1 -3
  8. package/dist/lib/agent-clients/adapters/codex.js +0 -2
  9. package/dist/lib/internal/cli-route-inventory.js +0 -1
  10. package/dist/lib/internal/sandbox-control.js +1 -81
  11. package/dist/lib/internal/task-finalization.js +29 -20
  12. package/dist/lib/internal/task-lifecycle.js +4 -2
  13. package/dist/lib/internal/task-operation-registry.js +1 -3
  14. package/dist/lib/internal/task-validate.js +14 -124
  15. package/dist/lib/platform/github-client.js +33 -3
  16. package/dist/lib/platform/release-notes.js +2 -2
  17. package/dist/lib/sandbox/commands/control-status.js +0 -1
  18. package/dist/lib/sandbox/commands/create.js +11 -6
  19. package/dist/lib/sandbox/commands/ls.js +4 -5
  20. package/dist/lib/sandbox/commands/rm.js +1 -1
  21. package/dist/lib/sandbox/config.js +12 -0
  22. package/dist/lib/sandbox/control/audit.js +1 -2
  23. package/dist/lib/sandbox/control/client.js +15 -110
  24. package/dist/lib/sandbox/control/executor.js +9 -59
  25. package/dist/lib/sandbox/control/host-recovery.js +89 -27
  26. package/dist/lib/sandbox/control/lifecycle.js +75 -9
  27. package/dist/lib/sandbox/control/protocol.js +11 -50
  28. package/dist/lib/sandbox/control/server.js +203 -253
  29. package/dist/lib/sandbox/control/task-view.js +0 -13
  30. package/dist/lib/sandbox/environment.js +7 -8
  31. package/dist/lib/sandbox/mounts.js +71 -52
  32. package/dist/lib/sandbox/recovery.js +30 -12
  33. package/dist/lib/sandbox/removal.js +37 -3
  34. package/dist/lib/sandbox/workspace-identity.js +18 -6
  35. package/dist/lib/sandbox/workspace-view.js +89 -6
  36. package/dist/lib/task/commands/ls.js +3 -3
  37. package/dist/lib/task/control-authority.js +41 -17
  38. package/dist/lib/task/control-recovery.js +2 -4
  39. package/dist/lib/task/finalization.js +27 -2
  40. package/dist/lib/task/lifecycle.js +294 -63
  41. package/dist/lib/task/orchestration.js +26 -72
  42. package/dist/lib/task/short-id.js +37 -27
  43. package/dist/lib/task/verification-engine.js +4 -11
  44. package/dist/lifecycle-build-manifest.json +3 -3
  45. package/lib/agent-clients/adapter.ts +0 -27
  46. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +6 -12
  47. package/lib/agent-clients/adapters/codex-lifecycle/cli.ts +1 -5
  48. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +0 -7
  49. package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +1 -5
  50. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +21 -27
  51. package/lib/agent-clients/adapters/codex-orchestration.ts +1 -3
  52. package/lib/agent-clients/adapters/codex.ts +0 -2
  53. package/lib/internal/cli-route-inventory.ts +0 -1
  54. package/lib/internal/sandbox-control.ts +0 -97
  55. package/lib/internal/task-finalization.ts +33 -19
  56. package/lib/internal/task-lifecycle.ts +4 -2
  57. package/lib/internal/task-operation-registry.ts +1 -3
  58. package/lib/internal/task-validate.ts +13 -127
  59. package/lib/platform/github-client.ts +34 -3
  60. package/lib/platform/release-notes.ts +2 -2
  61. package/lib/sandbox/commands/control-status.ts +0 -1
  62. package/lib/sandbox/commands/create.ts +11 -5
  63. package/lib/sandbox/commands/ls.ts +4 -5
  64. package/lib/sandbox/commands/rm.ts +1 -1
  65. package/lib/sandbox/config.ts +13 -0
  66. package/lib/sandbox/control/audit.ts +1 -2
  67. package/lib/sandbox/control/client.ts +17 -138
  68. package/lib/sandbox/control/executor.ts +8 -65
  69. package/lib/sandbox/control/host-recovery.ts +94 -21
  70. package/lib/sandbox/control/lifecycle.ts +75 -10
  71. package/lib/sandbox/control/protocol.ts +12 -62
  72. package/lib/sandbox/control/server.ts +11 -61
  73. package/lib/sandbox/control/task-view.ts +0 -19
  74. package/lib/sandbox/environment.ts +7 -8
  75. package/lib/sandbox/mounts.ts +93 -68
  76. package/lib/sandbox/recovery.ts +37 -19
  77. package/lib/sandbox/removal.ts +36 -5
  78. package/lib/sandbox/workspace-identity.ts +16 -6
  79. package/lib/sandbox/workspace-view.ts +81 -7
  80. package/lib/task/commands/ls.ts +3 -3
  81. package/lib/task/control-authority.ts +41 -26
  82. package/lib/task/control-recovery.ts +3 -5
  83. package/lib/task/finalization.ts +29 -2
  84. package/lib/task/lifecycle.ts +282 -57
  85. package/lib/task/orchestration.ts +30 -68
  86. package/lib/task/short-id.ts +33 -40
  87. package/lib/task/verification-engine.ts +3 -9
  88. package/package.json +1 -1
  89. package/templates/.agents/rules/README.en.md +1 -0
  90. package/templates/.agents/rules/README.zh-CN.md +1 -0
  91. package/templates/.agents/rules/lifecycle-orchestration.en.md +3 -4
  92. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +3 -4
  93. package/templates/.agents/rules/strategy-pattern.en.md +25 -0
  94. package/templates/.agents/rules/strategy-pattern.zh-CN.md +25 -0
  95. package/templates/.agents/rules/task-short-id.en.md +18 -12
  96. package/templates/.agents/rules/task-short-id.zh-CN.md +12 -7
  97. package/templates/.agents/skills/code-task/reference/code-rules.en.md +4 -0
  98. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +4 -0
  99. package/templates/.agents/skills/complete-task/SKILL.en.md +5 -26
  100. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +5 -23
  101. package/templates/.agents/skills/create-release-note/SKILL.en.md +3 -1
  102. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +3 -1
  103. package/templates/.agents/skills/plan-task/SKILL.en.md +7 -4
  104. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +7 -4
  105. package/templates/.agents/skills/run-task/SKILL.en.md +2 -2
  106. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +2 -2
  107. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-client.js +0 -130
  108. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +0 -127
  109. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-operation.js +0 -131
  110. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-registration.js +0 -275
  111. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.js +0 -106
  112. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +0 -345
  113. package/dist/lib/sandbox/control/agent-operation.js +0 -34
  114. package/lib/agent-clients/adapters/codex-lifecycle/controller-client.ts +0 -158
  115. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +0 -163
  116. package/lib/agent-clients/adapters/codex-lifecycle/controller-operation.ts +0 -151
  117. package/lib/agent-clients/adapters/codex-lifecycle/controller-registration.ts +0 -352
  118. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.ts +0 -109
  119. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +0 -437
  120. package/lib/sandbox/control/agent-operation.ts +0 -49
@@ -1,33 +1,12 @@
1
1
  import {
2
2
  recoverSandboxControl,
3
3
  requestSandboxControl,
4
- requestSandboxTaskFinalization,
5
4
  SandboxControlClientError
6
5
  } from '../sandbox/control/client.ts';
7
- import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.ts';
8
6
  import { serveSandboxControl } from '../sandbox/control/server.ts';
9
7
  import { runSandboxControlExecutor } from '../sandbox/control/executor.ts';
10
8
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
11
9
  import { parseTaskCreateResult, taskCreateExitCode } from '../task/create-service.ts';
12
- import { serializeTaskFinalizationEnvelope, type TaskFinalizationEnvelope } from '../task/finalization-envelope.ts';
13
-
14
- type FinalizationStatus = 'completed' | 'failed' | 'blocked' | 'unknown';
15
-
16
- function writeFinalizationEnvelope(
17
- status: FinalizationStatus,
18
- accepted: boolean,
19
- error: { code: string; message: string; retryable: boolean },
20
- changed: boolean | null = false,
21
- result: unknown = null,
22
- requestId: string | null = null
23
- ): void {
24
- process.stdout.write(serializeTaskFinalizationEnvelope({ status, changed, accepted, requestId, result, error }));
25
- }
26
-
27
- function finalizationErrorStatus(error: { retryable: boolean; code: string }): FinalizationStatus {
28
- if (error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN') return 'unknown';
29
- return error.retryable ? 'blocked' : 'failed';
30
- }
31
10
 
32
11
  function writeClientError(error: SandboxControlClientError): void {
33
12
  process.stderr.write(`${error.detail.message}\n`);
@@ -45,78 +24,6 @@ function recoveredExitCode(response: Awaited<ReturnType<typeof recoverSandboxCon
45
24
  return response.exitCode ?? 1;
46
25
  }
47
26
 
48
- function finalizationResponseOutput(response: Awaited<ReturnType<typeof requestSandboxTaskFinalization>>): string {
49
- try {
50
- const value = JSON.parse(response.stdout) as Partial<TaskFinalizationEnvelope>;
51
- if (value.version === 2
52
- && value.status
53
- && ['completed', 'failed', 'blocked', 'unknown'].includes(value.status)
54
- && (typeof value.changed === 'boolean' || value.changed === null)) {
55
- return serializeTaskFinalizationEnvelope({
56
- status: value.status,
57
- changed: value.changed,
58
- accepted: true,
59
- requestId: response.id,
60
- result: value.result ?? null,
61
- error: value.error ?? null
62
- });
63
- }
64
- } catch {
65
- // Preserve non-envelope output for diagnostics from the executor.
66
- }
67
- return response.stdout;
68
- }
69
-
70
- function sandboxFinalizationClient(args: string[]): void {
71
- if (args.length !== 4 || !args[0] || args[1] !== 'complete' || args[2] !== '--agent' || !args[3]) {
72
- const error = { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'task ref, complete intent, and --agent are required', retryable: false };
73
- writeFinalizationEnvelope('failed', false, error);
74
- process.stderr.write('Usage: agent-infra-internal task-finalization <N | TASK-id> complete --agent <agent>\n');
75
- process.exitCode = 1;
76
- return;
77
- }
78
- const agent = normalizeAgentToken(args[3]);
79
- if (!agent) {
80
- const error = { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: `invalid --agent: ${AGENT_USAGE_HINT}`, retryable: false };
81
- writeFinalizationEnvelope('failed', false, error);
82
- process.exitCode = 1;
83
- return;
84
- }
85
- let response;
86
- try {
87
- response = requestSandboxTaskFinalization({ agent });
88
- } catch (error) {
89
- if (!(error instanceof SandboxControlClientError)) throw error;
90
- const status = finalizationErrorStatus(error.detail);
91
- writeFinalizationEnvelope(status, error.accepted, error.detail, status === 'unknown' ? null : false, null, error.requestId);
92
- writeClientError(error);
93
- process.exitCode = error.detail.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? 1 : error.detail.retryable ? 2 : 1;
94
- return;
95
- }
96
- if (response.phase === 'rejected') {
97
- const error = response.error ?? {
98
- code: 'SANDBOX_CONTROL_REJECTED',
99
- message: 'sandbox control rejected the finalization request',
100
- retryable: false
101
- };
102
- const status = finalizationErrorStatus(error);
103
- writeFinalizationEnvelope(
104
- status,
105
- error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN',
106
- error,
107
- status === 'unknown' ? null : false,
108
- null,
109
- response.id
110
- );
111
- process.stderr.write(`${error.message}\n`);
112
- process.exitCode = error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? 1 : error.retryable ? 2 : 1;
113
- return;
114
- }
115
- process.stdout.write(finalizationResponseOutput(response));
116
- process.stderr.write(response.stderr);
117
- process.exitCode = response.exitCode ?? 1;
118
- }
119
-
120
27
  async function sandboxControl(args: string[]): Promise<void> {
121
28
  if (!ensureInternalHandlerRoute('sandbox-control', args)) return;
122
29
  const [operation, ...rest] = args;
@@ -165,10 +72,6 @@ async function sandboxControl(args: string[]): Promise<void> {
165
72
  }
166
73
  if (internalHandlerRoute('sandbox-control', 'client', operation ?? '')) {
167
74
  const [family = '', ...commandArgs] = rest;
168
- if (family === 'task-finalization') {
169
- sandboxFinalizationClient(commandArgs);
170
- return;
171
- }
172
75
  let response;
173
76
  try {
174
77
  response = requestSandboxControl({ family, args: commandArgs });
@@ -1,3 +1,4 @@
1
+ import path from 'node:path';
1
2
  import {
2
3
  createDirectHostExecutionContext,
3
4
  dispatchTaskControlOperation,
@@ -6,8 +7,10 @@ import {
6
7
  import { applyTaskFinalization } from '../task/finalization.ts';
7
8
  import { detectRepoRoot, resolveTaskRef } from '../task/resolve-ref.ts';
8
9
  import { resolveSandboxControlTransport } from './task-operation-registry.ts';
9
- import { requestSandboxTaskFinalization, SandboxControlClientError } from '../sandbox/control/client.ts';
10
+ import { quiesceSandboxControlForTaskCompletion } from '../sandbox/control/lifecycle.ts';
11
+ import { loadSandboxControlLocation } from '../sandbox/config.ts';
10
12
  import { serializeTaskFinalizationEnvelope } from '../task/finalization-envelope.ts';
13
+ import { verifyTaskEvent } from '../task/verification.ts';
11
14
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
12
15
 
13
16
  const USAGE = 'Usage: agent-infra-internal task-finalization <N | TASK-id> complete --agent <agent>\n';
@@ -79,28 +82,39 @@ async function taskFinalization(args: string[] = []): Promise<void> {
79
82
  };
80
83
  const transport = resolveSandboxControlTransport(process.env, { localWorkflow: true });
81
84
  if (transport.kind === 'sandbox-local') {
82
- try {
83
- const response = requestSandboxTaskFinalization({ agent: boundOperation.request.agent });
84
- process.stdout.write(response.stdout);
85
- process.stderr.write(response.stderr);
86
- process.exitCode = response.exitCode ?? 1;
87
- return;
88
- } catch (error) {
89
- const clientError = error instanceof SandboxControlClientError ? error : null;
90
- const detail = clientError
91
- ? clientError.detail
92
- : { code: 'SANDBOX_CONTROL_CLIENT_FAILED', message: error instanceof Error ? error.message : String(error), retryable: false };
93
- const status = detail.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN'
94
- ? 'unknown'
95
- : detail.retryable ? 'blocked' : 'failed';
96
- const changed = status === 'unknown' ? null : false;
97
- process.stdout.write(envelope(status, changed, clientError?.accepted ?? false, null, detail, clientError?.requestId ?? null));
98
- process.exitCode = exitCode(status);
85
+ const statusDir = process.env.AGENT_INFRA_CONTROL_STATUS_DIR;
86
+ const taskId = process.env.AGENT_INFRA_TASK_ID;
87
+ const generation = process.env.AGENT_INFRA_CONTROL_GENERATION;
88
+ const controlRootId = process.env.AGENT_INFRA_CONTROL_ROOT_ID;
89
+ const controlRoot = process.env.AGENT_INFRA_CONTROL_ROOT_DIR;
90
+ if (!statusDir || !taskId || !generation || !controlRootId || !controlRoot || taskId !== resolved.taskId) {
91
+ const detail = { code: 'TASK_CONTROL_TRANSPORT_INVALID', message: 'sandbox task control identity is incomplete', retryable: false };
92
+ process.stdout.write(envelope('failed', false, true, null, detail));
93
+ process.exitCode = 1;
99
94
  return;
100
95
  }
96
+ const result = await applyTaskFinalization(boundOperation.request, {
97
+ repoRoot,
98
+ beforeLifecycle: async (currentTaskId) => {
99
+ if (currentTaskId !== taskId) throw new Error('SANDBOX_CONTROL_IDENTITY_MISMATCH');
100
+ await quiesceSandboxControlForTaskCompletion(
101
+ controlRoot,
102
+ { taskId, generation, controlRootId, statusDir }
103
+ );
104
+ },
105
+ preflight: (request, options) => verifyTaskEvent(
106
+ { ...request, event: 'complete-task.hard-preflight' }, options
107
+ )
108
+ });
109
+ const status = result.status === 'prepared' ? 'failed' : result.status;
110
+ process.stdout.write(envelope(status, result.changed, true, result, result.error
111
+ ?? (result.status === 'prepared' ? { code: 'TASK_FINALIZATION_PREPARATION_INCOMPLETE', message: 'finalization did not commit', retryable: true } : null)));
112
+ process.exitCode = exitCode(status);
113
+ return;
101
114
  }
115
+ const controlLocation = loadSandboxControlLocation(repoRoot);
102
116
  const result = await dispatchTaskControlOperation(
103
- createDirectHostExecutionContext({ repoRoot }),
117
+ createDirectHostExecutionContext({ repoRoot, ...(controlLocation ?? {}) }),
104
118
  boundOperation
105
119
  );
106
120
  if (result.status === 'prepared') {
@@ -9,8 +9,9 @@ import { detectRepoRoot, resolveTaskRef } from '../task/resolve-ref.ts';
9
9
  import type { TaskLifecycleResult } from '../task/lifecycle.ts';
10
10
  import type { AgentClientLifecycleRecoveryResult as LifecycleRecoveryResult } from '../agent-clients/adapter.ts';
11
11
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
12
+ import { loadSandboxControlLocation } from '../sandbox/config.ts';
12
13
 
13
- const USAGE = `Usage: agent-infra-internal task-lifecycle <N | TASK-id> <intent> --agent <agent> [intent flags] [--dry-run]\n\nIntents: ${lifecycleIntentCatalog.join(', ')}\nrecover-started: --auto\n`;
14
+ const USAGE = `Usage: agent-infra-internal task-lifecycle <N | TASK-id> <intent> --agent <agent> [intent flags] [--dry-run]\n\nIntents: ${lifecycleIntentCatalog.filter((intent) => intent !== 'complete').join(', ')}\nrecover-started: --auto\n`;
14
15
 
15
16
  function usageFailure(message: string): void {
16
17
  process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'LIFECYCLE_PAYLOAD_INVALID', message } })}\n`);
@@ -59,8 +60,9 @@ async function taskLifecycle(args: string[] = []): Promise<void> {
59
60
  repoRoot = resolved.repoRoot;
60
61
  }
61
62
 
63
+ const controlLocation = loadSandboxControlLocation(repoRoot);
62
64
  const result = await dispatchTaskControlOperation(
63
- createDirectHostExecutionContext({ repoRoot }),
65
+ createDirectHostExecutionContext({ repoRoot, ...(controlLocation ?? {}) }),
64
66
  operation
65
67
  ) as TaskLifecycleResult | LifecycleRecoveryResult;
66
68
  process.stdout.write(`${JSON.stringify(result)}\n`);
@@ -60,8 +60,6 @@ function internalTaskRoutes(): TaskOperationDescriptor[] {
60
60
  descriptor('internal', 'codex-lifecycle', 'resolve-start', 'conditional', 'progress'),
61
61
  descriptor('internal', 'codex-lifecycle', 'resolve-stop', 'conditional', 'progress'),
62
62
  descriptor('internal', 'codex-lifecycle', 'consume', 'conditional', 'progress'),
63
- descriptor('internal', 'codex-sandbox-controller', 'verify-context', 'task-bound', 'diagnostic'),
64
- descriptor('internal', 'codex-sandbox-controller', 'run', 'task-bound', 'progress'),
65
63
  descriptor('internal', 'git-workflow', 'inspect', 'conditional', 'diagnostic'),
66
64
  descriptor('internal', 'git-workflow', 'preview-tree', 'conditional', 'diagnostic'),
67
65
  descriptor('internal', 'git-workflow', 'snapshot', 'conditional', 'diagnostic'),
@@ -213,7 +211,7 @@ export const TASK_OPERATION_DESCRIPTORS = Object.freeze([
213
211
  ]);
214
212
 
215
213
  export const INTERNAL_DISPATCHER_ROUTES = Object.freeze([
216
- 'task-create', 'task-qualification', 'sandbox-control', 'agent-client', 'codex-lifecycle', 'codex-sandbox-controller',
214
+ 'task-create', 'task-qualification', 'sandbox-control', 'agent-client', 'codex-lifecycle',
217
215
  'git-workflow', 'task-delivery', 'release-workflow', 'platform-release-notes', 'platform-security', 'platform-metadata', 'platform-context',
218
216
  'platform-comment', 'platform-issue', 'platform-pr', 'platform-pr-review', 'pr-review-grade',
219
217
  'platform-checks', 'task-context', 'task-ledger', 'task-warning', 'task-activity', 'task-artifact',
@@ -2,27 +2,9 @@ import { execFileSync, spawnSync } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
- import { randomUUID } from 'node:crypto';
6
5
  import { loadConfig } from '../sandbox/config.ts';
7
- import { detectEngine } from '../sandbox/engine.ts';
8
- import { runEngine } from '../sandbox/shell.ts';
9
- import { sandboxBranchLabel, sandboxLabel } from '../sandbox/constants.ts';
10
- import { fetchSandboxRows, selectSandboxContainer } from '../sandbox/commands/list-running.ts';
11
- import { containerNameCandidates } from '../sandbox/constants.ts';
12
6
  import { resolveSandboxTarget } from '../sandbox/workspace-identity.ts';
13
- import { sandboxControlPaths } from '../sandbox/workspace-view.ts';
14
- import {
15
- appendSandboxControlAudit,
16
- readActiveLease,
17
- readSandboxControlStatus
18
- } from '../sandbox/control/state.ts';
19
- import type { SandboxControlLease } from '../sandbox/control/protocol.ts';
20
- import { readSandboxControlManifest } from '../sandbox/control/lifecycle.ts';
21
- import {
22
- SANDBOX_CONTROL_FUTURE_SKEW_MS,
23
- SANDBOX_CONTROL_STATUS_STALE_MS
24
- } from '../sandbox/control/protocol.ts';
25
- import { getProcessStartTime } from '../server/process-state.ts';
7
+ import { resolveBranchWorktree } from '../git/branch-worktree.ts';
26
8
  import { assertGitWorktreeBinding } from '../git/worktree-identity.ts';
27
9
  import { createManualValidationEvidence, manualValidationEvidenceSummary } from '../task/manual-validation-evidence.ts';
28
10
  import type { ManualValidationCleanup } from '../task/manual-validation-evidence.ts';
@@ -31,7 +13,6 @@ import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-in
31
13
 
32
14
  const USAGE = `Usage: agent-infra-internal task-validate <branch | TASK-id | N> [--scope snapshot|inplace] [--timeout <ms>] [--format text|json] [--evidence-file <path>] -- <command> [args...]`;
33
15
  const MAX_TIMEOUT_MS = 60 * 60 * 1000;
34
- const RECOVERY_GRACE_MS = 30_000;
35
16
 
36
17
  type ValidateOptions = {
37
18
  target: string;
@@ -108,20 +89,6 @@ function git(root: string, args: string[]): string {
108
89
  return execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
109
90
  }
110
91
 
111
- function waitForState(statusDir: string, predicate: (state: ReturnType<typeof readSandboxControlStatus>) => boolean, timeoutMs = 5_000): void {
112
- const deadline = Date.now() + timeoutMs;
113
- while (Date.now() < deadline) {
114
- try {
115
- const status = readSandboxControlStatus(statusDir);
116
- if (predicate(status)) return;
117
- } catch {
118
- // Atomic status publication may be between renames.
119
- }
120
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
121
- }
122
- throw new Error('SANDBOX_VALIDATION_BROKER_STATE_TIMEOUT');
123
- }
124
-
125
92
  function snapshotValidation(repoRoot: string, commit: string, options: ValidateOptions) {
126
93
  const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-infra-validation-'));
127
94
  const worktree = path.join(tempRoot, 'worktree');
@@ -150,109 +117,28 @@ function snapshotValidation(repoRoot: string, commit: string, options: ValidateO
150
117
  }
151
118
 
152
119
  function inplaceValidation(config: ReturnType<typeof loadConfig>, target: ReturnType<typeof resolveSandboxTarget>, options: ValidateOptions) {
153
- const engine = detectEngine(config);
154
- const { running, nonRunning } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
155
- const row = selectSandboxContainer([...running, ...nonRunning], containerNameCandidates(config, target.branch));
156
- if (!row) throw new Error('SANDBOX_VALIDATION_CONTAINER_NOT_FOUND');
157
- const control = sandboxControlPaths({
158
- base: config.controlBase, repoRoot: config.repoRoot, project: config.project,
159
- container: row.name, identity: target.workspace
160
- });
161
- const manifest = readSandboxControlManifest(control.manifestPath);
162
- const initial = readSandboxControlStatus(control.statusDir);
163
- const now = Date.now();
164
- if (initial.generation !== manifest.generation
165
- || now - initial.updatedAt > SANDBOX_CONTROL_STATUS_STALE_MS
166
- || initial.updatedAt > now + SANDBOX_CONTROL_FUTURE_SKEW_MS) {
167
- throw new Error('SANDBOX_VALIDATION_BROKER_STATUS_STALE');
168
- }
169
- if (initial.state !== 'healthy' || initial.activeRequestId !== null) {
170
- throw new Error('SANDBOX_VALIDATION_BROKER_NOT_IDLE');
171
- }
172
- const startTime = getProcessStartTime(process.pid);
173
- if (!startTime) throw new Error('SANDBOX_VALIDATION_OWNER_IDENTITY_UNAVAILABLE');
174
- const leasePath = path.join(control.root, 'lease.json');
175
- const nonce = randomUUID();
176
- const issuedAt = Date.now();
177
- const lease: SandboxControlLease = {
178
- version: 2, generation: manifest.generation, nonce,
179
- owner: { pid: process.pid, startTime }, issuedAt,
180
- expiresAt: issuedAt + Math.min(options.timeoutMs + RECOVERY_GRACE_MS, MAX_TIMEOUT_MS + RECOVERY_GRACE_MS),
181
- taskId: target.workspace.mode === 'task-bound' ? target.workspace.taskId : null,
182
- branch: target.branch, reason: 'manual-validation'
183
- };
184
- const originalBranch = git(manifest.worktreeRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
185
- const originalHead = git(manifest.worktreeRoot, ['rev-parse', 'HEAD']);
186
- fs.writeFileSync(leasePath, `${JSON.stringify(lease)}\n`, { flag: 'wx', mode: 0o600 });
187
- let stopped = false;
120
+ const targetRoot = resolveBranchWorktree(config.repoRoot, target.branch);
121
+ if (!targetRoot) throw new Error(`TASK_VALIDATION_WORKTREE_NOT_FOUND: ${target.branch}`);
122
+ const worktree = assertGitWorktreeBinding(config.repoRoot, targetRoot, target.branch);
123
+ const originalBranch = git(worktree.worktreeRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
124
+ const originalHead = git(worktree.worktreeRoot, ['rev-parse', 'HEAD']);
188
125
  let restored = false;
189
126
  try {
190
- appendSandboxControlAudit(manifest, 'lease-acquire', { ownerPid: process.pid });
191
- waitForState(control.statusDir, (status) => status.state === 'parked' && status.reasonCode === 'SANDBOX_CONTROL_HANDOFF_ACTIVE');
192
- if (row.running) {
193
- runEngine(engine, 'docker', ['stop', row.name]);
194
- stopped = true;
195
- }
196
- const refreshedRows = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
197
- const refreshed = selectSandboxContainer(
198
- [...refreshedRows.running, ...refreshedRows.nonRunning],
199
- containerNameCandidates(config, target.branch)
200
- );
201
- if (!refreshed || refreshed.running) throw new Error('SANDBOX_VALIDATION_CONTAINER_STILL_RUNNING');
202
- const activeLease = readActiveLease(manifest);
203
- if (!activeLease || activeLease.nonce !== nonce) throw new Error('SANDBOX_VALIDATION_LEASE_OWNERSHIP_LOST');
204
- assertGitWorktreeBinding(config.repoRoot, manifest.worktreeRoot, target.branch);
205
- const result = runValidationCommand(options.command, manifest.worktreeRoot, 'inplace', options.timeoutMs, options.format === 'json');
127
+ const result = runValidationCommand(options.command, worktree.worktreeRoot, 'inplace', options.timeoutMs, options.format === 'json');
206
128
  return { ...result, cleanup: () => restored ? 'completed' : 'pending' };
207
129
  } finally {
208
- const recoveryErrors: string[] = [];
209
130
  try {
210
- const currentBranch = git(manifest.worktreeRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
211
- if (currentBranch !== originalBranch) git(manifest.worktreeRoot, ['switch', originalBranch]);
131
+ const currentBranch = git(worktree.worktreeRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
132
+ if (currentBranch !== originalBranch) git(worktree.worktreeRoot, ['switch', originalBranch]);
212
133
  } catch {
213
134
  try {
214
- git(manifest.worktreeRoot, ['checkout', '--detach', originalHead]);
215
- git(manifest.worktreeRoot, ['switch', originalBranch]);
216
- } catch (fallbackError) {
217
- recoveryErrors.push(`git binding restore failed: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`);
218
- }
219
- }
220
- if (stopped) {
221
- try {
222
- runEngine(engine, 'docker', ['start', row.name]);
223
- } catch (error) {
224
- recoveryErrors.push(`container start failed: ${error instanceof Error ? error.message : String(error)}`);
225
- }
226
- }
227
- try {
228
- const currentLease = JSON.parse(fs.readFileSync(leasePath, 'utf8')) as { nonce?: unknown };
229
- if (currentLease.nonce !== nonce) throw new Error('SANDBOX_VALIDATION_LEASE_OWNERSHIP_LOST');
230
- } catch (error) {
231
- recoveryErrors.push(error instanceof Error ? error.message : String(error));
232
- }
233
- if (recoveryErrors.length === 0) {
234
- try {
235
- fs.unlinkSync(leasePath);
236
- appendSandboxControlAudit(manifest, 'lease-release', { ownerPid: process.pid });
237
- waitForState(control.statusDir, (status) => status.state === 'healthy');
238
- restored = true;
135
+ git(worktree.worktreeRoot, ['checkout', '--detach', originalHead]);
136
+ git(worktree.worktreeRoot, ['switch', originalBranch]);
239
137
  } catch (error) {
240
- try {
241
- if (!fs.existsSync(leasePath)) {
242
- fs.writeFileSync(leasePath, `${JSON.stringify(lease)}\n`, { flag: 'wx', mode: 0o600 });
243
- }
244
- appendSandboxControlAudit(manifest, 'lease-release-failed', { ownerPid: process.pid });
245
- } catch (leaseError) {
246
- recoveryErrors.push(`lease preservation failed: ${leaseError instanceof Error ? leaseError.message : String(leaseError)}`);
247
- }
248
- recoveryErrors.push(error instanceof Error ? error.message : String(error));
138
+ throw new Error(`TASK_VALIDATION_WORKTREE_RESTORE_FAILED: ${error instanceof Error ? error.message : String(error)}`);
249
139
  }
250
140
  }
251
- if (recoveryErrors.length > 0) {
252
- throw new Error(
253
- `SANDBOX_VALIDATION_RECOVERY_INCOMPLETE: ${recoveryErrors.join('; ')}; run 'ai sandbox start ${target.branch}'`
254
- );
255
- }
141
+ restored = true;
256
142
  }
257
143
  }
258
144
 
@@ -51,6 +51,33 @@ function boundedFailureDetail(result: RunResult): string {
51
51
  : `${diagnostic.slice(0, ERROR_DETAIL_LIMIT)}… [truncated]`;
52
52
  }
53
53
 
54
+ function requestDiagnostics(args: string[], request: RequestOptions, attempt: number, errorMessage: string): string {
55
+ if (process.env.AGENT_INFRA_GH_DIAGNOSTICS !== '1' && !/rate.?limit/iu.test(errorMessage)) return '';
56
+
57
+ const apiIndex = args.indexOf('api');
58
+ const endpoint = apiIndex < 0
59
+ ? args.slice(0, 2).filter((arg) => !arg.startsWith('-')).join(' ')
60
+ : args.slice(apiIndex + 1).find((arg) => !arg.startsWith('-') && !arg.includes('=')) || 'unknown';
61
+ const safeEndpoint = endpoint.startsWith('http')
62
+ ? (() => {
63
+ try {
64
+ const url = new URL(endpoint);
65
+ return `${url.host}${url.pathname}`;
66
+ } catch {
67
+ return 'unknown';
68
+ }
69
+ })()
70
+ : endpoint.split('?')[0] || 'unknown';
71
+ const methodIndex = args.findIndex((arg) => arg === '-X' || arg === '--method');
72
+ const methodFromArgs = methodIndex >= 0 ? args[methodIndex + 1] : undefined;
73
+ const method = methodFromArgs || request.method || (safeEndpoint === 'graphql' ? 'POST' : 'GET');
74
+ const tokenEnvironment = ['GH_TOKEN', 'GITHUB_TOKEN', 'GH_ENTERPRISE_TOKEN', 'GITHUB_ENTERPRISE_TOKEN']
75
+ .filter((name) => Boolean(process.env[name]));
76
+ const configDirOverride = Boolean(process.env.GH_CONFIG_DIR);
77
+
78
+ return ` [gh-diagnostic endpoint=${safeEndpoint} method=${method} attempt=${attempt} tokenEnv=${tokenEnvironment.join(',') || 'none'} ghConfigDirOverride=${configDirOverride}]`;
79
+ }
80
+
54
81
  function defaultRunner(args: string[], options: RunOptions): RunResult {
55
82
  const command = process.env.AGENT_INFRA_GH_BIN || 'gh';
56
83
  let prefix: string[] = [];
@@ -168,13 +195,17 @@ function createGitHubClient(options: ClientOptions = {}): GitHubClient {
168
195
  const result = runner(args, request);
169
196
  if (result.status === 0) return { ok: true, value: result.stdout };
170
197
  const error = classifyGitHubFailure(result);
171
- if (!retryableMethod || !error.requestRetryable) return { ok: false, error: clientError(error) };
198
+ const requestDetail = requestDiagnostics(args, request, attempt + 1, error.message);
199
+ const contextualError = requestDetail
200
+ ? { ...error, message: `${error.message}${requestDetail}` }
201
+ : error;
202
+ if (!retryableMethod || !error.requestRetryable) return { ok: false, error: clientError(contextualError) };
172
203
  if (attempt >= delays.length) {
173
204
  return {
174
205
  ok: false,
175
206
  error: error.code === 'NETWORK_TRANSIENT'
176
- ? { ...clientError(error), code: 'NETWORK_RETRY_EXHAUSTED', message: `${error.message} (retry exhausted)` }
177
- : clientError(error)
207
+ ? { ...clientError(contextualError), code: 'NETWORK_RETRY_EXHAUSTED', message: `${contextualError.message} (retry exhausted)` }
208
+ : clientError(contextualError)
178
209
  };
179
210
  }
180
211
  sleep(delays[attempt]!);
@@ -136,8 +136,8 @@ async function releaseNoteContext(
136
136
  options: ReleaseNoteOptions = {}
137
137
  ) {
138
138
  const historyLimit = input.historyLimit ?? 3;
139
- if (!input.fromTag || !input.toTag || !input.branch || !Number.isInteger(historyLimit) || historyLimit < 1 || historyLimit > 20) {
140
- return { ...baseResult('failed'), error: { code: 'RELEASE_NOTES_INPUT_INVALID', message: 'from-tag, to-tag, branch, and a history limit from 1 to 20 are required', retryable: false } };
139
+ if (!input.fromTag || !input.toTag || !input.branch || !Number.isInteger(historyLimit) || historyLimit < 1 || historyLimit > 100) {
140
+ return { ...baseResult('failed'), error: { code: 'RELEASE_NOTES_INPUT_INVALID', message: 'from-tag, to-tag, branch, and a history limit from 1 to 100 are required', retryable: false } };
141
141
  }
142
142
  const cwd = path.resolve(options.cwd || process.cwd());
143
143
  const platformType = options.platformType ?? configuredPlatform(cwd);
@@ -14,7 +14,6 @@ export function readSandboxControlStatusForRow(
14
14
  : { mode: 'branch-only' as const };
15
15
  const control = sandboxControlPaths({
16
16
  base: config.controlBase,
17
- repoRoot: config.repoRoot,
18
17
  project: config.project,
19
18
  container: row.name,
20
19
  identity
@@ -91,6 +91,7 @@ import {
91
91
  materializeSandboxControl,
92
92
  materializeSandboxWorkspaceView,
93
93
  prepareSandboxWorkspaceMountTargets,
94
+ prepareSandboxCompletedTaskMountSource,
94
95
  sandboxControlPaths,
95
96
  } from '../workspace-view.ts';
96
97
  import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from '../clipboard/paths.ts';
@@ -1107,7 +1108,6 @@ export async function create(
1107
1108
  const container = containerName(effectiveConfig, branch);
1108
1109
  const controlPaths = sandboxControlPaths({
1109
1110
  base: effectiveConfig.controlBase,
1110
- repoRoot: effectiveConfig.repoRoot,
1111
1111
  project: effectiveConfig.project,
1112
1112
  container,
1113
1113
  identity: target.workspace
@@ -1451,7 +1451,10 @@ export async function create(
1451
1451
  const taskSource = target.workspace.mode === 'task-bound'
1452
1452
  ? assertSandboxTaskSource(effectiveConfig.repoRoot, target.workspace.taskId)
1453
1453
  : null;
1454
- prepareSandboxWorkspaceMountTargets(worktree);
1454
+ const completedTaskSource = target.workspace.mode === 'task-bound'
1455
+ ? prepareSandboxCompletedTaskMountSource(effectiveConfig.repoRoot, target.workspace.taskId)
1456
+ : null;
1457
+ prepareSandboxWorkspaceMountTargets(worktree, target.workspace.mode === 'task-bound' ? target.workspace.taskId : undefined);
1455
1458
  if (previousCutoverSnapshot) {
1456
1459
  assertSandboxControlCutoverSnapshot(previousCutoverSnapshot);
1457
1460
  if (previousManifest) {
@@ -1489,11 +1492,13 @@ export async function create(
1489
1492
  shellConfigHostDir: hostShellConfig.hostDir,
1490
1493
  workspaceViewRoot: workspaceView.root,
1491
1494
  controlDir: control.channelDir,
1495
+ ...(target.workspace.mode === 'task-bound' ? { controlRootDir: control.root } : {}),
1492
1496
  controlStatusDir: control.statusDir,
1493
1497
  ...(target.workspace.mode === 'task-bound' ? { runtimeDir: control.runtimeDir } : {}),
1494
1498
  ...(target.workspace.mode === 'task-bound'
1495
1499
  ? {
1496
1500
  taskSources: [taskSource!],
1501
+ completedTaskSource: completedTaskSource!,
1497
1502
  taskId: target.workspace.taskId
1498
1503
  }
1499
1504
  : {})
@@ -1583,8 +1588,6 @@ export async function create(
1583
1588
  ]
1584
1589
  : []),
1585
1590
  '-e',
1586
- `AGENT_INFRA_CONTROL_TOKEN=${control.token}`,
1587
- '-e',
1588
1591
  `AGENT_INFRA_CONTROL_GENERATION=${control.generation}`,
1589
1592
  '-e',
1590
1593
  'AGENT_INFRA_CONTROL_DIR=/run/agent-infra/control',
@@ -1593,7 +1596,10 @@ export async function create(
1593
1596
  '-e',
1594
1597
  `AGENT_INFRA_CONTROL_ROOT_ID=${control.controlRootId}`,
1595
1598
  ...(target.workspace.mode === 'task-bound'
1596
- ? ['-e', 'AGENT_INFRA_RUNTIME_DIR=/run/agent-infra/runtime']
1599
+ ? [
1600
+ '-e', 'AGENT_INFRA_CONTROL_ROOT_DIR=/run/agent-infra/control-root',
1601
+ '-e', 'AGENT_INFRA_RUNTIME_DIR=/run/agent-infra/runtime'
1602
+ ]
1597
1603
  : []),
1598
1604
  '--label',
1599
1605
  `${sandboxRuntimeCapabilityLabel(effectiveConfig)}=${capabilityPlan.runtimeSignature}`,
@@ -18,11 +18,10 @@ export { containerListFormat, parseLabels } from './list-running.ts';
18
18
  const USAGE = `Usage: ai sandbox ls
19
19
 
20
20
  Lists all containers for the current project. The '#' column is a
21
- display-only row number; the 'SHORT' column shows the active task short
22
- id bound to each container's branch (via
23
- .agents/workspace/active/.short-ids.json), or '-' if no active task is
24
- bound. Pass the SHORT value to "ai sandbox exec" (e.g. 'ai sandbox exec 11').
25
- A SHORT value of '-' means this container has no active task short id and may
21
+ display-only row number; the 'SHORT' column shows the task short id bound to
22
+ each task-bound container, or '-' if no short id is registered. Pass the
23
+ SHORT value to "ai sandbox exec" (e.g. 'ai sandbox exec 11').
24
+ A SHORT value of '-' means this container has no task short id and may
26
25
  be eligible for "ai sandbox rm --unbound" cleanup. Use a full TASK-id to
27
26
  remove a specific task-bound sandbox.
28
27
 
@@ -13,7 +13,7 @@ import { fetchSandboxRows } from './list-running.ts';
13
13
  import { rmOne, rmPurge, rmUnbound } from '../removal.ts';
14
14
 
15
15
  const USAGE = `Usage:
16
- ai sandbox rm <branch | TASK-id | short id> Remove one sandbox; use a full TASK-id for a task-bound sandbox and a branch for branch-only sandboxes
16
+ ai sandbox rm <branch | TASK-id | short id> Remove one sandbox; use a retained short id or full TASK-id for a task-bound sandbox and a branch for branch-only sandboxes
17
17
  ai sandbox rm --unbound [--dry-run] [--yes] Remove sandboxes not bound to an active task short id
18
18
  ai sandbox rm --purge Tear down ALL sandboxes for the project (containers, worktrees, image, VM)`;
19
19
 
@@ -237,3 +237,16 @@ export function loadConfig({
237
237
  }
238
238
  };
239
239
  }
240
+
241
+ export function loadSandboxControlLocation(repoRoot = detectRepoRoot()): Readonly<{
242
+ controlBase: string;
243
+ project: string;
244
+ }> | null {
245
+ const configPath = path.join(repoRoot, '.agents', '.airc.json');
246
+ if (!fs.existsSync(configPath)) return null;
247
+ const airc = JSON.parse(fs.readFileSync(configPath, 'utf8')) as AircConfig;
248
+ if (typeof airc.project !== 'string' || !airc.project) return null;
249
+ const home = homedir();
250
+ if (!home) throw new Error('sandbox: home directory is required');
251
+ return { controlBase: hostJoin(home, '.agent-infra', 'sandbox-control'), project: airc.project };
252
+ }
@@ -41,8 +41,7 @@ const AUDIT_MAX_BYTES = 1024 * 1024;
41
41
 
42
42
  export function requestAuditFields(manifest: SandboxControlManifest, request: SandboxControlRequest) {
43
43
  return {
44
- requestId: request.id, requestFamily: request.family, sandboxTaskId: manifest.taskId,
45
- requestGeneration: request.generation, requestIssuedAt: request.issuedAt, requestExpiresAt: request.expiresAt
44
+ requestId: request.id, requestFamily: request.family, sandboxTaskId: manifest.taskId
46
45
  };
47
46
  }
48
47