@fitlab-ai/agent-infra 0.7.1 → 0.7.2

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 (137) hide show
  1. package/bin/cli.ts +11 -0
  2. package/dist/bin/cli.js +12 -0
  3. package/dist/lib/sandbox/commands/create.js +10 -2
  4. package/dist/lib/sandbox/commands/enter.js +8 -7
  5. package/dist/lib/sandbox/commands/list-running.js +21 -32
  6. package/dist/lib/sandbox/commands/ls.js +20 -22
  7. package/dist/lib/sandbox/index.js +7 -3
  8. package/dist/lib/sandbox/task-resolver.js +1 -1
  9. package/dist/lib/sandbox/tools.js +1 -1
  10. package/dist/lib/table.js +29 -0
  11. package/dist/lib/task/commands/ls.js +122 -0
  12. package/dist/lib/task/commands/show.js +135 -0
  13. package/dist/lib/task/frontmatter.js +32 -0
  14. package/dist/lib/task/index.js +41 -0
  15. package/dist/lib/task/short-id.js +80 -0
  16. package/lib/sandbox/commands/create.ts +11 -2
  17. package/lib/sandbox/commands/enter.ts +8 -7
  18. package/lib/sandbox/commands/list-running.ts +23 -37
  19. package/lib/sandbox/commands/ls.ts +25 -25
  20. package/lib/sandbox/index.ts +7 -3
  21. package/lib/sandbox/task-resolver.ts +1 -1
  22. package/lib/sandbox/tools.ts +1 -1
  23. package/lib/table.ts +32 -0
  24. package/lib/task/commands/ls.ts +138 -0
  25. package/lib/task/commands/show.ts +139 -0
  26. package/lib/task/frontmatter.ts +30 -0
  27. package/lib/task/index.ts +44 -0
  28. package/lib/task/short-id.ts +97 -0
  29. package/package.json +1 -1
  30. package/templates/.agents/hooks/auto-resume.sh +87 -0
  31. package/templates/.agents/rules/create-issue.github.en.md +1 -1
  32. package/templates/.agents/rules/create-issue.github.zh-CN.md +1 -1
  33. package/templates/.agents/rules/milestone-inference.github.en.md +4 -1
  34. package/templates/.agents/rules/milestone-inference.github.zh-CN.md +4 -1
  35. package/templates/.agents/rules/next-step-output.en.md +59 -0
  36. package/templates/.agents/rules/next-step-output.zh-CN.md +59 -0
  37. package/templates/.agents/rules/task-short-id.en.md +54 -62
  38. package/templates/.agents/rules/task-short-id.zh-CN.md +35 -54
  39. package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +17 -0
  40. package/templates/.agents/scripts/task-short-id.js +32 -189
  41. package/templates/.agents/skills/analyze-task/SKILL.en.md +10 -12
  42. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +10 -12
  43. package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -1
  44. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -1
  45. package/templates/.agents/skills/block-task/SKILL.en.md +6 -6
  46. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +6 -6
  47. package/templates/.agents/skills/block-task/config/verify.json +1 -1
  48. package/templates/.agents/skills/cancel-task/SKILL.en.md +6 -6
  49. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +6 -6
  50. package/templates/.agents/skills/cancel-task/config/verify.json +1 -1
  51. package/templates/.agents/skills/check-task/SKILL.en.md +12 -10
  52. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +12 -10
  53. package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -6
  54. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -6
  55. package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -6
  56. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -6
  57. package/templates/.agents/skills/code-task/SKILL.en.md +10 -6
  58. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +11 -6
  59. package/templates/.agents/skills/code-task/config/verify.en.json +2 -1
  60. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +2 -1
  61. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +10 -5
  62. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +10 -5
  63. package/templates/.agents/skills/code-task/reference/output-template.en.md +3 -3
  64. package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +3 -3
  65. package/templates/.agents/skills/code-task/reference/report-template.en.md +8 -0
  66. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +8 -0
  67. package/templates/.agents/skills/commit/SKILL.en.md +2 -2
  68. package/templates/.agents/skills/commit/SKILL.zh-CN.md +2 -2
  69. package/templates/.agents/skills/commit/reference/task-status-update.en.md +9 -9
  70. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +9 -9
  71. package/templates/.agents/skills/complete-task/SKILL.en.md +6 -2
  72. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +6 -2
  73. package/templates/.agents/skills/complete-task/config/verify.en.json +1 -1
  74. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -1
  75. package/templates/.agents/skills/create-pr/SKILL.en.md +6 -6
  76. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -6
  77. package/templates/.agents/skills/create-pr/config/verify.json +2 -1
  78. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +1 -1
  79. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +1 -1
  80. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +3 -3
  81. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +3 -3
  82. package/templates/.agents/skills/create-task/SKILL.en.md +17 -17
  83. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +17 -17
  84. package/templates/.agents/skills/create-task/config/verify.json +1 -1
  85. package/templates/.agents/skills/import-codescan/SKILL.en.md +8 -8
  86. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +8 -8
  87. package/templates/.agents/skills/import-codescan/config/verify.json +1 -1
  88. package/templates/.agents/skills/import-dependabot/SKILL.en.md +8 -8
  89. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +8 -8
  90. package/templates/.agents/skills/import-dependabot/config/verify.json +1 -1
  91. package/templates/.agents/skills/import-issue/SKILL.en.md +7 -7
  92. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +7 -7
  93. package/templates/.agents/skills/plan-task/SKILL.en.md +10 -12
  94. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +10 -12
  95. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
  96. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
  97. package/templates/.agents/skills/restore-task/SKILL.en.md +1 -1
  98. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +1 -1
  99. package/templates/.agents/skills/review-analysis/SKILL.en.md +4 -2
  100. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +4 -2
  101. package/templates/.agents/skills/review-analysis/config/verify.en.json +3 -2
  102. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +3 -2
  103. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +15 -15
  104. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +15 -15
  105. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +7 -1
  106. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +7 -1
  107. package/templates/.agents/skills/review-analysis/reference/review-criteria.en.md +2 -0
  108. package/templates/.agents/skills/review-analysis/reference/review-criteria.zh-CN.md +2 -0
  109. package/templates/.agents/skills/review-code/SKILL.en.md +5 -2
  110. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -2
  111. package/templates/.agents/skills/review-code/config/verify.en.json +3 -2
  112. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +3 -2
  113. package/templates/.agents/skills/review-code/reference/output-templates.en.md +9 -9
  114. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +9 -9
  115. package/templates/.agents/skills/review-code/reference/report-template.en.md +7 -1
  116. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +7 -1
  117. package/templates/.agents/skills/review-code/reference/review-criteria.en.md +2 -0
  118. package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +2 -0
  119. package/templates/.agents/skills/review-plan/SKILL.en.md +4 -2
  120. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +4 -2
  121. package/templates/.agents/skills/review-plan/config/verify.en.json +3 -2
  122. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +3 -2
  123. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +15 -15
  124. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +15 -15
  125. package/templates/.agents/skills/review-plan/reference/report-template.en.md +7 -1
  126. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +7 -1
  127. package/templates/.agents/skills/review-plan/reference/review-criteria.en.md +2 -0
  128. package/templates/.agents/skills/review-plan/reference/review-criteria.zh-CN.md +2 -0
  129. package/templates/.agents/templates/task.en.md +0 -1
  130. package/templates/.agents/templates/task.zh-CN.md +0 -1
  131. package/templates/.agents/workflows/bug-fix.en.yaml +1 -1
  132. package/templates/.agents/workflows/bug-fix.zh-CN.yaml +1 -1
  133. package/templates/.agents/workflows/feature-development.en.yaml +1 -1
  134. package/templates/.agents/workflows/feature-development.zh-CN.yaml +1 -1
  135. package/templates/.agents/workflows/refactoring.en.yaml +1 -1
  136. package/templates/.agents/workflows/refactoring.zh-CN.yaml +1 -1
  137. package/templates/.claude/settings.json +11 -0
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)
@@ -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;
@@ -1,5 +1,5 @@
1
1
  import { loadConfig } from "../config.js";
2
- import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel } from "../constants.js";
2
+ import { assertValidBranchName, containerNameCandidates } from "../constants.js";
3
3
  import { detectEngine } from "../engine.js";
4
4
  import { formatCredentialWarnings, formatRemaining, reconcileClaudeCredentials, redactCommandError, validateClaudeCredentialsEnvOverride } from "../credentials.js";
5
5
  import { runInteractiveEngine, runSafeEngine } from "../shell.js";
@@ -7,11 +7,13 @@ import { resolveTaskBranch } from "../task-resolver.js";
7
7
  import { dotfilesCacheDir, materializeDotfiles } from "../dotfiles.js";
8
8
  import { runInteractiveWithClipboardBridge } from "../clipboard/bridge.js";
9
9
  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...]
10
+ import { isTaskShortRef, resolveTaskShortRef } from "./list-running.js";
11
+ const USAGE = `Usage: ai sandbox exec <branch | TASK-id | N | '#N'> [cmd...]
12
12
 
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.`;
13
+ N (bare) and '#N' both reference the same active task short id from
14
+ .agents/workspace/active/.short-ids.json. They resolve only via that
15
+ registry — they do not reference a container's row position in
16
+ 'ai sandbox ls' output.`;
15
17
  const TMUX_ENTRY_PATH = '/usr/local/bin/sandbox-tmux-entry';
16
18
  // Terminal-detection variables that interactive TUIs (e.g. claude-code)
17
19
  // inspect to enable progressive enhancements such as the kitty keyboard
@@ -86,8 +88,7 @@ export async function enter(args) {
86
88
  const [firstArg = '', ...cmd] = args;
87
89
  let branch;
88
90
  if (isTaskShortRef(firstArg)) {
89
- const { running } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
90
- branch = resolveTaskShortRef(firstArg, { running, repoRoot: config.repoRoot });
91
+ branch = resolveTaskShortRef(firstArg, { repoRoot: config.repoRoot });
91
92
  }
92
93
  else {
93
94
  branch = resolveTaskBranch(firstArg, config.repoRoot);
@@ -68,13 +68,14 @@ export function fetchSandboxRows(engine, label, branchKey) {
68
68
  return sortAndIndexSandboxRows(parseSandboxRows(raw, branchKey));
69
69
  }
70
70
  /**
71
- * Returns true iff `arg` is a syntactically valid task short reference ('#N').
71
+ * Returns true iff `arg` is a syntactically valid task short reference.
72
+ * Accepts both bare numeric ('11') and '#'-prefixed ('#11') forms.
72
73
  * Zero IO. Callers MUST use this as the gate before constructing any context
73
74
  * for resolveTaskShortRef — that way non-matching arguments (e.g. '#abc',
74
75
  * '#1.5', '#') never trigger sandbox list IO.
75
76
  */
76
77
  export function isTaskShortRef(arg) {
77
- return /^#\d+$/.test(arg);
78
+ return /^#?\d+$/.test(arg);
78
79
  }
79
80
  /**
80
81
  * Try to resolve a short ref against the global task-short-id registry.
@@ -89,6 +90,9 @@ function tryResolveFromRegistry(arg, repoRoot) {
89
90
  const scriptPath = path.join(repoRoot, '.agents', 'scripts', 'task-short-id.js');
90
91
  if (!fs.existsSync(scriptPath))
91
92
  return { status: 'miss' };
93
+ // Strip leading '#' when forwarding bare-numeric input through the script's CLI.
94
+ // (Script accepts both forms, but this avoids shell quoting confusion in error
95
+ // messages echoed back to the user.)
92
96
  const result = spawnSync('node', [scriptPath, 'resolve', arg], { encoding: 'utf8', cwd: repoRoot });
93
97
  if (result.status !== 0)
94
98
  return { status: 'miss' };
@@ -113,41 +117,26 @@ function tryResolveFromRegistry(arg, repoRoot) {
113
117
  }
114
118
  throw new Error(`Short ref '${arg}' resolved to task ${taskId} but task.md was not found under any workspace dir`);
115
119
  }
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
120
  /**
134
- * Resolve a task short reference ('#N') to a branch name for the sandbox entrypoint.
121
+ /**
122
+ * Resolve a task short reference (bare 'N' or '#N') to a branch name for the
123
+ * sandbox entrypoint.
135
124
  *
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).
125
+ * Resolution: registry-only. Look up the short id in the global task-short-id
126
+ * registry under repoRoot; if hit, read the branch from the matching task.md.
127
+ * On miss (registry empty or short id absent), throw with an actionable
128
+ * message instead of falling back to a container's row position in
129
+ * 'ai sandbox ls' output — that fallback would make the same syntax mean
130
+ * different things depending on `docker ps` state.
141
131
  *
142
132
  * Precondition: callers MUST gate on isTaskShortRef(arg) === true.
143
133
  */
144
134
  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.
150
- }
151
- return resolveByRunningIndex(arg, ctx.running);
135
+ const lookup = tryResolveFromRegistry(arg, ctx.repoRoot);
136
+ if (lookup.status === 'hit')
137
+ return lookup.branch;
138
+ throw new Error(`short ref '${arg}' is not in the active task registry. ` +
139
+ `'#N' and bare N resolve only via the registry (not by row position in 'ai sandbox ls'); ` +
140
+ `use a task short id (e.g. 'ai sandbox exec 11'), a TASK-id, or a branch name.`);
152
141
  }
153
142
  //# sourceMappingURL=list-running.js.map
@@ -6,27 +6,20 @@ 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'];
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
+ const CONTAINER_TABLE_HEADERS = ['#', 'SHORT', 'NAMES', 'STATUS', 'BRANCH'];
17
21
  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
- ];
22
+ return formatTable(CONTAINER_TABLE_HEADERS, rows.map((r) => [r.row, r.shortId, r.name, r.status, r.branch]));
30
23
  }
31
24
  function listChildren(dir) {
32
25
  if (!fs.existsSync(dir)) {
@@ -51,15 +44,20 @@ export function ls(args = []) {
51
44
  p.log.warn(' No sandbox containers');
52
45
  }
53
46
  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
- }));
47
+ const tableRows = ordered.map((container, i) => {
48
+ const shortId = container.branch ? lookupShortIdByBranch(container.branch, config.repoRoot) : null;
49
+ return {
50
+ row: String(i + 1),
51
+ shortId: shortId ?? '-',
52
+ name: container.name,
53
+ status: container.status,
54
+ branch: container.branch
55
+ };
56
+ });
60
57
  for (const line of formatContainerTable(tableRows)) {
61
58
  process.stdout.write(` ${line}\n`);
62
59
  }
60
+ process.stdout.write(` Total: ${ordered.length} containers\n`);
63
61
  }
64
62
  p.log.step('Worktrees');
65
63
  const worktrees = listChildren(config.worktreeBase);
@@ -2,9 +2,13 @@ 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
+ ls List sandboxes for the current project (the '#'
10
+ column is a display-only row number; the 'SHORT'
11
+ column shows the active task short id, '-' if none)
8
12
  prune [--dry-run] Remove orphaned per-branch state dirs
9
13
  rebuild [--quiet] [--refresh]
10
14
  Rebuild the sandbox image (--refresh pulls base + tools)
@@ -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,29 @@
1
+ function formatTable(headers, rows) {
2
+ const columnCount = headers.length;
3
+ const widths = headers.map((header, i) => {
4
+ const headerLen = header.length;
5
+ let max = headerLen;
6
+ for (const row of rows) {
7
+ const cell = row[i] ?? '';
8
+ if (cell.length > max)
9
+ max = cell.length;
10
+ }
11
+ return max;
12
+ });
13
+ const renderRow = (values) => {
14
+ const parts = [];
15
+ for (let i = 0; i < columnCount; i += 1) {
16
+ const cell = values[i] ?? '';
17
+ if (i === columnCount - 1) {
18
+ parts.push(cell);
19
+ }
20
+ else {
21
+ parts.push(cell.padEnd(widths[i]));
22
+ }
23
+ }
24
+ return parts.join(' ').trimEnd();
25
+ };
26
+ return [renderRow(headers), ...rows.map((row) => renderRow(row))];
27
+ }
28
+ export { formatTable };
29
+ //# sourceMappingURL=table.js.map
@@ -0,0 +1,122 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execFileSync } from 'node:child_process';
4
+ import { formatTable } from "../../table.js";
5
+ import { parseTaskFrontmatter, extractTitle } from "../frontmatter.js";
6
+ import { loadShortIdByTaskId } from "../short-id.js";
7
+ const USAGE = `Usage: ai task ls [--all | --blocked | --completed]
8
+
9
+ Lists tasks under .agents/workspace/. Defaults to active tasks only.
10
+ --all Include active + blocked + completed (excludes archive)
11
+ --blocked Only blocked tasks
12
+ --completed Only completed tasks
13
+
14
+ Columns: # (display-only row number) / SHORT (task short id, usable as an argument) / type / status / current_step / branch / title
15
+ `;
16
+ const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
17
+ const TABLE_HEADERS = ['#', 'SHORT', 'TYPE', 'STATUS', 'STEP', 'BRANCH', 'TITLE'];
18
+ function detectRepoRoot() {
19
+ try {
20
+ return execFileSync('git', ['rev-parse', '--show-toplevel'], {
21
+ encoding: 'utf8',
22
+ stdio: ['pipe', 'pipe', 'pipe']
23
+ }).trim();
24
+ }
25
+ catch {
26
+ throw new Error('ai task: current directory is not inside a git repository');
27
+ }
28
+ }
29
+ function parseSelection(args) {
30
+ const positional = args.filter((a) => !a.startsWith('--'));
31
+ if (positional.length > 0) {
32
+ return {
33
+ ok: false,
34
+ message: `ai task ls: unexpected positional argument(s): ${positional.join(' ')}`
35
+ };
36
+ }
37
+ const flags = args.filter((a) => a.startsWith('--'));
38
+ if (flags.length === 0)
39
+ return { ok: true, selection: ['active'] };
40
+ if (flags.length > 1) {
41
+ return {
42
+ ok: false,
43
+ message: 'ai task ls: pass at most one of --all / --blocked / --completed'
44
+ };
45
+ }
46
+ switch (flags[0]) {
47
+ case '--all':
48
+ return { ok: true, selection: ['active', 'blocked', 'completed'] };
49
+ case '--blocked':
50
+ return { ok: true, selection: ['blocked'] };
51
+ case '--completed':
52
+ return { ok: true, selection: ['completed'] };
53
+ default:
54
+ return { ok: false, message: `ai task ls: unknown flag: ${flags[0]}` };
55
+ }
56
+ }
57
+ function collectTasks(repoRoot, state) {
58
+ const dir = path.join(repoRoot, '.agents', 'workspace', state);
59
+ if (!fs.existsSync(dir))
60
+ return [];
61
+ // Short ids live only in the registry and only for active tasks; archived
62
+ // (blocked/completed) tasks have released their short id and render '-'.
63
+ const shortIdByTaskId = state === 'active' ? loadShortIdByTaskId(repoRoot) : new Map();
64
+ const rows = [];
65
+ for (const entry of fs.readdirSync(dir).sort()) {
66
+ if (!TASK_ID_RE.test(entry))
67
+ continue;
68
+ const taskMdPath = path.join(dir, entry, 'task.md');
69
+ if (!fs.existsSync(taskMdPath))
70
+ continue;
71
+ const content = fs.readFileSync(taskMdPath, 'utf8');
72
+ const fm = parseTaskFrontmatter(content);
73
+ const title = extractTitle(content);
74
+ const shortId = shortIdByTaskId.get(entry) ?? '-';
75
+ rows.push({
76
+ shortId,
77
+ type: fm.type ?? '-',
78
+ status: fm.status ?? state,
79
+ step: fm.current_step ?? '-',
80
+ branch: fm.branch ?? '-',
81
+ title: title || fm.id || entry
82
+ });
83
+ }
84
+ return rows;
85
+ }
86
+ function ls(args = []) {
87
+ if (args[0] === '--help' || args[0] === '-h') {
88
+ process.stdout.write(USAGE);
89
+ return;
90
+ }
91
+ const result = parseSelection(args);
92
+ if (!result.ok) {
93
+ process.stderr.write(`${result.message}\n`);
94
+ process.exitCode = 1;
95
+ return;
96
+ }
97
+ const { selection } = result;
98
+ const repoRoot = detectRepoRoot();
99
+ const rows = [];
100
+ for (const state of selection) {
101
+ rows.push(...collectTasks(repoRoot, state));
102
+ }
103
+ if (rows.length === 0) {
104
+ process.stdout.write(`No tasks under .agents/workspace/${selection.join('|')}\n`);
105
+ return;
106
+ }
107
+ const tableRows = rows.map((r, i) => [
108
+ String(i + 1),
109
+ r.shortId,
110
+ r.type,
111
+ r.status,
112
+ r.step,
113
+ r.branch,
114
+ r.title
115
+ ]);
116
+ for (const line of formatTable(TABLE_HEADERS, tableRows)) {
117
+ process.stdout.write(`${line}\n`);
118
+ }
119
+ process.stdout.write(`Total: ${rows.length} tasks\n`);
120
+ }
121
+ export { ls };
122
+ //# sourceMappingURL=ls.js.map
@@ -0,0 +1,135 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execFileSync, spawnSync } from 'node:child_process';
4
+ import { normalizeShortIdInput } from "../short-id.js";
5
+ const USAGE = `Usage: ai task show <N | #N | TASK-id>
6
+
7
+ Prints the task.md content for the matching task.
8
+ N (bare numeric) Recommended; resolves the active short id via the registry.
9
+ '#N' Compatibility form for old commands.
10
+ TASK-YYYYMMDD-HHMMSS Locates a task in active / blocked / completed / archive.
11
+ `;
12
+ const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
13
+ // Flat-structured workspace dirs that hold tasks under `{dir}/{taskId}/task.md`.
14
+ // Note: `archive` uses a three-level YYYY/MM/DD layout and is handled separately.
15
+ const FLAT_WORKSPACE_DIRS = ['active', 'blocked', 'completed'];
16
+ function detectRepoRoot() {
17
+ try {
18
+ return execFileSync('git', ['rev-parse', '--show-toplevel'], {
19
+ encoding: 'utf8',
20
+ stdio: ['pipe', 'pipe', 'pipe']
21
+ }).trim();
22
+ }
23
+ catch {
24
+ throw new Error('ai task: current directory is not inside a git repository');
25
+ }
26
+ }
27
+ function readShortIdLength(repoRoot) {
28
+ try {
29
+ const cfg = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8'));
30
+ const v = cfg?.task?.shortIdLength;
31
+ if (typeof v === 'number' && Number.isFinite(v) && v >= 1)
32
+ return v;
33
+ }
34
+ catch {
35
+ // fall through to default
36
+ }
37
+ return 2;
38
+ }
39
+ function resolveShortIdToTaskId(arg, repoRoot) {
40
+ const scriptPath = path.join(repoRoot, '.agents', 'scripts', 'task-short-id.js');
41
+ if (!fs.existsSync(scriptPath)) {
42
+ throw new Error(`task-short-id.js not found at ${scriptPath}`);
43
+ }
44
+ const result = spawnSync('node', [scriptPath, 'resolve', arg], {
45
+ encoding: 'utf8',
46
+ cwd: repoRoot
47
+ });
48
+ if (result.status !== 0) {
49
+ throw new Error((result.stderr || '').trim() || `failed to resolve '${arg}'`);
50
+ }
51
+ return result.stdout.trim();
52
+ }
53
+ function listSortedNumeric(dir, width) {
54
+ if (!fs.existsSync(dir))
55
+ return [];
56
+ const pattern = new RegExp(`^\\d{${width}}$`);
57
+ return fs
58
+ .readdirSync(dir)
59
+ .filter((entry) => pattern.test(entry))
60
+ .sort()
61
+ .reverse();
62
+ }
63
+ function findInArchive(repoRoot, taskId) {
64
+ // archive-tasks SKILL writes to .agents/workspace/archive/YYYY/MM/DD/{taskId}/task.md
65
+ // where YYYY/MM/DD comes from completed_at (or updated_at fallback) — NOT from
66
+ // the task id's creation date. So we cannot derive the path from taskId alone;
67
+ // walk the bounded YYYY/MM/DD tree instead. Newest-first to favor recent archives.
68
+ const archiveDir = path.join(repoRoot, '.agents', 'workspace', 'archive');
69
+ for (const year of listSortedNumeric(archiveDir, 4)) {
70
+ const yearDir = path.join(archiveDir, year);
71
+ for (const month of listSortedNumeric(yearDir, 2)) {
72
+ const monthDir = path.join(yearDir, month);
73
+ for (const day of listSortedNumeric(monthDir, 2)) {
74
+ const candidate = path.join(monthDir, day, taskId, 'task.md');
75
+ if (fs.existsSync(candidate))
76
+ return candidate;
77
+ }
78
+ }
79
+ }
80
+ return null;
81
+ }
82
+ function findTaskMd(repoRoot, taskId) {
83
+ for (const sub of FLAT_WORKSPACE_DIRS) {
84
+ const candidate = path.join(repoRoot, '.agents', 'workspace', sub, taskId, 'task.md');
85
+ if (fs.existsSync(candidate))
86
+ return candidate;
87
+ }
88
+ return findInArchive(repoRoot, taskId);
89
+ }
90
+ function show(args = []) {
91
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
92
+ process.stdout.write(USAGE);
93
+ if (args.length === 0)
94
+ process.exitCode = 1;
95
+ return;
96
+ }
97
+ const repoRoot = detectRepoRoot();
98
+ const arg = args[0];
99
+ let taskId;
100
+ if (TASK_ID_RE.test(arg)) {
101
+ taskId = arg;
102
+ }
103
+ else {
104
+ const shortIdLength = readShortIdLength(repoRoot);
105
+ const normalized = normalizeShortIdInput(arg, { shortIdLength });
106
+ if (normalized.kind === 'error') {
107
+ process.stderr.write(`ai task show: ${normalized.message}\n`);
108
+ process.exitCode = 1;
109
+ return;
110
+ }
111
+ if (normalized.kind === 'pass') {
112
+ process.stderr.write(`ai task show: '${arg}' is not a valid short id or TASK-id; ` +
113
+ `expected bare digits, '#N', or 'TASK-YYYYMMDD-HHMMSS'\n`);
114
+ process.exitCode = 1;
115
+ return;
116
+ }
117
+ try {
118
+ taskId = resolveShortIdToTaskId(normalized.value, repoRoot);
119
+ }
120
+ catch (e) {
121
+ process.stderr.write(`ai task show: ${e.message}\n`);
122
+ process.exitCode = 1;
123
+ return;
124
+ }
125
+ }
126
+ const taskMdPath = findTaskMd(repoRoot, taskId);
127
+ if (!taskMdPath) {
128
+ process.stderr.write(`ai task show: task ${taskId} not found in active / blocked / completed / archive\n`);
129
+ process.exitCode = 1;
130
+ return;
131
+ }
132
+ process.stdout.write(fs.readFileSync(taskMdPath, 'utf8'));
133
+ }
134
+ export { show };
135
+ //# sourceMappingURL=show.js.map
@@ -0,0 +1,32 @@
1
+ function parseTaskFrontmatter(content) {
2
+ const result = {};
3
+ if (!content.startsWith('---'))
4
+ return result;
5
+ const end = content.indexOf('\n---', 3);
6
+ if (end === -1)
7
+ return result;
8
+ const body = content.slice(3, end);
9
+ for (const rawLine of body.split('\n')) {
10
+ const line = rawLine.replace(/\r$/, '');
11
+ if (!line.trim())
12
+ continue;
13
+ const colon = line.indexOf(':');
14
+ if (colon === -1)
15
+ continue;
16
+ const key = line.slice(0, colon).trim();
17
+ const value = line.slice(colon + 1).trim();
18
+ if (key)
19
+ result[key] = value;
20
+ }
21
+ return result;
22
+ }
23
+ function extractTitle(content) {
24
+ for (const line of content.split('\n')) {
25
+ const m = /^#\s+(?:任务[::]?\s*)?(.+)$/.exec(line.trim());
26
+ if (m && m[1])
27
+ return m[1].trim();
28
+ }
29
+ return '';
30
+ }
31
+ export { parseTaskFrontmatter, extractTitle };
32
+ //# sourceMappingURL=frontmatter.js.map
@@ -0,0 +1,41 @@
1
+ const USAGE = `Usage: ai task <command> [options]
2
+
3
+ Commands:
4
+ ls [--all | --blocked | --completed] List tasks (default: active)
5
+ show <N | #N | TASK-id> Print a task.md
6
+
7
+ Examples:
8
+ ai task ls
9
+ ai task show 11
10
+ ai task show TASK-20260612-162737
11
+
12
+ Run 'ai task <command> --help' for details.`;
13
+ export async function runTask(args) {
14
+ const [subcommand, ...rest] = args;
15
+ if (!subcommand) {
16
+ process.stdout.write(`${USAGE}\n`);
17
+ process.exitCode = 1;
18
+ return;
19
+ }
20
+ if (subcommand === '--help' || subcommand === '-h' || subcommand === 'help') {
21
+ process.stdout.write(`${USAGE}\n`);
22
+ return;
23
+ }
24
+ switch (subcommand) {
25
+ case 'ls': {
26
+ const { ls } = await import("./commands/ls.js");
27
+ ls(rest);
28
+ break;
29
+ }
30
+ case 'show': {
31
+ const { show } = await import("./commands/show.js");
32
+ show(rest);
33
+ break;
34
+ }
35
+ default:
36
+ process.stderr.write(`Unknown task command: ${subcommand}\n\n`);
37
+ process.stdout.write(`${USAGE}\n`);
38
+ process.exitCode = 1;
39
+ }
40
+ }
41
+ //# sourceMappingURL=index.js.map