@fitlab-ai/agent-infra 0.7.7 → 0.8.1

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 (142) hide show
  1. package/README.md +2 -0
  2. package/README.zh-CN.md +2 -0
  3. package/bin/cli.ts +42 -15
  4. package/dist/bin/cli.js +46 -16
  5. package/dist/lib/decide.js +110 -0
  6. package/dist/lib/run/host.js +39 -0
  7. package/dist/lib/run/index.js +193 -0
  8. package/dist/lib/run/prompt.js +2 -0
  9. package/dist/lib/run/skills.js +29 -0
  10. package/dist/lib/run/tui.js +35 -0
  11. package/dist/lib/sandbox/capture.js +158 -0
  12. package/dist/lib/sandbox/commands/create.js +58 -9
  13. package/dist/lib/sandbox/commands/enter.js +7 -3
  14. package/dist/lib/sandbox/credentials.js +42 -3
  15. package/dist/lib/sandbox/runtimes/base.dockerfile +13 -0
  16. package/dist/lib/server/adapters/_contract.js +9 -0
  17. package/dist/lib/server/adapters/feishu/index.js +40 -0
  18. package/dist/lib/server/adapters/feishu/transport.js +95 -0
  19. package/dist/lib/server/auth.js +19 -0
  20. package/dist/lib/server/config.js +183 -0
  21. package/dist/lib/server/daemon.js +96 -0
  22. package/dist/lib/server/index.js +47 -0
  23. package/dist/lib/server/logger.js +34 -0
  24. package/dist/lib/server/plugin-loader.js +73 -0
  25. package/dist/lib/server/process-control.js +200 -0
  26. package/dist/lib/server/protocol.js +69 -0
  27. package/dist/lib/server/redact.js +12 -0
  28. package/dist/lib/server/runner.js +75 -0
  29. package/dist/lib/server/streamer.js +44 -0
  30. package/dist/lib/task/commands/decisions.js +258 -0
  31. package/dist/lib/task/commands/log.js +38 -41
  32. package/dist/lib/task/commands/status.js +144 -45
  33. package/dist/lib/task/index.js +9 -0
  34. package/dist/lib/task/ledger.js +66 -0
  35. package/dist/lib/task/sections.js +30 -1
  36. package/lib/decide.ts +124 -0
  37. package/lib/run/host.ts +47 -0
  38. package/lib/run/index.ts +237 -0
  39. package/lib/run/prompt.ts +1 -0
  40. package/lib/run/skills.ts +34 -0
  41. package/lib/run/tui.ts +40 -0
  42. package/lib/sandbox/capture.ts +215 -0
  43. package/lib/sandbox/commands/create.ts +64 -12
  44. package/lib/sandbox/commands/enter.ts +8 -2
  45. package/lib/sandbox/credentials.ts +57 -3
  46. package/lib/sandbox/runtimes/base.dockerfile +13 -0
  47. package/lib/server/adapters/_contract.ts +42 -0
  48. package/lib/server/adapters/feishu/index.ts +47 -0
  49. package/lib/server/adapters/feishu/transport.ts +128 -0
  50. package/lib/server/auth.ts +28 -0
  51. package/lib/server/config.ts +228 -0
  52. package/lib/server/daemon.ts +110 -0
  53. package/lib/server/index.ts +50 -0
  54. package/lib/server/logger.ts +48 -0
  55. package/lib/server/plugin-loader.ts +84 -0
  56. package/lib/server/process-control.ts +225 -0
  57. package/lib/server/protocol.ts +79 -0
  58. package/lib/server/redact.ts +12 -0
  59. package/lib/server/runner.ts +95 -0
  60. package/lib/server/server.schema.json +127 -0
  61. package/lib/server/streamer.ts +57 -0
  62. package/lib/task/commands/decisions.ts +272 -0
  63. package/lib/task/commands/log.ts +38 -39
  64. package/lib/task/commands/status.ts +200 -58
  65. package/lib/task/index.ts +9 -0
  66. package/lib/task/ledger.ts +73 -0
  67. package/lib/task/sections.ts +30 -1
  68. package/package.json +8 -3
  69. package/templates/.agents/QUICKSTART.zh-CN.md +2 -2
  70. package/templates/.agents/rules/issue-fields.github.en.md +1 -1
  71. package/templates/.agents/rules/issue-fields.github.zh-CN.md +1 -1
  72. package/templates/.agents/rules/next-step-output.en.md +5 -1
  73. package/templates/.agents/rules/next-step-output.zh-CN.md +5 -1
  74. package/templates/.agents/rules/no-mid-flow-questions.en.md +7 -2
  75. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +7 -2
  76. package/templates/.agents/rules/pr-sync.github.en.md +3 -3
  77. package/templates/.agents/rules/pr-sync.github.zh-CN.md +3 -3
  78. package/templates/.agents/rules/review-handshake.en.md +5 -1
  79. package/templates/.agents/rules/review-handshake.zh-CN.md +5 -1
  80. package/templates/.agents/rules/task-management.en.md +2 -2
  81. package/templates/.agents/rules/task-management.zh-CN.md +2 -2
  82. package/templates/.agents/scripts/lib/post-review-commit.js +15 -18
  83. package/templates/.agents/scripts/validate-artifact.js +8 -0
  84. package/templates/.agents/skills/analyze-task/SKILL.en.md +15 -3
  85. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +15 -3
  86. package/templates/.agents/skills/analyze-task/config/verify.en.json +2 -1
  87. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +2 -1
  88. package/templates/.agents/skills/code-task/SKILL.en.md +2 -2
  89. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
  90. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
  91. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
  92. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +3 -3
  93. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +4 -4
  94. package/templates/.agents/skills/complete-task/SKILL.en.md +1 -0
  95. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -0
  96. package/templates/.agents/skills/complete-task/config/verify.en.json +2 -1
  97. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -1
  98. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +1 -1
  99. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +1 -1
  100. package/templates/.agents/skills/create-task/SKILL.en.md +1 -3
  101. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -3
  102. package/templates/.agents/skills/import-codescan/SKILL.en.md +0 -3
  103. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +0 -3
  104. package/templates/.agents/skills/import-dependabot/SKILL.en.md +0 -1
  105. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +0 -1
  106. package/templates/.agents/skills/import-issue/SKILL.en.md +1 -2
  107. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -2
  108. package/templates/.agents/skills/plan-task/SKILL.en.md +1 -2
  109. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -2
  110. package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
  111. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
  112. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -1
  113. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -1
  114. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +11 -11
  115. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +11 -11
  116. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +4 -4
  117. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +4 -4
  118. package/templates/.agents/skills/review-analysis/reference/review-criteria.en.md +5 -5
  119. package/templates/.agents/skills/review-analysis/reference/review-criteria.zh-CN.md +5 -5
  120. package/templates/.agents/skills/review-code/SKILL.en.md +4 -4
  121. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -4
  122. package/templates/.agents/skills/review-code/config/verify.en.json +1 -1
  123. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -1
  124. package/templates/.agents/skills/review-code/reference/output-templates.en.md +16 -16
  125. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +12 -12
  126. package/templates/.agents/skills/review-code/reference/report-template.en.md +4 -4
  127. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +4 -4
  128. package/templates/.agents/skills/review-code/reference/review-criteria.en.md +5 -5
  129. package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +5 -5
  130. package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
  131. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
  132. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -1
  133. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -1
  134. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +11 -11
  135. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +11 -11
  136. package/templates/.agents/skills/review-plan/reference/report-template.en.md +4 -4
  137. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +4 -4
  138. package/templates/.agents/skills/review-plan/reference/review-criteria.en.md +5 -5
  139. package/templates/.agents/skills/review-plan/reference/review-criteria.zh-CN.md +5 -5
  140. package/templates/.agents/templates/task.en.md +3 -3
  141. package/templates/.agents/templates/task.zh-CN.md +3 -3
  142. package/templates/.github/workflows/metadata-sync.yml +0 -18
package/README.md CHANGED
@@ -82,6 +82,7 @@ Every command above works the same way in Claude Code, Codex, Gemini CLI, and Op
82
82
 
83
83
  - **Multi-AI collaboration**: one shared operating model for Claude Code, Codex, Gemini CLI, and OpenCode
84
84
  - **Bootstrap CLI + skill-driven execution**: initialize once, then let AI skills drive day-to-day work
85
+ - **IM command bridge**: route Feishu messages to built-ins, read-only task views, sandbox management, and controlled lifecycle skill runs
85
86
  - **Bilingual project docs**: English-first docs with synchronized Chinese translations
86
87
  - **Template-source architecture**: `templates/` mirrors the rendered project structure
87
88
  - **AI-assisted updates**: template changes can be merged while preserving project-specific customization
@@ -191,6 +192,7 @@ In-depth guides live under [`docs/en/`](./docs/en/README.md):
191
192
  - [Architecture Overview](./docs/en/architecture.md) — bootstrap CLI, end-to-end flow, layered architecture
192
193
  - [Platform Support](./docs/en/platform-support.md) — macOS, Linux, Windows; sandbox engines and resources
193
194
  - [Sandbox](./docs/en/sandbox.md) — sandbox aliases, host-sandbox file exchange, user-level dotfiles channel
195
+ - [Feishu Bridge](./docs/en/feishu-bridge.md) — configure the Feishu long-connection adapter, command routing, permissions, and TUI selection
194
196
  - [Built-in AI Skills](./docs/en/skills.md) — the full skill catalog by use case
195
197
  - [Custom Skills](./docs/en/custom-skills.md) — create and sync project-specific skills
196
198
  - [Custom TUI Configuration](./docs/en/custom-tui.md) — adapt agent-infra to non-built-in AI TUIs
package/README.zh-CN.md CHANGED
@@ -82,6 +82,7 @@ agent-infra 的目标就是把这层共享基础设施标准化。它为所有
82
82
 
83
83
  - **多 AI 协作**:为 Claude Code、Codex、Gemini CLI、OpenCode 提供统一的协作模型
84
84
  - **引导 CLI + skill 驱动执行**:初始化一次,后续日常操作交给 AI skills
85
+ - **IM 命令桥接**:把飞书消息路由到内置命令、任务只读视图、沙箱管理和受控生命周期 skill 执行
85
86
  - **双语文档**:英文为主文档,配套同步的中文版本
86
87
  - **模板源架构**:`templates/` 目录镜像最终渲染出的项目结构
87
88
  - **AI 辅助升级**:模板升级时可合并变更,同时尽量保留项目侧定制
@@ -191,6 +192,7 @@ my-project/
191
192
  - [架构概览](./docs/zh-CN/architecture.md) — 引导 CLI、端到端流程、分层架构
192
193
  - [平台支持](./docs/zh-CN/platform-support.md) — macOS、Linux、Windows;沙箱引擎与资源配置
193
194
  - [沙箱](./docs/zh-CN/sandbox.md) — 沙箱 aliases、宿主-沙箱文件交换、用户级 dotfiles 通道
195
+ - [飞书桥接](./docs/zh-CN/feishu-bridge.md) — 配置飞书长连接 adapter、命令路由、权限和 TUI 选择
194
196
  - [内置 AI Skills](./docs/zh-CN/skills.md) — 按使用场景分组的完整 skill 清单
195
197
  - [自定义 Skills](./docs/zh-CN/custom-skills.md) — 创建并同步项目专属 skill
196
198
  - [自定义 TUI 配置](./docs/zh-CN/custom-tui.md) — 适配非内置的 AI TUI
package/bin/cli.ts CHANGED
@@ -16,10 +16,13 @@ Usage: ai <command> [options]
16
16
 
17
17
  Commands:
18
18
  cp <ssh-alias> Copy local clipboard image to a remote macOS NSPasteboard
19
+ decide Record a human decision for a task HD item
19
20
  help Show this help message
20
21
  init Initialize a new project with update-agent-infra seed command
21
22
  merge Merge tasks from another workspace directory (active/blocked/completed/archive)
23
+ run Schedule a lifecycle skill in the task sandbox tmux session
22
24
  sandbox, s Manage Docker-based AI sandboxes
25
+ server Run the local AI collaboration daemon (start/stop/status/logs)
23
26
  task, t Read-only views over .agents/workspace tasks (cat / files / grep / log / ls / show / status)
24
27
  update Update seed files and sync file registry for an existing project
25
28
  version Show version
@@ -68,21 +71,29 @@ async function importCommand(importPath: string) {
68
71
  }
69
72
 
70
73
  switch (command) {
71
- case 'init': {
72
- const imported = await importCommand('../lib/init.ts');
74
+ case 'cp': {
75
+ const imported = await importCommand('../lib/cp.ts');
73
76
  if (!imported) break;
74
- const { cmdInit } = imported;
75
- await cmdInit().catch((e: unknown) => {
77
+ const { cmdCp } = imported;
78
+ const code = await cmdCp(process.argv.slice(3)).catch((e: unknown) => {
76
79
  process.stderr.write(`Error: ${errorMessage(e)}\n`);
77
- process.exitCode = 1;
80
+ return 1;
78
81
  });
82
+ if (code) process.exitCode = code;
79
83
  break;
80
84
  }
81
- case 'update': {
82
- const imported = await importCommand('../lib/update.ts');
85
+ case 'decide': {
86
+ const imported = await importCommand('../lib/decide.ts');
83
87
  if (!imported) break;
84
- const { cmdUpdate } = imported;
85
- await cmdUpdate().catch((e: unknown) => {
88
+ const { cmdDecide } = imported;
89
+ await cmdDecide(process.argv.slice(3));
90
+ break;
91
+ }
92
+ case 'init': {
93
+ const imported = await importCommand('../lib/init.ts');
94
+ if (!imported) break;
95
+ const { cmdInit } = imported;
96
+ await cmdInit().catch((e: unknown) => {
86
97
  process.stderr.write(`Error: ${errorMessage(e)}\n`);
87
98
  process.exitCode = 1;
88
99
  });
@@ -98,6 +109,13 @@ switch (command) {
98
109
  });
99
110
  break;
100
111
  }
112
+ case 'run': {
113
+ const imported = await importCommand('../lib/run/index.ts');
114
+ if (!imported) break;
115
+ const { cmdRun } = imported;
116
+ await cmdRun(process.argv.slice(3));
117
+ break;
118
+ }
101
119
  case 'sandbox': {
102
120
  const imported = await importCommand('../lib/sandbox/index.ts');
103
121
  if (!imported) break;
@@ -108,6 +126,16 @@ switch (command) {
108
126
  });
109
127
  break;
110
128
  }
129
+ case 'server': {
130
+ const imported = await importCommand('../lib/server/index.ts');
131
+ if (!imported) break;
132
+ const { runServer } = imported;
133
+ await runServer(process.argv.slice(3)).catch((e: unknown) => {
134
+ process.stderr.write(`Error: ${errorMessage(e)}\n`);
135
+ process.exit(1);
136
+ });
137
+ break;
138
+ }
111
139
  case 'task': {
112
140
  const imported = await importCommand('../lib/task/index.ts');
113
141
  if (!imported) break;
@@ -118,15 +146,14 @@ switch (command) {
118
146
  });
119
147
  break;
120
148
  }
121
- case 'cp': {
122
- const imported = await importCommand('../lib/cp.ts');
149
+ case 'update': {
150
+ const imported = await importCommand('../lib/update.ts');
123
151
  if (!imported) break;
124
- const { cmdCp } = imported;
125
- const code = await cmdCp(process.argv.slice(3)).catch((e: unknown) => {
152
+ const { cmdUpdate } = imported;
153
+ await cmdUpdate().catch((e: unknown) => {
126
154
  process.stderr.write(`Error: ${errorMessage(e)}\n`);
127
- return 1;
155
+ process.exitCode = 1;
128
156
  });
129
- if (code) process.exitCode = code;
130
157
  break;
131
158
  }
132
159
  case 'version': {
package/dist/bin/cli.js CHANGED
@@ -20,10 +20,13 @@ Usage: ai <command> [options]
20
20
 
21
21
  Commands:
22
22
  cp <ssh-alias> Copy local clipboard image to a remote macOS NSPasteboard
23
+ decide Record a human decision for a task HD item
23
24
  help Show this help message
24
25
  init Initialize a new project with update-agent-infra seed command
25
26
  merge Merge tasks from another workspace directory (active/blocked/completed/archive)
27
+ run Schedule a lifecycle skill in the task sandbox tmux session
26
28
  sandbox, s Manage Docker-based AI sandboxes
29
+ server Run the local AI collaboration daemon (start/stop/status/logs)
27
30
  task, t Read-only views over .agents/workspace tasks (cat / files / grep / log / ls / show / status)
28
31
  update Update seed files and sync file registry for an existing project
29
32
  version Show version
@@ -66,23 +69,33 @@ async function importCommand(importPath) {
66
69
  }
67
70
  }
68
71
  switch (command) {
69
- case 'init': {
70
- const imported = await importCommand('../lib/init.ts');
72
+ case 'cp': {
73
+ const imported = await importCommand('../lib/cp.ts');
71
74
  if (!imported)
72
75
  break;
73
- const { cmdInit } = imported;
74
- await cmdInit().catch((e) => {
76
+ const { cmdCp } = imported;
77
+ const code = await cmdCp(process.argv.slice(3)).catch((e) => {
75
78
  process.stderr.write(`Error: ${errorMessage(e)}\n`);
76
- process.exitCode = 1;
79
+ return 1;
77
80
  });
81
+ if (code)
82
+ process.exitCode = code;
78
83
  break;
79
84
  }
80
- case 'update': {
81
- const imported = await importCommand('../lib/update.ts');
85
+ case 'decide': {
86
+ const imported = await importCommand('../lib/decide.ts');
82
87
  if (!imported)
83
88
  break;
84
- const { cmdUpdate } = imported;
85
- await cmdUpdate().catch((e) => {
89
+ const { cmdDecide } = imported;
90
+ await cmdDecide(process.argv.slice(3));
91
+ break;
92
+ }
93
+ case 'init': {
94
+ const imported = await importCommand('../lib/init.ts');
95
+ if (!imported)
96
+ break;
97
+ const { cmdInit } = imported;
98
+ await cmdInit().catch((e) => {
86
99
  process.stderr.write(`Error: ${errorMessage(e)}\n`);
87
100
  process.exitCode = 1;
88
101
  });
@@ -99,6 +112,14 @@ switch (command) {
99
112
  });
100
113
  break;
101
114
  }
115
+ case 'run': {
116
+ const imported = await importCommand('../lib/run/index.ts');
117
+ if (!imported)
118
+ break;
119
+ const { cmdRun } = imported;
120
+ await cmdRun(process.argv.slice(3));
121
+ break;
122
+ }
102
123
  case 'sandbox': {
103
124
  const imported = await importCommand('../lib/sandbox/index.ts');
104
125
  if (!imported)
@@ -110,6 +131,17 @@ switch (command) {
110
131
  });
111
132
  break;
112
133
  }
134
+ case 'server': {
135
+ const imported = await importCommand('../lib/server/index.ts');
136
+ if (!imported)
137
+ break;
138
+ const { runServer } = imported;
139
+ await runServer(process.argv.slice(3)).catch((e) => {
140
+ process.stderr.write(`Error: ${errorMessage(e)}\n`);
141
+ process.exit(1);
142
+ });
143
+ break;
144
+ }
113
145
  case 'task': {
114
146
  const imported = await importCommand('../lib/task/index.ts');
115
147
  if (!imported)
@@ -121,17 +153,15 @@ switch (command) {
121
153
  });
122
154
  break;
123
155
  }
124
- case 'cp': {
125
- const imported = await importCommand('../lib/cp.ts');
156
+ case 'update': {
157
+ const imported = await importCommand('../lib/update.ts');
126
158
  if (!imported)
127
159
  break;
128
- const { cmdCp } = imported;
129
- const code = await cmdCp(process.argv.slice(3)).catch((e) => {
160
+ const { cmdUpdate } = imported;
161
+ await cmdUpdate().catch((e) => {
130
162
  process.stderr.write(`Error: ${errorMessage(e)}\n`);
131
- return 1;
163
+ process.exitCode = 1;
132
164
  });
133
- if (code)
134
- process.exitCode = code;
135
165
  break;
136
166
  }
137
167
  case 'version': {
@@ -0,0 +1,110 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execFileSync } from 'node:child_process';
4
+ import { VERSION } from "./version.js";
5
+ import { resolveTaskRef } from "./task/resolve-ref.js";
6
+ const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
7
+ function detectRepoRoot() {
8
+ return execFileSync('git', ['rev-parse', '--show-toplevel'], {
9
+ encoding: 'utf8',
10
+ stdio: ['pipe', 'pipe', 'pipe']
11
+ }).trim();
12
+ }
13
+ function defaultNow() {
14
+ return new Intl.DateTimeFormat('sv-SE', {
15
+ timeZoneName: 'longOffset',
16
+ year: 'numeric',
17
+ month: '2-digit',
18
+ day: '2-digit',
19
+ hour: '2-digit',
20
+ minute: '2-digit',
21
+ second: '2-digit',
22
+ hour12: false
23
+ })
24
+ .format(new Date())
25
+ .replace(' GMT', '');
26
+ }
27
+ function taskPath(repoRoot, ref) {
28
+ if (!TASK_ID_RE.test(ref)) {
29
+ const resolved = resolveTaskRef(ref);
30
+ if (!resolved.ok)
31
+ throw new Error(resolved.message);
32
+ if (!resolved.taskDir.includes(`${path.join('.agents', 'workspace', 'active')}${path.sep}`)) {
33
+ throw new Error(`task ${resolved.taskId} is not active`);
34
+ }
35
+ return resolved.taskMdPath;
36
+ }
37
+ const candidate = path.join(repoRoot, '.agents', 'workspace', 'active', ref, 'task.md');
38
+ if (!fs.existsSync(candidate))
39
+ throw new Error(`active task not found: ${ref}`);
40
+ return candidate;
41
+ }
42
+ function replaceFrontmatterField(content, field, value) {
43
+ const re = new RegExp(`^${field}:.*$`, 'm');
44
+ if (re.test(content))
45
+ return content.replace(re, `${field}: ${value}`);
46
+ return content.replace(/^---\n/, `---\n${field}: ${value}\n`);
47
+ }
48
+ function replaceLedgerRow(content, hdId) {
49
+ const lines = content.split('\n');
50
+ let found = false;
51
+ let pending = false;
52
+ for (let i = 0; i < lines.length; i += 1) {
53
+ const line = lines[i];
54
+ if (!line.trim().startsWith(`| ${hdId} |`))
55
+ continue;
56
+ found = true;
57
+ const cells = line.split('|').slice(1, -1).map((cell) => cell.trim());
58
+ if (cells[4] !== 'needs-human-decision')
59
+ break;
60
+ pending = true;
61
+ cells[4] = 'human-decided';
62
+ cells[5] = `task.md#${hdId}`;
63
+ lines[i] = `| ${cells.join(' | ')} |`;
64
+ break;
65
+ }
66
+ return { content: lines.join('\n'), found, pending };
67
+ }
68
+ function appendUnderHeading(content, heading, block) {
69
+ if (!content.includes(`${heading}\n`)) {
70
+ return `${content.trimEnd()}\n\n${heading}\n\n${block}\n`;
71
+ }
72
+ const idx = content.indexOf(`${heading}\n`) + heading.length + 1;
73
+ const before = content.slice(0, idx);
74
+ const after = content.slice(idx);
75
+ return `${before}\n${block}\n${after.replace(/^\n/, '')}`;
76
+ }
77
+ export async function decide(args, options = {}) {
78
+ const [taskRef, hdId, ...decisionParts] = args;
79
+ if (!taskRef || !hdId || decisionParts.length === 0) {
80
+ process.stderr.write('Usage: ai decide <task-ref> <HD-id> <decision>\n');
81
+ return 1;
82
+ }
83
+ try {
84
+ const repoRoot = options.repoRoot ?? detectRepoRoot();
85
+ const file = taskPath(repoRoot, taskRef);
86
+ let content = fs.readFileSync(file, 'utf8');
87
+ const replaced = replaceLedgerRow(content, hdId);
88
+ if (!replaced.found)
89
+ throw new Error(`${hdId} not found in review ledger`);
90
+ if (!replaced.pending)
91
+ throw new Error(`${hdId} is not needs-human-decision`);
92
+ content = replaced.content;
93
+ const now = (options.now ?? defaultNow)();
94
+ content = replaceFrontmatterField(content, 'updated_at', now);
95
+ content = replaceFrontmatterField(content, 'agent_infra_version', options.version ?? VERSION);
96
+ const decision = decisionParts.join(' ');
97
+ content = appendUnderHeading(content, '## 人工裁决', `### ${hdId}\n\n- **裁决时间**:${now}\n- **裁决结果**:${decision}`);
98
+ content = appendUnderHeading(content, '## 活动日志', `- ${now} — **Human Decision** by human — ${hdId} decided`);
99
+ fs.writeFileSync(file, content);
100
+ return 0;
101
+ }
102
+ catch (error) {
103
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
104
+ return 1;
105
+ }
106
+ }
107
+ export async function cmdDecide(args) {
108
+ process.exitCode = await decide(args);
109
+ }
110
+ //# sourceMappingURL=decide.js.map
@@ -0,0 +1,39 @@
1
+ import { spawn } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ function resolveCommand(file) {
5
+ if (process.platform !== 'win32' || path.extname(file)) {
6
+ return file;
7
+ }
8
+ const pathValue = process.env.Path || process.env.PATH || '';
9
+ const extensions = (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean);
10
+ for (const dir of pathValue.split(path.delimiter).filter(Boolean)) {
11
+ for (const extension of extensions) {
12
+ const lowerCandidate = path.join(dir, `${file}${extension.toLowerCase()}`);
13
+ if (fs.existsSync(lowerCandidate))
14
+ return lowerCandidate;
15
+ const upperCandidate = path.join(dir, `${file}${extension.toUpperCase()}`);
16
+ if (fs.existsSync(upperCandidate))
17
+ return upperCandidate;
18
+ }
19
+ }
20
+ return file;
21
+ }
22
+ function needsShell(file) {
23
+ return process.platform === 'win32' && /\.(?:bat|cmd)$/i.test(file);
24
+ }
25
+ export async function runHostCommand(command) {
26
+ const [file, ...args] = command;
27
+ if (!file)
28
+ throw new Error('run: missing command');
29
+ return new Promise((resolve, reject) => {
30
+ const resolvedFile = resolveCommand(file);
31
+ const child = spawn(resolvedFile, args, {
32
+ stdio: ['ignore', 'inherit', 'inherit'],
33
+ shell: needsShell(resolvedFile)
34
+ });
35
+ child.on('error', reject);
36
+ child.on('close', (exitCode, signal) => resolve({ exitCode, signal }));
37
+ });
38
+ }
39
+ //# sourceMappingURL=host.js.map
@@ -0,0 +1,193 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { loadServerConfig } from "../server/config.js";
4
+ import { resolveTaskBranch } from "../sandbox/task-resolver.js";
5
+ import { createRunId, runInSandbox } from "../sandbox/capture.js";
6
+ import { loadShortIdByTaskId, normalizeShortIdInput } from "../task/short-id.js";
7
+ import { buildTuiCommand, renderPrompt, selectTui } from "./tui.js";
8
+ import { getSkillRunSpec } from "./skills.js";
9
+ import { runHostCommand } from "./host.js";
10
+ const USAGE = `Usage: ai run <skill> [task-ref] [args...] [--tui <name>]
11
+
12
+ Task skills are scheduled inside the sandbox tmux session; ai run returns once
13
+ the tmux window is created.
14
+
15
+ Examples:
16
+ ai run create-task "describe the task" --tui codex
17
+ ai run code-task #7 --tui codex`;
18
+ function extractTui(args) {
19
+ const rest = [];
20
+ let tui = null;
21
+ for (let i = 0; i < args.length; i += 1) {
22
+ const arg = args[i];
23
+ if (arg === '--tui') {
24
+ const value = args[i + 1];
25
+ if (!value)
26
+ throw new Error('--tui requires a value');
27
+ tui = value;
28
+ i += 1;
29
+ continue;
30
+ }
31
+ rest.push(arg);
32
+ }
33
+ return { rest, tui };
34
+ }
35
+ export function parseRunArgs(args) {
36
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
37
+ throw new Error(USAGE);
38
+ }
39
+ const { rest, tui } = extractTui(args);
40
+ const [skill = '', maybeTaskRef, ...remaining] = rest;
41
+ const spec = getSkillRunSpec(skill);
42
+ if (!spec)
43
+ throw new Error(`Unknown skill '${skill}'`);
44
+ if (spec.kind === 'create') {
45
+ const createArgs = rest.slice(1);
46
+ if (createArgs.length === 0)
47
+ throw new Error('create-task requires a description');
48
+ return { skill, taskRef: null, args: createArgs, tui };
49
+ }
50
+ if (!maybeTaskRef)
51
+ throw new Error(`${skill} requires a task-ref`);
52
+ return { skill, taskRef: maybeTaskRef, args: remaining, tui };
53
+ }
54
+ function assertAllowedByConfig(skill, commandConfig) {
55
+ const allowed = commandConfig.allowedSkills;
56
+ if (!Array.isArray(allowed))
57
+ return;
58
+ if (!allowed.every((entry) => typeof entry === 'string')) {
59
+ throw new Error('command.allowedSkills must be an array of skill names');
60
+ }
61
+ if (!allowed.includes(skill)) {
62
+ throw new Error(`Skill '${skill}' is not allowed by command.allowedSkills`);
63
+ }
64
+ }
65
+ const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
66
+ function readShortIdLength(repoRoot) {
67
+ try {
68
+ const cfg = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8'));
69
+ const value = cfg?.task?.shortIdLength;
70
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 1)
71
+ return value;
72
+ }
73
+ catch {
74
+ // Use the project default when config is absent in lightweight tests.
75
+ }
76
+ return 2;
77
+ }
78
+ function readActiveShortIdRegistry(repoRoot) {
79
+ const registryPath = path.join(repoRoot, '.agents', 'workspace', 'active', '.short-ids.json');
80
+ try {
81
+ const data = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
82
+ return data && typeof data === 'object' && data.ids && typeof data.ids === 'object' ? data.ids : {};
83
+ }
84
+ catch {
85
+ return {};
86
+ }
87
+ }
88
+ function resolveActiveTaskIdentity(taskRef, repoRoot) {
89
+ let taskId = null;
90
+ let resolvedTaskRef = taskRef;
91
+ if (TASK_ID_RE.test(taskRef)) {
92
+ taskId = taskRef;
93
+ resolvedTaskRef = loadShortIdByTaskId(repoRoot).get(taskId) ?? taskRef;
94
+ }
95
+ else {
96
+ const normalized = normalizeShortIdInput(taskRef, { shortIdLength: readShortIdLength(repoRoot) });
97
+ if (normalized.kind !== 'shortId')
98
+ return null;
99
+ resolvedTaskRef = normalized.value;
100
+ taskId = readActiveShortIdRegistry(repoRoot)[normalized.value.slice(1)] ?? null;
101
+ }
102
+ if (!taskId)
103
+ return null;
104
+ const taskDir = path.join(repoRoot, '.agents', 'workspace', 'active', taskId);
105
+ if (!fs.existsSync(path.join(taskDir, 'task.md')))
106
+ return null;
107
+ return { taskId, taskDir, taskRef: resolvedTaskRef };
108
+ }
109
+ function formatLocalTimestamp(date = new Date()) {
110
+ const pad = (n) => String(n).padStart(2, '0');
111
+ const year = date.getFullYear();
112
+ const month = pad(date.getMonth() + 1);
113
+ const day = pad(date.getDate());
114
+ const hour = pad(date.getHours());
115
+ const minute = pad(date.getMinutes());
116
+ const second = pad(date.getSeconds());
117
+ const offsetMinutes = -date.getTimezoneOffset();
118
+ const sign = offsetMinutes >= 0 ? '+' : '-';
119
+ const offsetHour = pad(Math.floor(Math.abs(offsetMinutes) / 60));
120
+ const offsetMinute = pad(Math.abs(offsetMinutes) % 60);
121
+ return `${year}-${month}-${day} ${hour}:${minute}:${second}${sign}${offsetHour}:${offsetMinute}`;
122
+ }
123
+ function writeRunRecord(params) {
124
+ const runsDir = path.join(params.identity.taskDir, 'runs');
125
+ fs.mkdirSync(runsDir, { recursive: true });
126
+ const record = {
127
+ version: 1,
128
+ run_id: params.run.runId,
129
+ task_id: params.identity.taskId,
130
+ task_ref: params.identity.taskRef,
131
+ branch: params.branch,
132
+ engine: params.run.engine,
133
+ container: params.run.container,
134
+ run_dir: params.run.runDir,
135
+ status_file: `${params.run.runDir}/status`,
136
+ log_file: `${params.run.runDir}/output.log`,
137
+ created_at: formatLocalTimestamp(),
138
+ command: params.command
139
+ };
140
+ fs.writeFileSync(path.join(runsDir, `${params.run.runId}.json`), `${JSON.stringify(record, null, 2)}\n`, 'utf8');
141
+ }
142
+ export async function runSkill(args, options = {}) {
143
+ const parsed = parseRunArgs(args);
144
+ const config = options.command ? null : loadServerConfig({ rootDir: options.repoRoot });
145
+ const commandConfig = options.command ?? config?.command ?? {};
146
+ assertAllowedByConfig(parsed.skill, commandConfig);
147
+ const tui = selectTui(parsed.skill, { cliTui: parsed.tui, command: commandConfig });
148
+ const promptArgs = parsed.taskRef === null ? parsed.args : [parsed.taskRef, ...parsed.args];
149
+ const prompt = renderPrompt({ tui, skill: parsed.skill, args: promptArgs });
150
+ const [file, argv] = buildTuiCommand(tui, prompt);
151
+ const command = [file, ...argv];
152
+ if (parsed.taskRef === null) {
153
+ const result = await (options.runHost ?? runHostCommand)(command);
154
+ return result.exitCode ?? (result.signal ? 1 : 0);
155
+ }
156
+ const repoRoot = options.repoRoot ?? config?.repoRoot ?? process.cwd();
157
+ const branch = resolveTaskBranch(parsed.taskRef, repoRoot);
158
+ const identity = resolveActiveTaskIdentity(parsed.taskRef, repoRoot);
159
+ const runId = identity ? createRunId() : undefined;
160
+ const writeStdout = options.writeStdout ?? ((chunk) => process.stdout.write(chunk));
161
+ const writeStderr = options.writeStderr ?? ((chunk) => process.stderr.write(chunk));
162
+ const runSandbox = options.runSandbox ??
163
+ ((request) => runInSandbox(request));
164
+ const result = await runSandbox({ taskRef: parsed.taskRef, branch, command, runId });
165
+ if (result.stdout) {
166
+ writeStdout(result.stdout);
167
+ }
168
+ if (result.stderr) {
169
+ writeStderr(result.stderr);
170
+ }
171
+ if ((result.exitCode ?? (result.signal ? 1 : 0)) === 0 && identity && result.run) {
172
+ writeRunRecord({ identity, run: result.run, branch, command });
173
+ }
174
+ return result.exitCode ?? (result.signal ? 1 : 0);
175
+ }
176
+ export async function cmdRun(args) {
177
+ try {
178
+ const code = await runSkill(args);
179
+ process.exitCode = code;
180
+ }
181
+ catch (error) {
182
+ const message = error instanceof Error ? error.message : String(error);
183
+ if (message.startsWith('Usage:')) {
184
+ process.stdout.write(`${message}\n`);
185
+ process.exitCode = args.length === 0 ? 1 : 0;
186
+ }
187
+ else {
188
+ process.stderr.write(`Error: ${message}\n`);
189
+ process.exitCode = 1;
190
+ }
191
+ }
192
+ }
193
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,2 @@
1
+ export { renderPrompt } from "./tui.js";
2
+ //# sourceMappingURL=prompt.js.map
@@ -0,0 +1,29 @@
1
+ const TASK_SKILLS = new Set([
2
+ 'analyze-task',
3
+ 'block-task',
4
+ 'cancel-task',
5
+ 'code-task',
6
+ 'commit',
7
+ 'complete-task',
8
+ 'create-pr',
9
+ 'plan-task',
10
+ 'review-analysis',
11
+ 'review-code',
12
+ 'review-plan',
13
+ 'test',
14
+ 'test-integration',
15
+ 'watch-pr'
16
+ ]);
17
+ export function getSkillRunSpec(skill) {
18
+ if (skill === 'create-task') {
19
+ return { kind: 'create', skill: 'create-task', role: 'exec', requiresSandbox: false };
20
+ }
21
+ if (TASK_SKILLS.has(skill)) {
22
+ return { kind: 'task', skill, role: 'exec', requiresSandbox: true };
23
+ }
24
+ return null;
25
+ }
26
+ export function allowedSkillNames() {
27
+ return ['create-task', ...TASK_SKILLS].sort();
28
+ }
29
+ //# sourceMappingURL=skills.js.map
@@ -0,0 +1,35 @@
1
+ const TUI_NAMES = new Set(['claude', 'codex', 'gemini', 'opencode']);
2
+ function isTuiName(value) {
3
+ return typeof value === 'string' && TUI_NAMES.has(value);
4
+ }
5
+ export function selectTui(skill, options) {
6
+ if (isTuiName(options.cliTui))
7
+ return options.cliTui;
8
+ const defaults = options.command?.skillTuiDefaults;
9
+ if (defaults && typeof defaults === 'object' && !Array.isArray(defaults)) {
10
+ const value = defaults[skill];
11
+ if (isTuiName(value))
12
+ return value;
13
+ }
14
+ if (isTuiName(options.command?.defaultTui))
15
+ return options.command.defaultTui;
16
+ return 'codex';
17
+ }
18
+ export function renderPrompt(params) {
19
+ const suffix = [params.skill, ...params.args].join(' ').trim();
20
+ if (params.tui === 'codex')
21
+ return `$${suffix}`;
22
+ if (params.tui === 'gemini')
23
+ return `/agent-infra:${suffix}`;
24
+ return `/${suffix}`;
25
+ }
26
+ export function buildTuiCommand(tui, prompt) {
27
+ if (tui === 'claude')
28
+ return ['claude', ['--dangerously-skip-permissions', '--print', prompt]];
29
+ if (tui === 'gemini')
30
+ return ['gemini', ['--approval-mode', 'yolo', '--prompt', prompt]];
31
+ if (tui === 'opencode')
32
+ return ['opencode', ['run', '--dangerously-skip-permissions', prompt]];
33
+ return ['codex', ['exec', '--dangerously-bypass-approvals-and-sandbox', prompt]];
34
+ }
35
+ //# sourceMappingURL=tui.js.map