@fitlab-ai/agent-infra 0.6.4 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/README.md +63 -27
  2. package/README.zh-CN.md +61 -25
  3. package/bin/cli.ts +18 -6
  4. package/dist/bin/cli.js +20 -6
  5. package/dist/lib/cp.js +127 -0
  6. package/dist/lib/defaults.json +1 -0
  7. package/dist/lib/init.js +3 -0
  8. package/dist/lib/sandbox/clipboard/bridge.js +23 -4
  9. package/dist/lib/sandbox/clipboard/index.js +12 -3
  10. package/dist/lib/sandbox/commands/create.js +11 -2
  11. package/dist/lib/sandbox/commands/enter.js +29 -6
  12. package/dist/lib/sandbox/commands/list-running.js +108 -0
  13. package/dist/lib/sandbox/commands/ls.js +24 -45
  14. package/dist/lib/sandbox/commands/rebuild.js +15 -7
  15. package/dist/lib/sandbox/config.js +3 -0
  16. package/dist/lib/sandbox/index.js +6 -4
  17. package/dist/lib/sandbox/readme-scaffold.js +148 -0
  18. package/dist/lib/sandbox/runtimes/ai-tools.dockerfile +12 -6
  19. package/dist/lib/sandbox/runtimes/base.dockerfile +3 -3
  20. package/dist/lib/sandbox/tools.js +213 -8
  21. package/dist/lib/update.js +12 -1
  22. package/lib/cp.ts +177 -0
  23. package/lib/defaults.json +1 -0
  24. package/lib/init.ts +10 -0
  25. package/lib/sandbox/clipboard/bridge.ts +23 -4
  26. package/lib/sandbox/clipboard/index.ts +12 -3
  27. package/lib/sandbox/commands/create.ts +18 -2
  28. package/lib/sandbox/commands/enter.ts +48 -6
  29. package/lib/sandbox/commands/list-running.ts +135 -0
  30. package/lib/sandbox/commands/ls.ts +28 -49
  31. package/lib/sandbox/commands/rebuild.ts +24 -7
  32. package/lib/sandbox/config.ts +7 -0
  33. package/lib/sandbox/index.ts +6 -4
  34. package/lib/sandbox/readme-scaffold.ts +177 -0
  35. package/lib/sandbox/runtimes/ai-tools.dockerfile +12 -6
  36. package/lib/sandbox/runtimes/base.dockerfile +3 -3
  37. package/lib/sandbox/tools.ts +248 -9
  38. package/lib/update.ts +15 -1
  39. package/package.json +1 -1
  40. package/templates/.agents/QUICKSTART.en.md +1 -1
  41. package/templates/.agents/QUICKSTART.zh-CN.md +1 -1
  42. package/templates/.agents/README.en.md +79 -2
  43. package/templates/.agents/README.zh-CN.md +79 -2
  44. package/templates/.agents/rules/create-issue.en.md +1 -1
  45. package/templates/.agents/rules/create-issue.github.en.md +1 -1
  46. package/templates/.agents/rules/create-issue.github.zh-CN.md +1 -1
  47. package/templates/.agents/rules/create-issue.zh-CN.md +1 -1
  48. package/templates/.agents/rules/issue-sync.github.en.md +6 -5
  49. package/templates/.agents/rules/issue-sync.github.zh-CN.md +6 -5
  50. package/templates/.agents/rules/milestone-inference.github.en.md +2 -2
  51. package/templates/.agents/rules/milestone-inference.github.zh-CN.md +2 -2
  52. package/templates/.agents/rules/no-mid-flow-questions.en.md +57 -0
  53. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +57 -0
  54. package/templates/.agents/rules/pr-sync.github.en.md +4 -5
  55. package/templates/.agents/rules/pr-sync.github.zh-CN.md +4 -5
  56. package/templates/.agents/rules/task-management.en.md +9 -6
  57. package/templates/.agents/rules/task-management.zh-CN.md +9 -6
  58. package/templates/.agents/rules/testing-discipline.en.md +2 -2
  59. package/templates/.agents/rules/testing-discipline.zh-CN.md +2 -2
  60. package/templates/.agents/scripts/validate-artifact.js +1 -1
  61. package/templates/.agents/skills/analyze-task/SKILL.en.md +16 -4
  62. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +16 -4
  63. package/templates/.agents/skills/check-task/SKILL.en.md +43 -32
  64. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +42 -31
  65. package/templates/.agents/skills/code-task/SKILL.en.md +117 -0
  66. package/templates/.agents/skills/{implement-task → code-task}/SKILL.zh-CN.md +51 -24
  67. package/templates/.agents/skills/{implement-task → code-task}/config/verify.en.json +4 -4
  68. package/templates/.agents/skills/{implement-task → code-task}/config/verify.zh-CN.json +4 -4
  69. package/templates/.agents/skills/{implement-task → code-task}/reference/branch-management.zh-CN.md +2 -2
  70. package/templates/.agents/skills/{implement-task/reference/implementation-rules.en.md → code-task/reference/code-rules.en.md} +6 -6
  71. package/templates/.agents/skills/{implement-task/reference/implementation-rules.zh-CN.md → code-task/reference/code-rules.zh-CN.md} +3 -3
  72. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +69 -0
  73. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +69 -0
  74. package/templates/.agents/skills/{refine-task/reference/fix-workflow.en.md → code-task/reference/fix-mode.en.md} +12 -12
  75. package/templates/.agents/skills/{refine-task/reference/fix-workflow.zh-CN.md → code-task/reference/fix-mode.zh-CN.md} +8 -8
  76. package/templates/.agents/skills/code-task/reference/output-template.en.md +20 -0
  77. package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +20 -0
  78. package/templates/.agents/skills/{implement-task → code-task}/reference/report-template.en.md +4 -4
  79. package/templates/.agents/skills/{implement-task → code-task}/reference/report-template.zh-CN.md +3 -3
  80. package/templates/.agents/skills/code-task/scripts/detect-mode.js +370 -0
  81. package/templates/.agents/skills/commit/SKILL.en.md +2 -2
  82. package/templates/.agents/skills/commit/SKILL.zh-CN.md +2 -2
  83. package/templates/.agents/skills/commit/reference/task-status-update.en.md +10 -6
  84. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +10 -6
  85. package/templates/.agents/skills/complete-task/SKILL.en.md +5 -3
  86. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +5 -3
  87. package/templates/.agents/skills/create-pr/SKILL.en.md +17 -1
  88. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +17 -1
  89. package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
  90. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  91. package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -2
  92. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +2 -2
  93. package/templates/.agents/skills/import-issue/SKILL.en.md +3 -3
  94. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +3 -3
  95. package/templates/.agents/skills/plan-task/SKILL.en.md +4 -4
  96. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -4
  97. package/templates/.agents/skills/restore-task/SKILL.en.md +4 -3
  98. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +4 -3
  99. package/templates/.agents/skills/review-analysis/SKILL.en.md +76 -0
  100. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +102 -0
  101. package/templates/.agents/skills/review-analysis/config/verify.en.json +51 -0
  102. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +51 -0
  103. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +87 -0
  104. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +87 -0
  105. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +90 -0
  106. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +91 -0
  107. package/templates/.agents/skills/review-analysis/reference/review-criteria.en.md +47 -0
  108. package/templates/.agents/skills/review-analysis/reference/review-criteria.zh-CN.md +47 -0
  109. package/templates/.agents/skills/{review-task → review-code}/SKILL.en.md +11 -9
  110. package/templates/.agents/skills/{review-task → review-code}/SKILL.zh-CN.md +15 -9
  111. package/templates/.agents/skills/{review-task → review-code}/config/verify.en.json +7 -5
  112. package/templates/.agents/skills/{review-task → review-code}/config/verify.zh-CN.json +6 -4
  113. package/templates/.agents/skills/{review-task → review-code}/reference/output-templates.en.md +21 -17
  114. package/templates/.agents/skills/{review-task → review-code}/reference/output-templates.zh-CN.md +19 -15
  115. package/templates/.agents/skills/{review-task → review-code}/reference/report-template.en.md +5 -6
  116. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +91 -0
  117. package/templates/.agents/skills/review-code/reference/review-criteria.en.md +48 -0
  118. package/templates/.agents/skills/{review-task → review-code}/reference/review-criteria.zh-CN.md +10 -4
  119. package/templates/.agents/skills/review-plan/SKILL.en.md +76 -0
  120. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +102 -0
  121. package/templates/.agents/skills/{refine-task → review-plan}/config/verify.en.json +14 -10
  122. package/templates/.agents/skills/{refine-task → review-plan}/config/verify.zh-CN.json +14 -10
  123. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +87 -0
  124. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +87 -0
  125. package/templates/.agents/skills/review-plan/reference/report-template.en.md +90 -0
  126. package/templates/.agents/skills/{review-task → review-plan}/reference/report-template.zh-CN.md +3 -3
  127. package/templates/.agents/skills/review-plan/reference/review-criteria.en.md +47 -0
  128. package/templates/.agents/skills/review-plan/reference/review-criteria.zh-CN.md +47 -0
  129. package/templates/.agents/skills/test/SKILL.en.md +2 -2
  130. package/templates/.agents/skills/test/SKILL.zh-CN.md +13 -31
  131. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +1 -0
  132. package/templates/.agents/templates/task.en.md +3 -3
  133. package/templates/.agents/templates/task.zh-CN.md +2 -2
  134. package/templates/.agents/workflows/bug-fix.en.yaml +126 -80
  135. package/templates/.agents/workflows/bug-fix.zh-CN.yaml +90 -44
  136. package/templates/.agents/workflows/feature-development.en.yaml +115 -70
  137. package/templates/.agents/workflows/feature-development.zh-CN.yaml +92 -47
  138. package/templates/.agents/workflows/refactoring.en.yaml +123 -78
  139. package/templates/.agents/workflows/refactoring.zh-CN.yaml +89 -44
  140. package/templates/.claude/commands/code-task.en.md +8 -0
  141. package/templates/.claude/commands/code-task.zh-CN.md +8 -0
  142. package/templates/.claude/commands/review-analysis.en.md +8 -0
  143. package/templates/.claude/commands/review-analysis.zh-CN.md +8 -0
  144. package/templates/.claude/commands/review-code.en.md +8 -0
  145. package/templates/.claude/commands/review-code.zh-CN.md +8 -0
  146. package/templates/.claude/commands/review-plan.en.md +8 -0
  147. package/templates/.claude/commands/review-plan.zh-CN.md +8 -0
  148. package/templates/.gemini/commands/_project_/archive-tasks.zh-CN.toml +1 -1
  149. package/templates/.gemini/commands/_project_/code-task.en.toml +8 -0
  150. package/templates/.gemini/commands/_project_/code-task.zh-CN.toml +8 -0
  151. package/templates/.gemini/commands/_project_/init-labels.zh-CN.toml +1 -1
  152. package/templates/.gemini/commands/_project_/init-milestones.zh-CN.toml +1 -1
  153. package/templates/.gemini/commands/_project_/review-analysis.en.toml +8 -0
  154. package/templates/.gemini/commands/_project_/review-analysis.zh-CN.toml +8 -0
  155. package/templates/.gemini/commands/_project_/review-code.en.toml +8 -0
  156. package/templates/.gemini/commands/_project_/review-code.zh-CN.toml +8 -0
  157. package/templates/.gemini/commands/_project_/review-plan.en.toml +8 -0
  158. package/templates/.gemini/commands/_project_/review-plan.zh-CN.toml +8 -0
  159. package/templates/.opencode/commands/code-task.en.md +11 -0
  160. package/templates/.opencode/commands/code-task.zh-CN.md +11 -0
  161. package/templates/.opencode/commands/review-analysis.en.md +11 -0
  162. package/templates/.opencode/commands/review-analysis.zh-CN.md +11 -0
  163. package/templates/.opencode/commands/review-code.en.md +11 -0
  164. package/templates/.opencode/commands/review-code.zh-CN.md +11 -0
  165. package/templates/.opencode/commands/review-plan.en.md +11 -0
  166. package/templates/.opencode/commands/review-plan.zh-CN.md +11 -0
  167. package/templates/.agents/skills/implement-task/SKILL.en.md +0 -173
  168. package/templates/.agents/skills/implement-task/reference/output-template.en.md +0 -20
  169. package/templates/.agents/skills/implement-task/reference/output-template.zh-CN.md +0 -20
  170. package/templates/.agents/skills/refine-task/SKILL.en.md +0 -153
  171. package/templates/.agents/skills/refine-task/SKILL.zh-CN.md +0 -153
  172. package/templates/.agents/skills/refine-task/reference/report-template.en.md +0 -64
  173. package/templates/.agents/skills/refine-task/reference/report-template.zh-CN.md +0 -64
  174. package/templates/.agents/skills/review-task/reference/review-criteria.en.md +0 -42
  175. package/templates/.claude/commands/implement-task.en.md +0 -8
  176. package/templates/.claude/commands/implement-task.zh-CN.md +0 -8
  177. package/templates/.claude/commands/refine-task.en.md +0 -8
  178. package/templates/.claude/commands/refine-task.zh-CN.md +0 -8
  179. package/templates/.claude/commands/review-task.en.md +0 -8
  180. package/templates/.claude/commands/review-task.zh-CN.md +0 -8
  181. package/templates/.gemini/commands/_project_/implement-task.en.toml +0 -8
  182. package/templates/.gemini/commands/_project_/implement-task.zh-CN.toml +0 -8
  183. package/templates/.gemini/commands/_project_/refine-task.en.toml +0 -8
  184. package/templates/.gemini/commands/_project_/refine-task.zh-CN.toml +0 -8
  185. package/templates/.gemini/commands/_project_/review-task.en.toml +0 -8
  186. package/templates/.gemini/commands/_project_/review-task.zh-CN.toml +0 -8
  187. package/templates/.opencode/commands/implement-task.en.md +0 -11
  188. package/templates/.opencode/commands/implement-task.zh-CN.md +0 -11
  189. package/templates/.opencode/commands/refine-task.en.md +0 -11
  190. package/templates/.opencode/commands/refine-task.zh-CN.md +0 -11
  191. package/templates/.opencode/commands/review-task.en.md +0 -11
  192. package/templates/.opencode/commands/review-task.zh-CN.md +0 -11
  193. /package/templates/.agents/skills/{implement-task → code-task}/reference/branch-management.en.md +0 -0
package/dist/lib/cp.js ADDED
@@ -0,0 +1,127 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import { platform as currentPlatform, tmpdir as defaultTmpdir } from 'node:os';
5
+ import path from 'node:path';
6
+ import { parseArgs } from 'node:util';
7
+ import { createClipboardAdapter } from "./sandbox/clipboard/index.js";
8
+ const USAGE = 'Usage: ai cp <ssh-alias>\n\nCopy the local clipboard image (PNG) to a remote macOS NSPasteboard over ssh/scp.\n';
9
+ const COMMAND_TIMEOUT_MS = 30_000;
10
+ export function runCommand(cmd, args, input) {
11
+ const result = spawnSync(cmd, args, {
12
+ input,
13
+ encoding: 'utf8',
14
+ stdio: ['pipe', 'pipe', 'pipe'],
15
+ timeout: COMMAND_TIMEOUT_MS
16
+ });
17
+ return {
18
+ status: result.status,
19
+ stdout: result.stdout ?? '',
20
+ stderr: result.stderr ?? '',
21
+ error: result.error
22
+ };
23
+ }
24
+ export async function cmdCp(args, deps = {}) {
25
+ const { platform = currentPlatform(), createAdapter = createClipboardAdapter, spawnFn = runCommand, randomId = randomUUID, mkdtempFn = fs.mkdtempSync, writeFileFn = fs.writeFileSync, rmFn = fs.rmSync, tmpdir = defaultTmpdir, writeStdout = (chunk) => process.stdout.write(chunk), writeStderr = (chunk) => process.stderr.write(chunk) } = deps;
26
+ if (args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
27
+ writeStdout(USAGE);
28
+ return 0;
29
+ }
30
+ let positionals;
31
+ try {
32
+ ({ positionals } = parseArgs({ args, allowPositionals: true, strict: true }));
33
+ }
34
+ catch {
35
+ writeStderr(USAGE);
36
+ return 1;
37
+ }
38
+ const alias = positionals[0];
39
+ if (!alias || positionals.length !== 1) {
40
+ writeStderr(USAGE);
41
+ return 1;
42
+ }
43
+ if (alias.startsWith('-')) {
44
+ writeStderr(`invalid ssh alias '${alias}': must not start with '-'\n`);
45
+ return 1;
46
+ }
47
+ if (platform !== 'darwin') {
48
+ writeStderr(`ai cp currently supports macOS senders only (got ${platform})\n`);
49
+ return 1;
50
+ }
51
+ const adapter = createAdapter({ platformName: platform });
52
+ const png = adapter?.readImagePng() ?? null;
53
+ if (png === null) {
54
+ writeStderr('no image on clipboard\n');
55
+ return 1;
56
+ }
57
+ let uploaded = false;
58
+ let localTmpDir = null;
59
+ let remotePath = null;
60
+ try {
61
+ localTmpDir = mkdtempFn(path.join(tmpdir(), 'agent-infra-cp-'));
62
+ const localPng = path.join(localTmpDir, 'clipboard.png');
63
+ writeFileFn(localPng, png);
64
+ remotePath = `/tmp/agent-infra-cp-${randomId()}.png`;
65
+ const upload = spawnFn('scp', [
66
+ '-o',
67
+ 'BatchMode=yes',
68
+ '-o',
69
+ 'ConnectTimeout=10',
70
+ localPng,
71
+ `${alias}:${remotePath}`
72
+ ]);
73
+ if (upload.status !== 0) {
74
+ writeStderr(`failed to upload image to ${alias}:\n${commandDetail(upload)}\n`);
75
+ return 1;
76
+ }
77
+ uploaded = true;
78
+ // Remote write currently targets macOS only: it pipes an AppleScript to the
79
+ // remote `osascript` to set its NSPasteboard. This is the extension point for
80
+ // other remote platforms later (e.g. dispatch on remote OS to wl-copy/xclip
81
+ // on Linux); a non-macOS remote fails here with a clear non-zero error today.
82
+ const setRemote = spawnFn('ssh', [
83
+ '-o',
84
+ 'BatchMode=yes',
85
+ '-o',
86
+ 'ConnectTimeout=10',
87
+ alias,
88
+ 'osascript',
89
+ '-'
90
+ ], remoteSetScript(remotePath));
91
+ if (setRemote.status !== 0) {
92
+ writeStderr(`failed to set remote clipboard on ${alias}:\n${commandDetail(setRemote)}\n`);
93
+ return 1;
94
+ }
95
+ writeStdout(`copied clipboard image to ${alias}\n`);
96
+ return 0;
97
+ }
98
+ finally {
99
+ if (uploaded && remotePath) {
100
+ spawnFn('ssh', [
101
+ '-o',
102
+ 'BatchMode=yes',
103
+ '-o',
104
+ 'ConnectTimeout=10',
105
+ alias,
106
+ 'rm',
107
+ '-f',
108
+ remotePath
109
+ ]);
110
+ }
111
+ if (localTmpDir) {
112
+ rmFn(localTmpDir, { recursive: true, force: true });
113
+ }
114
+ }
115
+ }
116
+ function commandDetail(result) {
117
+ const detail = result.stderr || result.error?.message || result.stdout || 'unknown error';
118
+ return detail.trimEnd();
119
+ }
120
+ function remoteSetScript(remotePath) {
121
+ const escapedPath = remotePath.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
122
+ return [
123
+ `set theFile to POSIX file "${escapedPath}"`,
124
+ 'set the clipboard to (read theFile as «class PNGf»)'
125
+ ].join('\n');
126
+ }
127
+ //# sourceMappingURL=cp.js.map
@@ -2,6 +2,7 @@
2
2
  "platform": {
3
3
  "type": "github"
4
4
  },
5
+ "requiresPullRequest": true,
5
6
  "sandbox": {
6
7
  "engine": null,
7
8
  "runtimes": [
package/dist/lib/init.js CHANGED
@@ -156,6 +156,8 @@ async function cmdInit() {
156
156
  info(`Custom platform '${platformType}' selected. Built-in templates are only complete for github;`
157
157
  + ` provide matching '.${platformType}.' or generic templates before running update-agent-infra.`);
158
158
  }
159
+ const requiresPRChoice = await select('Require Pull Request flow?', ['yes', 'no'], 'yes');
160
+ const requiresPullRequest = requiresPRChoice !== 'no';
159
161
  const templateSources = parseLocalSources(await prompt('Template sources (optional, comma-separated local paths, e.g. ~/my-templates; Enter to skip)', ''));
160
162
  const skillSources = parseLocalSources(await prompt('Skill sources (optional, comma-separated local paths, e.g. ~/my-skills; Enter to skip)', ''));
161
163
  closePrompt();
@@ -199,6 +201,7 @@ async function cmdInit() {
199
201
  org: orgName,
200
202
  language,
201
203
  platform: { type: platformType },
204
+ requiresPullRequest,
202
205
  templateVersion: VERSION,
203
206
  sandbox: structuredClone(defaults.sandbox),
204
207
  labels: structuredClone(defaults.labels),
@@ -1,4 +1,5 @@
1
1
  import { StringDecoder } from 'node:string_decoder';
2
+ import { spawnSync } from 'node:child_process';
2
3
  import { createClipboardAdapter } from "./index.js";
3
4
  import { buildBracketedPaste, CtrlVDetector } from "./keys.js";
4
5
  import { clipboardHostDir, containerClipboardPath, pngClipboardFilename, pruneClipboardDir, writeClipboardPngAtomic } from "./paths.js";
@@ -6,20 +7,37 @@ import { commandForEngine, restoreTerminal, runInteractiveEngine, runOkEngine }
6
7
  import { loadNodePty } from "./node-pty.js";
7
8
  const FALLBACK_PREFIX = 'Warning: clipboard image paste bridge disabled';
8
9
  const PARTIAL_ESCAPE_FLUSH_MS = 30;
10
+ // Node's stdin.setRawMode(true) uses libuv's RAW mode, which (unlike the
11
+ // cfmakeraw that `docker exec -it` applies on the non-bridge path) keeps ONLCR
12
+ // set on the shared host TTY. With ONLCR on, the kernel rewrites the bare \n
13
+ // that tmux emits after homing the cursor inside the right pane into \r\n,
14
+ // snapping the cursor to column 1 so the following erase/redraw wipes the left
15
+ // pane. Clearing OPOST brings the host TTY in line with the non-bridge path.
16
+ // Best-effort: setRawMode(false) on teardown restores the original termios, and
17
+ // a missing/failed stty only reinstates the redraw glitch.
18
+ function disableOutputPostProcessing(stdin) {
19
+ const candidate = stdin.fd;
20
+ if (typeof candidate !== 'number') {
21
+ return;
22
+ }
23
+ try {
24
+ spawnSync('stty', ['-opost'], { stdio: [candidate, 'ignore', 'ignore'] });
25
+ }
26
+ catch {
27
+ // stty unavailable or fd is not a tty; leave the terminal as-is.
28
+ }
29
+ }
9
30
  export async function runInteractiveWithClipboardBridge(options) {
10
31
  const { engine, dockerArgs, container, home, cwd = process.cwd(), env = process.env, platformName = process.platform, adapter = createClipboardAdapter({ platformName }), loadPty = loadNodePty, runInteractive = runInteractiveEngine, runOk = runOkEngine, writeStderr = (chunk) => process.stderr.write(chunk), stdin = process.stdin, stdout = process.stdout, createDetector = () => new CtrlVDetector() } = options;
11
32
  function fallback(reason) {
12
33
  writeStderr(`${FALLBACK_PREFIX}: ${reason}\n`);
13
34
  return runInteractive(engine, 'docker', dockerArgs);
14
35
  }
15
- if (platformName !== 'darwin') {
16
- return runInteractive(engine, 'docker', dockerArgs);
17
- }
18
36
  if (!stdin.isTTY || !stdout.isTTY) {
19
37
  return fallback('host stdin/stdout is not a TTY');
20
38
  }
21
39
  if (!adapter) {
22
- return fallback('macOS clipboard adapter is unavailable');
40
+ return fallback('no clipboard adapter available on this platform');
23
41
  }
24
42
  const available = adapter.available();
25
43
  if (!available.ok) {
@@ -121,6 +139,7 @@ async function runBridge({ child, home, adapter, writeStderr, stdin, stdout, det
121
139
  }
122
140
  try {
123
141
  stdin.setRawMode?.(true);
142
+ disableOutputPostProcessing(stdin);
124
143
  stdin.resume();
125
144
  stdin.on('data', onData);
126
145
  stdout.on('resize', onResize);
@@ -1,9 +1,18 @@
1
1
  import { platform } from 'node:os';
2
2
  import { createDarwinClipboardAdapter } from "./darwin.js";
3
3
  export function createClipboardAdapter({ platformName = platform() } = {}) {
4
- if (platformName !== 'darwin') {
5
- return null;
4
+ switch (platformName) {
5
+ case 'darwin':
6
+ return createDarwinClipboardAdapter();
7
+ case 'linux':
8
+ // Future work: dispatch based on $WAYLAND_DISPLAY (wl-paste) or $DISPLAY (xclip);
9
+ // see Issue #386 follow-up. Returning null disables the bridge for now.
10
+ return null;
11
+ case 'win32':
12
+ // Future work: native Win32 clipboard reader. Returning null disables the bridge.
13
+ return null;
14
+ default:
15
+ return null;
6
16
  }
7
- return createDarwinClipboardAdapter();
8
17
  }
9
18
  //# sourceMappingURL=index.js.map
@@ -13,12 +13,13 @@ import { prepareDockerfile } from "../dockerfile.js";
13
13
  import { detectEngine, ensureDocker } from "../engine.js";
14
14
  import { commandForEngine, execEngine, run, runEngine, runOk, runOkEngine, runSafe, runSafeEngine, runVerboseEngine } from "../shell.js";
15
15
  import { resolveTaskBranch } from "../task-resolver.js";
16
- import { resolveTools, toolConfigDirCandidates, toolNpmPackagesArg } from "../tools.js";
16
+ import { imageSignatureFields, resolveTools, toolConfigDirCandidates, toolNpmPackagesArg, toolShellInstallScriptBase64 } from "../tools.js";
17
17
  import { hostJoin, toEnginePath, volumeArg } from "../engines/wsl2-paths.js";
18
18
  import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from "../clipboard/paths.js";
19
19
  import { validateSelinuxDisableEnv } from "../engines/selinux.js";
20
20
  import { resolveBuildUid } from "../engines/native.js";
21
21
  import { dotfilesCacheDir, materializeDotfiles } from "../dotfiles.js";
22
+ import { ensureSandboxDiscoveryReadmes } from "../readme-scaffold.js";
22
23
  import { prepareClaudeCredentials, redactCommandError, validateClaudeCredentialsEnvOverride } from "../credentials.js";
23
24
  import { detectHostTimezone } from "../host-timezone.js";
24
25
  const OPENCODE_YOLO_PERMISSION = '{"*":"allow","read":"allow","bash":"allow","edit":"allow","webfetch":"allow","external_directory":"allow","doom_loop":"allow"}';
@@ -57,7 +58,7 @@ function buildSignature(preparedDockerfile, tools) {
57
58
  return createHash('sha256')
58
59
  .update(JSON.stringify({
59
60
  dockerfile: preparedDockerfile.signature,
60
- tools: tools.map((tool) => tool.npmPackage)
61
+ tools: imageSignatureFields(tools)
61
62
  }))
62
63
  .digest('hex')
63
64
  .slice(0, 12);
@@ -788,6 +789,8 @@ export function buildImage(config, tools, dockerfilePath, imageSignature, { engi
788
789
  `HOST_GID=${hostGid}`,
789
790
  '--build-arg',
790
791
  `AI_TOOL_PACKAGES=${toolNpmPackagesArg(tools)}`,
792
+ '--build-arg',
793
+ `AI_TOOLS_SHELL_INSTALL_B64=${toolShellInstallScriptBase64(tools)}`,
791
794
  '--label',
792
795
  sandboxLabel(config),
793
796
  '--label',
@@ -981,6 +984,12 @@ export async function create(args) {
981
984
  if (aliasesFile.created) {
982
985
  message(`Created default sandbox aliases at ${aliasesFile.path}`);
983
986
  }
987
+ const readmeResults = ensureSandboxDiscoveryReadmes(effectiveConfig, branch);
988
+ for (const { created, path: readmePath } of readmeResults) {
989
+ if (created) {
990
+ message(`Created discovery README at ${readmePath}`);
991
+ }
992
+ }
984
993
  const gitconfigPath = path.join(effectiveConfig.home, '.gitconfig');
985
994
  const gitconfigContent = fs.existsSync(gitconfigPath)
986
995
  ? fs.readFileSync(gitconfigPath, 'utf8')
@@ -1,5 +1,5 @@
1
1
  import { loadConfig } from "../config.js";
2
- import { assertValidBranchName, containerNameCandidates } from "../constants.js";
2
+ import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel } from "../constants.js";
3
3
  import { detectEngine } from "../engine.js";
4
4
  import { formatCredentialWarnings, formatRemaining, reconcileClaudeCredentials, redactCommandError, validateClaudeCredentialsEnvOverride } from "../credentials.js";
5
5
  import { runInteractiveEngine, runSafeEngine } from "../shell.js";
@@ -7,7 +7,11 @@ import { resolveTaskBranch } from "../task-resolver.js";
7
7
  import { dotfilesCacheDir, materializeDotfiles } from "../dotfiles.js";
8
8
  import { runInteractiveWithClipboardBridge } from "../clipboard/bridge.js";
9
9
  import { detectHostTimezone } from "../host-timezone.js";
10
- const USAGE = `Usage: ai sandbox exec <branch> [cmd...]`;
10
+ import { fetchSandboxRows, isTaskShortRef, resolveTaskShortRef } from "./list-running.js";
11
+ const USAGE = `Usage: ai sandbox exec <branch | TASK-id | '#N'> [cmd...]
12
+
13
+ '#N' references the N-th running sandbox in 'ai sandbox ls' order (1-based).
14
+ Quote it as '#N' to avoid shell '#' comment handling.`;
11
15
  const TMUX_ENTRY_PATH = '/usr/local/bin/sandbox-tmux-entry';
12
16
  // Terminal-detection variables that interactive TUIs (e.g. claude-code)
13
17
  // inspect to enable progressive enhancements such as the kitty keyboard
@@ -34,6 +38,17 @@ export function hostTimezoneEnvFlags(detect = detectHostTimezone) {
34
38
  const tz = detect();
35
39
  return tz ? ['-e', `TZ=${tz}`] : [];
36
40
  }
41
+ export function clipboardBridgeDisabled(env = process.env) {
42
+ const value = (env.AI_SANDBOX_NO_CLIPBOARD_BRIDGE ?? '').trim().toLowerCase();
43
+ return value === '1' || value === 'true' || value === 'yes';
44
+ }
45
+ export function runSandboxInteractive(params) {
46
+ const { engine, dockerArgs, container, home, env = process.env, runBridge = runInteractiveWithClipboardBridge, runInteractive = runInteractiveEngine } = params;
47
+ if (clipboardBridgeDisabled(env)) {
48
+ return runInteractive(engine, 'docker', dockerArgs);
49
+ }
50
+ return runBridge({ engine, dockerArgs, container, home });
51
+ }
37
52
  export function formatCredentialSyncStatus(result, isTTY = process.stderr.isTTY) {
38
53
  if (result.status === 'STALE_ACCESS') {
39
54
  return 'Warning: Claude Code credentials on host appear stale. Run "ai sandbox refresh" or "claude /login" to renew.\n';
@@ -68,8 +83,15 @@ export async function enter(args) {
68
83
  const config = loadConfig();
69
84
  validateClaudeCredentialsEnvOverride();
70
85
  const engine = detectEngine(config);
71
- const [branchOrTaskId = '', ...cmd] = args;
72
- const branch = resolveTaskBranch(branchOrTaskId, config.repoRoot);
86
+ const [firstArg = '', ...cmd] = args;
87
+ let branch;
88
+ if (isTaskShortRef(firstArg)) {
89
+ const { running } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
90
+ branch = resolveTaskShortRef(firstArg, { running });
91
+ }
92
+ else {
93
+ branch = resolveTaskBranch(firstArg, config.repoRoot);
94
+ }
73
95
  assertValidBranchName(branch);
74
96
  const running = runSafeEngine(engine, 'docker', ['ps', '--format', '{{.Names}}']).split('\n');
75
97
  const container = containerNameCandidates(config, branch).find((name) => running.includes(name));
@@ -97,9 +119,10 @@ export async function enter(args) {
97
119
  catch (error) {
98
120
  process.stderr.write(`Warning: dotfiles snapshot rebuild failed: ${redactCommandError(error instanceof Error ? error.message : 'unknown error')}\n`);
99
121
  }
100
- return runInteractiveWithClipboardBridge({
122
+ const dockerArgs = ['exec', '-it', ...envFlags, container, 'bash', TMUX_ENTRY_PATH];
123
+ return runSandboxInteractive({
101
124
  engine,
102
- dockerArgs: ['exec', '-it', ...envFlags, container, 'bash', TMUX_ENTRY_PATH],
125
+ dockerArgs,
103
126
  container,
104
127
  home: config.home
105
128
  });
@@ -0,0 +1,108 @@
1
+ import { runSafeEngine } from "../shell.js";
2
+ export function containerListFormat() {
3
+ return '{{.Names}}\t{{.Status}}\t{{.Labels}}';
4
+ }
5
+ export function parseLabels(csv) {
6
+ if (!csv) {
7
+ return {};
8
+ }
9
+ const labels = {};
10
+ for (const pair of csv.split(',')) {
11
+ if (!pair) {
12
+ continue;
13
+ }
14
+ const eq = pair.indexOf('=');
15
+ if (eq < 0) {
16
+ continue;
17
+ }
18
+ labels[pair.slice(0, eq)] = pair.slice(eq + 1);
19
+ }
20
+ return labels;
21
+ }
22
+ export function parseSandboxRows(rawOutput, branchKey) {
23
+ if (!rawOutput) {
24
+ return [];
25
+ }
26
+ return rawOutput.split('\n').map((line) => {
27
+ const [name = '', status = '', labelsCsv = ''] = line.split('\t');
28
+ const branch = parseLabels(labelsCsv)[branchKey] ?? '';
29
+ return {
30
+ name,
31
+ status,
32
+ branch,
33
+ running: status.startsWith('Up '),
34
+ index: null
35
+ };
36
+ });
37
+ }
38
+ export function sortAndIndexSandboxRows(rows) {
39
+ const byName = (a, b) => {
40
+ if (a.name < b.name)
41
+ return -1;
42
+ if (a.name > b.name)
43
+ return 1;
44
+ return 0;
45
+ };
46
+ const running = rows.filter((row) => row.running).sort(byName).map((row, i) => ({
47
+ ...row,
48
+ index: i + 1
49
+ }));
50
+ const nonRunning = rows.filter((row) => !row.running).sort(byName).map((row) => ({
51
+ ...row,
52
+ index: null
53
+ }));
54
+ return { running, nonRunning };
55
+ }
56
+ export function fetchSandboxRows(engine, label, branchKey) {
57
+ const raw = runSafeEngine(engine, 'docker', [
58
+ 'ps',
59
+ '-a',
60
+ '--filter',
61
+ `label=${label}`,
62
+ '--format',
63
+ containerListFormat()
64
+ ]);
65
+ return sortAndIndexSandboxRows(parseSandboxRows(raw, branchKey));
66
+ }
67
+ /**
68
+ * Returns true iff `arg` is a syntactically valid task short reference ('#N').
69
+ * Zero IO. Callers MUST use this as the gate before constructing any context
70
+ * for resolveTaskShortRef — that way non-matching arguments (e.g. '#abc',
71
+ * '#1.5', '#') never trigger sandbox list IO.
72
+ */
73
+ export function isTaskShortRef(arg) {
74
+ return /^#\d+$/.test(arg);
75
+ }
76
+ /**
77
+ * Resolve a task short reference ('#N') to a branch name.
78
+ *
79
+ * Current implementation: treats the digits as a 1-based index into the
80
+ * supplied running-sandbox list (ls view order). This is the *only*
81
+ * resolution path until the global task-short-id registry lands in a
82
+ * follow-up task; do NOT read task.md or scan .agents/workspace/ from this
83
+ * helper here.
84
+ *
85
+ * Precondition: callers MUST gate on isTaskShortRef(arg) === true before
86
+ * constructing ctx and calling this function. Throws when arg is a valid
87
+ * short ref but cannot be resolved (out of range, no running sandboxes,
88
+ * etc.); the caller surfaces the error to the user.
89
+ */
90
+ export function resolveTaskShortRef(arg, ctx) {
91
+ const n = Number(arg.slice(1));
92
+ if (n < 1) {
93
+ throw new Error(`Invalid sandbox index '${arg}': must be >= 1`);
94
+ }
95
+ const { running } = ctx;
96
+ if (running.length === 0) {
97
+ throw new Error(`No running sandbox to reference with '${arg}'`);
98
+ }
99
+ if (n > running.length) {
100
+ throw new Error(`No running sandbox at index '${arg}' (only ${running.length} running)`);
101
+ }
102
+ const row = running[n - 1];
103
+ if (!row.branch) {
104
+ throw new Error(`Cannot resolve branch for sandbox '${arg}' (container '${row.name}' missing branch label)`);
105
+ }
106
+ return row.branch;
107
+ }
108
+ //# sourceMappingURL=list-running.js.map
@@ -5,39 +5,24 @@ import pc from 'picocolors';
5
5
  import { loadConfig } from "../config.js";
6
6
  import { sandboxBranchLabel, sandboxLabel } from "../constants.js";
7
7
  import { detectEngine } from "../engine.js";
8
- import { runSafeEngine } from "../shell.js";
9
8
  import { resolveTools, toolProjectDirCandidates } from "../tools.js";
10
- const USAGE = 'Usage: ai sandbox ls';
11
- const CONTAINER_TABLE_HEADERS = ['NAMES', 'STATUS', 'BRANCH'];
12
- // Exported to lock the docker/podman-compatible format in unit tests.
13
- export function containerListFormat() {
14
- return '{{.Names}}\t{{.Status}}\t{{.Labels}}';
15
- }
16
- export function parseLabels(csv) {
17
- if (!csv) {
18
- return {};
19
- }
20
- const labels = {};
21
- for (const pair of csv.split(',')) {
22
- if (!pair) {
23
- continue;
24
- }
25
- const eq = pair.indexOf('=');
26
- if (eq < 0) {
27
- continue;
28
- }
29
- labels[pair.slice(0, eq)] = pair.slice(eq + 1);
30
- }
31
- return labels;
32
- }
9
+ import { fetchSandboxRows } from "./list-running.js";
10
+ export { containerListFormat, parseLabels } from "./list-running.js";
11
+ const USAGE = `Usage: ai sandbox ls
12
+
13
+ Lists all containers for the current project. The leftmost '#' column
14
+ numbers running sandboxes; use it as "ai sandbox exec '#N'" to enter one.
15
+ Quote '#N' to avoid shell '#' comment handling.`;
16
+ const CONTAINER_TABLE_HEADERS = ['#', 'NAMES', 'STATUS', 'BRANCH'];
33
17
  export function formatContainerTable(rows) {
34
- const columns = rows.map((row) => [row.name, row.status, row.branch]);
18
+ const columns = rows.map((row) => [row.index, row.name, row.status, row.branch]);
35
19
  const widths = [
36
- Math.max(CONTAINER_TABLE_HEADERS[0].length, ...rows.map((row) => row.name.length)),
37
- Math.max(CONTAINER_TABLE_HEADERS[1].length, ...rows.map((row) => row.status.length)),
38
- Math.max(CONTAINER_TABLE_HEADERS[2].length, ...rows.map((row) => row.branch.length))
20
+ Math.max(CONTAINER_TABLE_HEADERS[0].length, ...rows.map((row) => row.index.length)),
21
+ Math.max(CONTAINER_TABLE_HEADERS[1].length, ...rows.map((row) => row.name.length)),
22
+ Math.max(CONTAINER_TABLE_HEADERS[2].length, ...rows.map((row) => row.status.length)),
23
+ Math.max(CONTAINER_TABLE_HEADERS[3].length, ...rows.map((row) => row.branch.length))
39
24
  ];
40
- const renderRow = (values) => `${values[0].padEnd(widths[0])} ${values[1].padEnd(widths[1])} ${values[2]}`.trimEnd();
25
+ const renderRow = (values) => `${values[0].padEnd(widths[0])} ${values[1].padEnd(widths[1])} ${values[2].padEnd(widths[2])} ${values[3]}`.trimEnd();
41
26
  return [
42
27
  renderRow(CONTAINER_TABLE_HEADERS),
43
28
  ...columns.map((column) => renderRow(column))
@@ -58,27 +43,21 @@ export function ls(args = []) {
58
43
  const engine = detectEngine(config);
59
44
  const tools = resolveTools(config);
60
45
  const label = sandboxLabel(config);
61
- const containers = runSafeEngine(engine, 'docker', [
62
- 'ps',
63
- '-a',
64
- '--filter',
65
- `label=${label}`,
66
- '--format',
67
- containerListFormat()
68
- ]);
46
+ const { running, nonRunning } = fetchSandboxRows(engine, label, sandboxBranchLabel(config));
69
47
  p.intro(pc.cyan(`Sandbox status for ${config.project}`));
70
48
  p.log.step('Containers');
71
- if (!containers) {
49
+ const ordered = [...running, ...nonRunning];
50
+ if (ordered.length === 0) {
72
51
  p.log.warn(' No sandbox containers');
73
52
  }
74
53
  else {
75
- const branchKey = sandboxBranchLabel(config);
76
- const rows = containers.split('\n').map((line) => {
77
- const [name = '', status = '', labelsCsv = ''] = line.split('\t');
78
- const branch = parseLabels(labelsCsv)[branchKey] ?? '';
79
- return { name, status, branch };
80
- });
81
- for (const line of formatContainerTable(rows)) {
54
+ const tableRows = ordered.map((row) => ({
55
+ index: row.index === null ? '' : String(row.index),
56
+ name: row.name,
57
+ status: row.status,
58
+ branch: row.branch
59
+ }));
60
+ for (const line of formatContainerTable(tableRows)) {
82
61
  process.stdout.write(` ${line}\n`);
83
62
  }
84
63
  }
@@ -7,20 +7,20 @@ import { prepareDockerfile } from "../dockerfile.js";
7
7
  import { sandboxImageConfigLabel, sandboxLabel } from "../constants.js";
8
8
  import { detectEngine, ensureDocker } from "../engine.js";
9
9
  import { runEngine, runOkEngine, runSafeEngine, runVerboseEngine } from "../shell.js";
10
- import { resolveTools, toolNpmPackagesArg } from "../tools.js";
10
+ import { imageSignatureFields, resolveTools, toolNpmPackagesArg, toolShellInstallScriptBase64 } from "../tools.js";
11
11
  import { toEnginePath } from "../engines/wsl2-paths.js";
12
12
  import { resolveBuildUid } from "../engines/native.js";
13
- const USAGE = `Usage: ai sandbox rebuild [--quiet]`;
13
+ const USAGE = `Usage: ai sandbox rebuild [--quiet] [--refresh]`;
14
14
  function buildSignature(preparedDockerfile, tools) {
15
15
  return createHash('sha256')
16
16
  .update(JSON.stringify({
17
17
  dockerfile: preparedDockerfile.signature,
18
- tools: tools.map((tool) => tool.npmPackage)
18
+ tools: imageSignatureFields(tools)
19
19
  }))
20
20
  .digest('hex')
21
21
  .slice(0, 12);
22
22
  }
23
- export function buildArgs(config, tools, dockerfilePath, imageSignature, { engine, runFn = runEngine, runSafeFn = runSafeEngine, env = process.env } = {}) {
23
+ export function buildArgs(config, tools, dockerfilePath, imageSignature, { engine, runFn = runEngine, runSafeFn = runSafeEngine, env = process.env, refresh = false } = {}) {
24
24
  const selectedEngine = engine ?? detectEngine(config);
25
25
  const { uid: hostUid, gid: hostGid } = resolveBuildUid({
26
26
  engine: selectedEngine,
@@ -28,7 +28,7 @@ export function buildArgs(config, tools, dockerfilePath, imageSignature, { engin
28
28
  runSafeFn,
29
29
  env
30
30
  });
31
- return [
31
+ const args = [
32
32
  'build',
33
33
  '-t',
34
34
  config.imageName,
@@ -38,6 +38,8 @@ export function buildArgs(config, tools, dockerfilePath, imageSignature, { engin
38
38
  `HOST_GID=${hostGid}`,
39
39
  '--build-arg',
40
40
  `AI_TOOL_PACKAGES=${toolNpmPackagesArg(tools)}`,
41
+ '--build-arg',
42
+ `AI_TOOLS_SHELL_INSTALL_B64=${toolShellInstallScriptBase64(tools)}`,
41
43
  '--label',
42
44
  sandboxLabel(config),
43
45
  '--label',
@@ -46,6 +48,10 @@ export function buildArgs(config, tools, dockerfilePath, imageSignature, { engin
46
48
  toEnginePath(selectedEngine, dockerfilePath),
47
49
  toEnginePath(selectedEngine, config.repoRoot)
48
50
  ];
51
+ if (refresh) {
52
+ args.splice(1, 0, '--no-cache', '--pull');
53
+ }
54
+ return args;
49
55
  }
50
56
  function removeImageIfPresent(imageName, engine) {
51
57
  if (runOkEngine(engine, 'docker', ['image', 'inspect', imageName])) {
@@ -58,6 +64,7 @@ export async function rebuild(args) {
58
64
  allowPositionals: true,
59
65
  strict: true,
60
66
  options: {
67
+ refresh: { type: 'boolean' },
61
68
  quiet: { type: 'boolean', short: 'q' },
62
69
  help: { type: 'boolean', short: 'h' }
63
70
  }
@@ -71,6 +78,7 @@ export async function rebuild(args) {
71
78
  const preparedDockerfile = prepareDockerfile(config);
72
79
  const imageSignature = buildSignature(preparedDockerfile, tools);
73
80
  const quiet = values.quiet ?? false;
81
+ const refresh = values.refresh ?? false;
74
82
  const engine = detectEngine(config);
75
83
  await ensureDocker(config, undefined);
76
84
  p.intro(pc.cyan('Rebuilding sandbox image'));
@@ -81,7 +89,7 @@ export async function rebuild(args) {
81
89
  removeImageIfPresent(config.imageName, engine);
82
90
  spinner.stop('Old image removed');
83
91
  spinner.start('Building image...');
84
- runEngine(engine, 'docker', buildArgs(config, tools, preparedDockerfile.path, imageSignature, { engine }), {
92
+ runEngine(engine, 'docker', buildArgs(config, tools, preparedDockerfile.path, imageSignature, { engine, refresh }), {
85
93
  cwd: config.repoRoot
86
94
  });
87
95
  spinner.stop(pc.green('Sandbox image rebuilt'));
@@ -90,7 +98,7 @@ export async function rebuild(args) {
90
98
  p.log.step(`Removing old image ${config.imageName}`);
91
99
  removeImageIfPresent(config.imageName, engine);
92
100
  p.log.step('Building image');
93
- runVerboseEngine(engine, 'docker', buildArgs(config, tools, preparedDockerfile.path, imageSignature, { engine }), { cwd: config.repoRoot });
101
+ runVerboseEngine(engine, 'docker', buildArgs(config, tools, preparedDockerfile.path, imageSignature, { engine, refresh }), { cwd: config.repoRoot });
94
102
  p.log.success(pc.green('Sandbox image rebuilt'));
95
103
  }
96
104
  }
@@ -6,6 +6,7 @@ import pc from 'picocolors';
6
6
  import { validateSandboxEngine } from "./engine.js";
7
7
  import { hostJoin } from "./engines/wsl2-paths.js";
8
8
  import { findRuntimeEngineMismatches } from "./runtime-engines.js";
9
+ import { parseCustomTools } from "./tools.js";
9
10
  const DEFAULTS = Object.freeze({
10
11
  engine: null,
11
12
  runtimes: ['node22'],
@@ -76,6 +77,7 @@ export function loadConfig({ platformFn = platform, writeStderr = (chunk) => pro
76
77
  ' Update "sandbox.runtimes" in .agents/.airc.json (e.g. "node22"), or relax "engines.node".\n'));
77
78
  }
78
79
  }
80
+ const customTools = parseCustomTools(sandbox.customTools, { home });
79
81
  return {
80
82
  repoRoot,
81
83
  configPath,
@@ -93,6 +95,7 @@ export function loadConfig({ platformFn = platform, writeStderr = (chunk) => pro
93
95
  tools: Array.isArray(sandbox.tools) && sandbox.tools.length > 0
94
96
  ? [...sandbox.tools]
95
97
  : defaults.tools,
98
+ customTools,
96
99
  dockerfile,
97
100
  vm: {
98
101
  cpu: asPositiveNumberOrNull(sandbox.vm?.cpu) ?? defaults.vm.cpu,