@fitlab-ai/agent-infra 0.6.5 → 0.7.0

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 (181) hide show
  1. package/README.md +51 -25
  2. package/README.zh-CN.md +49 -23
  3. package/dist/lib/defaults.json +1 -0
  4. package/dist/lib/init.js +3 -0
  5. package/dist/lib/sandbox/commands/create.js +4 -2
  6. package/dist/lib/sandbox/commands/enter.js +15 -4
  7. package/dist/lib/sandbox/commands/list-running.js +108 -0
  8. package/dist/lib/sandbox/commands/ls.js +24 -45
  9. package/dist/lib/sandbox/commands/rebuild.js +4 -2
  10. package/dist/lib/sandbox/config.js +3 -0
  11. package/dist/lib/sandbox/index.js +2 -1
  12. package/dist/lib/sandbox/runtimes/ai-tools.dockerfile +10 -6
  13. package/dist/lib/sandbox/tools.js +213 -8
  14. package/dist/lib/update.js +12 -1
  15. package/lib/defaults.json +1 -0
  16. package/lib/init.ts +10 -0
  17. package/lib/sandbox/commands/create.ts +10 -2
  18. package/lib/sandbox/commands/enter.ts +14 -4
  19. package/lib/sandbox/commands/list-running.ts +135 -0
  20. package/lib/sandbox/commands/ls.ts +28 -49
  21. package/lib/sandbox/commands/rebuild.ts +9 -2
  22. package/lib/sandbox/config.ts +7 -0
  23. package/lib/sandbox/index.ts +2 -1
  24. package/lib/sandbox/runtimes/ai-tools.dockerfile +10 -6
  25. package/lib/sandbox/tools.ts +248 -9
  26. package/lib/update.ts +15 -1
  27. package/package.json +1 -1
  28. package/templates/.agents/QUICKSTART.en.md +1 -1
  29. package/templates/.agents/QUICKSTART.zh-CN.md +1 -1
  30. package/templates/.agents/README.en.md +79 -2
  31. package/templates/.agents/README.zh-CN.md +79 -2
  32. package/templates/.agents/rules/create-issue.en.md +1 -1
  33. package/templates/.agents/rules/create-issue.github.en.md +1 -1
  34. package/templates/.agents/rules/create-issue.github.zh-CN.md +1 -1
  35. package/templates/.agents/rules/create-issue.zh-CN.md +1 -1
  36. package/templates/.agents/rules/issue-sync.github.en.md +6 -5
  37. package/templates/.agents/rules/issue-sync.github.zh-CN.md +6 -5
  38. package/templates/.agents/rules/milestone-inference.github.en.md +2 -2
  39. package/templates/.agents/rules/milestone-inference.github.zh-CN.md +2 -2
  40. package/templates/.agents/rules/no-mid-flow-questions.en.md +57 -0
  41. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +57 -0
  42. package/templates/.agents/rules/pr-sync.github.en.md +4 -5
  43. package/templates/.agents/rules/pr-sync.github.zh-CN.md +4 -5
  44. package/templates/.agents/rules/task-management.en.md +9 -6
  45. package/templates/.agents/rules/task-management.zh-CN.md +9 -6
  46. package/templates/.agents/rules/testing-discipline.en.md +2 -2
  47. package/templates/.agents/rules/testing-discipline.zh-CN.md +2 -2
  48. package/templates/.agents/scripts/validate-artifact.js +1 -1
  49. package/templates/.agents/skills/analyze-task/SKILL.en.md +16 -4
  50. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +16 -4
  51. package/templates/.agents/skills/check-task/SKILL.en.md +43 -32
  52. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +42 -31
  53. package/templates/.agents/skills/code-task/SKILL.en.md +117 -0
  54. package/templates/.agents/skills/{implement-task → code-task}/SKILL.zh-CN.md +51 -24
  55. package/templates/.agents/skills/{implement-task → code-task}/config/verify.en.json +4 -4
  56. package/templates/.agents/skills/{implement-task → code-task}/config/verify.zh-CN.json +4 -4
  57. package/templates/.agents/skills/{implement-task → code-task}/reference/branch-management.zh-CN.md +2 -2
  58. package/templates/.agents/skills/{implement-task/reference/implementation-rules.en.md → code-task/reference/code-rules.en.md} +6 -6
  59. package/templates/.agents/skills/{implement-task/reference/implementation-rules.zh-CN.md → code-task/reference/code-rules.zh-CN.md} +3 -3
  60. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +69 -0
  61. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +69 -0
  62. package/templates/.agents/skills/{refine-task/reference/fix-workflow.en.md → code-task/reference/fix-mode.en.md} +12 -12
  63. package/templates/.agents/skills/{refine-task/reference/fix-workflow.zh-CN.md → code-task/reference/fix-mode.zh-CN.md} +8 -8
  64. package/templates/.agents/skills/code-task/reference/output-template.en.md +20 -0
  65. package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +20 -0
  66. package/templates/.agents/skills/{implement-task → code-task}/reference/report-template.en.md +4 -4
  67. package/templates/.agents/skills/{implement-task → code-task}/reference/report-template.zh-CN.md +3 -3
  68. package/templates/.agents/skills/code-task/scripts/detect-mode.js +370 -0
  69. package/templates/.agents/skills/commit/SKILL.en.md +2 -2
  70. package/templates/.agents/skills/commit/SKILL.zh-CN.md +2 -2
  71. package/templates/.agents/skills/commit/reference/task-status-update.en.md +10 -6
  72. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +10 -6
  73. package/templates/.agents/skills/complete-task/SKILL.en.md +5 -3
  74. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +5 -3
  75. package/templates/.agents/skills/create-pr/SKILL.en.md +17 -1
  76. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +17 -1
  77. package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
  78. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  79. package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -2
  80. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +2 -2
  81. package/templates/.agents/skills/import-issue/SKILL.en.md +3 -3
  82. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +3 -3
  83. package/templates/.agents/skills/plan-task/SKILL.en.md +4 -4
  84. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -4
  85. package/templates/.agents/skills/restore-task/SKILL.en.md +4 -3
  86. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +4 -3
  87. package/templates/.agents/skills/review-analysis/SKILL.en.md +76 -0
  88. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +102 -0
  89. package/templates/.agents/skills/review-analysis/config/verify.en.json +51 -0
  90. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +51 -0
  91. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +87 -0
  92. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +87 -0
  93. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +90 -0
  94. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +91 -0
  95. package/templates/.agents/skills/review-analysis/reference/review-criteria.en.md +47 -0
  96. package/templates/.agents/skills/review-analysis/reference/review-criteria.zh-CN.md +47 -0
  97. package/templates/.agents/skills/{review-task → review-code}/SKILL.en.md +11 -9
  98. package/templates/.agents/skills/{review-task → review-code}/SKILL.zh-CN.md +15 -9
  99. package/templates/.agents/skills/{review-task → review-code}/config/verify.en.json +7 -5
  100. package/templates/.agents/skills/{review-task → review-code}/config/verify.zh-CN.json +6 -4
  101. package/templates/.agents/skills/{review-task → review-code}/reference/output-templates.en.md +21 -17
  102. package/templates/.agents/skills/{review-task → review-code}/reference/output-templates.zh-CN.md +19 -15
  103. package/templates/.agents/skills/{review-task → review-code}/reference/report-template.en.md +5 -6
  104. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +91 -0
  105. package/templates/.agents/skills/review-code/reference/review-criteria.en.md +48 -0
  106. package/templates/.agents/skills/{review-task → review-code}/reference/review-criteria.zh-CN.md +10 -4
  107. package/templates/.agents/skills/review-plan/SKILL.en.md +76 -0
  108. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +102 -0
  109. package/templates/.agents/skills/{refine-task → review-plan}/config/verify.en.json +14 -10
  110. package/templates/.agents/skills/{refine-task → review-plan}/config/verify.zh-CN.json +14 -10
  111. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +87 -0
  112. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +87 -0
  113. package/templates/.agents/skills/review-plan/reference/report-template.en.md +90 -0
  114. package/templates/.agents/skills/{review-task → review-plan}/reference/report-template.zh-CN.md +3 -3
  115. package/templates/.agents/skills/review-plan/reference/review-criteria.en.md +47 -0
  116. package/templates/.agents/skills/review-plan/reference/review-criteria.zh-CN.md +47 -0
  117. package/templates/.agents/skills/test/SKILL.en.md +2 -2
  118. package/templates/.agents/skills/test/SKILL.zh-CN.md +13 -31
  119. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +1 -0
  120. package/templates/.agents/templates/task.en.md +3 -3
  121. package/templates/.agents/templates/task.zh-CN.md +2 -2
  122. package/templates/.agents/workflows/bug-fix.en.yaml +126 -80
  123. package/templates/.agents/workflows/bug-fix.zh-CN.yaml +90 -44
  124. package/templates/.agents/workflows/feature-development.en.yaml +115 -70
  125. package/templates/.agents/workflows/feature-development.zh-CN.yaml +92 -47
  126. package/templates/.agents/workflows/refactoring.en.yaml +123 -78
  127. package/templates/.agents/workflows/refactoring.zh-CN.yaml +89 -44
  128. package/templates/.claude/commands/code-task.en.md +8 -0
  129. package/templates/.claude/commands/code-task.zh-CN.md +8 -0
  130. package/templates/.claude/commands/review-analysis.en.md +8 -0
  131. package/templates/.claude/commands/review-analysis.zh-CN.md +8 -0
  132. package/templates/.claude/commands/review-code.en.md +8 -0
  133. package/templates/.claude/commands/review-code.zh-CN.md +8 -0
  134. package/templates/.claude/commands/review-plan.en.md +8 -0
  135. package/templates/.claude/commands/review-plan.zh-CN.md +8 -0
  136. package/templates/.gemini/commands/_project_/archive-tasks.zh-CN.toml +1 -1
  137. package/templates/.gemini/commands/_project_/code-task.en.toml +8 -0
  138. package/templates/.gemini/commands/_project_/code-task.zh-CN.toml +8 -0
  139. package/templates/.gemini/commands/_project_/init-labels.zh-CN.toml +1 -1
  140. package/templates/.gemini/commands/_project_/init-milestones.zh-CN.toml +1 -1
  141. package/templates/.gemini/commands/_project_/review-analysis.en.toml +8 -0
  142. package/templates/.gemini/commands/_project_/review-analysis.zh-CN.toml +8 -0
  143. package/templates/.gemini/commands/_project_/review-code.en.toml +8 -0
  144. package/templates/.gemini/commands/_project_/review-code.zh-CN.toml +8 -0
  145. package/templates/.gemini/commands/_project_/review-plan.en.toml +8 -0
  146. package/templates/.gemini/commands/_project_/review-plan.zh-CN.toml +8 -0
  147. package/templates/.opencode/commands/code-task.en.md +11 -0
  148. package/templates/.opencode/commands/code-task.zh-CN.md +11 -0
  149. package/templates/.opencode/commands/review-analysis.en.md +11 -0
  150. package/templates/.opencode/commands/review-analysis.zh-CN.md +11 -0
  151. package/templates/.opencode/commands/review-code.en.md +11 -0
  152. package/templates/.opencode/commands/review-code.zh-CN.md +11 -0
  153. package/templates/.opencode/commands/review-plan.en.md +11 -0
  154. package/templates/.opencode/commands/review-plan.zh-CN.md +11 -0
  155. package/templates/.agents/skills/implement-task/SKILL.en.md +0 -173
  156. package/templates/.agents/skills/implement-task/reference/output-template.en.md +0 -20
  157. package/templates/.agents/skills/implement-task/reference/output-template.zh-CN.md +0 -20
  158. package/templates/.agents/skills/refine-task/SKILL.en.md +0 -153
  159. package/templates/.agents/skills/refine-task/SKILL.zh-CN.md +0 -153
  160. package/templates/.agents/skills/refine-task/reference/report-template.en.md +0 -64
  161. package/templates/.agents/skills/refine-task/reference/report-template.zh-CN.md +0 -64
  162. package/templates/.agents/skills/review-task/reference/review-criteria.en.md +0 -42
  163. package/templates/.claude/commands/implement-task.en.md +0 -8
  164. package/templates/.claude/commands/implement-task.zh-CN.md +0 -8
  165. package/templates/.claude/commands/refine-task.en.md +0 -8
  166. package/templates/.claude/commands/refine-task.zh-CN.md +0 -8
  167. package/templates/.claude/commands/review-task.en.md +0 -8
  168. package/templates/.claude/commands/review-task.zh-CN.md +0 -8
  169. package/templates/.gemini/commands/_project_/implement-task.en.toml +0 -8
  170. package/templates/.gemini/commands/_project_/implement-task.zh-CN.toml +0 -8
  171. package/templates/.gemini/commands/_project_/refine-task.en.toml +0 -8
  172. package/templates/.gemini/commands/_project_/refine-task.zh-CN.toml +0 -8
  173. package/templates/.gemini/commands/_project_/review-task.en.toml +0 -8
  174. package/templates/.gemini/commands/_project_/review-task.zh-CN.toml +0 -8
  175. package/templates/.opencode/commands/implement-task.en.md +0 -11
  176. package/templates/.opencode/commands/implement-task.zh-CN.md +0 -11
  177. package/templates/.opencode/commands/refine-task.en.md +0 -11
  178. package/templates/.opencode/commands/refine-task.zh-CN.md +0 -11
  179. package/templates/.opencode/commands/review-task.en.md +0 -11
  180. package/templates/.opencode/commands/review-task.zh-CN.md +0 -11
  181. /package/templates/.agents/skills/{implement-task → code-task}/reference/branch-management.en.md +0 -0
@@ -5,51 +5,36 @@ import pc from 'picocolors';
5
5
  import { loadConfig } from '../config.ts';
6
6
  import { sandboxBranchLabel, sandboxLabel } from '../constants.ts';
7
7
  import { detectEngine } from '../engine.ts';
8
- import { runSafeEngine } from '../shell.ts';
9
8
  import { resolveTools, toolProjectDirCandidates } from '../tools.ts';
9
+ import { fetchSandboxRows } from './list-running.ts';
10
10
 
11
- const USAGE = 'Usage: ai sandbox ls';
12
- const CONTAINER_TABLE_HEADERS = ['NAMES', 'STATUS', 'BRANCH'] as const;
11
+ export { containerListFormat, parseLabels } from './list-running.ts';
12
+
13
+ const USAGE = `Usage: ai sandbox ls
14
+
15
+ Lists all containers for the current project. The leftmost '#' column
16
+ numbers running sandboxes; use it as "ai sandbox exec '#N'" to enter one.
17
+ Quote '#N' to avoid shell '#' comment handling.`;
18
+
19
+ const CONTAINER_TABLE_HEADERS = ['#', 'NAMES', 'STATUS', 'BRANCH'] as const;
13
20
 
14
21
  type ContainerTableRow = {
22
+ index: string;
15
23
  name: string;
16
24
  status: string;
17
25
  branch: string;
18
26
  };
19
27
 
20
- // Exported to lock the docker/podman-compatible format in unit tests.
21
- export function containerListFormat(): string {
22
- return '{{.Names}}\t{{.Status}}\t{{.Labels}}';
23
- }
24
-
25
- export function parseLabels(csv: string): Record<string, string> {
26
- if (!csv) {
27
- return {};
28
- }
29
-
30
- const labels: Record<string, string> = {};
31
- for (const pair of csv.split(',')) {
32
- if (!pair) {
33
- continue;
34
- }
35
- const eq = pair.indexOf('=');
36
- if (eq < 0) {
37
- continue;
38
- }
39
- labels[pair.slice(0, eq)] = pair.slice(eq + 1);
40
- }
41
- return labels;
42
- }
43
-
44
28
  export function formatContainerTable(rows: ContainerTableRow[]): string[] {
45
- const columns = rows.map((row) => [row.name, row.status, row.branch] as const);
29
+ const columns = rows.map((row) => [row.index, row.name, row.status, row.branch] as const);
46
30
  const widths = [
47
- Math.max(CONTAINER_TABLE_HEADERS[0].length, ...rows.map((row) => row.name.length)),
48
- Math.max(CONTAINER_TABLE_HEADERS[1].length, ...rows.map((row) => row.status.length)),
49
- Math.max(CONTAINER_TABLE_HEADERS[2].length, ...rows.map((row) => row.branch.length))
31
+ Math.max(CONTAINER_TABLE_HEADERS[0].length, ...rows.map((row) => row.index.length)),
32
+ Math.max(CONTAINER_TABLE_HEADERS[1].length, ...rows.map((row) => row.name.length)),
33
+ Math.max(CONTAINER_TABLE_HEADERS[2].length, ...rows.map((row) => row.status.length)),
34
+ Math.max(CONTAINER_TABLE_HEADERS[3].length, ...rows.map((row) => row.branch.length))
50
35
  ] as const;
51
- const renderRow = (values: readonly [string, string, string]): string =>
52
- `${values[0].padEnd(widths[0])} ${values[1].padEnd(widths[1])} ${values[2]}`.trimEnd();
36
+ const renderRow = (values: readonly [string, string, string, string]): string =>
37
+ `${values[0].padEnd(widths[0])} ${values[1].padEnd(widths[1])} ${values[2].padEnd(widths[2])} ${values[3]}`.trimEnd();
53
38
 
54
39
  return [
55
40
  renderRow(CONTAINER_TABLE_HEADERS),
@@ -75,28 +60,22 @@ export function ls(args: string[] = []): void {
75
60
  const engine = detectEngine(config);
76
61
  const tools = resolveTools(config);
77
62
  const label = sandboxLabel(config);
78
- const containers = runSafeEngine(engine, 'docker', [
79
- 'ps',
80
- '-a',
81
- '--filter',
82
- `label=${label}`,
83
- '--format',
84
- containerListFormat()
85
- ]);
63
+ const { running, nonRunning } = fetchSandboxRows(engine, label, sandboxBranchLabel(config));
86
64
 
87
65
  p.intro(pc.cyan(`Sandbox status for ${config.project}`));
88
66
 
89
67
  p.log.step('Containers');
90
- if (!containers) {
68
+ const ordered = [...running, ...nonRunning];
69
+ if (ordered.length === 0) {
91
70
  p.log.warn(' No sandbox containers');
92
71
  } else {
93
- const branchKey = sandboxBranchLabel(config);
94
- const rows = containers.split('\n').map((line) => {
95
- const [name = '', status = '', labelsCsv = ''] = line.split('\t');
96
- const branch = parseLabels(labelsCsv)[branchKey] ?? '';
97
- return { name, status, branch };
98
- });
99
- for (const line of formatContainerTable(rows)) {
72
+ const tableRows: ContainerTableRow[] = ordered.map((row) => ({
73
+ index: row.index === null ? '' : String(row.index),
74
+ name: row.name,
75
+ status: row.status,
76
+ branch: row.branch
77
+ }));
78
+ for (const line of formatContainerTable(tableRows)) {
100
79
  process.stdout.write(` ${line}\n`);
101
80
  }
102
81
  }
@@ -8,7 +8,12 @@ import { prepareDockerfile } from '../dockerfile.ts';
8
8
  import { sandboxImageConfigLabel, sandboxLabel } from '../constants.ts';
9
9
  import { detectEngine, ensureDocker } from '../engine.ts';
10
10
  import { runEngine, runOkEngine, runSafeEngine, runVerboseEngine } from '../shell.ts';
11
- import { resolveTools, toolNpmPackagesArg } from '../tools.ts';
11
+ import {
12
+ imageSignatureFields,
13
+ resolveTools,
14
+ toolNpmPackagesArg,
15
+ toolShellInstallScriptBase64
16
+ } from '../tools.ts';
12
17
  import type { SandboxTool } from '../tools.ts';
13
18
  import { toEnginePath } from '../engines/wsl2-paths.ts';
14
19
  import { resolveBuildUid } from '../engines/native.ts';
@@ -23,7 +28,7 @@ function buildSignature(preparedDockerfile: PreparedDockerfile, tools: SandboxTo
23
28
  return createHash('sha256')
24
29
  .update(JSON.stringify({
25
30
  dockerfile: preparedDockerfile.signature,
26
- tools: tools.map((tool) => tool.npmPackage)
31
+ tools: imageSignatureFields(tools)
27
32
  }))
28
33
  .digest('hex')
29
34
  .slice(0, 12);
@@ -66,6 +71,8 @@ export function buildArgs(
66
71
  `HOST_GID=${hostGid}`,
67
72
  '--build-arg',
68
73
  `AI_TOOL_PACKAGES=${toolNpmPackagesArg(tools)}`,
74
+ '--build-arg',
75
+ `AI_TOOLS_SHELL_INSTALL_B64=${toolShellInstallScriptBase64(tools)}`,
69
76
  '--label',
70
77
  sandboxLabel(config),
71
78
  '--label',
@@ -6,6 +6,8 @@ import pc from 'picocolors';
6
6
  import { validateSandboxEngine } from './engine.ts';
7
7
  import { hostJoin } from './engines/wsl2-paths.ts';
8
8
  import { findRuntimeEngineMismatches } from './runtime-engines.ts';
9
+ import { parseCustomTools } from './tools.ts';
10
+ import type { SandboxTool } from './tools.ts';
9
11
 
10
12
  const DEFAULTS = Object.freeze({
11
13
  engine: null,
@@ -26,6 +28,7 @@ type SandboxConfigInput = {
26
28
  engine?: string | null;
27
29
  runtimes?: string[];
28
30
  tools?: string[];
31
+ customTools?: unknown;
29
32
  dockerfile?: string | null;
30
33
  vm?: Record<string, unknown>;
31
34
  };
@@ -51,6 +54,7 @@ export type SandboxConfig = {
51
54
  engine: string | null;
52
55
  runtimes: string[];
53
56
  tools: string[];
57
+ customTools: SandboxTool[];
54
58
  dockerfile: string | null;
55
59
  vm: SandboxVmConfig;
56
60
  };
@@ -136,6 +140,8 @@ export function loadConfig({
136
140
  }
137
141
  }
138
142
 
143
+ const customTools = parseCustomTools(sandbox.customTools, { home });
144
+
139
145
  return {
140
146
  repoRoot,
141
147
  configPath,
@@ -153,6 +159,7 @@ export function loadConfig({
153
159
  tools: Array.isArray(sandbox.tools) && sandbox.tools.length > 0
154
160
  ? [...sandbox.tools]
155
161
  : defaults.tools,
162
+ customTools,
156
163
  dockerfile,
157
164
  vm: {
158
165
  cpu: asPositiveNumberOrNull(sandbox.vm?.cpu) ?? defaults.vm.cpu,
@@ -2,7 +2,8 @@ const USAGE = `Usage: ai sandbox <command> [options]
2
2
 
3
3
  Commands:
4
4
  create <branch> [base] Create a sandbox (VM + image + worktree + container)
5
- exec <branch> [cmd...] Enter sandbox or run a command
5
+ exec <branch | '#N'> [cmd...]
6
+ Enter sandbox or run a command (use leftmost '#' column from 'ls')
6
7
  ls List sandboxes for the current project
7
8
  prune [--dry-run] Remove orphaned per-branch state dirs
8
9
  rebuild [--quiet] [--refresh]
@@ -4,16 +4,20 @@ ENV OPENCODE_DISABLE_AUTOUPDATE=1
4
4
  ENV NPM_CONFIG_PREFIX=/home/devuser/.npm-global
5
5
  ENV PATH="/home/devuser/.npm-global/bin:${PATH}"
6
6
 
7
- ARG AI_TOOL_PACKAGES
8
- RUN if [ -z "${AI_TOOL_PACKAGES}" ]; then \
9
- echo "AI_TOOL_PACKAGES build arg is required"; \
10
- exit 1; \
11
- fi && \
12
- set -e && \
7
+ ARG AI_TOOL_PACKAGES=
8
+ RUN set -e && \
13
9
  for pkg in ${AI_TOOL_PACKAGES}; do \
14
10
  npm install -g "$pkg"; \
15
11
  done
16
12
 
13
+ ARG AI_TOOLS_SHELL_INSTALL_B64=
14
+ RUN if [ -n "${AI_TOOLS_SHELL_INSTALL_B64}" ]; then \
15
+ set -e && \
16
+ echo "${AI_TOOLS_SHELL_INSTALL_B64}" | base64 -d > /tmp/ai-tools-install.sh && \
17
+ bash /tmp/ai-tools-install.sh && \
18
+ rm /tmp/ai-tools-install.sh; \
19
+ fi
20
+
17
21
  RUN npm install -g pyright
18
22
 
19
23
  RUN mkdir -p /home/devuser/.local/share /home/devuser/.local/state
@@ -1,10 +1,14 @@
1
1
  import { safeNameCandidates, sanitizeBranchName } from './constants.ts';
2
2
  import { hostJoin } from './engines/wsl2-paths.ts';
3
3
 
4
+ export type SandboxToolInstall =
5
+ | { type: 'npm'; cmd: string }
6
+ | { type: 'shell'; cmd: string };
7
+
4
8
  export type SandboxTool = {
5
9
  id: string;
6
10
  name: string;
7
- npmPackage: string;
11
+ install: SandboxToolInstall;
8
12
  sandboxBase: string;
9
13
  containerMount: string;
10
14
  versionCmd: string;
@@ -21,14 +25,17 @@ type ToolsConfig = {
21
25
  home: string;
22
26
  project: string;
23
27
  tools: string[];
28
+ customTools?: SandboxTool[];
24
29
  };
25
30
 
31
+ const TOOL_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
32
+
26
33
  function createBuiltinTools(home: string, project: string): Record<string, SandboxTool> {
27
34
  return {
28
35
  'claude-code': {
29
36
  id: 'claude-code',
30
37
  name: 'Claude Code',
31
- npmPackage: '@anthropic-ai/claude-code@stable',
38
+ install: { type: 'npm', cmd: '@anthropic-ai/claude-code@stable' },
32
39
  sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'claude-code'),
33
40
  containerMount: '/home/devuser/.claude',
34
41
  versionCmd: 'claude --version',
@@ -58,7 +65,7 @@ function createBuiltinTools(home: string, project: string): Record<string, Sandb
58
65
  codex: {
59
66
  id: 'codex',
60
67
  name: 'Codex',
61
- npmPackage: '@openai/codex',
68
+ install: { type: 'npm', cmd: '@openai/codex' },
62
69
  sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'codex'),
63
70
  containerMount: '/home/devuser/.codex',
64
71
  versionCmd: 'codex --version',
@@ -73,7 +80,7 @@ function createBuiltinTools(home: string, project: string): Record<string, Sandb
73
80
  opencode: {
74
81
  id: 'opencode',
75
82
  name: 'OpenCode',
76
- npmPackage: 'opencode-ai',
83
+ install: { type: 'npm', cmd: 'opencode-ai' },
77
84
  sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'opencode'),
78
85
  containerMount: '/home/devuser/.local/share/opencode',
79
86
  versionCmd: 'opencode version',
@@ -92,7 +99,7 @@ function createBuiltinTools(home: string, project: string): Record<string, Sandb
92
99
  'gemini-cli': {
93
100
  id: 'gemini-cli',
94
101
  name: 'Gemini CLI',
95
- npmPackage: '@google/gemini-cli',
102
+ install: { type: 'npm', cmd: '@google/gemini-cli' },
96
103
  sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'gemini-cli'),
97
104
  containerMount: '/home/devuser/.gemini',
98
105
  versionCmd: 'gemini --version',
@@ -108,16 +115,224 @@ function createBuiltinTools(home: string, project: string): Record<string, Sandb
108
115
  };
109
116
  }
110
117
 
118
+ export function builtinToolIds(): string[] {
119
+ return Object.keys(createBuiltinTools('', ''));
120
+ }
121
+
111
122
  function validateTool(tool: SandboxTool): void {
112
- if (!tool.npmPackage || !tool.containerMount.startsWith('/')) {
113
- throw new Error(`Invalid sandbox tool descriptor: ${tool.id}`);
123
+ if (!tool.id || !TOOL_ID_PATTERN.test(tool.id)) {
124
+ throw new Error(`Invalid sandbox tool id: ${String(tool.id)}`);
125
+ }
126
+ if (!tool.install || (tool.install.type !== 'npm' && tool.install.type !== 'shell')) {
127
+ throw new Error(`Sandbox tool ${tool.id} has invalid install.type`);
128
+ }
129
+ if (!tool.install.cmd) {
130
+ throw new Error(`Sandbox tool ${tool.id} has empty install.cmd`);
131
+ }
132
+ if (!tool.containerMount || !tool.containerMount.startsWith('/')) {
133
+ throw new Error(`Sandbox tool ${tool.id} containerMount must be an absolute path`);
134
+ }
135
+ }
136
+
137
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
138
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
139
+ }
140
+
141
+ function asString(value: unknown, field: string, context: string): string {
142
+ if (typeof value !== 'string') {
143
+ throw new Error(`${context}: field "${field}" must be a string`);
144
+ }
145
+ return value;
146
+ }
147
+
148
+ function asOptionalNonEmptyString(value: unknown, field: string, context: string): string | undefined {
149
+ if (value === undefined) {
150
+ return undefined;
151
+ }
152
+ if (typeof value !== 'string') {
153
+ throw new Error(`${context}: field "${field}" must be a string when provided`);
154
+ }
155
+ if (value.length === 0) {
156
+ throw new Error(`${context}: field "${field}" must be non-empty when provided`);
157
+ }
158
+ return value;
159
+ }
160
+
161
+ function asStringRecord(value: unknown, field: string, context: string): Record<string, string> | undefined {
162
+ if (value === undefined) {
163
+ return undefined;
164
+ }
165
+ if (!isPlainObject(value)) {
166
+ throw new Error(`${context}: field "${field}" must be an object when provided`);
167
+ }
168
+ const out: Record<string, string> = {};
169
+ for (const [key, val] of Object.entries(value)) {
170
+ if (typeof val !== 'string') {
171
+ throw new Error(`${context}: field "${field}.${key}" must be a string`);
172
+ }
173
+ out[key] = val;
174
+ }
175
+ return out;
176
+ }
177
+
178
+ function asStringArray(value: unknown, field: string, context: string): string[] | undefined {
179
+ if (value === undefined) {
180
+ return undefined;
181
+ }
182
+ if (!Array.isArray(value)) {
183
+ throw new Error(`${context}: field "${field}" must be an array when provided`);
184
+ }
185
+ return value.map((item, index) => {
186
+ if (typeof item !== 'string') {
187
+ throw new Error(`${context}: field "${field}[${index}]" must be a string`);
188
+ }
189
+ return item;
190
+ });
191
+ }
192
+
193
+ function parseInstall(value: unknown, context: string): SandboxToolInstall {
194
+ if (!isPlainObject(value)) {
195
+ throw new Error(`${context}: field "install" must be an object`);
196
+ }
197
+ const type = value.type;
198
+ if (type !== 'npm' && type !== 'shell') {
199
+ throw new Error(`${context}: field "install.type" must be "npm" or "shell"`);
200
+ }
201
+ const cmd = asString(value.cmd, 'install.cmd', context);
202
+ if (!cmd) {
203
+ throw new Error(`${context}: field "install.cmd" must be non-empty`);
204
+ }
205
+ return { type, cmd };
206
+ }
207
+
208
+ function parseHostPreSeedFiles(value: unknown, context: string): SandboxTool['hostPreSeedFiles'] {
209
+ if (value === undefined) {
210
+ return undefined;
211
+ }
212
+ if (!Array.isArray(value)) {
213
+ throw new Error(`${context}: field "hostPreSeedFiles" must be an array when provided`);
214
+ }
215
+ return value.map((item, index) => {
216
+ if (!isPlainObject(item)) {
217
+ throw new Error(`${context}: field "hostPreSeedFiles[${index}]" must be an object`);
218
+ }
219
+ return {
220
+ hostPath: asString(item.hostPath, `hostPreSeedFiles[${index}].hostPath`, context),
221
+ sandboxName: asString(item.sandboxName, `hostPreSeedFiles[${index}].sandboxName`, context)
222
+ };
223
+ });
224
+ }
225
+
226
+ function parseHostPreSeedDirs(value: unknown, context: string): SandboxTool['hostPreSeedDirs'] {
227
+ if (value === undefined) {
228
+ return undefined;
229
+ }
230
+ if (!Array.isArray(value)) {
231
+ throw new Error(`${context}: field "hostPreSeedDirs" must be an array when provided`);
114
232
  }
233
+ return value.map((item, index) => {
234
+ if (!isPlainObject(item)) {
235
+ throw new Error(`${context}: field "hostPreSeedDirs[${index}]" must be an object`);
236
+ }
237
+ return {
238
+ hostDir: asString(item.hostDir, `hostPreSeedDirs[${index}].hostDir`, context),
239
+ sandboxSubdir: asString(item.sandboxSubdir, `hostPreSeedDirs[${index}].sandboxSubdir`, context)
240
+ };
241
+ });
242
+ }
243
+
244
+ function parseHostLiveMounts(value: unknown, context: string): SandboxTool['hostLiveMounts'] {
245
+ if (value === undefined) {
246
+ return undefined;
247
+ }
248
+ if (!Array.isArray(value)) {
249
+ throw new Error(`${context}: field "hostLiveMounts" must be an array when provided`);
250
+ }
251
+ return value.map((item, index) => {
252
+ if (!isPlainObject(item)) {
253
+ throw new Error(`${context}: field "hostLiveMounts[${index}]" must be an object`);
254
+ }
255
+ return {
256
+ hostPath: asString(item.hostPath, `hostLiveMounts[${index}].hostPath`, context),
257
+ containerSubpath: asString(item.containerSubpath, `hostLiveMounts[${index}].containerSubpath`, context)
258
+ };
259
+ });
260
+ }
261
+
262
+ export function parseCustomTool(
263
+ entry: unknown,
264
+ index: number,
265
+ options: { home: string }
266
+ ): SandboxTool {
267
+ const context = `customTools[${index}]`;
268
+ if (!isPlainObject(entry)) {
269
+ throw new Error(`${context} must be an object`);
270
+ }
271
+
272
+ const id = asString(entry.id, 'id', context);
273
+ if (!TOOL_ID_PATTERN.test(id)) {
274
+ throw new Error(`${context}: field "id" must match ${TOOL_ID_PATTERN.source}`);
275
+ }
276
+
277
+ const containerMount = asOptionalNonEmptyString(entry.containerMount, 'containerMount', context)
278
+ ?? `/home/devuser/.${id}`;
279
+ if (!containerMount.startsWith('/')) {
280
+ throw new Error(`${context}: field "containerMount" must be an absolute path`);
281
+ }
282
+
283
+ const tool: SandboxTool = {
284
+ id,
285
+ name: asOptionalNonEmptyString(entry.name, 'name', context) ?? id,
286
+ install: parseInstall(entry.install, context),
287
+ sandboxBase: hostJoin(options.home, '.agent-infra', 'sandboxes', id),
288
+ containerMount,
289
+ versionCmd: asOptionalNonEmptyString(entry.versionCmd, 'versionCmd', context) ?? `which ${id}`,
290
+ setupHint: asOptionalNonEmptyString(entry.setupHint, 'setupHint', context)
291
+ ?? `Run \`${id}\` inside the container to set up.`,
292
+ envVars: asStringRecord(entry.envVars, 'envVars', context),
293
+ hostPreSeedFiles: parseHostPreSeedFiles(entry.hostPreSeedFiles, context),
294
+ hostPreSeedDirs: parseHostPreSeedDirs(entry.hostPreSeedDirs, context),
295
+ pathRewriteFiles: asStringArray(entry.pathRewriteFiles, 'pathRewriteFiles', context),
296
+ hostLiveMounts: parseHostLiveMounts(entry.hostLiveMounts, context),
297
+ postSetupCmds: asStringArray(entry.postSetupCmds, 'postSetupCmds', context)
298
+ };
299
+
300
+ validateTool(tool);
301
+ return tool;
302
+ }
303
+
304
+ export function parseCustomTools(value: unknown, options: { home: string }): SandboxTool[] {
305
+ if (value === undefined || value === null) {
306
+ return [];
307
+ }
308
+ if (!Array.isArray(value)) {
309
+ throw new Error('sandbox: "customTools" must be an array');
310
+ }
311
+ return value.map((entry, index) => parseCustomTool(entry, index, options));
115
312
  }
116
313
 
117
314
  export function resolveTools(config: ToolsConfig): SandboxTool[] {
118
315
  const builtins = createBuiltinTools(config.home, config.project);
316
+ const customs = config.customTools ?? [];
317
+
318
+ const seen = new Set<string>();
319
+ for (const tool of customs) {
320
+ if (builtins[tool.id]) {
321
+ throw new Error(`Custom sandbox tool id "${tool.id}" collides with a built-in tool`);
322
+ }
323
+ if (seen.has(tool.id)) {
324
+ throw new Error(`Duplicate sandbox tool id "${tool.id}" in customTools`);
325
+ }
326
+ seen.add(tool.id);
327
+ }
328
+
329
+ const merged: Record<string, SandboxTool> = { ...builtins };
330
+ for (const tool of customs) {
331
+ merged[tool.id] = tool;
332
+ }
333
+
119
334
  return config.tools.map((id) => {
120
- const tool = builtins[id];
335
+ const tool = merged[id];
121
336
  if (!tool) {
122
337
  throw new Error(`Unknown sandbox tool: ${id}`);
123
338
  }
@@ -139,5 +354,29 @@ export function toolProjectDirCandidates(tool: SandboxTool, project: string): st
139
354
  }
140
355
 
141
356
  export function toolNpmPackagesArg(tools: SandboxTool[]): string {
142
- return tools.map((tool) => tool.npmPackage).join(' ');
357
+ return tools
358
+ .filter((tool) => tool.install.type === 'npm')
359
+ .map((tool) => tool.install.cmd)
360
+ .join(' ');
361
+ }
362
+
363
+ export function toolShellInstallScript(tools: SandboxTool[]): string {
364
+ const blocks = tools
365
+ .filter((tool) => tool.install.type === 'shell')
366
+ .map((tool) => `# install: ${tool.id}\n${tool.install.cmd}`);
367
+
368
+ if (blocks.length === 0) {
369
+ return '';
370
+ }
371
+
372
+ return ['#!/bin/bash', 'set -e', '', ...blocks, ''].join('\n');
373
+ }
374
+
375
+ export function toolShellInstallScriptBase64(tools: SandboxTool[]): string {
376
+ const script = toolShellInstallScript(tools);
377
+ return script ? Buffer.from(script, 'utf8').toString('base64') : '';
378
+ }
379
+
380
+ export function imageSignatureFields(tools: SandboxTool[]): Array<{ id: string; install: SandboxToolInstall }> {
381
+ return tools.map((tool) => ({ id: tool.id, install: tool.install }));
143
382
  }
package/lib/update.ts CHANGED
@@ -15,6 +15,7 @@ type UpdateConfig = {
15
15
  org: string;
16
16
  language: string;
17
17
  platform?: { type?: string };
18
+ requiresPullRequest?: boolean;
18
19
  sandbox?: Record<string, unknown>;
19
20
  labels?: Record<string, unknown>;
20
21
  files?: Partial<FileRegistry>;
@@ -22,6 +23,7 @@ type UpdateConfig = {
22
23
 
23
24
  type Defaults = {
24
25
  platform: { type: string };
26
+ requiresPullRequest: boolean;
25
27
  sandbox: Record<string, unknown>;
26
28
  labels: Record<string, unknown>;
27
29
  files: FileRegistry;
@@ -178,6 +180,7 @@ async function cmdUpdate(): Promise<void> {
178
180
  const platformAdded = !config.platform;
179
181
  const sandboxAdded = !config.sandbox;
180
182
  const labelsAdded = !config.labels;
183
+ const requiresPullRequestAdded = config.requiresPullRequest === undefined;
181
184
  let configChanged = changed;
182
185
 
183
186
  if (platformAdded) {
@@ -195,6 +198,11 @@ async function cmdUpdate(): Promise<void> {
195
198
  configChanged = true;
196
199
  }
197
200
 
201
+ if (requiresPullRequestAdded) {
202
+ config.requiresPullRequest = defaults.requiresPullRequest;
203
+ configChanged = true;
204
+ }
205
+
198
206
  if (configChanged) {
199
207
  console.log('');
200
208
  if (hasNewEntries) {
@@ -205,7 +213,7 @@ async function cmdUpdate(): Promise<void> {
205
213
  for (const entry of added.merged) {
206
214
  ok(` merged: ${entry}`);
207
215
  }
208
- } else if (platformAdded || sandboxAdded || labelsAdded) {
216
+ } else if (platformAdded || sandboxAdded || labelsAdded || requiresPullRequestAdded) {
209
217
  if (platformAdded) {
210
218
  info(`Default platform config added to ${CONFIG_PATH}.`);
211
219
  }
@@ -215,6 +223,9 @@ async function cmdUpdate(): Promise<void> {
215
223
  if (labelsAdded) {
216
224
  info(`Default labels.in config added to ${CONFIG_PATH}.`);
217
225
  }
226
+ if (requiresPullRequestAdded) {
227
+ info(`Default requiresPullRequest=${defaults.requiresPullRequest} added to ${CONFIG_PATH}.`);
228
+ }
218
229
  } else {
219
230
  info(`File registry changed in ${CONFIG_PATH}.`);
220
231
  }
@@ -227,6 +238,9 @@ async function cmdUpdate(): Promise<void> {
227
238
  if (hasNewEntries && platformAdded) {
228
239
  info(`Default platform config added to ${CONFIG_PATH}.`);
229
240
  }
241
+ if (hasNewEntries && requiresPullRequestAdded) {
242
+ info(`Default requiresPullRequest=${defaults.requiresPullRequest} added to ${CONFIG_PATH}.`);
243
+ }
230
244
  fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', 'utf8');
231
245
  ok(`Updated ${CONFIG_PATH}`);
232
246
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fitlab-ai/agent-infra",
3
- "version": "0.6.5",
3
+ "version": "0.7.0",
4
4
  "description": "Bootstrap tool for AI multi-tool collaboration infrastructure — works with Claude Code, Codex, Gemini CLI, and OpenCode",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -164,7 +164,7 @@ The receiving AI should read this document first to get up to speed.
164
164
 
165
165
  ### 1. One AI Per Phase
166
166
 
167
- Don't have multiple AIs working on the same files simultaneously. Follow the sequential workflow: analyze, design, implement, review, fix, commit.
167
+ Don't have multiple AIs working on the same files simultaneously. Follow the sequential workflow: analysis → analysis-review → design design-review → code → code-review → commit (when any review finds issues, re-run the matching upstream stage).
168
168
 
169
169
  ### 2. Always Create Handoff Documents
170
170
 
@@ -164,7 +164,7 @@ cp .agents/templates/handoff.md .agents/workspace/active/handoff-task-001-phase2
164
164
 
165
165
  ### 1. 每个阶段一个 AI
166
166
 
167
- 不要让多个 AI 同时处理相同的文件。遵循顺序工作流:分析、设计、实现、审查、修复、提交。
167
+ 不要让多个 AI 同时处理相同的文件。遵循顺序工作流:分析 → 分析审查 → 设计 → 设计审查 → 编码 → 代码审查 → 提交(任一审查发现问题时,回到同名上游阶段重跑)。
168
168
 
169
169
  ### 2. 始终创建交接文档
170
170