@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
@@ -4,16 +4,23 @@ import { resolveTaskRef } from '../resolve-ref.ts';
4
4
 
5
5
  const USAGE = `Usage: ai task log <N | #N | TASK-id>
6
6
 
7
- Renders a task's activity log as a chronological timeline table.
7
+ Renders a task's activity log as a per-step status table. A step's start and
8
+ completion are paired onto one row: STARTED holds the start time, DONE the
9
+ completion time (or '(in progress)' while still running).
8
10
  <ref> Bare numeric / '#N' short id, or a full TASK-YYYYMMDD-HHMMSS id.
9
11
 
10
- Columns: # (timeline position) / TIME / STEP / AGENT / NOTE
12
+ Columns: # (row) / STEP / AGENT / STARTED / DONE / NOTE
13
+ A human-executed step shows AGENT as 'human' and, when it has no start marker,
14
+ a '-' STARTED placeholder. Review-step NOTE also carries two human counts in
15
+ the verdict list, right after blockers/major/minor: manual-verify (env-blocked)
16
+ and human-decision (current ledger stage total).
11
17
  `;
12
18
 
13
- const TABLE_HEADERS = ['#', 'TIME', 'STEP', 'AGENT', 'NOTE'] as const;
19
+ const TABLE_HEADERS = ['#', 'STEP', 'AGENT', 'STARTED', 'DONE', 'NOTE'] as const;
14
20
 
15
21
  // The activity-log H2 heading is language-dependent (zh template / en template).
16
22
  const HEADING_RE = /^##\s+(活动日志|Activity Log)\s*$/;
23
+ const LEDGER_HEADING_RE = /^##\s+(审查分歧账本|Review Disagreement Ledger)\s*$/;
17
24
  const NEXT_H2_RE = /^##\s/;
18
25
  // `- {time} — **{step}** by {agent} — {note}` ; the separator is an em-dash
19
26
  // (U+2014). STEP/AGENT are non-greedy so a note that itself contains ' — ' or
@@ -22,6 +29,34 @@ const ENTRY_RE =
22
29
  /^- (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}) — \*\*(.+?)\*\* by (.+?) — (.*)$/;
23
30
 
24
31
  type LogEntry = { time: string; step: string; agent: string; note: string };
32
+ type ReviewStage = 'analysis' | 'plan' | 'code';
33
+ type LedgerRow = { stage: string; status: string };
34
+
35
+ // One rendered row = one step instance. `started`/`done` are timestamps; an empty
36
+ // `done` with a non-empty `started` means the step is still in flight, while an
37
+ // empty `started` is a historical done-only entry (no start marker was written).
38
+ type StepRow = { step: string; agent: string; started: string; done: string; note: string };
39
+
40
+ // A start marker reuses the normal entry grammar and only suffixes its action
41
+ // with ` [started]`; the matching done entry carries the identical base action
42
+ // without the suffix. Pairing therefore keys on the base action (including any
43
+ // `(Round N)`), so every round and every repeated execution pairs on its own.
44
+ const STARTED_SUFFIX_RE = /\s*\[started\]\s*$/;
45
+ // Short agent tokens that actually appear in activity logs for AI executors
46
+ // (workflow recommended_agents claude/codex/gemini/cursor + enabled TUI opencode;
47
+ // note these differ from the `.airc.json` long names claude-code/gemini-cli).
48
+ // Any other executor token (a human name, possibly CJK) is treated as human.
49
+ const KNOWN_AI_AGENTS = new Set(['claude', 'codex', 'gemini', 'opencode', 'cursor']);
50
+ const HUMAN_DECISION_STATUSES = new Set(['needs-human-decision', 'human-decided']);
51
+ const ENV_BLOCKED_RE = /\(\+\s*(\d+)\s+env-blocked\)/i;
52
+ // Same match plus any leading whitespace, so folding the count into the verdict
53
+ // text drops the redundant `(+ n env-blocked)` fragment without leaving a gap.
54
+ const ENV_BLOCKED_STRIP_RE = /\s*\(\+\s*\d+\s+env-blocked\)/i;
55
+ const REVIEW_STAGE_PREFIXES: { prefix: string; stage: ReviewStage }[] = [
56
+ { prefix: 'Review Analysis', stage: 'analysis' },
57
+ { prefix: 'Review Plan', stage: 'plan' },
58
+ { prefix: 'Review Code', stage: 'code' }
59
+ ];
25
60
 
26
61
  function parseActivityLog(content: string): { sectionFound: boolean; entries: LogEntry[] } {
27
62
  const lines = content.split('\n');
@@ -44,6 +79,100 @@ function parseActivityLog(content: string): { sectionFound: boolean; entries: Lo
44
79
  return { sectionFound: true, entries: parsed.map((p) => p.entry) };
45
80
  }
46
81
 
82
+ // Collapse a chronological entry list into per-step rows: a `[started]` marker
83
+ // opens a row, the next matching done entry fills it in place (FIFO per base
84
+ // action). Started-only rows stay in flight; done-only entries (legacy logs with
85
+ // no start marker) render as standalone rows. Result order = first-seen order,
86
+ // which is already ascending because `entries` is sorted ascending.
87
+ function pairEntries(entries: LogEntry[]): StepRow[] {
88
+ const rows: StepRow[] = [];
89
+ const open = new Map<string, StepRow[]>();
90
+ for (const e of entries) {
91
+ const isStarted = STARTED_SUFFIX_RE.test(e.step);
92
+ const base = e.step.replace(STARTED_SUFFIX_RE, '');
93
+ if (isStarted) {
94
+ const row: StepRow = { step: base, agent: e.agent, started: e.time, done: '', note: e.note };
95
+ rows.push(row);
96
+ const queue = open.get(base);
97
+ if (queue) queue.push(row);
98
+ else open.set(base, [row]);
99
+ } else {
100
+ const pending = open.get(base)?.shift();
101
+ if (pending) {
102
+ // Done fills the open row; the done entry carries the meaningful note.
103
+ pending.done = e.time;
104
+ pending.agent = e.agent;
105
+ pending.note = e.note;
106
+ } else {
107
+ rows.push({ step: base, agent: e.agent, started: '', done: e.time, note: e.note });
108
+ }
109
+ }
110
+ }
111
+ return rows;
112
+ }
113
+
114
+ function parseReviewLedger(content: string): LedgerRow[] {
115
+ const lines = content.split('\n');
116
+ let i = 0;
117
+ while (i < lines.length && !LEDGER_HEADING_RE.test(lines[i]!)) i += 1;
118
+ if (i >= lines.length) return [];
119
+
120
+ const rows: LedgerRow[] = [];
121
+ for (let j = i + 1; j < lines.length; j += 1) {
122
+ if (NEXT_H2_RE.test(lines[j]!)) break;
123
+ const line = lines[j]!.trim();
124
+ if (!line.startsWith('|')) continue;
125
+ const cells = line
126
+ .split('|')
127
+ .slice(1, -1)
128
+ .map((cell) => cell.trim());
129
+ if (cells.length < 6 || cells[0] === 'id' || /^-+$/.test(cells[0] ?? '')) continue;
130
+ rows.push({ stage: cells[1]!, status: cells[4]! });
131
+ }
132
+ return rows;
133
+ }
134
+
135
+ function countHumanDecisionsByStage(rows: LedgerRow[]): Map<ReviewStage, number> {
136
+ const counts = new Map<ReviewStage, number>();
137
+ for (const row of rows) {
138
+ if (!isReviewStage(row.stage) || !HUMAN_DECISION_STATUSES.has(row.status)) continue;
139
+ counts.set(row.stage, (counts.get(row.stage) ?? 0) + 1);
140
+ }
141
+ return counts;
142
+ }
143
+
144
+ function isReviewStage(stage: string): stage is ReviewStage {
145
+ return stage === 'analysis' || stage === 'plan' || stage === 'code';
146
+ }
147
+
148
+ function reviewStageForStep(step: string): ReviewStage | undefined {
149
+ return REVIEW_STAGE_PREFIXES.find(({ prefix }) => step.startsWith(prefix))?.stage;
150
+ }
151
+
152
+ function humanValidationCount(note: string): number {
153
+ const match = ENV_BLOCKED_RE.exec(note);
154
+ return match ? Number(match[1]) : 0;
155
+ }
156
+
157
+ // A step is human-executed when its agent token is not a known AI token. Take
158
+ // the first whitespace-delimited token and drop any trailing parenthetical
159
+ // annotation (e.g. `张三 (executed on host)` -> `张三`) before the lookup.
160
+ function isHumanAgent(agent: string): boolean {
161
+ const token = agent.trim().split(/\s+/)[0]?.replace(/\(.*$/, '') ?? '';
162
+ return token !== '' && !KNOWN_AI_AGENTS.has(token);
163
+ }
164
+
165
+ // Fold the two human counts into a review row's verdict NOTE: comma-joined, right
166
+ // after the blockers/major/minor list and before the ` → artifact` link, mirroring
167
+ // the review count line. The raw `(+ n env-blocked)` fragment is dropped so the
168
+ // env-blocked number is not shown twice (it becomes the manual-verify count).
169
+ function foldHumanCounts(note: string, decisions: number, envBlocked: number): string {
170
+ const base = note.replace(ENV_BLOCKED_STRIP_RE, '');
171
+ const group = `Manual-verify: ${envBlocked}, Human-decision: ${decisions}`;
172
+ const arrow = base.indexOf(' → ');
173
+ return arrow === -1 ? `${base}, ${group}` : `${base.slice(0, arrow)}, ${group}${base.slice(arrow)}`;
174
+ }
175
+
47
176
  function log(args: string[] = []): void {
48
177
  if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
49
178
  process.stdout.write(USAGE);
@@ -70,11 +199,22 @@ function log(args: string[] = []): void {
70
199
  process.exitCode = 1;
71
200
  return;
72
201
  }
73
- const rows = entries.map((e, idx) => [String(idx + 1), e.time, e.step, e.agent, e.note]);
202
+ const steps = pairEntries(entries);
203
+ const humanDecisionCounts = countHumanDecisionsByStage(parseReviewLedger(content));
204
+ const rows = steps.map((s, idx) => {
205
+ const stage = reviewStageForStep(s.step);
206
+ const note = stage
207
+ ? foldHumanCounts(s.note, humanDecisionCounts.get(stage) ?? 0, humanValidationCount(s.note))
208
+ : s.note;
209
+ const human = isHumanAgent(s.agent);
210
+ const agent = human ? 'human' : s.agent;
211
+ const started = s.started || (human ? '-' : '');
212
+ return [String(idx + 1), s.step, agent, started, s.done || (s.started ? '(in progress)' : ''), note];
213
+ });
74
214
  for (const line of formatTable(TABLE_HEADERS, rows, { zebra: Boolean(process.stdout.isTTY) })) {
75
215
  process.stdout.write(`${line}\n`);
76
216
  }
77
- process.stdout.write(`Total: ${entries.length} entries\n`);
217
+ process.stdout.write(`Total: ${steps.length} steps\n`);
78
218
  }
79
219
 
80
- export { log, parseActivityLog };
220
+ export { log, parseActivityLog, pairEntries, isHumanAgent };
package/lib/task/index.ts CHANGED
@@ -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
@@ -53,6 +56,11 @@ export async function runTask(args: string[]): Promise<void> {
53
56
  grep(rest);
54
57
  break;
55
58
  }
59
+ case 'issue-body': {
60
+ const { issueBody } = await import('./commands/issue-body.ts');
61
+ issueBody(rest);
62
+ break;
63
+ }
56
64
  case 'log': {
57
65
  const { log } = await import('./commands/log.ts');
58
66
  log(rest);
@@ -0,0 +1,77 @@
1
+ import { parse } from 'yaml';
2
+
3
+ type TaskFields = {
4
+ title: string;
5
+ description: string;
6
+ requirements: string;
7
+ };
8
+
9
+ // Field id -> task value mapping (single source of truth, per HD-1).
10
+ // Only ids that map cleanly to title / description / requirements get a value;
11
+ // every other text field gets `N/A`. This deliberately tightens the older
12
+ // "suggested" mapping table (impact / context / alternatives / steps / expected
13
+ // no longer flow into requirements) so the requirements checklist is never
14
+ // pushed into an unrelated field.
15
+ const TITLE_IDS = new Set(['summary', 'title']);
16
+ const DESCRIPTION_IDS = new Set([
17
+ 'description',
18
+ 'problem',
19
+ 'what-happened',
20
+ 'question',
21
+ 'current-content',
22
+ 'issue-description',
23
+ 'detailed-description'
24
+ ]);
25
+ const REQUIREMENTS_IDS = new Set(['requirements', 'solution', 'suggested-content']);
26
+
27
+ // Only free-text field types carry task content; structural / static fields are skipped.
28
+ const TEXT_FIELD_TYPES = new Set(['input', 'textarea']);
29
+ const PLACEHOLDER = 'N/A';
30
+
31
+ function mapFieldValue(id: string, fields: TaskFields): string {
32
+ if (TITLE_IDS.has(id)) return fields.title;
33
+ if (DESCRIPTION_IDS.has(id)) return fields.description;
34
+ if (REQUIREMENTS_IDS.has(id)) return fields.requirements;
35
+ return '';
36
+ }
37
+
38
+ type IssueFormField = {
39
+ type?: unknown;
40
+ id?: unknown;
41
+ attributes?: { label?: unknown };
42
+ };
43
+
44
+ /**
45
+ * Render the final Issue body for a GitHub Issue Form (scenario A).
46
+ *
47
+ * Walks the form's `body[]` in order, skips `markdown` / `dropdown` /
48
+ * `checkboxes`, and renders each `input` / `textarea` as `### {label}` followed
49
+ * by the deterministically-mapped task value (or `N/A` when the field has no
50
+ * reliable source). The template structure is preserved; the whole task.md and
51
+ * its scaffolding sections are never emitted.
52
+ *
53
+ * Throws on unreadable / non-object YAML or a missing `body[]` list so the
54
+ * caller can fall back to the default body.
55
+ */
56
+ function renderTemplateBody(formText: string, fields: TaskFields): string {
57
+ const doc = parse(formText) as { body?: unknown } | null;
58
+ if (!doc || typeof doc !== 'object' || !Array.isArray(doc.body)) {
59
+ throw new Error('Issue Form has no body[] list');
60
+ }
61
+ const sections: string[] = [];
62
+ for (const raw of doc.body as IssueFormField[]) {
63
+ if (!raw || typeof raw !== 'object') continue;
64
+ const type = typeof raw.type === 'string' ? raw.type : '';
65
+ if (!TEXT_FIELD_TYPES.has(type)) continue;
66
+ const label = typeof raw.attributes?.label === 'string' ? raw.attributes.label.trim() : '';
67
+ if (!label) continue;
68
+ const id = typeof raw.id === 'string' ? raw.id : '';
69
+ const mapped = mapFieldValue(id, fields);
70
+ const value = mapped.trim() === '' ? PLACEHOLDER : mapped;
71
+ sections.push(`### ${label}\n\n${value}`);
72
+ }
73
+ return `${sections.join('\n\n')}\n`;
74
+ }
75
+
76
+ export { renderTemplateBody, mapFieldValue, PLACEHOLDER };
77
+ export type { TaskFields };
@@ -0,0 +1,44 @@
1
+ function escapeRegExp(value: string): string {
2
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3
+ }
4
+
5
+ /**
6
+ * Return the body of the first `## {alias}` section (any alias matches), from
7
+ * the heading line to the next `## ` heading or EOF. Lines are preserved
8
+ * verbatim (checkbox text is never normalized); only leading/trailing blank
9
+ * lines are trimmed. Returns '' when no alias heading is present.
10
+ */
11
+ function extractSection(content: string, aliases: string[]): string {
12
+ const lines = content.split('\n');
13
+ let start = -1;
14
+ for (let i = 0; i < lines.length; i += 1) {
15
+ const line = lines[i]!.trim();
16
+ if (aliases.some((alias) => new RegExp(`^##\\s+${escapeRegExp(alias)}\\s*$`).test(line))) {
17
+ start = i + 1;
18
+ break;
19
+ }
20
+ }
21
+ if (start === -1) 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
+ /**
33
+ * Return the heading alias that actually appears as a `## {alias}` line, so a
34
+ * rendered section can mirror the source language. Falls back to the first
35
+ * alias when none is present.
36
+ */
37
+ function findSectionHeading(content: string, aliases: string[]): string {
38
+ for (const alias of aliases) {
39
+ if (new RegExp(`^##\\s+${escapeRegExp(alias)}\\s*$`, 'm').test(content)) return alias;
40
+ }
41
+ return aliases[0]!;
42
+ }
43
+
44
+ export { extractSection, findSectionHeading };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fitlab-ai/agent-infra",
3
- "version": "0.7.5",
3
+ "version": "0.7.7",
4
4
  "description": "Bootstrap tool for AI multi-tool collaboration infrastructure — works with Claude Code, Codex, Gemini CLI, and OpenCode",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -47,7 +47,8 @@
47
47
  "cross-spawn": "^7.0.6",
48
48
  "picocolors": "1.1.1",
49
49
  "semver": "^7.8.1",
50
- "smol-toml": "^1.6.1"
50
+ "smol-toml": "^1.6.1",
51
+ "yaml": "^2.9.0"
51
52
  },
52
53
  "scripts": {
53
54
  "build": "tsc -p tsconfig.json && node scripts/build.js && node scripts/build-inline.js",
@@ -163,11 +163,13 @@ Recommended frontmatter:
163
163
  ```yaml
164
164
  ---
165
165
  name: enforce-style
166
- description: "Apply the team style guide before code review"
166
+ description: "Apply the team style guide before code review. Use when you need to align the team's code style before review"
167
167
  args: "<task-id>" # optional
168
168
  ---
169
169
  ```
170
170
 
171
+ Write `description` as "one-line responsibility + scenario clause": after the short responsibility, append "Use when …" (the Chinese `SKILL.zh-CN.md` variant uses "当……时使用") as the cross-TUI trigger semantics, so Agent Skills-capable tools can self-discover the skill in natural conversation. Do **not** add a separate `triggers` (or similar) frontmatter field for this.
172
+
171
173
  After adding or updating a custom skill, run `update-agent-infra` again. The sync step detects non-built-in skills and generates matching commands for Claude Code, Gemini CLI, and OpenCode automatically.
172
174
 
173
175
  ### Shared skill sources
@@ -163,11 +163,13 @@
163
163
  ```yaml
164
164
  ---
165
165
  name: enforce-style
166
- description: "在代码审查前应用团队风格规范"
166
+ description: "在代码审查前应用团队风格规范。当需要在评审前统一团队代码风格时使用"
167
167
  args: "<task-id>" # 可选
168
168
  ---
169
169
  ```
170
170
 
171
+ `description` 采用「一句话职责 + 场景触发子句」写法:在简短职责描述后补充「当……时使用」(英文 `SKILL.en.md` 用「Use when …」),作为跨 TUI 的触发语义,供支持 Agent Skills 的工具在自然对话中自发现该 skill。**不要**为此新增 `triggers` 等额外 frontmatter 字段。
172
+
171
173
  新增或修改自定义 skill 后,再执行一次 `update-agent-infra`。同步过程会自动检测非内置 skill,并为 Claude Code、Gemini CLI、OpenCode 生成对应命令。
172
174
 
173
175
  ### 共享 skill 源
@@ -22,11 +22,11 @@ After `create-task` writes the local `task.md`, follow this rule to cascade Issu
22
22
 
23
23
  Pull the following from `task.md`:
24
24
 
25
- - Task title (the first `# ` heading, stripped of `任务:` / `Task:` prefixes)
26
- - The `## Description` / `## 描述` section
27
- - The `## Requirements` / `## 需求` section
25
+ - Task title (the first `# ` heading, stripped of `任务:` / `Task:` prefixes) — used to build the Issue title
28
26
  - frontmatter fields `type` and (optionally) `milestone`
29
27
 
28
+ > The Issue **body** is not extracted by hand here. It is generated deterministically in §3 by the `ai task issue-body` command from the `## 描述` / `## 需求` sections; callers must not assemble it themselves.
29
+
30
30
  Build the Issue title:
31
31
 
32
32
  | task.md `type` | Conventional Commits type |
@@ -41,45 +41,35 @@ Scope inference: read known module names from `.agents/.airc.json`'s `labels.in`
41
41
 
42
42
  ### 3. Build the Issue Body
43
43
 
44
+ > **Mechanization boundary (mandatory)**: the Issue body is always produced deterministically by the `ai task issue-body` command; callers only pass the command's stdout to `gh issue create` via `--body-file` and must **not** assemble, rewrite, or truncate the body themselves. The command emits only the task title / `## 描述` / `## 需求` content; every other task.md scaffolding section never enters the body.
45
+
44
46
  Issue Form detection: follow the "Issue Template Detection" section in `.agents/rules/issue-pr-commands.md` to scan `.github/ISSUE_TEMPLATE/*.yml` (excluding `config.yml`).
45
47
 
46
48
  #### Scenario A: A matching template was detected
47
49
 
48
50
  Pick the form whose `name` (or filename) best matches the task type (e.g., a task with `type: bugfix` prefers a form whose name contains `bug`); if no match, fall back to a generic form like `other.yml`; if none, take the first form in the directory.
49
51
 
50
- Populate the form by following the field-handling rules in `.agents/rules/issue-pr-commands.md` § "Issue Template Detection":
51
-
52
- - `textarea` / `input` fields: use `attributes.label` as a markdown heading and pull values from task.md
53
- - `markdown` fields: skip (these are description blurbs)
54
- - `dropdown` / `checkboxes` fields: skip
52
+ Once the form file `{form-path}` is chosen, let the command render the final body from that Issue Form and write it to the body file `{body-file}` for §5:
55
53
 
56
- Recommended field-to-source mapping:
57
-
58
- | Template field hint | Source in task.md |
59
- |---|---|
60
- | `summary`, `title` | task title |
61
- | `description`, `problem`, `what happened`, `issue-description`, `current-content` | task description |
62
- | `solution`, `requirements`, `steps`, `suggested-content`, `impact`, `context`, `alternatives`, `expected` | requirements list (preserve checked / unchecked state as-is) |
63
- | Other `textarea` / `input` fields | task description, or `N/A` if missing |
54
+ ```bash
55
+ ai task issue-body {task-id} --template "{form-path}" > "{body-file}"
56
+ ```
64
57
 
65
- Whenever task.md does not provide a usable value, write `N/A`.
58
+ The command skips `markdown` / `dropdown` / `checkboxes` fields and, for `input` / `textarea` fields, uses `attributes.label` as the heading and deterministically fills the task title / description / requirements by field `id`, writing `N/A` for fields with no reliable source (the field mapping table is the single source of truth inside the command and is not restated here). When the command exits non-zero (missing file / invalid YAML / no `body`), regenerate `{body-file}` with the Scenario B command instead.
66
59
 
67
60
  #### Scenario B: No template, or template parsing failed
68
61
 
69
- Fall back to the default body:
70
-
71
- ```markdown
72
- ## Description
62
+ Let the command emit the default body (only `## 描述` + `## 需求`, checkbox text preserved verbatim, missing sections filled with `N/A`) into `{body-file}`:
73
63
 
74
- {task description, or N/A if missing}
64
+ ```bash
65
+ ai task issue-body {task-id} > "{body-file}"
66
+ ```
75
67
 
76
- ## Requirements
68
+ #### Red line: never use the entire task.md as the body
77
69
 
78
- - [ ] {requirement-1}
79
- - [ ] {requirement-2}
80
- ```
70
+ In both Scenario A and B, the body may only come from `ai task issue-body` stdout and contains **only the description + requirements content** (Scenario A is the equivalent content mapped onto template fields).
81
71
 
82
- If the requirements list is empty, write `N/A` in that section.
72
+ Wrong example (❌ forbidden): pasting the entire task.md including scaffolding sections such as `## 分析` / `## 设计` / `## 实现备注` / `## 审查反馈` / `## 审查分歧账本` / `## 人工裁决` / `## 活动日志` / `## 完成检查清单` and `#XXX` placeholders directly as the Issue body. Those sections only go to the `sync-issue:{task-id}:task` comment, never the body.
83
73
 
84
74
  ### 4. Resolve labels / Issue Type / milestone
85
75
 
@@ -137,17 +127,18 @@ When a milestone is selected, pass the release line (or `General Backlog` or the
137
127
 
138
128
  ### 5. Call the GitHub CLI to Create the Issue
139
129
 
140
- Run the "Create Issue" command from `.agents/rules/issue-pr-commands.md`:
130
+ Run the "Create Issue" command from `.agents/rules/issue-pr-commands.md`; always use the `{body-file}` produced in §3, overriding the generic command's `--body`:
141
131
 
142
132
  ```bash
143
133
  gh issue create -R "$upstream_repo" \
144
134
  --title "{title}" \
145
- --body "{body}" \
135
+ --body-file "{body-file}" \
146
136
  --assignee @me \
147
137
  {label-args} \
148
138
  {milestone-arg}
149
139
  ```
150
140
 
141
+ - `{body-file}` is the body file produced in §3 by `ai task issue-body`; do **not** switch back to `--body` and assemble the body by hand
151
142
  - `{label-args}` is expanded from the result of §4 into multiple `--label "..."`; if empty, omit the entire argument
152
143
  - `{milestone-arg}` is only expanded to `--milestone "..."` when `has_triage=true` and milestone is non-empty; otherwise omit
153
144
  - `--assignee @me` requires no permission probe; on failure, skip silently
@@ -22,11 +22,11 @@
22
22
 
23
23
  从 `task.md` 提取以下字段:
24
24
 
25
- - 任务标题(首个 `# ` 标题,去掉 `任务:` / `Task:` 前缀)
26
- - `## Description` / `## 描述` 段落
27
- - `## Requirements` / `## 需求` 段落
25
+ - 任务标题(首个 `# ` 标题,去掉 `任务:` / `Task:` 前缀)—— 用于构造 Issue 标题
28
26
  - frontmatter 中的 `type` 与(可选的)`milestone`
29
27
 
28
+ > Issue **正文**不在此处手工提取。正文由 §3 调用 `ai task issue-body` 命令从 `## 描述` / `## 需求` 段确定性生成,调用方不得自行拼装。
29
+
30
30
  构造 Issue 标题:
31
31
 
32
32
  | task.md `type` | Conventional Commits type |
@@ -41,45 +41,35 @@ scope 推断:从 `.agents/.airc.json` 的 `labels.in` 字段读取已知模块
41
41
 
42
42
  ### 3. 构建 Issue 正文
43
43
 
44
+ > **机械化边界(必须遵守)**:Issue 正文一律由 `ai task issue-body` 命令确定性生成;调用方只把命令 stdout 作为 `gh issue create` 的 `--body-file` 传入,**不得**自行拼装、改写或截断正文。命令只输出 task 标题 / `## 描述` / `## 需求` 对应内容,task.md 其余脚手架段落永不进入 body。
45
+
44
46
  Issue 模板检测:按 `.agents/rules/issue-pr-commands.md` 的 "Issue 模板检测" 规则扫描 `.github/ISSUE_TEMPLATE/*.yml`(排除 `config.yml`)。
45
47
 
46
48
  #### 场景 A:检测到匹配模板
47
49
 
48
50
  按模板顶层 `name` 与任务类型挑选最匹配的 form(如任务 `type: bugfix` 优先选名称含 `bug` 的模板);找不到匹配时,回退到通用 form(如 `other.yml`),仍找不到时取目录中第一个 form。
49
51
 
50
- `.agents/rules/issue-pr-commands.md` "Issue 模板检测" 章节的字段处理规则填充 form:
51
-
52
- - `textarea` / `input` 字段:使用 `attributes.label` 作为 markdown 标题,从 task.md 取值
53
- - `markdown` 字段:跳过(说明文案)
54
- - `dropdown` / `checkboxes` 字段:跳过
52
+ 确定模板文件 `{form-path}` 后,由命令按该 Issue Form 渲染最终正文,并写入正文文件 `{body-file}` 供 §5 使用:
55
53
 
56
- 字段值映射(建议):
57
-
58
- | 模板字段提示 | task.md 来源 |
59
- |---|---|
60
- | `summary`, `title` | 任务标题 |
61
- | `description`, `problem`, `what happened`, `issue-description`, `current-content` | 任务描述 |
62
- | `solution`, `requirements`, `steps`, `suggested-content`, `impact`, `context`, `alternatives`, `expected` | 需求列表(已勾选与未勾选混合,原样保留) |
63
- | 其它 `textarea` / `input` 字段 | 任务描述;缺失时填 `N/A` |
54
+ ```bash
55
+ ai task issue-body {task-id} --template "{form-path}" > "{body-file}"
56
+ ```
64
57
 
65
- 任何字段在 task.md 中找不到合适值时,写入 `N/A`。
58
+ 命令跳过 `markdown` / `dropdown` / `checkboxes` 字段,对 `input` / `textarea` 字段以 `attributes.label` 作标题、按字段 `id` 确定性填入 task 标题 / 描述 / 需求,无可靠映射源的字段填 `N/A`(字段映射表是命令内的单一真源,不在本规则重述)。命令退出码非 0(文件缺失 / 非法 YAML / 无 `body`)时,改用场景 B 命令重新生成 `{body-file}`。
66
59
 
67
60
  #### 场景 B:无模板或解析失败
68
61
 
69
- 回退到默认正文:
70
-
71
- ```markdown
72
- ## Description
62
+ 由命令输出默认正文(仅 `## 描述` + `## 需求`,复选框逐字保留,缺失段落填 `N/A`),写入 `{body-file}`:
73
63
 
74
- {任务描述;缺失时填 N/A}
64
+ ```bash
65
+ ai task issue-body {task-id} > "{body-file}"
66
+ ```
75
67
 
76
- ## Requirements
68
+ #### 红线:禁止把整份 task.md 当 body
77
69
 
78
- - [ ] {requirement-1}
79
- - [ ] {requirement-2}
80
- ```
70
+ 无论场景 A / B,body 只能来自 `ai task issue-body` stdout,**只含 描述 + 需求两段内容**(场景 A 为按模板字段映射后的等价内容)。
81
71
 
82
- 需求列表为空时,整段写 `N/A`。
72
+ 错误示范(❌ 禁止):把含 `## 分析` / `## 设计` / `## 实现备注` / `## 审查反馈` / `## 审查分歧账本` / `## 人工裁决` / `## 活动日志` / `## 完成检查清单` 等脚手架段落、以及 `#XXX` 占位的整份 task.md 直接作为 Issue body。这些段落只走 `sync-issue:{task-id}:task` 评论,绝不进 body。
83
73
 
84
74
  ### 4. 解析 labels / Issue Type / milestone
85
75
 
@@ -137,17 +127,18 @@ Issue 模板检测:按 `.agents/rules/issue-pr-commands.md` 的 "Issue 模板
137
127
 
138
128
  ### 5. 调用 GitHub CLI 创建 Issue
139
129
 
140
- 按 `.agents/rules/issue-pr-commands.md` 中的 "创建 Issue" 命令执行:
130
+ 按 `.agents/rules/issue-pr-commands.md` 中的 "创建 Issue" 命令执行;正文统一用 §3 生成的 `{body-file}`,覆盖通用命令的 `--body`:
141
131
 
142
132
  ```bash
143
133
  gh issue create -R "$upstream_repo" \
144
134
  --title "{title}" \
145
- --body "{body}" \
135
+ --body-file "{body-file}" \
146
136
  --assignee @me \
147
137
  {label-args} \
148
138
  {milestone-arg}
149
139
  ```
150
140
 
141
+ - `{body-file}` 为 §3 由 `ai task issue-body` 生成的正文文件;**不得**改用 `--body` 自行拼装正文
151
142
  - `{label-args}` 由 §4 计算结果展开为多个 `--label "..."`;为空则整体省略
152
143
  - `{milestone-arg}` 仅当 `has_triage=true` 且 milestone 非空时展开为 `--milestone "..."`;否则整体省略
153
144
  - `--assignee @me` 不做权限预判,失败时静默跳过
@@ -186,6 +186,12 @@ List PRs:
186
186
  gh pr list --state {state} --base {base-branch} --json number,title,url,headRefName,baseRefName
187
187
  ```
188
188
 
189
+ Find the open PR whose head is the current branch (used by the `commit` push wrap-up):
190
+
191
+ ```bash
192
+ gh pr list --head "{branch}" --state open --json number,url --jq '.[0].url // empty'
193
+ ```
194
+
189
195
  Create a PR:
190
196
 
191
197
  ```bash
@@ -186,6 +186,12 @@ gh pr view {pr-number} --json number,title,body,labels,state,milestone,url,files
186
186
  gh pr list --state {state} --base {base-branch} --json number,title,url,headRefName,baseRefName
187
187
  ```
188
188
 
189
+ 按 head 分支查询当前分支是否存在开放 PR(`commit` 推送收尾用):
190
+
191
+ ```bash
192
+ gh pr list --head "{branch}" --state open --json number,url --jq '.[0].url // empty'
193
+ ```
194
+
189
195
  创建 PR:
190
196
 
191
197
  ```bash
@@ -1,9 +1,10 @@
1
1
  # Next-Step Output Rule
2
2
 
3
- This file defines two **independent** rules for a skill's "notify-user / Next steps" output; read this file before rendering the final output and apply both:
3
+ This file defines three **independent** rules for a skill's "notify-user / Next steps" output (the 3rd applies to review-* only); read this file before rendering the final output and apply whichever rules apply:
4
4
 
5
5
  1. **Next-step output structure**: how "Next steps" commands and the "Task info" block present the task ID (placeholders / short-id lookup / fallback).
6
6
  2. **Agent output trailing line (Completed at)**: the **very last line** of user-facing output, **independent of the "Next steps" block**, applying to normal / error / early-return paths alike.
7
+ 3. **Pending human-decision pre-block**: applies only to `review-analysis` / `review-plan` / `review-code` when this stage has pending rulings (`{h} > 0`) — expand the pending items before the "Next steps" commands and prompt to resolve them first.
7
8
 
8
9
  ## Placeholder semantics
9
10
 
@@ -60,3 +61,27 @@ Completed at: YYYY-MM-DD HH:mm:ss
60
61
  - Value command (local timezone, no offset): `date "+%Y-%m-%d %H:%M:%S"`
61
62
  - Position: it must be the last line of the entire user-facing output, after all "Next steps" commands. If a scenario has a conditional reminder line after the commands (e.g. the env-blocked reminder), the completion line goes after that reminder.
62
63
  - This line is for terminal scanning only; it is never written to any artifact file or Issue/PR comment. The single source of truth for completion time remains the Activity Log in task.md.
64
+
65
+ ## Pending human-decision pre-block (review-* only, when {h} > 0)
66
+
67
+ This section is a **third standalone rule, co-equal with the two above**, used only by the "notify-user / report conclusion" step of `review-analysis` / `review-plan` / `review-code`.
68
+
69
+ `{h}` has the same meaning as the count line in each review skill's `reference/output-templates.md`: the number of rows in task.md `## 审查分歧账本` (Review Disagreement Ledger) for **this stage** (`stage ∈ {analysis|plan|code}`) whose `status = needs-human-decision` — **pending items only, excluding rows already `human-decided`**.
70
+
71
+ - **`{h} = 0`**: do not emit this block; render "Next steps" exactly as the selected output-templates scenario.
72
+ - **`{h} > 0`**: insert the block below **before** the selected scenario's "Next steps - <stage>" commands; the next-stage commands are still listed after the block.
73
+
74
+ ```text
75
+ ⚠️ Pending human decisions ({h}) — please rule on each before continuing to the next stage:
76
+ - {ledger-id} ({stage}/{severity}): {summary}
77
+ Location: the matching row in task.md `## 审查分歧账本` · Evidence: {evidence}
78
+ …(one entry per status=needs-human-decision row of this stage in task.md `## 审查分歧账本`)
79
+
80
+ To resolve:
81
+ 1. In the task.md `## 人工裁决` section, record your ruling and rationale for each item above.
82
+ 2. Flip the status of the matching row in `## 审查分歧账本` from `needs-human-decision` to `human-decided`.
83
+
84
+ Note: until all those rows are flipped to `human-decided`, running the next-stage command directly will be blocked by gates such as complete-task (`needs-human-decision` is non-terminal). The next-stage commands are still listed below for use after the decisions are made.
85
+ ```
86
+
87
+ Field values: `{ledger-id}` / `{stage}` / `{severity}` / `{evidence}` come from the same-named columns of the matching `## 审查分歧账本` row; `{summary}` comes from the artifact anchor referenced by `{evidence}` (e.g. the decision title at `plan.md#HD-1`), falling back to a one-line summary of the finding when no anchor title exists.