@fitlab-ai/agent-infra 0.7.5 → 0.7.7

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 (123) hide show
  1. package/bin/cli.ts +11 -3
  2. package/dist/bin/cli.js +10 -3
  3. package/dist/lib/sandbox/commands/create.js +26 -4
  4. package/dist/lib/sandbox/commands/ls.js +4 -33
  5. package/dist/lib/sandbox/commands/show.js +67 -0
  6. package/dist/lib/sandbox/index.js +7 -0
  7. package/dist/lib/sandbox/tools.js +20 -1
  8. package/dist/lib/task/commands/issue-body.js +94 -0
  9. package/dist/lib/task/commands/log.js +139 -6
  10. package/dist/lib/task/index.js +8 -0
  11. package/dist/lib/task/issue-form.js +66 -0
  12. package/dist/lib/task/sections.js +44 -0
  13. package/lib/sandbox/commands/create.ts +33 -4
  14. package/lib/sandbox/commands/ls.ts +4 -36
  15. package/lib/sandbox/commands/show.ts +80 -0
  16. package/lib/sandbox/index.ts +7 -0
  17. package/lib/sandbox/tools.ts +28 -1
  18. package/lib/task/commands/issue-body.ts +102 -0
  19. package/lib/task/commands/log.ts +146 -6
  20. package/lib/task/index.ts +8 -0
  21. package/lib/task/issue-form.ts +77 -0
  22. package/lib/task/sections.ts +44 -0
  23. package/package.json +3 -2
  24. package/templates/.agents/README.en.md +3 -1
  25. package/templates/.agents/README.zh-CN.md +3 -1
  26. package/templates/.agents/rules/create-issue.github.en.md +20 -29
  27. package/templates/.agents/rules/create-issue.github.zh-CN.md +20 -29
  28. package/templates/.agents/rules/issue-pr-commands.github.en.md +6 -0
  29. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +6 -0
  30. package/templates/.agents/rules/next-step-output.en.md +26 -1
  31. package/templates/.agents/rules/next-step-output.zh-CN.md +26 -1
  32. package/templates/.agents/rules/no-mid-flow-questions.en.md +11 -0
  33. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +11 -0
  34. package/templates/.agents/rules/review-handshake.en.md +15 -1
  35. package/templates/.agents/rules/review-handshake.zh-CN.md +15 -1
  36. package/templates/.agents/rules/task-management.en.md +27 -0
  37. package/templates/.agents/rules/task-management.zh-CN.md +31 -0
  38. package/templates/.agents/scripts/validate-artifact.js +11 -2
  39. package/templates/.agents/skills/analyze-task/SKILL.en.md +14 -1
  40. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +14 -1
  41. package/templates/.agents/skills/archive-tasks/SKILL.en.md +3 -1
  42. package/templates/.agents/skills/archive-tasks/SKILL.zh-CN.md +3 -1
  43. package/templates/.agents/skills/block-task/SKILL.en.md +13 -1
  44. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +13 -1
  45. package/templates/.agents/skills/cancel-task/SKILL.en.md +13 -1
  46. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +14 -2
  47. package/templates/.agents/skills/check-task/SKILL.en.md +30 -81
  48. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +30 -80
  49. package/templates/.agents/skills/close-codescan/SKILL.en.md +13 -1
  50. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +13 -1
  51. package/templates/.agents/skills/close-dependabot/SKILL.en.md +13 -1
  52. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +13 -1
  53. package/templates/.agents/skills/code-task/SKILL.en.md +14 -1
  54. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +14 -1
  55. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +7 -2
  56. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +7 -2
  57. package/templates/.agents/skills/code-task/scripts/detect-mode.js +6 -14
  58. package/templates/.agents/skills/commit/SKILL.en.md +37 -5
  59. package/templates/.agents/skills/commit/SKILL.zh-CN.md +37 -5
  60. package/templates/.agents/skills/complete-task/SKILL.en.md +13 -1
  61. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +13 -1
  62. package/templates/.agents/skills/create-pr/SKILL.en.md +23 -2
  63. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +23 -2
  64. package/templates/.agents/skills/create-release-note/SKILL.en.md +19 -2
  65. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +19 -2
  66. package/templates/.agents/skills/create-task/SKILL.en.md +14 -1
  67. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +17 -4
  68. package/templates/.agents/skills/import-codescan/SKILL.en.md +14 -1
  69. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +14 -1
  70. package/templates/.agents/skills/import-dependabot/SKILL.en.md +14 -1
  71. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +14 -1
  72. package/templates/.agents/skills/import-issue/SKILL.en.md +35 -2
  73. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +35 -2
  74. package/templates/.agents/skills/init-labels/SKILL.en.md +3 -1
  75. package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +3 -1
  76. package/templates/.agents/skills/init-milestones/SKILL.en.md +3 -1
  77. package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +3 -1
  78. package/templates/.agents/skills/plan-task/SKILL.en.md +15 -1
  79. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +15 -1
  80. package/templates/.agents/skills/post-release/SKILL.en.md +3 -1
  81. package/templates/.agents/skills/post-release/SKILL.zh-CN.md +3 -1
  82. package/templates/.agents/skills/refine-title/SKILL.en.md +3 -1
  83. package/templates/.agents/skills/refine-title/SKILL.zh-CN.md +3 -1
  84. package/templates/.agents/skills/release/SKILL.en.md +3 -1
  85. package/templates/.agents/skills/release/SKILL.zh-CN.md +3 -1
  86. package/templates/.agents/skills/restore-task/SKILL.en.md +13 -1
  87. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +13 -1
  88. package/templates/.agents/skills/review-analysis/SKILL.en.md +16 -2
  89. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +16 -2
  90. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +1 -1
  91. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +1 -1
  92. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +1 -1
  93. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +1 -2
  94. package/templates/.agents/skills/review-analysis/reference/review-criteria.en.md +1 -0
  95. package/templates/.agents/skills/review-analysis/reference/review-criteria.zh-CN.md +1 -0
  96. package/templates/.agents/skills/review-code/SKILL.en.md +15 -2
  97. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +15 -2
  98. package/templates/.agents/skills/review-code/reference/output-templates.en.md +1 -1
  99. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +1 -1
  100. package/templates/.agents/skills/review-code/reference/report-template.en.md +1 -1
  101. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +1 -2
  102. package/templates/.agents/skills/review-code/reference/review-criteria.en.md +1 -0
  103. package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +1 -0
  104. package/templates/.agents/skills/review-plan/SKILL.en.md +16 -2
  105. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +16 -2
  106. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +1 -1
  107. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +1 -1
  108. package/templates/.agents/skills/review-plan/reference/report-template.en.md +1 -1
  109. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +1 -2
  110. package/templates/.agents/skills/review-plan/reference/review-criteria.en.md +1 -0
  111. package/templates/.agents/skills/review-plan/reference/review-criteria.zh-CN.md +1 -0
  112. package/templates/.agents/skills/test/SKILL.en.md +3 -1
  113. package/templates/.agents/skills/test/SKILL.zh-CN.md +1 -1
  114. package/templates/.agents/skills/test-integration/SKILL.en.md +3 -1
  115. package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +3 -1
  116. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +3 -1
  117. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +3 -1
  118. package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +3 -1
  119. package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +3 -1
  120. package/templates/.agents/skills/watch-pr/SKILL.en.md +13 -1
  121. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +13 -1
  122. package/templates/.agents/templates/task.en.md +5 -0
  123. package/templates/.agents/templates/task.zh-CN.md +5 -0
package/bin/cli.ts CHANGED
@@ -19,8 +19,8 @@ Commands:
19
19
  help Show this help message
20
20
  init Initialize a new project with update-agent-infra seed command
21
21
  merge Merge tasks from another workspace directory (active/blocked/completed/archive)
22
- sandbox Manage Docker-based AI sandboxes
23
- task Read-only views over .agents/workspace tasks (cat / files / grep / log / ls / show / status)
22
+ sandbox, s Manage Docker-based AI sandboxes
23
+ task, t Read-only views over .agents/workspace tasks (cat / files / grep / log / ls / show / status)
24
24
  update Update seed files and sync file registry for an existing project
25
25
  version Show version
26
26
 
@@ -37,7 +37,15 @@ Examples:
37
37
  npx @fitlab-ai/agent-infra init
38
38
  `;
39
39
 
40
- const command = process.argv[2] || '';
40
+ const COMMAND_ALIASES: Record<string, string> = {
41
+ s: 'sandbox',
42
+ t: 'task'
43
+ };
44
+
45
+ const rawCommand = process.argv[2] || '';
46
+ const command = Object.hasOwn(COMMAND_ALIASES, rawCommand)
47
+ ? COMMAND_ALIASES[rawCommand]
48
+ : rawCommand;
41
49
 
42
50
  function errorMessage(error: unknown): string {
43
51
  return error instanceof Error ? error.message : String(error);
package/dist/bin/cli.js CHANGED
@@ -23,8 +23,8 @@ Commands:
23
23
  help Show this help message
24
24
  init Initialize a new project with update-agent-infra seed command
25
25
  merge Merge tasks from another workspace directory (active/blocked/completed/archive)
26
- sandbox Manage Docker-based AI sandboxes
27
- task Read-only views over .agents/workspace tasks (cat / files / grep / log / ls / show / status)
26
+ sandbox, s Manage Docker-based AI sandboxes
27
+ task, t Read-only views over .agents/workspace tasks (cat / files / grep / log / ls / show / status)
28
28
  update Update seed files and sync file registry for an existing project
29
29
  version Show version
30
30
 
@@ -40,7 +40,14 @@ Examples:
40
40
  cd my-project && ai init
41
41
  npx @fitlab-ai/agent-infra init
42
42
  `;
43
- const command = process.argv[2] || '';
43
+ const COMMAND_ALIASES = {
44
+ s: 'sandbox',
45
+ t: 'task'
46
+ };
47
+ const rawCommand = process.argv[2] || '';
48
+ const command = Object.hasOwn(COMMAND_ALIASES, rawCommand)
49
+ ? COMMAND_ALIASES[rawCommand]
50
+ : rawCommand;
44
51
  function errorMessage(error) {
45
52
  return error instanceof Error ? error.message : String(error);
46
53
  }
@@ -820,6 +820,12 @@ function runEngineTaskCommand(engine, cmd, args, opts = {}) {
820
820
  const command = commandForEngine(engine, cmd, args);
821
821
  return runTaskCommand(command.cmd, command.args, opts);
822
822
  }
823
+ // `docker run` args for mounting a tool's containerMount as an in-container
824
+ // tmpfs. containerMount is an in-container path, so it is NOT engine-converted.
825
+ export function buildTmpfsRunArgs(containerMount, tmpfs) {
826
+ const size = tmpfs.size ?? '512m';
827
+ return ['--tmpfs', `${containerMount}:rw,size=${size}`];
828
+ }
823
829
  export function buildImage(config, tools, dockerfilePath, imageSignature, { engine, runFn = runEngine, runSafeFn = runSafeEngine, runVerboseFn = runVerboseEngine, env = process.env } = {}) {
824
830
  const selectedEngine = engine ?? detectEngine({ engine: config.engine });
825
831
  const { uid: hostUid, gid: hostGid } = resolveBuildUid({
@@ -1076,10 +1082,8 @@ export async function create(args) {
1076
1082
  // The TUI reads <toolDir>/opencode.json via OPENCODE_CONFIG pinned in tools.js.
1077
1083
  ensureOpenCodeModelInheritance(opencodeEntry.dir, effectiveConfig.home);
1078
1084
  }
1079
- const toolVolumes = effectiveResolvedTools.flatMap(({ tool, dir }) => [
1080
- '-v',
1081
- volumeArg(engine, dir, tool.containerMount)
1082
- ]);
1085
+ const toolVolumes = effectiveResolvedTools.flatMap(({ tool, dir }) => tool.tmpfs ? [] : ['-v', volumeArg(engine, dir, tool.containerMount)]);
1086
+ const tmpfsArgs = effectiveResolvedTools.flatMap(({ tool }) => tool.tmpfs ? buildTmpfsRunArgs(tool.containerMount, tool.tmpfs) : []);
1083
1087
  const workspaceDir = path.join(effectiveConfig.repoRoot, '.agents', 'workspace');
1084
1088
  hostShellConfig = prepareHostShellConfig({
1085
1089
  home: effectiveConfig.home,
@@ -1091,6 +1095,22 @@ export async function create(args) {
1091
1095
  '-v',
1092
1096
  volumeArg(engine, hostPath, containerPath, ':ro')
1093
1097
  ]);
1098
+ // A tmpfs containerMount starts empty, so the config seeded into the
1099
+ // host dir before launch would be invisible in-container. Bind only
1100
+ // the explicitly declared seed entries (config.toml, model-catalogs)
1101
+ // back over the tmpfs as nested mounts — the same proven mechanism as
1102
+ // hostLiveMounts/auth.json, established at `docker run` time (no
1103
+ // post-start `docker cp`, which can land under a freshly-mounted
1104
+ // tmpfs instead of inside it). The allowlist is deliberate: any
1105
+ // runtime files left in the host dir (e.g. a stale logs_2.sqlite or
1106
+ // sessions/ from a previous bind-mount era) must NOT be re-mounted,
1107
+ // or the high-churn writes would land on the host SSD again.
1108
+ const tmpfsSeedVolumes = effectiveResolvedTools.flatMap(({ tool, dir }) => (tool.tmpfs?.seed ?? []).flatMap((entry) => {
1109
+ const hostPath = path.join(dir, entry);
1110
+ return fs.existsSync(hostPath)
1111
+ ? ['-v', volumeArg(engine, hostPath, path.posix.join(tool.containerMount, entry))]
1112
+ : [];
1113
+ }));
1094
1114
  const liveMountVolumes = effectiveResolvedTools.flatMap(({ tool }) => (tool.hostLiveMounts ?? [])
1095
1115
  .filter(({ hostPath }) => fs.existsSync(hostPath))
1096
1116
  .flatMap(({ hostPath, containerSubpath }) => [
@@ -1133,6 +1153,8 @@ export async function create(args) {
1133
1153
  volumeArg(engine, hostJoin(effectiveConfig.home, '.ssh'), '/home/devuser/.ssh', ':ro'),
1134
1154
  ...dotfilesMount,
1135
1155
  ...toolVolumes,
1156
+ ...tmpfsArgs,
1157
+ ...tmpfsSeedVolumes,
1136
1158
  ...liveMountVolumes,
1137
1159
  ...shellConfigVolumes,
1138
1160
  ...envFile.dockerArgs,
@@ -1,11 +1,8 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
1
  import * as p from '@clack/prompts';
4
2
  import pc from 'picocolors';
5
3
  import { loadConfig } from "../config.js";
6
4
  import { sandboxBranchLabel, sandboxLabel } from "../constants.js";
7
5
  import { detectEngine } from "../engine.js";
8
- import { resolveTools, toolProjectDirCandidates } from "../tools.js";
9
6
  import { formatTable } from "../../table.js";
10
7
  import { lookupShortIdByBranch } from "../../task/short-id.js";
11
8
  import { fetchSandboxRows } from "./list-running.js";
@@ -18,17 +15,14 @@ id bound to each container's branch (via
18
15
  .agents/workspace/active/.short-ids.json), or '-' if no active task is
19
16
  bound. Pass the SHORT value to "ai sandbox exec" (e.g. 'ai sandbox exec 11').
20
17
  A '-' means no active task is bound to that branch, so the sandbox is free
21
- to remove with "ai sandbox rm <branch>".`;
18
+ to remove with "ai sandbox rm <branch>".
19
+
20
+ Use "ai sandbox show <ref>" for a single sandbox's worktree and per-tool
21
+ state paths.`;
22
22
  const CONTAINER_TABLE_HEADERS = ['#', 'SHORT', 'NAMES', 'STATUS', 'BRANCH'];
23
23
  export function formatContainerTable(rows, zebra = false) {
24
24
  return formatTable(CONTAINER_TABLE_HEADERS, rows.map((r) => [r.row, r.shortId, r.name, r.status, r.branch]), { zebra });
25
25
  }
26
- function listChildren(dir) {
27
- if (!fs.existsSync(dir)) {
28
- return [];
29
- }
30
- return fs.readdirSync(dir).sort().map((entry) => path.join(dir, entry));
31
- }
32
26
  export function ls(args = []) {
33
27
  if (args.length > 0 && (args[0] === '--help' || args[0] === '-h')) {
34
28
  process.stdout.write(`${USAGE}\n`);
@@ -36,7 +30,6 @@ export function ls(args = []) {
36
30
  }
37
31
  const config = loadConfig();
38
32
  const engine = detectEngine(config);
39
- const tools = resolveTools(config);
40
33
  const label = sandboxLabel(config);
41
34
  const { running, nonRunning } = fetchSandboxRows(engine, label, sandboxBranchLabel(config));
42
35
  p.intro(pc.cyan(`Sandbox status for ${config.project}`));
@@ -64,27 +57,5 @@ export function ls(args = []) {
64
57
  process.stdout.write(` SHORT '-' = no active task bound; that sandbox is free to remove with 'ai sandbox rm <branch>'.\n`);
65
58
  }
66
59
  }
67
- p.log.step('Worktrees');
68
- const worktrees = listChildren(config.worktreeBase);
69
- if (worktrees.length === 0) {
70
- p.log.warn(' No sandbox worktrees');
71
- }
72
- else {
73
- for (const worktree of worktrees) {
74
- process.stdout.write(` ${worktree}\n`);
75
- }
76
- }
77
- for (const tool of tools) {
78
- p.log.step(`${tool.name} state`);
79
- const entries = toolProjectDirCandidates(tool, config.project)
80
- .flatMap((dir) => listChildren(dir));
81
- if (entries.length === 0) {
82
- p.log.warn(` No ${tool.name} sandbox state`);
83
- continue;
84
- }
85
- for (const entry of entries) {
86
- process.stdout.write(` ${entry}\n`);
87
- }
88
- }
89
60
  }
90
61
  //# sourceMappingURL=ls.js.map
@@ -0,0 +1,67 @@
1
+ import fs from 'node:fs';
2
+ import * as p from '@clack/prompts';
3
+ import pc from 'picocolors';
4
+ import { loadConfig } from "../config.js";
5
+ import { assertValidBranchName, worktreeDirCandidates } from "../constants.js";
6
+ import { resolveBranchArg } from "./list-running.js";
7
+ import { resolveTools, toolConfigDirCandidates } from "../tools.js";
8
+ const USAGE = `Usage: ai sandbox show <branch | TASK-id | N | '#N'>
9
+
10
+ Shows one sandbox's worktree path and per-tool state paths (Claude Code,
11
+ Codex, Gemini CLI, OpenCode). The argument follows the same contract as
12
+ 'ai sandbox exec' and 'ai sandbox start': N (bare) is the recommended form
13
+ for task short ids (e.g. 'ai sandbox show 11'); '#N', a TASK-id, or a plain
14
+ branch name are also accepted. Use 'ai sandbox ls' for the container list.`;
15
+ function existingDirs(candidates) {
16
+ const seen = new Set();
17
+ const result = [];
18
+ for (const candidate of candidates) {
19
+ if (!seen.has(candidate) && fs.existsSync(candidate)) {
20
+ seen.add(candidate);
21
+ result.push(candidate);
22
+ }
23
+ }
24
+ return result;
25
+ }
26
+ export function collectSandboxDetail(config, branch) {
27
+ const worktrees = existingDirs(worktreeDirCandidates(config, branch));
28
+ const toolStates = resolveTools(config).map((tool) => ({
29
+ name: tool.name,
30
+ entries: existingDirs(toolConfigDirCandidates(tool, config.project, branch))
31
+ }));
32
+ return { worktrees, toolStates };
33
+ }
34
+ export function show(args = []) {
35
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
36
+ process.stdout.write(`${USAGE}\n`);
37
+ if (args.length === 0) {
38
+ process.exitCode = 1;
39
+ }
40
+ return;
41
+ }
42
+ const config = loadConfig();
43
+ const branch = resolveBranchArg(args[0], { repoRoot: config.repoRoot });
44
+ assertValidBranchName(branch);
45
+ const detail = collectSandboxDetail(config, branch);
46
+ p.intro(pc.cyan(`Sandbox detail for ${config.project} · ${branch}`));
47
+ p.log.step('Worktree');
48
+ if (detail.worktrees.length === 0) {
49
+ p.log.warn(' No worktree for this branch');
50
+ }
51
+ else {
52
+ for (const worktree of detail.worktrees) {
53
+ process.stdout.write(` ${worktree}\n`);
54
+ }
55
+ }
56
+ for (const tool of detail.toolStates) {
57
+ p.log.step(`${tool.name} state`);
58
+ if (tool.entries.length === 0) {
59
+ p.log.warn(` No ${tool.name} sandbox state`);
60
+ continue;
61
+ }
62
+ for (const entry of tool.entries) {
63
+ process.stdout.write(` ${entry}\n`);
64
+ }
65
+ }
66
+ }
67
+ //# sourceMappingURL=show.js.map
@@ -9,6 +9,8 @@ Commands:
9
9
  ls List sandboxes for the current project (the '#'
10
10
  column is a display-only row number; the 'SHORT'
11
11
  column shows the active task short id, '-' if none)
12
+ show <branch | TASK-id | N | '#N'>
13
+ Show one sandbox's worktree and per-tool state paths
12
14
  prune [--dry-run] Remove orphaned per-branch state dirs
13
15
  rebuild [--quiet] [--refresh]
14
16
  Rebuild the sandbox image (--refresh pulls base + tools)
@@ -52,6 +54,11 @@ export async function runSandbox(args) {
52
54
  ls(rest);
53
55
  break;
54
56
  }
57
+ case 'show': {
58
+ const { show } = await import("./commands/show.js");
59
+ show(rest);
60
+ break;
61
+ }
55
62
  case 'prune': {
56
63
  const { prune } = await import("./commands/prune.js");
57
64
  await prune(rest);
@@ -41,6 +41,12 @@ function createBuiltinTools(home, project) {
41
41
  containerMount: '/home/devuser/.codex',
42
42
  versionCmd: 'codex --version',
43
43
  setupHint: 'Run codex once inside the container and choose Device Code login if needed.',
44
+ // codex churns ~/.codex/logs_2.sqlite heavily (upstream openai/codex#24275);
45
+ // a bind-mount would write-amplify onto the host SSD via virtiofs. Mount the
46
+ // codex home as tmpfs so those logs stay in RAM and die with the container.
47
+ // Only the seeded config (config.toml, model-catalogs) is bound back over
48
+ // the tmpfs; runtime files like logs_2.sqlite must stay in RAM.
49
+ tmpfs: { size: '512m', seed: ['config.toml', 'model-catalogs'] },
44
50
  hostLiveMounts: [
45
51
  { hostPath: hostJoin(home, '.codex', 'auth.json'), containerSubpath: 'auth.json' }
46
52
  ],
@@ -218,6 +224,18 @@ function parseHostLiveMounts(value, context) {
218
224
  };
219
225
  });
220
226
  }
227
+ function parseTmpfs(value, context) {
228
+ if (value === undefined) {
229
+ return undefined;
230
+ }
231
+ if (!isPlainObject(value)) {
232
+ throw new Error(`${context}: field "tmpfs" must be an object when provided`);
233
+ }
234
+ return {
235
+ size: asOptionalNonEmptyString(value.size, 'tmpfs.size', context),
236
+ seed: asStringArray(value.seed, 'tmpfs.seed', context)
237
+ };
238
+ }
221
239
  export function parseCustomTool(entry, index, options) {
222
240
  const context = `customTools[${index}]`;
223
241
  if (!isPlainObject(entry)) {
@@ -246,7 +264,8 @@ export function parseCustomTool(entry, index, options) {
246
264
  hostPreSeedDirs: parseHostPreSeedDirs(entry.hostPreSeedDirs, context),
247
265
  pathRewriteFiles: asStringArray(entry.pathRewriteFiles, 'pathRewriteFiles', context),
248
266
  hostLiveMounts: parseHostLiveMounts(entry.hostLiveMounts, context),
249
- postSetupCmds: asStringArray(entry.postSetupCmds, 'postSetupCmds', context)
267
+ postSetupCmds: asStringArray(entry.postSetupCmds, 'postSetupCmds', context),
268
+ tmpfs: parseTmpfs(entry.tmpfs, context)
250
269
  };
251
270
  validateTool(tool);
252
271
  return tool;
@@ -0,0 +1,94 @@
1
+ import fs from 'node:fs';
2
+ import { resolveTaskRef } from "../resolve-ref.js";
3
+ import { extractTitle } from "../frontmatter.js";
4
+ import { extractSection, findSectionHeading } from "../sections.js";
5
+ import { renderTemplateBody, PLACEHOLDER } from "../issue-form.js";
6
+ const USAGE = `Usage: ai task issue-body <N | #N | TASK-id> [--template <path>]
7
+
8
+ Print a deterministic Issue body extracted from a task's task.md.
9
+ <ref> Bare numeric / '#N' short id, or a full TASK-YYYYMMDD-HHMMSS id.
10
+ --template <path> Render the final body for the given GitHub Issue Form (scenario A);
11
+ without it, print the default '描述 + 需求' body (scenario B).
12
+
13
+ Only the task title, '## 描述' and '## 需求' sections are ever emitted; the rest of
14
+ task.md (scaffolding sections, placeholders) is never written to the body.
15
+ `;
16
+ const DESCRIPTION_ALIASES = ['描述', 'Description'];
17
+ const REQUIREMENTS_ALIASES = ['需求', 'Requirements'];
18
+ /**
19
+ * Build the scenario B default body, mirroring whichever heading language the
20
+ * task.md actually uses, with empty sections falling back to `N/A`.
21
+ */
22
+ function buildDefaultBody(content) {
23
+ const descHeading = findSectionHeading(content, DESCRIPTION_ALIASES);
24
+ const reqHeading = findSectionHeading(content, REQUIREMENTS_ALIASES);
25
+ const description = extractSection(content, DESCRIPTION_ALIASES) || PLACEHOLDER;
26
+ const requirements = extractSection(content, REQUIREMENTS_ALIASES) || PLACEHOLDER;
27
+ return `## ${descHeading}\n\n${description}\n\n## ${reqHeading}\n\n${requirements}\n`;
28
+ }
29
+ function readTaskFields(content) {
30
+ return {
31
+ title: extractTitle(content),
32
+ description: extractSection(content, DESCRIPTION_ALIASES),
33
+ requirements: extractSection(content, REQUIREMENTS_ALIASES)
34
+ };
35
+ }
36
+ function issueBody(args = []) {
37
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
38
+ process.stdout.write(USAGE);
39
+ if (args.length === 0)
40
+ process.exitCode = 1;
41
+ return;
42
+ }
43
+ let ref;
44
+ let templatePath;
45
+ for (let i = 0; i < args.length; i += 1) {
46
+ const arg = args[i];
47
+ if (arg === '--template') {
48
+ templatePath = args[i + 1];
49
+ i += 1;
50
+ }
51
+ else if (ref === undefined) {
52
+ ref = arg;
53
+ }
54
+ }
55
+ if (!ref) {
56
+ process.stderr.write('ai task issue-body: missing task ref\n');
57
+ process.exitCode = 1;
58
+ return;
59
+ }
60
+ if (templatePath === undefined && args.includes('--template')) {
61
+ process.stderr.write('ai task issue-body: --template requires a path\n');
62
+ process.exitCode = 1;
63
+ return;
64
+ }
65
+ const resolved = resolveTaskRef(ref);
66
+ if (!resolved.ok) {
67
+ process.stderr.write(`ai task issue-body: ${resolved.message}\n`);
68
+ process.exitCode = 1;
69
+ return;
70
+ }
71
+ const content = fs.readFileSync(resolved.taskMdPath, 'utf8');
72
+ if (templatePath !== undefined) {
73
+ let formText;
74
+ try {
75
+ formText = fs.readFileSync(templatePath, 'utf8');
76
+ }
77
+ catch (e) {
78
+ process.stderr.write(`ai task issue-body: cannot read template '${templatePath}': ${e.message}\n`);
79
+ process.exitCode = 1;
80
+ return;
81
+ }
82
+ try {
83
+ process.stdout.write(renderTemplateBody(formText, readTaskFields(content)));
84
+ }
85
+ catch (e) {
86
+ process.stderr.write(`ai task issue-body: cannot render template '${templatePath}': ${e.message}\n`);
87
+ process.exitCode = 1;
88
+ }
89
+ return;
90
+ }
91
+ process.stdout.write(buildDefaultBody(content));
92
+ }
93
+ export { issueBody, buildDefaultBody };
94
+ //# sourceMappingURL=issue-body.js.map
@@ -3,19 +3,46 @@ import { formatTable } from "../../table.js";
3
3
  import { resolveTaskRef } from "../resolve-ref.js";
4
4
  const USAGE = `Usage: ai task log <N | #N | TASK-id>
5
5
 
6
- Renders a task's activity log as a chronological timeline table.
6
+ Renders a task's activity log as a per-step status table. A step's start and
7
+ completion are paired onto one row: STARTED holds the start time, DONE the
8
+ completion time (or '(in progress)' while still running).
7
9
  <ref> Bare numeric / '#N' short id, or a full TASK-YYYYMMDD-HHMMSS id.
8
10
 
9
- Columns: # (timeline position) / TIME / STEP / AGENT / NOTE
11
+ Columns: # (row) / STEP / AGENT / STARTED / DONE / NOTE
12
+ A human-executed step shows AGENT as 'human' and, when it has no start marker,
13
+ a '-' STARTED placeholder. Review-step NOTE also carries two human counts in
14
+ the verdict list, right after blockers/major/minor: manual-verify (env-blocked)
15
+ and human-decision (current ledger stage total).
10
16
  `;
11
- const TABLE_HEADERS = ['#', 'TIME', 'STEP', 'AGENT', 'NOTE'];
17
+ const TABLE_HEADERS = ['#', 'STEP', 'AGENT', 'STARTED', 'DONE', 'NOTE'];
12
18
  // The activity-log H2 heading is language-dependent (zh template / en template).
13
19
  const HEADING_RE = /^##\s+(活动日志|Activity Log)\s*$/;
20
+ const LEDGER_HEADING_RE = /^##\s+(审查分歧账本|Review Disagreement Ledger)\s*$/;
14
21
  const NEXT_H2_RE = /^##\s/;
15
22
  // `- {time} — **{step}** by {agent} — {note}` ; the separator is an em-dash
16
23
  // (U+2014). STEP/AGENT are non-greedy so a note that itself contains ' — ' or
17
24
  // '→' is not mis-split; NOTE greedily takes the rest of the line.
18
25
  const ENTRY_RE = /^- (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}) — \*\*(.+?)\*\* by (.+?) — (.*)$/;
26
+ // A start marker reuses the normal entry grammar and only suffixes its action
27
+ // with ` [started]`; the matching done entry carries the identical base action
28
+ // without the suffix. Pairing therefore keys on the base action (including any
29
+ // `(Round N)`), so every round and every repeated execution pairs on its own.
30
+ const STARTED_SUFFIX_RE = /\s*\[started\]\s*$/;
31
+ // Short agent tokens that actually appear in activity logs for AI executors
32
+ // (workflow recommended_agents claude/codex/gemini/cursor + enabled TUI opencode;
33
+ // note these differ from the `.airc.json` long names claude-code/gemini-cli).
34
+ // Any other executor token (a human name, possibly CJK) is treated as human.
35
+ const KNOWN_AI_AGENTS = new Set(['claude', 'codex', 'gemini', 'opencode', 'cursor']);
36
+ const HUMAN_DECISION_STATUSES = new Set(['needs-human-decision', 'human-decided']);
37
+ const ENV_BLOCKED_RE = /\(\+\s*(\d+)\s+env-blocked\)/i;
38
+ // Same match plus any leading whitespace, so folding the count into the verdict
39
+ // text drops the redundant `(+ n env-blocked)` fragment without leaving a gap.
40
+ const ENV_BLOCKED_STRIP_RE = /\s*\(\+\s*\d+\s+env-blocked\)/i;
41
+ const REVIEW_STAGE_PREFIXES = [
42
+ { prefix: 'Review Analysis', stage: 'analysis' },
43
+ { prefix: 'Review Plan', stage: 'plan' },
44
+ { prefix: 'Review Code', stage: 'code' }
45
+ ];
19
46
  function parseActivityLog(content) {
20
47
  const lines = content.split('\n');
21
48
  let i = 0;
@@ -40,6 +67,101 @@ function parseActivityLog(content) {
40
67
  parsed.sort((a, b) => a.epoch - b.epoch || a.order - b.order);
41
68
  return { sectionFound: true, entries: parsed.map((p) => p.entry) };
42
69
  }
70
+ // Collapse a chronological entry list into per-step rows: a `[started]` marker
71
+ // opens a row, the next matching done entry fills it in place (FIFO per base
72
+ // action). Started-only rows stay in flight; done-only entries (legacy logs with
73
+ // no start marker) render as standalone rows. Result order = first-seen order,
74
+ // which is already ascending because `entries` is sorted ascending.
75
+ function pairEntries(entries) {
76
+ const rows = [];
77
+ const open = new Map();
78
+ for (const e of entries) {
79
+ const isStarted = STARTED_SUFFIX_RE.test(e.step);
80
+ const base = e.step.replace(STARTED_SUFFIX_RE, '');
81
+ if (isStarted) {
82
+ const row = { step: base, agent: e.agent, started: e.time, done: '', note: e.note };
83
+ rows.push(row);
84
+ const queue = open.get(base);
85
+ if (queue)
86
+ queue.push(row);
87
+ else
88
+ open.set(base, [row]);
89
+ }
90
+ else {
91
+ const pending = open.get(base)?.shift();
92
+ if (pending) {
93
+ // Done fills the open row; the done entry carries the meaningful note.
94
+ pending.done = e.time;
95
+ pending.agent = e.agent;
96
+ pending.note = e.note;
97
+ }
98
+ else {
99
+ rows.push({ step: base, agent: e.agent, started: '', done: e.time, note: e.note });
100
+ }
101
+ }
102
+ }
103
+ return rows;
104
+ }
105
+ function parseReviewLedger(content) {
106
+ const lines = content.split('\n');
107
+ let i = 0;
108
+ while (i < lines.length && !LEDGER_HEADING_RE.test(lines[i]))
109
+ i += 1;
110
+ if (i >= lines.length)
111
+ return [];
112
+ const rows = [];
113
+ for (let j = i + 1; j < lines.length; j += 1) {
114
+ if (NEXT_H2_RE.test(lines[j]))
115
+ break;
116
+ const line = lines[j].trim();
117
+ if (!line.startsWith('|'))
118
+ continue;
119
+ const cells = line
120
+ .split('|')
121
+ .slice(1, -1)
122
+ .map((cell) => cell.trim());
123
+ if (cells.length < 6 || cells[0] === 'id' || /^-+$/.test(cells[0] ?? ''))
124
+ continue;
125
+ rows.push({ stage: cells[1], status: cells[4] });
126
+ }
127
+ return rows;
128
+ }
129
+ function countHumanDecisionsByStage(rows) {
130
+ const counts = new Map();
131
+ for (const row of rows) {
132
+ if (!isReviewStage(row.stage) || !HUMAN_DECISION_STATUSES.has(row.status))
133
+ continue;
134
+ counts.set(row.stage, (counts.get(row.stage) ?? 0) + 1);
135
+ }
136
+ return counts;
137
+ }
138
+ function isReviewStage(stage) {
139
+ return stage === 'analysis' || stage === 'plan' || stage === 'code';
140
+ }
141
+ function reviewStageForStep(step) {
142
+ return REVIEW_STAGE_PREFIXES.find(({ prefix }) => step.startsWith(prefix))?.stage;
143
+ }
144
+ function humanValidationCount(note) {
145
+ const match = ENV_BLOCKED_RE.exec(note);
146
+ return match ? Number(match[1]) : 0;
147
+ }
148
+ // A step is human-executed when its agent token is not a known AI token. Take
149
+ // the first whitespace-delimited token and drop any trailing parenthetical
150
+ // annotation (e.g. `张三 (executed on host)` -> `张三`) before the lookup.
151
+ function isHumanAgent(agent) {
152
+ const token = agent.trim().split(/\s+/)[0]?.replace(/\(.*$/, '') ?? '';
153
+ return token !== '' && !KNOWN_AI_AGENTS.has(token);
154
+ }
155
+ // Fold the two human counts into a review row's verdict NOTE: comma-joined, right
156
+ // after the blockers/major/minor list and before the ` → artifact` link, mirroring
157
+ // the review count line. The raw `(+ n env-blocked)` fragment is dropped so the
158
+ // env-blocked number is not shown twice (it becomes the manual-verify count).
159
+ function foldHumanCounts(note, decisions, envBlocked) {
160
+ const base = note.replace(ENV_BLOCKED_STRIP_RE, '');
161
+ const group = `Manual-verify: ${envBlocked}, Human-decision: ${decisions}`;
162
+ const arrow = base.indexOf(' → ');
163
+ return arrow === -1 ? `${base}, ${group}` : `${base.slice(0, arrow)}, ${group}${base.slice(arrow)}`;
164
+ }
43
165
  function log(args = []) {
44
166
  if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
45
167
  process.stdout.write(USAGE);
@@ -65,11 +187,22 @@ function log(args = []) {
65
187
  process.exitCode = 1;
66
188
  return;
67
189
  }
68
- const rows = entries.map((e, idx) => [String(idx + 1), e.time, e.step, e.agent, e.note]);
190
+ const steps = pairEntries(entries);
191
+ const humanDecisionCounts = countHumanDecisionsByStage(parseReviewLedger(content));
192
+ const rows = steps.map((s, idx) => {
193
+ const stage = reviewStageForStep(s.step);
194
+ const note = stage
195
+ ? foldHumanCounts(s.note, humanDecisionCounts.get(stage) ?? 0, humanValidationCount(s.note))
196
+ : s.note;
197
+ const human = isHumanAgent(s.agent);
198
+ const agent = human ? 'human' : s.agent;
199
+ const started = s.started || (human ? '-' : '');
200
+ return [String(idx + 1), s.step, agent, started, s.done || (s.started ? '(in progress)' : ''), note];
201
+ });
69
202
  for (const line of formatTable(TABLE_HEADERS, rows, { zebra: Boolean(process.stdout.isTTY) })) {
70
203
  process.stdout.write(`${line}\n`);
71
204
  }
72
- process.stdout.write(`Total: ${entries.length} entries\n`);
205
+ process.stdout.write(`Total: ${steps.length} steps\n`);
73
206
  }
74
- export { log, parseActivityLog };
207
+ export { log, parseActivityLog, pairEntries, isHumanAgent };
75
208
  //# sourceMappingURL=log.js.map
@@ -4,6 +4,7 @@ Commands:
4
4
  cat <ref> <artifact | N> Print a task artifact (by name or number)
5
5
  files <ref> List artifacts in a task dir (numbered)
6
6
  grep <pattern> [ref] [artifact | N] Literal search across task artifacts (omit ref to scan all)
7
+ issue-body <ref> [--template <path>] Print a deterministic Issue body from task.md (描述 + 需求, or an Issue Form)
7
8
  log <ref> Render a task's activity log as a timeline
8
9
  ls [--all | --blocked | --completed] List tasks (default: active)
9
10
  show <N | #N | TASK-id> Print a task.md
@@ -15,6 +16,8 @@ Examples:
15
16
  ai task files 11
16
17
  ai task grep resolveArtifact
17
18
  ai task grep resolveArtifact 11
19
+ ai task issue-body 11
20
+ ai task issue-body 11 --template .github/ISSUE_TEMPLATE/05_other.yml
18
21
  ai task log 11
19
22
  ai task ls
20
23
  ai task show 11
@@ -49,6 +52,11 @@ export async function runTask(args) {
49
52
  grep(rest);
50
53
  break;
51
54
  }
55
+ case 'issue-body': {
56
+ const { issueBody } = await import("./commands/issue-body.js");
57
+ issueBody(rest);
58
+ break;
59
+ }
52
60
  case 'log': {
53
61
  const { log } = await import("./commands/log.js");
54
62
  log(rest);