@fitlab-ai/agent-infra 0.9.3 → 0.9.4

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 (175) hide show
  1. package/README.md +2 -0
  2. package/README.zh-CN.md +2 -0
  3. package/bin/cli.ts +12 -0
  4. package/dist/bin/cli.js +14 -0
  5. package/dist/bin/internal-cli.js +134 -123
  6. package/dist/lib/agent-clients/adapter.js +92 -2
  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/opencode-sandbox.js +108 -0
  12. package/dist/lib/agent-clients/adapters/opencode.js +15 -3
  13. package/dist/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
  14. package/dist/lib/agent-clients/registry.js +9 -1
  15. package/dist/lib/internal/sandbox-control.js +24 -0
  16. package/dist/lib/internal/task-activity.js +45 -13
  17. package/dist/lib/internal/task-ledger.js +12 -3
  18. package/dist/lib/internal/task-lifecycle.js +19 -1
  19. package/dist/lib/internal/task-orchestration.js +122 -18
  20. package/dist/lib/platform/github-client.js +19 -2
  21. package/dist/lib/platform/pr-checks.js +1 -1
  22. package/dist/lib/platform/pull-requests.js +47 -1
  23. package/dist/lib/platform/releases.js +13 -5
  24. package/dist/lib/process-data/index.js +239 -0
  25. package/dist/lib/process-data/normalize.js +198 -0
  26. package/dist/lib/process-data/privacy.js +33 -0
  27. package/dist/lib/process-data/reconcile.js +95 -0
  28. package/dist/lib/process-data/repair.js +58 -0
  29. package/dist/lib/process-data/sources.js +345 -0
  30. package/dist/lib/process-data/store.js +282 -0
  31. package/dist/lib/process-data/types.js +2 -0
  32. package/dist/lib/run/index.js +4 -3
  33. package/dist/lib/run/tui.js +7 -8
  34. package/dist/lib/sandbox/agent-client-reconciler.js +26 -2
  35. package/dist/lib/sandbox/capture.js +7 -2
  36. package/dist/lib/sandbox/commands/create.js +109 -228
  37. package/dist/lib/sandbox/commands/enter.js +15 -47
  38. package/dist/lib/sandbox/commands/list-running.js +16 -6
  39. package/dist/lib/sandbox/commands/ls.js +10 -6
  40. package/dist/lib/sandbox/commands/rebuild.js +4 -3
  41. package/dist/lib/sandbox/commands/refresh.js +4 -84
  42. package/dist/lib/sandbox/commands/rm.js +24 -3
  43. package/dist/lib/sandbox/commands/show.js +12 -3
  44. package/dist/lib/sandbox/commands/start.js +7 -4
  45. package/dist/lib/sandbox/config.js +2 -0
  46. package/dist/lib/sandbox/constants.js +6 -0
  47. package/dist/lib/sandbox/control/client.js +44 -0
  48. package/dist/lib/sandbox/control/protocol.js +35 -0
  49. package/dist/lib/sandbox/control/server.js +109 -0
  50. package/dist/lib/sandbox/dockerfile.js +36 -13
  51. package/dist/lib/sandbox/mounts.js +17 -4
  52. package/dist/lib/sandbox/recovery.js +128 -5
  53. package/dist/lib/sandbox/redaction.js +12 -0
  54. package/dist/lib/sandbox/runtimes/base.dockerfile +24 -74
  55. package/dist/lib/sandbox/workspace-identity.js +95 -0
  56. package/dist/lib/sandbox/workspace-view.js +103 -0
  57. package/dist/lib/task/activity-intent.js +257 -51
  58. package/dist/lib/task/commit-finalization.js +267 -0
  59. package/dist/lib/task/commit-intent.js +165 -0
  60. package/dist/lib/task/decision-intents.js +31 -17
  61. package/dist/lib/task/delegation-receipts.js +2 -2
  62. package/dist/lib/task/implementation-inputs.js +54 -7
  63. package/dist/lib/task/ledger-intents.js +35 -1
  64. package/dist/lib/task/orchestration.js +363 -28
  65. package/dist/lib/task/resolve-ref.js +41 -1
  66. package/lib/agent-clients/adapter.ts +126 -2
  67. package/lib/{sandbox/credentials.ts → agent-clients/adapters/claude-code-credentials.ts} +3 -18
  68. package/lib/agent-clients/adapters/claude-code-refresh.ts +128 -0
  69. package/lib/agent-clients/adapters/claude-code-sandbox.ts +295 -0
  70. package/lib/agent-clients/adapters/claude-code.ts +21 -3
  71. package/lib/agent-clients/adapters/opencode-sandbox.ts +136 -0
  72. package/lib/agent-clients/adapters/opencode.ts +18 -3
  73. package/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
  74. package/lib/agent-clients/registry.ts +9 -1
  75. package/lib/internal/sandbox-control.ts +24 -0
  76. package/lib/internal/task-activity.ts +50 -14
  77. package/lib/internal/task-ledger.ts +12 -3
  78. package/lib/internal/task-lifecycle.ts +22 -1
  79. package/lib/internal/task-orchestration.ts +81 -11
  80. package/lib/platform/github-client.ts +20 -2
  81. package/lib/platform/pr-checks.ts +1 -1
  82. package/lib/platform/pull-requests.ts +49 -1
  83. package/lib/platform/releases.ts +16 -4
  84. package/lib/process-data/index.ts +224 -0
  85. package/lib/process-data/normalize.ts +202 -0
  86. package/lib/process-data/privacy.ts +41 -0
  87. package/lib/process-data/reconcile.ts +110 -0
  88. package/lib/process-data/repair.ts +64 -0
  89. package/lib/process-data/sources.ts +354 -0
  90. package/lib/process-data/store.ts +315 -0
  91. package/lib/process-data/types.ts +135 -0
  92. package/lib/run/index.ts +5 -3
  93. package/lib/run/tui.ts +11 -8
  94. package/lib/sandbox/agent-client-reconciler.ts +44 -2
  95. package/lib/sandbox/capture.ts +9 -2
  96. package/lib/sandbox/commands/create.ts +126 -260
  97. package/lib/sandbox/commands/enter.ts +13 -59
  98. package/lib/sandbox/commands/list-running.ts +23 -6
  99. package/lib/sandbox/commands/ls.ts +22 -6
  100. package/lib/sandbox/commands/rebuild.ts +4 -3
  101. package/lib/sandbox/commands/refresh.ts +4 -126
  102. package/lib/sandbox/commands/rm.ts +24 -3
  103. package/lib/sandbox/commands/show.ts +29 -3
  104. package/lib/sandbox/commands/start.ts +6 -4
  105. package/lib/sandbox/config.ts +4 -0
  106. package/lib/sandbox/constants.ts +8 -0
  107. package/lib/sandbox/control/client.ts +55 -0
  108. package/lib/sandbox/control/protocol.ts +75 -0
  109. package/lib/sandbox/control/server.ts +117 -0
  110. package/lib/sandbox/dockerfile.ts +66 -13
  111. package/lib/sandbox/mounts.ts +24 -4
  112. package/lib/sandbox/recovery.ts +163 -6
  113. package/lib/sandbox/redaction.ts +14 -0
  114. package/lib/sandbox/runtimes/base.dockerfile +24 -74
  115. package/lib/sandbox/tool-types.ts +16 -0
  116. package/lib/sandbox/workspace-identity.ts +123 -0
  117. package/lib/sandbox/workspace-view.ts +143 -0
  118. package/lib/task/activity-intent.ts +350 -61
  119. package/lib/task/commit-finalization.ts +356 -0
  120. package/lib/task/commit-intent.ts +230 -0
  121. package/lib/task/decision-intents.ts +32 -12
  122. package/lib/task/delegation-receipts.ts +2 -2
  123. package/lib/task/implementation-inputs.ts +62 -8
  124. package/lib/task/ledger-intents.ts +43 -3
  125. package/lib/task/orchestration.ts +501 -33
  126. package/lib/task/resolve-ref.ts +46 -1
  127. package/package.json +1 -1
  128. package/templates/.agents/rules/README.en.md +1 -1
  129. package/templates/.agents/rules/README.zh-CN.md +1 -1
  130. package/templates/.agents/rules/human-decision-context.en.md +11 -2
  131. package/templates/.agents/rules/human-decision-context.zh-CN.md +11 -2
  132. package/templates/.agents/rules/pr-checks-commands.en.md +1 -1
  133. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  134. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +2 -2
  135. package/templates/.agents/rules/pr-checks-commands.zh-CN.md +1 -1
  136. package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
  137. package/templates/.agents/rules/task-management.en.md +2 -0
  138. package/templates/.agents/rules/task-management.zh-CN.md +2 -0
  139. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  140. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
  141. package/templates/.agents/skills/commit/SKILL.en.md +21 -11
  142. package/templates/.agents/skills/commit/SKILL.zh-CN.md +27 -11
  143. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +49 -0
  144. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +55 -0
  145. package/templates/.agents/skills/commit/reference/task-status-update.en.md +4 -4
  146. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +4 -4
  147. package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
  148. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
  149. package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
  150. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
  151. package/templates/.agents/skills/create-task/reference/context-capture.en.md +34 -0
  152. package/templates/.agents/skills/create-task/reference/context-capture.zh-CN.md +34 -0
  153. package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
  154. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
  155. package/templates/.agents/skills/review-code/reference/output-templates.en.md +1 -1
  156. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +1 -1
  157. package/templates/.agents/skills/review-pr/SKILL.en.md +36 -8
  158. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +36 -8
  159. package/templates/.agents/skills/review-pr/reference/report-template.en.md +1 -1
  160. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +1 -1
  161. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +67 -50
  162. package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
  163. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
  164. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +2 -2
  165. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +2 -2
  166. package/templates/.agents/templates/task.en.md +1 -1
  167. package/templates/.agents/templates/task.zh-CN.md +1 -1
  168. package/templates/.claude/commands/watch-pr.en.md +1 -1
  169. package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
  170. package/templates/.claude/rules/tool-preferences.en.md +20 -0
  171. package/templates/.claude/rules/tool-preferences.zh-CN.md +20 -0
  172. package/templates/.opencode/commands/watch-pr.en.md +1 -1
  173. package/templates/.opencode/commands/watch-pr.zh-CN.md +1 -1
  174. package/templates/.gemini/commands/_project_/review-pr.en.toml +0 -10
  175. package/templates/.gemini/commands/_project_/review-pr.zh-CN.toml +0 -10
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
  import { runSafeEngine, runVerboseEngine } from '../shell.ts';
5
5
  import { resolveTaskBranch } from '../task-resolver.ts';
6
6
  import { isRemovedHashShortIdInput } from '../../task/short-id.ts';
7
+ import { parseSandboxWorkspaceIdentity } from '../workspace-identity.ts';
7
8
 
8
9
  export type SandboxRow = {
9
10
  name: string;
@@ -11,6 +12,8 @@ export type SandboxRow = {
11
12
  branch: string;
12
13
  running: boolean;
13
14
  index: number | null;
15
+ workspaceMode?: 'task-bound' | 'branch-only' | 'legacy-invalid';
16
+ taskId?: string | null;
14
17
  };
15
18
 
16
19
  export function containerListFormat(): string {
@@ -36,19 +39,32 @@ export function parseLabels(csv: string): Record<string, string> {
36
39
  return labels;
37
40
  }
38
41
 
39
- export function parseSandboxRows(rawOutput: string, branchKey: string): SandboxRow[] {
42
+ export function parseSandboxRows(
43
+ rawOutput: string,
44
+ branchKey: string,
45
+ workspaceKeys?: Readonly<{ mode: string; taskId: string }>
46
+ ): SandboxRow[] {
40
47
  if (!rawOutput) {
41
48
  return [];
42
49
  }
43
50
  return rawOutput.split('\n').map((line) => {
44
51
  const [name = '', status = '', labelsCsv = ''] = line.split('\t');
45
- const branch = parseLabels(labelsCsv)[branchKey] ?? '';
52
+ const labels = parseLabels(labelsCsv);
53
+ const workspace = workspaceKeys
54
+ ? parseSandboxWorkspaceIdentity(labels, workspaceKeys)
55
+ : null;
46
56
  return {
47
57
  name,
48
58
  status,
49
- branch,
59
+ branch: labels[branchKey] ?? '',
50
60
  running: status.startsWith('Up '),
51
- index: null
61
+ index: null,
62
+ ...(workspace
63
+ ? {
64
+ workspaceMode: workspace.mode,
65
+ taskId: workspace.mode === 'task-bound' ? workspace.taskId : null
66
+ }
67
+ : {})
52
68
  };
53
69
  });
54
70
  }
@@ -76,7 +92,8 @@ export function sortAndIndexSandboxRows(rows: SandboxRow[]): {
76
92
  export function fetchSandboxRows(
77
93
  engine: string,
78
94
  label: string,
79
- branchKey: string
95
+ branchKey: string,
96
+ workspaceKeys?: Readonly<{ mode: string; taskId: string }>
80
97
  ): { running: SandboxRow[]; nonRunning: SandboxRow[] } {
81
98
  const raw = runSafeEngine(engine, 'docker', [
82
99
  'ps',
@@ -86,7 +103,7 @@ export function fetchSandboxRows(
86
103
  '--format',
87
104
  containerListFormat()
88
105
  ]);
89
- return sortAndIndexSandboxRows(parseSandboxRows(raw, branchKey));
106
+ return sortAndIndexSandboxRows(parseSandboxRows(raw, branchKey, workspaceKeys));
90
107
  }
91
108
 
92
109
  /**
@@ -1,10 +1,15 @@
1
1
  import * as p from '@clack/prompts';
2
2
  import pc from 'picocolors';
3
3
  import { loadConfig } from '../config.ts';
4
- import { sandboxBranchLabel, sandboxLabel } from '../constants.ts';
4
+ import {
5
+ sandboxBranchLabel,
6
+ sandboxLabel,
7
+ sandboxTaskIdLabel,
8
+ sandboxWorkspaceModeLabel
9
+ } from '../constants.ts';
5
10
  import { detectEngine } from '../engine.ts';
6
11
  import { formatTable } from '../../table.ts';
7
- import { lookupShortIdByBranch } from '../../task/short-id.ts';
12
+ import { loadShortIdByTaskId } from '../../task/short-id.ts';
8
13
  import { fetchSandboxRows } from './list-running.ts';
9
14
 
10
15
  export { containerListFormat, parseLabels } from './list-running.ts';
@@ -22,20 +27,22 @@ to remove with "ai sandbox rm <branch>".
22
27
  Use "ai sandbox show <ref>" for a single sandbox's worktree and per-tool
23
28
  state paths.`;
24
29
 
25
- const CONTAINER_TABLE_HEADERS = ['#', 'SHORT', 'NAMES', 'STATUS', 'BRANCH'] as const;
30
+ const CONTAINER_TABLE_HEADERS = ['#', 'SHORT', 'NAMES', 'STATUS', 'WORKSPACE', 'TASK', 'BRANCH'] as const;
26
31
 
27
32
  type ContainerTableRow = {
28
33
  row: string;
29
34
  shortId: string;
30
35
  name: string;
31
36
  status: string;
37
+ workspace: string;
38
+ taskId: string;
32
39
  branch: string;
33
40
  };
34
41
 
35
42
  export function formatContainerTable(rows: ContainerTableRow[], zebra = false): string[] {
36
43
  return formatTable(
37
44
  CONTAINER_TABLE_HEADERS,
38
- rows.map((r) => [r.row, r.shortId, r.name, r.status, r.branch]),
45
+ rows.map((r) => [r.row, r.shortId, r.name, r.status, r.workspace, r.taskId, r.branch]),
39
46
  { zebra }
40
47
  );
41
48
  }
@@ -49,7 +56,12 @@ export function ls(args: string[] = []): void {
49
56
  const config = loadConfig();
50
57
  const engine = detectEngine(config);
51
58
  const label = sandboxLabel(config);
52
- const { running, nonRunning } = fetchSandboxRows(engine, label, sandboxBranchLabel(config));
59
+ const { running, nonRunning } = fetchSandboxRows(
60
+ engine,
61
+ label,
62
+ sandboxBranchLabel(config),
63
+ { mode: sandboxWorkspaceModeLabel(config), taskId: sandboxTaskIdLabel(config) }
64
+ );
53
65
 
54
66
  p.intro(pc.cyan(`Sandbox status for ${config.project}`));
55
67
 
@@ -59,12 +71,16 @@ export function ls(args: string[] = []): void {
59
71
  p.log.warn(' No sandbox containers');
60
72
  } else {
61
73
  const tableRows: ContainerTableRow[] = ordered.map((container, i) => {
62
- const shortId = container.branch ? lookupShortIdByBranch(container.branch, config.repoRoot) : null;
74
+ const shortId = container.taskId
75
+ ? loadShortIdByTaskId(config.repoRoot).get(container.taskId) ?? null
76
+ : null;
63
77
  return {
64
78
  row: String(i + 1),
65
79
  shortId: shortId ?? '-',
66
80
  name: container.name,
67
81
  status: container.status,
82
+ workspace: container.workspaceMode ?? 'legacy-invalid',
83
+ taskId: container.taskId ?? '-',
68
84
  branch: container.branch
69
85
  };
70
86
  });
@@ -8,8 +8,8 @@ import { sandboxImageRefreshLabel } from '../constants.ts';
8
8
  import { detectEngine, ensureDocker } from '../engine.ts';
9
9
  import { runEngine, runOkEngine, runSafeEngine, runVerboseEngine } from '../shell.ts';
10
10
  import { pruneSandboxDanglingImages } from '../image-prune.ts';
11
- import { resolveTools } from '../tools.ts';
12
11
  import type { SandboxTool } from '../tools.ts';
12
+ import { createSandboxCapabilityPlan } from '../agent-client-reconciler.ts';
13
13
  import {
14
14
  buildImageSignature,
15
15
  buildSandboxImageArgs,
@@ -97,8 +97,9 @@ export async function rebuild(args: string[]): Promise<void> {
97
97
  }
98
98
 
99
99
  const config = loadConfig();
100
- const tools = resolveTools(config);
101
- const preparedDockerfile = prepareDockerfile(config);
100
+ const capabilityPlan = createSandboxCapabilityPlan(config);
101
+ const tools = [...capabilityPlan.tools];
102
+ const preparedDockerfile = prepareDockerfile(config, capabilityPlan.image);
102
103
  const imageSignature = buildImageSignature(preparedDockerfile, tools);
103
104
  const quiet = values.quiet ?? false;
104
105
  const refresh = values.refresh ?? false;
@@ -1,128 +1,6 @@
1
- import { homedir } from 'node:os';
2
- import { parseArgs } from 'node:util';
3
- import {
4
- buildLockedGuidance,
5
- discoverProjects,
6
- formatCredentialWarnings,
7
- formatRemaining,
8
- reconcileClaudeCredentials,
9
- redactCommandError,
10
- validateClaudeCredentialsEnvOverride
11
- } from '../credentials.ts';
12
- import { runProbe } from '../shell.ts';
1
+ import { runClaudeCodeRefresh } from '../../agent-clients/adapters/claude-code-refresh.ts';
13
2
 
14
- const USAGE = 'Usage: ai sandbox refresh';
3
+ const refresh = runClaudeCodeRefresh;
15
4
 
16
- type ReconcileOptions = NonNullable<Parameters<typeof reconcileClaudeCredentials>[1]>;
17
-
18
- type RefreshDeps = Partial<ReconcileOptions> & {
19
- spawnFn?: typeof runProbe;
20
- discoverFn?: typeof discoverProjects;
21
- writeStdout?: (chunk: string) => unknown;
22
- writeStderr?: (chunk: string) => unknown;
23
- };
24
-
25
- export function probeClaudeStatus(spawnFn: typeof runProbe = runProbe): { ok: boolean; stderr: string; error: string | null } {
26
- const result = spawnFn('claude', ['/status'], {
27
- encoding: 'utf8',
28
- stdio: ['ignore', 'pipe', 'pipe'],
29
- timeout: 30_000
30
- });
31
- return {
32
- ok: result.status === 0,
33
- stderr: result.stderr?.toString() ?? '',
34
- error: result.error?.message ?? null
35
- };
36
- }
37
-
38
- export async function refresh(args: string[], deps: RefreshDeps = {}): Promise<number> {
39
- const {
40
- spawnFn = runProbe,
41
- execFn,
42
- readFn,
43
- existsFn,
44
- writeFn,
45
- writeHostFn,
46
- discoverFn = discoverProjects,
47
- writeStdout = (chunk: string) => process.stdout.write(chunk),
48
- writeStderr = (chunk: string) => process.stderr.write(chunk)
49
- } = deps;
50
-
51
- if (args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
52
- writeStdout(`${USAGE}\n`);
53
- return 0;
54
- }
55
-
56
- const { positionals } = parseArgs({ args, allowPositionals: true, strict: true });
57
- if (positionals.length > 0) {
58
- throw new Error(USAGE);
59
- }
60
- validateClaudeCredentialsEnvOverride();
61
-
62
- const home = homedir();
63
- if (!home) {
64
- throw new Error('sandbox: home directory is required');
65
- }
66
-
67
- const projects = discoverFn(home);
68
- if (projects.length === 0) {
69
- writeStdout('No project credentials to refresh.\n');
70
- return 0;
71
- }
72
-
73
- const reconcileOptions = { execFn, readFn, existsFn, writeFn, writeHostFn, projects };
74
- let result = reconcileClaudeCredentials(home, reconcileOptions);
75
- if (result.status === 'STALE_ACCESS' && result.authoritative === null) {
76
- writeStdout('Host credentials appear stale; probing claude /status to trigger refresh...\n');
77
- const probe = probeClaudeStatus(spawnFn);
78
- if (!probe.ok) {
79
- writeStderr(`Probe failed: ${redactCommandError(probe.stderr || probe.error || 'unknown error')}\n`);
80
- writeStderr('Run "claude /login" on the host to renew credentials.\n');
81
- return 1;
82
- }
83
- writeStdout('Probe succeeded; re-inspecting host credentials.\n');
84
- result = reconcileClaudeCredentials(home, reconcileOptions);
85
- }
86
-
87
- if (result.status === 'MISSING') {
88
- writeStderr('No Claude Code credentials found on host.\n');
89
- writeStderr('Run "claude /login" on the host to authenticate.\n');
90
- return 1;
91
- }
92
-
93
- if (result.status === 'KEYCHAIN_LOCKED') {
94
- writeStderr(`${buildLockedGuidance()}\n`);
95
- return 1;
96
- }
97
-
98
- if (result.status === 'KEYCHAIN_ERROR') {
99
- writeStderr(`Host keychain error: ${redactCommandError(result.detail || 'unknown error')}\n`);
100
- writeStderr(`${buildLockedGuidance()}\n`);
101
- return 1;
102
- }
103
-
104
- if (result.status === 'KEYCHAIN_WRITE_FAILED') {
105
- writeStderr(`[host] keychain write failed: ${formatCredentialWarnings(result.warnings) || 'unknown error'}\n`);
106
- return 1;
107
- }
108
-
109
- if (result.status !== 'OK') {
110
- writeStderr('Host credentials still invalid after probe; run "claude /login".\n');
111
- return 1;
112
- }
113
-
114
- if (result.authoritative && result.authoritative !== 'host' && result.hostWritten) {
115
- writeStdout(`[host] reconciled from ${result.authoritative}\n`);
116
- }
117
-
118
- for (const project of projects) {
119
- const action = result.filesWritten.includes(project) ? 'updated' : 'unchanged';
120
- writeStdout(`[${project}] ${action}; expires in ${formatRemaining(result.expiresAt)}\n`);
121
- }
122
-
123
- for (const failure of result.fileErrors) {
124
- writeStderr(`[${failure.project}] sync failed: ${failure.error}\n`);
125
- }
126
-
127
- return result.fileErrors.length > 0 ? 1 : 0;
128
- }
5
+ export { probeClaudeStatus, runClaudeCodeRefresh } from '../../agent-clients/adapters/claude-code-refresh.ts';
6
+ export { refresh };
@@ -18,7 +18,7 @@ import { ENGINES, detectEngine, engineDisplayName, isManagedEngine, stopManagedV
18
18
  import { pruneSandboxDanglingImages } from '../image-prune.ts';
19
19
  import { removeManagedDir, removeWorktreeDir } from '../managed-fs.ts';
20
20
  import { runOk, runSafe, runSafeEngine } from '../shell.ts';
21
- import { resolveTaskBranch } from '../task-resolver.ts';
21
+ import { resolveSandboxTarget } from '../workspace-identity.ts';
22
22
  import { toolConfigDirCandidates, toolProjectDirCandidates } from '../tools.ts';
23
23
  import { createSandboxCapabilityPlan } from '../agent-client-reconciler.ts';
24
24
  import type { SandboxTool } from '../tools.ts';
@@ -81,6 +81,16 @@ async function rmOne(
81
81
  for (const name of matchedContainers) {
82
82
  runSafeEngine(engine, 'docker', ['stop', name]);
83
83
  runSafeEngine(engine, 'docker', ['rm', name]);
84
+ for (const [base, label] of [
85
+ [path.join(config.workspaceViewBase, config.project), 'Workspace view'],
86
+ [path.join(config.controlBase, config.project), 'Control channel']
87
+ ] as const) {
88
+ const directory = path.join(base, name);
89
+ if (fs.existsSync(directory)) {
90
+ removeManagedDir(base, directory);
91
+ if (!options.quiet) p.log.success(`${label} removed: ${directory}`);
92
+ }
93
+ }
84
94
  }
85
95
  spinner.stop(pc.green(`Removed container(s): ${matchedContainers.join(', ')}`));
86
96
  } else {
@@ -224,6 +234,17 @@ async function rmPurge(config: SandboxConfig, tools: SandboxTool[]): Promise<voi
224
234
  }
225
235
  }
226
236
 
237
+ for (const [base, label] of [
238
+ [config.workspaceViewBase, 'workspace views'],
239
+ [config.controlBase, 'control channels']
240
+ ] as const) {
241
+ const projectDir = path.join(base, config.project);
242
+ if (fs.existsSync(projectDir)) {
243
+ removeManagedDir(base, projectDir);
244
+ p.log.success(`Removed project ${label}: ${projectDir}`);
245
+ }
246
+ }
247
+
227
248
  const shouldRemoveImage = await p.confirm({
228
249
  message: `Remove image ${config.imageName}?`,
229
250
  initialValue: false
@@ -369,6 +390,6 @@ export async function rm(args: string[]): Promise<void> {
369
390
  return;
370
391
  }
371
392
 
372
- const branch = resolveTaskBranch(positionals[0] ?? '', config.repoRoot);
373
- await rmOne(config, tools, branch);
393
+ const target = resolveSandboxTarget(positionals[0] ?? '', config.repoRoot);
394
+ await rmOne(config, tools, target.branch);
374
395
  }
@@ -3,9 +3,19 @@ import * as p from '@clack/prompts';
3
3
  import pc from 'picocolors';
4
4
  import type { SandboxConfig } from '../config.ts';
5
5
  import { loadConfig } from '../config.ts';
6
- import { assertValidBranchName, worktreeDirCandidates } from '../constants.ts';
7
- import { resolveBranchArg } from './list-running.ts';
6
+ import {
7
+ assertValidBranchName,
8
+ containerNameCandidates,
9
+ sandboxBranchLabel,
10
+ sandboxLabel,
11
+ sandboxTaskIdLabel,
12
+ sandboxWorkspaceModeLabel,
13
+ worktreeDirCandidates
14
+ } from '../constants.ts';
15
+ import { fetchSandboxRows, selectSandboxContainer } from './list-running.ts';
8
16
  import { resolveTools, toolConfigDirCandidates } from '../tools.ts';
17
+ import { detectEngine } from '../engine.ts';
18
+ import { resolveSandboxTarget } from '../workspace-identity.ts';
9
19
 
10
20
  const USAGE = `Usage: ai sandbox show <branch | TASK-id | N>
11
21
 
@@ -51,10 +61,22 @@ export function show(args: string[] = []): void {
51
61
  }
52
62
 
53
63
  const config = loadConfig();
54
- const branch = resolveBranchArg(args[0]!, { repoRoot: config.repoRoot });
64
+ const target = resolveSandboxTarget(args[0]!, config.repoRoot);
65
+ const branch = target.branch;
55
66
  assertValidBranchName(branch);
56
67
 
57
68
  const detail = collectSandboxDetail(config, branch);
69
+ const engine = detectEngine(config);
70
+ const rows = fetchSandboxRows(
71
+ engine,
72
+ sandboxLabel(config),
73
+ sandboxBranchLabel(config),
74
+ { mode: sandboxWorkspaceModeLabel(config), taskId: sandboxTaskIdLabel(config) }
75
+ );
76
+ const container = selectSandboxContainer(
77
+ [...rows.running, ...rows.nonRunning],
78
+ containerNameCandidates(config, branch)
79
+ );
58
80
 
59
81
  p.intro(pc.cyan(`Sandbox detail for ${config.project} · ${branch}`));
60
82
 
@@ -67,6 +89,10 @@ export function show(args: string[] = []): void {
67
89
  }
68
90
  }
69
91
 
92
+ p.log.step('Workspace identity');
93
+ process.stdout.write(` Mode: ${container?.workspaceMode ?? 'legacy-invalid'}\n`);
94
+ process.stdout.write(` Task: ${container?.taskId ?? '-'}\n`);
95
+
70
96
  for (const tool of detail.toolStates) {
71
97
  p.log.step(`${tool.name} state`);
72
98
  if (tool.entries.length === 0) {
@@ -9,9 +9,9 @@ import { detectEngine } from '../engine.ts';
9
9
  import { ensureSandboxReady } from '../recovery.ts';
10
10
  import {
11
11
  fetchSandboxRows,
12
- resolveBranchArg,
13
12
  selectSandboxContainer,
14
13
  } from './list-running.ts';
14
+ import { resolveSandboxTarget } from '../workspace-identity.ts';
15
15
 
16
16
  const USAGE = `Usage: ai sandbox start [--recreate] <branch | TASK-id | N>
17
17
 
@@ -56,7 +56,8 @@ export async function start(args: string[]): Promise<void> {
56
56
 
57
57
  const config = loadConfig();
58
58
  const engine = detectEngine(config);
59
- const branch = resolveBranchArg(parsed.target, { repoRoot: config.repoRoot });
59
+ const target = resolveSandboxTarget(parsed.target, config.repoRoot);
60
+ const branch = target.branch;
60
61
  assertValidBranchName(branch);
61
62
 
62
63
  const { running, nonRunning } = fetchSandboxRows(
@@ -79,11 +80,12 @@ export async function start(args: string[]): Promise<void> {
79
80
  config,
80
81
  engine,
81
82
  branch,
83
+ workspace: target.workspace,
82
84
  row: found,
83
85
  allowRecreate: parsed.recreate,
84
- recreate: async (targetBranch) => {
86
+ recreate: async () => {
85
87
  const { create } = await import('./create.ts');
86
- await create([targetBranch, '--no-refresh']);
88
+ await create([target.requestedRef, '--no-refresh']);
87
89
  }
88
90
  });
89
91
  const message = ready.path === 'healthy'
@@ -55,6 +55,8 @@ export type SandboxConfig = {
55
55
  worktreeBase: string;
56
56
  shareBase: string;
57
57
  shellConfigBase: string;
58
+ workspaceViewBase: string;
59
+ controlBase: string;
58
60
  dotfilesDir: string;
59
61
  engine: string | null;
60
62
  runtimes: string[];
@@ -188,6 +190,8 @@ export function loadConfig({
188
190
  worktreeBase: hostJoin(home, '.agent-infra', 'worktrees', project),
189
191
  shareBase: hostJoin(home, '.agent-infra', 'share', project),
190
192
  shellConfigBase: hostJoin(home, '.agent-infra', 'config', project),
193
+ workspaceViewBase: hostJoin(home, '.agent-infra', 'workspace-views'),
194
+ controlBase: hostJoin(home, '.agent-infra', 'sandbox-control'),
191
195
  dotfilesDir: hostJoin(home, '.agent-infra', 'dotfiles'),
192
196
  engine,
193
197
  runtimes,
@@ -103,6 +103,14 @@ export function sandboxBranchLabel(config: Pick<SandboxPathConfig, 'project'>):
103
103
  return `${sandboxLabel(config)}.branch`;
104
104
  }
105
105
 
106
+ export function sandboxWorkspaceModeLabel(config: Pick<SandboxPathConfig, 'project'>): string {
107
+ return `${sandboxLabel(config)}.workspace-mode`;
108
+ }
109
+
110
+ export function sandboxTaskIdLabel(config: Pick<SandboxPathConfig, 'project'>): string {
111
+ return `${sandboxLabel(config)}.task-id`;
112
+ }
113
+
106
114
  export function sandboxImageConfigLabel(config: Pick<SandboxPathConfig, 'project'>): string {
107
115
  return `${sandboxLabel(config)}.image-config`;
108
116
  }
@@ -0,0 +1,55 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import {
5
+ SANDBOX_CONTROL_MAX_BYTES,
6
+ isSandboxControlFamily,
7
+ type SandboxControlRequest,
8
+ type SandboxControlResponse
9
+ } from './protocol.ts';
10
+
11
+ function sleep(milliseconds: number): void {
12
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
13
+ }
14
+
15
+ export function requestSandboxControl(params: Readonly<{
16
+ family: string;
17
+ args: string[];
18
+ channelDir?: string;
19
+ token?: string;
20
+ timeoutMs?: number;
21
+ }>): SandboxControlResponse {
22
+ if (!isSandboxControlFamily(params.family)) {
23
+ throw new Error(`SANDBOX_CONTROL_COMMAND_DENIED: '${params.family}' is not allowed`);
24
+ }
25
+ const channelDir = params.channelDir ?? process.env.AGENT_INFRA_CONTROL_DIR ?? '/run/agent-infra/control';
26
+ const token = params.token ?? process.env.AGENT_INFRA_CONTROL_TOKEN;
27
+ if (!token) throw new Error('SANDBOX_CONTROL_UNAVAILABLE: control token is missing');
28
+ const requestsDir = path.join(channelDir, 'requests');
29
+ const responsesDir = path.join(channelDir, 'responses');
30
+ const id = randomUUID();
31
+ const request: SandboxControlRequest = { version: 1, id, token, family: params.family, args: params.args };
32
+ const encoded = `${JSON.stringify(request)}\n`;
33
+ if (Buffer.byteLength(encoded, 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
34
+ throw new Error('SANDBOX_CONTROL_REQUEST_TOO_LARGE: request exceeds the control limit');
35
+ }
36
+ const temporary = path.join(requestsDir, `.${id}.tmp`);
37
+ const requestPath = path.join(requestsDir, `${id}.json`);
38
+ const responsePath = path.join(responsesDir, `${id}.json`);
39
+ fs.writeFileSync(temporary, encoded, { mode: 0o600, flag: 'wx' });
40
+ fs.renameSync(temporary, requestPath);
41
+ const deadline = Date.now() + (params.timeoutMs ?? 30_000);
42
+ while (Date.now() < deadline) {
43
+ if (fs.existsSync(responsePath)) {
44
+ const raw = fs.readFileSync(responsePath, 'utf8');
45
+ fs.unlinkSync(responsePath);
46
+ const response = JSON.parse(raw) as SandboxControlResponse;
47
+ if (response.version !== 1 || response.id !== id || !Number.isInteger(response.exitCode)) {
48
+ throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID: broker response is invalid');
49
+ }
50
+ return response;
51
+ }
52
+ sleep(25);
53
+ }
54
+ throw new Error(`SANDBOX_CONTROL_TIMEOUT: broker did not respond within ${params.timeoutMs ?? 30_000}ms`);
55
+ }
@@ -0,0 +1,75 @@
1
+ export const SANDBOX_CONTROL_MAX_BYTES = 64 * 1024;
2
+ export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-orchestration'] as const;
3
+
4
+ export type SandboxControlFamily = typeof SANDBOX_CONTROL_FAMILIES[number];
5
+
6
+ export type SandboxControlManifest = Readonly<{
7
+ version: 1;
8
+ repoRoot: string;
9
+ project: string;
10
+ container: string;
11
+ branch: string;
12
+ mode: 'task-bound' | 'branch-only';
13
+ taskId: string | null;
14
+ token: string;
15
+ channelDir: string;
16
+ }>;
17
+
18
+ export type SandboxControlRequest = Readonly<{
19
+ version: 1;
20
+ id: string;
21
+ token: string;
22
+ family: SandboxControlFamily;
23
+ args: string[];
24
+ }>;
25
+
26
+ export type SandboxControlResponse = Readonly<{
27
+ version: 1;
28
+ id: string;
29
+ exitCode: number;
30
+ stdout: string;
31
+ stderr: string;
32
+ }>;
33
+
34
+ export function isSandboxControlFamily(value: string): value is SandboxControlFamily {
35
+ return SANDBOX_CONTROL_FAMILIES.includes(value as SandboxControlFamily);
36
+ }
37
+
38
+ export function validateSandboxControlRequest(
39
+ value: unknown,
40
+ manifest: SandboxControlManifest
41
+ ): SandboxControlRequest {
42
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
43
+ throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request must be an object');
44
+ }
45
+ const request = value as Partial<SandboxControlRequest>;
46
+ if (
47
+ request.version !== 1
48
+ || typeof request.id !== 'string'
49
+ || !/^[a-f0-9-]{16,64}$/.test(request.id)
50
+ || request.token !== manifest.token
51
+ || typeof request.family !== 'string'
52
+ || !isSandboxControlFamily(request.family)
53
+ || !Array.isArray(request.args)
54
+ || !request.args.every((arg) => typeof arg === 'string')
55
+ ) {
56
+ throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request schema or authorization is invalid');
57
+ }
58
+ if (manifest.mode !== 'task-bound' || !manifest.taskId) {
59
+ throw new Error('SANDBOX_CONTROL_BRANCH_ONLY: branch-only sandboxes cannot coordinate tasks');
60
+ }
61
+ if (Buffer.byteLength(JSON.stringify(request), 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
62
+ throw new Error('SANDBOX_CONTROL_REQUEST_TOO_LARGE: request exceeds the control limit');
63
+ }
64
+ return request as SandboxControlRequest;
65
+ }
66
+
67
+ export function bindSandboxControlTask(
68
+ request: SandboxControlRequest,
69
+ taskId: string
70
+ ): string[] {
71
+ if (request.args.length === 0) {
72
+ throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: command arguments are required');
73
+ }
74
+ return [taskId, ...request.args.slice(1)];
75
+ }