@fitlab-ai/agent-infra 0.7.1 → 0.7.3

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 (165) hide show
  1. package/README.md +7 -1
  2. package/README.zh-CN.md +9 -3
  3. package/bin/cli.ts +11 -0
  4. package/dist/bin/cli.js +12 -0
  5. package/dist/lib/defaults.json +0 -1
  6. package/dist/lib/init.js +0 -3
  7. package/dist/lib/sandbox/commands/create.js +10 -2
  8. package/dist/lib/sandbox/commands/enter.js +17 -18
  9. package/dist/lib/sandbox/commands/list-running.js +56 -32
  10. package/dist/lib/sandbox/commands/ls.js +27 -24
  11. package/dist/lib/sandbox/commands/start.js +36 -0
  12. package/dist/lib/sandbox/index.js +15 -3
  13. package/dist/lib/sandbox/task-resolver.js +1 -1
  14. package/dist/lib/sandbox/tools.js +1 -1
  15. package/dist/lib/table.js +38 -0
  16. package/dist/lib/task/commands/ls.js +122 -0
  17. package/dist/lib/task/commands/show.js +135 -0
  18. package/dist/lib/task/frontmatter.js +32 -0
  19. package/dist/lib/task/index.js +41 -0
  20. package/dist/lib/task/short-id.js +90 -0
  21. package/dist/lib/update.js +25 -8
  22. package/lib/defaults.json +0 -1
  23. package/lib/init.ts +0 -10
  24. package/lib/sandbox/commands/create.ts +11 -2
  25. package/lib/sandbox/commands/enter.ts +40 -20
  26. package/lib/sandbox/commands/list-running.ts +65 -37
  27. package/lib/sandbox/commands/ls.ts +35 -27
  28. package/lib/sandbox/commands/start.ts +61 -0
  29. package/lib/sandbox/index.ts +15 -3
  30. package/lib/sandbox/task-resolver.ts +1 -1
  31. package/lib/sandbox/tools.ts +1 -1
  32. package/lib/table.ts +44 -0
  33. package/lib/task/commands/ls.ts +138 -0
  34. package/lib/task/commands/show.ts +139 -0
  35. package/lib/task/frontmatter.ts +30 -0
  36. package/lib/task/index.ts +44 -0
  37. package/lib/task/short-id.ts +107 -0
  38. package/lib/update.ts +28 -10
  39. package/package.json +1 -1
  40. package/templates/.agents/hooks/auto-resume.sh +104 -0
  41. package/templates/.agents/rules/create-issue.github.en.md +1 -1
  42. package/templates/.agents/rules/create-issue.github.zh-CN.md +1 -1
  43. package/templates/.agents/rules/milestone-inference.github.en.md +4 -1
  44. package/templates/.agents/rules/milestone-inference.github.zh-CN.md +4 -1
  45. package/templates/.agents/rules/next-step-output.en.md +62 -0
  46. package/templates/.agents/rules/next-step-output.zh-CN.md +62 -0
  47. package/templates/.agents/rules/pr-checks-commands.en.md +5 -0
  48. package/templates/.agents/rules/pr-checks-commands.github.en.md +62 -0
  49. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +62 -0
  50. package/templates/.agents/rules/pr-checks-commands.zh-CN.md +5 -0
  51. package/templates/.agents/rules/pr-sync.github.en.md +7 -0
  52. package/templates/.agents/rules/pr-sync.github.zh-CN.md +7 -0
  53. package/templates/.agents/rules/task-short-id.en.md +54 -62
  54. package/templates/.agents/rules/task-short-id.zh-CN.md +35 -54
  55. package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +17 -0
  56. package/templates/.agents/scripts/task-short-id.js +32 -189
  57. package/templates/.agents/skills/analyze-task/SKILL.en.md +10 -12
  58. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +10 -12
  59. package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -1
  60. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -1
  61. package/templates/.agents/skills/block-task/SKILL.en.md +13 -6
  62. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +13 -6
  63. package/templates/.agents/skills/block-task/config/verify.json +1 -1
  64. package/templates/.agents/skills/cancel-task/SKILL.en.md +13 -6
  65. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +13 -6
  66. package/templates/.agents/skills/cancel-task/config/verify.json +1 -1
  67. package/templates/.agents/skills/check-task/SKILL.en.md +12 -10
  68. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +12 -10
  69. package/templates/.agents/skills/close-codescan/SKILL.en.md +13 -6
  70. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +13 -6
  71. package/templates/.agents/skills/close-dependabot/SKILL.en.md +13 -6
  72. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +13 -6
  73. package/templates/.agents/skills/code-task/SKILL.en.md +10 -6
  74. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +11 -6
  75. package/templates/.agents/skills/code-task/config/verify.en.json +2 -1
  76. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +2 -1
  77. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +10 -5
  78. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +10 -5
  79. package/templates/.agents/skills/code-task/reference/output-template.en.md +3 -3
  80. package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +3 -3
  81. package/templates/.agents/skills/code-task/reference/report-template.en.md +8 -0
  82. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +8 -0
  83. package/templates/.agents/skills/commit/SKILL.en.md +3 -4
  84. package/templates/.agents/skills/commit/SKILL.zh-CN.md +3 -4
  85. package/templates/.agents/skills/commit/reference/task-status-update.en.md +37 -29
  86. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +37 -29
  87. package/templates/.agents/skills/complete-task/SKILL.en.md +41 -4
  88. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +41 -4
  89. package/templates/.agents/skills/complete-task/config/verify.en.json +1 -1
  90. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -1
  91. package/templates/.agents/skills/create-pr/SKILL.en.md +20 -11
  92. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +20 -11
  93. package/templates/.agents/skills/create-pr/config/verify.json +2 -1
  94. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -1
  95. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -1
  96. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +3 -3
  97. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +3 -3
  98. package/templates/.agents/skills/create-task/SKILL.en.md +17 -17
  99. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +17 -17
  100. package/templates/.agents/skills/create-task/config/verify.json +1 -1
  101. package/templates/.agents/skills/import-codescan/SKILL.en.md +8 -8
  102. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +8 -8
  103. package/templates/.agents/skills/import-codescan/config/verify.json +1 -1
  104. package/templates/.agents/skills/import-dependabot/SKILL.en.md +8 -8
  105. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +8 -8
  106. package/templates/.agents/skills/import-dependabot/config/verify.json +1 -1
  107. package/templates/.agents/skills/import-issue/SKILL.en.md +7 -7
  108. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +7 -7
  109. package/templates/.agents/skills/plan-task/SKILL.en.md +10 -12
  110. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +10 -12
  111. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
  112. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
  113. package/templates/.agents/skills/restore-task/SKILL.en.md +1 -1
  114. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +1 -1
  115. package/templates/.agents/skills/review-analysis/SKILL.en.md +4 -2
  116. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +4 -2
  117. package/templates/.agents/skills/review-analysis/config/verify.en.json +3 -2
  118. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +3 -2
  119. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +15 -15
  120. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +15 -15
  121. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +7 -1
  122. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +7 -1
  123. package/templates/.agents/skills/review-analysis/reference/review-criteria.en.md +2 -0
  124. package/templates/.agents/skills/review-analysis/reference/review-criteria.zh-CN.md +2 -0
  125. package/templates/.agents/skills/review-code/SKILL.en.md +5 -2
  126. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -2
  127. package/templates/.agents/skills/review-code/config/verify.en.json +3 -2
  128. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +3 -2
  129. package/templates/.agents/skills/review-code/reference/output-templates.en.md +9 -9
  130. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +9 -9
  131. package/templates/.agents/skills/review-code/reference/report-template.en.md +7 -1
  132. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +7 -1
  133. package/templates/.agents/skills/review-code/reference/review-criteria.en.md +2 -0
  134. package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +2 -0
  135. package/templates/.agents/skills/review-plan/SKILL.en.md +4 -2
  136. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +4 -2
  137. package/templates/.agents/skills/review-plan/config/verify.en.json +3 -2
  138. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +3 -2
  139. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +15 -15
  140. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +15 -15
  141. package/templates/.agents/skills/review-plan/reference/report-template.en.md +7 -1
  142. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +7 -1
  143. package/templates/.agents/skills/review-plan/reference/review-criteria.en.md +2 -0
  144. package/templates/.agents/skills/review-plan/reference/review-criteria.zh-CN.md +2 -0
  145. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +0 -1
  146. package/templates/.agents/skills/watch-pr/SKILL.en.md +131 -0
  147. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +131 -0
  148. package/templates/.agents/skills/watch-pr/config/verify.json +22 -0
  149. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +43 -0
  150. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +43 -0
  151. package/templates/.agents/templates/task.en.md +1 -1
  152. package/templates/.agents/templates/task.zh-CN.md +1 -1
  153. package/templates/.agents/workflows/bug-fix.en.yaml +7 -5
  154. package/templates/.agents/workflows/bug-fix.zh-CN.yaml +6 -5
  155. package/templates/.agents/workflows/feature-development.en.yaml +7 -5
  156. package/templates/.agents/workflows/feature-development.zh-CN.yaml +6 -5
  157. package/templates/.agents/workflows/refactoring.en.yaml +7 -5
  158. package/templates/.agents/workflows/refactoring.zh-CN.yaml +6 -5
  159. package/templates/.claude/commands/watch-pr.en.md +8 -0
  160. package/templates/.claude/commands/watch-pr.zh-CN.md +8 -0
  161. package/templates/.claude/settings.json +11 -0
  162. package/templates/.gemini/commands/_project_/watch-pr.en.toml +8 -0
  163. package/templates/.gemini/commands/_project_/watch-pr.zh-CN.toml +8 -0
  164. package/templates/.opencode/commands/watch-pr.en.md +11 -0
  165. package/templates/.opencode/commands/watch-pr.zh-CN.md +11 -0
package/README.md CHANGED
@@ -165,6 +165,9 @@ curl -fsSL https://raw.githubusercontent.com/fitlab-ai/agent-infra/main/install.
165
165
  **Option C - Homebrew (macOS)**
166
166
 
167
167
  ```bash
168
+ # Newer Homebrew refuses to load formulae from third-party taps until trusted,
169
+ # which silently blocks upgrades. Trust the tap once before installing.
170
+ brew trust fitlab-ai/tap
168
171
  brew install fitlab-ai/tap/agent-infra
169
172
  ```
170
173
 
@@ -218,6 +221,8 @@ The sandbox image also preinstalls `gh`. When `gh auth token` succeeds on the ho
218
221
 
219
222
  `ai sandbox exec` also forwards a small terminal-detection whitelist (`TERM_PROGRAM`, `TERM_PROGRAM_VERSION`, `LC_TERMINAL`, `LC_TERMINAL_VERSION`) into the container. This keeps interactive TUIs aligned with the host terminal for behaviors such as Claude Code's Shift+Enter newline support, without passing through the full host environment.
220
223
 
224
+ `ai sandbox start <branch | TASK-id | N | '#N'>` recovers a sandbox container that has stopped — for example after the host Docker daemon was restarted or replaced (a common case is installing OrbStack over an existing Docker), which leaves the container `Exited`. It only starts a container that already exists and is stopped; if none exists it points you to `ai sandbox create`, and a container that is already running is left untouched. `ai sandbox exec <branch>` performs the same recovery automatically: when the target container exists but is stopped, it starts the container first and then enters it. Because each worktree and per-AI state directory is persisted on the host, restarting a stopped container is safe and loses no data.
225
+
221
226
  On macOS, interactive `ai sandbox exec <branch>` sessions can bridge image paste into the sandbox. When you press `Ctrl+V` and the host clipboard currently holds an image, agent-infra reads the image from the host clipboard, writes a PNG under `~/.agent-infra/clipboard/`, and injects the container path as bracketed paste so Claude Code, Codex, Gemini CLI, and OpenCode can attach it. The host clipboard is only read, never rewritten. The bridge is best-effort: existing sandboxes must be rebuilt to receive the `/clipboard` mount, and if the optional pty dependency or clipboard probe is unavailable the session falls back to the normal interactive path. Set `AI_SANDBOX_NO_CLIPBOARD_BRIDGE=1` to skip the bridge and enter the normal interactive path directly when diagnosing mouse, scrolling, or other input issues.
222
227
 
223
228
  When you run the sandbox from a remote Mac over SSH, use `ai cp <ssh-alias>` on the Mac in front of you to push the local clipboard image to that remote Mac first. Copy an image with Cmd+C, run `ai cp mini`, then return to the existing SSH session and press `Ctrl+V`; the sandbox bridge reads the remote Mac's NSPasteboard and injects the image as usual. This command handles PNG images only and uses non-interactive ssh/scp with key-based authentication. For now both the sender and the remote must be macOS—the remote NSPasteboard is written via `osascript`—but the remote-write step is the natural extension point for other platforms later.
@@ -484,7 +489,7 @@ These configurations are not actively tested in this release:
484
489
 
485
490
  - **Podman** instead of Docker: Works on Fedora 40+ and other `dnf`-based RHEL family distros (RHEL, CentOS Stream, Rocky, Alma) via the `podman-docker` shim (`sudo dnf install podman podman-docker`; optionally `sudo touch /etc/containers/nodocker` to silence its per-command notice).
486
491
  - **SELinux-enforcing** hosts (Fedora / RHEL): `ai sandbox create` automatically labels bind mounts with Docker's shared `:z` flag — no setup required. Set `AGENT_INFRA_SELINUX_DISABLE=1` to opt out for debugging.
487
- - `ai sandbox vm` is a no-op on Linux. Linux uses native Docker directly with no VM to manage; use `ai sandbox create`, `ai sandbox exec`, `ai sandbox refresh`, `ai sandbox ls`, `ai sandbox rebuild`, `ai sandbox rm`, and `ai sandbox prune` directly.
492
+ - `ai sandbox vm` is a no-op on Linux. Linux uses native Docker directly with no VM to manage; use `ai sandbox create`, `ai sandbox exec`, `ai sandbox start`, `ai sandbox refresh`, `ai sandbox ls`, `ai sandbox rebuild`, `ai sandbox rm`, and `ai sandbox prune` directly.
488
493
 
489
494
  ### Windows
490
495
 
@@ -560,6 +565,7 @@ agent-infra ships with **a rich set of built-in AI skills**. They are organized
560
565
  | Skill | Description | Parameters | Recommended use case |
561
566
  |-------|-------------|------------|----------------------|
562
567
  | `create-pr` | Open a Pull Request to an inferred or explicit target branch. | `task-id` (optional), `target-branch` (optional) | Publish reviewed changes for merge, with optional explicit task linkage after a fresh session. |
568
+ | `watch-pr` | Watch a PR's required checks and self-heal failures until green. | `task-id` or `--pr <number>` (optional; defaults to the current branch's PR) | Monitor CI after create-pr and auto-fix simple failures before merging. |
563
569
 
564
570
  <a id="code-quality"></a>
565
571
 
package/README.zh-CN.md CHANGED
@@ -19,7 +19,7 @@
19
19
  <a href="https://nodejs.org/"><img src="https://img.shields.io/badge/Node.js-%3E%3D22-brightgreen?logo=node.js" alt="Node.js >= 22"></a>
20
20
  <a href="https://github.com/fitlab-ai/agent-infra/releases"><img src="https://img.shields.io/github/v/release/fitlab-ai/agent-infra" alt="GitHub release"></a>
21
21
  <a href="https://codecov.io/gh/fitlab-ai/agent-infra"><img src="https://codecov.io/gh/fitlab-ai/agent-infra/graph/badge.svg" alt="codecov"></a>
22
- <a href="CONTRIBUTING.md"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs Welcome"></a>
22
+ <a href="CONTRIBUTING.zh-CN.md"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs Welcome"></a>
23
23
  </p>
24
24
 
25
25
  <p align="center">
@@ -165,6 +165,9 @@ curl -fsSL https://raw.githubusercontent.com/fitlab-ai/agent-infra/main/install.
165
165
  **方式 C - Homebrew (macOS)**
166
166
 
167
167
  ```bash
168
+ # 新版 Homebrew 默认拒绝加载第三方 tap 的 formula,
169
+ # 会导致升级被静默跳过。首次安装前先信任本 tap。
170
+ brew trust fitlab-ai/tap
168
171
  brew install fitlab-ai/tap/agent-infra
169
172
  ```
170
173
 
@@ -218,6 +221,8 @@ CLI 会收集项目元数据,向所有支持的 AI TUI 安装 `update-agent-in
218
221
 
219
222
  `ai sandbox exec` 也会向容器透传一小组终端检测白名单变量(`TERM_PROGRAM`、`TERM_PROGRAM_VERSION`、`LC_TERMINAL`、`LC_TERMINAL_VERSION`)。这样可以让交互式 TUI 保持与宿主终端一致的行为,例如 Claude Code 的 `Shift+Enter` 换行支持,同时避免把整个宿主环境灌入容器。
220
223
 
224
+ `ai sandbox start <branch | TASK-id | N | '#N'>` 用于恢复已停止的沙箱容器——典型场景是宿主机 Docker daemon 被重启或替换(例如在已有 Docker 上安装 OrbStack 接管),导致容器变成 `Exited`。它只启动「已存在且已停止」的容器;容器不存在时会提示改用 `ai sandbox create`,已在运行的容器则保持不动。`ai sandbox exec <branch>` 会自动执行同样的恢复:当目标容器存在但已停止时,先启动容器再进入。由于每个 worktree 和各 AI 的 state 目录都持久化在宿主机,重启已停止的容器是安全的,不会丢失数据。
225
+
221
226
  在 macOS 上,交互式 `ai sandbox exec <branch>` 会尽力桥接宿主图片粘贴。当你按下 `Ctrl+V` 且宿主剪贴板当前是图片时,agent-infra 会从宿主剪贴板读取图片,将 PNG 写到 `~/.agent-infra/clipboard/`,再以 bracketed paste 注入容器内路径,让 Claude Code、Codex、Gemini CLI 和 OpenCode 按图片附件处理。宿主剪贴板只读,不会被改写。该能力会自动降级:已有沙箱需要重建后才有 `/clipboard` 挂载;如果可选 pty 依赖或剪贴板探测不可用,会回退到原本的交互进入方式。排查鼠标、滚动或其他输入异常时,可以设置 `AI_SANDBOX_NO_CLIPBOARD_BRIDGE=1` 跳过桥接,直接进入原本的交互路径。
222
227
 
223
228
  当你通过 SSH 在远端 Mac 上运行沙箱时,可先在手边这台 Mac 上执行 `ai cp <ssh-alias>`,把本机剪贴板图片推送到远端 Mac。典型流程是:Cmd+C 复制图片,运行 `ai cp mini`,回到已有 SSH session 后按 `Ctrl+V`;沙箱桥会读取远端 Mac 的 NSPasteboard,并按原路径注入图片。该命令只处理 PNG 图片,并使用基于 ssh key 的非交互 ssh/scp。目前发送端与远端都需为 macOS(远端通过 `osascript` 写入 NSPasteboard),后续可扩展支持其他远端平台。
@@ -457,7 +462,7 @@ Rootless 模式的已知差异:
457
462
 
458
463
  - 用 **Podman** 替代 Docker:Fedora 40+ 及其他 `dnf` 系 RHEL 发行版(RHEL、CentOS Stream、Rocky、Alma)上通过 `podman-docker` shim 已可使用(`sudo dnf install podman podman-docker`;可选 `sudo touch /etc/containers/nodocker` 抑制 podman 在每条命令前打印的提示)。
459
464
  - **SELinux enforcing** 宿主机(Fedora / RHEL):`ai sandbox create` 会自动给 bind mount 加 Docker 共享 `:z` 标签,无需手动准备。如需排障可设 `AGENT_INFRA_SELINUX_DISABLE=1` 关闭。
460
- - `ai sandbox vm` 在 Linux 上是空操作。Linux 直接使用 native Docker,没有 VM 需要管理;请直接使用 `ai sandbox create`、`ai sandbox exec`、`ai sandbox refresh`、`ai sandbox ls`、`ai sandbox rebuild`、`ai sandbox rm`、`ai sandbox prune`。
465
+ - `ai sandbox vm` 在 Linux 上是空操作。Linux 直接使用 native Docker,没有 VM 需要管理;请直接使用 `ai sandbox create`、`ai sandbox exec`、`ai sandbox start`、`ai sandbox refresh`、`ai sandbox ls`、`ai sandbox rebuild`、`ai sandbox rm`、`ai sandbox prune`。
461
466
 
462
467
  ### Windows
463
468
 
@@ -533,6 +538,7 @@ agent-infra 提供 **丰富的内置 AI skills**。它们按使用场景分组
533
538
  | Skill | 描述 | 参数 | 推荐场景 |
534
539
  |-------|------|------|---------|
535
540
  | `create-pr` | 向推断出的目标分支或显式指定分支创建 Pull Request。 | `task-id`(可选)、`target-branch`(可选) | 变更准备合入时创建 PR;清空上下文后也可显式传入任务关联。 |
541
+ | `watch-pr` | 监控 PR 的 required checks,失败时自愈直到全绿。 | `task-id` 或 `--pr <number>`(可选;默认取当前分支的 PR) | create-pr 后监控 CI,合入前自动修复简单失败。 |
536
542
 
537
543
  <a id="code-quality"></a>
538
544
 
@@ -923,7 +929,7 @@ agent-infra 通过 Git tag 和 GitHub release 使用语义化版本号。当前
923
929
 
924
930
  ## 参与贡献
925
931
 
926
- 开发规范请参阅 [CONTRIBUTING.md](CONTRIBUTING.md)。
932
+ 开发规范请参阅 [CONTRIBUTING.zh-CN.md](CONTRIBUTING.zh-CN.md)。
927
933
 
928
934
  <a id="license"></a>
929
935
 
package/bin/cli.ts CHANGED
@@ -18,6 +18,7 @@ Usage:
18
18
  agent-infra init Initialize a new project with update-agent-infra seed command
19
19
  agent-infra merge Merge tasks from another workspace directory (active/blocked/completed/archive)
20
20
  agent-infra sandbox Manage Docker-based AI sandboxes
21
+ agent-infra task Read-only views over .agents/workspace tasks (ls / show)
21
22
  agent-infra update Update seed files and sync file registry for an existing project
22
23
  agent-infra version Show version
23
24
 
@@ -97,6 +98,16 @@ switch (command) {
97
98
  });
98
99
  break;
99
100
  }
101
+ case 'task': {
102
+ const imported = await importCommand('../lib/task/index.ts');
103
+ if (!imported) break;
104
+ const { runTask } = imported;
105
+ await runTask(process.argv.slice(3)).catch((e: unknown) => {
106
+ process.stderr.write(`Error: ${errorMessage(e)}\n`);
107
+ process.exit(1);
108
+ });
109
+ break;
110
+ }
100
111
  case 'cp': {
101
112
  const imported = await importCommand('../lib/cp.ts');
102
113
  if (!imported) break;
package/dist/bin/cli.js CHANGED
@@ -22,6 +22,7 @@ Usage:
22
22
  agent-infra init Initialize a new project with update-agent-infra seed command
23
23
  agent-infra merge Merge tasks from another workspace directory (active/blocked/completed/archive)
24
24
  agent-infra sandbox Manage Docker-based AI sandboxes
25
+ agent-infra task Read-only views over .agents/workspace tasks (ls / show)
25
26
  agent-infra update Update seed files and sync file registry for an existing project
26
27
  agent-infra version Show version
27
28
 
@@ -100,6 +101,17 @@ switch (command) {
100
101
  });
101
102
  break;
102
103
  }
104
+ case 'task': {
105
+ const imported = await importCommand('../lib/task/index.ts');
106
+ if (!imported)
107
+ break;
108
+ const { runTask } = imported;
109
+ await runTask(process.argv.slice(3)).catch((e) => {
110
+ process.stderr.write(`Error: ${errorMessage(e)}\n`);
111
+ process.exit(1);
112
+ });
113
+ break;
114
+ }
103
115
  case 'cp': {
104
116
  const imported = await importCommand('../lib/cp.ts');
105
117
  if (!imported)
@@ -2,7 +2,6 @@
2
2
  "platform": {
3
3
  "type": "github"
4
4
  },
5
- "requiresPullRequest": true,
6
5
  "sandbox": {
7
6
  "engine": null,
8
7
  "runtimes": [
package/dist/lib/init.js CHANGED
@@ -158,8 +158,6 @@ async function cmdInit() {
158
158
  info(`Custom platform '${platformType}' selected. Built-in templates are only complete for github;`
159
159
  + ` provide matching '.${platformType}.' or generic templates before running update-agent-infra.`);
160
160
  }
161
- const requiresPRChoice = await select('Require Pull Request flow?', ['yes', 'no'], 'yes');
162
- const requiresPullRequest = requiresPRChoice !== 'no';
163
161
  let enabledTUIs;
164
162
  try {
165
163
  enabledTUIs = await multiSelect('Built-in TUI command files to install/manage', BUILTIN_TUI_IDS.map((id) => ({ id, label: BUILTIN_TUI_DISPLAY[id] })));
@@ -220,7 +218,6 @@ async function cmdInit() {
220
218
  org: orgName,
221
219
  language,
222
220
  platform: { type: platformType },
223
- requiresPullRequest,
224
221
  templateVersion: VERSION,
225
222
  sandbox: structuredClone(defaults.sandbox),
226
223
  task: structuredClone(defaults.task),
@@ -630,13 +630,21 @@ export function ensureCodexModelInheritance(toolDir, hostHomeDir) {
630
630
  return;
631
631
  }
632
632
  }
633
+ const inheritSpecs = [
634
+ ['model', 'string'],
635
+ ['model_reasoning_effort', 'string'],
636
+ ['model_auto_compact_token_limit', 'number']
637
+ ];
633
638
  let changed = false;
634
- for (const key of ['model', 'model_reasoning_effort']) {
639
+ for (const [key, type] of inheritSpecs) {
635
640
  if (Object.hasOwn(sandboxParsed, key)) {
636
641
  continue;
637
642
  }
638
643
  const value = hostParsed[key];
639
- if (typeof value !== 'string' || value === '') {
644
+ if (type === 'string' && (typeof value !== 'string' || value === '')) {
645
+ continue;
646
+ }
647
+ if (type === 'number' && (typeof value !== 'number' || !Number.isFinite(value) || value <= 0)) {
640
648
  continue;
641
649
  }
642
650
  sandboxParsed[key] = value;
@@ -2,16 +2,17 @@ import { loadConfig } from "../config.js";
2
2
  import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel } from "../constants.js";
3
3
  import { detectEngine } from "../engine.js";
4
4
  import { formatCredentialWarnings, formatRemaining, reconcileClaudeCredentials, redactCommandError, validateClaudeCredentialsEnvOverride } from "../credentials.js";
5
- import { runInteractiveEngine, runSafeEngine } from "../shell.js";
6
- import { resolveTaskBranch } from "../task-resolver.js";
5
+ import { runInteractiveEngine } from "../shell.js";
7
6
  import { dotfilesCacheDir, materializeDotfiles } from "../dotfiles.js";
8
7
  import { runInteractiveWithClipboardBridge } from "../clipboard/bridge.js";
9
8
  import { detectHostTimezone } from "../host-timezone.js";
10
- import { fetchSandboxRows, isTaskShortRef, resolveTaskShortRef } from "./list-running.js";
11
- const USAGE = `Usage: ai sandbox exec <branch | TASK-id | '#N'> [cmd...]
9
+ import { fetchSandboxRows, resolveBranchArg, selectSandboxContainer, startSandboxContainer } from "./list-running.js";
10
+ const USAGE = `Usage: ai sandbox exec <branch | TASK-id | N | '#N'> [cmd...]
12
11
 
13
- '#N' references the N-th running sandbox in 'ai sandbox ls' order (1-based).
14
- Quote it as '#N' to avoid shell '#' comment handling.`;
12
+ N (bare) and '#N' both reference the same active task short id from
13
+ .agents/workspace/active/.short-ids.json. They resolve only via that
14
+ registry — they do not reference a container's row position in
15
+ 'ai sandbox ls' output.`;
15
16
  const TMUX_ENTRY_PATH = '/usr/local/bin/sandbox-tmux-entry';
16
17
  // Terminal-detection variables that interactive TUIs (e.g. claude-code)
17
18
  // inspect to enable progressive enhancements such as the kitty keyboard
@@ -84,20 +85,18 @@ export async function enter(args) {
84
85
  validateClaudeCredentialsEnvOverride();
85
86
  const engine = detectEngine(config);
86
87
  const [firstArg = '', ...cmd] = args;
87
- let branch;
88
- if (isTaskShortRef(firstArg)) {
89
- const { running } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
90
- branch = resolveTaskShortRef(firstArg, { running, repoRoot: config.repoRoot });
91
- }
92
- else {
93
- branch = resolveTaskBranch(firstArg, config.repoRoot);
94
- }
88
+ const branch = resolveBranchArg(firstArg, { repoRoot: config.repoRoot });
95
89
  assertValidBranchName(branch);
96
- const running = runSafeEngine(engine, 'docker', ['ps', '--format', '{{.Names}}']).split('\n');
97
- const container = containerNameCandidates(config, branch).find((name) => running.includes(name));
98
- if (!container) {
99
- throw new Error(`No running sandbox found for branch '${branch}'`);
90
+ const { running, nonRunning } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
91
+ const found = selectSandboxContainer([...running, ...nonRunning], containerNameCandidates(config, branch));
92
+ if (!found) {
93
+ throw new Error(`No sandbox found for branch '${branch}'. Run 'ai sandbox create ${branch}' to create one.`);
94
+ }
95
+ if (!found.running) {
96
+ process.stderr.write(`Sandbox '${found.name}' is stopped; starting it...\n`);
97
+ startSandboxContainer(engine, found.name);
100
98
  }
99
+ const container = found.name;
101
100
  if (config.tools.includes('claude-code')) {
102
101
  try {
103
102
  // Scan all projects so a refresh from a neighbouring sandbox can still flow back to the host.
@@ -1,7 +1,8 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import { runSafeEngine } from "../shell.js";
4
+ import { runSafeEngine, runVerboseEngine } from "../shell.js";
5
+ import { resolveTaskBranch } from "../task-resolver.js";
5
6
  export function containerListFormat() {
6
7
  return '{{.Names}}\t{{.Status}}\t{{.Labels}}';
7
8
  }
@@ -68,13 +69,14 @@ export function fetchSandboxRows(engine, label, branchKey) {
68
69
  return sortAndIndexSandboxRows(parseSandboxRows(raw, branchKey));
69
70
  }
70
71
  /**
71
- * Returns true iff `arg` is a syntactically valid task short reference ('#N').
72
+ * Returns true iff `arg` is a syntactically valid task short reference.
73
+ * Accepts both bare numeric ('11') and '#'-prefixed ('#11') forms.
72
74
  * Zero IO. Callers MUST use this as the gate before constructing any context
73
75
  * for resolveTaskShortRef — that way non-matching arguments (e.g. '#abc',
74
76
  * '#1.5', '#') never trigger sandbox list IO.
75
77
  */
76
78
  export function isTaskShortRef(arg) {
77
- return /^#\d+$/.test(arg);
79
+ return /^#?\d+$/.test(arg);
78
80
  }
79
81
  /**
80
82
  * Try to resolve a short ref against the global task-short-id registry.
@@ -89,6 +91,9 @@ function tryResolveFromRegistry(arg, repoRoot) {
89
91
  const scriptPath = path.join(repoRoot, '.agents', 'scripts', 'task-short-id.js');
90
92
  if (!fs.existsSync(scriptPath))
91
93
  return { status: 'miss' };
94
+ // Strip leading '#' when forwarding bare-numeric input through the script's CLI.
95
+ // (Script accepts both forms, but this avoids shell quoting confusion in error
96
+ // messages echoed back to the user.)
92
97
  const result = spawnSync('node', [scriptPath, 'resolve', arg], { encoding: 'utf8', cwd: repoRoot });
93
98
  if (result.status !== 0)
94
99
  return { status: 'miss' };
@@ -113,41 +118,60 @@ function tryResolveFromRegistry(arg, repoRoot) {
113
118
  }
114
119
  throw new Error(`Short ref '${arg}' resolved to task ${taskId} but task.md was not found under any workspace dir`);
115
120
  }
116
- function resolveByRunningIndex(arg, running) {
117
- const n = Number(arg.slice(1));
118
- if (n < 1) {
119
- throw new Error(`Invalid sandbox index '${arg}': must be >= 1`);
120
- }
121
- if (running.length === 0) {
122
- throw new Error(`No running sandbox to reference with '${arg}'`);
123
- }
124
- if (n > running.length) {
125
- throw new Error(`No running sandbox at index '${arg}' (only ${running.length} running)`);
126
- }
127
- const row = running[n - 1];
128
- if (!row.branch) {
129
- throw new Error(`Cannot resolve branch for sandbox '${arg}' (container '${row.name}' missing branch label)`);
130
- }
131
- return row.branch;
132
- }
133
121
  /**
134
- * Resolve a task short reference ('#N') to a branch name for the sandbox entrypoint.
122
+ /**
123
+ * Resolve a task short reference (bare 'N' or '#N') to a branch name for the
124
+ * sandbox entrypoint.
135
125
  *
136
- * Resolution order (sandbox fallback mode, plan-r7 C2):
137
- * 1. Try the global task-short-id registry under repoRoot. If hit, look up the
138
- * branch from the matching task.md.
139
- * 2. Fallback to the running-sandbox list index (preserves the #414 ls-index
140
- * behaviour; long-term contract per analysis-r5).
126
+ * Resolution: registry-only. Look up the short id in the global task-short-id
127
+ * registry under repoRoot; if hit, read the branch from the matching task.md.
128
+ * On miss (registry empty or short id absent), throw with an actionable
129
+ * message instead of falling back to a container's row position in
130
+ * 'ai sandbox ls' output — that fallback would make the same syntax mean
131
+ * different things depending on `docker ps` state.
141
132
  *
142
133
  * Precondition: callers MUST gate on isTaskShortRef(arg) === true.
143
134
  */
144
135
  export function resolveTaskShortRef(arg, ctx) {
145
- if (ctx.repoRoot) {
146
- const lookup = tryResolveFromRegistry(arg, ctx.repoRoot);
147
- if (lookup.status === 'hit')
148
- return lookup.branch;
149
- // 'miss' falls through to ls-index fallback (preserves #414 behaviour); 'hit-but-invalid' already threw above.
136
+ const lookup = tryResolveFromRegistry(arg, ctx.repoRoot);
137
+ if (lookup.status === 'hit')
138
+ return lookup.branch;
139
+ throw new Error(`short ref '${arg}' is not in the active task registry. ` +
140
+ `'#N' and bare N resolve only via the registry (not by row position in 'ai sandbox ls'); ` +
141
+ `use a task short id (e.g. 'ai sandbox exec 11'), a TASK-id, or a branch name.`);
142
+ }
143
+ /**
144
+ * Resolve a sandbox command argument (`<branch | TASK-id | N | '#N'>`) to a
145
+ * branch name, mirroring `ai sandbox exec` so that `start` and `exec` share one
146
+ * input contract. Short refs go through the registry-only resolver (which throws
147
+ * an actionable error on a miss); everything else flows through resolveTaskBranch
148
+ * (plain branch names pass through unchanged, TASK-ids resolve via task.md).
149
+ * Callers still run assertValidBranchName on the result.
150
+ */
151
+ export function resolveBranchArg(arg, ctx) {
152
+ return isTaskShortRef(arg)
153
+ ? resolveTaskShortRef(arg, ctx)
154
+ : resolveTaskBranch(arg, ctx.repoRoot);
155
+ }
156
+ /**
157
+ * Pick the sandbox container row whose name matches one of the candidate
158
+ * container names (covers both the '..' and legacy '-' branch sanitizations).
159
+ * Pure: no IO. Returns null when no row matches.
160
+ */
161
+ export function selectSandboxContainer(rows, candidates) {
162
+ return rows.find((row) => candidates.includes(row.name)) ?? null;
163
+ }
164
+ /**
165
+ * Start an existing (stopped) sandbox container by name. Throws a distinct,
166
+ * actionable error when `docker start` fails, so callers can tell "start failed"
167
+ * apart from "container not found".
168
+ */
169
+ export function startSandboxContainer(engine, name) {
170
+ try {
171
+ runVerboseEngine(engine, 'docker', ['start', name]);
172
+ }
173
+ catch (error) {
174
+ throw new Error(`Failed to start sandbox container '${name}': ${error instanceof Error ? error.message : 'unknown error'}`);
150
175
  }
151
- return resolveByRunningIndex(arg, ctx.running);
152
176
  }
153
177
  //# sourceMappingURL=list-running.js.map
@@ -6,27 +6,22 @@ import { loadConfig } from "../config.js";
6
6
  import { sandboxBranchLabel, sandboxLabel } from "../constants.js";
7
7
  import { detectEngine } from "../engine.js";
8
8
  import { resolveTools, toolProjectDirCandidates } from "../tools.js";
9
+ import { formatTable } from "../../table.js";
10
+ import { lookupShortIdByBranch } from "../../task/short-id.js";
9
11
  import { fetchSandboxRows } from "./list-running.js";
10
12
  export { containerListFormat, parseLabels } from "./list-running.js";
11
13
  const USAGE = `Usage: ai sandbox ls
12
14
 
13
- Lists all containers for the current project. The leftmost '#' column
14
- numbers running sandboxes; use it as "ai sandbox exec '#N'" to enter one.
15
- Quote '#N' to avoid shell '#' comment handling.`;
16
- const CONTAINER_TABLE_HEADERS = ['#', 'NAMES', 'STATUS', 'BRANCH'];
17
- export function formatContainerTable(rows) {
18
- const columns = rows.map((row) => [row.index, row.name, row.status, row.branch]);
19
- const widths = [
20
- Math.max(CONTAINER_TABLE_HEADERS[0].length, ...rows.map((row) => row.index.length)),
21
- Math.max(CONTAINER_TABLE_HEADERS[1].length, ...rows.map((row) => row.name.length)),
22
- Math.max(CONTAINER_TABLE_HEADERS[2].length, ...rows.map((row) => row.status.length)),
23
- Math.max(CONTAINER_TABLE_HEADERS[3].length, ...rows.map((row) => row.branch.length))
24
- ];
25
- const renderRow = (values) => `${values[0].padEnd(widths[0])} ${values[1].padEnd(widths[1])} ${values[2].padEnd(widths[2])} ${values[3]}`.trimEnd();
26
- return [
27
- renderRow(CONTAINER_TABLE_HEADERS),
28
- ...columns.map((column) => renderRow(column))
29
- ];
15
+ Lists all containers for the current project. The '#' column is a
16
+ display-only row number; the 'SHORT' column shows the active task short
17
+ id bound to each container's branch (via
18
+ .agents/workspace/active/.short-ids.json), or '-' if no active task is
19
+ bound. Pass the SHORT value to "ai sandbox exec" (e.g. 'ai sandbox exec 11').
20
+ A '-' means no active task is bound to that branch, so the sandbox is free
21
+ to remove with "ai sandbox rm <branch>".`;
22
+ const CONTAINER_TABLE_HEADERS = ['#', 'SHORT', 'NAMES', 'STATUS', 'BRANCH'];
23
+ export function formatContainerTable(rows, zebra = false) {
24
+ return formatTable(CONTAINER_TABLE_HEADERS, rows.map((r) => [r.row, r.shortId, r.name, r.status, r.branch]), { zebra });
30
25
  }
31
26
  function listChildren(dir) {
32
27
  if (!fs.existsSync(dir)) {
@@ -51,15 +46,23 @@ export function ls(args = []) {
51
46
  p.log.warn(' No sandbox containers');
52
47
  }
53
48
  else {
54
- const tableRows = ordered.map((row) => ({
55
- index: row.index === null ? '' : String(row.index),
56
- name: row.name,
57
- status: row.status,
58
- branch: row.branch
59
- }));
60
- for (const line of formatContainerTable(tableRows)) {
49
+ const tableRows = ordered.map((container, i) => {
50
+ const shortId = container.branch ? lookupShortIdByBranch(container.branch, config.repoRoot) : null;
51
+ return {
52
+ row: String(i + 1),
53
+ shortId: shortId ?? '-',
54
+ name: container.name,
55
+ status: container.status,
56
+ branch: container.branch
57
+ };
58
+ });
59
+ for (const line of formatContainerTable(tableRows, Boolean(process.stdout.isTTY))) {
61
60
  process.stdout.write(` ${line}\n`);
62
61
  }
62
+ process.stdout.write(` Total: ${ordered.length} containers\n`);
63
+ if (tableRows.some((r) => r.shortId === '-')) {
64
+ process.stdout.write(` SHORT '-' = no active task bound; that sandbox is free to remove with 'ai sandbox rm <branch>'.\n`);
65
+ }
63
66
  }
64
67
  p.log.step('Worktrees');
65
68
  const worktrees = listChildren(config.worktreeBase);
@@ -0,0 +1,36 @@
1
+ import { loadConfig } from "../config.js";
2
+ import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel } from "../constants.js";
3
+ import { detectEngine } from "../engine.js";
4
+ import { fetchSandboxRows, resolveBranchArg, selectSandboxContainer, startSandboxContainer } from "./list-running.js";
5
+ const USAGE = `Usage: ai sandbox start <branch | TASK-id | N | '#N'>
6
+
7
+ Start an existing sandbox container that has stopped (for example after the
8
+ Docker daemon was restarted or replaced). The container must already exist:
9
+ if none is found, run 'ai sandbox create <branch>' first. A container that is
10
+ already running is left untouched.`;
11
+ export async function start(args) {
12
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
13
+ process.stdout.write(`${USAGE}\n`);
14
+ if (args.length === 0) {
15
+ process.exitCode = 1;
16
+ }
17
+ return;
18
+ }
19
+ const [firstArg = ''] = args;
20
+ const config = loadConfig();
21
+ const engine = detectEngine(config);
22
+ const branch = resolveBranchArg(firstArg, { repoRoot: config.repoRoot });
23
+ assertValidBranchName(branch);
24
+ const { running, nonRunning } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
25
+ const found = selectSandboxContainer([...running, ...nonRunning], containerNameCandidates(config, branch));
26
+ if (!found) {
27
+ throw new Error(`No sandbox container for branch '${branch}'. Run 'ai sandbox create ${branch}' to create one.`);
28
+ }
29
+ if (found.running) {
30
+ process.stdout.write(`Sandbox '${found.name}' is already running.\n`);
31
+ return;
32
+ }
33
+ startSandboxContainer(engine, found.name);
34
+ process.stdout.write(`Started sandbox '${found.name}'.\n`);
35
+ }
36
+ //# sourceMappingURL=start.js.map
@@ -2,9 +2,16 @@ 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 | '#N'> [cmd...]
6
- Enter sandbox or run a command (use leftmost '#' column from 'ls')
7
- ls List sandboxes for the current project
5
+ exec <branch | TASK-id | N | '#N'> [cmd...]
6
+ Enter sandbox or run a command. N (bare) is the
7
+ recommended form for task short ids (e.g.
8
+ 'ai sandbox exec 11'); '#N' is also accepted.
9
+ start <branch | TASK-id | N | '#N'>
10
+ Start an existing stopped sandbox container
11
+ (e.g. after the Docker daemon restarted)
12
+ ls List sandboxes for the current project (the '#'
13
+ column is a display-only row number; the 'SHORT'
14
+ column shows the active task short id, '-' if none)
8
15
  prune [--dry-run] Remove orphaned per-branch state dirs
9
16
  rebuild [--quiet] [--refresh]
10
17
  Rebuild the sandbox image (--refresh pulls base + tools)
@@ -46,6 +53,11 @@ export async function runSandbox(args) {
46
53
  }
47
54
  break;
48
55
  }
56
+ case 'start': {
57
+ const { start } = await import("./commands/start.js");
58
+ await start(rest);
59
+ break;
60
+ }
49
61
  case 'ls': {
50
62
  const { ls } = await import("./commands/ls.js");
51
63
  ls(rest);
@@ -2,7 +2,7 @@ import { spawnSync } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
5
- const SHORT_ID_RE = /^#\d+$/;
5
+ const SHORT_ID_RE = /^#?\d+$/;
6
6
  const WORKSPACE_DIRS = ['active', 'completed', 'blocked', 'archive'];
7
7
  function resolveShortIdStrict(arg, repoRoot) {
8
8
  const scriptPath = path.join(repoRoot, '.agents', 'scripts', 'task-short-id.js');
@@ -6,7 +6,7 @@ function createBuiltinTools(home, project) {
6
6
  'claude-code': {
7
7
  id: 'claude-code',
8
8
  name: 'Claude Code',
9
- install: { type: 'npm', cmd: '@anthropic-ai/claude-code@stable' },
9
+ install: { type: 'npm', cmd: '@anthropic-ai/claude-code@latest' },
10
10
  sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'claude-code'),
11
11
  containerMount: '/home/devuser/.claude',
12
12
  versionCmd: 'claude --version',
@@ -0,0 +1,38 @@
1
+ import pc from 'picocolors';
2
+ function formatTable(headers, rows, options = {}) {
3
+ const { zebra = false } = options;
4
+ const columnCount = headers.length;
5
+ const widths = headers.map((header, i) => {
6
+ const headerLen = header.length;
7
+ let max = headerLen;
8
+ for (const row of rows) {
9
+ const cell = row[i] ?? '';
10
+ if (cell.length > max)
11
+ max = cell.length;
12
+ }
13
+ return max;
14
+ });
15
+ const renderRow = (values) => {
16
+ const parts = [];
17
+ for (let i = 0; i < columnCount; i += 1) {
18
+ const cell = values[i] ?? '';
19
+ if (i === columnCount - 1) {
20
+ parts.push(cell);
21
+ }
22
+ else {
23
+ parts.push(cell.padEnd(widths[i]));
24
+ }
25
+ }
26
+ return parts.join(' ').trimEnd();
27
+ };
28
+ const dataLines = rows.map((row, i) => {
29
+ const line = renderRow(row);
30
+ // Zebra stripes: dim even-numbered data rows (rows 2, 4, 6... -> 0-based
31
+ // odd index). The header and odd rows are left untouched. When zebra is
32
+ // off, pc.dim is never called, so the output is byte-identical to before.
33
+ return zebra && i % 2 === 1 ? pc.dim(line) : line;
34
+ });
35
+ return [renderRow(headers), ...dataLines];
36
+ }
37
+ export { formatTable };
38
+ //# sourceMappingURL=table.js.map