@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
package/lib/init.ts CHANGED
@@ -3,11 +3,18 @@ import path from 'node:path';
3
3
  import { execSync } from 'node:child_process';
4
4
  import { platform } from 'node:os';
5
5
  import { info, ok, err } from './log.ts';
6
- import { prompt, select, closePrompt } from './prompt.ts';
6
+ import { prompt, select, multiSelect, closePrompt } from './prompt.ts';
7
7
  import { resolveTemplateDir } from './paths.ts';
8
8
  import { renderFile, copySkillDir, KNOWN_PLATFORMS } from './render.ts';
9
9
  import { enginesForPlatform } from './sandbox/engines/index.ts';
10
10
  import { VERSION } from './version.ts';
11
+ import {
12
+ BUILTIN_TUI_IDS,
13
+ BUILTIN_TUI_DISPLAY,
14
+ isPathOwnedByDisabledTUI,
15
+ resolveEnabledTUIs
16
+ } from './builtin-tuis.ts';
17
+ import type { BuiltinTUIId } from './builtin-tuis.ts';
11
18
 
12
19
  type FileRegistry = {
13
20
  managed: string[];
@@ -23,7 +30,9 @@ type SourceEntry = {
23
30
  type Defaults = {
24
31
  files: FileRegistry;
25
32
  sandbox: Record<string, unknown>;
33
+ task: { shortIdLength: number };
26
34
  labels: Record<string, unknown>;
35
+ requiresPullRequest: boolean;
27
36
  };
28
37
 
29
38
  type AgentConfig = {
@@ -31,10 +40,13 @@ type AgentConfig = {
31
40
  org: string;
32
41
  language: string;
33
42
  platform: { type: string };
43
+ requiresPullRequest: boolean;
34
44
  templateVersion: string;
35
45
  sandbox: Record<string, unknown>;
46
+ task: { shortIdLength: number };
36
47
  labels: Record<string, unknown>;
37
48
  files: FileRegistry;
49
+ tuis: string[];
38
50
  templates?: { sources: SourceEntry[] };
39
51
  skills?: { sources: SourceEntry[] };
40
52
  };
@@ -58,10 +70,15 @@ function isPathOwnedByOtherPlatform(relativePath: string, platformType: string):
58
70
  return candidate !== platformType;
59
71
  }
60
72
 
61
- function buildDefaultFiles(platformType: string): FileRegistry {
73
+ function buildDefaultFiles(platformType: string, enabledTUIs: Set<BuiltinTUIId>): FileRegistry {
74
+ const ownedByDisabled = (entry: string) => isPathOwnedByDisabledTUI(entry, enabledTUIs);
62
75
  return {
63
- managed: (defaults.files.managed || []).filter((entry) => !isPathOwnedByOtherPlatform(entry, platformType)),
64
- merged: (defaults.files.merged || []).filter((entry) => !isPathOwnedByOtherPlatform(entry, platformType)),
76
+ managed: (defaults.files.managed || []).filter(
77
+ (entry) => !isPathOwnedByOtherPlatform(entry, platformType) && !ownedByDisabled(entry)
78
+ ),
79
+ merged: (defaults.files.merged || []).filter(
80
+ (entry) => !isPathOwnedByOtherPlatform(entry, platformType) && !ownedByDisabled(entry)
81
+ ),
65
82
  ejected: structuredClone(defaults.files.ejected || [])
66
83
  };
67
84
  }
@@ -207,6 +224,27 @@ async function cmdInit(): Promise<void> {
207
224
  );
208
225
  }
209
226
 
227
+ const requiresPRChoice = await select(
228
+ 'Require Pull Request flow?',
229
+ ['yes', 'no'],
230
+ 'yes'
231
+ );
232
+ const requiresPullRequest = requiresPRChoice !== 'no';
233
+
234
+ let enabledTUIs: string[];
235
+ try {
236
+ enabledTUIs = await multiSelect(
237
+ 'Built-in TUI command files to install/manage',
238
+ BUILTIN_TUI_IDS.map((id) => ({ id, label: BUILTIN_TUI_DISPLAY[id] }))
239
+ );
240
+ } catch (e) {
241
+ err(e instanceof Error ? e.message : String(e));
242
+ closePrompt();
243
+ process.exitCode = 1;
244
+ return;
245
+ }
246
+ const enabledTUISet = resolveEnabledTUIs(enabledTUIs);
247
+
210
248
  const templateSources = parseLocalSources(await prompt(
211
249
  'Template sources (optional, comma-separated local paths, e.g. ~/my-templates; Enter to skip)',
212
250
  ''
@@ -250,29 +288,35 @@ async function cmdInit(): Promise<void> {
250
288
  );
251
289
  ok('Installed .agents/skills/update-agent-infra/');
252
290
 
253
- // install Claude command
254
- renderFile(
255
- path.join(templateDir, '.claude', 'commands', claudeSrc),
256
- path.join('.claude', 'commands', 'update-agent-infra.md'),
257
- replacements
258
- );
259
- ok('Installed .claude/commands/update-agent-infra.md');
291
+ // install Claude command (only if enabled)
292
+ if (enabledTUISet.has('claude-code')) {
293
+ renderFile(
294
+ path.join(templateDir, '.claude', 'commands', claudeSrc),
295
+ path.join('.claude', 'commands', 'update-agent-infra.md'),
296
+ replacements
297
+ );
298
+ ok('Installed .claude/commands/update-agent-infra.md');
299
+ }
260
300
 
261
- // install Gemini command
262
- renderFile(
263
- path.join(templateDir, '.gemini', 'commands', '_project_', geminiSrc),
264
- path.join('.gemini', 'commands', project, 'update-agent-infra.toml'),
265
- replacements
266
- );
267
- ok(`Installed .gemini/commands/${project}/update-agent-infra.toml`);
301
+ // install Gemini command (only if enabled)
302
+ if (enabledTUISet.has('gemini-cli')) {
303
+ renderFile(
304
+ path.join(templateDir, '.gemini', 'commands', '_project_', geminiSrc),
305
+ path.join('.gemini', 'commands', project, 'update-agent-infra.toml'),
306
+ replacements
307
+ );
308
+ ok(`Installed .gemini/commands/${project}/update-agent-infra.toml`);
309
+ }
268
310
 
269
- // install OpenCode command
270
- renderFile(
271
- path.join(templateDir, '.opencode', 'commands', opencodeSrc),
272
- path.join('.opencode', 'commands', 'update-agent-infra.md'),
273
- replacements
274
- );
275
- ok('Installed .opencode/commands/update-agent-infra.md');
311
+ // install OpenCode command (only if enabled)
312
+ if (enabledTUISet.has('opencode')) {
313
+ renderFile(
314
+ path.join(templateDir, '.opencode', 'commands', opencodeSrc),
315
+ path.join('.opencode', 'commands', 'update-agent-infra.md'),
316
+ replacements
317
+ );
318
+ ok('Installed .opencode/commands/update-agent-infra.md');
319
+ }
276
320
 
277
321
  // generate .agents/.airc.json
278
322
  const config: AgentConfig = {
@@ -280,10 +324,13 @@ async function cmdInit(): Promise<void> {
280
324
  org: orgName,
281
325
  language,
282
326
  platform: { type: platformType },
327
+ requiresPullRequest,
283
328
  templateVersion: VERSION,
284
329
  sandbox: structuredClone(defaults.sandbox),
330
+ task: structuredClone(defaults.task),
285
331
  labels: structuredClone(defaults.labels),
286
- files: buildDefaultFiles(platformType)
332
+ files: buildDefaultFiles(platformType, enabledTUISet),
333
+ tuis: enabledTUIs
287
334
  };
288
335
 
289
336
  if (sandboxEngine) {
@@ -310,15 +357,30 @@ async function cmdInit(): Promise<void> {
310
357
  console.log('');
311
358
  ok('Project initialized successfully!');
312
359
  console.log('');
313
- console.log(' Next step: open this project in any AI TUI and run:');
314
- console.log('');
315
- console.log(' Claude Code / OpenCode: /update-agent-infra');
316
- console.log(` Gemini CLI: /${project}:update-agent-infra`);
317
- console.log(' Codex CLI: $update-agent-infra');
318
- console.log('');
319
- console.log(' This will render all templates and set up the full');
320
- console.log(' AI collaboration infrastructure.');
321
- console.log('');
360
+ if (enabledTUISet.size === 0) {
361
+ console.log(' No built-in TUI selected.');
362
+ console.log(` Configure "customTUIs" in ${configPath} before running update-agent-infra.`);
363
+ console.log('');
364
+ } else {
365
+ console.log(' Next step: open this project in any AI TUI and run:');
366
+ console.log('');
367
+ const claudeOrOpencode: string[] = [];
368
+ if (enabledTUISet.has('claude-code')) claudeOrOpencode.push('Claude Code');
369
+ if (enabledTUISet.has('opencode')) claudeOrOpencode.push('OpenCode');
370
+ if (claudeOrOpencode.length > 0) {
371
+ console.log(` ${claudeOrOpencode.join(' / ')}: /update-agent-infra`);
372
+ }
373
+ if (enabledTUISet.has('gemini-cli')) {
374
+ console.log(` Gemini CLI: /${project}:update-agent-infra`);
375
+ }
376
+ if (enabledTUISet.has('codex')) {
377
+ console.log(' Codex CLI: $update-agent-infra');
378
+ }
379
+ console.log('');
380
+ console.log(' This will render all templates and set up the full');
381
+ console.log(' AI collaboration infrastructure.');
382
+ console.log('');
383
+ }
322
384
  }
323
385
 
324
386
  export { cmdInit };
package/lib/prompt.ts CHANGED
@@ -86,6 +86,59 @@ async function select(question: string, choices: string[], defaultValue?: string
86
86
  return trimmed;
87
87
  }
88
88
 
89
+ async function multiSelect(
90
+ question: string,
91
+ choices: { id: string; label: string }[]
92
+ ): Promise<string[]> {
93
+ process.stdout.write(` ${question}:\n`);
94
+ const idWidth = Math.max(...choices.map((c) => c.id.length));
95
+ choices.forEach((c, i) => {
96
+ process.stdout.write(` ${i + 1}) ${c.id.padEnd(idWidth)} (${c.label})\n`);
97
+ });
98
+ ask('Enter comma-separated numbers or ids to keep, or "none" to select nothing [default: all]: ');
99
+
100
+ setupInterface();
101
+
102
+ const line = await nextLine();
103
+ // Strictly distinguish bare Enter (null/empty string) from whitespace input.
104
+ if (line === null || line === '') return choices.map((c) => c.id);
105
+ // Explicit empty selection: "none" means deliberately zero built-in choices.
106
+ if (line.trim().toLowerCase() === 'none') return [];
107
+
108
+ const tokens = line.split(',').map((t) => t.trim());
109
+ if (tokens.some((t) => t === '')) {
110
+ throw new Error(`Invalid selection input: "${line}" (empty token)`);
111
+ }
112
+
113
+ const idSet = new Set(choices.map((c) => c.id));
114
+ const seenIds = new Set<string>();
115
+ for (const t of tokens) {
116
+ let resolvedId: string | undefined;
117
+ if (/^[0-9]+$/.test(t)) {
118
+ const n = Number.parseInt(t, 10);
119
+ if (n < 1 || n > choices.length) {
120
+ throw new Error(`Selection out of range: "${t}" (expected 1..${choices.length})`);
121
+ }
122
+ resolvedId = choices[n - 1]!.id;
123
+ } else if (idSet.has(t)) {
124
+ resolvedId = t;
125
+ } else {
126
+ throw new Error(`Unknown TUI selection token: "${t}"`);
127
+ }
128
+ if (seenIds.has(resolvedId)) {
129
+ throw new Error(`Duplicate selection: "${t}" resolves to already-selected "${resolvedId}"`);
130
+ }
131
+ seenIds.add(resolvedId);
132
+ }
133
+
134
+ // Normalize to prompt order: users can type tokens in any order, but the
135
+ // persisted array follows the canonical choices order to keep .airc.json
136
+ // diffs stable. An empty result here is impossible (tokens.length > 0 and
137
+ // every token resolves to an id), so no separate empty guard is needed —
138
+ // explicit "none" handled above.
139
+ return choices.map((c) => c.id).filter((id) => seenIds.has(id));
140
+ }
141
+
89
142
  function closePrompt(): void {
90
143
  if (_rl) {
91
144
  _rl.close();
@@ -94,4 +147,4 @@ function closePrompt(): void {
94
147
  }
95
148
  }
96
149
 
97
- export { prompt, select, closePrompt };
150
+ export { prompt, select, multiSelect, closePrompt };
@@ -36,7 +36,13 @@ import {
36
36
  runVerboseEngine
37
37
  } from '../shell.ts';
38
38
  import { resolveTaskBranch } from '../task-resolver.ts';
39
- import { resolveTools, toolConfigDirCandidates, toolNpmPackagesArg } from '../tools.ts';
39
+ import {
40
+ imageSignatureFields,
41
+ resolveTools,
42
+ toolConfigDirCandidates,
43
+ toolNpmPackagesArg,
44
+ toolShellInstallScriptBase64
45
+ } from '../tools.ts';
40
46
  import type { SandboxTool } from '../tools.ts';
41
47
  import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.ts';
42
48
  import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from '../clipboard/paths.ts';
@@ -113,7 +119,7 @@ function buildSignature(preparedDockerfile: PreparedDockerfile, tools: SandboxTo
113
119
  return createHash('sha256')
114
120
  .update(JSON.stringify({
115
121
  dockerfile: preparedDockerfile.signature,
116
- tools: tools.map((tool) => tool.npmPackage)
122
+ tools: imageSignatureFields(tools)
117
123
  }))
118
124
  .digest('hex')
119
125
  .slice(0, 12);
@@ -1063,6 +1069,8 @@ export function buildImage(
1063
1069
  `HOST_GID=${hostGid}`,
1064
1070
  '--build-arg',
1065
1071
  `AI_TOOL_PACKAGES=${toolNpmPackagesArg(tools)}`,
1072
+ '--build-arg',
1073
+ `AI_TOOLS_SHELL_INSTALL_B64=${toolShellInstallScriptBase64(tools)}`,
1066
1074
  '--label',
1067
1075
  sandboxLabel(config),
1068
1076
  '--label',
@@ -1,5 +1,5 @@
1
1
  import { loadConfig } from '../config.ts';
2
- import { assertValidBranchName, containerNameCandidates } from '../constants.ts';
2
+ import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel } from '../constants.ts';
3
3
  import { detectEngine } from '../engine.ts';
4
4
  import {
5
5
  formatCredentialWarnings,
@@ -13,8 +13,12 @@ import { resolveTaskBranch } from '../task-resolver.ts';
13
13
  import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.ts';
14
14
  import { runInteractiveWithClipboardBridge } from '../clipboard/bridge.ts';
15
15
  import { detectHostTimezone } from '../host-timezone.ts';
16
+ import { fetchSandboxRows, isTaskShortRef, resolveTaskShortRef } from './list-running.ts';
16
17
 
17
- const USAGE = `Usage: ai sandbox exec <branch> [cmd...]`;
18
+ const USAGE = `Usage: ai sandbox exec <branch | TASK-id | '#N'> [cmd...]
19
+
20
+ '#N' references the N-th running sandbox in 'ai sandbox ls' order (1-based).
21
+ Quote it as '#N' to avoid shell '#' comment handling.`;
18
22
  const TMUX_ENTRY_PATH = '/usr/local/bin/sandbox-tmux-entry';
19
23
 
20
24
  // Terminal-detection variables that interactive TUIs (e.g. claude-code)
@@ -115,8 +119,14 @@ export async function enter(args: string[]): Promise<number> {
115
119
  const config = loadConfig();
116
120
  validateClaudeCredentialsEnvOverride();
117
121
  const engine = detectEngine(config);
118
- const [branchOrTaskId = '', ...cmd] = args;
119
- const branch = resolveTaskBranch(branchOrTaskId, config.repoRoot);
122
+ const [firstArg = '', ...cmd] = args;
123
+ let branch: string;
124
+ if (isTaskShortRef(firstArg)) {
125
+ const { running } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
126
+ branch = resolveTaskShortRef(firstArg, { running, repoRoot: config.repoRoot });
127
+ } else {
128
+ branch = resolveTaskBranch(firstArg, config.repoRoot);
129
+ }
120
130
  assertValidBranchName(branch);
121
131
  const running = runSafeEngine(engine, 'docker', ['ps', '--format', '{{.Names}}']).split('\n');
122
132
  const container = containerNameCandidates(config, branch).find((name) => running.includes(name));
@@ -0,0 +1,188 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { runSafeEngine } from '../shell.ts';
5
+
6
+ export type SandboxRow = {
7
+ name: string;
8
+ status: string;
9
+ branch: string;
10
+ running: boolean;
11
+ index: number | null;
12
+ };
13
+
14
+ export function containerListFormat(): string {
15
+ return '{{.Names}}\t{{.Status}}\t{{.Labels}}';
16
+ }
17
+
18
+ export function parseLabels(csv: string): Record<string, string> {
19
+ if (!csv) {
20
+ return {};
21
+ }
22
+
23
+ const labels: Record<string, string> = {};
24
+ for (const pair of csv.split(',')) {
25
+ if (!pair) {
26
+ continue;
27
+ }
28
+ const eq = pair.indexOf('=');
29
+ if (eq < 0) {
30
+ continue;
31
+ }
32
+ labels[pair.slice(0, eq)] = pair.slice(eq + 1);
33
+ }
34
+ return labels;
35
+ }
36
+
37
+ export function parseSandboxRows(rawOutput: string, branchKey: string): SandboxRow[] {
38
+ if (!rawOutput) {
39
+ return [];
40
+ }
41
+ return rawOutput.split('\n').map((line) => {
42
+ const [name = '', status = '', labelsCsv = ''] = line.split('\t');
43
+ const branch = parseLabels(labelsCsv)[branchKey] ?? '';
44
+ return {
45
+ name,
46
+ status,
47
+ branch,
48
+ running: status.startsWith('Up '),
49
+ index: null
50
+ };
51
+ });
52
+ }
53
+
54
+ export function sortAndIndexSandboxRows(rows: SandboxRow[]): {
55
+ running: SandboxRow[];
56
+ nonRunning: SandboxRow[];
57
+ } {
58
+ const byName = (a: SandboxRow, b: SandboxRow): number => {
59
+ if (a.name < b.name) return -1;
60
+ if (a.name > b.name) return 1;
61
+ return 0;
62
+ };
63
+ const running = rows.filter((row) => row.running).sort(byName).map((row, i) => ({
64
+ ...row,
65
+ index: i + 1
66
+ }));
67
+ const nonRunning = rows.filter((row) => !row.running).sort(byName).map((row) => ({
68
+ ...row,
69
+ index: null
70
+ }));
71
+ return { running, nonRunning };
72
+ }
73
+
74
+ export function fetchSandboxRows(
75
+ engine: string,
76
+ label: string,
77
+ branchKey: string
78
+ ): { running: SandboxRow[]; nonRunning: SandboxRow[] } {
79
+ const raw = runSafeEngine(engine, 'docker', [
80
+ 'ps',
81
+ '-a',
82
+ '--filter',
83
+ `label=${label}`,
84
+ '--format',
85
+ containerListFormat()
86
+ ]);
87
+ return sortAndIndexSandboxRows(parseSandboxRows(raw, branchKey));
88
+ }
89
+
90
+ /**
91
+ * Returns true iff `arg` is a syntactically valid task short reference ('#N').
92
+ * Zero IO. Callers MUST use this as the gate before constructing any context
93
+ * for resolveTaskShortRef — that way non-matching arguments (e.g. '#abc',
94
+ * '#1.5', '#') never trigger sandbox list IO.
95
+ */
96
+ export function isTaskShortRef(arg: string): boolean {
97
+ return /^#\d+$/.test(arg);
98
+ }
99
+
100
+ type RegistryLookup =
101
+ | { status: 'miss' }
102
+ | { status: 'hit'; branch: string };
103
+
104
+ /**
105
+ * Try to resolve a short ref against the global task-short-id registry.
106
+ *
107
+ * Tri-state semantics (review-code Round 1 M-1 fix):
108
+ * - 'miss' → script reports no entry (or registry script missing). Caller may fall back.
109
+ * - 'hit' → registry resolved to a task id and branch is found in task.md.
110
+ * - throws → registry hit but task.md is missing or branch metadata is unparseable;
111
+ * surfacing this error is critical — never silently fall back to running index.
112
+ */
113
+ function tryResolveFromRegistry(arg: string, repoRoot: string): RegistryLookup {
114
+ const scriptPath = path.join(repoRoot, '.agents', 'scripts', 'task-short-id.js');
115
+ if (!fs.existsSync(scriptPath)) return { status: 'miss' };
116
+ const result = spawnSync('node', [scriptPath, 'resolve', arg], { encoding: 'utf8', cwd: repoRoot });
117
+ if (result.status !== 0) return { status: 'miss' };
118
+ const taskId = (result.stdout || '').trim();
119
+ if (!/^TASK-\d{8}-\d{6}$/.test(taskId)) {
120
+ throw new Error(
121
+ `Registry returned malformed task id for '${arg}': ${JSON.stringify(taskId)}`
122
+ );
123
+ }
124
+ for (const sub of ['active', 'completed', 'blocked', 'archive']) {
125
+ const taskMdPath = path.join(repoRoot, '.agents', 'workspace', sub, taskId, 'task.md');
126
+ if (!fs.existsSync(taskMdPath)) continue;
127
+ const content = fs.readFileSync(taskMdPath, 'utf8');
128
+ const fm = content.match(/^branch:\s*(.+)$/m);
129
+ if (fm?.[1]?.trim()) {
130
+ return { status: 'hit', branch: fm[1].trim().replace(/^(["'])(.*)\1$/, '$2') };
131
+ }
132
+ const ctx = content.match(/^- \*\*(?:分支|Branch)\*\*:[ \t]*`?([^`\n]+)`?$/m);
133
+ if (ctx?.[1]?.trim()) {
134
+ return { status: 'hit', branch: ctx[1].trim().replace(/^(["'])(.*)\1$/, '$2') };
135
+ }
136
+ throw new Error(
137
+ `Short ref '${arg}' resolved to task ${taskId} but task.md has no branch field`
138
+ );
139
+ }
140
+ throw new Error(
141
+ `Short ref '${arg}' resolved to task ${taskId} but task.md was not found under any workspace dir`
142
+ );
143
+ }
144
+
145
+ function resolveByRunningIndex(arg: string, running: SandboxRow[]): string {
146
+ const n = Number(arg.slice(1));
147
+ if (n < 1) {
148
+ throw new Error(`Invalid sandbox index '${arg}': must be >= 1`);
149
+ }
150
+ if (running.length === 0) {
151
+ throw new Error(`No running sandbox to reference with '${arg}'`);
152
+ }
153
+ if (n > running.length) {
154
+ throw new Error(
155
+ `No running sandbox at index '${arg}' (only ${running.length} running)`
156
+ );
157
+ }
158
+ const row = running[n - 1]!;
159
+ if (!row.branch) {
160
+ throw new Error(
161
+ `Cannot resolve branch for sandbox '${arg}' (container '${row.name}' missing branch label)`
162
+ );
163
+ }
164
+ return row.branch;
165
+ }
166
+
167
+ /**
168
+ * Resolve a task short reference ('#N') to a branch name for the sandbox entrypoint.
169
+ *
170
+ * Resolution order (sandbox fallback mode, plan-r7 C2):
171
+ * 1. Try the global task-short-id registry under repoRoot. If hit, look up the
172
+ * branch from the matching task.md.
173
+ * 2. Fallback to the running-sandbox list index (preserves the #414 ls-index
174
+ * behaviour; long-term contract per analysis-r5).
175
+ *
176
+ * Precondition: callers MUST gate on isTaskShortRef(arg) === true.
177
+ */
178
+ export function resolveTaskShortRef(
179
+ arg: string,
180
+ ctx: { running: SandboxRow[]; repoRoot?: string }
181
+ ): string {
182
+ if (ctx.repoRoot) {
183
+ const lookup = tryResolveFromRegistry(arg, ctx.repoRoot);
184
+ if (lookup.status === 'hit') return lookup.branch;
185
+ // 'miss' falls through to ls-index fallback (preserves #414 behaviour); 'hit-but-invalid' already threw above.
186
+ }
187
+ return resolveByRunningIndex(arg, ctx.running);
188
+ }
@@ -5,51 +5,36 @@ import pc from 'picocolors';
5
5
  import { loadConfig } from '../config.ts';
6
6
  import { sandboxBranchLabel, sandboxLabel } from '../constants.ts';
7
7
  import { detectEngine } from '../engine.ts';
8
- import { runSafeEngine } from '../shell.ts';
9
8
  import { resolveTools, toolProjectDirCandidates } from '../tools.ts';
9
+ import { fetchSandboxRows } from './list-running.ts';
10
10
 
11
- const USAGE = 'Usage: ai sandbox ls';
12
- const CONTAINER_TABLE_HEADERS = ['NAMES', 'STATUS', 'BRANCH'] as const;
11
+ export { containerListFormat, parseLabels } from './list-running.ts';
12
+
13
+ const USAGE = `Usage: ai sandbox ls
14
+
15
+ Lists all containers for the current project. The leftmost '#' column
16
+ numbers running sandboxes; use it as "ai sandbox exec '#N'" to enter one.
17
+ Quote '#N' to avoid shell '#' comment handling.`;
18
+
19
+ const CONTAINER_TABLE_HEADERS = ['#', 'NAMES', 'STATUS', 'BRANCH'] as const;
13
20
 
14
21
  type ContainerTableRow = {
22
+ index: string;
15
23
  name: string;
16
24
  status: string;
17
25
  branch: string;
18
26
  };
19
27
 
20
- // Exported to lock the docker/podman-compatible format in unit tests.
21
- export function containerListFormat(): string {
22
- return '{{.Names}}\t{{.Status}}\t{{.Labels}}';
23
- }
24
-
25
- export function parseLabels(csv: string): Record<string, string> {
26
- if (!csv) {
27
- return {};
28
- }
29
-
30
- const labels: Record<string, string> = {};
31
- for (const pair of csv.split(',')) {
32
- if (!pair) {
33
- continue;
34
- }
35
- const eq = pair.indexOf('=');
36
- if (eq < 0) {
37
- continue;
38
- }
39
- labels[pair.slice(0, eq)] = pair.slice(eq + 1);
40
- }
41
- return labels;
42
- }
43
-
44
28
  export function formatContainerTable(rows: ContainerTableRow[]): string[] {
45
- const columns = rows.map((row) => [row.name, row.status, row.branch] as const);
29
+ const columns = rows.map((row) => [row.index, row.name, row.status, row.branch] as const);
46
30
  const widths = [
47
- Math.max(CONTAINER_TABLE_HEADERS[0].length, ...rows.map((row) => row.name.length)),
48
- Math.max(CONTAINER_TABLE_HEADERS[1].length, ...rows.map((row) => row.status.length)),
49
- Math.max(CONTAINER_TABLE_HEADERS[2].length, ...rows.map((row) => row.branch.length))
31
+ Math.max(CONTAINER_TABLE_HEADERS[0].length, ...rows.map((row) => row.index.length)),
32
+ Math.max(CONTAINER_TABLE_HEADERS[1].length, ...rows.map((row) => row.name.length)),
33
+ Math.max(CONTAINER_TABLE_HEADERS[2].length, ...rows.map((row) => row.status.length)),
34
+ Math.max(CONTAINER_TABLE_HEADERS[3].length, ...rows.map((row) => row.branch.length))
50
35
  ] as const;
51
- const renderRow = (values: readonly [string, string, string]): string =>
52
- `${values[0].padEnd(widths[0])} ${values[1].padEnd(widths[1])} ${values[2]}`.trimEnd();
36
+ const renderRow = (values: readonly [string, string, string, string]): string =>
37
+ `${values[0].padEnd(widths[0])} ${values[1].padEnd(widths[1])} ${values[2].padEnd(widths[2])} ${values[3]}`.trimEnd();
53
38
 
54
39
  return [
55
40
  renderRow(CONTAINER_TABLE_HEADERS),
@@ -75,28 +60,22 @@ export function ls(args: string[] = []): void {
75
60
  const engine = detectEngine(config);
76
61
  const tools = resolveTools(config);
77
62
  const label = sandboxLabel(config);
78
- const containers = runSafeEngine(engine, 'docker', [
79
- 'ps',
80
- '-a',
81
- '--filter',
82
- `label=${label}`,
83
- '--format',
84
- containerListFormat()
85
- ]);
63
+ const { running, nonRunning } = fetchSandboxRows(engine, label, sandboxBranchLabel(config));
86
64
 
87
65
  p.intro(pc.cyan(`Sandbox status for ${config.project}`));
88
66
 
89
67
  p.log.step('Containers');
90
- if (!containers) {
68
+ const ordered = [...running, ...nonRunning];
69
+ if (ordered.length === 0) {
91
70
  p.log.warn(' No sandbox containers');
92
71
  } else {
93
- const branchKey = sandboxBranchLabel(config);
94
- const rows = containers.split('\n').map((line) => {
95
- const [name = '', status = '', labelsCsv = ''] = line.split('\t');
96
- const branch = parseLabels(labelsCsv)[branchKey] ?? '';
97
- return { name, status, branch };
98
- });
99
- for (const line of formatContainerTable(rows)) {
72
+ const tableRows: ContainerTableRow[] = ordered.map((row) => ({
73
+ index: row.index === null ? '' : String(row.index),
74
+ name: row.name,
75
+ status: row.status,
76
+ branch: row.branch
77
+ }));
78
+ for (const line of formatContainerTable(tableRows)) {
100
79
  process.stdout.write(` ${line}\n`);
101
80
  }
102
81
  }