@fitlab-ai/agent-infra 0.7.0 → 0.7.2

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 (156) hide show
  1. package/bin/cli.ts +12 -1
  2. package/dist/bin/cli.js +13 -1
  3. package/dist/lib/builtin-tuis.js +45 -0
  4. package/dist/lib/defaults.json +3 -0
  5. package/dist/lib/init.js +62 -23
  6. package/dist/lib/prompt.js +49 -1
  7. package/dist/lib/sandbox/commands/create.js +10 -2
  8. package/dist/lib/sandbox/commands/enter.js +8 -7
  9. package/dist/lib/sandbox/commands/list-running.js +62 -28
  10. package/dist/lib/sandbox/commands/ls.js +20 -22
  11. package/dist/lib/sandbox/commands/rebuild.js +3 -11
  12. package/dist/lib/sandbox/commands/rm.js +2 -0
  13. package/dist/lib/sandbox/image-prune.js +18 -0
  14. package/dist/lib/sandbox/index.js +7 -3
  15. package/dist/lib/sandbox/task-resolver.js +18 -0
  16. package/dist/lib/sandbox/tools.js +1 -1
  17. package/dist/lib/table.js +29 -0
  18. package/dist/lib/task/commands/ls.js +122 -0
  19. package/dist/lib/task/commands/show.js +135 -0
  20. package/dist/lib/task/frontmatter.js +32 -0
  21. package/dist/lib/task/index.js +41 -0
  22. package/dist/lib/task/short-id.js +80 -0
  23. package/dist/lib/update.js +59 -18
  24. package/lib/builtin-tuis.ts +55 -0
  25. package/lib/defaults.json +3 -0
  26. package/lib/init.ts +87 -35
  27. package/lib/prompt.ts +54 -1
  28. package/lib/sandbox/commands/create.ts +11 -2
  29. package/lib/sandbox/commands/enter.ts +8 -7
  30. package/lib/sandbox/commands/list-running.ts +70 -31
  31. package/lib/sandbox/commands/ls.ts +25 -25
  32. package/lib/sandbox/commands/rebuild.ts +3 -12
  33. package/lib/sandbox/commands/rm.ts +3 -0
  34. package/lib/sandbox/image-prune.ts +23 -0
  35. package/lib/sandbox/index.ts +7 -3
  36. package/lib/sandbox/task-resolver.ts +23 -1
  37. package/lib/sandbox/tools.ts +1 -1
  38. package/lib/table.ts +32 -0
  39. package/lib/task/commands/ls.ts +138 -0
  40. package/lib/task/commands/show.ts +139 -0
  41. package/lib/task/frontmatter.ts +30 -0
  42. package/lib/task/index.ts +44 -0
  43. package/lib/task/short-id.ts +97 -0
  44. package/lib/update.ts +71 -30
  45. package/package.json +1 -1
  46. package/templates/.agents/README.en.md +32 -0
  47. package/templates/.agents/README.zh-CN.md +32 -0
  48. package/templates/.agents/hooks/auto-resume.sh +87 -0
  49. package/templates/.agents/rules/create-issue.github.en.md +1 -1
  50. package/templates/.agents/rules/create-issue.github.zh-CN.md +1 -1
  51. package/templates/.agents/rules/milestone-inference.github.en.md +4 -1
  52. package/templates/.agents/rules/milestone-inference.github.zh-CN.md +4 -1
  53. package/templates/.agents/rules/next-step-output.en.md +59 -0
  54. package/templates/.agents/rules/next-step-output.zh-CN.md +59 -0
  55. package/templates/.agents/rules/task-short-id.en.md +133 -0
  56. package/templates/.agents/rules/task-short-id.zh-CN.md +105 -0
  57. package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +17 -0
  58. package/templates/.agents/scripts/task-short-id.js +556 -0
  59. package/templates/.agents/skills/analyze-task/SKILL.en.md +13 -11
  60. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +13 -12
  61. package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -1
  62. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -1
  63. package/templates/.agents/skills/block-task/SKILL.en.md +17 -5
  64. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +17 -6
  65. package/templates/.agents/skills/block-task/config/verify.json +1 -1
  66. package/templates/.agents/skills/cancel-task/SKILL.en.md +17 -5
  67. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +17 -6
  68. package/templates/.agents/skills/cancel-task/config/verify.json +1 -1
  69. package/templates/.agents/skills/check-task/SKILL.en.md +15 -9
  70. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +15 -10
  71. package/templates/.agents/skills/close-codescan/SKILL.en.md +16 -5
  72. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +16 -5
  73. package/templates/.agents/skills/close-dependabot/SKILL.en.md +16 -5
  74. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +16 -5
  75. package/templates/.agents/skills/code-task/SKILL.en.md +13 -5
  76. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +14 -6
  77. package/templates/.agents/skills/code-task/config/verify.en.json +2 -1
  78. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +2 -1
  79. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +10 -5
  80. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +10 -5
  81. package/templates/.agents/skills/code-task/reference/output-template.en.md +3 -3
  82. package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +3 -3
  83. package/templates/.agents/skills/code-task/reference/report-template.en.md +8 -0
  84. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +8 -0
  85. package/templates/.agents/skills/commit/SKILL.en.md +5 -1
  86. package/templates/.agents/skills/commit/SKILL.zh-CN.md +5 -1
  87. package/templates/.agents/skills/commit/reference/task-status-update.en.md +9 -9
  88. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +9 -9
  89. package/templates/.agents/skills/complete-task/SKILL.en.md +17 -1
  90. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +17 -2
  91. package/templates/.agents/skills/complete-task/config/verify.en.json +1 -1
  92. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -1
  93. package/templates/.agents/skills/create-pr/SKILL.en.md +9 -5
  94. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +9 -5
  95. package/templates/.agents/skills/create-pr/config/verify.json +2 -1
  96. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +1 -1
  97. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +1 -1
  98. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +3 -3
  99. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +3 -3
  100. package/templates/.agents/skills/create-task/SKILL.en.md +29 -15
  101. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +29 -16
  102. package/templates/.agents/skills/create-task/config/verify.json +1 -1
  103. package/templates/.agents/skills/import-codescan/SKILL.en.md +20 -6
  104. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +20 -6
  105. package/templates/.agents/skills/import-codescan/config/verify.json +1 -1
  106. package/templates/.agents/skills/import-dependabot/SKILL.en.md +20 -6
  107. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +20 -6
  108. package/templates/.agents/skills/import-dependabot/config/verify.json +1 -1
  109. package/templates/.agents/skills/import-issue/SKILL.en.md +19 -5
  110. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +19 -5
  111. package/templates/.agents/skills/plan-task/SKILL.en.md +13 -11
  112. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +13 -12
  113. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
  114. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
  115. package/templates/.agents/skills/restore-task/SKILL.en.md +12 -0
  116. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +12 -1
  117. package/templates/.agents/skills/review-analysis/SKILL.en.md +7 -1
  118. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +7 -2
  119. package/templates/.agents/skills/review-analysis/config/verify.en.json +3 -2
  120. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +3 -2
  121. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +15 -15
  122. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +15 -15
  123. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +7 -1
  124. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +7 -1
  125. package/templates/.agents/skills/review-analysis/reference/review-criteria.en.md +2 -0
  126. package/templates/.agents/skills/review-analysis/reference/review-criteria.zh-CN.md +2 -0
  127. package/templates/.agents/skills/review-code/SKILL.en.md +8 -1
  128. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +8 -2
  129. package/templates/.agents/skills/review-code/config/verify.en.json +3 -2
  130. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +3 -2
  131. package/templates/.agents/skills/review-code/reference/output-templates.en.md +9 -9
  132. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +9 -9
  133. package/templates/.agents/skills/review-code/reference/report-template.en.md +7 -1
  134. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +7 -1
  135. package/templates/.agents/skills/review-code/reference/review-criteria.en.md +2 -0
  136. package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +2 -0
  137. package/templates/.agents/skills/review-plan/SKILL.en.md +7 -1
  138. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +7 -2
  139. package/templates/.agents/skills/review-plan/config/verify.en.json +3 -2
  140. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +3 -2
  141. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +15 -15
  142. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +15 -15
  143. package/templates/.agents/skills/review-plan/reference/report-template.en.md +7 -1
  144. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +7 -1
  145. package/templates/.agents/skills/review-plan/reference/review-criteria.en.md +2 -0
  146. package/templates/.agents/skills/review-plan/reference/review-criteria.zh-CN.md +2 -0
  147. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +1 -0
  148. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +1 -0
  149. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +112 -21
  150. package/templates/.agents/workflows/bug-fix.en.yaml +1 -1
  151. package/templates/.agents/workflows/bug-fix.zh-CN.yaml +1 -1
  152. package/templates/.agents/workflows/feature-development.en.yaml +1 -1
  153. package/templates/.agents/workflows/feature-development.zh-CN.yaml +1 -1
  154. package/templates/.agents/workflows/refactoring.en.yaml +1 -1
  155. package/templates/.agents/workflows/refactoring.zh-CN.yaml +1 -1
  156. package/templates/.claude/settings.json +11 -0
package/bin/cli.ts CHANGED
@@ -18,6 +18,7 @@ Usage:
18
18
  agent-infra init Initialize a new project with update-agent-infra seed command
19
19
  agent-infra merge Merge tasks from another workspace directory (active/blocked/completed/archive)
20
20
  agent-infra sandbox Manage Docker-based AI sandboxes
21
+ agent-infra task Read-only views over .agents/workspace tasks (ls / show)
21
22
  agent-infra update Update seed files and sync file registry for an existing project
22
23
  agent-infra version Show version
23
24
 
@@ -93,7 +94,17 @@ switch (command) {
93
94
  const { runSandbox } = imported;
94
95
  await runSandbox(process.argv.slice(3)).catch((e: unknown) => {
95
96
  process.stderr.write(`Error: ${errorMessage(e)}\n`);
96
- process.exitCode = 1;
97
+ process.exit(1);
98
+ });
99
+ break;
100
+ }
101
+ case 'task': {
102
+ const imported = await importCommand('../lib/task/index.ts');
103
+ if (!imported) break;
104
+ const { runTask } = imported;
105
+ await runTask(process.argv.slice(3)).catch((e: unknown) => {
106
+ process.stderr.write(`Error: ${errorMessage(e)}\n`);
107
+ process.exit(1);
97
108
  });
98
109
  break;
99
110
  }
package/dist/bin/cli.js CHANGED
@@ -22,6 +22,7 @@ Usage:
22
22
  agent-infra init Initialize a new project with update-agent-infra seed command
23
23
  agent-infra merge Merge tasks from another workspace directory (active/blocked/completed/archive)
24
24
  agent-infra sandbox Manage Docker-based AI sandboxes
25
+ agent-infra task Read-only views over .agents/workspace tasks (ls / show)
25
26
  agent-infra update Update seed files and sync file registry for an existing project
26
27
  agent-infra version Show version
27
28
 
@@ -96,7 +97,18 @@ switch (command) {
96
97
  const { runSandbox } = imported;
97
98
  await runSandbox(process.argv.slice(3)).catch((e) => {
98
99
  process.stderr.write(`Error: ${errorMessage(e)}\n`);
99
- process.exitCode = 1;
100
+ process.exit(1);
101
+ });
102
+ break;
103
+ }
104
+ case 'task': {
105
+ const imported = await importCommand('../lib/task/index.ts');
106
+ if (!imported)
107
+ break;
108
+ const { runTask } = imported;
109
+ await runTask(process.argv.slice(3)).catch((e) => {
110
+ process.stderr.write(`Error: ${errorMessage(e)}\n`);
111
+ process.exit(1);
100
112
  });
101
113
  break;
102
114
  }
@@ -0,0 +1,45 @@
1
+ const BUILTIN_TUI_IDS = ['claude-code', 'codex', 'gemini-cli', 'opencode'];
2
+ const BUILTIN_TUI_DISPLAY = {
3
+ 'claude-code': 'Claude Code',
4
+ 'codex': 'Codex',
5
+ 'gemini-cli': 'Gemini CLI',
6
+ 'opencode': 'OpenCode'
7
+ };
8
+ const BUILTIN_TUI_OWNED_PATH_PREFIXES = {
9
+ 'claude-code': ['.claude/'],
10
+ 'codex': ['.codex/'],
11
+ 'gemini-cli': ['.gemini/'],
12
+ 'opencode': ['.opencode/']
13
+ };
14
+ function isBuiltinTUIId(value) {
15
+ return typeof value === 'string' && BUILTIN_TUI_IDS.includes(value);
16
+ }
17
+ function resolveEnabledTUIs(value) {
18
+ // Missing field / null / non-array → full set (backward compat for legacy
19
+ // .airc.json predating the `tuis` field).
20
+ if (!Array.isArray(value))
21
+ return new Set(BUILTIN_TUI_IDS);
22
+ // Empty array is a meaningful, user-set value: "no built-in TUI managed".
23
+ // This supports the customTUI-only project layout.
24
+ const set = new Set();
25
+ for (const v of value) {
26
+ if (isBuiltinTUIId(v))
27
+ set.add(v);
28
+ }
29
+ return set;
30
+ }
31
+ function isPathOwnedByDisabledTUI(rel, enabled) {
32
+ const normalized = String(rel || '').replace(/\\/g, '/').replace(/^\.\//, '');
33
+ for (const tui of BUILTIN_TUI_IDS) {
34
+ if (enabled.has(tui))
35
+ continue;
36
+ for (const prefix of BUILTIN_TUI_OWNED_PATH_PREFIXES[tui]) {
37
+ const trimmed = prefix.replace(/\/$/, '');
38
+ if (normalized === trimmed || normalized.startsWith(prefix))
39
+ return true;
40
+ }
41
+ }
42
+ return false;
43
+ }
44
+ export { BUILTIN_TUI_IDS, BUILTIN_TUI_DISPLAY, BUILTIN_TUI_OWNED_PATH_PREFIXES, isBuiltinTUIId, resolveEnabledTUIs, isPathOwnedByDisabledTUI };
45
+ //# sourceMappingURL=builtin-tuis.js.map
@@ -21,6 +21,9 @@
21
21
  "disk": null
22
22
  }
23
23
  },
24
+ "task": {
25
+ "shortIdLength": 2
26
+ },
24
27
  "labels": {
25
28
  "in": {}
26
29
  },
package/dist/lib/init.js CHANGED
@@ -3,11 +3,12 @@ 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.js";
6
- import { prompt, select, closePrompt } from "./prompt.js";
6
+ import { prompt, select, multiSelect, closePrompt } from "./prompt.js";
7
7
  import { resolveTemplateDir } from "./paths.js";
8
8
  import { renderFile, copySkillDir, KNOWN_PLATFORMS } from "./render.js";
9
9
  import { enginesForPlatform } from "./sandbox/engines/index.js";
10
10
  import { VERSION } from "./version.js";
11
+ import { BUILTIN_TUI_IDS, BUILTIN_TUI_DISPLAY, isPathOwnedByDisabledTUI, resolveEnabledTUIs } from "./builtin-tuis.js";
11
12
  const defaults = JSON.parse(fs.readFileSync(new URL('./defaults.json', import.meta.url), 'utf8'));
12
13
  const PLATFORM_DEFAULT_ENGINES = Object.freeze({
13
14
  linux: 'native',
@@ -23,10 +24,11 @@ function isPathOwnedByOtherPlatform(relativePath, platformType) {
23
24
  return false;
24
25
  return candidate !== platformType;
25
26
  }
26
- function buildDefaultFiles(platformType) {
27
+ function buildDefaultFiles(platformType, enabledTUIs) {
28
+ const ownedByDisabled = (entry) => isPathOwnedByDisabledTUI(entry, enabledTUIs);
27
29
  return {
28
- managed: (defaults.files.managed || []).filter((entry) => !isPathOwnedByOtherPlatform(entry, platformType)),
29
- merged: (defaults.files.merged || []).filter((entry) => !isPathOwnedByOtherPlatform(entry, platformType)),
30
+ managed: (defaults.files.managed || []).filter((entry) => !isPathOwnedByOtherPlatform(entry, platformType) && !ownedByDisabled(entry)),
31
+ merged: (defaults.files.merged || []).filter((entry) => !isPathOwnedByOtherPlatform(entry, platformType) && !ownedByDisabled(entry)),
30
32
  ejected: structuredClone(defaults.files.ejected || [])
31
33
  };
32
34
  }
@@ -158,6 +160,17 @@ async function cmdInit() {
158
160
  }
159
161
  const requiresPRChoice = await select('Require Pull Request flow?', ['yes', 'no'], 'yes');
160
162
  const requiresPullRequest = requiresPRChoice !== 'no';
163
+ let enabledTUIs;
164
+ try {
165
+ enabledTUIs = await multiSelect('Built-in TUI command files to install/manage', BUILTIN_TUI_IDS.map((id) => ({ id, label: BUILTIN_TUI_DISPLAY[id] })));
166
+ }
167
+ catch (e) {
168
+ err(e instanceof Error ? e.message : String(e));
169
+ closePrompt();
170
+ process.exitCode = 1;
171
+ return;
172
+ }
173
+ const enabledTUISet = resolveEnabledTUIs(enabledTUIs);
161
174
  const templateSources = parseLocalSources(await prompt('Template sources (optional, comma-separated local paths, e.g. ~/my-templates; Enter to skip)', ''));
162
175
  const skillSources = parseLocalSources(await prompt('Skill sources (optional, comma-separated local paths, e.g. ~/my-skills; Enter to skip)', ''));
163
176
  closePrompt();
@@ -186,15 +199,21 @@ async function cmdInit() {
186
199
  // install skill
187
200
  copySkillDir(path.join(templateDir, '.agents', 'skills', 'update-agent-infra'), path.join('.agents', 'skills', 'update-agent-infra'), replacements, language, platformType);
188
201
  ok('Installed .agents/skills/update-agent-infra/');
189
- // install Claude command
190
- renderFile(path.join(templateDir, '.claude', 'commands', claudeSrc), path.join('.claude', 'commands', 'update-agent-infra.md'), replacements);
191
- ok('Installed .claude/commands/update-agent-infra.md');
192
- // install Gemini command
193
- renderFile(path.join(templateDir, '.gemini', 'commands', '_project_', geminiSrc), path.join('.gemini', 'commands', project, 'update-agent-infra.toml'), replacements);
194
- ok(`Installed .gemini/commands/${project}/update-agent-infra.toml`);
195
- // install OpenCode command
196
- renderFile(path.join(templateDir, '.opencode', 'commands', opencodeSrc), path.join('.opencode', 'commands', 'update-agent-infra.md'), replacements);
197
- ok('Installed .opencode/commands/update-agent-infra.md');
202
+ // install Claude command (only if enabled)
203
+ if (enabledTUISet.has('claude-code')) {
204
+ renderFile(path.join(templateDir, '.claude', 'commands', claudeSrc), path.join('.claude', 'commands', 'update-agent-infra.md'), replacements);
205
+ ok('Installed .claude/commands/update-agent-infra.md');
206
+ }
207
+ // install Gemini command (only if enabled)
208
+ if (enabledTUISet.has('gemini-cli')) {
209
+ renderFile(path.join(templateDir, '.gemini', 'commands', '_project_', geminiSrc), path.join('.gemini', 'commands', project, 'update-agent-infra.toml'), replacements);
210
+ ok(`Installed .gemini/commands/${project}/update-agent-infra.toml`);
211
+ }
212
+ // install OpenCode command (only if enabled)
213
+ if (enabledTUISet.has('opencode')) {
214
+ renderFile(path.join(templateDir, '.opencode', 'commands', opencodeSrc), path.join('.opencode', 'commands', 'update-agent-infra.md'), replacements);
215
+ ok('Installed .opencode/commands/update-agent-infra.md');
216
+ }
198
217
  // generate .agents/.airc.json
199
218
  const config = {
200
219
  project: projectName,
@@ -204,8 +223,10 @@ async function cmdInit() {
204
223
  requiresPullRequest,
205
224
  templateVersion: VERSION,
206
225
  sandbox: structuredClone(defaults.sandbox),
226
+ task: structuredClone(defaults.task),
207
227
  labels: structuredClone(defaults.labels),
208
- files: buildDefaultFiles(platformType)
228
+ files: buildDefaultFiles(platformType, enabledTUISet),
229
+ tuis: enabledTUIs
209
230
  };
210
231
  if (sandboxEngine) {
211
232
  config.sandbox.engine = sandboxEngine;
@@ -227,15 +248,33 @@ async function cmdInit() {
227
248
  console.log('');
228
249
  ok('Project initialized successfully!');
229
250
  console.log('');
230
- console.log(' Next step: open this project in any AI TUI and run:');
231
- console.log('');
232
- console.log(' Claude Code / OpenCode: /update-agent-infra');
233
- console.log(` Gemini CLI: /${project}:update-agent-infra`);
234
- console.log(' Codex CLI: $update-agent-infra');
235
- console.log('');
236
- console.log(' This will render all templates and set up the full');
237
- console.log(' AI collaboration infrastructure.');
238
- console.log('');
251
+ if (enabledTUISet.size === 0) {
252
+ console.log(' No built-in TUI selected.');
253
+ console.log(` Configure "customTUIs" in ${configPath} before running update-agent-infra.`);
254
+ console.log('');
255
+ }
256
+ else {
257
+ console.log(' Next step: open this project in any AI TUI and run:');
258
+ console.log('');
259
+ const claudeOrOpencode = [];
260
+ if (enabledTUISet.has('claude-code'))
261
+ claudeOrOpencode.push('Claude Code');
262
+ if (enabledTUISet.has('opencode'))
263
+ claudeOrOpencode.push('OpenCode');
264
+ if (claudeOrOpencode.length > 0) {
265
+ console.log(` ${claudeOrOpencode.join(' / ')}: /update-agent-infra`);
266
+ }
267
+ if (enabledTUISet.has('gemini-cli')) {
268
+ console.log(` Gemini CLI: /${project}:update-agent-infra`);
269
+ }
270
+ if (enabledTUISet.has('codex')) {
271
+ console.log(' Codex CLI: $update-agent-infra');
272
+ }
273
+ console.log('');
274
+ console.log(' This will render all templates and set up the full');
275
+ console.log(' AI collaboration infrastructure.');
276
+ console.log('');
277
+ }
239
278
  }
240
279
  export { cmdInit };
241
280
  //# sourceMappingURL=init.js.map
@@ -74,6 +74,54 @@ async function select(question, choices, defaultValue) {
74
74
  }
75
75
  return trimmed;
76
76
  }
77
+ async function multiSelect(question, choices) {
78
+ process.stdout.write(` ${question}:\n`);
79
+ const idWidth = Math.max(...choices.map((c) => c.id.length));
80
+ choices.forEach((c, i) => {
81
+ process.stdout.write(` ${i + 1}) ${c.id.padEnd(idWidth)} (${c.label})\n`);
82
+ });
83
+ ask('Enter comma-separated numbers or ids to keep, or "none" to select nothing [default: all]: ');
84
+ setupInterface();
85
+ const line = await nextLine();
86
+ // Strictly distinguish bare Enter (null/empty string) from whitespace input.
87
+ if (line === null || line === '')
88
+ return choices.map((c) => c.id);
89
+ // Explicit empty selection: "none" means deliberately zero built-in choices.
90
+ if (line.trim().toLowerCase() === 'none')
91
+ return [];
92
+ const tokens = line.split(',').map((t) => t.trim());
93
+ if (tokens.some((t) => t === '')) {
94
+ throw new Error(`Invalid selection input: "${line}" (empty token)`);
95
+ }
96
+ const idSet = new Set(choices.map((c) => c.id));
97
+ const seenIds = new Set();
98
+ for (const t of tokens) {
99
+ let resolvedId;
100
+ if (/^[0-9]+$/.test(t)) {
101
+ const n = Number.parseInt(t, 10);
102
+ if (n < 1 || n > choices.length) {
103
+ throw new Error(`Selection out of range: "${t}" (expected 1..${choices.length})`);
104
+ }
105
+ resolvedId = choices[n - 1].id;
106
+ }
107
+ else if (idSet.has(t)) {
108
+ resolvedId = t;
109
+ }
110
+ else {
111
+ throw new Error(`Unknown TUI selection token: "${t}"`);
112
+ }
113
+ if (seenIds.has(resolvedId)) {
114
+ throw new Error(`Duplicate selection: "${t}" resolves to already-selected "${resolvedId}"`);
115
+ }
116
+ seenIds.add(resolvedId);
117
+ }
118
+ // Normalize to prompt order: users can type tokens in any order, but the
119
+ // persisted array follows the canonical choices order to keep .airc.json
120
+ // diffs stable. An empty result here is impossible (tokens.length > 0 and
121
+ // every token resolves to an id), so no separate empty guard is needed —
122
+ // explicit "none" handled above.
123
+ return choices.map((c) => c.id).filter((id) => seenIds.has(id));
124
+ }
77
125
  function closePrompt() {
78
126
  if (_rl) {
79
127
  _rl.close();
@@ -81,5 +129,5 @@ function closePrompt() {
81
129
  _stdinDone = true;
82
130
  }
83
131
  }
84
- export { prompt, select, closePrompt };
132
+ export { prompt, select, multiSelect, closePrompt };
85
133
  //# sourceMappingURL=prompt.js.map
@@ -630,13 +630,21 @@ export function ensureCodexModelInheritance(toolDir, hostHomeDir) {
630
630
  return;
631
631
  }
632
632
  }
633
+ const inheritSpecs = [
634
+ ['model', 'string'],
635
+ ['model_reasoning_effort', 'string'],
636
+ ['model_auto_compact_token_limit', 'number']
637
+ ];
633
638
  let changed = false;
634
- for (const key of ['model', 'model_reasoning_effort']) {
639
+ for (const [key, type] of inheritSpecs) {
635
640
  if (Object.hasOwn(sandboxParsed, key)) {
636
641
  continue;
637
642
  }
638
643
  const value = hostParsed[key];
639
- if (typeof value !== 'string' || value === '') {
644
+ if (type === 'string' && (typeof value !== 'string' || value === '')) {
645
+ continue;
646
+ }
647
+ if (type === 'number' && (typeof value !== 'number' || !Number.isFinite(value) || value <= 0)) {
640
648
  continue;
641
649
  }
642
650
  sandboxParsed[key] = value;
@@ -1,5 +1,5 @@
1
1
  import { loadConfig } from "../config.js";
2
- import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel } from "../constants.js";
2
+ import { assertValidBranchName, containerNameCandidates } 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,11 +7,13 @@ 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
- import { fetchSandboxRows, isTaskShortRef, resolveTaskShortRef } from "./list-running.js";
11
- const USAGE = `Usage: ai sandbox exec <branch | TASK-id | '#N'> [cmd...]
10
+ import { isTaskShortRef, resolveTaskShortRef } from "./list-running.js";
11
+ const USAGE = `Usage: ai sandbox exec <branch | TASK-id | N | '#N'> [cmd...]
12
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.`;
13
+ N (bare) and '#N' both reference the same active task short id from
14
+ .agents/workspace/active/.short-ids.json. They resolve only via that
15
+ registry — they do not reference a container's row position in
16
+ 'ai sandbox ls' output.`;
15
17
  const TMUX_ENTRY_PATH = '/usr/local/bin/sandbox-tmux-entry';
16
18
  // Terminal-detection variables that interactive TUIs (e.g. claude-code)
17
19
  // inspect to enable progressive enhancements such as the kitty keyboard
@@ -86,8 +88,7 @@ export async function enter(args) {
86
88
  const [firstArg = '', ...cmd] = args;
87
89
  let branch;
88
90
  if (isTaskShortRef(firstArg)) {
89
- const { running } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
90
- branch = resolveTaskShortRef(firstArg, { running });
91
+ branch = resolveTaskShortRef(firstArg, { repoRoot: config.repoRoot });
91
92
  }
92
93
  else {
93
94
  branch = resolveTaskBranch(firstArg, config.repoRoot);
@@ -1,3 +1,6 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
1
4
  import { runSafeEngine } from "../shell.js";
2
5
  export function containerListFormat() {
3
6
  return '{{.Names}}\t{{.Status}}\t{{.Labels}}';
@@ -65,44 +68,75 @@ export function fetchSandboxRows(engine, label, branchKey) {
65
68
  return sortAndIndexSandboxRows(parseSandboxRows(raw, branchKey));
66
69
  }
67
70
  /**
68
- * Returns true iff `arg` is a syntactically valid task short reference ('#N').
71
+ * Returns true iff `arg` is a syntactically valid task short reference.
72
+ * Accepts both bare numeric ('11') and '#'-prefixed ('#11') forms.
69
73
  * Zero IO. Callers MUST use this as the gate before constructing any context
70
74
  * for resolveTaskShortRef — that way non-matching arguments (e.g. '#abc',
71
75
  * '#1.5', '#') never trigger sandbox list IO.
72
76
  */
73
77
  export function isTaskShortRef(arg) {
74
- return /^#\d+$/.test(arg);
78
+ return /^#?\d+$/.test(arg);
75
79
  }
76
80
  /**
77
- * Resolve a task short reference ('#N') to a branch name.
81
+ * Try to resolve a short ref against the global task-short-id registry.
78
82
  *
79
- * Current implementation: treats the digits as a 1-based index into the
80
- * supplied running-sandbox list (ls view order). This is the *only*
81
- * resolution path until the global task-short-id registry lands in a
82
- * follow-up task; do NOT read task.md or scan .agents/workspace/ from this
83
- * helper here.
84
- *
85
- * Precondition: callers MUST gate on isTaskShortRef(arg) === true before
86
- * constructing ctx and calling this function. Throws when arg is a valid
87
- * short ref but cannot be resolved (out of range, no running sandboxes,
88
- * etc.); the caller surfaces the error to the user.
83
+ * Tri-state semantics (review-code Round 1 M-1 fix):
84
+ * - 'miss' → script reports no entry (or registry script missing). Caller may fall back.
85
+ * - 'hit' → registry resolved to a task id and branch is found in task.md.
86
+ * - throws → registry hit but task.md is missing or branch metadata is unparseable;
87
+ * surfacing this error is critical — never silently fall back to running index.
89
88
  */
90
- export function resolveTaskShortRef(arg, ctx) {
91
- const n = Number(arg.slice(1));
92
- if (n < 1) {
93
- throw new Error(`Invalid sandbox index '${arg}': must be >= 1`);
94
- }
95
- const { running } = ctx;
96
- if (running.length === 0) {
97
- throw new Error(`No running sandbox to reference with '${arg}'`);
98
- }
99
- if (n > running.length) {
100
- throw new Error(`No running sandbox at index '${arg}' (only ${running.length} running)`);
89
+ function tryResolveFromRegistry(arg, repoRoot) {
90
+ const scriptPath = path.join(repoRoot, '.agents', 'scripts', 'task-short-id.js');
91
+ if (!fs.existsSync(scriptPath))
92
+ return { status: 'miss' };
93
+ // Strip leading '#' when forwarding bare-numeric input through the script's CLI.
94
+ // (Script accepts both forms, but this avoids shell quoting confusion in error
95
+ // messages echoed back to the user.)
96
+ const result = spawnSync('node', [scriptPath, 'resolve', arg], { encoding: 'utf8', cwd: repoRoot });
97
+ if (result.status !== 0)
98
+ return { status: 'miss' };
99
+ const taskId = (result.stdout || '').trim();
100
+ if (!/^TASK-\d{8}-\d{6}$/.test(taskId)) {
101
+ throw new Error(`Registry returned malformed task id for '${arg}': ${JSON.stringify(taskId)}`);
101
102
  }
102
- const row = running[n - 1];
103
- if (!row.branch) {
104
- throw new Error(`Cannot resolve branch for sandbox '${arg}' (container '${row.name}' missing branch label)`);
103
+ for (const sub of ['active', 'completed', 'blocked', 'archive']) {
104
+ const taskMdPath = path.join(repoRoot, '.agents', 'workspace', sub, taskId, 'task.md');
105
+ if (!fs.existsSync(taskMdPath))
106
+ continue;
107
+ const content = fs.readFileSync(taskMdPath, 'utf8');
108
+ const fm = content.match(/^branch:\s*(.+)$/m);
109
+ if (fm?.[1]?.trim()) {
110
+ return { status: 'hit', branch: fm[1].trim().replace(/^(["'])(.*)\1$/, '$2') };
111
+ }
112
+ const ctx = content.match(/^- \*\*(?:分支|Branch)\*\*:[ \t]*`?([^`\n]+)`?$/m);
113
+ if (ctx?.[1]?.trim()) {
114
+ return { status: 'hit', branch: ctx[1].trim().replace(/^(["'])(.*)\1$/, '$2') };
115
+ }
116
+ throw new Error(`Short ref '${arg}' resolved to task ${taskId} but task.md has no branch field`);
105
117
  }
106
- return row.branch;
118
+ throw new Error(`Short ref '${arg}' resolved to task ${taskId} but task.md was not found under any workspace dir`);
119
+ }
120
+ /**
121
+ /**
122
+ * Resolve a task short reference (bare 'N' or '#N') to a branch name for the
123
+ * sandbox entrypoint.
124
+ *
125
+ * Resolution: registry-only. Look up the short id in the global task-short-id
126
+ * registry under repoRoot; if hit, read the branch from the matching task.md.
127
+ * On miss (registry empty or short id absent), throw with an actionable
128
+ * message instead of falling back to a container's row position in
129
+ * 'ai sandbox ls' output — that fallback would make the same syntax mean
130
+ * different things depending on `docker ps` state.
131
+ *
132
+ * Precondition: callers MUST gate on isTaskShortRef(arg) === true.
133
+ */
134
+ export function resolveTaskShortRef(arg, ctx) {
135
+ const lookup = tryResolveFromRegistry(arg, ctx.repoRoot);
136
+ if (lookup.status === 'hit')
137
+ return lookup.branch;
138
+ throw new Error(`short ref '${arg}' is not in the active task registry. ` +
139
+ `'#N' and bare N resolve only via the registry (not by row position in 'ai sandbox ls'); ` +
140
+ `use a task short id (e.g. 'ai sandbox exec 11'), a TASK-id, or a branch name.`);
107
141
  }
108
142
  //# sourceMappingURL=list-running.js.map
@@ -6,27 +6,20 @@ import { loadConfig } from "../config.js";
6
6
  import { sandboxBranchLabel, sandboxLabel } from "../constants.js";
7
7
  import { detectEngine } from "../engine.js";
8
8
  import { resolveTools, toolProjectDirCandidates } from "../tools.js";
9
+ import { formatTable } from "../../table.js";
10
+ import { lookupShortIdByBranch } from "../../task/short-id.js";
9
11
  import { fetchSandboxRows } from "./list-running.js";
10
12
  export { containerListFormat, parseLabels } from "./list-running.js";
11
13
  const USAGE = `Usage: ai sandbox ls
12
14
 
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'];
15
+ Lists all containers for the current project. The '#' column is a
16
+ display-only row number; the 'SHORT' column shows the active task short
17
+ id bound to each container's branch (via
18
+ .agents/workspace/active/.short-ids.json), or '-' if no active task is
19
+ bound. Pass the SHORT value to "ai sandbox exec" (e.g. 'ai sandbox exec 11').`;
20
+ const CONTAINER_TABLE_HEADERS = ['#', 'SHORT', 'NAMES', 'STATUS', 'BRANCH'];
17
21
  export function formatContainerTable(rows) {
18
- const columns = rows.map((row) => [row.index, row.name, row.status, row.branch]);
19
- const widths = [
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))
24
- ];
25
- const renderRow = (values) => `${values[0].padEnd(widths[0])} ${values[1].padEnd(widths[1])} ${values[2].padEnd(widths[2])} ${values[3]}`.trimEnd();
26
- return [
27
- renderRow(CONTAINER_TABLE_HEADERS),
28
- ...columns.map((column) => renderRow(column))
29
- ];
22
+ return formatTable(CONTAINER_TABLE_HEADERS, rows.map((r) => [r.row, r.shortId, r.name, r.status, r.branch]));
30
23
  }
31
24
  function listChildren(dir) {
32
25
  if (!fs.existsSync(dir)) {
@@ -51,15 +44,20 @@ export function ls(args = []) {
51
44
  p.log.warn(' No sandbox containers');
52
45
  }
53
46
  else {
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
- }));
47
+ const tableRows = ordered.map((container, i) => {
48
+ const shortId = container.branch ? lookupShortIdByBranch(container.branch, config.repoRoot) : null;
49
+ return {
50
+ row: String(i + 1),
51
+ shortId: shortId ?? '-',
52
+ name: container.name,
53
+ status: container.status,
54
+ branch: container.branch
55
+ };
56
+ });
60
57
  for (const line of formatContainerTable(tableRows)) {
61
58
  process.stdout.write(` ${line}\n`);
62
59
  }
60
+ process.stdout.write(` Total: ${ordered.length} containers\n`);
63
61
  }
64
62
  p.log.step('Worktrees');
65
63
  const worktrees = listChildren(config.worktreeBase);
@@ -6,7 +6,8 @@ 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";
9
+ import { runEngine, runSafeEngine, runVerboseEngine } from "../shell.js";
10
+ import { pruneSandboxDanglingImages } from "../image-prune.js";
10
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";
@@ -53,11 +54,6 @@ export function buildArgs(config, tools, dockerfilePath, imageSignature, { engin
53
54
  }
54
55
  return args;
55
56
  }
56
- function removeImageIfPresent(imageName, engine) {
57
- if (runOkEngine(engine, 'docker', ['image', 'inspect', imageName])) {
58
- runEngine(engine, 'docker', ['rmi', imageName]);
59
- }
60
- }
61
57
  export async function rebuild(args) {
62
58
  const { values } = parseArgs({
63
59
  args,
@@ -85,9 +81,6 @@ export async function rebuild(args) {
85
81
  try {
86
82
  if (quiet) {
87
83
  const spinner = p.spinner();
88
- spinner.start(`Removing old image ${config.imageName}...`);
89
- removeImageIfPresent(config.imageName, engine);
90
- spinner.stop('Old image removed');
91
84
  spinner.start('Building image...');
92
85
  runEngine(engine, 'docker', buildArgs(config, tools, preparedDockerfile.path, imageSignature, { engine, refresh }), {
93
86
  cwd: config.repoRoot
@@ -95,12 +88,11 @@ export async function rebuild(args) {
95
88
  spinner.stop(pc.green('Sandbox image rebuilt'));
96
89
  }
97
90
  else {
98
- p.log.step(`Removing old image ${config.imageName}`);
99
- removeImageIfPresent(config.imageName, engine);
100
91
  p.log.step('Building image');
101
92
  runVerboseEngine(engine, 'docker', buildArgs(config, tools, preparedDockerfile.path, imageSignature, { engine, refresh }), { cwd: config.repoRoot });
102
93
  p.log.success(pc.green('Sandbox image rebuilt'));
103
94
  }
95
+ pruneSandboxDanglingImages(config, engine);
104
96
  }
105
97
  finally {
106
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.');
@@ -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