@fitlab-ai/agent-infra 0.8.2 → 0.8.4

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 (198) hide show
  1. package/README.zh-CN.md +10 -0
  2. package/bin/cli.ts +1 -1
  3. package/dist/bin/cli.js +2 -2
  4. package/dist/lib/cp.js +57 -16
  5. package/dist/lib/decide.js +2 -2
  6. package/dist/lib/defaults.json +2 -0
  7. package/dist/lib/init.js +7 -7
  8. package/dist/lib/merge.js +1 -1
  9. package/dist/lib/prompt.js +1 -1
  10. package/dist/lib/run/index.js +7 -7
  11. package/dist/lib/run/prompt.js +1 -1
  12. package/dist/lib/sandbox/capture.js +10 -8
  13. package/dist/lib/sandbox/clipboard/bridge.js +59 -21
  14. package/dist/lib/sandbox/clipboard/inbox.js +82 -0
  15. package/dist/lib/sandbox/clipboard/index.js +5 -6
  16. package/dist/lib/sandbox/clipboard/linux.js +91 -0
  17. package/dist/lib/sandbox/clipboard/paths.js +1 -1
  18. package/dist/lib/sandbox/clipboard/win32.js +144 -0
  19. package/dist/lib/sandbox/commands/create.js +106 -88
  20. package/dist/lib/sandbox/commands/enter.js +9 -9
  21. package/dist/lib/sandbox/commands/list-running.js +2 -2
  22. package/dist/lib/sandbox/commands/ls.js +7 -7
  23. package/dist/lib/sandbox/commands/prune.js +8 -8
  24. package/dist/lib/sandbox/commands/rebuild.js +38 -50
  25. package/dist/lib/sandbox/commands/refresh.js +2 -2
  26. package/dist/lib/sandbox/commands/rm.js +11 -11
  27. package/dist/lib/sandbox/commands/show.js +4 -4
  28. package/dist/lib/sandbox/commands/start.js +4 -4
  29. package/dist/lib/sandbox/commands/vm.js +4 -4
  30. package/dist/lib/sandbox/config.js +10 -4
  31. package/dist/lib/sandbox/constants.js +4 -1
  32. package/dist/lib/sandbox/credentials.js +1 -1
  33. package/dist/lib/sandbox/dotfiles.js +1 -1
  34. package/dist/lib/sandbox/engine.js +3 -3
  35. package/dist/lib/sandbox/engines/index.js +5 -5
  36. package/dist/lib/sandbox/engines/wsl2-paths.js +1 -1
  37. package/dist/lib/sandbox/image-build.js +80 -0
  38. package/dist/lib/sandbox/image-prune.js +2 -2
  39. package/dist/lib/sandbox/index.js +10 -10
  40. package/dist/lib/sandbox/managed-fs.js +1 -1
  41. package/dist/lib/sandbox/readme-scaffold.js +1 -1
  42. package/dist/lib/sandbox/runtimes/base.dockerfile +3 -0
  43. package/dist/lib/sandbox/shell.js +9 -1
  44. package/dist/lib/sandbox/tools.js +4 -4
  45. package/dist/lib/server/adapters/feishu/index.js +10 -3
  46. package/dist/lib/server/adapters/feishu/renderer.js +88 -0
  47. package/dist/lib/server/adapters/feishu/transport.js +2 -13
  48. package/dist/lib/server/daemon.js +64 -43
  49. package/dist/lib/server/display.js +83 -0
  50. package/dist/lib/server/index.js +2 -2
  51. package/dist/lib/server/process-control.js +2 -2
  52. package/dist/lib/server/protocol.js +2 -2
  53. package/dist/lib/server/streamer.js +5 -4
  54. package/dist/lib/task/commands/cat.js +2 -2
  55. package/dist/lib/task/commands/decisions.js +4 -4
  56. package/dist/lib/task/commands/files.js +3 -3
  57. package/dist/lib/task/commands/grep.js +3 -3
  58. package/dist/lib/task/commands/issue-body.js +4 -4
  59. package/dist/lib/task/commands/log.js +5 -5
  60. package/dist/lib/task/commands/ls.js +3 -3
  61. package/dist/lib/task/commands/show.js +1 -1
  62. package/dist/lib/task/commands/status.js +71 -25
  63. package/dist/lib/task/index.js +9 -9
  64. package/dist/lib/task/resolve-ref.js +1 -1
  65. package/dist/lib/task/workflow-warnings.js +94 -0
  66. package/dist/lib/update.js +4 -4
  67. package/lib/cp.ts +56 -15
  68. package/lib/defaults.json +2 -0
  69. package/lib/sandbox/capture.ts +5 -3
  70. package/lib/sandbox/clipboard/bridge.ts +62 -18
  71. package/lib/sandbox/clipboard/inbox.ts +83 -0
  72. package/lib/sandbox/clipboard/index.ts +5 -6
  73. package/lib/sandbox/clipboard/linux.ts +124 -0
  74. package/lib/sandbox/clipboard/win32.ts +173 -0
  75. package/lib/sandbox/commands/create.ts +132 -88
  76. package/lib/sandbox/commands/rebuild.ts +42 -54
  77. package/lib/sandbox/config.ts +13 -1
  78. package/lib/sandbox/constants.ts +4 -0
  79. package/lib/sandbox/image-build.ts +134 -0
  80. package/lib/sandbox/runtimes/base.dockerfile +3 -0
  81. package/lib/sandbox/shell.ts +11 -2
  82. package/lib/sandbox/tools.ts +5 -5
  83. package/lib/server/adapters/_contract.ts +3 -0
  84. package/lib/server/adapters/feishu/index.ts +11 -3
  85. package/lib/server/adapters/feishu/renderer.ts +99 -0
  86. package/lib/server/adapters/feishu/transport.ts +5 -18
  87. package/lib/server/daemon.ts +74 -38
  88. package/lib/server/display.ts +136 -0
  89. package/lib/server/streamer.ts +5 -4
  90. package/lib/task/commands/log.ts +3 -3
  91. package/lib/task/commands/status.ts +102 -21
  92. package/lib/task/workflow-warnings.ts +121 -0
  93. package/package.json +2 -2
  94. package/templates/.agents/README.en.md +3 -0
  95. package/templates/.agents/README.zh-CN.md +3 -0
  96. package/templates/.agents/rules/create-issue.github.en.md +4 -4
  97. package/templates/.agents/rules/create-issue.github.zh-CN.md +4 -4
  98. package/templates/.agents/rules/issue-pr-commands.github.en.md +7 -2
  99. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +7 -2
  100. package/templates/.agents/rules/issue-sync.github.en.md +13 -0
  101. package/templates/.agents/rules/issue-sync.github.zh-CN.md +13 -0
  102. package/templates/.agents/rules/next-step-output.en.md +15 -1
  103. package/templates/.agents/rules/next-step-output.zh-CN.md +15 -1
  104. package/templates/.agents/rules/pr-sync.github.en.md +17 -2
  105. package/templates/.agents/rules/pr-sync.github.zh-CN.md +17 -2
  106. package/templates/.agents/rules/review-handshake.en.md +2 -2
  107. package/templates/.agents/rules/review-handshake.zh-CN.md +2 -2
  108. package/templates/.agents/rules/task-management.en.md +1 -1
  109. package/templates/.agents/rules/task-management.zh-CN.md +1 -1
  110. package/templates/.agents/rules/testing-discipline.en.md +3 -41
  111. package/templates/.agents/rules/testing-discipline.zh-CN.md +3 -41
  112. package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +57 -0
  113. package/templates/.agents/scripts/validate-artifact.js +213 -0
  114. package/templates/.agents/scripts/workflow-warnings.js +290 -0
  115. package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
  116. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +2 -2
  117. package/templates/.agents/skills/archive-tasks/scripts/archive-tasks.sh +1 -1
  118. package/templates/.agents/skills/block-task/SKILL.en.md +1 -1
  119. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
  120. package/templates/.agents/skills/cancel-task/SKILL.en.md +1 -1
  121. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +2 -2
  122. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +9 -9
  123. package/templates/.agents/skills/close-codescan/SKILL.en.md +1 -1
  124. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +2 -2
  125. package/templates/.agents/skills/close-dependabot/SKILL.en.md +1 -1
  126. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +2 -2
  127. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  128. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
  129. package/templates/.agents/skills/code-task/reference/branch-management.zh-CN.md +2 -2
  130. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +2 -2
  131. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +2 -2
  132. package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +1 -1
  133. package/templates/.agents/skills/commit/SKILL.en.md +1 -1
  134. package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
  135. package/templates/.agents/skills/commit/reference/task-status-update.en.md +6 -6
  136. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +6 -6
  137. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +142 -0
  138. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +142 -0
  139. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +49 -0
  140. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +49 -0
  141. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +48 -0
  142. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +48 -0
  143. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +60 -0
  144. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +87 -0
  145. package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
  146. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
  147. package/templates/.agents/skills/create-pr/SKILL.en.md +6 -4
  148. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -4
  149. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
  150. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
  151. package/templates/.agents/skills/create-task/SKILL.en.md +6 -3
  152. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +9 -6
  153. package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -2
  154. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +3 -3
  155. package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -2
  156. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +3 -3
  157. package/templates/.agents/skills/import-issue/SKILL.en.md +2 -2
  158. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +3 -3
  159. package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +1 -1
  160. package/templates/.agents/skills/init-milestones/SKILL.en.md +3 -2
  161. package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +4 -3
  162. package/templates/.agents/skills/init-milestones/scripts/init-milestones.github.sh +165 -41
  163. package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
  164. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -2
  165. package/templates/.agents/skills/restore-task/SKILL.en.md +1 -1
  166. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +1 -1
  167. package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
  168. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
  169. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +5 -5
  170. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +5 -5
  171. package/templates/.agents/skills/review-code/SKILL.en.md +8 -4
  172. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +6 -3
  173. package/templates/.agents/skills/review-code/config/verify.en.json +1 -0
  174. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -0
  175. package/templates/.agents/skills/review-code/reference/output-templates.en.md +5 -5
  176. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +5 -5
  177. package/templates/.agents/skills/review-code/reference/report-template.en.md +2 -2
  178. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +2 -2
  179. package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
  180. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
  181. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +5 -5
  182. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +5 -5
  183. package/templates/.agents/skills/test/SKILL.zh-CN.md +26 -8
  184. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +1 -1
  185. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +36 -6
  186. package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
  187. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
  188. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +8 -1
  189. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +8 -1
  190. package/templates/.agents/templates/task.en.md +7 -0
  191. package/templates/.agents/templates/task.zh-CN.md +7 -0
  192. package/templates/.claude/commands/complete-manual-validation.en.md +9 -0
  193. package/templates/.claude/commands/complete-manual-validation.zh-CN.md +9 -0
  194. package/templates/.gemini/commands/_project_/complete-manual-validation.en.toml +8 -0
  195. package/templates/.gemini/commands/_project_/complete-manual-validation.zh-CN.toml +8 -0
  196. package/templates/.github/workflows/metadata-sync.yml +5 -0
  197. package/templates/.opencode/commands/complete-manual-validation.en.md +11 -0
  198. package/templates/.opencode/commands/complete-manual-validation.zh-CN.md +11 -0
@@ -41,48 +41,48 @@ export async function runTask(args) {
41
41
  }
42
42
  switch (subcommand) {
43
43
  case 'cat': {
44
- const { cat } = await import("./commands/cat.js");
44
+ const { cat } = await import('./commands/cat.js');
45
45
  cat(rest);
46
46
  break;
47
47
  }
48
48
  case 'decisions':
49
49
  case 'd': {
50
- const { decisions } = await import("./commands/decisions.js");
50
+ const { decisions } = await import('./commands/decisions.js');
51
51
  decisions(rest);
52
52
  break;
53
53
  }
54
54
  case 'files': {
55
- const { files } = await import("./commands/files.js");
55
+ const { files } = await import('./commands/files.js');
56
56
  files(rest);
57
57
  break;
58
58
  }
59
59
  case 'grep': {
60
- const { grep } = await import("./commands/grep.js");
60
+ const { grep } = await import('./commands/grep.js');
61
61
  grep(rest);
62
62
  break;
63
63
  }
64
64
  case 'issue-body': {
65
- const { issueBody } = await import("./commands/issue-body.js");
65
+ const { issueBody } = await import('./commands/issue-body.js');
66
66
  issueBody(rest);
67
67
  break;
68
68
  }
69
69
  case 'log': {
70
- const { log } = await import("./commands/log.js");
70
+ const { log } = await import('./commands/log.js');
71
71
  log(rest);
72
72
  break;
73
73
  }
74
74
  case 'ls': {
75
- const { ls } = await import("./commands/ls.js");
75
+ const { ls } = await import('./commands/ls.js');
76
76
  ls(rest);
77
77
  break;
78
78
  }
79
79
  case 'show': {
80
- const { show } = await import("./commands/show.js");
80
+ const { show } = await import('./commands/show.js');
81
81
  show(rest);
82
82
  break;
83
83
  }
84
84
  case 'status': {
85
- const { status } = await import("./commands/status.js");
85
+ const { status } = await import('./commands/status.js');
86
86
  status(rest);
87
87
  break;
88
88
  }
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { execFileSync, spawnSync } from 'node:child_process';
4
- import { normalizeShortIdInput } from "./short-id.js";
4
+ import { normalizeShortIdInput } from './short-id.js';
5
5
  const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
6
6
  // Flat-structured workspace dirs that hold tasks under `{dir}/{taskId}/task.md`.
7
7
  // Note: `archive` uses a three-level YYYY/MM/DD layout and is handled separately.
@@ -0,0 +1,94 @@
1
+ import { extractSection } from './sections.js';
2
+ const WORKFLOW_WARNING_HEADINGS = ['工作流告警', 'Workflow Warnings'];
3
+ const WORKFLOW_WARNING_STATUSES = new Set(['open', 'resolved', 'ignored']);
4
+ const WORKFLOW_WARNING_SEVERITIES = new Set(['IMPORTANT', 'ACTION_REQUIRED']);
5
+ function splitTableRow(line) {
6
+ let value = line.trim();
7
+ if (!value.startsWith('|'))
8
+ return [];
9
+ value = value.replace(/^\|/, '').replace(/\|$/, '');
10
+ const cells = [];
11
+ let cell = '';
12
+ for (let index = 0; index < value.length; index += 1) {
13
+ const char = value[index];
14
+ if (char === '|' && !isEscapedAt(value, index)) {
15
+ cells.push(unescapeTableCell(cell.trim()));
16
+ cell = '';
17
+ continue;
18
+ }
19
+ cell += char;
20
+ }
21
+ cells.push(unescapeTableCell(cell.trim()));
22
+ return cells;
23
+ }
24
+ function unescapeTableCell(value) {
25
+ let output = '';
26
+ for (let index = 0; index < value.length; index += 1) {
27
+ const char = value[index];
28
+ const next = value[index + 1];
29
+ if (char === '\\' && (next === '\\' || next === '|')) {
30
+ output += next;
31
+ index += 1;
32
+ continue;
33
+ }
34
+ output += char;
35
+ }
36
+ return output;
37
+ }
38
+ function isEscapedAt(value, index) {
39
+ let backslashes = 0;
40
+ for (let cursor = index - 1; cursor >= 0 && value[cursor] === '\\'; cursor -= 1) {
41
+ backslashes += 1;
42
+ }
43
+ return backslashes % 2 === 1;
44
+ }
45
+ function isSeparatorRow(cells) {
46
+ return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/.test(cell));
47
+ }
48
+ /**
49
+ * Parses well-formed workflow warning rows. Malformed rows are intentionally
50
+ * ignored here; validate-artifact is responsible for reporting structure errors.
51
+ */
52
+ function parseWorkflowWarnings(content) {
53
+ const section = extractSection(content, WORKFLOW_WARNING_HEADINGS);
54
+ if (!section)
55
+ return [];
56
+ const rows = [];
57
+ for (const rawLine of section.split(/\r?\n/)) {
58
+ const cells = splitTableRow(rawLine);
59
+ if (cells.length === 0)
60
+ continue;
61
+ if ((cells[0] ?? '').toLowerCase() === 'id')
62
+ continue;
63
+ if (isSeparatorRow(cells))
64
+ continue;
65
+ if (cells.length < 11)
66
+ continue;
67
+ rows.push({
68
+ id: cells[0] ?? '',
69
+ time: cells[1] ?? '',
70
+ step: cells[2] ?? '',
71
+ severity: cells[3] ?? '',
72
+ code: cells[4] ?? '',
73
+ status: cells[5] ?? '',
74
+ target: cells[6] ?? '',
75
+ message: cells[7] ?? '',
76
+ action: cells[8] ?? '',
77
+ resolvedAt: cells[9] ?? '',
78
+ resolution: cells[10] ?? ''
79
+ });
80
+ }
81
+ return rows;
82
+ }
83
+ function getOpenWorkflowWarnings(content) {
84
+ return parseWorkflowWarnings(content).filter((warning) => warning.status === 'open');
85
+ }
86
+ function formatWorkflowWarningSummary(warnings) {
87
+ return warnings.map((warning) => {
88
+ const target = warning.target ? ` ${warning.target}` : '';
89
+ const action = warning.action ? ` - ${warning.action}` : '';
90
+ return `${warning.id} [${warning.severity}] ${warning.code}${target}${action}`;
91
+ });
92
+ }
93
+ export { WORKFLOW_WARNING_HEADINGS, WORKFLOW_WARNING_STATUSES, WORKFLOW_WARNING_SEVERITIES, parseWorkflowWarnings, getOpenWorkflowWarnings, formatWorkflowWarningSummary };
94
+ //# sourceMappingURL=workflow-warnings.js.map
@@ -1,9 +1,9 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { info, ok, err } from "./log.js";
4
- import { resolveTemplateDir } from "./paths.js";
5
- import { renderFile, copySkillDir, KNOWN_PLATFORMS } from "./render.js";
6
- import { isPathOwnedByDisabledTUI, resolveEnabledTUIs } from "./builtin-tuis.js";
3
+ import { info, ok, err } from './log.js';
4
+ import { resolveTemplateDir } from './paths.js';
5
+ import { renderFile, copySkillDir, KNOWN_PLATFORMS } from './render.js';
6
+ import { isPathOwnedByDisabledTUI, resolveEnabledTUIs } from './builtin-tuis.js';
7
7
  const defaults = JSON.parse(fs.readFileSync(new URL('./defaults.json', import.meta.url), 'utf8'));
8
8
  const CONFIG_DIR = '.agents';
9
9
  const CONFIG_PATH = path.join(CONFIG_DIR, '.airc.json');
package/lib/cp.ts CHANGED
@@ -5,8 +5,9 @@ import { platform as currentPlatform, tmpdir as defaultTmpdir } from 'node:os';
5
5
  import path from 'node:path';
6
6
  import { parseArgs } from 'node:util';
7
7
  import { createClipboardAdapter, type ClipboardAdapter } from './sandbox/clipboard/index.ts';
8
+ import { RECEIVER_CAPABILITY, receiveRemoteClipboardPng } from './sandbox/clipboard/inbox.ts';
8
9
 
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 USAGE = 'Usage: ai cp <ssh-alias>\n\nCopy the local clipboard image (PNG) to a remote macOS clipboard or Linux sandbox over SSH.\n';
10
11
  const COMMAND_TIMEOUT_MS = 30_000;
11
12
 
12
13
  export type SpawnResult = {
@@ -65,6 +66,21 @@ export async function cmdCp(args: string[], deps: CpDeps = {}): Promise<number>
65
66
  writeStderr = (chunk: string) => process.stderr.write(chunk)
66
67
  } = deps;
67
68
 
69
+ if (args.length === 1 && args[0] === '--remote-receive-capability') {
70
+ if (platform === 'linux') writeStdout(`${RECEIVER_CAPABILITY}\n`);
71
+ return platform === 'linux' ? 0 : 1;
72
+ }
73
+ if (args.length === 2 && args[0] === '--remote-receive-v1') {
74
+ try {
75
+ const result = receiveRemoteClipboardPng(process.env.HOME || process.env.USERPROFILE || '', args[1]!, platform);
76
+ writeStdout(`${result}\n`);
77
+ return 0;
78
+ } catch (error) {
79
+ writeStderr(`${error instanceof Error ? error.message : 'clipboard receive failed'}\n`);
80
+ return 1;
81
+ }
82
+ }
83
+
68
84
  if (args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
69
85
  writeStdout(USAGE);
70
86
  return 0;
@@ -110,6 +126,35 @@ export async function cmdCp(args: string[], deps: CpDeps = {}): Promise<number>
110
126
  writeFileFn(localPng, png);
111
127
 
112
128
  remotePath = `/tmp/agent-infra-cp-${randomId()}.png`;
129
+ const remoteSystem = spawnFn('ssh', [
130
+ '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', alias, 'uname', '-s'
131
+ ]);
132
+ if (remoteSystem.status !== 0) {
133
+ writeStderr(`failed to detect remote platform on ${alias}:\n${commandDetail(remoteSystem)}\n`);
134
+ return 1;
135
+ }
136
+ const remotePlatform = remoteSystem.stdout.trim();
137
+ if (remotePlatform !== 'Darwin' && remotePlatform !== 'Linux') {
138
+ writeStderr(`unsupported remote platform on ${alias}: ${remotePlatform || 'unknown'}\n`);
139
+ return 1;
140
+ }
141
+ if (remotePlatform === 'Linux') {
142
+ const homeCheck = spawnFn('ssh', [
143
+ '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', alias, 'test', '-d', '$HOME/.agent-infra'
144
+ ]);
145
+ if (homeCheck.status !== 0) {
146
+ writeStderr(`remote agent-infra installation is missing on ${alias}\n`);
147
+ return 1;
148
+ }
149
+ const capability = spawnFn('ssh', [
150
+ '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', alias, 'ai', 'cp', '--remote-receive-capability'
151
+ ]);
152
+ if (capability.status !== 0 || capability.stderr !== '' || capability.stdout.replace(/\n$/u, '') !== RECEIVER_CAPABILITY) {
153
+ writeStderr(`remote agent-infra clipboard receiver is unavailable or incompatible on ${alias}; update agent-infra\n`);
154
+ return 1;
155
+ }
156
+ }
157
+
113
158
  const upload = spawnFn('scp', [
114
159
  '-o',
115
160
  'BatchMode=yes',
@@ -124,25 +169,21 @@ export async function cmdCp(args: string[], deps: CpDeps = {}): Promise<number>
124
169
  }
125
170
  uploaded = true;
126
171
 
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));
172
+ const setRemote = remotePlatform === 'Darwin'
173
+ ? spawnFn('ssh', [
174
+ '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', alias, 'osascript', '-'
175
+ ], remoteSetScript(remotePath))
176
+ : spawnFn('ssh', [
177
+ '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', alias, 'ai', 'cp', '--remote-receive-v1', remotePath
178
+ ]);
140
179
  if (setRemote.status !== 0) {
141
180
  writeStderr(`failed to set remote clipboard on ${alias}:\n${commandDetail(setRemote)}\n`);
142
181
  return 1;
143
182
  }
144
183
 
145
- writeStdout(`copied clipboard image to ${alias}\n`);
184
+ writeStdout(remotePlatform === 'Linux'
185
+ ? `uploaded clipboard image to ${alias}; return to the SSH session and press Ctrl+V\n`
186
+ : `copied clipboard image to ${alias}\n`);
146
187
  return 0;
147
188
  } finally {
148
189
  if (uploaded && remotePath) {
package/lib/defaults.json CHANGED
@@ -14,6 +14,7 @@
14
14
  "gemini-cli",
15
15
  "opencode"
16
16
  ],
17
+ "refreshIntervalDays": 7,
17
18
  "dockerfile": null,
18
19
  "vm": {
19
20
  "cpu": null,
@@ -51,6 +52,7 @@
51
52
  "**/test-integration.*",
52
53
  "**/test.*",
53
54
  "**/upgrade-dependency.*",
55
+ ".agents/rules/testing-discipline.*",
54
56
  ".agents/skills/post-release/SKILL.*",
55
57
  ".agents/skills/release/SKILL.*",
56
58
  ".agents/skills/test-integration/SKILL.*",
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process';
2
2
  import { loadConfig } from './config.ts';
3
3
  import { containerNameCandidates, sandboxBranchLabel, sandboxLabel } from './constants.ts';
4
4
  import { detectEngine } from './engine.ts';
5
+ import { commandForEngine } from './shell.ts';
5
6
  import { hostTimezoneEnvFlags, terminalEnvFlags } from './commands/enter.ts';
6
7
  import {
7
8
  fetchSandboxRows,
@@ -95,12 +96,12 @@ function buildRunScript(params: {
95
96
  return `#!/bin/sh
96
97
  set -u
97
98
  cd /workspace
98
- date "+%Y-%m-%d %H:%M:%S%:z" > ${runDir}/started_at
99
+ date "+%Y-%m-%d %H:%M:%S%z" | sed 's/\\([+-][0-9][0-9]\\)\\([0-9][0-9]\\)$/\\1:\\2/' > ${runDir}/started_at
99
100
  printf '%s\\n' running > ${runDir}/status
100
101
  ${command}
101
102
  code=$?
102
103
  printf '%s\\n' "$code" > ${runDir}/exit_code
103
- date "+%Y-%m-%d %H:%M:%S%:z" > ${runDir}/finished_at
104
+ date "+%Y-%m-%d %H:%M:%S%z" | sed 's/\\([+-][0-9][0-9]\\)\\([0-9][0-9]\\)$/\\1:\\2/' > ${runDir}/finished_at
104
105
  if [ "$code" -eq 0 ]; then
105
106
  printf '%s\\n' completed > ${runDir}/status
106
107
  else
@@ -202,7 +203,8 @@ export async function runInSandbox(
202
203
  '-lc',
203
204
  buildTmuxLauncher({ request, runId })
204
205
  ];
205
- const result = await (options.spawn ?? spawnCapture)('docker', dockerArgs);
206
+ const command = commandForEngine(engine, 'docker', dockerArgs);
207
+ const result = await (options.spawn ?? spawnCapture)(command.cmd, command.args);
206
208
  return {
207
209
  ...result,
208
210
  run: {
@@ -9,6 +9,7 @@ import {
9
9
  pruneClipboardDir,
10
10
  writeClipboardPngAtomic
11
11
  } from './paths.ts';
12
+ import { isClipboardInboxReadable, readPendingClipboardPath } from './inbox.ts';
12
13
  import { commandForEngine, restoreTerminal, runInteractiveEngine, runOkEngine } from '../shell.ts';
13
14
  import { loadNodePty, type NodePty, type PtyProcess } from './node-pty.ts';
14
15
 
@@ -30,8 +31,13 @@ type BridgeOptions = {
30
31
  createDetector?: () => CtrlVDetector;
31
32
  };
32
33
 
34
+ type TextImageClipboardAdapter = ClipboardAdapter & {
35
+ readImageFromText?: (text: string) => Buffer | null;
36
+ };
37
+
33
38
  const FALLBACK_PREFIX = 'Warning: clipboard image paste bridge disabled';
34
39
  const PARTIAL_ESCAPE_FLUSH_MS = 30;
40
+ const BRACKETED_PASTE_RE = /^\x1b\[200~([\s\S]*)\x1b\[201~$/u;
35
41
 
36
42
  // Node's stdin.setRawMode(true) uses libuv's RAW mode, which (unlike the
37
43
  // cfmakeraw that `docker exec -it` applies on the non-bridge path) keeps ONLCR
@@ -80,13 +86,9 @@ export async function runInteractiveWithClipboardBridge(options: BridgeOptions):
80
86
  if (!stdin.isTTY || !stdout.isTTY) {
81
87
  return fallback('host stdin/stdout is not a TTY');
82
88
  }
83
- if (!adapter) {
84
- return fallback('no clipboard adapter available on this platform');
85
- }
86
- const available = adapter.available();
87
- if (!available.ok) {
88
- return fallback(available.reason);
89
- }
89
+ const available = adapter?.available() ?? { ok: false as const, reason: 'no clipboard adapter available on this platform' };
90
+ const inboxReady = isClipboardInboxReadable(home);
91
+ if (!available.ok && !inboxReady) return fallback(available.reason);
90
92
  if (!runOk(engine, 'docker', ['exec', container, 'sh', '-c', '[ -d /clipboard ] && [ -r /clipboard ]'])) {
91
93
  return fallback('container /clipboard mount is missing; rebuild the sandbox to enable image paste');
92
94
  }
@@ -114,7 +116,7 @@ export async function runInteractiveWithClipboardBridge(options: BridgeOptions):
114
116
  return runBridge({
115
117
  child,
116
118
  home,
117
- adapter,
119
+ adapter: available.ok ? adapter : null,
118
120
  writeStderr,
119
121
  stdin,
120
122
  stdout,
@@ -133,7 +135,7 @@ async function runBridge({
133
135
  }: {
134
136
  child: PtyProcess;
135
137
  home: string;
136
- adapter: ClipboardAdapter;
138
+ adapter: ClipboardAdapter | null;
137
139
  writeStderr: (chunk: string) => unknown;
138
140
  stdin: NodeJS.ReadStream;
139
141
  stdout: NodeJS.WriteStream;
@@ -147,7 +149,7 @@ async function runBridge({
147
149
  clearFlushTimer();
148
150
  for (const token of detector.feed(inputDecoder.write(chunk))) {
149
151
  if (token.kind === 'text') {
150
- child.write(token.raw);
152
+ handleText(token.raw, child);
151
153
  } else {
152
154
  handleCtrlV(token, child);
153
155
  }
@@ -157,7 +159,7 @@ async function runBridge({
157
159
  flushTimer = null;
158
160
  for (const token of detector.flush()) {
159
161
  if (token.kind === 'text') {
160
- child.write(token.raw);
162
+ handleText(token.raw, child);
161
163
  } else {
162
164
  handleCtrlV(token, child);
163
165
  }
@@ -169,22 +171,55 @@ async function runBridge({
169
171
  const onSigint = () => child.kill('SIGINT');
170
172
  const onSigterm = () => child.kill('SIGTERM');
171
173
 
174
+ function writeImagePaste(png: Buffer, target: PtyProcess): void {
175
+ const filename = pngClipboardFilename(png);
176
+ writeClipboardPngAtomic(clipboardHostDir(home), filename, png);
177
+ pruneClipboardDir(clipboardHostDir(home));
178
+ target.write(buildBracketedPaste(containerClipboardPath(filename)));
179
+ }
180
+
181
+ function handleText(raw: string, target: PtyProcess): void {
182
+ const textAdapter = adapter as TextImageClipboardAdapter | null;
183
+ const pastedText = extractSinglePathPaste(raw);
184
+ if (!textAdapter || !textAdapter.readImageFromText || pastedText === null) {
185
+ target.write(raw);
186
+ return;
187
+ }
188
+
189
+ try {
190
+ const png = textAdapter.readImageFromText(pastedText);
191
+ if (!png) {
192
+ target.write(raw);
193
+ return;
194
+ }
195
+ writeImagePaste(png, target);
196
+ } catch (error) {
197
+ target.write(raw);
198
+ if (!warnedPasteFailure) {
199
+ warnedPasteFailure = true;
200
+ writeStderr(`Warning: clipboard image paste failed; forwarded pasted text (${error instanceof Error ? error.message : 'unknown error'})\n`);
201
+ }
202
+ }
203
+ }
204
+
172
205
  function handleCtrlV(match: CtrlVMatch, target: PtyProcess): void {
173
206
  try {
207
+ const pendingPath = readPendingClipboardPath(home);
208
+ if (pendingPath) {
209
+ target.write(buildBracketedPaste(pendingPath));
210
+ return;
211
+ }
174
212
  // readImagePng returns null both for "no image on clipboard" and for
175
213
  // unexpected read failures; both cases forward the original Ctrl+V so
176
214
  // the container app handles it as a regular keystroke. The throw branch
177
215
  // below only fires on truly unexpected exceptions (e.g. fs write
178
216
  // errors writing to the host clipboard dir).
179
- const png = adapter.readImagePng();
217
+ const png = adapter?.readImagePng() ?? null;
180
218
  if (!png) {
181
219
  target.write(match.raw);
182
220
  return;
183
221
  }
184
- const filename = pngClipboardFilename(png);
185
- writeClipboardPngAtomic(clipboardHostDir(home), filename, png);
186
- pruneClipboardDir(clipboardHostDir(home));
187
- target.write(buildBracketedPaste(containerClipboardPath(filename)));
222
+ writeImagePaste(png, target);
188
223
  } catch (error) {
189
224
  target.write(match.raw);
190
225
  if (!warnedPasteFailure) {
@@ -219,14 +254,14 @@ async function runBridge({
219
254
  try {
220
255
  for (const token of detector.feed(inputDecoder.end())) {
221
256
  if (token.kind === 'text') {
222
- child.write(token.raw);
257
+ handleText(token.raw, child);
223
258
  } else {
224
259
  handleCtrlV(token, child);
225
260
  }
226
261
  }
227
262
  for (const token of detector.flush()) {
228
263
  if (token.kind === 'text') {
229
- child.write(token.raw);
264
+ handleText(token.raw, child);
230
265
  }
231
266
  }
232
267
  } catch {
@@ -244,6 +279,15 @@ async function runBridge({
244
279
  }
245
280
  }
246
281
 
282
+ function extractSinglePathPaste(raw: string): string | null {
283
+ const match = raw.match(BRACKETED_PASTE_RE);
284
+ const text = (match ? match[1] : raw)?.trim();
285
+ if (!text || /[\r\n]/u.test(text)) {
286
+ return null;
287
+ }
288
+ return text;
289
+ }
290
+
247
291
  function onceExit(
248
292
  child: PtyProcess,
249
293
  stdin: NodeJS.ReadStream
@@ -0,0 +1,83 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {
4
+ clipboardHostDir,
5
+ containerClipboardPath,
6
+ pngClipboardFilename,
7
+ pruneClipboardDir,
8
+ writeClipboardPngAtomic
9
+ } from './paths.ts';
10
+
11
+ export const RECEIVER_CAPABILITY = 'AGENT_INFRA_CLIPBOARD_RECEIVER:v1';
12
+ const MARKER = '.pending.json';
13
+ const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
14
+ const HASHED_PNG = /^[a-f0-9]{16}\.png$/u;
15
+ const REMOTE_TEMP = /^agent-infra-cp-[0-9a-f-]{36}\.png$/u;
16
+
17
+ export function isClipboardInboxReadable(home: string): boolean {
18
+ const dir = clipboardHostDir(home);
19
+ try {
20
+ fs.accessSync(dir, fs.constants.R_OK);
21
+ return fs.statSync(dir).isDirectory();
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ export function receiveRemoteClipboardPng(home: string, tempPath: string, platformName: NodeJS.Platform = process.platform): string {
28
+ if (platformName !== 'linux') throw new Error('remote clipboard receiver requires Linux');
29
+ if (path.dirname(tempPath) !== '/tmp' || !REMOTE_TEMP.test(path.basename(tempPath))) {
30
+ throw new Error('invalid clipboard upload path');
31
+ }
32
+ const signature = Buffer.alloc(PNG_SIGNATURE.length);
33
+ const fd = fs.openSync(tempPath, 'r');
34
+ let signatureBytes: number;
35
+ try {
36
+ signatureBytes = fs.readSync(fd, signature, 0, signature.length, 0);
37
+ } finally {
38
+ fs.closeSync(fd);
39
+ }
40
+ if (signatureBytes !== PNG_SIGNATURE.length || !signature.equals(PNG_SIGNATURE)) {
41
+ throw new Error('uploaded clipboard file is not a PNG');
42
+ }
43
+ const png = fs.readFileSync(tempPath);
44
+ const dir = clipboardHostDir(home);
45
+ if (!fs.existsSync(path.dirname(dir))) throw new Error('agent-infra is not installed on this host');
46
+ const filename = pngClipboardFilename(png);
47
+ writeClipboardPngAtomic(dir, filename, png);
48
+ pruneClipboardDir(dir);
49
+ const markerTmp = path.join(dir, `.${MARKER}.${process.pid}.tmp`);
50
+ fs.writeFileSync(markerTmp, JSON.stringify({ version: 1, filename, createdAt: new Date().toISOString() }) + '\n', { mode: 0o600 });
51
+ fs.renameSync(markerTmp, path.join(dir, MARKER));
52
+ return containerClipboardPath(filename);
53
+ }
54
+
55
+ export function readPendingClipboardPath(home: string): string | null {
56
+ const dir = clipboardHostDir(home);
57
+ const markerPath = path.join(dir, MARKER);
58
+ let markerText: string;
59
+ try {
60
+ markerText = fs.readFileSync(markerPath, 'utf8');
61
+ } catch (error) {
62
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
63
+ throw new Error('invalid clipboard pending marker', { cause: error });
64
+ }
65
+
66
+ let marker: { version?: unknown; filename?: unknown };
67
+ try {
68
+ marker = JSON.parse(markerText) as { version?: unknown; filename?: unknown };
69
+ } catch (error) {
70
+ throw new Error('invalid clipboard pending marker', { cause: error });
71
+ }
72
+ if (marker.version !== 1 || typeof marker.filename !== 'string' || !HASHED_PNG.test(marker.filename)) {
73
+ throw new Error('invalid clipboard pending marker');
74
+ }
75
+ const filePath = path.join(dir, marker.filename);
76
+ try {
77
+ const stat = fs.lstatSync(filePath);
78
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('pending clipboard image is not a regular file');
79
+ } catch (error) {
80
+ throw new Error('invalid clipboard pending marker', { cause: error });
81
+ }
82
+ return containerClipboardPath(marker.filename);
83
+ }
@@ -1,7 +1,9 @@
1
1
  import { platform } from 'node:os';
2
2
  import { createDarwinClipboardAdapter, type DarwinClipboardAdapter } from './darwin.ts';
3
+ import { createLinuxClipboardAdapter, type LinuxClipboardAdapter } from './linux.ts';
4
+ import { createWin32ClipboardAdapter, type Win32ClipboardAdapter } from './win32.ts';
3
5
 
4
- export type ClipboardAdapter = DarwinClipboardAdapter;
6
+ export type ClipboardAdapter = DarwinClipboardAdapter | LinuxClipboardAdapter | Win32ClipboardAdapter;
5
7
 
6
8
  export function createClipboardAdapter({
7
9
  platformName = platform()
@@ -10,12 +12,9 @@ export function createClipboardAdapter({
10
12
  case 'darwin':
11
13
  return createDarwinClipboardAdapter();
12
14
  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;
15
+ return createLinuxClipboardAdapter();
16
16
  case 'win32':
17
- // Future work: native Win32 clipboard reader. Returning null disables the bridge.
18
- return null;
17
+ return createWin32ClipboardAdapter();
19
18
  default:
20
19
  return null;
21
20
  }