@fitlab-ai/agent-infra 0.9.3 → 0.9.5

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 (203) hide show
  1. package/README.md +3 -1
  2. package/README.zh-CN.md +3 -1
  3. package/bin/cli.ts +12 -0
  4. package/dist/bin/cli.js +14 -0
  5. package/dist/bin/internal-cli.js +144 -123
  6. package/dist/lib/agent-clients/adapter.js +94 -4
  7. package/dist/lib/{sandbox/credentials.js → agent-clients/adapters/claude-code-credentials.js} +4 -14
  8. package/dist/lib/agent-clients/adapters/claude-code-refresh.js +85 -0
  9. package/dist/lib/agent-clients/adapters/claude-code-sandbox.js +247 -0
  10. package/dist/lib/agent-clients/adapters/claude-code.js +17 -3
  11. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +382 -0
  12. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +163 -0
  13. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +211 -0
  14. package/dist/lib/agent-clients/adapters/codex.js +2 -2
  15. package/dist/lib/agent-clients/adapters/opencode-sandbox.js +108 -0
  16. package/dist/lib/agent-clients/adapters/opencode.js +15 -3
  17. package/dist/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
  18. package/dist/lib/agent-clients/registry.js +9 -1
  19. package/dist/lib/git/worktree-identity.js +64 -0
  20. package/dist/lib/internal/codex-lifecycle.js +202 -0
  21. package/dist/lib/internal/sandbox-control.js +24 -0
  22. package/dist/lib/internal/task-activity.js +45 -13
  23. package/dist/lib/internal/task-create.js +48 -0
  24. package/dist/lib/internal/task-ledger.js +12 -3
  25. package/dist/lib/internal/task-lifecycle.js +19 -1
  26. package/dist/lib/internal/task-orchestration.js +170 -27
  27. package/dist/lib/platform/github-client.js +19 -2
  28. package/dist/lib/platform/pr-checks.js +1 -1
  29. package/dist/lib/platform/pull-requests.js +47 -1
  30. package/dist/lib/platform/releases.js +13 -5
  31. package/dist/lib/process-data/index.js +239 -0
  32. package/dist/lib/process-data/normalize.js +198 -0
  33. package/dist/lib/process-data/privacy.js +33 -0
  34. package/dist/lib/process-data/reconcile.js +95 -0
  35. package/dist/lib/process-data/repair.js +58 -0
  36. package/dist/lib/process-data/sources.js +345 -0
  37. package/dist/lib/process-data/store.js +282 -0
  38. package/dist/lib/process-data/types.js +2 -0
  39. package/dist/lib/run/index.js +4 -3
  40. package/dist/lib/run/tui.js +7 -8
  41. package/dist/lib/sandbox/agent-client-reconciler.js +26 -2
  42. package/dist/lib/sandbox/capture.js +7 -2
  43. package/dist/lib/sandbox/commands/create.js +110 -228
  44. package/dist/lib/sandbox/commands/enter.js +15 -47
  45. package/dist/lib/sandbox/commands/list-running.js +16 -6
  46. package/dist/lib/sandbox/commands/ls.js +10 -6
  47. package/dist/lib/sandbox/commands/rebuild.js +4 -3
  48. package/dist/lib/sandbox/commands/refresh.js +4 -84
  49. package/dist/lib/sandbox/commands/rm.js +24 -3
  50. package/dist/lib/sandbox/commands/show.js +12 -3
  51. package/dist/lib/sandbox/commands/start.js +7 -4
  52. package/dist/lib/sandbox/config.js +2 -0
  53. package/dist/lib/sandbox/constants.js +6 -0
  54. package/dist/lib/sandbox/control/client.js +58 -0
  55. package/dist/lib/sandbox/control/protocol.js +54 -0
  56. package/dist/lib/sandbox/control/server.js +144 -0
  57. package/dist/lib/sandbox/dockerfile.js +36 -13
  58. package/dist/lib/sandbox/mounts.js +17 -4
  59. package/dist/lib/sandbox/recovery.js +128 -5
  60. package/dist/lib/sandbox/redaction.js +12 -0
  61. package/dist/lib/sandbox/runtimes/base.dockerfile +24 -74
  62. package/dist/lib/sandbox/workspace-identity.js +95 -0
  63. package/dist/lib/sandbox/workspace-view.js +109 -0
  64. package/dist/lib/task/activity-intent.js +257 -51
  65. package/dist/lib/task/activity-log.js +32 -5
  66. package/dist/lib/task/commit-finalization.js +278 -0
  67. package/dist/lib/task/commit-intent.js +165 -0
  68. package/dist/lib/task/create-service.js +137 -0
  69. package/dist/lib/task/create.js +333 -0
  70. package/dist/lib/task/decision-intents.js +31 -17
  71. package/dist/lib/task/delegation-receipts.js +2 -2
  72. package/dist/lib/task/implementation-inputs.js +54 -7
  73. package/dist/lib/task/ledger-intents.js +35 -1
  74. package/dist/lib/task/orchestration.js +505 -34
  75. package/dist/lib/task/resolve-ref.js +41 -1
  76. package/dist/lib/task/review-fingerprint.js +4 -0
  77. package/dist/lib/task/workspace-snapshot.js +78 -3
  78. package/lib/agent-clients/adapter.ts +130 -6
  79. package/lib/{sandbox/credentials.ts → agent-clients/adapters/claude-code-credentials.ts} +3 -18
  80. package/lib/agent-clients/adapters/claude-code-refresh.ts +128 -0
  81. package/lib/agent-clients/adapters/claude-code-sandbox.ts +295 -0
  82. package/lib/agent-clients/adapters/claude-code.ts +21 -3
  83. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +442 -0
  84. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +307 -0
  85. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +250 -0
  86. package/lib/agent-clients/adapters/codex.ts +2 -2
  87. package/lib/agent-clients/adapters/opencode-sandbox.ts +136 -0
  88. package/lib/agent-clients/adapters/opencode.ts +18 -3
  89. package/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
  90. package/lib/agent-clients/registry.ts +9 -1
  91. package/lib/git/worktree-identity.ts +86 -0
  92. package/lib/internal/codex-lifecycle.ts +204 -0
  93. package/lib/internal/sandbox-control.ts +24 -0
  94. package/lib/internal/task-activity.ts +50 -14
  95. package/lib/internal/task-create.ts +49 -0
  96. package/lib/internal/task-ledger.ts +12 -3
  97. package/lib/internal/task-lifecycle.ts +22 -1
  98. package/lib/internal/task-orchestration.ts +117 -21
  99. package/lib/platform/github-client.ts +20 -2
  100. package/lib/platform/pr-checks.ts +1 -1
  101. package/lib/platform/pull-requests.ts +49 -1
  102. package/lib/platform/releases.ts +16 -4
  103. package/lib/process-data/index.ts +224 -0
  104. package/lib/process-data/normalize.ts +202 -0
  105. package/lib/process-data/privacy.ts +41 -0
  106. package/lib/process-data/reconcile.ts +110 -0
  107. package/lib/process-data/repair.ts +64 -0
  108. package/lib/process-data/sources.ts +354 -0
  109. package/lib/process-data/store.ts +315 -0
  110. package/lib/process-data/types.ts +135 -0
  111. package/lib/run/index.ts +5 -3
  112. package/lib/run/tui.ts +11 -8
  113. package/lib/sandbox/agent-client-reconciler.ts +44 -2
  114. package/lib/sandbox/capture.ts +9 -2
  115. package/lib/sandbox/commands/create.ts +127 -260
  116. package/lib/sandbox/commands/enter.ts +13 -59
  117. package/lib/sandbox/commands/list-running.ts +23 -6
  118. package/lib/sandbox/commands/ls.ts +22 -6
  119. package/lib/sandbox/commands/rebuild.ts +4 -3
  120. package/lib/sandbox/commands/refresh.ts +4 -126
  121. package/lib/sandbox/commands/rm.ts +24 -3
  122. package/lib/sandbox/commands/show.ts +29 -3
  123. package/lib/sandbox/commands/start.ts +6 -4
  124. package/lib/sandbox/config.ts +4 -0
  125. package/lib/sandbox/constants.ts +8 -0
  126. package/lib/sandbox/control/client.ts +79 -0
  127. package/lib/sandbox/control/protocol.ts +110 -0
  128. package/lib/sandbox/control/server.ts +155 -0
  129. package/lib/sandbox/dockerfile.ts +66 -13
  130. package/lib/sandbox/mounts.ts +24 -4
  131. package/lib/sandbox/recovery.ts +163 -6
  132. package/lib/sandbox/redaction.ts +14 -0
  133. package/lib/sandbox/runtimes/base.dockerfile +24 -74
  134. package/lib/sandbox/tool-types.ts +16 -0
  135. package/lib/sandbox/workspace-identity.ts +123 -0
  136. package/lib/sandbox/workspace-view.ts +150 -0
  137. package/lib/task/activity-intent.ts +350 -61
  138. package/lib/task/activity-log.ts +46 -7
  139. package/lib/task/commit-finalization.ts +376 -0
  140. package/lib/task/commit-intent.ts +230 -0
  141. package/lib/task/create-service.ts +171 -0
  142. package/lib/task/create.ts +372 -0
  143. package/lib/task/decision-intents.ts +32 -12
  144. package/lib/task/delegation-receipts.ts +2 -2
  145. package/lib/task/implementation-inputs.ts +62 -8
  146. package/lib/task/ledger-intents.ts +43 -3
  147. package/lib/task/orchestration.ts +673 -41
  148. package/lib/task/resolve-ref.ts +46 -1
  149. package/lib/task/review-fingerprint.ts +9 -0
  150. package/lib/task/workspace-snapshot.ts +98 -4
  151. package/package.json +7 -7
  152. package/templates/.agents/hooks/lifecycle-delegation.js +47 -3
  153. package/templates/.agents/rules/README.en.md +1 -1
  154. package/templates/.agents/rules/README.zh-CN.md +1 -1
  155. package/templates/.agents/rules/human-decision-context.en.md +11 -2
  156. package/templates/.agents/rules/human-decision-context.zh-CN.md +11 -2
  157. package/templates/.agents/rules/pr-checks-commands.en.md +1 -1
  158. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  159. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +2 -2
  160. package/templates/.agents/rules/pr-checks-commands.zh-CN.md +1 -1
  161. package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
  162. package/templates/.agents/rules/task-management.en.md +3 -0
  163. package/templates/.agents/rules/task-management.zh-CN.md +3 -0
  164. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  165. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
  166. package/templates/.agents/skills/commit/SKILL.en.md +21 -17
  167. package/templates/.agents/skills/commit/SKILL.zh-CN.md +28 -16
  168. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +50 -0
  169. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +56 -0
  170. package/templates/.agents/skills/commit/reference/task-status-update.en.md +4 -4
  171. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +4 -4
  172. package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
  173. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
  174. package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
  175. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
  176. package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
  177. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
  178. package/templates/.agents/skills/create-task/reference/context-capture.en.md +34 -0
  179. package/templates/.agents/skills/create-task/reference/context-capture.zh-CN.md +34 -0
  180. package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
  181. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
  182. package/templates/.agents/skills/review-code/reference/output-templates.en.md +1 -1
  183. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +1 -1
  184. package/templates/.agents/skills/review-pr/SKILL.en.md +36 -8
  185. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +36 -8
  186. package/templates/.agents/skills/review-pr/reference/report-template.en.md +1 -1
  187. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +1 -1
  188. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +67 -50
  189. package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
  190. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
  191. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +2 -2
  192. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +2 -2
  193. package/templates/.agents/templates/task.en.md +1 -1
  194. package/templates/.agents/templates/task.zh-CN.md +1 -1
  195. package/templates/.claude/commands/watch-pr.en.md +1 -1
  196. package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
  197. package/templates/.claude/rules/tool-preferences.en.md +20 -0
  198. package/templates/.claude/rules/tool-preferences.zh-CN.md +20 -0
  199. package/templates/.codex/hooks.json +50 -0
  200. package/templates/.opencode/commands/watch-pr.en.md +1 -1
  201. package/templates/.opencode/commands/watch-pr.zh-CN.md +1 -1
  202. package/templates/.gemini/commands/_project_/review-pr.en.toml +0 -10
  203. package/templates/.gemini/commands/_project_/review-pr.zh-CN.toml +0 -10
@@ -3,12 +3,13 @@ import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, san
3
3
  import { detectEngine } from '../engine.js';
4
4
  import { ensureSandboxReady } from '../recovery.js';
5
5
  import { createSandboxCapabilityPlan, runBoundedSandboxHookCommand, runSandboxHooks } from '../agent-client-reconciler.js';
6
- import { formatCredentialWarnings, formatRemaining, hasClaudeProviderAuth, reconcileClaudeCredentials, redactCommandError, validateClaudeCredentialsEnvOverride } from '../credentials.js';
7
6
  import { runInteractiveEngine } from '../shell.js';
8
7
  import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.js';
8
+ import { redactCommandError } from '../redaction.js';
9
9
  import { runInteractiveWithClipboardBridge } from '../clipboard/bridge.js';
10
10
  import { detectHostTimezone } from '../host-timezone.js';
11
- import { fetchSandboxRows, resolveBranchArg, selectSandboxContainer, } from './list-running.js';
11
+ import { fetchSandboxRows, selectSandboxContainer, } from './list-running.js';
12
+ import { resolveSandboxTarget } from '../workspace-identity.js';
12
13
  const USAGE = `Usage: ai sandbox exec [--recreate] <branch | TASK-id | N> [cmd...]
13
14
 
14
15
  N references an active task short id from
@@ -53,32 +54,6 @@ export function runSandboxInteractive(params) {
53
54
  }
54
55
  return runBridge({ engine, dockerArgs, container, home });
55
56
  }
56
- export function formatCredentialSyncStatus(result, isTTY = process.stderr.isTTY, providerAuthAvailable = false) {
57
- if (providerAuthAvailable && (result.status === 'STALE_ACCESS' || result.status === 'MISSING')) {
58
- return null;
59
- }
60
- if (result.status === 'STALE_ACCESS') {
61
- return 'Warning: Claude Code credentials on host appear stale. Run "ai sandbox refresh" or "claude /login" to renew.\n';
62
- }
63
- if (result.status === 'MISSING') {
64
- return 'Warning: Claude Code credentials missing on host. Run "claude /login" to authenticate.\n';
65
- }
66
- if (result.status === 'KEYCHAIN_WRITE_FAILED') {
67
- return `Warning: A sandbox refresh produced newer credentials but host Keychain write failed (${formatCredentialWarnings(result.warnings)}). Run "ai sandbox refresh" again or "claude /status" on the host to retry.\n`;
68
- }
69
- if (result.status === 'KEYCHAIN_LOCKED' || result.status === 'KEYCHAIN_ERROR') {
70
- return 'Warning: Host keychain is unavailable; Claude credential sync skipped. Run "ai sandbox refresh" for details.\n';
71
- }
72
- if (result.status === 'OK' && result.authoritative !== 'host') {
73
- const message = `Synced Claude Code credentials from sandbox refresh back to host (expires in ${formatRemaining(result.expiresAt)})`;
74
- return isTTY ? `\x1b[2m${message}\x1b[0m\n` : `${message}\n`;
75
- }
76
- if (result.status === 'OK' && result.filesWritten.length > 0) {
77
- const message = `Synced Claude Code credentials from host Keychain (expires in ${formatRemaining(result.expiresAt)})`;
78
- return isTTY ? `\x1b[2m${message}\x1b[0m\n` : `${message}\n`;
79
- }
80
- return null;
81
- }
82
57
  export function parseEnterArgs(args) {
83
58
  let recreate = false;
84
59
  let index = 0;
@@ -101,10 +76,10 @@ export async function enter(args) {
101
76
  return 0;
102
77
  }
103
78
  const config = loadConfig();
104
- validateClaudeCredentialsEnvOverride();
105
79
  const engine = detectEngine(config);
106
80
  const parsed = parseEnterArgs(args);
107
- const branch = resolveBranchArg(parsed.target, { repoRoot: config.repoRoot });
81
+ const target = resolveSandboxTarget(parsed.target, config.repoRoot);
82
+ const branch = target.branch;
108
83
  assertValidBranchName(branch);
109
84
  const { running, nonRunning } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
110
85
  const found = selectSandboxContainer([...running, ...nonRunning], containerNameCandidates(config, branch));
@@ -115,11 +90,12 @@ export async function enter(args) {
115
90
  config,
116
91
  engine,
117
92
  branch,
93
+ workspace: target.workspace,
118
94
  row: found,
119
95
  allowRecreate: parsed.recreate,
120
- recreate: async (targetBranch) => {
96
+ recreate: async () => {
121
97
  const { create } = await import('./create.js');
122
- await create([targetBranch, '--no-refresh']);
98
+ await create([target.requestedRef, '--no-refresh']);
123
99
  }
124
100
  });
125
101
  const container = ready.container;
@@ -128,7 +104,11 @@ export async function enter(args) {
128
104
  const enterHookResults = await runSandboxHooks({
129
105
  hooks: capabilityPlan.hooksByPhase['before-enter'],
130
106
  phase: 'before-enter',
131
- context: { config, plan: capabilityPlan },
107
+ context: {
108
+ config,
109
+ plan: capabilityPlan,
110
+ enter: { hostHome: config.home, hostEnv: { ...process.env } }
111
+ },
132
112
  runCommand: runBoundedSandboxHookCommand
133
113
  });
134
114
  for (const result of enterHookResults) {
@@ -138,20 +118,8 @@ export async function enter(args) {
138
118
  if (result.status === 'warning') {
139
119
  process.stderr.write(`Warning: ${result.message ?? `Sandbox hook '${result.hookId}' did not complete.`}\n`);
140
120
  }
141
- }
142
- if (capabilityPlan.selectedAgentClients.some((adapter) => adapter.id === 'claude-code')) {
143
- try {
144
- // Scan all projects so a refresh from a neighbouring sandbox can still flow back to the host.
145
- const providerAuthAvailable = hasClaudeProviderAuth(config.home);
146
- const result = reconcileClaudeCredentials(config.home);
147
- const message = formatCredentialSyncStatus(result, process.stderr.isTTY, providerAuthAvailable);
148
- if (message) {
149
- process.stderr.write(message);
150
- }
151
- }
152
- catch (error) {
153
- process.stderr.write(`Warning: Failed to sync Claude Code credentials: ${redactCommandError(error instanceof Error ? error.message : 'unknown error')}\n`);
154
- }
121
+ if (result.message && result.status !== 'warning')
122
+ process.stderr.write(result.message);
155
123
  }
156
124
  const envFlags = [...terminalEnvFlags(), ...hostTimezoneEnvFlags()];
157
125
  if (cmd.length === 0) {
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
  import { runSafeEngine, runVerboseEngine } from '../shell.js';
5
5
  import { resolveTaskBranch } from '../task-resolver.js';
6
6
  import { isRemovedHashShortIdInput } from '../../task/short-id.js';
7
+ import { parseSandboxWorkspaceIdentity } from '../workspace-identity.js';
7
8
  export function containerListFormat() {
8
9
  return '{{.Names}}\t{{.Status}}\t{{.Labels}}';
9
10
  }
@@ -24,19 +25,28 @@ export function parseLabels(csv) {
24
25
  }
25
26
  return labels;
26
27
  }
27
- export function parseSandboxRows(rawOutput, branchKey) {
28
+ export function parseSandboxRows(rawOutput, branchKey, workspaceKeys) {
28
29
  if (!rawOutput) {
29
30
  return [];
30
31
  }
31
32
  return rawOutput.split('\n').map((line) => {
32
33
  const [name = '', status = '', labelsCsv = ''] = line.split('\t');
33
- const branch = parseLabels(labelsCsv)[branchKey] ?? '';
34
+ const labels = parseLabels(labelsCsv);
35
+ const workspace = workspaceKeys
36
+ ? parseSandboxWorkspaceIdentity(labels, workspaceKeys)
37
+ : null;
34
38
  return {
35
39
  name,
36
40
  status,
37
- branch,
41
+ branch: labels[branchKey] ?? '',
38
42
  running: status.startsWith('Up '),
39
- index: null
43
+ index: null,
44
+ ...(workspace
45
+ ? {
46
+ workspaceMode: workspace.mode,
47
+ taskId: workspace.mode === 'task-bound' ? workspace.taskId : null
48
+ }
49
+ : {})
40
50
  };
41
51
  });
42
52
  }
@@ -58,7 +68,7 @@ export function sortAndIndexSandboxRows(rows) {
58
68
  }));
59
69
  return { running, nonRunning };
60
70
  }
61
- export function fetchSandboxRows(engine, label, branchKey) {
71
+ export function fetchSandboxRows(engine, label, branchKey, workspaceKeys) {
62
72
  const raw = runSafeEngine(engine, 'docker', [
63
73
  'ps',
64
74
  '-a',
@@ -67,7 +77,7 @@ export function fetchSandboxRows(engine, label, branchKey) {
67
77
  '--format',
68
78
  containerListFormat()
69
79
  ]);
70
- return sortAndIndexSandboxRows(parseSandboxRows(raw, branchKey));
80
+ return sortAndIndexSandboxRows(parseSandboxRows(raw, branchKey, workspaceKeys));
71
81
  }
72
82
  /**
73
83
  * Returns true iff `arg` is a syntactically valid task short reference.
@@ -1,10 +1,10 @@
1
1
  import * as p from '@clack/prompts';
2
2
  import pc from 'picocolors';
3
3
  import { loadConfig } from '../config.js';
4
- import { sandboxBranchLabel, sandboxLabel } from '../constants.js';
4
+ import { sandboxBranchLabel, sandboxLabel, sandboxTaskIdLabel, sandboxWorkspaceModeLabel } from '../constants.js';
5
5
  import { detectEngine } from '../engine.js';
6
6
  import { formatTable } from '../../table.js';
7
- import { lookupShortIdByBranch } from '../../task/short-id.js';
7
+ import { loadShortIdByTaskId } from '../../task/short-id.js';
8
8
  import { fetchSandboxRows } from './list-running.js';
9
9
  export { containerListFormat, parseLabels } from './list-running.js';
10
10
  const USAGE = `Usage: ai sandbox ls
@@ -19,9 +19,9 @@ to remove with "ai sandbox rm <branch>".
19
19
 
20
20
  Use "ai sandbox show <ref>" for a single sandbox's worktree and per-tool
21
21
  state paths.`;
22
- const CONTAINER_TABLE_HEADERS = ['#', 'SHORT', 'NAMES', 'STATUS', 'BRANCH'];
22
+ const CONTAINER_TABLE_HEADERS = ['#', 'SHORT', 'NAMES', 'STATUS', 'WORKSPACE', 'TASK', 'BRANCH'];
23
23
  export function formatContainerTable(rows, zebra = false) {
24
- return formatTable(CONTAINER_TABLE_HEADERS, rows.map((r) => [r.row, r.shortId, r.name, r.status, r.branch]), { zebra });
24
+ return formatTable(CONTAINER_TABLE_HEADERS, rows.map((r) => [r.row, r.shortId, r.name, r.status, r.workspace, r.taskId, r.branch]), { zebra });
25
25
  }
26
26
  export function ls(args = []) {
27
27
  if (args.length > 0 && (args[0] === '--help' || args[0] === '-h')) {
@@ -31,7 +31,7 @@ export function ls(args = []) {
31
31
  const config = loadConfig();
32
32
  const engine = detectEngine(config);
33
33
  const label = sandboxLabel(config);
34
- const { running, nonRunning } = fetchSandboxRows(engine, label, sandboxBranchLabel(config));
34
+ const { running, nonRunning } = fetchSandboxRows(engine, label, sandboxBranchLabel(config), { mode: sandboxWorkspaceModeLabel(config), taskId: sandboxTaskIdLabel(config) });
35
35
  p.intro(pc.cyan(`Sandbox status for ${config.project}`));
36
36
  p.log.step('Containers');
37
37
  const ordered = [...running, ...nonRunning];
@@ -40,12 +40,16 @@ export function ls(args = []) {
40
40
  }
41
41
  else {
42
42
  const tableRows = ordered.map((container, i) => {
43
- const shortId = container.branch ? lookupShortIdByBranch(container.branch, config.repoRoot) : null;
43
+ const shortId = container.taskId
44
+ ? loadShortIdByTaskId(config.repoRoot).get(container.taskId) ?? null
45
+ : null;
44
46
  return {
45
47
  row: String(i + 1),
46
48
  shortId: shortId ?? '-',
47
49
  name: container.name,
48
50
  status: container.status,
51
+ workspace: container.workspaceMode ?? 'legacy-invalid',
52
+ taskId: container.taskId ?? '-',
49
53
  branch: container.branch
50
54
  };
51
55
  });
@@ -7,7 +7,7 @@ import { sandboxImageRefreshLabel } from '../constants.js';
7
7
  import { detectEngine, ensureDocker } from '../engine.js';
8
8
  import { runEngine, runOkEngine, runSafeEngine, runVerboseEngine } from '../shell.js';
9
9
  import { pruneSandboxDanglingImages } from '../image-prune.js';
10
- import { resolveTools } from '../tools.js';
10
+ import { createSandboxCapabilityPlan } from '../agent-client-reconciler.js';
11
11
  import { buildImageSignature, buildSandboxImageArgs, parseImageLabels, parseRefreshTimestamp } from '../image-build.js';
12
12
  import { assertBuildProxyCompatibility, buildProxyFailureHint, prepareBuildProxy, redactBuildProxyValues } from '../build-proxy.js';
13
13
  const USAGE = `Usage: ai sandbox rebuild [--quiet] [--refresh] [--inherit-build-proxy|-B]`;
@@ -52,8 +52,9 @@ export async function rebuild(args) {
52
52
  return;
53
53
  }
54
54
  const config = loadConfig();
55
- const tools = resolveTools(config);
56
- const preparedDockerfile = prepareDockerfile(config);
55
+ const capabilityPlan = createSandboxCapabilityPlan(config);
56
+ const tools = [...capabilityPlan.tools];
57
+ const preparedDockerfile = prepareDockerfile(config, capabilityPlan.image);
57
58
  const imageSignature = buildImageSignature(preparedDockerfile, tools);
58
59
  const quiet = values.quiet ?? false;
59
60
  const refresh = values.refresh ?? false;
@@ -1,85 +1,5 @@
1
- import { homedir } from 'node:os';
2
- import { parseArgs } from 'node:util';
3
- import { buildLockedGuidance, discoverProjects, formatCredentialWarnings, formatRemaining, reconcileClaudeCredentials, redactCommandError, validateClaudeCredentialsEnvOverride } from '../credentials.js';
4
- import { runProbe } from '../shell.js';
5
- const USAGE = 'Usage: ai sandbox refresh';
6
- export function probeClaudeStatus(spawnFn = runProbe) {
7
- const result = spawnFn('claude', ['/status'], {
8
- encoding: 'utf8',
9
- stdio: ['ignore', 'pipe', 'pipe'],
10
- timeout: 30_000
11
- });
12
- return {
13
- ok: result.status === 0,
14
- stderr: result.stderr?.toString() ?? '',
15
- error: result.error?.message ?? null
16
- };
17
- }
18
- export async function refresh(args, deps = {}) {
19
- const { spawnFn = runProbe, execFn, readFn, existsFn, writeFn, writeHostFn, discoverFn = discoverProjects, writeStdout = (chunk) => process.stdout.write(chunk), writeStderr = (chunk) => process.stderr.write(chunk) } = deps;
20
- if (args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
21
- writeStdout(`${USAGE}\n`);
22
- return 0;
23
- }
24
- const { positionals } = parseArgs({ args, allowPositionals: true, strict: true });
25
- if (positionals.length > 0) {
26
- throw new Error(USAGE);
27
- }
28
- validateClaudeCredentialsEnvOverride();
29
- const home = homedir();
30
- if (!home) {
31
- throw new Error('sandbox: home directory is required');
32
- }
33
- const projects = discoverFn(home);
34
- if (projects.length === 0) {
35
- writeStdout('No project credentials to refresh.\n');
36
- return 0;
37
- }
38
- const reconcileOptions = { execFn, readFn, existsFn, writeFn, writeHostFn, projects };
39
- let result = reconcileClaudeCredentials(home, reconcileOptions);
40
- if (result.status === 'STALE_ACCESS' && result.authoritative === null) {
41
- writeStdout('Host credentials appear stale; probing claude /status to trigger refresh...\n');
42
- const probe = probeClaudeStatus(spawnFn);
43
- if (!probe.ok) {
44
- writeStderr(`Probe failed: ${redactCommandError(probe.stderr || probe.error || 'unknown error')}\n`);
45
- writeStderr('Run "claude /login" on the host to renew credentials.\n');
46
- return 1;
47
- }
48
- writeStdout('Probe succeeded; re-inspecting host credentials.\n');
49
- result = reconcileClaudeCredentials(home, reconcileOptions);
50
- }
51
- if (result.status === 'MISSING') {
52
- writeStderr('No Claude Code credentials found on host.\n');
53
- writeStderr('Run "claude /login" on the host to authenticate.\n');
54
- return 1;
55
- }
56
- if (result.status === 'KEYCHAIN_LOCKED') {
57
- writeStderr(`${buildLockedGuidance()}\n`);
58
- return 1;
59
- }
60
- if (result.status === 'KEYCHAIN_ERROR') {
61
- writeStderr(`Host keychain error: ${redactCommandError(result.detail || 'unknown error')}\n`);
62
- writeStderr(`${buildLockedGuidance()}\n`);
63
- return 1;
64
- }
65
- if (result.status === 'KEYCHAIN_WRITE_FAILED') {
66
- writeStderr(`[host] keychain write failed: ${formatCredentialWarnings(result.warnings) || 'unknown error'}\n`);
67
- return 1;
68
- }
69
- if (result.status !== 'OK') {
70
- writeStderr('Host credentials still invalid after probe; run "claude /login".\n');
71
- return 1;
72
- }
73
- if (result.authoritative && result.authoritative !== 'host' && result.hostWritten) {
74
- writeStdout(`[host] reconciled from ${result.authoritative}\n`);
75
- }
76
- for (const project of projects) {
77
- const action = result.filesWritten.includes(project) ? 'updated' : 'unchanged';
78
- writeStdout(`[${project}] ${action}; expires in ${formatRemaining(result.expiresAt)}\n`);
79
- }
80
- for (const failure of result.fileErrors) {
81
- writeStderr(`[${failure.project}] sync failed: ${failure.error}\n`);
82
- }
83
- return result.fileErrors.length > 0 ? 1 : 0;
84
- }
1
+ import { runClaudeCodeRefresh } from '../../agent-clients/adapters/claude-code-refresh.js';
2
+ const refresh = runClaudeCodeRefresh;
3
+ export { probeClaudeStatus, runClaudeCodeRefresh } from '../../agent-clients/adapters/claude-code-refresh.js';
4
+ export { refresh };
85
5
  //# sourceMappingURL=refresh.js.map
@@ -9,7 +9,7 @@ import { ENGINES, detectEngine, engineDisplayName, isManagedEngine, stopManagedV
9
9
  import { pruneSandboxDanglingImages } from '../image-prune.js';
10
10
  import { removeManagedDir, removeWorktreeDir } from '../managed-fs.js';
11
11
  import { runOk, runSafe, runSafeEngine } from '../shell.js';
12
- import { resolveTaskBranch } from '../task-resolver.js';
12
+ import { resolveSandboxTarget } from '../workspace-identity.js';
13
13
  import { toolConfigDirCandidates, toolProjectDirCandidates } from '../tools.js';
14
14
  import { createSandboxCapabilityPlan } from '../agent-client-reconciler.js';
15
15
  import { fetchSandboxRows } from './list-running.js';
@@ -57,6 +57,17 @@ async function rmOne(config, tools, branch, options = {}) {
57
57
  for (const name of matchedContainers) {
58
58
  runSafeEngine(engine, 'docker', ['stop', name]);
59
59
  runSafeEngine(engine, 'docker', ['rm', name]);
60
+ for (const [base, label] of [
61
+ [path.join(config.workspaceViewBase, config.project), 'Workspace view'],
62
+ [path.join(config.controlBase, config.project), 'Control channel']
63
+ ]) {
64
+ const directory = path.join(base, name);
65
+ if (fs.existsSync(directory)) {
66
+ removeManagedDir(base, directory);
67
+ if (!options.quiet)
68
+ p.log.success(`${label} removed: ${directory}`);
69
+ }
70
+ }
60
71
  }
61
72
  spinner.stop(pc.green(`Removed container(s): ${matchedContainers.join(', ')}`));
62
73
  }
@@ -184,6 +195,16 @@ async function rmPurge(config, tools) {
184
195
  p.log.success(`Project share dirs removed: ${config.shareBase}`);
185
196
  }
186
197
  }
198
+ for (const [base, label] of [
199
+ [config.workspaceViewBase, 'workspace views'],
200
+ [config.controlBase, 'control channels']
201
+ ]) {
202
+ const projectDir = path.join(base, config.project);
203
+ if (fs.existsSync(projectDir)) {
204
+ removeManagedDir(base, projectDir);
205
+ p.log.success(`Removed project ${label}: ${projectDir}`);
206
+ }
207
+ }
187
208
  const shouldRemoveImage = await p.confirm({
188
209
  message: `Remove image ${config.imageName}?`,
189
210
  initialValue: false
@@ -297,7 +318,7 @@ export async function rm(args) {
297
318
  });
298
319
  return;
299
320
  }
300
- const branch = resolveTaskBranch(positionals[0] ?? '', config.repoRoot);
301
- await rmOne(config, tools, branch);
321
+ const target = resolveSandboxTarget(positionals[0] ?? '', config.repoRoot);
322
+ await rmOne(config, tools, target.branch);
302
323
  }
303
324
  //# sourceMappingURL=rm.js.map
@@ -2,9 +2,11 @@ import fs from 'node:fs';
2
2
  import * as p from '@clack/prompts';
3
3
  import pc from 'picocolors';
4
4
  import { loadConfig } from '../config.js';
5
- import { assertValidBranchName, worktreeDirCandidates } from '../constants.js';
6
- import { resolveBranchArg } from './list-running.js';
5
+ import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel, sandboxTaskIdLabel, sandboxWorkspaceModeLabel, worktreeDirCandidates } from '../constants.js';
6
+ import { fetchSandboxRows, selectSandboxContainer } from './list-running.js';
7
7
  import { resolveTools, toolConfigDirCandidates } from '../tools.js';
8
+ import { detectEngine } from '../engine.js';
9
+ import { resolveSandboxTarget } from '../workspace-identity.js';
8
10
  const USAGE = `Usage: ai sandbox show <branch | TASK-id | N>
9
11
 
10
12
  Shows one sandbox's worktree path and per-tool state paths (Claude Code,
@@ -40,9 +42,13 @@ export function show(args = []) {
40
42
  return;
41
43
  }
42
44
  const config = loadConfig();
43
- const branch = resolveBranchArg(args[0], { repoRoot: config.repoRoot });
45
+ const target = resolveSandboxTarget(args[0], config.repoRoot);
46
+ const branch = target.branch;
44
47
  assertValidBranchName(branch);
45
48
  const detail = collectSandboxDetail(config, branch);
49
+ const engine = detectEngine(config);
50
+ const rows = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config), { mode: sandboxWorkspaceModeLabel(config), taskId: sandboxTaskIdLabel(config) });
51
+ const container = selectSandboxContainer([...rows.running, ...rows.nonRunning], containerNameCandidates(config, branch));
46
52
  p.intro(pc.cyan(`Sandbox detail for ${config.project} · ${branch}`));
47
53
  p.log.step('Worktree');
48
54
  if (detail.worktrees.length === 0) {
@@ -53,6 +59,9 @@ export function show(args = []) {
53
59
  process.stdout.write(` ${worktree}\n`);
54
60
  }
55
61
  }
62
+ p.log.step('Workspace identity');
63
+ process.stdout.write(` Mode: ${container?.workspaceMode ?? 'legacy-invalid'}\n`);
64
+ process.stdout.write(` Task: ${container?.taskId ?? '-'}\n`);
56
65
  for (const tool of detail.toolStates) {
57
66
  p.log.step(`${tool.name} state`);
58
67
  if (tool.entries.length === 0) {
@@ -2,7 +2,8 @@ import { loadConfig } from '../config.js';
2
2
  import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel } from '../constants.js';
3
3
  import { detectEngine } from '../engine.js';
4
4
  import { ensureSandboxReady } from '../recovery.js';
5
- import { fetchSandboxRows, resolveBranchArg, selectSandboxContainer, } from './list-running.js';
5
+ import { fetchSandboxRows, selectSandboxContainer, } from './list-running.js';
6
+ import { resolveSandboxTarget } from '../workspace-identity.js';
6
7
  const USAGE = `Usage: ai sandbox start [--recreate] <branch | TASK-id | N>
7
8
 
8
9
  Start an existing sandbox container that has stopped (for example after the
@@ -46,7 +47,8 @@ export async function start(args) {
46
47
  }
47
48
  const config = loadConfig();
48
49
  const engine = detectEngine(config);
49
- const branch = resolveBranchArg(parsed.target, { repoRoot: config.repoRoot });
50
+ const target = resolveSandboxTarget(parsed.target, config.repoRoot);
51
+ const branch = target.branch;
50
52
  assertValidBranchName(branch);
51
53
  const { running, nonRunning } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
52
54
  const found = selectSandboxContainer([...running, ...nonRunning], containerNameCandidates(config, branch));
@@ -57,11 +59,12 @@ export async function start(args) {
57
59
  config,
58
60
  engine,
59
61
  branch,
62
+ workspace: target.workspace,
60
63
  row: found,
61
64
  allowRecreate: parsed.recreate,
62
- recreate: async (targetBranch) => {
65
+ recreate: async () => {
63
66
  const { create } = await import('./create.js');
64
- await create([targetBranch, '--no-refresh']);
67
+ await create([target.requestedRef, '--no-refresh']);
65
68
  }
66
69
  });
67
70
  const message = ready.path === 'healthy'
@@ -111,6 +111,8 @@ export function loadConfig({ platformFn = platform, writeStderr = (chunk) => pro
111
111
  worktreeBase: hostJoin(home, '.agent-infra', 'worktrees', project),
112
112
  shareBase: hostJoin(home, '.agent-infra', 'share', project),
113
113
  shellConfigBase: hostJoin(home, '.agent-infra', 'config', project),
114
+ workspaceViewBase: hostJoin(home, '.agent-infra', 'workspace-views'),
115
+ controlBase: hostJoin(home, '.agent-infra', 'sandbox-control'),
114
116
  dotfilesDir: hostJoin(home, '.agent-infra', 'dotfiles'),
115
117
  engine,
116
118
  runtimes,
@@ -69,6 +69,12 @@ export function sandboxLabel(config) {
69
69
  export function sandboxBranchLabel(config) {
70
70
  return `${sandboxLabel(config)}.branch`;
71
71
  }
72
+ export function sandboxWorkspaceModeLabel(config) {
73
+ return `${sandboxLabel(config)}.workspace-mode`;
74
+ }
75
+ export function sandboxTaskIdLabel(config) {
76
+ return `${sandboxLabel(config)}.task-id`;
77
+ }
72
78
  export function sandboxImageConfigLabel(config) {
73
79
  return `${sandboxLabel(config)}.image-config`;
74
80
  }
@@ -0,0 +1,58 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { SANDBOX_CONTROL_MAX_BYTES, isSandboxControlFamily } from './protocol.js';
5
+ function sleep(milliseconds) {
6
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
7
+ }
8
+ function exchangeSandboxControl(request, params) {
9
+ const channelDir = params.channelDir ?? process.env.AGENT_INFRA_CONTROL_DIR ?? '/run/agent-infra/control';
10
+ const encoded = `${JSON.stringify(request)}\n`;
11
+ if (Buffer.byteLength(encoded, 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
12
+ throw new Error('SANDBOX_CONTROL_REQUEST_TOO_LARGE: request exceeds the control limit');
13
+ }
14
+ const requestsDir = path.join(channelDir, 'requests');
15
+ const responsesDir = path.join(channelDir, 'responses');
16
+ const temporary = path.join(requestsDir, `.${request.id}.tmp`);
17
+ const requestPath = path.join(requestsDir, `${request.id}.json`);
18
+ const responsePath = path.join(responsesDir, `${request.id}.json`);
19
+ fs.writeFileSync(temporary, encoded, { mode: 0o600, flag: 'wx' });
20
+ fs.renameSync(temporary, requestPath);
21
+ const deadline = Date.now() + (params.timeoutMs ?? 30_000);
22
+ while (Date.now() < deadline) {
23
+ if (fs.existsSync(responsePath)) {
24
+ const raw = fs.readFileSync(responsePath, 'utf8');
25
+ fs.unlinkSync(responsePath);
26
+ const response = JSON.parse(raw);
27
+ if (response.version !== 1 || response.id !== request.id || !Number.isInteger(response.exitCode)) {
28
+ throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID: broker response is invalid');
29
+ }
30
+ return response;
31
+ }
32
+ sleep(25);
33
+ }
34
+ throw new Error(`SANDBOX_CONTROL_TIMEOUT: broker did not respond within ${params.timeoutMs ?? 30_000}ms`);
35
+ }
36
+ export function requestSandboxControl(params) {
37
+ if (!isSandboxControlFamily(params.family)) {
38
+ throw new Error(`SANDBOX_CONTROL_COMMAND_DENIED: '${params.family}' is not allowed`);
39
+ }
40
+ if (params.family === 'task-create')
41
+ throw new Error("SANDBOX_CONTROL_COMMAND_DENIED: 'task-create' requires a typed candidate");
42
+ const token = params.token ?? process.env.AGENT_INFRA_CONTROL_TOKEN;
43
+ if (!token)
44
+ throw new Error('SANDBOX_CONTROL_UNAVAILABLE: control token is missing');
45
+ const id = randomUUID();
46
+ const request = { version: 1, id, token, family: params.family, args: params.args };
47
+ return exchangeSandboxControl(request, params);
48
+ }
49
+ export function requestSandboxTaskCreate(params) {
50
+ const token = params.token ?? process.env.AGENT_INFRA_CONTROL_TOKEN;
51
+ if (!token)
52
+ throw new Error('SANDBOX_CONTROL_UNAVAILABLE: control token is missing');
53
+ const request = {
54
+ version: 1, id: randomUUID(), token, family: 'task-create', candidate: params.candidate
55
+ };
56
+ return exchangeSandboxControl(request, { ...params, timeoutMs: params.timeoutMs ?? 120_000 });
57
+ }
58
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1,54 @@
1
+ import { validateTaskCreateCandidate } from '../../task/create.js';
2
+ export const SANDBOX_CONTROL_MAX_BYTES = 64 * 1024;
3
+ export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-orchestration', 'task-create'];
4
+ export function isSandboxControlFamily(value) {
5
+ return SANDBOX_CONTROL_FAMILIES.includes(value);
6
+ }
7
+ export function validateSandboxControlRequest(value, manifest) {
8
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
9
+ throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request must be an object');
10
+ }
11
+ const request = value;
12
+ if (request.version !== 1
13
+ || typeof request.id !== 'string'
14
+ || !/^[a-f0-9-]{16,64}$/.test(request.id)
15
+ || request.token !== manifest.token
16
+ || typeof request.family !== 'string'
17
+ || !isSandboxControlFamily(request.family)) {
18
+ throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request schema or authorization is invalid');
19
+ }
20
+ if (Buffer.byteLength(JSON.stringify(request), 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
21
+ throw new Error('SANDBOX_CONTROL_REQUEST_TOO_LARGE: request exceeds the control limit');
22
+ }
23
+ if (request.family === 'task-create') {
24
+ const keys = Object.keys(request).sort().join(',');
25
+ if (keys !== ['candidate', 'family', 'id', 'token', 'version'].sort().join(',')) {
26
+ throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request schema or authorization is invalid');
27
+ }
28
+ return { ...request, candidate: validateTaskCreateCandidate(request.candidate) };
29
+ }
30
+ const keys = Object.keys(request).sort().join(',');
31
+ if (keys !== ['args', 'family', 'id', 'token', 'version'].sort().join(',')
32
+ || !Array.isArray(request.args)
33
+ || !request.args.every((arg) => typeof arg === 'string')) {
34
+ throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request schema or authorization is invalid');
35
+ }
36
+ if (manifest.mode !== 'task-bound' || !manifest.taskId) {
37
+ throw new Error('SANDBOX_CONTROL_BRANCH_ONLY: branch-only sandboxes cannot coordinate tasks');
38
+ }
39
+ if (request.family === 'task-orchestration'
40
+ && request.args.some((arg) => arg === '--git-worktree-root' || arg.startsWith('--git-worktree-root='))) {
41
+ throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: worktree binding is reserved for the control broker');
42
+ }
43
+ return request;
44
+ }
45
+ export function bindSandboxControlTask(request, taskId) {
46
+ if (request.family === 'task-create') {
47
+ throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: task-create requests do not bind a current task');
48
+ }
49
+ if (request.args.length === 0) {
50
+ throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: command arguments are required');
51
+ }
52
+ return [taskId, ...request.args.slice(1)];
53
+ }
54
+ //# sourceMappingURL=protocol.js.map