@fitlab-ai/agent-infra 0.7.3 → 0.7.5

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 (107) hide show
  1. package/README.md +32 -790
  2. package/README.zh-CN.md +32 -763
  3. package/bin/cli.ts +13 -11
  4. package/dist/bin/cli.js +13 -11
  5. package/dist/lib/init.js +1 -1
  6. package/dist/lib/merge.js +1 -1
  7. package/dist/lib/sandbox/commands/create.js +44 -3
  8. package/dist/lib/sandbox/commands/rm.js +99 -19
  9. package/dist/lib/sandbox/index.js +24 -22
  10. package/dist/lib/sandbox/readme-scaffold.js +6 -6
  11. package/dist/lib/task/artifacts.js +58 -0
  12. package/dist/lib/task/commands/cat.js +38 -0
  13. package/dist/lib/task/commands/files.js +47 -0
  14. package/dist/lib/task/commands/grep.js +143 -0
  15. package/dist/lib/task/commands/log.js +75 -0
  16. package/dist/lib/task/commands/show.js +5 -114
  17. package/dist/lib/task/commands/status.js +239 -0
  18. package/dist/lib/task/index.js +37 -0
  19. package/dist/lib/task/resolve-ref.js +150 -0
  20. package/dist/lib/update.js +1 -1
  21. package/lib/init.ts +1 -1
  22. package/lib/merge.ts +1 -1
  23. package/lib/sandbox/commands/create.ts +47 -4
  24. package/lib/sandbox/commands/rm.ts +128 -19
  25. package/lib/sandbox/index.ts +24 -22
  26. package/lib/sandbox/readme-scaffold.ts +6 -6
  27. package/lib/task/artifacts.ts +72 -0
  28. package/lib/task/commands/cat.ts +39 -0
  29. package/lib/task/commands/files.ts +53 -0
  30. package/lib/task/commands/grep.ts +147 -0
  31. package/lib/task/commands/log.ts +80 -0
  32. package/lib/task/commands/show.ts +5 -117
  33. package/lib/task/commands/status.ts +302 -0
  34. package/lib/task/index.ts +37 -0
  35. package/lib/task/resolve-ref.ts +160 -0
  36. package/lib/update.ts +1 -1
  37. package/package.json +1 -1
  38. package/templates/.agents/README.en.md +1 -0
  39. package/templates/.agents/README.zh-CN.md +1 -0
  40. package/templates/.agents/rules/README.en.md +45 -0
  41. package/templates/.agents/rules/README.zh-CN.md +44 -0
  42. package/templates/.agents/rules/cli-help-format.en.md +49 -0
  43. package/templates/.agents/rules/cli-help-format.zh-CN.md +49 -0
  44. package/templates/.agents/rules/debugging-guide.en.md +25 -0
  45. package/templates/.agents/rules/debugging-guide.zh-CN.md +25 -0
  46. package/templates/.agents/rules/no-mid-flow-questions.en.md +14 -2
  47. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +14 -2
  48. package/templates/.agents/rules/pr-sync.github.en.md +8 -6
  49. package/templates/.agents/rules/pr-sync.github.zh-CN.md +8 -6
  50. package/templates/.agents/rules/review-handshake.en.md +83 -0
  51. package/templates/.agents/rules/review-handshake.zh-CN.md +83 -0
  52. package/templates/.agents/scripts/lib/post-review-commit.js +56 -0
  53. package/templates/.agents/scripts/lib/review-artifacts.js +117 -0
  54. package/templates/.agents/scripts/review-diff-fingerprint.js +99 -0
  55. package/templates/.agents/scripts/validate-artifact.js +240 -0
  56. package/templates/.agents/skills/analyze-task/SKILL.en.md +52 -6
  57. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +52 -6
  58. package/templates/.agents/skills/code-task/SKILL.en.md +2 -0
  59. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -0
  60. package/templates/.agents/skills/code-task/config/verify.en.json +3 -0
  61. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +3 -0
  62. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +5 -3
  63. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +5 -3
  64. package/templates/.agents/skills/code-task/reference/report-template.en.md +4 -4
  65. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +4 -4
  66. package/templates/.agents/skills/code-task/scripts/detect-mode.js +2 -107
  67. package/templates/.agents/skills/commit/SKILL.en.md +6 -0
  68. package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -0
  69. package/templates/.agents/skills/commit/reference/task-status-update.en.md +8 -0
  70. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +8 -0
  71. package/templates/.agents/skills/complete-task/SKILL.en.md +10 -0
  72. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +10 -0
  73. package/templates/.agents/skills/complete-task/config/verify.en.json +2 -0
  74. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -0
  75. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +1 -1
  76. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +1 -1
  77. package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
  78. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -1
  79. package/templates/.agents/skills/plan-task/config/verify.en.json +3 -0
  80. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +3 -0
  81. package/templates/.agents/skills/review-analysis/config/verify.en.json +2 -1
  82. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +2 -1
  83. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +5 -4
  84. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +5 -4
  85. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +4 -0
  86. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +4 -0
  87. package/templates/.agents/skills/review-code/SKILL.en.md +4 -1
  88. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -1
  89. package/templates/.agents/skills/review-code/config/verify.en.json +5 -2
  90. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +5 -2
  91. package/templates/.agents/skills/review-code/reference/output-templates.en.md +5 -4
  92. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +5 -4
  93. package/templates/.agents/skills/review-code/reference/report-template.en.md +6 -0
  94. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +6 -0
  95. package/templates/.agents/skills/review-plan/config/verify.en.json +2 -1
  96. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +2 -1
  97. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +5 -4
  98. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +5 -4
  99. package/templates/.agents/skills/review-plan/reference/report-template.en.md +4 -0
  100. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +4 -0
  101. package/templates/.agents/skills/watch-pr/SKILL.en.md +1 -1
  102. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +1 -1
  103. package/templates/.agents/templates/task.en.md +7 -0
  104. package/templates/.agents/templates/task.zh-CN.md +7 -0
  105. package/templates/.github/workflows/metadata-sync.yml +1 -1
  106. package/templates/.github/workflows/pr-label.yml +1 -1
  107. package/templates/.github/workflows/status-label.yml +1 -1
package/bin/cli.ts CHANGED
@@ -12,17 +12,19 @@ if (major < 22) {
12
12
 
13
13
  const USAGE = `agent-infra ${VERSION} - bootstrap AI collaboration infrastructure
14
14
 
15
- Usage:
16
- agent-infra cp <ssh-alias> Copy local clipboard image to a remote macOS NSPasteboard
17
- agent-infra help Show this help message
18
- agent-infra init Initialize a new project with update-agent-infra seed command
19
- agent-infra merge Merge tasks from another workspace directory (active/blocked/completed/archive)
20
- agent-infra sandbox Manage Docker-based AI sandboxes
21
- agent-infra task Read-only views over .agents/workspace tasks (ls / show)
22
- agent-infra update Update seed files and sync file registry for an existing project
23
- agent-infra version Show version
15
+ Usage: ai <command> [options]
24
16
 
25
- Shorthand: ai (e.g. ai init)
17
+ Commands:
18
+ cp <ssh-alias> Copy local clipboard image to a remote macOS NSPasteboard
19
+ help Show this help message
20
+ init Initialize a new project with update-agent-infra seed command
21
+ merge Merge tasks from another workspace directory (active/blocked/completed/archive)
22
+ sandbox Manage Docker-based AI sandboxes
23
+ task Read-only views over .agents/workspace tasks (cat / files / grep / log / ls / show / status)
24
+ update Update seed files and sync file registry for an existing project
25
+ version Show version
26
+
27
+ 'ai' and 'agent-infra' are interchangeable; 'ai' is the shorter form.
26
28
 
27
29
  Install methods:
28
30
  npm: npm install -g @fitlab-ai/agent-infra
@@ -31,7 +33,7 @@ Install methods:
31
33
  curl: curl -fsSL https://raw.githubusercontent.com/fitlab-ai/agent-infra/main/install.sh | sh (runs npm install -g internally)
32
34
 
33
35
  Examples:
34
- cd my-project && agent-infra init
36
+ cd my-project && ai init
35
37
  npx @fitlab-ai/agent-infra init
36
38
  `;
37
39
 
package/dist/bin/cli.js CHANGED
@@ -16,17 +16,19 @@ if (major < 22) {
16
16
  }
17
17
  const USAGE = `agent-infra ${VERSION} - bootstrap AI collaboration infrastructure
18
18
 
19
- Usage:
20
- agent-infra cp <ssh-alias> Copy local clipboard image to a remote macOS NSPasteboard
21
- agent-infra help Show this help message
22
- agent-infra init Initialize a new project with update-agent-infra seed command
23
- agent-infra merge Merge tasks from another workspace directory (active/blocked/completed/archive)
24
- agent-infra sandbox Manage Docker-based AI sandboxes
25
- agent-infra task Read-only views over .agents/workspace tasks (ls / show)
26
- agent-infra update Update seed files and sync file registry for an existing project
27
- agent-infra version Show version
19
+ Usage: ai <command> [options]
28
20
 
29
- Shorthand: ai (e.g. ai init)
21
+ Commands:
22
+ cp <ssh-alias> Copy local clipboard image to a remote macOS NSPasteboard
23
+ help Show this help message
24
+ init Initialize a new project with update-agent-infra seed command
25
+ merge Merge tasks from another workspace directory (active/blocked/completed/archive)
26
+ sandbox Manage Docker-based AI sandboxes
27
+ task Read-only views over .agents/workspace tasks (cat / files / grep / log / ls / show / status)
28
+ update Update seed files and sync file registry for an existing project
29
+ version Show version
30
+
31
+ 'ai' and 'agent-infra' are interchangeable; 'ai' is the shorter form.
30
32
 
31
33
  Install methods:
32
34
  npm: npm install -g @fitlab-ai/agent-infra
@@ -35,7 +37,7 @@ Install methods:
35
37
  curl: curl -fsSL https://raw.githubusercontent.com/fitlab-ai/agent-infra/main/install.sh | sh (runs npm install -g internally)
36
38
 
37
39
  Examples:
38
- cd my-project && agent-infra init
40
+ cd my-project && ai init
39
41
  npx @fitlab-ai/agent-infra init
40
42
  `;
41
43
  const command = process.argv[2] || '';
package/dist/lib/init.js CHANGED
@@ -70,7 +70,7 @@ function parseLocalSources(input) {
70
70
  }
71
71
  async function cmdInit() {
72
72
  console.log('');
73
- console.log(' agent-infra init');
73
+ console.log(' ai init');
74
74
  console.log(' ================================');
75
75
  console.log(' Optional template and skill sources can be added now or later in .agents/.airc.json.');
76
76
  console.log('');
package/dist/lib/merge.js CHANGED
@@ -702,7 +702,7 @@ function printReport(report) {
702
702
  async function cmdMerge(args) {
703
703
  const sourcePath = args[0];
704
704
  if (!sourcePath) {
705
- throw new Error('Usage: agent-infra merge <source-path>');
705
+ throw new Error('Usage: ai merge <source-path>');
706
706
  }
707
707
  const resolvedSource = path.resolve(sourcePath);
708
708
  if (!fs.existsSync(resolvedSource)) {
@@ -604,7 +604,32 @@ export function ensureClaudeSettings(toolDir, hostHomeDir) {
604
604
  fs.writeFileSync(settingsPath, JSON.stringify(data, null, 4), 'utf8');
605
605
  }
606
606
  }
607
- export function ensureCodexModelInheritance(toolDir, hostHomeDir) {
607
+ function resolveHostCatalogPath(value, hostHomeDir) {
608
+ if (typeof value !== 'string' || value === '') {
609
+ return null;
610
+ }
611
+ let resolved;
612
+ if (value === '~' || value.startsWith('~/') || value.startsWith('~\\')) {
613
+ resolved = path.join(hostHomeDir, value.slice(1).replace(/^[/\\]+/, ''));
614
+ }
615
+ else if (path.isAbsolute(value)) {
616
+ resolved = value;
617
+ }
618
+ else {
619
+ resolved = path.join(hostHomeDir, '.codex', value);
620
+ }
621
+ try {
622
+ if (!fs.statSync(resolved).isFile()) {
623
+ return null;
624
+ }
625
+ fs.accessSync(resolved, fs.constants.R_OK);
626
+ return resolved;
627
+ }
628
+ catch {
629
+ return null;
630
+ }
631
+ }
632
+ export function ensureCodexModelInheritance(toolDir, hostHomeDir, containerCodexDir = '/home/devuser/.codex') {
608
633
  if (!hostHomeDir) {
609
634
  return;
610
635
  }
@@ -650,6 +675,22 @@ export function ensureCodexModelInheritance(toolDir, hostHomeDir) {
650
675
  sandboxParsed[key] = value;
651
676
  changed = true;
652
677
  }
678
+ if (!Object.hasOwn(sandboxParsed, 'model_catalog_json')) {
679
+ const hostCatalogPath = resolveHostCatalogPath(hostParsed['model_catalog_json'], hostHomeDir);
680
+ if (hostCatalogPath) {
681
+ try {
682
+ const basename = path.basename(hostCatalogPath);
683
+ const destDir = path.join(toolDir, 'model-catalogs');
684
+ fs.mkdirSync(destDir, { recursive: true });
685
+ fs.copyFileSync(hostCatalogPath, path.join(destDir, basename));
686
+ sandboxParsed['model_catalog_json'] = path.posix.join(containerCodexDir, 'model-catalogs', basename);
687
+ changed = true;
688
+ }
689
+ catch {
690
+ // Copy failed (e.g. permissions): skip catalog, keep scalar inheritance intact.
691
+ }
692
+ }
693
+ }
653
694
  if (changed) {
654
695
  fs.writeFileSync(sandboxConfigPath, `${toml.stringify(sandboxParsed)}\n`, 'utf8');
655
696
  }
@@ -780,7 +821,7 @@ function runEngineTaskCommand(engine, cmd, args, opts = {}) {
780
821
  return runTaskCommand(command.cmd, command.args, opts);
781
822
  }
782
823
  export function buildImage(config, tools, dockerfilePath, imageSignature, { engine, runFn = runEngine, runSafeFn = runSafeEngine, runVerboseFn = runVerboseEngine, env = process.env } = {}) {
783
- const selectedEngine = engine ?? detectEngine(config);
824
+ const selectedEngine = engine ?? detectEngine({ engine: config.engine });
784
825
  const { uid: hostUid, gid: hostGid } = resolveBuildUid({
785
826
  engine: selectedEngine,
786
827
  runFn,
@@ -1023,7 +1064,7 @@ export async function create(args) {
1023
1064
  }
1024
1065
  const codexEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'codex');
1025
1066
  if (codexEntry) {
1026
- ensureCodexModelInheritance(codexEntry.dir, effectiveConfig.home);
1067
+ ensureCodexModelInheritance(codexEntry.dir, effectiveConfig.home, codexEntry.tool.containerMount);
1027
1068
  ensureCodexWorkspaceTrust(codexEntry.dir);
1028
1069
  }
1029
1070
  const geminiEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'gemini-cli');
@@ -11,12 +11,17 @@ import { removeManagedDir, removeWorktreeDir } from "../managed-fs.js";
11
11
  import { runOk, runSafe, runSafeEngine } from "../shell.js";
12
12
  import { resolveTaskBranch } from "../task-resolver.js";
13
13
  import { resolveTools, toolConfigDirCandidates, toolProjectDirCandidates } from "../tools.js";
14
- const USAGE = `Usage: ai sandbox rm <branch> [--all]`;
14
+ import { fetchSandboxRows } from "./list-running.js";
15
+ import { lookupShortIdByBranch } from "../../task/short-id.js";
16
+ const USAGE = `Usage:
17
+ ai sandbox rm <branch> Remove one sandbox (branch | TASK-id | short id)
18
+ ai sandbox rm --all [--dry-run] [--yes] Remove every sandbox not bound to an active task
19
+ ai sandbox rm --purge Tear down ALL sandboxes for the project (containers, worktrees, image, VM)`;
15
20
  export { assertManagedPath } from "../managed-fs.js";
16
21
  function projectToolDirs(config, tools) {
17
22
  return tools.flatMap((tool) => toolProjectDirCandidates(tool, config.project));
18
23
  }
19
- async function rmOne(config, tools, branch) {
24
+ async function rmOne(config, tools, branch, options = {}) {
20
25
  assertValidBranchName(branch);
21
26
  const engine = detectEngine(config);
22
27
  let effectiveBranch = branch;
@@ -25,7 +30,9 @@ async function rmOne(config, tools, branch) {
25
30
  tool,
26
31
  candidates: toolConfigDirCandidates(tool, config.project, branch)
27
32
  }));
28
- p.intro(pc.cyan(`Removing sandbox for ${branch}`));
33
+ if (!options.quiet) {
34
+ p.intro(pc.cyan(`Removing sandbox for ${branch}`));
35
+ }
29
36
  const existing = runSafeEngine(engine, 'docker', ['ps', '-a', '--format', '{{.Names}}']).split('\n').filter(Boolean);
30
37
  const matchedContainers = containerNameCandidates(config, branch)
31
38
  .filter((name) => existing.includes(name));
@@ -57,10 +64,12 @@ async function rmOne(config, tools, branch) {
57
64
  }
58
65
  const existingWorktrees = worktreeCandidates.filter((candidate) => fs.existsSync(candidate));
59
66
  if (existingWorktrees.length > 0) {
60
- const shouldRemoveWorktree = await p.confirm({
61
- message: `Remove worktree(s): ${existingWorktrees.join(', ')}?`,
62
- initialValue: true
63
- });
67
+ const shouldRemoveWorktree = options.assumeYes
68
+ ? true
69
+ : await p.confirm({
70
+ message: `Remove worktree(s): ${existingWorktrees.join(', ')}?`,
71
+ initialValue: true
72
+ });
64
73
  if (p.isCancel(shouldRemoveWorktree)) {
65
74
  p.outro('Cancelled');
66
75
  return;
@@ -69,10 +78,12 @@ async function rmOne(config, tools, branch) {
69
78
  for (const worktree of existingWorktrees) {
70
79
  removeWorktreeDir(config.repoRoot, config.worktreeBase, worktree);
71
80
  }
72
- const shouldDeleteBranch = await p.confirm({
73
- message: `Also delete local branch '${effectiveBranch}'?`,
74
- initialValue: true
75
- });
81
+ const shouldDeleteBranch = options.assumeYes
82
+ ? true
83
+ : await p.confirm({
84
+ message: `Also delete local branch '${effectiveBranch}'?`,
85
+ initialValue: true
86
+ });
76
87
  if (!p.isCancel(shouldDeleteBranch) && shouldDeleteBranch) {
77
88
  if (!runOk('git', ['-C', config.repoRoot, 'branch', '-D', effectiveBranch])) {
78
89
  p.log.warn(`Local branch '${effectiveBranch}' was not deleted`);
@@ -92,18 +103,22 @@ async function rmOne(config, tools, branch) {
92
103
  }
93
104
  const shareBranch = shareBranchDir(config, effectiveBranch);
94
105
  if (fs.existsSync(shareBranch)) {
95
- const shouldRemoveShare = await p.confirm({
96
- message: `Remove share dir for branch '${effectiveBranch}' (${shareBranch})?`,
97
- initialValue: true
98
- });
106
+ const shouldRemoveShare = options.assumeYes
107
+ ? true
108
+ : await p.confirm({
109
+ message: `Remove share dir for branch '${effectiveBranch}' (${shareBranch})?`,
110
+ initialValue: true
111
+ });
99
112
  if (!p.isCancel(shouldRemoveShare) && shouldRemoveShare) {
100
113
  removeManagedDir(config.shareBase, shareBranch);
101
114
  p.log.success(`Share dir removed: ${shareBranch}`);
102
115
  }
103
116
  }
104
- p.outro(pc.green('Sandbox removed'));
117
+ if (!options.quiet) {
118
+ p.outro(pc.green('Sandbox removed'));
119
+ }
105
120
  }
106
- async function rmAll(config, tools) {
121
+ async function rmPurge(config, tools) {
107
122
  const engine = detectEngine(config);
108
123
  p.intro(pc.cyan(`Removing all sandboxes for ${config.project}`));
109
124
  const containers = runSafeEngine(engine, 'docker', [
@@ -193,6 +208,52 @@ async function rmAll(config, tools) {
193
208
  }
194
209
  p.outro(pc.green('All project sandboxes removed'));
195
210
  }
211
+ async function rmUnbound(config, tools, options) {
212
+ const engine = detectEngine(config);
213
+ const { running, nonRunning } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
214
+ const removable = [...running, ...nonRunning].filter((row) => row.branch && lookupShortIdByBranch(row.branch, config.repoRoot) === null);
215
+ p.intro(pc.cyan(`Removing sandboxes not bound to an active task for ${config.project}`));
216
+ if (removable.length === 0) {
217
+ p.outro('No removable sandboxes: every container is bound to an active task (or none exist)');
218
+ return;
219
+ }
220
+ for (const row of removable) {
221
+ p.log.message(`${row.name} ${row.branch}`);
222
+ }
223
+ if (options.dryRun) {
224
+ p.outro(`Dry run: ${removable.length} sandbox(es) would be removed, nothing deleted`);
225
+ return;
226
+ }
227
+ if (!options.assumeYes) {
228
+ if (!process.stdin.isTTY) {
229
+ throw new Error('Refusing to remove sandboxes without confirmation in a non-interactive shell; pass --yes to proceed.');
230
+ }
231
+ const confirmed = await p.confirm({
232
+ message: `Remove these ${removable.length} sandbox(es)?`,
233
+ initialValue: false
234
+ });
235
+ if (p.isCancel(confirmed) || !confirmed) {
236
+ p.outro('Cancelled');
237
+ return;
238
+ }
239
+ }
240
+ const failures = [];
241
+ for (const row of removable) {
242
+ try {
243
+ await rmOne(config, tools, row.branch, { assumeYes: true, quiet: true });
244
+ }
245
+ catch (error) {
246
+ failures.push({ branch: row.branch, message: error instanceof Error ? error.message : String(error) });
247
+ }
248
+ }
249
+ if (failures.length > 0) {
250
+ for (const failure of failures) {
251
+ p.log.error(`Failed to remove '${failure.branch}': ${failure.message}`);
252
+ }
253
+ throw new Error(`Removed ${removable.length - failures.length}/${removable.length} sandbox(es); ${failures.length} failed`);
254
+ }
255
+ p.outro(pc.green(`Removed ${removable.length} sandbox(es)`));
256
+ }
196
257
  export async function rm(args) {
197
258
  const { values, positionals } = parseArgs({
198
259
  args,
@@ -200,6 +261,9 @@ export async function rm(args) {
200
261
  strict: true,
201
262
  options: {
202
263
  all: { type: 'boolean' },
264
+ purge: { type: 'boolean' },
265
+ 'dry-run': { type: 'boolean' },
266
+ yes: { type: 'boolean', short: 'y' },
203
267
  help: { type: 'boolean', short: 'h' }
204
268
  }
205
269
  });
@@ -207,13 +271,29 @@ export async function rm(args) {
207
271
  process.stdout.write(`${USAGE}\n`);
208
272
  return;
209
273
  }
210
- if (!values.all && positionals.length !== 1) {
274
+ if (values.all && values.purge) {
275
+ throw new Error('--all and --purge are mutually exclusive');
276
+ }
277
+ if ((values['dry-run'] || values.yes) && !values.all) {
278
+ throw new Error('--dry-run and --yes only apply to --all');
279
+ }
280
+ if ((values.all || values.purge) && positionals.length > 0) {
281
+ throw new Error(`${values.all ? '--all' : '--purge'} does not take a branch argument`);
282
+ }
283
+ if (!values.all && !values.purge && positionals.length !== 1) {
211
284
  throw new Error(USAGE);
212
285
  }
213
286
  const config = loadConfig();
214
287
  const tools = resolveTools(config);
288
+ if (values.purge) {
289
+ await rmPurge(config, tools);
290
+ return;
291
+ }
215
292
  if (values.all) {
216
- await rmAll(config, tools);
293
+ await rmUnbound(config, tools, {
294
+ dryRun: Boolean(values['dry-run']),
295
+ assumeYes: Boolean(values.yes)
296
+ });
217
297
  return;
218
298
  }
219
299
  const branch = resolveTaskBranch(positionals[0] ?? '', config.repoRoot);
@@ -6,9 +6,6 @@ Commands:
6
6
  Enter sandbox or run a command. N (bare) is the
7
7
  recommended form for task short ids (e.g.
8
8
  'ai sandbox exec 11'); '#N' is also accepted.
9
- start <branch | TASK-id | N | '#N'>
10
- Start an existing stopped sandbox container
11
- (e.g. after the Docker daemon restarted)
12
9
  ls List sandboxes for the current project (the '#'
13
10
  column is a display-only row number; the 'SHORT'
14
11
  column shows the active task short id, '-' if none)
@@ -16,7 +13,12 @@ Commands:
16
13
  rebuild [--quiet] [--refresh]
17
14
  Rebuild the sandbox image (--refresh pulls base + tools)
18
15
  refresh Sync host Claude Code credentials to all sandbox copies
19
- rm <branch> [--all] Remove a sandbox or all sandboxes
16
+ rm <branch> | --all | --purge
17
+ Remove one sandbox, all sandboxes not bound to an
18
+ active task (--all), or tear down everything (--purge)
19
+ start <branch | TASK-id | N | '#N'>
20
+ Start an existing stopped sandbox container
21
+ (e.g. after the Docker daemon restarted)
20
22
  vm status|start|stop Manage the sandbox VM (macOS) or check the backend (Windows)
21
23
 
22
24
  Run 'ai sandbox <command> --help' for details.`;
@@ -45,6 +47,21 @@ export async function runSandbox(args) {
45
47
  }
46
48
  break;
47
49
  }
50
+ case 'ls': {
51
+ const { ls } = await import("./commands/ls.js");
52
+ ls(rest);
53
+ break;
54
+ }
55
+ case 'prune': {
56
+ const { prune } = await import("./commands/prune.js");
57
+ await prune(rest);
58
+ break;
59
+ }
60
+ case 'rebuild': {
61
+ const { rebuild } = await import("./commands/rebuild.js");
62
+ await rebuild(rest);
63
+ break;
64
+ }
48
65
  case 'refresh': {
49
66
  const { refresh } = await import("./commands/refresh.js");
50
67
  const exitCode = await refresh(rest);
@@ -53,24 +70,14 @@ export async function runSandbox(args) {
53
70
  }
54
71
  break;
55
72
  }
56
- case 'start': {
57
- const { start } = await import("./commands/start.js");
58
- await start(rest);
59
- break;
60
- }
61
- case 'ls': {
62
- const { ls } = await import("./commands/ls.js");
63
- ls(rest);
64
- break;
65
- }
66
73
  case 'rm': {
67
74
  const { rm } = await import("./commands/rm.js");
68
75
  await rm(rest);
69
76
  break;
70
77
  }
71
- case 'prune': {
72
- const { prune } = await import("./commands/prune.js");
73
- await prune(rest);
78
+ case 'start': {
79
+ const { start } = await import("./commands/start.js");
80
+ await start(rest);
74
81
  break;
75
82
  }
76
83
  case 'vm': {
@@ -78,11 +85,6 @@ export async function runSandbox(args) {
78
85
  await vm(rest);
79
86
  break;
80
87
  }
81
- case 'rebuild': {
82
- const { rebuild } = await import("./commands/rebuild.js");
83
- await rebuild(rest);
84
- break;
85
- }
86
88
  default:
87
89
  throw new Error(`Unknown sandbox command: ${subcommand}`);
88
90
  }
@@ -9,7 +9,7 @@ symlink under \`$HOME\` (e.g. \`.tmux.conf\` -> \`/home/devuser/.tmux.conf\`),
9
9
  overriding image defaults so your editor, shell, and tool preferences follow
10
10
  you across \`ai sandbox destroy + create\`.
11
11
 
12
- See: https://github.com/fitlab-ai/agent-infra/blob/main/README.md#user-level-dotfiles-channel
12
+ See: https://github.com/fitlab-ai/agent-infra/blob/main/docs/en/sandbox.md#user-level-dotfiles-channel
13
13
 
14
14
  Common usage - drop files or symlinks here:
15
15
 
@@ -37,7 +37,7 @@ only writes \`README.md\` when it is missing, never when it already exists.
37
37
  (例如 \`.tmux.conf -> /home/devuser/.tmux.conf\`),覆盖镜像默认值,让你的编辑器、
38
38
  shell、工具偏好跨 \`ai sandbox destroy + create\` 持久存在。
39
39
 
40
- 参考:https://github.com/fitlab-ai/agent-infra/blob/main/README.zh-CN.md#用户级-dotfiles-通道
40
+ 参考:https://github.com/fitlab-ai/agent-infra/blob/main/docs/zh-CN/sandbox.md#用户级-dotfiles-通道
41
41
 
42
42
  常见用法:把文件或符号链接放进来:
43
43
 
@@ -62,7 +62,7 @@ This directory is mounted **read-write** into every sandbox container of this
62
62
  project at \`/share/common\`, regardless of branch. Drop files here to share
63
63
  between host and any sandbox without polluting the git worktree.
64
64
 
65
- See: https://github.com/fitlab-ai/agent-infra/blob/main/README.md#host-sandbox-file-exchange
65
+ See: https://github.com/fitlab-ai/agent-infra/blob/main/docs/en/sandbox.md#host-sandbox-file-exchange
66
66
 
67
67
  This file is safe to delete; the next \`ai sandbox create\` will re-create it.
68
68
 
@@ -73,7 +73,7 @@ This file is safe to delete; the next \`ai sandbox create\` will re-create it.
73
73
  该目录被以**读写**方式挂载到本项目所有 sandbox 容器的 \`/share/common\`,
74
74
  跨分支可见。可用来在宿主和任意 sandbox 之间传文件,无需弄脏 git worktree。
75
75
 
76
- 参考:https://github.com/fitlab-ai/agent-infra/blob/main/README.zh-CN.md#宿主-沙箱文件交换
76
+ 参考:https://github.com/fitlab-ai/agent-infra/blob/main/docs/zh-CN/sandbox.md#宿主-沙箱文件交换
77
77
 
78
78
  该文件可以安全删除;下一次 \`ai sandbox create\` 会重新生成。
79
79
  `;
@@ -83,7 +83,7 @@ This directory is mounted **read-write** into the sandbox container of this
83
83
  project's current branch at \`/share/branch\`. Files here are exclusive to this
84
84
  branch's sandbox and do not leak across branches.
85
85
 
86
- See: https://github.com/fitlab-ai/agent-infra/blob/main/README.md#host-sandbox-file-exchange
86
+ See: https://github.com/fitlab-ai/agent-infra/blob/main/docs/en/sandbox.md#host-sandbox-file-exchange
87
87
 
88
88
  This file is safe to delete; the next \`ai sandbox create\` will re-create it.
89
89
 
@@ -94,7 +94,7 @@ This file is safe to delete; the next \`ai sandbox create\` will re-create it.
94
94
  该目录被以**读写**方式挂载到本项目当前分支 sandbox 容器的 \`/share/branch\`,
95
95
  仅当前分支可见,不会跨分支泄漏。
96
96
 
97
- 参考:https://github.com/fitlab-ai/agent-infra/blob/main/README.zh-CN.md#宿主-沙箱文件交换
97
+ 参考:https://github.com/fitlab-ai/agent-infra/blob/main/docs/zh-CN/sandbox.md#宿主-沙箱文件交换
98
98
 
99
99
  该文件可以安全删除;下一次 \`ai sandbox create\` 会重新生成。
100
100
  `;
@@ -0,0 +1,58 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ /**
4
+ * Enumerate a task directory's artifacts ordered by modification time, oldest
5
+ * first, so the listing reads like the task's timeline. Filename ascending is a
6
+ * deterministic tiebreak when two files share the same mtime (e.g. written in
7
+ * the same millisecond).
8
+ *
9
+ * Only top-level regular files are included; subdirectories and dotfiles are
10
+ * skipped so every entry is something `cat` can print. The returned 1-based
11
+ * `index` is the source of truth shared by `files` and `cat`.
12
+ */
13
+ function enumerateArtifacts(taskDir) {
14
+ const entries = fs
15
+ .readdirSync(taskDir, { withFileTypes: true })
16
+ .filter((dirent) => dirent.isFile() && !dirent.name.startsWith('.'))
17
+ .map((dirent) => {
18
+ const abs = path.join(taskDir, dirent.name);
19
+ const stat = fs.statSync(abs);
20
+ return { name: dirent.name, path: abs, size: stat.size, mtimeMs: stat.mtimeMs };
21
+ });
22
+ entries.sort((a, b) => {
23
+ if (a.mtimeMs !== b.mtimeMs)
24
+ return a.mtimeMs - b.mtimeMs;
25
+ return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
26
+ });
27
+ return entries.map((entry, i) => ({ index: i + 1, ...entry }));
28
+ }
29
+ /**
30
+ * Resolve an artifact selector to an absolute path within `taskDir`. The
31
+ * selector is either a 1-based index `N` (as listed by `files`) or a filename
32
+ * (with or without the `.md` suffix). Throws with a clear message on failure.
33
+ */
34
+ function resolveArtifact(taskDir, artifactOrN) {
35
+ if (path.basename(artifactOrN) !== artifactOrN) {
36
+ throw new Error('artifact name must not contain path separators');
37
+ }
38
+ if (/^\d+$/.test(artifactOrN)) {
39
+ const n = Number(artifactOrN);
40
+ const match = enumerateArtifacts(taskDir).find((a) => a.index === n);
41
+ if (!match) {
42
+ throw new Error(`invalid artifact index ${n} (run 'ai task files <ref>' to list)`);
43
+ }
44
+ return match.path;
45
+ }
46
+ const candidates = artifactOrN.endsWith('.md')
47
+ ? [artifactOrN]
48
+ : [artifactOrN, `${artifactOrN}.md`];
49
+ for (const candidate of candidates) {
50
+ const abs = path.join(taskDir, candidate);
51
+ if (fs.existsSync(abs) && fs.statSync(abs).isFile()) {
52
+ return abs;
53
+ }
54
+ }
55
+ throw new Error(`artifact '${artifactOrN}' not found in task directory`);
56
+ }
57
+ export { enumerateArtifacts, resolveArtifact };
58
+ //# sourceMappingURL=artifacts.js.map
@@ -0,0 +1,38 @@
1
+ import fs from 'node:fs';
2
+ import { resolveTaskRef } from "../resolve-ref.js";
3
+ import { resolveArtifact } from "../artifacts.js";
4
+ const USAGE = `Usage: ai task cat <N | #N | TASK-id> <artifact | N>
5
+
6
+ Prints a task artifact's raw content to stdout.
7
+ <ref> Bare numeric / '#N' short id, or a full TASK-YYYYMMDD-HHMMSS id.
8
+ <artifact | N> Artifact filename (with or without '.md'), or the number from 'ai task files'.
9
+ `;
10
+ function cat(args = []) {
11
+ if (args[0] === '--help' || args[0] === '-h') {
12
+ process.stdout.write(USAGE);
13
+ return;
14
+ }
15
+ if (args.length < 2) {
16
+ process.stdout.write(USAGE);
17
+ process.exitCode = 1;
18
+ return;
19
+ }
20
+ const resolved = resolveTaskRef(args[0]);
21
+ if (!resolved.ok) {
22
+ process.stderr.write(`ai task cat: ${resolved.message}\n`);
23
+ process.exitCode = 1;
24
+ return;
25
+ }
26
+ let artifactPath;
27
+ try {
28
+ artifactPath = resolveArtifact(resolved.taskDir, args[1]);
29
+ }
30
+ catch (e) {
31
+ process.stderr.write(`ai task cat: ${e.message}\n`);
32
+ process.exitCode = 1;
33
+ return;
34
+ }
35
+ process.stdout.write(fs.readFileSync(artifactPath, 'utf8'));
36
+ }
37
+ export { cat };
38
+ //# sourceMappingURL=cat.js.map
@@ -0,0 +1,47 @@
1
+ import { formatTable } from "../../table.js";
2
+ import { resolveTaskRef } from "../resolve-ref.js";
3
+ import { enumerateArtifacts } from "../artifacts.js";
4
+ const USAGE = `Usage: ai task files <N | #N | TASK-id>
5
+
6
+ Lists the artifacts in a task directory with stable numbers.
7
+ <ref> Bare numeric / '#N' short id, or a full TASK-YYYYMMDD-HHMMSS id.
8
+
9
+ Columns: # (artifact number, usable with 'ai task cat') / NAME / SIZE (bytes) / MTIME
10
+ `;
11
+ const TABLE_HEADERS = ['#', 'NAME', 'SIZE', 'MTIME'];
12
+ function pad2(n) {
13
+ return String(n).padStart(2, '0');
14
+ }
15
+ function formatMtime(mtimeMs) {
16
+ const d = new Date(mtimeMs);
17
+ return (`${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ` +
18
+ `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`);
19
+ }
20
+ function files(args = []) {
21
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
22
+ process.stdout.write(USAGE);
23
+ if (args.length === 0)
24
+ process.exitCode = 1;
25
+ return;
26
+ }
27
+ const resolved = resolveTaskRef(args[0]);
28
+ if (!resolved.ok) {
29
+ process.stderr.write(`ai task files: ${resolved.message}\n`);
30
+ process.exitCode = 1;
31
+ return;
32
+ }
33
+ const artifacts = enumerateArtifacts(resolved.taskDir);
34
+ // Show the name without the `.md` suffix so the NAME column is exactly what
35
+ // `ai task cat <ref> <name>` accepts (the resolver re-adds `.md`).
36
+ const rows = artifacts.map((a) => [
37
+ String(a.index),
38
+ a.name.replace(/\.md$/, ''),
39
+ String(a.size),
40
+ formatMtime(a.mtimeMs)
41
+ ]);
42
+ for (const line of formatTable(TABLE_HEADERS, rows, { zebra: Boolean(process.stdout.isTTY) })) {
43
+ process.stdout.write(`${line}\n`);
44
+ }
45
+ }
46
+ export { files };
47
+ //# sourceMappingURL=files.js.map