@fitlab-ai/agent-infra 0.6.4 → 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 (193) hide show
  1. package/README.md +63 -27
  2. package/README.zh-CN.md +61 -25
  3. package/bin/cli.ts +18 -6
  4. package/dist/bin/cli.js +20 -6
  5. package/dist/lib/cp.js +127 -0
  6. package/dist/lib/defaults.json +1 -0
  7. package/dist/lib/init.js +3 -0
  8. package/dist/lib/sandbox/clipboard/bridge.js +23 -4
  9. package/dist/lib/sandbox/clipboard/index.js +12 -3
  10. package/dist/lib/sandbox/commands/create.js +11 -2
  11. package/dist/lib/sandbox/commands/enter.js +29 -6
  12. package/dist/lib/sandbox/commands/list-running.js +108 -0
  13. package/dist/lib/sandbox/commands/ls.js +24 -45
  14. package/dist/lib/sandbox/commands/rebuild.js +15 -7
  15. package/dist/lib/sandbox/config.js +3 -0
  16. package/dist/lib/sandbox/index.js +6 -4
  17. package/dist/lib/sandbox/readme-scaffold.js +148 -0
  18. package/dist/lib/sandbox/runtimes/ai-tools.dockerfile +12 -6
  19. package/dist/lib/sandbox/runtimes/base.dockerfile +3 -3
  20. package/dist/lib/sandbox/tools.js +213 -8
  21. package/dist/lib/update.js +12 -1
  22. package/lib/cp.ts +177 -0
  23. package/lib/defaults.json +1 -0
  24. package/lib/init.ts +10 -0
  25. package/lib/sandbox/clipboard/bridge.ts +23 -4
  26. package/lib/sandbox/clipboard/index.ts +12 -3
  27. package/lib/sandbox/commands/create.ts +18 -2
  28. package/lib/sandbox/commands/enter.ts +48 -6
  29. package/lib/sandbox/commands/list-running.ts +135 -0
  30. package/lib/sandbox/commands/ls.ts +28 -49
  31. package/lib/sandbox/commands/rebuild.ts +24 -7
  32. package/lib/sandbox/config.ts +7 -0
  33. package/lib/sandbox/index.ts +6 -4
  34. package/lib/sandbox/readme-scaffold.ts +177 -0
  35. package/lib/sandbox/runtimes/ai-tools.dockerfile +12 -6
  36. package/lib/sandbox/runtimes/base.dockerfile +3 -3
  37. package/lib/sandbox/tools.ts +248 -9
  38. package/lib/update.ts +15 -1
  39. package/package.json +1 -1
  40. package/templates/.agents/QUICKSTART.en.md +1 -1
  41. package/templates/.agents/QUICKSTART.zh-CN.md +1 -1
  42. package/templates/.agents/README.en.md +79 -2
  43. package/templates/.agents/README.zh-CN.md +79 -2
  44. package/templates/.agents/rules/create-issue.en.md +1 -1
  45. package/templates/.agents/rules/create-issue.github.en.md +1 -1
  46. package/templates/.agents/rules/create-issue.github.zh-CN.md +1 -1
  47. package/templates/.agents/rules/create-issue.zh-CN.md +1 -1
  48. package/templates/.agents/rules/issue-sync.github.en.md +6 -5
  49. package/templates/.agents/rules/issue-sync.github.zh-CN.md +6 -5
  50. package/templates/.agents/rules/milestone-inference.github.en.md +2 -2
  51. package/templates/.agents/rules/milestone-inference.github.zh-CN.md +2 -2
  52. package/templates/.agents/rules/no-mid-flow-questions.en.md +57 -0
  53. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +57 -0
  54. package/templates/.agents/rules/pr-sync.github.en.md +4 -5
  55. package/templates/.agents/rules/pr-sync.github.zh-CN.md +4 -5
  56. package/templates/.agents/rules/task-management.en.md +9 -6
  57. package/templates/.agents/rules/task-management.zh-CN.md +9 -6
  58. package/templates/.agents/rules/testing-discipline.en.md +2 -2
  59. package/templates/.agents/rules/testing-discipline.zh-CN.md +2 -2
  60. package/templates/.agents/scripts/validate-artifact.js +1 -1
  61. package/templates/.agents/skills/analyze-task/SKILL.en.md +16 -4
  62. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +16 -4
  63. package/templates/.agents/skills/check-task/SKILL.en.md +43 -32
  64. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +42 -31
  65. package/templates/.agents/skills/code-task/SKILL.en.md +117 -0
  66. package/templates/.agents/skills/{implement-task → code-task}/SKILL.zh-CN.md +51 -24
  67. package/templates/.agents/skills/{implement-task → code-task}/config/verify.en.json +4 -4
  68. package/templates/.agents/skills/{implement-task → code-task}/config/verify.zh-CN.json +4 -4
  69. package/templates/.agents/skills/{implement-task → code-task}/reference/branch-management.zh-CN.md +2 -2
  70. package/templates/.agents/skills/{implement-task/reference/implementation-rules.en.md → code-task/reference/code-rules.en.md} +6 -6
  71. package/templates/.agents/skills/{implement-task/reference/implementation-rules.zh-CN.md → code-task/reference/code-rules.zh-CN.md} +3 -3
  72. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +69 -0
  73. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +69 -0
  74. package/templates/.agents/skills/{refine-task/reference/fix-workflow.en.md → code-task/reference/fix-mode.en.md} +12 -12
  75. package/templates/.agents/skills/{refine-task/reference/fix-workflow.zh-CN.md → code-task/reference/fix-mode.zh-CN.md} +8 -8
  76. package/templates/.agents/skills/code-task/reference/output-template.en.md +20 -0
  77. package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +20 -0
  78. package/templates/.agents/skills/{implement-task → code-task}/reference/report-template.en.md +4 -4
  79. package/templates/.agents/skills/{implement-task → code-task}/reference/report-template.zh-CN.md +3 -3
  80. package/templates/.agents/skills/code-task/scripts/detect-mode.js +370 -0
  81. package/templates/.agents/skills/commit/SKILL.en.md +2 -2
  82. package/templates/.agents/skills/commit/SKILL.zh-CN.md +2 -2
  83. package/templates/.agents/skills/commit/reference/task-status-update.en.md +10 -6
  84. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +10 -6
  85. package/templates/.agents/skills/complete-task/SKILL.en.md +5 -3
  86. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +5 -3
  87. package/templates/.agents/skills/create-pr/SKILL.en.md +17 -1
  88. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +17 -1
  89. package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
  90. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  91. package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -2
  92. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +2 -2
  93. package/templates/.agents/skills/import-issue/SKILL.en.md +3 -3
  94. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +3 -3
  95. package/templates/.agents/skills/plan-task/SKILL.en.md +4 -4
  96. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -4
  97. package/templates/.agents/skills/restore-task/SKILL.en.md +4 -3
  98. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +4 -3
  99. package/templates/.agents/skills/review-analysis/SKILL.en.md +76 -0
  100. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +102 -0
  101. package/templates/.agents/skills/review-analysis/config/verify.en.json +51 -0
  102. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +51 -0
  103. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +87 -0
  104. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +87 -0
  105. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +90 -0
  106. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +91 -0
  107. package/templates/.agents/skills/review-analysis/reference/review-criteria.en.md +47 -0
  108. package/templates/.agents/skills/review-analysis/reference/review-criteria.zh-CN.md +47 -0
  109. package/templates/.agents/skills/{review-task → review-code}/SKILL.en.md +11 -9
  110. package/templates/.agents/skills/{review-task → review-code}/SKILL.zh-CN.md +15 -9
  111. package/templates/.agents/skills/{review-task → review-code}/config/verify.en.json +7 -5
  112. package/templates/.agents/skills/{review-task → review-code}/config/verify.zh-CN.json +6 -4
  113. package/templates/.agents/skills/{review-task → review-code}/reference/output-templates.en.md +21 -17
  114. package/templates/.agents/skills/{review-task → review-code}/reference/output-templates.zh-CN.md +19 -15
  115. package/templates/.agents/skills/{review-task → review-code}/reference/report-template.en.md +5 -6
  116. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +91 -0
  117. package/templates/.agents/skills/review-code/reference/review-criteria.en.md +48 -0
  118. package/templates/.agents/skills/{review-task → review-code}/reference/review-criteria.zh-CN.md +10 -4
  119. package/templates/.agents/skills/review-plan/SKILL.en.md +76 -0
  120. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +102 -0
  121. package/templates/.agents/skills/{refine-task → review-plan}/config/verify.en.json +14 -10
  122. package/templates/.agents/skills/{refine-task → review-plan}/config/verify.zh-CN.json +14 -10
  123. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +87 -0
  124. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +87 -0
  125. package/templates/.agents/skills/review-plan/reference/report-template.en.md +90 -0
  126. package/templates/.agents/skills/{review-task → review-plan}/reference/report-template.zh-CN.md +3 -3
  127. package/templates/.agents/skills/review-plan/reference/review-criteria.en.md +47 -0
  128. package/templates/.agents/skills/review-plan/reference/review-criteria.zh-CN.md +47 -0
  129. package/templates/.agents/skills/test/SKILL.en.md +2 -2
  130. package/templates/.agents/skills/test/SKILL.zh-CN.md +13 -31
  131. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +1 -0
  132. package/templates/.agents/templates/task.en.md +3 -3
  133. package/templates/.agents/templates/task.zh-CN.md +2 -2
  134. package/templates/.agents/workflows/bug-fix.en.yaml +126 -80
  135. package/templates/.agents/workflows/bug-fix.zh-CN.yaml +90 -44
  136. package/templates/.agents/workflows/feature-development.en.yaml +115 -70
  137. package/templates/.agents/workflows/feature-development.zh-CN.yaml +92 -47
  138. package/templates/.agents/workflows/refactoring.en.yaml +123 -78
  139. package/templates/.agents/workflows/refactoring.zh-CN.yaml +89 -44
  140. package/templates/.claude/commands/code-task.en.md +8 -0
  141. package/templates/.claude/commands/code-task.zh-CN.md +8 -0
  142. package/templates/.claude/commands/review-analysis.en.md +8 -0
  143. package/templates/.claude/commands/review-analysis.zh-CN.md +8 -0
  144. package/templates/.claude/commands/review-code.en.md +8 -0
  145. package/templates/.claude/commands/review-code.zh-CN.md +8 -0
  146. package/templates/.claude/commands/review-plan.en.md +8 -0
  147. package/templates/.claude/commands/review-plan.zh-CN.md +8 -0
  148. package/templates/.gemini/commands/_project_/archive-tasks.zh-CN.toml +1 -1
  149. package/templates/.gemini/commands/_project_/code-task.en.toml +8 -0
  150. package/templates/.gemini/commands/_project_/code-task.zh-CN.toml +8 -0
  151. package/templates/.gemini/commands/_project_/init-labels.zh-CN.toml +1 -1
  152. package/templates/.gemini/commands/_project_/init-milestones.zh-CN.toml +1 -1
  153. package/templates/.gemini/commands/_project_/review-analysis.en.toml +8 -0
  154. package/templates/.gemini/commands/_project_/review-analysis.zh-CN.toml +8 -0
  155. package/templates/.gemini/commands/_project_/review-code.en.toml +8 -0
  156. package/templates/.gemini/commands/_project_/review-code.zh-CN.toml +8 -0
  157. package/templates/.gemini/commands/_project_/review-plan.en.toml +8 -0
  158. package/templates/.gemini/commands/_project_/review-plan.zh-CN.toml +8 -0
  159. package/templates/.opencode/commands/code-task.en.md +11 -0
  160. package/templates/.opencode/commands/code-task.zh-CN.md +11 -0
  161. package/templates/.opencode/commands/review-analysis.en.md +11 -0
  162. package/templates/.opencode/commands/review-analysis.zh-CN.md +11 -0
  163. package/templates/.opencode/commands/review-code.en.md +11 -0
  164. package/templates/.opencode/commands/review-code.zh-CN.md +11 -0
  165. package/templates/.opencode/commands/review-plan.en.md +11 -0
  166. package/templates/.opencode/commands/review-plan.zh-CN.md +11 -0
  167. package/templates/.agents/skills/implement-task/SKILL.en.md +0 -173
  168. package/templates/.agents/skills/implement-task/reference/output-template.en.md +0 -20
  169. package/templates/.agents/skills/implement-task/reference/output-template.zh-CN.md +0 -20
  170. package/templates/.agents/skills/refine-task/SKILL.en.md +0 -153
  171. package/templates/.agents/skills/refine-task/SKILL.zh-CN.md +0 -153
  172. package/templates/.agents/skills/refine-task/reference/report-template.en.md +0 -64
  173. package/templates/.agents/skills/refine-task/reference/report-template.zh-CN.md +0 -64
  174. package/templates/.agents/skills/review-task/reference/review-criteria.en.md +0 -42
  175. package/templates/.claude/commands/implement-task.en.md +0 -8
  176. package/templates/.claude/commands/implement-task.zh-CN.md +0 -8
  177. package/templates/.claude/commands/refine-task.en.md +0 -8
  178. package/templates/.claude/commands/refine-task.zh-CN.md +0 -8
  179. package/templates/.claude/commands/review-task.en.md +0 -8
  180. package/templates/.claude/commands/review-task.zh-CN.md +0 -8
  181. package/templates/.gemini/commands/_project_/implement-task.en.toml +0 -8
  182. package/templates/.gemini/commands/_project_/implement-task.zh-CN.toml +0 -8
  183. package/templates/.gemini/commands/_project_/refine-task.en.toml +0 -8
  184. package/templates/.gemini/commands/_project_/refine-task.zh-CN.toml +0 -8
  185. package/templates/.gemini/commands/_project_/review-task.en.toml +0 -8
  186. package/templates/.gemini/commands/_project_/review-task.zh-CN.toml +0 -8
  187. package/templates/.opencode/commands/implement-task.en.md +0 -11
  188. package/templates/.opencode/commands/implement-task.zh-CN.md +0 -11
  189. package/templates/.opencode/commands/refine-task.en.md +0 -11
  190. package/templates/.opencode/commands/refine-task.zh-CN.md +0 -11
  191. package/templates/.opencode/commands/review-task.en.md +0 -11
  192. package/templates/.opencode/commands/review-task.zh-CN.md +0 -11
  193. /package/templates/.agents/skills/{implement-task → code-task}/reference/branch-management.en.md +0 -0
package/lib/cp.ts ADDED
@@ -0,0 +1,177 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import { platform as currentPlatform, tmpdir as defaultTmpdir } from 'node:os';
5
+ import path from 'node:path';
6
+ import { parseArgs } from 'node:util';
7
+ import { createClipboardAdapter, type ClipboardAdapter } from './sandbox/clipboard/index.ts';
8
+
9
+ const USAGE = 'Usage: ai cp <ssh-alias>\n\nCopy the local clipboard image (PNG) to a remote macOS NSPasteboard over ssh/scp.\n';
10
+ const COMMAND_TIMEOUT_MS = 30_000;
11
+
12
+ export type SpawnResult = {
13
+ status: number | null;
14
+ stdout: string;
15
+ stderr: string;
16
+ error?: Error;
17
+ };
18
+
19
+ type SpawnFn = (cmd: string, args: string[], input?: string) => SpawnResult;
20
+ type CreateAdapterFn = (options?: { platformName?: NodeJS.Platform }) => ClipboardAdapter | null;
21
+ type MkDTempFn = (prefix: string) => string;
22
+ type WriteFileFn = (file: string, data: Buffer) => void;
23
+ type RmFn = (target: string, options: { recursive: boolean; force: boolean }) => void;
24
+
25
+ export type CpDeps = {
26
+ platform?: NodeJS.Platform;
27
+ createAdapter?: CreateAdapterFn;
28
+ spawnFn?: SpawnFn;
29
+ randomId?: () => string;
30
+ mkdtempFn?: MkDTempFn;
31
+ writeFileFn?: WriteFileFn;
32
+ rmFn?: RmFn;
33
+ tmpdir?: () => string;
34
+ writeStdout?: (chunk: string) => unknown;
35
+ writeStderr?: (chunk: string) => unknown;
36
+ };
37
+
38
+ export function runCommand(cmd: string, args: string[], input?: string): SpawnResult {
39
+ const result = spawnSync(cmd, args, {
40
+ input,
41
+ encoding: 'utf8',
42
+ stdio: ['pipe', 'pipe', 'pipe'],
43
+ timeout: COMMAND_TIMEOUT_MS
44
+ });
45
+
46
+ return {
47
+ status: result.status,
48
+ stdout: result.stdout ?? '',
49
+ stderr: result.stderr ?? '',
50
+ error: result.error
51
+ };
52
+ }
53
+
54
+ export async function cmdCp(args: string[], deps: CpDeps = {}): Promise<number> {
55
+ const {
56
+ platform = currentPlatform(),
57
+ createAdapter = createClipboardAdapter,
58
+ spawnFn = runCommand,
59
+ randomId = randomUUID,
60
+ mkdtempFn = fs.mkdtempSync,
61
+ writeFileFn = fs.writeFileSync,
62
+ rmFn = fs.rmSync,
63
+ tmpdir = defaultTmpdir,
64
+ writeStdout = (chunk: string) => process.stdout.write(chunk),
65
+ writeStderr = (chunk: string) => process.stderr.write(chunk)
66
+ } = deps;
67
+
68
+ if (args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
69
+ writeStdout(USAGE);
70
+ return 0;
71
+ }
72
+
73
+ let positionals: string[];
74
+ try {
75
+ ({ positionals } = parseArgs({ args, allowPositionals: true, strict: true }));
76
+ } catch {
77
+ writeStderr(USAGE);
78
+ return 1;
79
+ }
80
+
81
+ const alias = positionals[0];
82
+ if (!alias || positionals.length !== 1) {
83
+ writeStderr(USAGE);
84
+ return 1;
85
+ }
86
+ if (alias.startsWith('-')) {
87
+ writeStderr(`invalid ssh alias '${alias}': must not start with '-'\n`);
88
+ return 1;
89
+ }
90
+
91
+ if (platform !== 'darwin') {
92
+ writeStderr(`ai cp currently supports macOS senders only (got ${platform})\n`);
93
+ return 1;
94
+ }
95
+
96
+ const adapter = createAdapter({ platformName: platform });
97
+ const png = adapter?.readImagePng() ?? null;
98
+ if (png === null) {
99
+ writeStderr('no image on clipboard\n');
100
+ return 1;
101
+ }
102
+
103
+ let uploaded = false;
104
+ let localTmpDir: string | null = null;
105
+ let remotePath: string | null = null;
106
+
107
+ try {
108
+ localTmpDir = mkdtempFn(path.join(tmpdir(), 'agent-infra-cp-'));
109
+ const localPng = path.join(localTmpDir, 'clipboard.png');
110
+ writeFileFn(localPng, png);
111
+
112
+ remotePath = `/tmp/agent-infra-cp-${randomId()}.png`;
113
+ const upload = spawnFn('scp', [
114
+ '-o',
115
+ 'BatchMode=yes',
116
+ '-o',
117
+ 'ConnectTimeout=10',
118
+ localPng,
119
+ `${alias}:${remotePath}`
120
+ ]);
121
+ if (upload.status !== 0) {
122
+ writeStderr(`failed to upload image to ${alias}:\n${commandDetail(upload)}\n`);
123
+ return 1;
124
+ }
125
+ uploaded = true;
126
+
127
+ // Remote write currently targets macOS only: it pipes an AppleScript to the
128
+ // remote `osascript` to set its NSPasteboard. This is the extension point for
129
+ // other remote platforms later (e.g. dispatch on remote OS to wl-copy/xclip
130
+ // on Linux); a non-macOS remote fails here with a clear non-zero error today.
131
+ const setRemote = spawnFn('ssh', [
132
+ '-o',
133
+ 'BatchMode=yes',
134
+ '-o',
135
+ 'ConnectTimeout=10',
136
+ alias,
137
+ 'osascript',
138
+ '-'
139
+ ], remoteSetScript(remotePath));
140
+ if (setRemote.status !== 0) {
141
+ writeStderr(`failed to set remote clipboard on ${alias}:\n${commandDetail(setRemote)}\n`);
142
+ return 1;
143
+ }
144
+
145
+ writeStdout(`copied clipboard image to ${alias}\n`);
146
+ return 0;
147
+ } finally {
148
+ if (uploaded && remotePath) {
149
+ spawnFn('ssh', [
150
+ '-o',
151
+ 'BatchMode=yes',
152
+ '-o',
153
+ 'ConnectTimeout=10',
154
+ alias,
155
+ 'rm',
156
+ '-f',
157
+ remotePath
158
+ ]);
159
+ }
160
+ if (localTmpDir) {
161
+ rmFn(localTmpDir, { recursive: true, force: true });
162
+ }
163
+ }
164
+ }
165
+
166
+ function commandDetail(result: SpawnResult): string {
167
+ const detail = result.stderr || result.error?.message || result.stdout || 'unknown error';
168
+ return detail.trimEnd();
169
+ }
170
+
171
+ function remoteSetScript(remotePath: string): string {
172
+ const escapedPath = remotePath.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
173
+ return [
174
+ `set theFile to POSIX file "${escapedPath}"`,
175
+ 'set the clipboard to (read theFile as «class PNGf»)'
176
+ ].join('\n');
177
+ }
package/lib/defaults.json CHANGED
@@ -2,6 +2,7 @@
2
2
  "platform": {
3
3
  "type": "github"
4
4
  },
5
+ "requiresPullRequest": true,
5
6
  "sandbox": {
6
7
  "engine": null,
7
8
  "runtimes": [
package/lib/init.ts CHANGED
@@ -24,6 +24,7 @@ type Defaults = {
24
24
  files: FileRegistry;
25
25
  sandbox: Record<string, unknown>;
26
26
  labels: Record<string, unknown>;
27
+ requiresPullRequest: boolean;
27
28
  };
28
29
 
29
30
  type AgentConfig = {
@@ -31,6 +32,7 @@ type AgentConfig = {
31
32
  org: string;
32
33
  language: string;
33
34
  platform: { type: string };
35
+ requiresPullRequest: boolean;
34
36
  templateVersion: string;
35
37
  sandbox: Record<string, unknown>;
36
38
  labels: Record<string, unknown>;
@@ -207,6 +209,13 @@ async function cmdInit(): Promise<void> {
207
209
  );
208
210
  }
209
211
 
212
+ const requiresPRChoice = await select(
213
+ 'Require Pull Request flow?',
214
+ ['yes', 'no'],
215
+ 'yes'
216
+ );
217
+ const requiresPullRequest = requiresPRChoice !== 'no';
218
+
210
219
  const templateSources = parseLocalSources(await prompt(
211
220
  'Template sources (optional, comma-separated local paths, e.g. ~/my-templates; Enter to skip)',
212
221
  ''
@@ -280,6 +289,7 @@ async function cmdInit(): Promise<void> {
280
289
  org: orgName,
281
290
  language,
282
291
  platform: { type: platformType },
292
+ requiresPullRequest,
283
293
  templateVersion: VERSION,
284
294
  sandbox: structuredClone(defaults.sandbox),
285
295
  labels: structuredClone(defaults.labels),
@@ -1,4 +1,5 @@
1
1
  import { StringDecoder } from 'node:string_decoder';
2
+ import { spawnSync } from 'node:child_process';
2
3
  import { createClipboardAdapter, type ClipboardAdapter } from './index.ts';
3
4
  import { buildBracketedPaste, CtrlVDetector, type CtrlVMatch } from './keys.ts';
4
5
  import {
@@ -32,6 +33,26 @@ type BridgeOptions = {
32
33
  const FALLBACK_PREFIX = 'Warning: clipboard image paste bridge disabled';
33
34
  const PARTIAL_ESCAPE_FLUSH_MS = 30;
34
35
 
36
+ // Node's stdin.setRawMode(true) uses libuv's RAW mode, which (unlike the
37
+ // cfmakeraw that `docker exec -it` applies on the non-bridge path) keeps ONLCR
38
+ // set on the shared host TTY. With ONLCR on, the kernel rewrites the bare \n
39
+ // that tmux emits after homing the cursor inside the right pane into \r\n,
40
+ // snapping the cursor to column 1 so the following erase/redraw wipes the left
41
+ // pane. Clearing OPOST brings the host TTY in line with the non-bridge path.
42
+ // Best-effort: setRawMode(false) on teardown restores the original termios, and
43
+ // a missing/failed stty only reinstates the redraw glitch.
44
+ function disableOutputPostProcessing(stdin: NodeJS.ReadStream): void {
45
+ const candidate = (stdin as { fd?: unknown }).fd;
46
+ if (typeof candidate !== 'number') {
47
+ return;
48
+ }
49
+ try {
50
+ spawnSync('stty', ['-opost'], { stdio: [candidate, 'ignore', 'ignore'] });
51
+ } catch {
52
+ // stty unavailable or fd is not a tty; leave the terminal as-is.
53
+ }
54
+ }
55
+
35
56
  export async function runInteractiveWithClipboardBridge(options: BridgeOptions): Promise<number> {
36
57
  const {
37
58
  engine,
@@ -56,14 +77,11 @@ export async function runInteractiveWithClipboardBridge(options: BridgeOptions):
56
77
  return runInteractive(engine, 'docker', dockerArgs);
57
78
  }
58
79
 
59
- if (platformName !== 'darwin') {
60
- return runInteractive(engine, 'docker', dockerArgs);
61
- }
62
80
  if (!stdin.isTTY || !stdout.isTTY) {
63
81
  return fallback('host stdin/stdout is not a TTY');
64
82
  }
65
83
  if (!adapter) {
66
- return fallback('macOS clipboard adapter is unavailable');
84
+ return fallback('no clipboard adapter available on this platform');
67
85
  }
68
86
  const available = adapter.available();
69
87
  if (!available.ok) {
@@ -185,6 +203,7 @@ async function runBridge({
185
203
 
186
204
  try {
187
205
  stdin.setRawMode?.(true);
206
+ disableOutputPostProcessing(stdin);
188
207
  stdin.resume();
189
208
  stdin.on('data', onData);
190
209
  stdout.on('resize', onResize);
@@ -6,8 +6,17 @@ export type ClipboardAdapter = DarwinClipboardAdapter;
6
6
  export function createClipboardAdapter({
7
7
  platformName = platform()
8
8
  }: { platformName?: NodeJS.Platform } = {}): ClipboardAdapter | null {
9
- if (platformName !== 'darwin') {
10
- return null;
9
+ switch (platformName) {
10
+ case 'darwin':
11
+ return createDarwinClipboardAdapter();
12
+ case 'linux':
13
+ // Future work: dispatch based on $WAYLAND_DISPLAY (wl-paste) or $DISPLAY (xclip);
14
+ // see Issue #386 follow-up. Returning null disables the bridge for now.
15
+ return null;
16
+ case 'win32':
17
+ // Future work: native Win32 clipboard reader. Returning null disables the bridge.
18
+ return null;
19
+ default:
20
+ return null;
11
21
  }
12
- return createDarwinClipboardAdapter();
13
22
  }
@@ -36,13 +36,20 @@ import {
36
36
  runVerboseEngine
37
37
  } from '../shell.ts';
38
38
  import { resolveTaskBranch } from '../task-resolver.ts';
39
- import { resolveTools, toolConfigDirCandidates, toolNpmPackagesArg } from '../tools.ts';
39
+ import {
40
+ imageSignatureFields,
41
+ resolveTools,
42
+ toolConfigDirCandidates,
43
+ toolNpmPackagesArg,
44
+ toolShellInstallScriptBase64
45
+ } from '../tools.ts';
40
46
  import type { SandboxTool } from '../tools.ts';
41
47
  import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.ts';
42
48
  import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from '../clipboard/paths.ts';
43
49
  import { validateSelinuxDisableEnv } from '../engines/selinux.ts';
44
50
  import { resolveBuildUid } from '../engines/native.ts';
45
51
  import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.ts';
52
+ import { ensureSandboxDiscoveryReadmes } from '../readme-scaffold.ts';
46
53
  import {
47
54
  prepareClaudeCredentials,
48
55
  redactCommandError,
@@ -112,7 +119,7 @@ function buildSignature(preparedDockerfile: PreparedDockerfile, tools: SandboxTo
112
119
  return createHash('sha256')
113
120
  .update(JSON.stringify({
114
121
  dockerfile: preparedDockerfile.signature,
115
- tools: tools.map((tool) => tool.npmPackage)
122
+ tools: imageSignatureFields(tools)
116
123
  }))
117
124
  .digest('hex')
118
125
  .slice(0, 12);
@@ -1062,6 +1069,8 @@ export function buildImage(
1062
1069
  `HOST_GID=${hostGid}`,
1063
1070
  '--build-arg',
1064
1071
  `AI_TOOL_PACKAGES=${toolNpmPackagesArg(tools)}`,
1072
+ '--build-arg',
1073
+ `AI_TOOLS_SHELL_INSTALL_B64=${toolShellInstallScriptBase64(tools)}`,
1065
1074
  '--label',
1066
1075
  sandboxLabel(config),
1067
1076
  '--label',
@@ -1287,6 +1296,13 @@ export async function create(args: string[]): Promise<void> {
1287
1296
  message(`Created default sandbox aliases at ${aliasesFile.path}`);
1288
1297
  }
1289
1298
 
1299
+ const readmeResults = ensureSandboxDiscoveryReadmes(effectiveConfig, branch);
1300
+ for (const { created, path: readmePath } of readmeResults) {
1301
+ if (created) {
1302
+ message(`Created discovery README at ${readmePath}`);
1303
+ }
1304
+ }
1305
+
1290
1306
  const gitconfigPath = path.join(effectiveConfig.home, '.gitconfig');
1291
1307
  const gitconfigContent = fs.existsSync(gitconfigPath)
1292
1308
  ? fs.readFileSync(gitconfigPath, 'utf8')
@@ -1,5 +1,5 @@
1
1
  import { loadConfig } from '../config.ts';
2
- import { assertValidBranchName, containerNameCandidates } from '../constants.ts';
2
+ import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel } from '../constants.ts';
3
3
  import { detectEngine } from '../engine.ts';
4
4
  import {
5
5
  formatCredentialWarnings,
@@ -13,8 +13,12 @@ import { resolveTaskBranch } from '../task-resolver.ts';
13
13
  import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.ts';
14
14
  import { runInteractiveWithClipboardBridge } from '../clipboard/bridge.ts';
15
15
  import { detectHostTimezone } from '../host-timezone.ts';
16
+ import { fetchSandboxRows, isTaskShortRef, resolveTaskShortRef } from './list-running.ts';
16
17
 
17
- const USAGE = `Usage: ai sandbox exec <branch> [cmd...]`;
18
+ const USAGE = `Usage: ai sandbox exec <branch | TASK-id | '#N'> [cmd...]
19
+
20
+ '#N' references the N-th running sandbox in 'ai sandbox ls' order (1-based).
21
+ Quote it as '#N' to avoid shell '#' comment handling.`;
18
22
  const TMUX_ENTRY_PATH = '/usr/local/bin/sandbox-tmux-entry';
19
23
 
20
24
  // Terminal-detection variables that interactive TUIs (e.g. claude-code)
@@ -45,6 +49,37 @@ export function hostTimezoneEnvFlags(detect = detectHostTimezone): string[] {
45
49
  return tz ? ['-e', `TZ=${tz}`] : [];
46
50
  }
47
51
 
52
+ export function clipboardBridgeDisabled(env: NodeJS.ProcessEnv = process.env): boolean {
53
+ const value = (env.AI_SANDBOX_NO_CLIPBOARD_BRIDGE ?? '').trim().toLowerCase();
54
+ return value === '1' || value === 'true' || value === 'yes';
55
+ }
56
+
57
+ export function runSandboxInteractive(params: {
58
+ engine: string;
59
+ dockerArgs: string[];
60
+ container: string;
61
+ home: string;
62
+ env?: NodeJS.ProcessEnv;
63
+ runBridge?: typeof runInteractiveWithClipboardBridge;
64
+ runInteractive?: typeof runInteractiveEngine;
65
+ }): number | Promise<number> {
66
+ const {
67
+ engine,
68
+ dockerArgs,
69
+ container,
70
+ home,
71
+ env = process.env,
72
+ runBridge = runInteractiveWithClipboardBridge,
73
+ runInteractive = runInteractiveEngine
74
+ } = params;
75
+
76
+ if (clipboardBridgeDisabled(env)) {
77
+ return runInteractive(engine, 'docker', dockerArgs);
78
+ }
79
+
80
+ return runBridge({ engine, dockerArgs, container, home });
81
+ }
82
+
48
83
  export function formatCredentialSyncStatus(
49
84
  result: ReturnType<typeof reconcileClaudeCredentials>,
50
85
  isTTY = process.stderr.isTTY
@@ -84,8 +119,14 @@ export async function enter(args: string[]): Promise<number> {
84
119
  const config = loadConfig();
85
120
  validateClaudeCredentialsEnvOverride();
86
121
  const engine = detectEngine(config);
87
- const [branchOrTaskId = '', ...cmd] = args;
88
- const branch = resolveTaskBranch(branchOrTaskId, config.repoRoot);
122
+ const [firstArg = '', ...cmd] = args;
123
+ let branch: string;
124
+ if (isTaskShortRef(firstArg)) {
125
+ const { running } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
126
+ branch = resolveTaskShortRef(firstArg, { running });
127
+ } else {
128
+ branch = resolveTaskBranch(firstArg, config.repoRoot);
129
+ }
89
130
  assertValidBranchName(branch);
90
131
  const running = runSafeEngine(engine, 'docker', ['ps', '--format', '{{.Names}}']).split('\n');
91
132
  const container = containerNameCandidates(config, branch).find((name) => running.includes(name));
@@ -115,9 +156,10 @@ export async function enter(args: string[]): Promise<number> {
115
156
  process.stderr.write(`Warning: dotfiles snapshot rebuild failed: ${redactCommandError(error instanceof Error ? error.message : 'unknown error')}\n`);
116
157
  }
117
158
 
118
- return runInteractiveWithClipboardBridge({
159
+ const dockerArgs = ['exec', '-it', ...envFlags, container, 'bash', TMUX_ENTRY_PATH];
160
+ return runSandboxInteractive({
119
161
  engine,
120
- dockerArgs: ['exec', '-it', ...envFlags, container, 'bash', TMUX_ENTRY_PATH],
162
+ dockerArgs,
121
163
  container,
122
164
  home: config.home
123
165
  });
@@ -0,0 +1,135 @@
1
+ import { runSafeEngine } from '../shell.ts';
2
+
3
+ export type SandboxRow = {
4
+ name: string;
5
+ status: string;
6
+ branch: string;
7
+ running: boolean;
8
+ index: number | null;
9
+ };
10
+
11
+ export function containerListFormat(): string {
12
+ return '{{.Names}}\t{{.Status}}\t{{.Labels}}';
13
+ }
14
+
15
+ export function parseLabels(csv: string): Record<string, string> {
16
+ if (!csv) {
17
+ return {};
18
+ }
19
+
20
+ const labels: Record<string, string> = {};
21
+ for (const pair of csv.split(',')) {
22
+ if (!pair) {
23
+ continue;
24
+ }
25
+ const eq = pair.indexOf('=');
26
+ if (eq < 0) {
27
+ continue;
28
+ }
29
+ labels[pair.slice(0, eq)] = pair.slice(eq + 1);
30
+ }
31
+ return labels;
32
+ }
33
+
34
+ export function parseSandboxRows(rawOutput: string, branchKey: string): SandboxRow[] {
35
+ if (!rawOutput) {
36
+ return [];
37
+ }
38
+ return rawOutput.split('\n').map((line) => {
39
+ const [name = '', status = '', labelsCsv = ''] = line.split('\t');
40
+ const branch = parseLabels(labelsCsv)[branchKey] ?? '';
41
+ return {
42
+ name,
43
+ status,
44
+ branch,
45
+ running: status.startsWith('Up '),
46
+ index: null
47
+ };
48
+ });
49
+ }
50
+
51
+ export function sortAndIndexSandboxRows(rows: SandboxRow[]): {
52
+ running: SandboxRow[];
53
+ nonRunning: SandboxRow[];
54
+ } {
55
+ const byName = (a: SandboxRow, b: SandboxRow): number => {
56
+ if (a.name < b.name) return -1;
57
+ if (a.name > b.name) return 1;
58
+ return 0;
59
+ };
60
+ const running = rows.filter((row) => row.running).sort(byName).map((row, i) => ({
61
+ ...row,
62
+ index: i + 1
63
+ }));
64
+ const nonRunning = rows.filter((row) => !row.running).sort(byName).map((row) => ({
65
+ ...row,
66
+ index: null
67
+ }));
68
+ return { running, nonRunning };
69
+ }
70
+
71
+ export function fetchSandboxRows(
72
+ engine: string,
73
+ label: string,
74
+ branchKey: string
75
+ ): { running: SandboxRow[]; nonRunning: SandboxRow[] } {
76
+ const raw = runSafeEngine(engine, 'docker', [
77
+ 'ps',
78
+ '-a',
79
+ '--filter',
80
+ `label=${label}`,
81
+ '--format',
82
+ containerListFormat()
83
+ ]);
84
+ return sortAndIndexSandboxRows(parseSandboxRows(raw, branchKey));
85
+ }
86
+
87
+ /**
88
+ * Returns true iff `arg` is a syntactically valid task short reference ('#N').
89
+ * Zero IO. Callers MUST use this as the gate before constructing any context
90
+ * for resolveTaskShortRef — that way non-matching arguments (e.g. '#abc',
91
+ * '#1.5', '#') never trigger sandbox list IO.
92
+ */
93
+ export function isTaskShortRef(arg: string): boolean {
94
+ return /^#\d+$/.test(arg);
95
+ }
96
+
97
+ /**
98
+ * Resolve a task short reference ('#N') to a branch name.
99
+ *
100
+ * Current implementation: treats the digits as a 1-based index into the
101
+ * supplied running-sandbox list (ls view order). This is the *only*
102
+ * resolution path until the global task-short-id registry lands in a
103
+ * follow-up task; do NOT read task.md or scan .agents/workspace/ from this
104
+ * helper here.
105
+ *
106
+ * Precondition: callers MUST gate on isTaskShortRef(arg) === true before
107
+ * constructing ctx and calling this function. Throws when arg is a valid
108
+ * short ref but cannot be resolved (out of range, no running sandboxes,
109
+ * etc.); the caller surfaces the error to the user.
110
+ */
111
+ export function resolveTaskShortRef(
112
+ arg: string,
113
+ ctx: { running: SandboxRow[] }
114
+ ): string {
115
+ const n = Number(arg.slice(1));
116
+ if (n < 1) {
117
+ throw new Error(`Invalid sandbox index '${arg}': must be >= 1`);
118
+ }
119
+ const { running } = ctx;
120
+ if (running.length === 0) {
121
+ throw new Error(`No running sandbox to reference with '${arg}'`);
122
+ }
123
+ if (n > running.length) {
124
+ throw new Error(
125
+ `No running sandbox at index '${arg}' (only ${running.length} running)`
126
+ );
127
+ }
128
+ const row = running[n - 1]!;
129
+ if (!row.branch) {
130
+ throw new Error(
131
+ `Cannot resolve branch for sandbox '${arg}' (container '${row.name}' missing branch label)`
132
+ );
133
+ }
134
+ return row.branch;
135
+ }
@@ -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
  }