@fitlab-ai/agent-infra 0.7.0 → 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 (73) hide show
  1. package/bin/cli.ts +1 -1
  2. package/dist/bin/cli.js +1 -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/enter.js +1 -1
  8. package/dist/lib/sandbox/commands/list-running.js +58 -13
  9. package/dist/lib/sandbox/commands/rebuild.js +3 -11
  10. package/dist/lib/sandbox/commands/rm.js +2 -0
  11. package/dist/lib/sandbox/image-prune.js +18 -0
  12. package/dist/lib/sandbox/task-resolver.js +18 -0
  13. package/dist/lib/update.js +59 -18
  14. package/lib/builtin-tuis.ts +55 -0
  15. package/lib/defaults.json +3 -0
  16. package/lib/init.ts +87 -35
  17. package/lib/prompt.ts +54 -1
  18. package/lib/sandbox/commands/enter.ts +1 -1
  19. package/lib/sandbox/commands/list-running.ts +69 -16
  20. package/lib/sandbox/commands/rebuild.ts +3 -12
  21. package/lib/sandbox/commands/rm.ts +3 -0
  22. package/lib/sandbox/image-prune.ts +23 -0
  23. package/lib/sandbox/task-resolver.ts +23 -1
  24. package/lib/update.ts +71 -30
  25. package/package.json +1 -1
  26. package/templates/.agents/README.en.md +32 -0
  27. package/templates/.agents/README.zh-CN.md +32 -0
  28. package/templates/.agents/rules/task-short-id.en.md +141 -0
  29. package/templates/.agents/rules/task-short-id.zh-CN.md +124 -0
  30. package/templates/.agents/scripts/task-short-id.js +713 -0
  31. package/templates/.agents/skills/analyze-task/SKILL.en.md +4 -0
  32. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +4 -1
  33. package/templates/.agents/skills/block-task/SKILL.en.md +12 -0
  34. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +12 -1
  35. package/templates/.agents/skills/cancel-task/SKILL.en.md +12 -0
  36. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +12 -1
  37. package/templates/.agents/skills/check-task/SKILL.en.md +4 -0
  38. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +4 -1
  39. package/templates/.agents/skills/close-codescan/SKILL.en.md +11 -0
  40. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +11 -0
  41. package/templates/.agents/skills/close-dependabot/SKILL.en.md +11 -0
  42. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +11 -0
  43. package/templates/.agents/skills/code-task/SKILL.en.md +4 -0
  44. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +4 -1
  45. package/templates/.agents/skills/commit/SKILL.en.md +4 -0
  46. package/templates/.agents/skills/commit/SKILL.zh-CN.md +4 -0
  47. package/templates/.agents/skills/complete-task/SKILL.en.md +12 -0
  48. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +12 -1
  49. package/templates/.agents/skills/create-pr/SKILL.en.md +4 -0
  50. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -0
  51. package/templates/.agents/skills/create-task/SKILL.en.md +14 -0
  52. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +14 -1
  53. package/templates/.agents/skills/import-codescan/SKILL.en.md +14 -0
  54. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +14 -0
  55. package/templates/.agents/skills/import-dependabot/SKILL.en.md +14 -0
  56. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +14 -0
  57. package/templates/.agents/skills/import-issue/SKILL.en.md +14 -0
  58. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +14 -0
  59. package/templates/.agents/skills/plan-task/SKILL.en.md +4 -0
  60. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -1
  61. package/templates/.agents/skills/restore-task/SKILL.en.md +12 -0
  62. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +12 -1
  63. package/templates/.agents/skills/review-analysis/SKILL.en.md +4 -0
  64. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +4 -1
  65. package/templates/.agents/skills/review-code/SKILL.en.md +4 -0
  66. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -1
  67. package/templates/.agents/skills/review-plan/SKILL.en.md +4 -0
  68. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +4 -1
  69. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +1 -0
  70. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +1 -0
  71. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +112 -21
  72. package/templates/.agents/templates/task.en.md +1 -0
  73. package/templates/.agents/templates/task.zh-CN.md +1 -0
package/bin/cli.ts CHANGED
@@ -93,7 +93,7 @@ switch (command) {
93
93
  const { runSandbox } = imported;
94
94
  await runSandbox(process.argv.slice(3)).catch((e: unknown) => {
95
95
  process.stderr.write(`Error: ${errorMessage(e)}\n`);
96
- process.exitCode = 1;
96
+ process.exit(1);
97
97
  });
98
98
  break;
99
99
  }
package/dist/bin/cli.js CHANGED
@@ -96,7 +96,7 @@ switch (command) {
96
96
  const { runSandbox } = imported;
97
97
  await runSandbox(process.argv.slice(3)).catch((e) => {
98
98
  process.stderr.write(`Error: ${errorMessage(e)}\n`);
99
- process.exitCode = 1;
99
+ process.exit(1);
100
100
  });
101
101
  break;
102
102
  }
@@ -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
@@ -87,7 +87,7 @@ export async function enter(args) {
87
87
  let branch;
88
88
  if (isTaskShortRef(firstArg)) {
89
89
  const { running } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
90
- branch = resolveTaskShortRef(firstArg, { running });
90
+ branch = resolveTaskShortRef(firstArg, { running, repoRoot: config.repoRoot });
91
91
  }
92
92
  else {
93
93
  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}}';
@@ -74,25 +77,47 @@ export function isTaskShortRef(arg) {
74
77
  return /^#\d+$/.test(arg);
75
78
  }
76
79
  /**
77
- * Resolve a task short reference ('#N') to a branch name.
80
+ * Try to resolve a short ref against the global task-short-id registry.
78
81
  *
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.
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.
89
87
  */
90
- export function resolveTaskShortRef(arg, ctx) {
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) {
91
117
  const n = Number(arg.slice(1));
92
118
  if (n < 1) {
93
119
  throw new Error(`Invalid sandbox index '${arg}': must be >= 1`);
94
120
  }
95
- const { running } = ctx;
96
121
  if (running.length === 0) {
97
122
  throw new Error(`No running sandbox to reference with '${arg}'`);
98
123
  }
@@ -105,4 +130,24 @@ export function resolveTaskShortRef(arg, ctx) {
105
130
  }
106
131
  return row.branch;
107
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
+ }
108
153
  //# sourceMappingURL=list-running.js.map
@@ -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
@@ -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
  }
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { info, ok, err } from "./log.js";
4
4
  import { resolveTemplateDir } from "./paths.js";
5
5
  import { renderFile, copySkillDir, KNOWN_PLATFORMS } from "./render.js";
6
+ import { isPathOwnedByDisabledTUI, resolveEnabledTUIs } from "./builtin-tuis.js";
6
7
  const defaults = JSON.parse(fs.readFileSync(new URL('./defaults.json', import.meta.url), 'utf8'));
7
8
  const CONFIG_DIR = '.agents';
8
9
  const CONFIG_PATH = path.join(CONFIG_DIR, '.airc.json');
@@ -15,7 +16,7 @@ function isPathOwnedByOtherPlatform(relativePath, platformType) {
15
16
  return false;
16
17
  return candidate !== platformType;
17
18
  }
18
- function syncFileRegistry(config, platformType) {
19
+ function syncFileRegistry(config, platformType, enabledTUIs) {
19
20
  config.files ||= {};
20
21
  const before = JSON.stringify({
21
22
  files: {
@@ -36,6 +37,8 @@ function syncFileRegistry(config, platformType) {
36
37
  for (const entry of defaults.files.managed) {
37
38
  if (isPathOwnedByOtherPlatform(entry, platformType))
38
39
  continue;
40
+ if (isPathOwnedByDisabledTUI(entry, enabledTUIs))
41
+ continue;
39
42
  if (!allExisting.includes(entry)) {
40
43
  config.files.managed.push(entry);
41
44
  added.managed.push(entry);
@@ -44,6 +47,8 @@ function syncFileRegistry(config, platformType) {
44
47
  for (const entry of defaults.files.merged) {
45
48
  if (isPathOwnedByOtherPlatform(entry, platformType))
46
49
  continue;
50
+ if (isPathOwnedByDisabledTUI(entry, enabledTUIs))
51
+ continue;
47
52
  if (!allExisting.includes(entry)) {
48
53
  config.files.merged.push(entry);
49
54
  added.merged.push(entry);
@@ -82,6 +87,7 @@ async function cmdUpdate() {
82
87
  const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
83
88
  const { project, org, language } = config;
84
89
  const platformType = config.platform?.type || defaults.platform.type;
90
+ const enabledTUIs = resolveEnabledTUIs(config.tuis);
85
91
  const replacements = { project, org };
86
92
  info(`Updating seed files for: ${project}`);
87
93
  console.log('');
@@ -106,20 +112,27 @@ async function cmdUpdate() {
106
112
  catch {
107
113
  // Ignore missing legacy script from pre-ESM installs.
108
114
  }
109
- // update Claude command
110
- renderFile(path.join(templateDir, '.claude', 'commands', claudeSrc), path.join('.claude', 'commands', 'update-agent-infra.md'), replacements);
111
- ok('Updated .claude/commands/update-agent-infra.md');
112
- // update Gemini command
113
- renderFile(path.join(templateDir, '.gemini', 'commands', '_project_', geminiSrc), path.join('.gemini', 'commands', project, 'update-agent-infra.toml'), replacements);
114
- ok(`Updated .gemini/commands/${project}/update-agent-infra.toml`);
115
- // update OpenCode command
116
- renderFile(path.join(templateDir, '.opencode', 'commands', opencodeSrc), path.join('.opencode', 'commands', 'update-agent-infra.md'), replacements);
117
- ok('Updated .opencode/commands/update-agent-infra.md');
115
+ // update Claude command (only if enabled)
116
+ if (enabledTUIs.has('claude-code')) {
117
+ renderFile(path.join(templateDir, '.claude', 'commands', claudeSrc), path.join('.claude', 'commands', 'update-agent-infra.md'), replacements);
118
+ ok('Updated .claude/commands/update-agent-infra.md');
119
+ }
120
+ // update Gemini command (only if enabled)
121
+ if (enabledTUIs.has('gemini-cli')) {
122
+ renderFile(path.join(templateDir, '.gemini', 'commands', '_project_', geminiSrc), path.join('.gemini', 'commands', project, 'update-agent-infra.toml'), replacements);
123
+ ok(`Updated .gemini/commands/${project}/update-agent-infra.toml`);
124
+ }
125
+ // update OpenCode command (only if enabled)
126
+ if (enabledTUIs.has('opencode')) {
127
+ renderFile(path.join(templateDir, '.opencode', 'commands', opencodeSrc), path.join('.opencode', 'commands', 'update-agent-infra.md'), replacements);
128
+ ok('Updated .opencode/commands/update-agent-infra.md');
129
+ }
118
130
  // sync file registry
119
- const { added, changed } = syncFileRegistry(config, platformType);
131
+ const { added, changed } = syncFileRegistry(config, platformType, enabledTUIs);
120
132
  const hasNewEntries = added.managed.length > 0 || added.merged.length > 0;
121
133
  const platformAdded = !config.platform;
122
134
  const sandboxAdded = !config.sandbox;
135
+ const taskAdded = !config.task;
123
136
  const labelsAdded = !config.labels;
124
137
  const requiresPullRequestAdded = config.requiresPullRequest === undefined;
125
138
  let configChanged = changed;
@@ -131,6 +144,10 @@ async function cmdUpdate() {
131
144
  config.sandbox = structuredClone(defaults.sandbox);
132
145
  configChanged = true;
133
146
  }
147
+ if (taskAdded) {
148
+ config.task = structuredClone(defaults.task);
149
+ configChanged = true;
150
+ }
134
151
  if (labelsAdded) {
135
152
  config.labels = structuredClone(defaults.labels);
136
153
  configChanged = true;
@@ -150,13 +167,16 @@ async function cmdUpdate() {
150
167
  ok(` merged: ${entry}`);
151
168
  }
152
169
  }
153
- else if (platformAdded || sandboxAdded || labelsAdded || requiresPullRequestAdded) {
170
+ else if (platformAdded || sandboxAdded || taskAdded || labelsAdded || requiresPullRequestAdded) {
154
171
  if (platformAdded) {
155
172
  info(`Default platform config added to ${CONFIG_PATH}.`);
156
173
  }
157
174
  if (sandboxAdded) {
158
175
  info(`Default sandbox config added to ${CONFIG_PATH}.`);
159
176
  }
177
+ if (taskAdded) {
178
+ info(`Default task.shortIdLength=${defaults.task.shortIdLength} added to ${CONFIG_PATH}.`);
179
+ }
160
180
  if (labelsAdded) {
161
181
  info(`Default labels.in config added to ${CONFIG_PATH}.`);
162
182
  }
@@ -170,6 +190,9 @@ async function cmdUpdate() {
170
190
  if (hasNewEntries && sandboxAdded) {
171
191
  info(`Default sandbox config added to ${CONFIG_PATH}.`);
172
192
  }
193
+ if (hasNewEntries && taskAdded) {
194
+ info(`Default task.shortIdLength=${defaults.task.shortIdLength} added to ${CONFIG_PATH}.`);
195
+ }
173
196
  if (hasNewEntries && labelsAdded) {
174
197
  info(`Default labels.in config added to ${CONFIG_PATH}.`);
175
198
  }
@@ -186,12 +209,30 @@ async function cmdUpdate() {
186
209
  console.log('');
187
210
  ok('Seed files updated successfully!');
188
211
  console.log('');
189
- console.log(' Next step: run the full update in your AI TUI:');
190
- console.log('');
191
- console.log(' Claude Code / OpenCode: /update-agent-infra');
192
- console.log(` Gemini CLI: /${project}:update-agent-infra`);
193
- console.log(' Codex CLI: $update-agent-infra');
194
- console.log('');
212
+ if (enabledTUIs.size === 0) {
213
+ console.log(' No built-in TUI enabled (tuis: []).');
214
+ console.log(` Configure "customTUIs" in ${CONFIG_PATH} if needed.`);
215
+ console.log('');
216
+ }
217
+ else {
218
+ console.log(' Next step: run the full update in your AI TUI:');
219
+ console.log('');
220
+ const claudeOrOpencode = [];
221
+ if (enabledTUIs.has('claude-code'))
222
+ claudeOrOpencode.push('Claude Code');
223
+ if (enabledTUIs.has('opencode'))
224
+ claudeOrOpencode.push('OpenCode');
225
+ if (claudeOrOpencode.length > 0) {
226
+ console.log(` ${claudeOrOpencode.join(' / ')}: /update-agent-infra`);
227
+ }
228
+ if (enabledTUIs.has('gemini-cli')) {
229
+ console.log(` Gemini CLI: /${project}:update-agent-infra`);
230
+ }
231
+ if (enabledTUIs.has('codex')) {
232
+ console.log(' Codex CLI: $update-agent-infra');
233
+ }
234
+ console.log('');
235
+ }
195
236
  }
196
237
  export { cmdUpdate };
197
238
  //# sourceMappingURL=update.js.map