@fitlab-ai/agent-infra 0.7.0 → 0.7.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 (73) hide show
  1. package/bin/cli.ts +1 -1
  2. package/dist/bin/cli.js +1 -1
  3. package/dist/lib/builtin-tuis.js +45 -0
  4. package/dist/lib/defaults.json +3 -0
  5. package/dist/lib/init.js +62 -23
  6. package/dist/lib/prompt.js +49 -1
  7. package/dist/lib/sandbox/commands/enter.js +1 -1
  8. package/dist/lib/sandbox/commands/list-running.js +58 -13
  9. package/dist/lib/sandbox/commands/rebuild.js +3 -11
  10. package/dist/lib/sandbox/commands/rm.js +2 -0
  11. package/dist/lib/sandbox/image-prune.js +18 -0
  12. package/dist/lib/sandbox/task-resolver.js +18 -0
  13. package/dist/lib/update.js +59 -18
  14. package/lib/builtin-tuis.ts +55 -0
  15. package/lib/defaults.json +3 -0
  16. package/lib/init.ts +87 -35
  17. package/lib/prompt.ts +54 -1
  18. package/lib/sandbox/commands/enter.ts +1 -1
  19. package/lib/sandbox/commands/list-running.ts +69 -16
  20. package/lib/sandbox/commands/rebuild.ts +3 -12
  21. package/lib/sandbox/commands/rm.ts +3 -0
  22. package/lib/sandbox/image-prune.ts +23 -0
  23. package/lib/sandbox/task-resolver.ts +23 -1
  24. package/lib/update.ts +71 -30
  25. package/package.json +1 -1
  26. package/templates/.agents/README.en.md +32 -0
  27. package/templates/.agents/README.zh-CN.md +32 -0
  28. package/templates/.agents/rules/task-short-id.en.md +141 -0
  29. package/templates/.agents/rules/task-short-id.zh-CN.md +124 -0
  30. package/templates/.agents/scripts/task-short-id.js +713 -0
  31. package/templates/.agents/skills/analyze-task/SKILL.en.md +4 -0
  32. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +4 -1
  33. package/templates/.agents/skills/block-task/SKILL.en.md +12 -0
  34. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +12 -1
  35. package/templates/.agents/skills/cancel-task/SKILL.en.md +12 -0
  36. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +12 -1
  37. package/templates/.agents/skills/check-task/SKILL.en.md +4 -0
  38. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +4 -1
  39. package/templates/.agents/skills/close-codescan/SKILL.en.md +11 -0
  40. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +11 -0
  41. package/templates/.agents/skills/close-dependabot/SKILL.en.md +11 -0
  42. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +11 -0
  43. package/templates/.agents/skills/code-task/SKILL.en.md +4 -0
  44. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +4 -1
  45. package/templates/.agents/skills/commit/SKILL.en.md +4 -0
  46. package/templates/.agents/skills/commit/SKILL.zh-CN.md +4 -0
  47. package/templates/.agents/skills/complete-task/SKILL.en.md +12 -0
  48. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +12 -1
  49. package/templates/.agents/skills/create-pr/SKILL.en.md +4 -0
  50. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -0
  51. package/templates/.agents/skills/create-task/SKILL.en.md +14 -0
  52. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +14 -1
  53. package/templates/.agents/skills/import-codescan/SKILL.en.md +14 -0
  54. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +14 -0
  55. package/templates/.agents/skills/import-dependabot/SKILL.en.md +14 -0
  56. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +14 -0
  57. package/templates/.agents/skills/import-issue/SKILL.en.md +14 -0
  58. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +14 -0
  59. package/templates/.agents/skills/plan-task/SKILL.en.md +4 -0
  60. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -1
  61. package/templates/.agents/skills/restore-task/SKILL.en.md +12 -0
  62. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +12 -1
  63. package/templates/.agents/skills/review-analysis/SKILL.en.md +4 -0
  64. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +4 -1
  65. package/templates/.agents/skills/review-code/SKILL.en.md +4 -0
  66. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -1
  67. package/templates/.agents/skills/review-plan/SKILL.en.md +4 -0
  68. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +4 -1
  69. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +1 -0
  70. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +1 -0
  71. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +112 -21
  72. package/templates/.agents/templates/task.en.md +1 -0
  73. package/templates/.agents/templates/task.zh-CN.md +1 -0
package/lib/update.ts CHANGED
@@ -3,6 +3,8 @@ import path from 'node:path';
3
3
  import { info, ok, err } from './log.ts';
4
4
  import { resolveTemplateDir } from './paths.ts';
5
5
  import { renderFile, copySkillDir, KNOWN_PLATFORMS } from './render.ts';
6
+ import { isPathOwnedByDisabledTUI, resolveEnabledTUIs } from './builtin-tuis.ts';
7
+ import type { BuiltinTUIId } from './builtin-tuis.ts';
6
8
 
7
9
  type FileRegistry = {
8
10
  managed: string[];
@@ -17,14 +19,17 @@ type UpdateConfig = {
17
19
  platform?: { type?: string };
18
20
  requiresPullRequest?: boolean;
19
21
  sandbox?: Record<string, unknown>;
22
+ task?: { shortIdLength: number };
20
23
  labels?: Record<string, unknown>;
21
24
  files?: Partial<FileRegistry>;
25
+ tuis?: unknown;
22
26
  };
23
27
 
24
28
  type Defaults = {
25
29
  platform: { type: string };
26
30
  requiresPullRequest: boolean;
27
31
  sandbox: Record<string, unknown>;
32
+ task: { shortIdLength: number };
28
33
  labels: Record<string, unknown>;
29
34
  files: FileRegistry;
30
35
  };
@@ -45,7 +50,7 @@ function isPathOwnedByOtherPlatform(relativePath: string, platformType: string):
45
50
  return candidate !== platformType;
46
51
  }
47
52
 
48
- function syncFileRegistry(config: UpdateConfig, platformType: string) {
53
+ function syncFileRegistry(config: UpdateConfig, platformType: string, enabledTUIs: Set<BuiltinTUIId>) {
49
54
  config.files ||= {};
50
55
  const before = JSON.stringify({
51
56
  files: {
@@ -67,6 +72,7 @@ function syncFileRegistry(config: UpdateConfig, platformType: string) {
67
72
 
68
73
  for (const entry of defaults.files.managed) {
69
74
  if (isPathOwnedByOtherPlatform(entry, platformType)) continue;
75
+ if (isPathOwnedByDisabledTUI(entry, enabledTUIs)) continue;
70
76
  if (!allExisting.includes(entry)) {
71
77
  config.files.managed.push(entry);
72
78
  added.managed.push(entry);
@@ -74,6 +80,7 @@ function syncFileRegistry(config: UpdateConfig, platformType: string) {
74
80
  }
75
81
  for (const entry of defaults.files.merged) {
76
82
  if (isPathOwnedByOtherPlatform(entry, platformType)) continue;
83
+ if (isPathOwnedByDisabledTUI(entry, enabledTUIs)) continue;
77
84
  if (!allExisting.includes(entry)) {
78
85
  config.files.merged.push(entry);
79
86
  added.merged.push(entry);
@@ -118,6 +125,7 @@ async function cmdUpdate(): Promise<void> {
118
125
  const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')) as UpdateConfig;
119
126
  const { project, org, language } = config;
120
127
  const platformType = config.platform?.type || defaults.platform.type;
128
+ const enabledTUIs = resolveEnabledTUIs(config.tuis);
121
129
  const replacements = { project, org };
122
130
 
123
131
  info(`Updating seed files for: ${project}`);
@@ -150,35 +158,42 @@ async function cmdUpdate(): Promise<void> {
150
158
  // Ignore missing legacy script from pre-ESM installs.
151
159
  }
152
160
 
153
- // update Claude command
154
- renderFile(
155
- path.join(templateDir, '.claude', 'commands', claudeSrc),
156
- path.join('.claude', 'commands', 'update-agent-infra.md'),
157
- replacements
158
- );
159
- ok('Updated .claude/commands/update-agent-infra.md');
161
+ // update Claude command (only if enabled)
162
+ if (enabledTUIs.has('claude-code')) {
163
+ renderFile(
164
+ path.join(templateDir, '.claude', 'commands', claudeSrc),
165
+ path.join('.claude', 'commands', 'update-agent-infra.md'),
166
+ replacements
167
+ );
168
+ ok('Updated .claude/commands/update-agent-infra.md');
169
+ }
160
170
 
161
- // update Gemini command
162
- renderFile(
163
- path.join(templateDir, '.gemini', 'commands', '_project_', geminiSrc),
164
- path.join('.gemini', 'commands', project, 'update-agent-infra.toml'),
165
- replacements
166
- );
167
- ok(`Updated .gemini/commands/${project}/update-agent-infra.toml`);
171
+ // update Gemini command (only if enabled)
172
+ if (enabledTUIs.has('gemini-cli')) {
173
+ renderFile(
174
+ path.join(templateDir, '.gemini', 'commands', '_project_', geminiSrc),
175
+ path.join('.gemini', 'commands', project, 'update-agent-infra.toml'),
176
+ replacements
177
+ );
178
+ ok(`Updated .gemini/commands/${project}/update-agent-infra.toml`);
179
+ }
168
180
 
169
- // update OpenCode command
170
- renderFile(
171
- path.join(templateDir, '.opencode', 'commands', opencodeSrc),
172
- path.join('.opencode', 'commands', 'update-agent-infra.md'),
173
- replacements
174
- );
175
- ok('Updated .opencode/commands/update-agent-infra.md');
181
+ // update OpenCode command (only if enabled)
182
+ if (enabledTUIs.has('opencode')) {
183
+ renderFile(
184
+ path.join(templateDir, '.opencode', 'commands', opencodeSrc),
185
+ path.join('.opencode', 'commands', 'update-agent-infra.md'),
186
+ replacements
187
+ );
188
+ ok('Updated .opencode/commands/update-agent-infra.md');
189
+ }
176
190
 
177
191
  // sync file registry
178
- const { added, changed } = syncFileRegistry(config, platformType);
192
+ const { added, changed } = syncFileRegistry(config, platformType, enabledTUIs);
179
193
  const hasNewEntries = added.managed.length > 0 || added.merged.length > 0;
180
194
  const platformAdded = !config.platform;
181
195
  const sandboxAdded = !config.sandbox;
196
+ const taskAdded = !config.task;
182
197
  const labelsAdded = !config.labels;
183
198
  const requiresPullRequestAdded = config.requiresPullRequest === undefined;
184
199
  let configChanged = changed;
@@ -193,6 +208,11 @@ async function cmdUpdate(): Promise<void> {
193
208
  configChanged = true;
194
209
  }
195
210
 
211
+ if (taskAdded) {
212
+ config.task = structuredClone(defaults.task);
213
+ configChanged = true;
214
+ }
215
+
196
216
  if (labelsAdded) {
197
217
  config.labels = structuredClone(defaults.labels);
198
218
  configChanged = true;
@@ -213,13 +233,16 @@ async function cmdUpdate(): Promise<void> {
213
233
  for (const entry of added.merged) {
214
234
  ok(` merged: ${entry}`);
215
235
  }
216
- } else if (platformAdded || sandboxAdded || labelsAdded || requiresPullRequestAdded) {
236
+ } else if (platformAdded || sandboxAdded || taskAdded || labelsAdded || requiresPullRequestAdded) {
217
237
  if (platformAdded) {
218
238
  info(`Default platform config added to ${CONFIG_PATH}.`);
219
239
  }
220
240
  if (sandboxAdded) {
221
241
  info(`Default sandbox config added to ${CONFIG_PATH}.`);
222
242
  }
243
+ if (taskAdded) {
244
+ info(`Default task.shortIdLength=${defaults.task.shortIdLength} added to ${CONFIG_PATH}.`);
245
+ }
223
246
  if (labelsAdded) {
224
247
  info(`Default labels.in config added to ${CONFIG_PATH}.`);
225
248
  }
@@ -232,6 +255,9 @@ async function cmdUpdate(): Promise<void> {
232
255
  if (hasNewEntries && sandboxAdded) {
233
256
  info(`Default sandbox config added to ${CONFIG_PATH}.`);
234
257
  }
258
+ if (hasNewEntries && taskAdded) {
259
+ info(`Default task.shortIdLength=${defaults.task.shortIdLength} added to ${CONFIG_PATH}.`);
260
+ }
235
261
  if (hasNewEntries && labelsAdded) {
236
262
  info(`Default labels.in config added to ${CONFIG_PATH}.`);
237
263
  }
@@ -249,12 +275,27 @@ async function cmdUpdate(): Promise<void> {
249
275
  console.log('');
250
276
  ok('Seed files updated successfully!');
251
277
  console.log('');
252
- console.log(' Next step: run the full update in your AI TUI:');
253
- console.log('');
254
- console.log(' Claude Code / OpenCode: /update-agent-infra');
255
- console.log(` Gemini CLI: /${project}:update-agent-infra`);
256
- console.log(' Codex CLI: $update-agent-infra');
257
- console.log('');
278
+ if (enabledTUIs.size === 0) {
279
+ console.log(' No built-in TUI enabled (tuis: []).');
280
+ console.log(` Configure "customTUIs" in ${CONFIG_PATH} if needed.`);
281
+ console.log('');
282
+ } else {
283
+ console.log(' Next step: run the full update in your AI TUI:');
284
+ console.log('');
285
+ const claudeOrOpencode: string[] = [];
286
+ if (enabledTUIs.has('claude-code')) claudeOrOpencode.push('Claude Code');
287
+ if (enabledTUIs.has('opencode')) claudeOrOpencode.push('OpenCode');
288
+ if (claudeOrOpencode.length > 0) {
289
+ console.log(` ${claudeOrOpencode.join(' / ')}: /update-agent-infra`);
290
+ }
291
+ if (enabledTUIs.has('gemini-cli')) {
292
+ console.log(` Gemini CLI: /${project}:update-agent-infra`);
293
+ }
294
+ if (enabledTUIs.has('codex')) {
295
+ console.log(' Codex CLI: $update-agent-infra');
296
+ }
297
+ console.log('');
298
+ }
258
299
  }
259
300
 
260
301
  export { cmdUpdate };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fitlab-ai/agent-infra",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Bootstrap tool for AI multi-tool collaboration infrastructure — works with Claude Code, Codex, Gemini CLI, and OpenCode",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -210,6 +210,38 @@ The `files` field in `.agents/.airc.json` groups project files into three catego
210
210
 
211
211
  `ejected` entries support literal paths or globs, using the same matching rules as `merged`.
212
212
 
213
+ ## Built-in TUI Selection
214
+
215
+ Use the top-level `.agents/.airc.json` `tuis` array to pick which built-in TUIs (`claude-code`, `codex`, `gemini-cli`, `opencode`) agent-infra should install command files for and keep in sync.
216
+
217
+ | Value | Meaning |
218
+ |-------|---------|
219
+ | `tuis` missing or `null` | All four built-in TUIs are enabled (backward-compatible default for legacy `.airc.json` predating this field). |
220
+ | `tuis: []` | No built-in TUI is managed. Use this when the project only relies on `customTUIs` and does not need any built-in command files installed. |
221
+ | `tuis: [<subset>]` | Only the listed TUIs are managed. Unknown ids are ignored. |
222
+
223
+ `ai init` includes an interactive multi-select for this field:
224
+
225
+ - Press Enter to accept the default (all built-in TUIs enabled).
226
+ - Type comma-separated numbers or ids (e.g. `1,3` or `claude-code,opencode`) to keep a subset.
227
+ - Type `none` to explicitly disable every built-in TUI (typically combined with a `customTUIs` entry added later).
228
+ - Invalid input (duplicate, out-of-range, unknown id, whitespace-only) aborts init with a non-zero exit code.
229
+
230
+ ### Side effects of disabling a TUI
231
+
232
+ When you disable a built-in TUI (either via `ai init` or by hand-editing `.airc.json`), the next `ai update` / `update-agent-infra` will:
233
+
234
+ - skip seed command writes for that TUI (e.g. `.gemini/commands/<project>/update-agent-infra.toml`);
235
+ - skip the TUI's owned default entries when backfilling `files.managed` / `files.merged`;
236
+ - **clean up existing files** under the TUI's owned path prefix (`.claude/`, `.codex/`, `.gemini/`, `.opencode/`) — these are listed in `report.managed.removed`, mirroring the cleanup behavior when switching `platform`.
237
+
238
+ To opt a specific file out of this cleanup, list it in `files.ejected`; ejected entries owned by disabled TUIs are preserved as-is and are not re-created by sync.
239
+
240
+ ### Relation to other config fields
241
+
242
+ - `tuis` controls **which TUI command files agent-infra writes and maintains**. It is independent from `sandbox.tools`, which controls **which CLIs the sandbox image installs**. Toggling one does not affect the other; the README for `sandbox.tools` lives in the Sandbox section.
243
+ - `tuis` is independent from `customTUIs` (see below). CustomTUI command files are not removed when you disable a built-in TUI, even if the customTUI's `dir` falls under that TUI's owned prefix (e.g. a customTUI configured with `dir: ".codex/commands"` is preserved when `codex` is disabled).
244
+
213
245
  ## Custom TUI Configuration
214
246
 
215
247
  Use the top-level `.agents/.airc.json` `customTUIs` array when your team uses an AI TUI that is not one of the built-in command targets. This config lets agent-infra show the correct next-step commands and generate command files for project custom skills by learning from an existing command in the custom TUI directory.
@@ -210,6 +210,38 @@ args: "<task-id>" # 可选
210
210
 
211
211
  `ejected` 条目支持字面路径或 glob,匹配规则与 `merged` 相同。
212
212
 
213
+ ## 内建 TUI 选择
214
+
215
+ `.agents/.airc.json` 顶层 `tuis` 数组用于决定 agent-infra 应当为哪些内建 TUI(`claude-code`、`codex`、`gemini-cli`、`opencode`)安装并维护命令文件。
216
+
217
+ | 取值 | 含义 |
218
+ |------|------|
219
+ | `tuis` 缺失或为 `null` | 启用全部四个内建 TUI(向后兼容默认,适用于本字段引入之前的 `.airc.json`) |
220
+ | `tuis: []` | 不维护任何内建 TUI。适用于只依赖 `customTUIs`、不需要安装任何内建命令文件的项目 |
221
+ | `tuis: [<子集>]` | 仅维护列出的 TUI;未知 id 会被忽略 |
222
+
223
+ `ai init` 会通过交互式多选询问该字段:
224
+
225
+ - 直接回车 = 接受默认值(全部内建 TUI 启用)。
226
+ - 输入逗号分隔的编号或 id(如 `1,3` 或 `claude-code,opencode`)= 只保留子集。
227
+ - 输入 `none` = 明确不启用任何内建 TUI(通常配合后续在 `customTUIs` 添加条目使用)。
228
+ - 非法输入(重复、超界、未知 id、纯空白)会让 init 以非零退出码终止。
229
+
230
+ ### 取消某个 TUI 的副作用
231
+
232
+ 通过 `ai init` 或手工编辑 `.airc.json` 取消某个内建 TUI 后,下一次 `ai update` / `update-agent-infra` 会:
233
+
234
+ - 跳过该 TUI 的 seed 命令文件写入(例如 `.gemini/commands/<project>/update-agent-infra.toml`);
235
+ - 在回填 `files.managed` / `files.merged` 时跳过该 TUI owned 的默认条目;
236
+ - **物理清理**该 TUI owned 路径前缀(`.claude/`、`.codex/`、`.gemini/`、`.opencode/`)下的已有文件——清理列表会出现在 `report.managed.removed`,与切换 `platform` 时的清理行为一致。
237
+
238
+ 若希望保留某个具体文件,把它加入 `files.ejected`:被 ejected 的、属于已取消 TUI 的条目会保持原状,sync 不会重新创建也不会删除。
239
+
240
+ ### 与其他配置字段的关系
241
+
242
+ - `tuis` 控制 **agent-infra 写入与维护哪些 TUI 的命令文件**,与 `sandbox.tools`(控制**沙箱镜像里安装哪些 CLI**)相互独立。两者互不影响;`sandbox.tools` 的说明见 Sandbox 一节。
243
+ - `tuis` 与 `customTUIs`(见下)相互独立。取消某个内建 TUI 时 customTUI 命令文件不会被清理,即便 customTUI 的 `dir` 落在该 TUI 的 owned 前缀下(例如 `dir: ".codex/commands"` 的 customTUI 在 `codex` 被取消时仍会保留)。
244
+
213
245
  ## 自定义 TUI 配置
214
246
 
215
247
  当团队使用的 AI TUI 不属于内置命令目标时,可以在 `.agents/.airc.json` 顶层配置 `customTUIs` 数组。该配置用于让 agent-infra 输出正确的下一步命令,并通过学习自定义 TUI 目录中的既有命令文件,为项目自定义 skill 生成同格式命令。
@@ -0,0 +1,141 @@
1
+ # Task short id
2
+
3
+ Task short ids let mobile-style SKILL invocations replace the full 22-char
4
+ `TASK-YYYYMMDD-HHMMSS` with `#NN` while a task is active.
5
+
6
+ ## Syntax
7
+
8
+ - Format: `^#\d{shortIdLength}$` (**zero-padded to a fixed width**; with the
9
+ default `shortIdLength=2`, e.g. `#01`, `#07`, `#42`).
10
+ - **Must** be zero-padded to `shortIdLength` digits (default 2: `#1` is a format
11
+ error, use `#01`). This keeps things aligned and touch-typeable.
12
+ - `#00` (or `#0` when `shortIdLength=1`) is reserved and never allocated; digits
13
+ only, no letters.
14
+ - The plain `TASK-…` form keeps working everywhere; `#NN` is an alias, not the
15
+ persisted task id.
16
+
17
+ ## Lifecycle
18
+
19
+ | Action | When | Effect on registry & task.md |
20
+ |------------|-----------------------------------------------------------------------|---------------------------------------------------------------|
21
+ | alloc | `create-task`, `import-issue`, `import-codescan`, `import-dependabot` | Assigns lowest free `#NN`; writes `short_id` into task.md. |
22
+ | resolve | Lifecycle SKILLs (`analyze-task`, `plan-task`, `code-task`, …) | Looks up `#NN` → full task id. Does not allocate. |
23
+ | release | `complete-task`, `cancel-task`, `block-task`, `close-codescan`, `close-dependabot` | Removes the registry entry; leaves task.md `short_id` as a historical value. |
24
+ | re-alloc | `restore-task` | Re-allocates a (possibly new) `#NN` and writes it to task.md. |
25
+
26
+ Short ids are valid only while a task lives in `.agents/workspace/active/`.
27
+ Once it is moved to `completed/`, `blocked/`, or `archive/`, the `#NN` slot is
28
+ freed and may be reused by a new task.
29
+
30
+ ## Configuration
31
+
32
+ ```jsonc
33
+ // .agents/.airc.json
34
+ {
35
+ "task": {
36
+ "shortIdLength": 2 // default; capacity = 99 (#01–#99). Set to 3 for #001–#999.
37
+ }
38
+ }
39
+ ```
40
+
41
+ When all slots for the configured width are in use, `alloc` fails with a clear
42
+ error suggesting either archiving some tasks or raising `task.shortIdLength`.
43
+ There is no silent extension or truncation. Changing `shortIdLength` requires
44
+ archiving all active tasks first (the registry key width depends on it).
45
+
46
+ ## `#NN` resolution scope (split by entrypoint)
47
+
48
+ | Entrypoint | Hit | Miss |
49
+ |-------------------------------------------------------------|----------------------|------------------------------------------------------|
50
+ | SKILL parameter resolver (lifecycle SKILLs) | resolve to full id | **strict error** — short id not found / invalid |
51
+ | `ai sandbox enter '#NN'` / `ai sandbox exec '#NN' …` | resolve to full id | fall back to running-sandbox ls index (`#414`) |
52
+
53
+ `list --verify` is strictly read-only: it reports discrepancies between active
54
+ dir, registry, and `short_id` declared in each `task.md`, but never writes.
55
+
56
+ ## SKILL parameter resolver
57
+
58
+ Any SKILL (alloc / resolve / release / re-alloc lifecycle entry-points) that
59
+ receives a `{task-id}` argument must follow this contract:
60
+
61
+ 1. If `{task-id}` starts with `#`:
62
+
63
+ ```bash
64
+ if [[ "{task-id}" == "#"* ]]; then
65
+ # The script writes the full error message (including "expected #NN
66
+ # (N-digit zero-padded; e.g. '#01')") to stderr; callers only forward the exit.
67
+ task_id=$(node .agents/scripts/task-short-id.js resolve "{task-id}") || exit 1
68
+ else
69
+ task_id="{task-id}"
70
+ fi
71
+ ```
72
+
73
+ 2. Every downstream command treats `{task-id}` as `$task_id` (already the full
74
+ `TASK-YYYYMMDD-HHMMSS` form).
75
+ 3. Error-code semantics for resolve are documented under "Error scenarios"; do
76
+ not reimplement error handling inside each SKILL.
77
+
78
+ ## Storage
79
+
80
+ The short id system persists state in two places that stay in sync at rest:
81
+
82
+ | Location | Written by | Read by | Removed by |
83
+ |---|---|---|---|
84
+ | `.agents/workspace/active/.short-ids.json` (registry) | `alloc` / cold-start migration | `resolve` (authoritative) / `list` / `list --verify` | `release` / cold-start stale cleanup |
85
+ | `short_id` frontmatter field in each task.md | `alloc` / cold-start migration | `list --verify` (consistency check) | **never** (kept as historical value after archive) |
86
+
87
+ **Registry**:
88
+
89
+ - Path: `<repo-root>/.agents/workspace/active/.short-ids.json`
90
+ - Schema: `{ "version": 1, "ids": { "01": "TASK-20260609-192644", "02": "TASK-…" } }`
91
+ - Keys are zero-padded decimal strings of `task.shortIdLength` digits; values are
92
+ full `TASK-…` task ids.
93
+ - Automatically git-ignored (the whole active workspace is ignored; no new
94
+ ignore entry needed).
95
+ - Created on demand by the first `alloc` / `resolve`; an absent file is treated
96
+ as an empty registry.
97
+
98
+ **`short_id` field in task.md**:
99
+
100
+ - Lives in frontmatter, immediately after `id`; formatted `short_id: #01`.
101
+ - Matches the registry key byte-for-byte (including the `#` prefix).
102
+ - After archive (complete-task / cancel-task / block-task / close-*) the
103
+ registry entry is deleted immediately (the short id can be reused), but the
104
+ `short_id` field in task.md is kept as a historical value. The resolver
105
+ trusts the registry only.
106
+ - Cold-start migration: the first `alloc` / `resolve` after an upgrade scans
107
+ the active directory and fills in the missing field for legacy tasks; the
108
+ field write is constrained (does NOT refresh `updated_at` /
109
+ `agent_infra_version` and does NOT append Activity Log).
110
+
111
+ `resolve('#NN')` workflow: ① validate arg matches `^#\d{shortIdLength}$` →
112
+ ② look up `NN` directly as the registry `ids` key → ③ return full task id on
113
+ hit; on miss, exit 1 with the `list --verify` repair hint.
114
+
115
+ ## Error scenarios
116
+
117
+ - **Short id not found**: the registry has no entry for `#NN`. Either the task
118
+ was archived (release freed the slot) or the input is wrong.
119
+ - **Registry corruption** (duplicate registry entries for the same task id, or
120
+ the JSON is unparsable): exit code 2; manual cleanup required.
121
+ - **Parameter format error** (e.g. `#00`, `#abc`, `#`, or `#1` when
122
+ `shortIdLength=2`): exit code 1.
123
+
124
+ ## Cross-TUI quoting
125
+
126
+ Bash treats `#` as a comment marker. Always single-quote: `ai sandbox exec '#03' 'npm test'`.
127
+ Claude Code / Codex / Gemini CLI / OpenCode all forward `#NN` to SKILL
128
+ `ARGUMENTS` literally when quoted.
129
+
130
+ ## Cold-start migration
131
+
132
+ When a project upgrades to a version with this feature, the first call to
133
+ `alloc` / `resolve` runs the cold-start path:
134
+
135
+ - Active tasks whose `task.md` lacks `short_id` get one allocated and written
136
+ back (the only frontmatter mutation; `updated_at` / `agent_infra_version`
137
+ are **not** refreshed and Activity Log is **not** appended).
138
+ - If active task count exceeds `shortIdLength` capacity, the migration aborts
139
+ **before any write** with a capacity error.
140
+ - If a partial write fails midway, `tx.commit()` rolls all task.md files back to
141
+ their original content (including `mtime` / `atime`).
@@ -0,0 +1,124 @@
1
+ # 任务短号
2
+
3
+ 短号让所有 SKILL 在 active 任务生命周期内可以用 `#NN` 替代完整的 22 字符
4
+ `TASK-YYYYMMDD-HHMMSS`。
5
+
6
+ ## 语法
7
+
8
+ - 格式:`^#\d{shortIdLength}$`(**零填充到固定宽度**;默认 `shortIdLength=2` 时
9
+ 形如 `#01`、`#07`、`#42`)。
10
+ - **必须**零填充到 `shortIdLength` 位(默认 2 位:`#1` 视为格式错误,应输入
11
+ `#01`)。这是为了视觉对齐与盲打体验。
12
+ - `#00`(或 `shortIdLength=1` 时 `#0`)保留、永不分配;纯数字、不引入字母。
13
+ - 完整 `TASK-…` 入参在所有路径下行为与现状等价;`#NN` 只是别名,不是持久化任务 ID。
14
+
15
+ ## 生命周期
16
+
17
+ | 动作 | 触发时机 | 注册表 / task.md 效应 |
18
+ |-----------|---------------------------------------------------------------------------------------------|------------------------------------------------------------------|
19
+ | alloc | `create-task`、`import-issue`、`import-codescan`、`import-dependabot` | 分配最小可用 `#NN`,写入 task.md 的 `short_id` 字段。 |
20
+ | resolve | 生命周期 SKILL(`analyze-task` / `plan-task` / `code-task` / `review-*` / `commit` / …) | `#NN` → 完整 task id 查询,不分配。 |
21
+ | release | `complete-task`、`cancel-task`、`block-task`、`close-codescan`、`close-dependabot` | 从注册表移除;task.md 的 `short_id` 字段保留作为历史值。 |
22
+ | re-alloc | `restore-task` | 重新分配(可能与历史不同),写入注册表与 task.md。 |
23
+
24
+ 短号仅在任务处于 `.agents/workspace/active/` 期间有效;任务移动到
25
+ `completed/` / `blocked/` / `archive/` 后短号立即释放,可被新任务复用。
26
+
27
+ ## 配置
28
+
29
+ ```jsonc
30
+ // .agents/.airc.json
31
+ {
32
+ "task": {
33
+ "shortIdLength": 2 // 默认;容量 = 99(#01–#99)。改为 3 时容量 = #001–#999。
34
+ }
35
+ }
36
+ ```
37
+
38
+ 当前位宽容量耗尽时,`alloc` 给出明确错误并建议「归档若干任务」或「调高
39
+ `task.shortIdLength`」两种修复路径;不静默扩位、不静默截断。
40
+ 切换 `shortIdLength` 配置需要先归档所有 active 任务(注册表 key 宽度依赖配置)。
41
+
42
+ ## `#NN` 解析作用域(按入口二分)
43
+
44
+ | 入口 | 注册表命中 | 注册表未命中 |
45
+ |-----------------------------------------------------------|----------------------|--------------------------------------------------------|
46
+ | SKILL 入参解析器(生命周期 SKILL) | 解析为完整 task id | **严格报错** —— 短号不存在 / 格式错误 |
47
+ | `ai sandbox enter '#NN'` / `ai sandbox exec '#NN' …` | 解析为完整 task id | 回退到 running sandbox 的 ls 行号语义(保留 #414 行为)|
48
+
49
+ `list --verify` 严格只读:报告 active 目录 / 注册表 / 各 task.md 的 `short_id`
50
+ 三者差异,但不修改任何状态。
51
+
52
+ ## SKILL 入参解析
53
+
54
+ 任意 SKILL(含 alloc / resolve / release / re-alloc 四类生命周期入口)在收到
55
+ `{task-id}` 入参后,必须按以下契约处理:
56
+
57
+ 1. 如果 `{task-id}` 字面以 `#` 开头:
58
+
59
+ ```bash
60
+ if [[ "{task-id}" == "#"* ]]; then
61
+ # 脚本本身已输出完整错误(含「expected #NN (N-digit zero-padded; e.g. '#01')」);
62
+ # 调用方只需透传退出码
63
+ task_id=$(node .agents/scripts/task-short-id.js resolve "{task-id}") || exit 1
64
+ else
65
+ task_id="{task-id}"
66
+ fi
67
+ ```
68
+
69
+ 2. 后续所有命令把 `{task-id}` 视为 `$task_id`(已是完整 `TASK-YYYYMMDD-HHMMSS` 形式)
70
+ 3. 解析失败的退出码语义参见「错误场景」段;不要在 SKILL 中重写错误处理
71
+
72
+ ## 存储位置
73
+
74
+ 短号系统跨两处持久化状态,二者在稳态时一一对应:
75
+
76
+ | 位置 | 写入时机 | 读取时机 | 删除时机 |
77
+ |---|---|---|---|
78
+ | `.agents/workspace/active/.short-ids.json`(注册表) | `alloc` / 冷启动迁移 | `resolve` 唯一权威源 / `list` / `list --verify` | `release` / 冷启动 stale 清理 |
79
+ | 各 task.md frontmatter 的 `short_id` 字段 | `alloc` / 冷启动迁移 | `list --verify`(比对一致性) | **永不删除**(归档后保留为历史值) |
80
+
81
+ **注册表**:
82
+
83
+ - 路径:`<repo-root>/.agents/workspace/active/.short-ids.json`
84
+ - Schema:`{ "version": 1, "ids": { "01": "TASK-20260609-192644", "02": "TASK-…" } }`
85
+ - key 是零填充到 `task.shortIdLength` 位的字符串,value 是完整 `TASK-…` task id
86
+ - 自动 git ignore(active 工作区整体 ignore;无需新增 ignore 条目)
87
+ - 首次 `alloc` / `resolve` 时按需自动创建;不存在时按空注册表处理
88
+
89
+ **task.md `short_id` 字段**:
90
+
91
+ - 在 frontmatter 中、紧跟 `id` 字段之后;格式 `short_id: #01`
92
+ - 与注册表 key 字面一致(含 `#` 前缀)
93
+ - 归档(complete-task / cancel-task / block-task / close-*)后:注册表 entry 立即
94
+ 删除(短号可被新任务复用),但 task.md `short_id` 字段保留作为历史值。解析器
95
+ 只信任注册表
96
+ - 冷启动迁移:升级 agent-infra 后首次 alloc / resolve 路径会扫描所有 active
97
+ 目录并为缺字段的 task.md 补发短号;补发受字段保护约束(不刷新
98
+ `updated_at` / `agent_infra_version`、不追加 Activity Log)
99
+
100
+ `resolve('#NN')` 工作流:① 校验入参严格匹配 `^#\d{shortIdLength}$` → ② 直接以
101
+ `NN` 作为 key 查注册表 `ids` → ③ 命中返回完整 task id,未命中按 `list --verify`
102
+ 给出修复指引退出 1。
103
+
104
+ ## 错误场景
105
+
106
+ - **短号不存在**:注册表中无 `#NN`。可能是任务已归档(短号已释放)或输入错误。
107
+ - **注册表损坏**(同一 taskId 出现多次或 JSON 无法解析):退出码 2,需人工处理。
108
+ - **参数格式错误**(如 `#00`、`#abc`、`#`、`#1` 当 `shortIdLength=2` 时):退出码 1。
109
+
110
+ ## 跨 TUI 引号要求
111
+
112
+ bash 中 `#` 是注释起始符,必须单引号:`ai sandbox exec '#03' 'npm test'`。
113
+ Claude Code / Codex / Gemini CLI / OpenCode 在加引号时都能把 `#NN` 字面传递到
114
+ SKILL 的 `ARGUMENTS`。
115
+
116
+ ## 冷启动迁移
117
+
118
+ 升级 agent-infra 后,首次 `alloc` / `resolve` 调用会触发冷启动迁移:
119
+
120
+ - 所有 active task.md 缺 `short_id` 字段时自动补发并回写(仅修改 `short_id`
121
+ 一行,不刷新 `updated_at` / `agent_infra_version`,不追加 Activity Log)。
122
+ - 若 active 任务总数超过 `shortIdLength` 容量,**在任何写入之前**报错退出 2。
123
+ - 若 task.md 写入中途失败,`tx.commit()` 按缓存的原内容回滚所有已写文件(含
124
+ `mtime` / `atime` 恢复)。