@fitlab-ai/agent-infra 0.7.6 → 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 (106) hide show
  1. package/bin/cli.ts +11 -3
  2. package/dist/bin/cli.js +10 -3
  3. package/dist/lib/sandbox/commands/ls.js +4 -33
  4. package/dist/lib/sandbox/commands/show.js +67 -0
  5. package/dist/lib/sandbox/index.js +7 -0
  6. package/dist/lib/task/commands/issue-body.js +94 -0
  7. package/dist/lib/task/commands/log.js +92 -9
  8. package/dist/lib/task/index.js +8 -0
  9. package/dist/lib/task/issue-form.js +66 -0
  10. package/dist/lib/task/sections.js +44 -0
  11. package/lib/sandbox/commands/ls.ts +4 -36
  12. package/lib/sandbox/commands/show.ts +80 -0
  13. package/lib/sandbox/index.ts +7 -0
  14. package/lib/task/commands/issue-body.ts +102 -0
  15. package/lib/task/commands/log.ts +96 -9
  16. package/lib/task/index.ts +8 -0
  17. package/lib/task/issue-form.ts +77 -0
  18. package/lib/task/sections.ts +44 -0
  19. package/package.json +3 -2
  20. package/templates/.agents/README.en.md +3 -1
  21. package/templates/.agents/README.zh-CN.md +3 -1
  22. package/templates/.agents/rules/create-issue.github.en.md +20 -29
  23. package/templates/.agents/rules/create-issue.github.zh-CN.md +20 -29
  24. package/templates/.agents/rules/issue-pr-commands.github.en.md +6 -0
  25. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +6 -0
  26. package/templates/.agents/rules/next-step-output.en.md +26 -1
  27. package/templates/.agents/rules/next-step-output.zh-CN.md +26 -1
  28. package/templates/.agents/rules/task-management.en.md +2 -0
  29. package/templates/.agents/rules/task-management.zh-CN.md +2 -0
  30. package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -1
  31. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -1
  32. package/templates/.agents/skills/archive-tasks/SKILL.en.md +3 -1
  33. package/templates/.agents/skills/archive-tasks/SKILL.zh-CN.md +3 -1
  34. package/templates/.agents/skills/block-task/SKILL.en.md +3 -1
  35. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +3 -1
  36. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -1
  37. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -1
  38. package/templates/.agents/skills/check-task/SKILL.en.md +30 -81
  39. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +30 -80
  40. package/templates/.agents/skills/close-codescan/SKILL.en.md +3 -1
  41. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +3 -1
  42. package/templates/.agents/skills/close-dependabot/SKILL.en.md +3 -1
  43. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +3 -1
  44. package/templates/.agents/skills/code-task/SKILL.en.md +3 -1
  45. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -1
  46. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +7 -2
  47. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +7 -2
  48. package/templates/.agents/skills/code-task/scripts/detect-mode.js +6 -14
  49. package/templates/.agents/skills/commit/SKILL.en.md +27 -5
  50. package/templates/.agents/skills/commit/SKILL.zh-CN.md +27 -5
  51. package/templates/.agents/skills/complete-task/SKILL.en.md +3 -1
  52. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -1
  53. package/templates/.agents/skills/create-pr/SKILL.en.md +3 -1
  54. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +3 -1
  55. package/templates/.agents/skills/create-release-note/SKILL.en.md +3 -1
  56. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +3 -1
  57. package/templates/.agents/skills/create-task/SKILL.en.md +3 -1
  58. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +3 -1
  59. package/templates/.agents/skills/import-codescan/SKILL.en.md +3 -1
  60. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +3 -1
  61. package/templates/.agents/skills/import-dependabot/SKILL.en.md +3 -1
  62. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +3 -1
  63. package/templates/.agents/skills/import-issue/SKILL.en.md +19 -2
  64. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +19 -2
  65. package/templates/.agents/skills/init-labels/SKILL.en.md +3 -1
  66. package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +3 -1
  67. package/templates/.agents/skills/init-milestones/SKILL.en.md +3 -1
  68. package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +3 -1
  69. package/templates/.agents/skills/plan-task/SKILL.en.md +3 -1
  70. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -1
  71. package/templates/.agents/skills/post-release/SKILL.en.md +3 -1
  72. package/templates/.agents/skills/post-release/SKILL.zh-CN.md +3 -1
  73. package/templates/.agents/skills/refine-title/SKILL.en.md +3 -1
  74. package/templates/.agents/skills/refine-title/SKILL.zh-CN.md +3 -1
  75. package/templates/.agents/skills/release/SKILL.en.md +3 -1
  76. package/templates/.agents/skills/release/SKILL.zh-CN.md +3 -1
  77. package/templates/.agents/skills/restore-task/SKILL.en.md +3 -1
  78. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +3 -1
  79. package/templates/.agents/skills/review-analysis/SKILL.en.md +6 -2
  80. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +6 -2
  81. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +1 -1
  82. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +1 -1
  83. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +1 -1
  84. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +1 -2
  85. package/templates/.agents/skills/review-code/SKILL.en.md +5 -2
  86. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -2
  87. package/templates/.agents/skills/review-code/reference/output-templates.en.md +1 -1
  88. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +1 -1
  89. package/templates/.agents/skills/review-code/reference/report-template.en.md +1 -1
  90. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +1 -2
  91. package/templates/.agents/skills/review-plan/SKILL.en.md +6 -2
  92. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +6 -2
  93. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +1 -1
  94. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +1 -1
  95. package/templates/.agents/skills/review-plan/reference/report-template.en.md +1 -1
  96. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +1 -2
  97. package/templates/.agents/skills/test/SKILL.en.md +3 -1
  98. package/templates/.agents/skills/test/SKILL.zh-CN.md +1 -1
  99. package/templates/.agents/skills/test-integration/SKILL.en.md +3 -1
  100. package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +3 -1
  101. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +3 -1
  102. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +3 -1
  103. package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +3 -1
  104. package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +3 -1
  105. package/templates/.agents/skills/watch-pr/SKILL.en.md +3 -1
  106. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +3 -1
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
  }
@@ -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);
@@ -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
@@ -9,10 +9,15 @@ completion time (or '(in progress)' while still running).
9
9
  <ref> Bare numeric / '#N' short id, or a full TASK-YYYYMMDD-HHMMSS id.
10
10
 
11
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).
12
16
  `;
13
17
  const TABLE_HEADERS = ['#', 'STEP', 'AGENT', 'STARTED', 'DONE', 'NOTE'];
14
18
  // The activity-log H2 heading is language-dependent (zh template / en template).
15
19
  const HEADING_RE = /^##\s+(活动日志|Activity Log)\s*$/;
20
+ const LEDGER_HEADING_RE = /^##\s+(审查分歧账本|Review Disagreement Ledger)\s*$/;
16
21
  const NEXT_H2_RE = /^##\s/;
17
22
  // `- {time} — **{step}** by {agent} — {note}` ; the separator is an em-dash
18
23
  // (U+2014). STEP/AGENT are non-greedy so a note that itself contains ' — ' or
@@ -23,6 +28,21 @@ const ENTRY_RE = /^- (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}) — \*
23
28
  // without the suffix. Pairing therefore keys on the base action (including any
24
29
  // `(Round N)`), so every round and every repeated execution pairs on its own.
25
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
+ ];
26
46
  function parseActivityLog(content) {
27
47
  const lines = content.split('\n');
28
48
  let i = 0;
@@ -82,6 +102,66 @@ function pairEntries(entries) {
82
102
  }
83
103
  return rows;
84
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
+ }
85
165
  function log(args = []) {
86
166
  if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
87
167
  process.stdout.write(USAGE);
@@ -108,18 +188,21 @@ function log(args = []) {
108
188
  return;
109
189
  }
110
190
  const steps = pairEntries(entries);
111
- const rows = steps.map((s, idx) => [
112
- String(idx + 1),
113
- s.step,
114
- s.agent,
115
- s.started,
116
- s.done || (s.started ? '(in progress)' : ''),
117
- s.note
118
- ]);
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
+ });
119
202
  for (const line of formatTable(TABLE_HEADERS, rows, { zebra: Boolean(process.stdout.isTTY) })) {
120
203
  process.stdout.write(`${line}\n`);
121
204
  }
122
205
  process.stdout.write(`Total: ${steps.length} steps\n`);
123
206
  }
124
- export { log, parseActivityLog, pairEntries };
207
+ export { log, parseActivityLog, pairEntries, isHumanAgent };
125
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);
@@ -0,0 +1,66 @@
1
+ import { parse } from 'yaml';
2
+ // Field id -> task value mapping (single source of truth, per HD-1).
3
+ // Only ids that map cleanly to title / description / requirements get a value;
4
+ // every other text field gets `N/A`. This deliberately tightens the older
5
+ // "suggested" mapping table (impact / context / alternatives / steps / expected
6
+ // no longer flow into requirements) so the requirements checklist is never
7
+ // pushed into an unrelated field.
8
+ const TITLE_IDS = new Set(['summary', 'title']);
9
+ const DESCRIPTION_IDS = new Set([
10
+ 'description',
11
+ 'problem',
12
+ 'what-happened',
13
+ 'question',
14
+ 'current-content',
15
+ 'issue-description',
16
+ 'detailed-description'
17
+ ]);
18
+ const REQUIREMENTS_IDS = new Set(['requirements', 'solution', 'suggested-content']);
19
+ // Only free-text field types carry task content; structural / static fields are skipped.
20
+ const TEXT_FIELD_TYPES = new Set(['input', 'textarea']);
21
+ const PLACEHOLDER = 'N/A';
22
+ function mapFieldValue(id, fields) {
23
+ if (TITLE_IDS.has(id))
24
+ return fields.title;
25
+ if (DESCRIPTION_IDS.has(id))
26
+ return fields.description;
27
+ if (REQUIREMENTS_IDS.has(id))
28
+ return fields.requirements;
29
+ return '';
30
+ }
31
+ /**
32
+ * Render the final Issue body for a GitHub Issue Form (scenario A).
33
+ *
34
+ * Walks the form's `body[]` in order, skips `markdown` / `dropdown` /
35
+ * `checkboxes`, and renders each `input` / `textarea` as `### {label}` followed
36
+ * by the deterministically-mapped task value (or `N/A` when the field has no
37
+ * reliable source). The template structure is preserved; the whole task.md and
38
+ * its scaffolding sections are never emitted.
39
+ *
40
+ * Throws on unreadable / non-object YAML or a missing `body[]` list so the
41
+ * caller can fall back to the default body.
42
+ */
43
+ function renderTemplateBody(formText, fields) {
44
+ const doc = parse(formText);
45
+ if (!doc || typeof doc !== 'object' || !Array.isArray(doc.body)) {
46
+ throw new Error('Issue Form has no body[] list');
47
+ }
48
+ const sections = [];
49
+ for (const raw of doc.body) {
50
+ if (!raw || typeof raw !== 'object')
51
+ continue;
52
+ const type = typeof raw.type === 'string' ? raw.type : '';
53
+ if (!TEXT_FIELD_TYPES.has(type))
54
+ continue;
55
+ const label = typeof raw.attributes?.label === 'string' ? raw.attributes.label.trim() : '';
56
+ if (!label)
57
+ continue;
58
+ const id = typeof raw.id === 'string' ? raw.id : '';
59
+ const mapped = mapFieldValue(id, fields);
60
+ const value = mapped.trim() === '' ? PLACEHOLDER : mapped;
61
+ sections.push(`### ${label}\n\n${value}`);
62
+ }
63
+ return `${sections.join('\n\n')}\n`;
64
+ }
65
+ export { renderTemplateBody, mapFieldValue, PLACEHOLDER };
66
+ //# sourceMappingURL=issue-form.js.map
@@ -0,0 +1,44 @@
1
+ function escapeRegExp(value) {
2
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3
+ }
4
+ /**
5
+ * Return the body of the first `## {alias}` section (any alias matches), from
6
+ * the heading line to the next `## ` heading or EOF. Lines are preserved
7
+ * verbatim (checkbox text is never normalized); only leading/trailing blank
8
+ * lines are trimmed. Returns '' when no alias heading is present.
9
+ */
10
+ function extractSection(content, aliases) {
11
+ const lines = content.split('\n');
12
+ let start = -1;
13
+ for (let i = 0; i < lines.length; i += 1) {
14
+ const line = lines[i].trim();
15
+ if (aliases.some((alias) => new RegExp(`^##\\s+${escapeRegExp(alias)}\\s*$`).test(line))) {
16
+ start = i + 1;
17
+ break;
18
+ }
19
+ }
20
+ if (start === -1)
21
+ return '';
22
+ let end = lines.length;
23
+ for (let i = start; i < lines.length; i += 1) {
24
+ if (/^##\s+/.test(lines[i])) {
25
+ end = i;
26
+ break;
27
+ }
28
+ }
29
+ return lines.slice(start, end).join('\n').replace(/^\n+/, '').replace(/\n+$/, '');
30
+ }
31
+ /**
32
+ * Return the heading alias that actually appears as a `## {alias}` line, so a
33
+ * rendered section can mirror the source language. Falls back to the first
34
+ * alias when none is present.
35
+ */
36
+ function findSectionHeading(content, aliases) {
37
+ for (const alias of aliases) {
38
+ if (new RegExp(`^##\\s+${escapeRegExp(alias)}\\s*$`, 'm').test(content))
39
+ return alias;
40
+ }
41
+ return aliases[0];
42
+ }
43
+ export { extractSection, findSectionHeading };
44
+ //# sourceMappingURL=sections.js.map
@@ -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.ts';
6
4
  import { sandboxBranchLabel, sandboxLabel } from '../constants.ts';
7
5
  import { detectEngine } from '../engine.ts';
8
- import { resolveTools, toolProjectDirCandidates } from '../tools.ts';
9
6
  import { formatTable } from '../../table.ts';
10
7
  import { lookupShortIdByBranch } from '../../task/short-id.ts';
11
8
  import { fetchSandboxRows } from './list-running.ts';
@@ -20,7 +17,10 @@ id bound to each container's branch (via
20
17
  .agents/workspace/active/.short-ids.json), or '-' if no active task is
21
18
  bound. Pass the SHORT value to "ai sandbox exec" (e.g. 'ai sandbox exec 11').
22
19
  A '-' means no active task is bound to that branch, so the sandbox is free
23
- to remove with "ai sandbox rm <branch>".`;
20
+ to remove with "ai sandbox rm <branch>".
21
+
22
+ Use "ai sandbox show <ref>" for a single sandbox's worktree and per-tool
23
+ state paths.`;
24
24
 
25
25
  const CONTAINER_TABLE_HEADERS = ['#', 'SHORT', 'NAMES', 'STATUS', 'BRANCH'] as const;
26
26
 
@@ -40,14 +40,6 @@ export function formatContainerTable(rows: ContainerTableRow[], zebra = false):
40
40
  );
41
41
  }
42
42
 
43
- function listChildren(dir: string): string[] {
44
- if (!fs.existsSync(dir)) {
45
- return [];
46
- }
47
-
48
- return fs.readdirSync(dir).sort().map((entry) => path.join(dir, entry));
49
- }
50
-
51
43
  export function ls(args: string[] = []): void {
52
44
  if (args.length > 0 && (args[0] === '--help' || args[0] === '-h')) {
53
45
  process.stdout.write(`${USAGE}\n`);
@@ -56,7 +48,6 @@ export function ls(args: string[] = []): void {
56
48
 
57
49
  const config = loadConfig();
58
50
  const engine = detectEngine(config);
59
- const tools = resolveTools(config);
60
51
  const label = sandboxLabel(config);
61
52
  const { running, nonRunning } = fetchSandboxRows(engine, label, sandboxBranchLabel(config));
62
53
 
@@ -87,27 +78,4 @@ export function ls(args: string[] = []): void {
87
78
  );
88
79
  }
89
80
  }
90
-
91
- p.log.step('Worktrees');
92
- const worktrees = listChildren(config.worktreeBase);
93
- if (worktrees.length === 0) {
94
- p.log.warn(' No sandbox worktrees');
95
- } else {
96
- for (const worktree of worktrees) {
97
- process.stdout.write(` ${worktree}\n`);
98
- }
99
- }
100
-
101
- for (const tool of tools) {
102
- p.log.step(`${tool.name} state`);
103
- const entries = toolProjectDirCandidates(tool, config.project)
104
- .flatMap((dir) => listChildren(dir));
105
- if (entries.length === 0) {
106
- p.log.warn(` No ${tool.name} sandbox state`);
107
- continue;
108
- }
109
- for (const entry of entries) {
110
- process.stdout.write(` ${entry}\n`);
111
- }
112
- }
113
81
  }