@fitlab-ai/agent-infra 0.6.5 → 0.7.1

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 (208) hide show
  1. package/README.md +51 -25
  2. package/README.zh-CN.md +49 -23
  3. package/bin/cli.ts +1 -1
  4. package/dist/bin/cli.js +1 -1
  5. package/dist/lib/builtin-tuis.js +45 -0
  6. package/dist/lib/defaults.json +4 -0
  7. package/dist/lib/init.js +65 -23
  8. package/dist/lib/prompt.js +49 -1
  9. package/dist/lib/sandbox/commands/create.js +4 -2
  10. package/dist/lib/sandbox/commands/enter.js +15 -4
  11. package/dist/lib/sandbox/commands/list-running.js +153 -0
  12. package/dist/lib/sandbox/commands/ls.js +24 -45
  13. package/dist/lib/sandbox/commands/rebuild.js +7 -13
  14. package/dist/lib/sandbox/commands/rm.js +2 -0
  15. package/dist/lib/sandbox/config.js +3 -0
  16. package/dist/lib/sandbox/image-prune.js +18 -0
  17. package/dist/lib/sandbox/index.js +2 -1
  18. package/dist/lib/sandbox/runtimes/ai-tools.dockerfile +10 -6
  19. package/dist/lib/sandbox/task-resolver.js +18 -0
  20. package/dist/lib/sandbox/tools.js +213 -8
  21. package/dist/lib/update.js +70 -18
  22. package/lib/builtin-tuis.ts +55 -0
  23. package/lib/defaults.json +4 -0
  24. package/lib/init.ts +97 -35
  25. package/lib/prompt.ts +54 -1
  26. package/lib/sandbox/commands/create.ts +10 -2
  27. package/lib/sandbox/commands/enter.ts +14 -4
  28. package/lib/sandbox/commands/list-running.ts +188 -0
  29. package/lib/sandbox/commands/ls.ts +28 -49
  30. package/lib/sandbox/commands/rebuild.ts +12 -14
  31. package/lib/sandbox/commands/rm.ts +3 -0
  32. package/lib/sandbox/config.ts +7 -0
  33. package/lib/sandbox/image-prune.ts +23 -0
  34. package/lib/sandbox/index.ts +2 -1
  35. package/lib/sandbox/runtimes/ai-tools.dockerfile +10 -6
  36. package/lib/sandbox/task-resolver.ts +23 -1
  37. package/lib/sandbox/tools.ts +248 -9
  38. package/lib/update.ts +85 -30
  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 +111 -2
  43. package/templates/.agents/README.zh-CN.md +111 -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/task-short-id.en.md +141 -0
  59. package/templates/.agents/rules/task-short-id.zh-CN.md +124 -0
  60. package/templates/.agents/rules/testing-discipline.en.md +2 -2
  61. package/templates/.agents/rules/testing-discipline.zh-CN.md +2 -2
  62. package/templates/.agents/scripts/task-short-id.js +713 -0
  63. package/templates/.agents/scripts/validate-artifact.js +1 -1
  64. package/templates/.agents/skills/analyze-task/SKILL.en.md +20 -4
  65. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +20 -5
  66. package/templates/.agents/skills/block-task/SKILL.en.md +12 -0
  67. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +12 -1
  68. package/templates/.agents/skills/cancel-task/SKILL.en.md +12 -0
  69. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +12 -1
  70. package/templates/.agents/skills/check-task/SKILL.en.md +47 -32
  71. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +46 -32
  72. package/templates/.agents/skills/close-codescan/SKILL.en.md +11 -0
  73. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +11 -0
  74. package/templates/.agents/skills/close-dependabot/SKILL.en.md +11 -0
  75. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +11 -0
  76. package/templates/.agents/skills/code-task/SKILL.en.md +121 -0
  77. package/templates/.agents/skills/{implement-task → code-task}/SKILL.zh-CN.md +55 -25
  78. package/templates/.agents/skills/{implement-task → code-task}/config/verify.en.json +4 -4
  79. package/templates/.agents/skills/{implement-task → code-task}/config/verify.zh-CN.json +4 -4
  80. package/templates/.agents/skills/{implement-task → code-task}/reference/branch-management.zh-CN.md +2 -2
  81. package/templates/.agents/skills/{implement-task/reference/implementation-rules.en.md → code-task/reference/code-rules.en.md} +6 -6
  82. package/templates/.agents/skills/{implement-task/reference/implementation-rules.zh-CN.md → code-task/reference/code-rules.zh-CN.md} +3 -3
  83. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +69 -0
  84. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +69 -0
  85. package/templates/.agents/skills/{refine-task/reference/fix-workflow.en.md → code-task/reference/fix-mode.en.md} +12 -12
  86. package/templates/.agents/skills/{refine-task/reference/fix-workflow.zh-CN.md → code-task/reference/fix-mode.zh-CN.md} +8 -8
  87. package/templates/.agents/skills/code-task/reference/output-template.en.md +20 -0
  88. package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +20 -0
  89. package/templates/.agents/skills/{implement-task → code-task}/reference/report-template.en.md +4 -4
  90. package/templates/.agents/skills/{implement-task → code-task}/reference/report-template.zh-CN.md +3 -3
  91. package/templates/.agents/skills/code-task/scripts/detect-mode.js +370 -0
  92. package/templates/.agents/skills/commit/SKILL.en.md +6 -2
  93. package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -2
  94. package/templates/.agents/skills/commit/reference/task-status-update.en.md +10 -6
  95. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +10 -6
  96. package/templates/.agents/skills/complete-task/SKILL.en.md +17 -3
  97. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +17 -4
  98. package/templates/.agents/skills/create-pr/SKILL.en.md +21 -1
  99. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +21 -1
  100. package/templates/.agents/skills/create-task/SKILL.en.md +14 -0
  101. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +14 -1
  102. package/templates/.agents/skills/import-codescan/SKILL.en.md +15 -1
  103. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +15 -1
  104. package/templates/.agents/skills/import-dependabot/SKILL.en.md +16 -2
  105. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +16 -2
  106. package/templates/.agents/skills/import-issue/SKILL.en.md +17 -3
  107. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +17 -3
  108. package/templates/.agents/skills/plan-task/SKILL.en.md +8 -4
  109. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +8 -5
  110. package/templates/.agents/skills/restore-task/SKILL.en.md +16 -3
  111. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +16 -4
  112. package/templates/.agents/skills/review-analysis/SKILL.en.md +80 -0
  113. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +105 -0
  114. package/templates/.agents/skills/review-analysis/config/verify.en.json +51 -0
  115. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +51 -0
  116. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +87 -0
  117. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +87 -0
  118. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +90 -0
  119. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +91 -0
  120. package/templates/.agents/skills/review-analysis/reference/review-criteria.en.md +47 -0
  121. package/templates/.agents/skills/review-analysis/reference/review-criteria.zh-CN.md +47 -0
  122. package/templates/.agents/skills/{review-task → review-code}/SKILL.en.md +15 -9
  123. package/templates/.agents/skills/{review-task → review-code}/SKILL.zh-CN.md +19 -10
  124. package/templates/.agents/skills/{review-task → review-code}/config/verify.en.json +7 -5
  125. package/templates/.agents/skills/{review-task → review-code}/config/verify.zh-CN.json +6 -4
  126. package/templates/.agents/skills/{review-task → review-code}/reference/output-templates.en.md +21 -17
  127. package/templates/.agents/skills/{review-task → review-code}/reference/output-templates.zh-CN.md +19 -15
  128. package/templates/.agents/skills/{review-task → review-code}/reference/report-template.en.md +5 -6
  129. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +91 -0
  130. package/templates/.agents/skills/review-code/reference/review-criteria.en.md +48 -0
  131. package/templates/.agents/skills/{review-task → review-code}/reference/review-criteria.zh-CN.md +10 -4
  132. package/templates/.agents/skills/review-plan/SKILL.en.md +80 -0
  133. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +105 -0
  134. package/templates/.agents/skills/{refine-task → review-plan}/config/verify.en.json +14 -10
  135. package/templates/.agents/skills/{refine-task → review-plan}/config/verify.zh-CN.json +14 -10
  136. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +87 -0
  137. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +87 -0
  138. package/templates/.agents/skills/review-plan/reference/report-template.en.md +90 -0
  139. package/templates/.agents/skills/{review-task → review-plan}/reference/report-template.zh-CN.md +3 -3
  140. package/templates/.agents/skills/review-plan/reference/review-criteria.en.md +47 -0
  141. package/templates/.agents/skills/review-plan/reference/review-criteria.zh-CN.md +47 -0
  142. package/templates/.agents/skills/test/SKILL.en.md +2 -2
  143. package/templates/.agents/skills/test/SKILL.zh-CN.md +13 -31
  144. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +1 -0
  145. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +1 -0
  146. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +113 -21
  147. package/templates/.agents/templates/task.en.md +4 -3
  148. package/templates/.agents/templates/task.zh-CN.md +3 -2
  149. package/templates/.agents/workflows/bug-fix.en.yaml +126 -80
  150. package/templates/.agents/workflows/bug-fix.zh-CN.yaml +90 -44
  151. package/templates/.agents/workflows/feature-development.en.yaml +115 -70
  152. package/templates/.agents/workflows/feature-development.zh-CN.yaml +92 -47
  153. package/templates/.agents/workflows/refactoring.en.yaml +123 -78
  154. package/templates/.agents/workflows/refactoring.zh-CN.yaml +89 -44
  155. package/templates/.claude/commands/code-task.en.md +8 -0
  156. package/templates/.claude/commands/code-task.zh-CN.md +8 -0
  157. package/templates/.claude/commands/review-analysis.en.md +8 -0
  158. package/templates/.claude/commands/review-analysis.zh-CN.md +8 -0
  159. package/templates/.claude/commands/review-code.en.md +8 -0
  160. package/templates/.claude/commands/review-code.zh-CN.md +8 -0
  161. package/templates/.claude/commands/review-plan.en.md +8 -0
  162. package/templates/.claude/commands/review-plan.zh-CN.md +8 -0
  163. package/templates/.gemini/commands/_project_/archive-tasks.zh-CN.toml +1 -1
  164. package/templates/.gemini/commands/_project_/code-task.en.toml +8 -0
  165. package/templates/.gemini/commands/_project_/code-task.zh-CN.toml +8 -0
  166. package/templates/.gemini/commands/_project_/init-labels.zh-CN.toml +1 -1
  167. package/templates/.gemini/commands/_project_/init-milestones.zh-CN.toml +1 -1
  168. package/templates/.gemini/commands/_project_/review-analysis.en.toml +8 -0
  169. package/templates/.gemini/commands/_project_/review-analysis.zh-CN.toml +8 -0
  170. package/templates/.gemini/commands/_project_/review-code.en.toml +8 -0
  171. package/templates/.gemini/commands/_project_/review-code.zh-CN.toml +8 -0
  172. package/templates/.gemini/commands/_project_/review-plan.en.toml +8 -0
  173. package/templates/.gemini/commands/_project_/review-plan.zh-CN.toml +8 -0
  174. package/templates/.opencode/commands/code-task.en.md +11 -0
  175. package/templates/.opencode/commands/code-task.zh-CN.md +11 -0
  176. package/templates/.opencode/commands/review-analysis.en.md +11 -0
  177. package/templates/.opencode/commands/review-analysis.zh-CN.md +11 -0
  178. package/templates/.opencode/commands/review-code.en.md +11 -0
  179. package/templates/.opencode/commands/review-code.zh-CN.md +11 -0
  180. package/templates/.opencode/commands/review-plan.en.md +11 -0
  181. package/templates/.opencode/commands/review-plan.zh-CN.md +11 -0
  182. package/templates/.agents/skills/implement-task/SKILL.en.md +0 -173
  183. package/templates/.agents/skills/implement-task/reference/output-template.en.md +0 -20
  184. package/templates/.agents/skills/implement-task/reference/output-template.zh-CN.md +0 -20
  185. package/templates/.agents/skills/refine-task/SKILL.en.md +0 -153
  186. package/templates/.agents/skills/refine-task/SKILL.zh-CN.md +0 -153
  187. package/templates/.agents/skills/refine-task/reference/report-template.en.md +0 -64
  188. package/templates/.agents/skills/refine-task/reference/report-template.zh-CN.md +0 -64
  189. package/templates/.agents/skills/review-task/reference/review-criteria.en.md +0 -42
  190. package/templates/.claude/commands/implement-task.en.md +0 -8
  191. package/templates/.claude/commands/implement-task.zh-CN.md +0 -8
  192. package/templates/.claude/commands/refine-task.en.md +0 -8
  193. package/templates/.claude/commands/refine-task.zh-CN.md +0 -8
  194. package/templates/.claude/commands/review-task.en.md +0 -8
  195. package/templates/.claude/commands/review-task.zh-CN.md +0 -8
  196. package/templates/.gemini/commands/_project_/implement-task.en.toml +0 -8
  197. package/templates/.gemini/commands/_project_/implement-task.zh-CN.toml +0 -8
  198. package/templates/.gemini/commands/_project_/refine-task.en.toml +0 -8
  199. package/templates/.gemini/commands/_project_/refine-task.zh-CN.toml +0 -8
  200. package/templates/.gemini/commands/_project_/review-task.en.toml +0 -8
  201. package/templates/.gemini/commands/_project_/review-task.zh-CN.toml +0 -8
  202. package/templates/.opencode/commands/implement-task.en.md +0 -11
  203. package/templates/.opencode/commands/implement-task.zh-CN.md +0 -11
  204. package/templates/.opencode/commands/refine-task.en.md +0 -11
  205. package/templates/.opencode/commands/refine-task.zh-CN.md +0 -11
  206. package/templates/.opencode/commands/review-task.en.md +0 -11
  207. package/templates/.opencode/commands/review-task.zh-CN.md +0 -11
  208. /package/templates/.agents/skills/{implement-task → code-task}/reference/branch-management.en.md +0 -0
@@ -7,8 +7,14 @@ import type { SandboxConfig } from '../config.ts';
7
7
  import { prepareDockerfile } from '../dockerfile.ts';
8
8
  import { sandboxImageConfigLabel, sandboxLabel } from '../constants.ts';
9
9
  import { detectEngine, ensureDocker } from '../engine.ts';
10
- import { runEngine, runOkEngine, runSafeEngine, runVerboseEngine } from '../shell.ts';
11
- import { resolveTools, toolNpmPackagesArg } from '../tools.ts';
10
+ import { runEngine, runSafeEngine, runVerboseEngine } from '../shell.ts';
11
+ import { pruneSandboxDanglingImages } from '../image-prune.ts';
12
+ import {
13
+ imageSignatureFields,
14
+ resolveTools,
15
+ toolNpmPackagesArg,
16
+ toolShellInstallScriptBase64
17
+ } from '../tools.ts';
12
18
  import type { SandboxTool } from '../tools.ts';
13
19
  import { toEnginePath } from '../engines/wsl2-paths.ts';
14
20
  import { resolveBuildUid } from '../engines/native.ts';
@@ -23,7 +29,7 @@ function buildSignature(preparedDockerfile: PreparedDockerfile, tools: SandboxTo
23
29
  return createHash('sha256')
24
30
  .update(JSON.stringify({
25
31
  dockerfile: preparedDockerfile.signature,
26
- tools: tools.map((tool) => tool.npmPackage)
32
+ tools: imageSignatureFields(tools)
27
33
  }))
28
34
  .digest('hex')
29
35
  .slice(0, 12);
@@ -66,6 +72,8 @@ export function buildArgs(
66
72
  `HOST_GID=${hostGid}`,
67
73
  '--build-arg',
68
74
  `AI_TOOL_PACKAGES=${toolNpmPackagesArg(tools)}`,
75
+ '--build-arg',
76
+ `AI_TOOLS_SHELL_INSTALL_B64=${toolShellInstallScriptBase64(tools)}`,
69
77
  '--label',
70
78
  sandboxLabel(config),
71
79
  '--label',
@@ -82,12 +90,6 @@ export function buildArgs(
82
90
  return args;
83
91
  }
84
92
 
85
- function removeImageIfPresent(imageName: string, engine: string): void {
86
- if (runOkEngine(engine, 'docker', ['image', 'inspect', imageName])) {
87
- runEngine(engine, 'docker', ['rmi', imageName]);
88
- }
89
- }
90
-
91
93
  export async function rebuild(args: string[]): Promise<void> {
92
94
  const { values } = parseArgs({
93
95
  args,
@@ -119,17 +121,12 @@ export async function rebuild(args: string[]): Promise<void> {
119
121
  try {
120
122
  if (quiet) {
121
123
  const spinner = p.spinner();
122
- spinner.start(`Removing old image ${config.imageName}...`);
123
- removeImageIfPresent(config.imageName, engine);
124
- spinner.stop('Old image removed');
125
124
  spinner.start('Building image...');
126
125
  runEngine(engine, 'docker', buildArgs(config, tools, preparedDockerfile.path, imageSignature, { engine, refresh }), {
127
126
  cwd: config.repoRoot
128
127
  });
129
128
  spinner.stop(pc.green('Sandbox image rebuilt'));
130
129
  } else {
131
- p.log.step(`Removing old image ${config.imageName}`);
132
- removeImageIfPresent(config.imageName, engine);
133
130
  p.log.step('Building image');
134
131
  runVerboseEngine(
135
132
  engine,
@@ -139,6 +136,7 @@ export async function rebuild(args: string[]): Promise<void> {
139
136
  );
140
137
  p.log.success(pc.green('Sandbox image rebuilt'));
141
138
  }
139
+ pruneSandboxDanglingImages(config, engine);
142
140
  } finally {
143
141
  preparedDockerfile.cleanup();
144
142
  }
@@ -15,6 +15,7 @@ import {
15
15
  worktreeDirCandidates
16
16
  } from '../constants.ts';
17
17
  import { ENGINES, detectEngine, engineDisplayName, isManagedEngine, stopManagedVm } from '../engine.ts';
18
+ import { pruneSandboxDanglingImages } from '../image-prune.ts';
18
19
  import { removeManagedDir, removeWorktreeDir } from '../managed-fs.ts';
19
20
  import { runOk, runSafe, runSafeEngine } from '../shell.ts';
20
21
  import { resolveTaskBranch } from '../task-resolver.ts';
@@ -208,6 +209,8 @@ async function rmAll(config: SandboxConfig, tools: SandboxTool[]): Promise<void>
208
209
  runSafeEngine(engine, 'docker', ['rmi', config.imageName]);
209
210
  }
210
211
 
212
+ pruneSandboxDanglingImages(config, engine);
213
+
211
214
  if (isManagedEngine(engine)) {
212
215
  if (engine === ENGINES.WSL2) {
213
216
  p.log.warn('Windows uses Docker Desktop with WSL2. Stop it from Docker Desktop or run "wsl --shutdown" manually.');
@@ -6,6 +6,8 @@ import pc from 'picocolors';
6
6
  import { validateSandboxEngine } from './engine.ts';
7
7
  import { hostJoin } from './engines/wsl2-paths.ts';
8
8
  import { findRuntimeEngineMismatches } from './runtime-engines.ts';
9
+ import { parseCustomTools } from './tools.ts';
10
+ import type { SandboxTool } from './tools.ts';
9
11
 
10
12
  const DEFAULTS = Object.freeze({
11
13
  engine: null,
@@ -26,6 +28,7 @@ type SandboxConfigInput = {
26
28
  engine?: string | null;
27
29
  runtimes?: string[];
28
30
  tools?: string[];
31
+ customTools?: unknown;
29
32
  dockerfile?: string | null;
30
33
  vm?: Record<string, unknown>;
31
34
  };
@@ -51,6 +54,7 @@ export type SandboxConfig = {
51
54
  engine: string | null;
52
55
  runtimes: string[];
53
56
  tools: string[];
57
+ customTools: SandboxTool[];
54
58
  dockerfile: string | null;
55
59
  vm: SandboxVmConfig;
56
60
  };
@@ -136,6 +140,8 @@ export function loadConfig({
136
140
  }
137
141
  }
138
142
 
143
+ const customTools = parseCustomTools(sandbox.customTools, { home });
144
+
139
145
  return {
140
146
  repoRoot,
141
147
  configPath,
@@ -153,6 +159,7 @@ export function loadConfig({
153
159
  tools: Array.isArray(sandbox.tools) && sandbox.tools.length > 0
154
160
  ? [...sandbox.tools]
155
161
  : defaults.tools,
162
+ customTools,
156
163
  dockerfile,
157
164
  vm: {
158
165
  cpu: asPositiveNumberOrNull(sandbox.vm?.cpu) ?? defaults.vm.cpu,
@@ -0,0 +1,23 @@
1
+ import * as p from '@clack/prompts';
2
+ import type { SandboxConfig } from './config.ts';
3
+ import { sandboxLabel } from './constants.ts';
4
+ import { runEngine } from './shell.ts';
5
+
6
+ export function pruneSandboxDanglingImages(
7
+ config: Pick<SandboxConfig, 'project'>,
8
+ engine: string
9
+ ): void {
10
+ try {
11
+ runEngine(engine, 'docker', [
12
+ 'image',
13
+ 'prune',
14
+ '-f',
15
+ '--filter',
16
+ `label=${sandboxLabel(config)}`
17
+ ]);
18
+ } catch {
19
+ p.log.warn(
20
+ `Failed to prune dangling sandbox images (label=${sandboxLabel(config)}); leaving them in place.`
21
+ );
22
+ }
23
+ }
@@ -2,7 +2,8 @@ const USAGE = `Usage: ai sandbox <command> [options]
2
2
 
3
3
  Commands:
4
4
  create <branch> [base] Create a sandbox (VM + image + worktree + container)
5
- exec <branch> [cmd...] Enter sandbox or run a command
5
+ exec <branch | '#N'> [cmd...]
6
+ Enter sandbox or run a command (use leftmost '#' column from 'ls')
6
7
  ls List sandboxes for the current project
7
8
  prune [--dry-run] Remove orphaned per-branch state dirs
8
9
  rebuild [--quiet] [--refresh]
@@ -4,16 +4,20 @@ ENV OPENCODE_DISABLE_AUTOUPDATE=1
4
4
  ENV NPM_CONFIG_PREFIX=/home/devuser/.npm-global
5
5
  ENV PATH="/home/devuser/.npm-global/bin:${PATH}"
6
6
 
7
- ARG AI_TOOL_PACKAGES
8
- RUN if [ -z "${AI_TOOL_PACKAGES}" ]; then \
9
- echo "AI_TOOL_PACKAGES build arg is required"; \
10
- exit 1; \
11
- fi && \
12
- set -e && \
7
+ ARG AI_TOOL_PACKAGES=
8
+ RUN set -e && \
13
9
  for pkg in ${AI_TOOL_PACKAGES}; do \
14
10
  npm install -g "$pkg"; \
15
11
  done
16
12
 
13
+ ARG AI_TOOLS_SHELL_INSTALL_B64=
14
+ RUN if [ -n "${AI_TOOLS_SHELL_INSTALL_B64}" ]; then \
15
+ set -e && \
16
+ echo "${AI_TOOLS_SHELL_INSTALL_B64}" | base64 -d > /tmp/ai-tools-install.sh && \
17
+ bash /tmp/ai-tools-install.sh && \
18
+ rm /tmp/ai-tools-install.sh; \
19
+ fi
20
+
17
21
  RUN npm install -g pyright
18
22
 
19
23
  RUN mkdir -p /home/devuser/.local/share /home/devuser/.local/state
@@ -1,9 +1,27 @@
1
+ import { spawnSync } from 'node:child_process';
1
2
  import fs from 'node:fs';
2
3
  import path from 'node:path';
3
4
 
4
5
  const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
6
+ const SHORT_ID_RE = /^#\d+$/;
5
7
  const WORKSPACE_DIRS = ['active', 'completed', 'blocked', 'archive'];
6
8
 
9
+ function resolveShortIdStrict(arg: string, repoRoot: string): string {
10
+ const scriptPath = path.join(repoRoot, '.agents', 'scripts', 'task-short-id.js');
11
+ if (!fs.existsSync(scriptPath)) {
12
+ throw new Error(
13
+ `Short id '${arg}' provided but task-short-id.js script is missing at ${scriptPath}`
14
+ );
15
+ }
16
+ const result = spawnSync('node', [scriptPath, 'resolve', arg], { encoding: 'utf8', cwd: repoRoot });
17
+ if (result.status !== 0) {
18
+ throw new Error(
19
+ `Short id '${arg}' not found in active task registry: ${(result.stderr || '').trim()}`
20
+ );
21
+ }
22
+ return result.stdout.trim();
23
+ }
24
+
7
25
  function stripQuotes(value: string): string {
8
26
  return value.replace(/^(["'])(.*)\1$/, '$2');
9
27
  }
@@ -33,10 +51,14 @@ function resolveBranchFromTaskContent(content: string, taskId: string): string {
33
51
  }
34
52
 
35
53
  export function resolveTaskBranch(arg: string, repoRoot: string): string {
54
+ if (SHORT_ID_RE.test(arg)) {
55
+ const taskId = resolveShortIdStrict(arg, repoRoot);
56
+ const content = readTaskContent(repoRoot, taskId);
57
+ return resolveBranchFromTaskContent(content, taskId);
58
+ }
36
59
  if (!TASK_ID_RE.test(arg)) {
37
60
  return arg;
38
61
  }
39
-
40
62
  const content = readTaskContent(repoRoot, arg);
41
63
  return resolveBranchFromTaskContent(content, arg);
42
64
  }
@@ -1,10 +1,14 @@
1
1
  import { safeNameCandidates, sanitizeBranchName } from './constants.ts';
2
2
  import { hostJoin } from './engines/wsl2-paths.ts';
3
3
 
4
+ export type SandboxToolInstall =
5
+ | { type: 'npm'; cmd: string }
6
+ | { type: 'shell'; cmd: string };
7
+
4
8
  export type SandboxTool = {
5
9
  id: string;
6
10
  name: string;
7
- npmPackage: string;
11
+ install: SandboxToolInstall;
8
12
  sandboxBase: string;
9
13
  containerMount: string;
10
14
  versionCmd: string;
@@ -21,14 +25,17 @@ type ToolsConfig = {
21
25
  home: string;
22
26
  project: string;
23
27
  tools: string[];
28
+ customTools?: SandboxTool[];
24
29
  };
25
30
 
31
+ const TOOL_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
32
+
26
33
  function createBuiltinTools(home: string, project: string): Record<string, SandboxTool> {
27
34
  return {
28
35
  'claude-code': {
29
36
  id: 'claude-code',
30
37
  name: 'Claude Code',
31
- npmPackage: '@anthropic-ai/claude-code@stable',
38
+ install: { type: 'npm', cmd: '@anthropic-ai/claude-code@stable' },
32
39
  sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'claude-code'),
33
40
  containerMount: '/home/devuser/.claude',
34
41
  versionCmd: 'claude --version',
@@ -58,7 +65,7 @@ function createBuiltinTools(home: string, project: string): Record<string, Sandb
58
65
  codex: {
59
66
  id: 'codex',
60
67
  name: 'Codex',
61
- npmPackage: '@openai/codex',
68
+ install: { type: 'npm', cmd: '@openai/codex' },
62
69
  sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'codex'),
63
70
  containerMount: '/home/devuser/.codex',
64
71
  versionCmd: 'codex --version',
@@ -73,7 +80,7 @@ function createBuiltinTools(home: string, project: string): Record<string, Sandb
73
80
  opencode: {
74
81
  id: 'opencode',
75
82
  name: 'OpenCode',
76
- npmPackage: 'opencode-ai',
83
+ install: { type: 'npm', cmd: 'opencode-ai' },
77
84
  sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'opencode'),
78
85
  containerMount: '/home/devuser/.local/share/opencode',
79
86
  versionCmd: 'opencode version',
@@ -92,7 +99,7 @@ function createBuiltinTools(home: string, project: string): Record<string, Sandb
92
99
  'gemini-cli': {
93
100
  id: 'gemini-cli',
94
101
  name: 'Gemini CLI',
95
- npmPackage: '@google/gemini-cli',
102
+ install: { type: 'npm', cmd: '@google/gemini-cli' },
96
103
  sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'gemini-cli'),
97
104
  containerMount: '/home/devuser/.gemini',
98
105
  versionCmd: 'gemini --version',
@@ -108,16 +115,224 @@ function createBuiltinTools(home: string, project: string): Record<string, Sandb
108
115
  };
109
116
  }
110
117
 
118
+ export function builtinToolIds(): string[] {
119
+ return Object.keys(createBuiltinTools('', ''));
120
+ }
121
+
111
122
  function validateTool(tool: SandboxTool): void {
112
- if (!tool.npmPackage || !tool.containerMount.startsWith('/')) {
113
- throw new Error(`Invalid sandbox tool descriptor: ${tool.id}`);
123
+ if (!tool.id || !TOOL_ID_PATTERN.test(tool.id)) {
124
+ throw new Error(`Invalid sandbox tool id: ${String(tool.id)}`);
125
+ }
126
+ if (!tool.install || (tool.install.type !== 'npm' && tool.install.type !== 'shell')) {
127
+ throw new Error(`Sandbox tool ${tool.id} has invalid install.type`);
128
+ }
129
+ if (!tool.install.cmd) {
130
+ throw new Error(`Sandbox tool ${tool.id} has empty install.cmd`);
131
+ }
132
+ if (!tool.containerMount || !tool.containerMount.startsWith('/')) {
133
+ throw new Error(`Sandbox tool ${tool.id} containerMount must be an absolute path`);
134
+ }
135
+ }
136
+
137
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
138
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
139
+ }
140
+
141
+ function asString(value: unknown, field: string, context: string): string {
142
+ if (typeof value !== 'string') {
143
+ throw new Error(`${context}: field "${field}" must be a string`);
144
+ }
145
+ return value;
146
+ }
147
+
148
+ function asOptionalNonEmptyString(value: unknown, field: string, context: string): string | undefined {
149
+ if (value === undefined) {
150
+ return undefined;
151
+ }
152
+ if (typeof value !== 'string') {
153
+ throw new Error(`${context}: field "${field}" must be a string when provided`);
154
+ }
155
+ if (value.length === 0) {
156
+ throw new Error(`${context}: field "${field}" must be non-empty when provided`);
157
+ }
158
+ return value;
159
+ }
160
+
161
+ function asStringRecord(value: unknown, field: string, context: string): Record<string, string> | undefined {
162
+ if (value === undefined) {
163
+ return undefined;
164
+ }
165
+ if (!isPlainObject(value)) {
166
+ throw new Error(`${context}: field "${field}" must be an object when provided`);
167
+ }
168
+ const out: Record<string, string> = {};
169
+ for (const [key, val] of Object.entries(value)) {
170
+ if (typeof val !== 'string') {
171
+ throw new Error(`${context}: field "${field}.${key}" must be a string`);
172
+ }
173
+ out[key] = val;
174
+ }
175
+ return out;
176
+ }
177
+
178
+ function asStringArray(value: unknown, field: string, context: string): string[] | undefined {
179
+ if (value === undefined) {
180
+ return undefined;
181
+ }
182
+ if (!Array.isArray(value)) {
183
+ throw new Error(`${context}: field "${field}" must be an array when provided`);
184
+ }
185
+ return value.map((item, index) => {
186
+ if (typeof item !== 'string') {
187
+ throw new Error(`${context}: field "${field}[${index}]" must be a string`);
188
+ }
189
+ return item;
190
+ });
191
+ }
192
+
193
+ function parseInstall(value: unknown, context: string): SandboxToolInstall {
194
+ if (!isPlainObject(value)) {
195
+ throw new Error(`${context}: field "install" must be an object`);
196
+ }
197
+ const type = value.type;
198
+ if (type !== 'npm' && type !== 'shell') {
199
+ throw new Error(`${context}: field "install.type" must be "npm" or "shell"`);
200
+ }
201
+ const cmd = asString(value.cmd, 'install.cmd', context);
202
+ if (!cmd) {
203
+ throw new Error(`${context}: field "install.cmd" must be non-empty`);
204
+ }
205
+ return { type, cmd };
206
+ }
207
+
208
+ function parseHostPreSeedFiles(value: unknown, context: string): SandboxTool['hostPreSeedFiles'] {
209
+ if (value === undefined) {
210
+ return undefined;
211
+ }
212
+ if (!Array.isArray(value)) {
213
+ throw new Error(`${context}: field "hostPreSeedFiles" must be an array when provided`);
214
+ }
215
+ return value.map((item, index) => {
216
+ if (!isPlainObject(item)) {
217
+ throw new Error(`${context}: field "hostPreSeedFiles[${index}]" must be an object`);
218
+ }
219
+ return {
220
+ hostPath: asString(item.hostPath, `hostPreSeedFiles[${index}].hostPath`, context),
221
+ sandboxName: asString(item.sandboxName, `hostPreSeedFiles[${index}].sandboxName`, context)
222
+ };
223
+ });
224
+ }
225
+
226
+ function parseHostPreSeedDirs(value: unknown, context: string): SandboxTool['hostPreSeedDirs'] {
227
+ if (value === undefined) {
228
+ return undefined;
229
+ }
230
+ if (!Array.isArray(value)) {
231
+ throw new Error(`${context}: field "hostPreSeedDirs" must be an array when provided`);
114
232
  }
233
+ return value.map((item, index) => {
234
+ if (!isPlainObject(item)) {
235
+ throw new Error(`${context}: field "hostPreSeedDirs[${index}]" must be an object`);
236
+ }
237
+ return {
238
+ hostDir: asString(item.hostDir, `hostPreSeedDirs[${index}].hostDir`, context),
239
+ sandboxSubdir: asString(item.sandboxSubdir, `hostPreSeedDirs[${index}].sandboxSubdir`, context)
240
+ };
241
+ });
242
+ }
243
+
244
+ function parseHostLiveMounts(value: unknown, context: string): SandboxTool['hostLiveMounts'] {
245
+ if (value === undefined) {
246
+ return undefined;
247
+ }
248
+ if (!Array.isArray(value)) {
249
+ throw new Error(`${context}: field "hostLiveMounts" must be an array when provided`);
250
+ }
251
+ return value.map((item, index) => {
252
+ if (!isPlainObject(item)) {
253
+ throw new Error(`${context}: field "hostLiveMounts[${index}]" must be an object`);
254
+ }
255
+ return {
256
+ hostPath: asString(item.hostPath, `hostLiveMounts[${index}].hostPath`, context),
257
+ containerSubpath: asString(item.containerSubpath, `hostLiveMounts[${index}].containerSubpath`, context)
258
+ };
259
+ });
260
+ }
261
+
262
+ export function parseCustomTool(
263
+ entry: unknown,
264
+ index: number,
265
+ options: { home: string }
266
+ ): SandboxTool {
267
+ const context = `customTools[${index}]`;
268
+ if (!isPlainObject(entry)) {
269
+ throw new Error(`${context} must be an object`);
270
+ }
271
+
272
+ const id = asString(entry.id, 'id', context);
273
+ if (!TOOL_ID_PATTERN.test(id)) {
274
+ throw new Error(`${context}: field "id" must match ${TOOL_ID_PATTERN.source}`);
275
+ }
276
+
277
+ const containerMount = asOptionalNonEmptyString(entry.containerMount, 'containerMount', context)
278
+ ?? `/home/devuser/.${id}`;
279
+ if (!containerMount.startsWith('/')) {
280
+ throw new Error(`${context}: field "containerMount" must be an absolute path`);
281
+ }
282
+
283
+ const tool: SandboxTool = {
284
+ id,
285
+ name: asOptionalNonEmptyString(entry.name, 'name', context) ?? id,
286
+ install: parseInstall(entry.install, context),
287
+ sandboxBase: hostJoin(options.home, '.agent-infra', 'sandboxes', id),
288
+ containerMount,
289
+ versionCmd: asOptionalNonEmptyString(entry.versionCmd, 'versionCmd', context) ?? `which ${id}`,
290
+ setupHint: asOptionalNonEmptyString(entry.setupHint, 'setupHint', context)
291
+ ?? `Run \`${id}\` inside the container to set up.`,
292
+ envVars: asStringRecord(entry.envVars, 'envVars', context),
293
+ hostPreSeedFiles: parseHostPreSeedFiles(entry.hostPreSeedFiles, context),
294
+ hostPreSeedDirs: parseHostPreSeedDirs(entry.hostPreSeedDirs, context),
295
+ pathRewriteFiles: asStringArray(entry.pathRewriteFiles, 'pathRewriteFiles', context),
296
+ hostLiveMounts: parseHostLiveMounts(entry.hostLiveMounts, context),
297
+ postSetupCmds: asStringArray(entry.postSetupCmds, 'postSetupCmds', context)
298
+ };
299
+
300
+ validateTool(tool);
301
+ return tool;
302
+ }
303
+
304
+ export function parseCustomTools(value: unknown, options: { home: string }): SandboxTool[] {
305
+ if (value === undefined || value === null) {
306
+ return [];
307
+ }
308
+ if (!Array.isArray(value)) {
309
+ throw new Error('sandbox: "customTools" must be an array');
310
+ }
311
+ return value.map((entry, index) => parseCustomTool(entry, index, options));
115
312
  }
116
313
 
117
314
  export function resolveTools(config: ToolsConfig): SandboxTool[] {
118
315
  const builtins = createBuiltinTools(config.home, config.project);
316
+ const customs = config.customTools ?? [];
317
+
318
+ const seen = new Set<string>();
319
+ for (const tool of customs) {
320
+ if (builtins[tool.id]) {
321
+ throw new Error(`Custom sandbox tool id "${tool.id}" collides with a built-in tool`);
322
+ }
323
+ if (seen.has(tool.id)) {
324
+ throw new Error(`Duplicate sandbox tool id "${tool.id}" in customTools`);
325
+ }
326
+ seen.add(tool.id);
327
+ }
328
+
329
+ const merged: Record<string, SandboxTool> = { ...builtins };
330
+ for (const tool of customs) {
331
+ merged[tool.id] = tool;
332
+ }
333
+
119
334
  return config.tools.map((id) => {
120
- const tool = builtins[id];
335
+ const tool = merged[id];
121
336
  if (!tool) {
122
337
  throw new Error(`Unknown sandbox tool: ${id}`);
123
338
  }
@@ -139,5 +354,29 @@ export function toolProjectDirCandidates(tool: SandboxTool, project: string): st
139
354
  }
140
355
 
141
356
  export function toolNpmPackagesArg(tools: SandboxTool[]): string {
142
- return tools.map((tool) => tool.npmPackage).join(' ');
357
+ return tools
358
+ .filter((tool) => tool.install.type === 'npm')
359
+ .map((tool) => tool.install.cmd)
360
+ .join(' ');
361
+ }
362
+
363
+ export function toolShellInstallScript(tools: SandboxTool[]): string {
364
+ const blocks = tools
365
+ .filter((tool) => tool.install.type === 'shell')
366
+ .map((tool) => `# install: ${tool.id}\n${tool.install.cmd}`);
367
+
368
+ if (blocks.length === 0) {
369
+ return '';
370
+ }
371
+
372
+ return ['#!/bin/bash', 'set -e', '', ...blocks, ''].join('\n');
373
+ }
374
+
375
+ export function toolShellInstallScriptBase64(tools: SandboxTool[]): string {
376
+ const script = toolShellInstallScript(tools);
377
+ return script ? Buffer.from(script, 'utf8').toString('base64') : '';
378
+ }
379
+
380
+ export function imageSignatureFields(tools: SandboxTool[]): Array<{ id: string; install: SandboxToolInstall }> {
381
+ return tools.map((tool) => ({ id: tool.id, install: tool.install }));
143
382
  }