@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
@@ -13,7 +13,7 @@ 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";
@@ -58,7 +58,7 @@ function buildSignature(preparedDockerfile, tools) {
58
58
  return createHash('sha256')
59
59
  .update(JSON.stringify({
60
60
  dockerfile: preparedDockerfile.signature,
61
- tools: tools.map((tool) => tool.npmPackage)
61
+ tools: imageSignatureFields(tools)
62
62
  }))
63
63
  .digest('hex')
64
64
  .slice(0, 12);
@@ -789,6 +789,8 @@ export function buildImage(config, tools, dockerfilePath, imageSignature, { engi
789
789
  `HOST_GID=${hostGid}`,
790
790
  '--build-arg',
791
791
  `AI_TOOL_PACKAGES=${toolNpmPackagesArg(tools)}`,
792
+ '--build-arg',
793
+ `AI_TOOLS_SHELL_INSTALL_B64=${toolShellInstallScriptBase64(tools)}`,
792
794
  '--label',
793
795
  sandboxLabel(config),
794
796
  '--label',
@@ -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
@@ -79,8 +83,15 @@ export async function enter(args) {
79
83
  const config = loadConfig();
80
84
  validateClaudeCredentialsEnvOverride();
81
85
  const engine = detectEngine(config);
82
- const [branchOrTaskId = '', ...cmd] = args;
83
- 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, repoRoot: config.repoRoot });
91
+ }
92
+ else {
93
+ branch = resolveTaskBranch(firstArg, config.repoRoot);
94
+ }
84
95
  assertValidBranchName(branch);
85
96
  const running = runSafeEngine(engine, 'docker', ['ps', '--format', '{{.Names}}']).split('\n');
86
97
  const container = containerNameCandidates(config, branch).find((name) => running.includes(name));
@@ -0,0 +1,153 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { runSafeEngine } from "../shell.js";
5
+ export function containerListFormat() {
6
+ return '{{.Names}}\t{{.Status}}\t{{.Labels}}';
7
+ }
8
+ export function parseLabels(csv) {
9
+ if (!csv) {
10
+ return {};
11
+ }
12
+ const labels = {};
13
+ for (const pair of csv.split(',')) {
14
+ if (!pair) {
15
+ continue;
16
+ }
17
+ const eq = pair.indexOf('=');
18
+ if (eq < 0) {
19
+ continue;
20
+ }
21
+ labels[pair.slice(0, eq)] = pair.slice(eq + 1);
22
+ }
23
+ return labels;
24
+ }
25
+ export function parseSandboxRows(rawOutput, branchKey) {
26
+ if (!rawOutput) {
27
+ return [];
28
+ }
29
+ return rawOutput.split('\n').map((line) => {
30
+ const [name = '', status = '', labelsCsv = ''] = line.split('\t');
31
+ const branch = parseLabels(labelsCsv)[branchKey] ?? '';
32
+ return {
33
+ name,
34
+ status,
35
+ branch,
36
+ running: status.startsWith('Up '),
37
+ index: null
38
+ };
39
+ });
40
+ }
41
+ export function sortAndIndexSandboxRows(rows) {
42
+ const byName = (a, b) => {
43
+ if (a.name < b.name)
44
+ return -1;
45
+ if (a.name > b.name)
46
+ return 1;
47
+ return 0;
48
+ };
49
+ const running = rows.filter((row) => row.running).sort(byName).map((row, i) => ({
50
+ ...row,
51
+ index: i + 1
52
+ }));
53
+ const nonRunning = rows.filter((row) => !row.running).sort(byName).map((row) => ({
54
+ ...row,
55
+ index: null
56
+ }));
57
+ return { running, nonRunning };
58
+ }
59
+ export function fetchSandboxRows(engine, label, branchKey) {
60
+ const raw = runSafeEngine(engine, 'docker', [
61
+ 'ps',
62
+ '-a',
63
+ '--filter',
64
+ `label=${label}`,
65
+ '--format',
66
+ containerListFormat()
67
+ ]);
68
+ return sortAndIndexSandboxRows(parseSandboxRows(raw, branchKey));
69
+ }
70
+ /**
71
+ * Returns true iff `arg` is a syntactically valid task short reference ('#N').
72
+ * Zero IO. Callers MUST use this as the gate before constructing any context
73
+ * for resolveTaskShortRef — that way non-matching arguments (e.g. '#abc',
74
+ * '#1.5', '#') never trigger sandbox list IO.
75
+ */
76
+ export function isTaskShortRef(arg) {
77
+ return /^#\d+$/.test(arg);
78
+ }
79
+ /**
80
+ * Try to resolve a short ref against the global task-short-id registry.
81
+ *
82
+ * Tri-state semantics (review-code Round 1 M-1 fix):
83
+ * - 'miss' → script reports no entry (or registry script missing). Caller may fall back.
84
+ * - 'hit' → registry resolved to a task id and branch is found in task.md.
85
+ * - throws → registry hit but task.md is missing or branch metadata is unparseable;
86
+ * surfacing this error is critical — never silently fall back to running index.
87
+ */
88
+ function tryResolveFromRegistry(arg, repoRoot) {
89
+ const scriptPath = path.join(repoRoot, '.agents', 'scripts', 'task-short-id.js');
90
+ if (!fs.existsSync(scriptPath))
91
+ return { status: 'miss' };
92
+ const result = spawnSync('node', [scriptPath, 'resolve', arg], { encoding: 'utf8', cwd: repoRoot });
93
+ if (result.status !== 0)
94
+ return { status: 'miss' };
95
+ const taskId = (result.stdout || '').trim();
96
+ if (!/^TASK-\d{8}-\d{6}$/.test(taskId)) {
97
+ throw new Error(`Registry returned malformed task id for '${arg}': ${JSON.stringify(taskId)}`);
98
+ }
99
+ for (const sub of ['active', 'completed', 'blocked', 'archive']) {
100
+ const taskMdPath = path.join(repoRoot, '.agents', 'workspace', sub, taskId, 'task.md');
101
+ if (!fs.existsSync(taskMdPath))
102
+ continue;
103
+ const content = fs.readFileSync(taskMdPath, 'utf8');
104
+ const fm = content.match(/^branch:\s*(.+)$/m);
105
+ if (fm?.[1]?.trim()) {
106
+ return { status: 'hit', branch: fm[1].trim().replace(/^(["'])(.*)\1$/, '$2') };
107
+ }
108
+ const ctx = content.match(/^- \*\*(?:分支|Branch)\*\*:[ \t]*`?([^`\n]+)`?$/m);
109
+ if (ctx?.[1]?.trim()) {
110
+ return { status: 'hit', branch: ctx[1].trim().replace(/^(["'])(.*)\1$/, '$2') };
111
+ }
112
+ throw new Error(`Short ref '${arg}' resolved to task ${taskId} but task.md has no branch field`);
113
+ }
114
+ throw new Error(`Short ref '${arg}' resolved to task ${taskId} but task.md was not found under any workspace dir`);
115
+ }
116
+ function resolveByRunningIndex(arg, running) {
117
+ const n = Number(arg.slice(1));
118
+ if (n < 1) {
119
+ throw new Error(`Invalid sandbox index '${arg}': must be >= 1`);
120
+ }
121
+ if (running.length === 0) {
122
+ throw new Error(`No running sandbox to reference with '${arg}'`);
123
+ }
124
+ if (n > running.length) {
125
+ throw new Error(`No running sandbox at index '${arg}' (only ${running.length} running)`);
126
+ }
127
+ const row = running[n - 1];
128
+ if (!row.branch) {
129
+ throw new Error(`Cannot resolve branch for sandbox '${arg}' (container '${row.name}' missing branch label)`);
130
+ }
131
+ return row.branch;
132
+ }
133
+ /**
134
+ * Resolve a task short reference ('#N') to a branch name for the sandbox entrypoint.
135
+ *
136
+ * Resolution order (sandbox fallback mode, plan-r7 C2):
137
+ * 1. Try the global task-short-id registry under repoRoot. If hit, look up the
138
+ * branch from the matching task.md.
139
+ * 2. Fallback to the running-sandbox list index (preserves the #414 ls-index
140
+ * behaviour; long-term contract per analysis-r5).
141
+ *
142
+ * Precondition: callers MUST gate on isTaskShortRef(arg) === true.
143
+ */
144
+ export function resolveTaskShortRef(arg, ctx) {
145
+ if (ctx.repoRoot) {
146
+ const lookup = tryResolveFromRegistry(arg, ctx.repoRoot);
147
+ if (lookup.status === 'hit')
148
+ return lookup.branch;
149
+ // 'miss' falls through to ls-index fallback (preserves #414 behaviour); 'hit-but-invalid' already threw above.
150
+ }
151
+ return resolveByRunningIndex(arg, ctx.running);
152
+ }
153
+ //# 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
  }
@@ -6,8 +6,9 @@ import { loadConfig } from "../config.js";
6
6
  import { prepareDockerfile } from "../dockerfile.js";
7
7
  import { sandboxImageConfigLabel, sandboxLabel } from "../constants.js";
8
8
  import { detectEngine, ensureDocker } from "../engine.js";
9
- import { runEngine, runOkEngine, runSafeEngine, runVerboseEngine } from "../shell.js";
10
- import { resolveTools, toolNpmPackagesArg } from "../tools.js";
9
+ import { runEngine, runSafeEngine, runVerboseEngine } from "../shell.js";
10
+ import { pruneSandboxDanglingImages } from "../image-prune.js";
11
+ import { imageSignatureFields, resolveTools, toolNpmPackagesArg, toolShellInstallScriptBase64 } from "../tools.js";
11
12
  import { toEnginePath } from "../engines/wsl2-paths.js";
12
13
  import { resolveBuildUid } from "../engines/native.js";
13
14
  const USAGE = `Usage: ai sandbox rebuild [--quiet] [--refresh]`;
@@ -15,7 +16,7 @@ function buildSignature(preparedDockerfile, tools) {
15
16
  return createHash('sha256')
16
17
  .update(JSON.stringify({
17
18
  dockerfile: preparedDockerfile.signature,
18
- tools: tools.map((tool) => tool.npmPackage)
19
+ tools: imageSignatureFields(tools)
19
20
  }))
20
21
  .digest('hex')
21
22
  .slice(0, 12);
@@ -38,6 +39,8 @@ export function buildArgs(config, tools, dockerfilePath, imageSignature, { engin
38
39
  `HOST_GID=${hostGid}`,
39
40
  '--build-arg',
40
41
  `AI_TOOL_PACKAGES=${toolNpmPackagesArg(tools)}`,
42
+ '--build-arg',
43
+ `AI_TOOLS_SHELL_INSTALL_B64=${toolShellInstallScriptBase64(tools)}`,
41
44
  '--label',
42
45
  sandboxLabel(config),
43
46
  '--label',
@@ -51,11 +54,6 @@ export function buildArgs(config, tools, dockerfilePath, imageSignature, { engin
51
54
  }
52
55
  return args;
53
56
  }
54
- function removeImageIfPresent(imageName, engine) {
55
- if (runOkEngine(engine, 'docker', ['image', 'inspect', imageName])) {
56
- runEngine(engine, 'docker', ['rmi', imageName]);
57
- }
58
- }
59
57
  export async function rebuild(args) {
60
58
  const { values } = parseArgs({
61
59
  args,
@@ -83,9 +81,6 @@ export async function rebuild(args) {
83
81
  try {
84
82
  if (quiet) {
85
83
  const spinner = p.spinner();
86
- spinner.start(`Removing old image ${config.imageName}...`);
87
- removeImageIfPresent(config.imageName, engine);
88
- spinner.stop('Old image removed');
89
84
  spinner.start('Building image...');
90
85
  runEngine(engine, 'docker', buildArgs(config, tools, preparedDockerfile.path, imageSignature, { engine, refresh }), {
91
86
  cwd: config.repoRoot
@@ -93,12 +88,11 @@ export async function rebuild(args) {
93
88
  spinner.stop(pc.green('Sandbox image rebuilt'));
94
89
  }
95
90
  else {
96
- p.log.step(`Removing old image ${config.imageName}`);
97
- removeImageIfPresent(config.imageName, engine);
98
91
  p.log.step('Building image');
99
92
  runVerboseEngine(engine, 'docker', buildArgs(config, tools, preparedDockerfile.path, imageSignature, { engine, refresh }), { cwd: config.repoRoot });
100
93
  p.log.success(pc.green('Sandbox image rebuilt'));
101
94
  }
95
+ pruneSandboxDanglingImages(config, engine);
102
96
  }
103
97
  finally {
104
98
  preparedDockerfile.cleanup();
@@ -6,6 +6,7 @@ import pc from 'picocolors';
6
6
  import { loadConfig } from "../config.js";
7
7
  import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel, shareBranchDir, shellConfigDirCandidates, worktreeDirCandidates } from "../constants.js";
8
8
  import { ENGINES, detectEngine, engineDisplayName, isManagedEngine, stopManagedVm } from "../engine.js";
9
+ import { pruneSandboxDanglingImages } from "../image-prune.js";
9
10
  import { removeManagedDir, removeWorktreeDir } from "../managed-fs.js";
10
11
  import { runOk, runSafe, runSafeEngine } from "../shell.js";
11
12
  import { resolveTaskBranch } from "../task-resolver.js";
@@ -174,6 +175,7 @@ async function rmAll(config, tools) {
174
175
  if (!p.isCancel(shouldRemoveImage) && shouldRemoveImage) {
175
176
  runSafeEngine(engine, 'docker', ['rmi', config.imageName]);
176
177
  }
178
+ pruneSandboxDanglingImages(config, engine);
177
179
  if (isManagedEngine(engine)) {
178
180
  if (engine === ENGINES.WSL2) {
179
181
  p.log.warn('Windows uses Docker Desktop with WSL2. Stop it from Docker Desktop or run "wsl --shutdown" manually.');
@@ -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,
@@ -0,0 +1,18 @@
1
+ import * as p from '@clack/prompts';
2
+ import { sandboxLabel } from "./constants.js";
3
+ import { runEngine } from "./shell.js";
4
+ export function pruneSandboxDanglingImages(config, engine) {
5
+ try {
6
+ runEngine(engine, 'docker', [
7
+ 'image',
8
+ 'prune',
9
+ '-f',
10
+ '--filter',
11
+ `label=${sandboxLabel(config)}`
12
+ ]);
13
+ }
14
+ catch {
15
+ p.log.warn(`Failed to prune dangling sandbox images (label=${sandboxLabel(config)}); leaving them in place.`);
16
+ }
17
+ }
18
+ //# sourceMappingURL=image-prune.js.map
@@ -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,7 +1,20 @@
1
+ import { spawnSync } from 'node:child_process';
1
2
  import fs from 'node:fs';
2
3
  import path from 'node:path';
3
4
  const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
5
+ const SHORT_ID_RE = /^#\d+$/;
4
6
  const WORKSPACE_DIRS = ['active', 'completed', 'blocked', 'archive'];
7
+ function resolveShortIdStrict(arg, repoRoot) {
8
+ const scriptPath = path.join(repoRoot, '.agents', 'scripts', 'task-short-id.js');
9
+ if (!fs.existsSync(scriptPath)) {
10
+ throw new Error(`Short id '${arg}' provided but task-short-id.js script is missing at ${scriptPath}`);
11
+ }
12
+ const result = spawnSync('node', [scriptPath, 'resolve', arg], { encoding: 'utf8', cwd: repoRoot });
13
+ if (result.status !== 0) {
14
+ throw new Error(`Short id '${arg}' not found in active task registry: ${(result.stderr || '').trim()}`);
15
+ }
16
+ return result.stdout.trim();
17
+ }
5
18
  function stripQuotes(value) {
6
19
  return value.replace(/^(["'])(.*)\1$/, '$2');
7
20
  }
@@ -26,6 +39,11 @@ function resolveBranchFromTaskContent(content, taskId) {
26
39
  throw new Error(`Task ${taskId} has no branch field in task.md`);
27
40
  }
28
41
  export function resolveTaskBranch(arg, repoRoot) {
42
+ if (SHORT_ID_RE.test(arg)) {
43
+ const taskId = resolveShortIdStrict(arg, repoRoot);
44
+ const content = readTaskContent(repoRoot, taskId);
45
+ return resolveBranchFromTaskContent(content, taskId);
46
+ }
29
47
  if (!TASK_ID_RE.test(arg)) {
30
48
  return arg;
31
49
  }