@fitlab-ai/agent-infra 0.7.6 → 0.8.0

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 (173) hide show
  1. package/README.md +2 -0
  2. package/README.zh-CN.md +2 -0
  3. package/bin/cli.ts +53 -18
  4. package/dist/bin/cli.js +56 -19
  5. package/dist/lib/decide.js +110 -0
  6. package/dist/lib/run/host.js +39 -0
  7. package/dist/lib/run/index.js +102 -0
  8. package/dist/lib/run/prompt.js +2 -0
  9. package/dist/lib/run/skills.js +29 -0
  10. package/dist/lib/run/tui.js +35 -0
  11. package/dist/lib/sandbox/capture.js +49 -0
  12. package/dist/lib/sandbox/commands/create.js +55 -6
  13. package/dist/lib/sandbox/commands/ls.js +4 -33
  14. package/dist/lib/sandbox/commands/show.js +67 -0
  15. package/dist/lib/sandbox/index.js +7 -0
  16. package/dist/lib/server/adapters/_contract.js +9 -0
  17. package/dist/lib/server/adapters/feishu/index.js +40 -0
  18. package/dist/lib/server/adapters/feishu/transport.js +95 -0
  19. package/dist/lib/server/auth.js +19 -0
  20. package/dist/lib/server/config.js +183 -0
  21. package/dist/lib/server/daemon.js +96 -0
  22. package/dist/lib/server/index.js +47 -0
  23. package/dist/lib/server/logger.js +34 -0
  24. package/dist/lib/server/plugin-loader.js +73 -0
  25. package/dist/lib/server/process-control.js +200 -0
  26. package/dist/lib/server/protocol.js +69 -0
  27. package/dist/lib/server/redact.js +12 -0
  28. package/dist/lib/server/runner.js +75 -0
  29. package/dist/lib/server/streamer.js +44 -0
  30. package/dist/lib/task/commands/decisions.js +258 -0
  31. package/dist/lib/task/commands/issue-body.js +94 -0
  32. package/dist/lib/task/commands/log.js +67 -9
  33. package/dist/lib/task/index.js +17 -0
  34. package/dist/lib/task/issue-form.js +66 -0
  35. package/dist/lib/task/ledger.js +66 -0
  36. package/dist/lib/task/sections.js +73 -0
  37. package/lib/decide.ts +124 -0
  38. package/lib/run/host.ts +47 -0
  39. package/lib/run/index.ts +124 -0
  40. package/lib/run/prompt.ts +1 -0
  41. package/lib/run/skills.ts +34 -0
  42. package/lib/run/tui.ts +40 -0
  43. package/lib/sandbox/capture.ts +84 -0
  44. package/lib/sandbox/commands/create.ts +61 -9
  45. package/lib/sandbox/commands/ls.ts +4 -36
  46. package/lib/sandbox/commands/show.ts +80 -0
  47. package/lib/sandbox/index.ts +7 -0
  48. package/lib/server/adapters/_contract.ts +42 -0
  49. package/lib/server/adapters/feishu/index.ts +47 -0
  50. package/lib/server/adapters/feishu/transport.ts +128 -0
  51. package/lib/server/auth.ts +28 -0
  52. package/lib/server/config.ts +228 -0
  53. package/lib/server/daemon.ts +110 -0
  54. package/lib/server/index.ts +50 -0
  55. package/lib/server/logger.ts +48 -0
  56. package/lib/server/plugin-loader.ts +84 -0
  57. package/lib/server/process-control.ts +225 -0
  58. package/lib/server/protocol.ts +79 -0
  59. package/lib/server/redact.ts +12 -0
  60. package/lib/server/runner.ts +95 -0
  61. package/lib/server/server.schema.json +127 -0
  62. package/lib/server/streamer.ts +57 -0
  63. package/lib/task/commands/decisions.ts +272 -0
  64. package/lib/task/commands/issue-body.ts +102 -0
  65. package/lib/task/commands/log.ts +73 -9
  66. package/lib/task/index.ts +17 -0
  67. package/lib/task/issue-form.ts +77 -0
  68. package/lib/task/ledger.ts +73 -0
  69. package/lib/task/sections.ts +73 -0
  70. package/package.json +9 -4
  71. package/templates/.agents/QUICKSTART.zh-CN.md +2 -2
  72. package/templates/.agents/README.en.md +3 -1
  73. package/templates/.agents/README.zh-CN.md +3 -1
  74. package/templates/.agents/rules/create-issue.github.en.md +20 -29
  75. package/templates/.agents/rules/create-issue.github.zh-CN.md +20 -29
  76. package/templates/.agents/rules/issue-fields.github.en.md +1 -1
  77. package/templates/.agents/rules/issue-fields.github.zh-CN.md +1 -1
  78. package/templates/.agents/rules/issue-pr-commands.github.en.md +6 -0
  79. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +6 -0
  80. package/templates/.agents/rules/next-step-output.en.md +30 -1
  81. package/templates/.agents/rules/next-step-output.zh-CN.md +30 -1
  82. package/templates/.agents/rules/no-mid-flow-questions.en.md +7 -2
  83. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +7 -2
  84. package/templates/.agents/rules/review-handshake.en.md +5 -1
  85. package/templates/.agents/rules/review-handshake.zh-CN.md +5 -1
  86. package/templates/.agents/rules/task-management.en.md +3 -1
  87. package/templates/.agents/rules/task-management.zh-CN.md +3 -1
  88. package/templates/.agents/scripts/lib/post-review-commit.js +15 -18
  89. package/templates/.agents/scripts/validate-artifact.js +8 -0
  90. package/templates/.agents/skills/analyze-task/SKILL.en.md +18 -4
  91. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +18 -4
  92. package/templates/.agents/skills/analyze-task/config/verify.en.json +2 -1
  93. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +2 -1
  94. package/templates/.agents/skills/archive-tasks/SKILL.en.md +3 -1
  95. package/templates/.agents/skills/archive-tasks/SKILL.zh-CN.md +3 -1
  96. package/templates/.agents/skills/block-task/SKILL.en.md +3 -1
  97. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +3 -1
  98. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -1
  99. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -1
  100. package/templates/.agents/skills/check-task/SKILL.en.md +30 -81
  101. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +30 -80
  102. package/templates/.agents/skills/close-codescan/SKILL.en.md +3 -1
  103. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +3 -1
  104. package/templates/.agents/skills/close-dependabot/SKILL.en.md +3 -1
  105. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +3 -1
  106. package/templates/.agents/skills/code-task/SKILL.en.md +4 -2
  107. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +4 -2
  108. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +7 -2
  109. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +7 -2
  110. package/templates/.agents/skills/code-task/scripts/detect-mode.js +6 -14
  111. package/templates/.agents/skills/commit/SKILL.en.md +27 -5
  112. package/templates/.agents/skills/commit/SKILL.zh-CN.md +27 -5
  113. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -1
  114. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -1
  115. package/templates/.agents/skills/complete-task/config/verify.en.json +2 -1
  116. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -1
  117. package/templates/.agents/skills/create-pr/SKILL.en.md +3 -1
  118. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +3 -1
  119. package/templates/.agents/skills/create-release-note/SKILL.en.md +3 -1
  120. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +3 -1
  121. package/templates/.agents/skills/create-task/SKILL.en.md +4 -4
  122. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +4 -4
  123. package/templates/.agents/skills/import-codescan/SKILL.en.md +3 -4
  124. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +3 -4
  125. package/templates/.agents/skills/import-dependabot/SKILL.en.md +3 -2
  126. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +3 -2
  127. package/templates/.agents/skills/import-issue/SKILL.en.md +20 -4
  128. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +20 -4
  129. package/templates/.agents/skills/init-labels/SKILL.en.md +3 -1
  130. package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +3 -1
  131. package/templates/.agents/skills/init-milestones/SKILL.en.md +3 -1
  132. package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +3 -1
  133. package/templates/.agents/skills/plan-task/SKILL.en.md +4 -3
  134. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -3
  135. package/templates/.agents/skills/post-release/SKILL.en.md +3 -1
  136. package/templates/.agents/skills/post-release/SKILL.zh-CN.md +3 -1
  137. package/templates/.agents/skills/refine-title/SKILL.en.md +3 -1
  138. package/templates/.agents/skills/refine-title/SKILL.zh-CN.md +3 -1
  139. package/templates/.agents/skills/release/SKILL.en.md +3 -1
  140. package/templates/.agents/skills/release/SKILL.zh-CN.md +3 -1
  141. package/templates/.agents/skills/restore-task/SKILL.en.md +3 -1
  142. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +3 -1
  143. package/templates/.agents/skills/review-analysis/SKILL.en.md +6 -2
  144. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +6 -2
  145. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +1 -1
  146. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +1 -1
  147. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +1 -1
  148. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +1 -2
  149. package/templates/.agents/skills/review-code/SKILL.en.md +5 -2
  150. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -2
  151. package/templates/.agents/skills/review-code/reference/output-templates.en.md +1 -1
  152. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +1 -1
  153. package/templates/.agents/skills/review-code/reference/report-template.en.md +1 -1
  154. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +1 -2
  155. package/templates/.agents/skills/review-plan/SKILL.en.md +6 -2
  156. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +6 -2
  157. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +1 -1
  158. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +1 -1
  159. package/templates/.agents/skills/review-plan/reference/report-template.en.md +1 -1
  160. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +1 -2
  161. package/templates/.agents/skills/test/SKILL.en.md +3 -1
  162. package/templates/.agents/skills/test/SKILL.zh-CN.md +1 -1
  163. package/templates/.agents/skills/test-integration/SKILL.en.md +3 -1
  164. package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +3 -1
  165. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +3 -1
  166. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +3 -1
  167. package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +3 -1
  168. package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +3 -1
  169. package/templates/.agents/skills/watch-pr/SKILL.en.md +3 -1
  170. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +3 -1
  171. package/templates/.agents/templates/task.en.md +3 -3
  172. package/templates/.agents/templates/task.zh-CN.md +3 -3
  173. package/templates/.github/workflows/metadata-sync.yml +0 -18
@@ -0,0 +1,258 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { formatTable } from "../../table.js";
4
+ import { resolveTaskRef } from "../resolve-ref.js";
5
+ import { parseLedger, HUMAN_DECISION_STATUSES } from "../ledger.js";
6
+ import { extractSubSection } from "../sections.js";
7
+ const USAGE = `Usage: ai task decisions <N | #N | TASK-id> [selector] [options]
8
+
9
+ Lists the human-decision (HD-) items recorded in a task's review disagreement
10
+ ledger, or prints the full detail block for a single item. Read-only.
11
+
12
+ <ref> Bare numeric / '#N' short id, or a full TASK-YYYYMMDD-HHMMSS id.
13
+ [selector] Ordinal (1-based) or HD id (e.g. 'HD-3') to show one item's detail.
14
+
15
+ Options:
16
+ --all Include already-decided (human-decided) items, not just pending.
17
+ --stage <s> Filter to one stage: analysis | plan | code.
18
+ --format <fmt> Output format: text (default) | markdown.
19
+ -h, --help Show this help.
20
+
21
+ Aliased as 'ai task d'.
22
+ `;
23
+ const STAGES = new Set(['analysis', 'plan', 'code']);
24
+ const FORMATS = new Set(['text', 'markdown']);
25
+ const HD_ID_RE = /^HD-\d+$/;
26
+ function fail(message) {
27
+ process.stderr.write(`ai task decisions: ${message}\n`);
28
+ process.exitCode = 1;
29
+ }
30
+ // Returns null and sets the exit code when an option is malformed.
31
+ function parseArgs(args) {
32
+ const out = { positionals: [], all: false, format: 'text' };
33
+ for (let i = 0; i < args.length; i += 1) {
34
+ const a = args[i];
35
+ if (a === '--all') {
36
+ out.all = true;
37
+ }
38
+ else if (a === '--stage') {
39
+ const v = args[i + 1];
40
+ if (v === undefined) {
41
+ fail('--stage requires a value (analysis|plan|code)');
42
+ return null;
43
+ }
44
+ out.stage = v;
45
+ i += 1;
46
+ }
47
+ else if (a.startsWith('--stage=')) {
48
+ out.stage = a.slice('--stage='.length);
49
+ }
50
+ else if (a === '--format') {
51
+ const v = args[i + 1];
52
+ if (v === undefined) {
53
+ fail('--format requires a value (text|markdown)');
54
+ return null;
55
+ }
56
+ out.format = v;
57
+ i += 1;
58
+ }
59
+ else if (a.startsWith('--format=')) {
60
+ out.format = a.slice('--format='.length);
61
+ }
62
+ else if (a.startsWith('-')) {
63
+ fail(`unknown option '${a}'`);
64
+ return null;
65
+ }
66
+ else {
67
+ out.positionals.push(a);
68
+ }
69
+ }
70
+ return out;
71
+ }
72
+ // Parse `<file>.md#anchor` evidence into its filename, when present.
73
+ function evidenceFile(evidence) {
74
+ const m = /([\w.-]+\.md)#/.exec(evidence);
75
+ return m ? m[1] : null;
76
+ }
77
+ function roundOf(file) {
78
+ const m = /-r(\d+)\.md$/.exec(file);
79
+ return m ? Number.parseInt(m[1], 10) : 1;
80
+ }
81
+ // Locate the `### HD-N` detail block for a row. Prefer the artifact named by the
82
+ // row's evidence anchor; otherwise scan analysis/plan/code artifacts and return
83
+ // the block from the highest-round file that contains it. Returns '' when none
84
+ // is found (caller degrades gracefully — plan B3).
85
+ function findDetailBlock(row, taskDir) {
86
+ const hinted = evidenceFile(row.evidence);
87
+ if (hinted) {
88
+ const p = path.join(taskDir, hinted);
89
+ if (fs.existsSync(p)) {
90
+ const block = extractSubSection(fs.readFileSync(p, 'utf8'), row.id);
91
+ if (block)
92
+ return block;
93
+ }
94
+ }
95
+ let best = '';
96
+ let bestRound = -1;
97
+ let entries;
98
+ try {
99
+ entries = fs.readdirSync(taskDir);
100
+ }
101
+ catch {
102
+ return '';
103
+ }
104
+ for (const file of entries) {
105
+ if (!/^(analysis|plan|code)(-r\d+)?\.md$/.test(file))
106
+ continue;
107
+ const block = extractSubSection(fs.readFileSync(path.join(taskDir, file), 'utf8'), row.id);
108
+ if (block && roundOf(file) > bestRound) {
109
+ best = block;
110
+ bestRound = roundOf(file);
111
+ }
112
+ }
113
+ return best;
114
+ }
115
+ // Pull the `## 人工裁决` record lines that mention this HD id, so a decided item
116
+ // shows the human's recorded ruling alongside its detail block.
117
+ function findDecisionRecord(id, content) {
118
+ const lines = content.split('\n');
119
+ let i = 0;
120
+ while (i < lines.length && !/^##\s+(人工裁决|Human Decisions?)\s*$/.test(lines[i]))
121
+ i += 1;
122
+ if (i >= lines.length)
123
+ return [];
124
+ const idRe = new RegExp(`(^|[^\\w-])${id}(?![\\w-])`);
125
+ const out = [];
126
+ for (let j = i + 1; j < lines.length; j += 1) {
127
+ if (/^##\s/.test(lines[j]))
128
+ break;
129
+ if (lines[j].trim().startsWith('-') && idRe.test(lines[j]))
130
+ out.push(lines[j]);
131
+ }
132
+ return out;
133
+ }
134
+ function titleOf(row, taskDir) {
135
+ const block = findDetailBlock(row, taskDir);
136
+ if (block) {
137
+ return block
138
+ .split('\n')[0]
139
+ .replace(/^###\s+/, '')
140
+ .replace(/\s*\[needs-human-decision\]\s*$/, '')
141
+ .trim();
142
+ }
143
+ return row.evidence || '(无详情)';
144
+ }
145
+ function renderList(rows, format, taskDir) {
146
+ if (rows.length === 0) {
147
+ process.stdout.write('无待裁决项。\n');
148
+ return;
149
+ }
150
+ const headers = ['#', 'ID', 'STAGE', 'SEVERITY', 'STATUS', 'EVIDENCE', 'TITLE'];
151
+ const data = rows.map((r, i) => [
152
+ String(i + 1),
153
+ r.id,
154
+ r.stage,
155
+ r.severity,
156
+ r.status,
157
+ r.evidence,
158
+ titleOf(r, taskDir)
159
+ ]);
160
+ if (format === 'markdown') {
161
+ const sep = headers.map(() => '---');
162
+ const md = [
163
+ `| ${headers.join(' | ')} |`,
164
+ `| ${sep.join(' | ')} |`,
165
+ ...data.map((row) => `| ${row.join(' | ')} |`)
166
+ ];
167
+ process.stdout.write(`${md.join('\n')}\n`);
168
+ return;
169
+ }
170
+ process.stdout.write(`${formatTable(headers, data).join('\n')}\n`);
171
+ }
172
+ function renderDetail(rows, selector, format, taskDir, content) {
173
+ let row;
174
+ if (/^\d+$/.test(selector)) {
175
+ const idx = Number.parseInt(selector, 10) - 1;
176
+ if (idx < 0 || idx >= rows.length) {
177
+ fail(`ordinal '${selector}' out of range (1..${rows.length})`);
178
+ return;
179
+ }
180
+ row = rows[idx];
181
+ }
182
+ else {
183
+ const want = selector.toUpperCase();
184
+ const matches = rows.filter((r) => r.id.toUpperCase() === want);
185
+ if (matches.length === 0) {
186
+ fail(`no decision item matches '${selector}'`);
187
+ return;
188
+ }
189
+ if (matches.length > 1) {
190
+ fail(`duplicate id '${selector}' in ledger; select by ordinal instead`);
191
+ return;
192
+ }
193
+ row = matches[0];
194
+ }
195
+ const r = row;
196
+ const block = findDetailBlock(r, taskDir);
197
+ const lines = [];
198
+ if (format === 'markdown') {
199
+ lines.push(`**${r.id}** (${r.stage}/${r.severity}) · status=\`${r.status}\` · evidence: \`${r.evidence}\``, '');
200
+ }
201
+ else {
202
+ lines.push(`${r.id} (${r.stage}/${r.severity}) status=${r.status}`, `evidence: ${r.evidence}`, '');
203
+ }
204
+ if (block) {
205
+ lines.push(block);
206
+ }
207
+ else {
208
+ lines.push(`(详情块未找到:未在任务产物中定位到 \`### ${r.id}\` 锚点,可能为历史产物或尚未写入;evidence 指向 ${r.evidence})`);
209
+ }
210
+ if (r.status === 'human-decided') {
211
+ const record = findDecisionRecord(r.id, content);
212
+ if (record.length) {
213
+ lines.push('', '人工裁定:', ...record);
214
+ }
215
+ }
216
+ process.stdout.write(`${lines.join('\n')}\n`);
217
+ }
218
+ function decisions(args = []) {
219
+ if (args[0] === '--help' || args[0] === '-h') {
220
+ process.stdout.write(USAGE);
221
+ return;
222
+ }
223
+ const parsed = parseArgs(args);
224
+ if (!parsed)
225
+ return;
226
+ if (parsed.positionals.length === 0) {
227
+ process.stdout.write(USAGE);
228
+ process.exitCode = 1;
229
+ return;
230
+ }
231
+ if (parsed.stage !== undefined && !STAGES.has(parsed.stage)) {
232
+ fail(`invalid --stage '${parsed.stage}' (expected analysis|plan|code)`);
233
+ return;
234
+ }
235
+ if (!FORMATS.has(parsed.format)) {
236
+ fail(`invalid --format '${parsed.format}' (expected text|markdown)`);
237
+ return;
238
+ }
239
+ const resolved = resolveTaskRef(parsed.positionals[0]);
240
+ if (!resolved.ok) {
241
+ fail(resolved.message);
242
+ return;
243
+ }
244
+ const content = fs.readFileSync(resolved.taskMdPath, 'utf8');
245
+ let rows = parseLedger(content).filter((r) => HD_ID_RE.test(r.id));
246
+ rows = rows.filter((r) => parsed.all ? HUMAN_DECISION_STATUSES.has(r.status) : r.status === 'needs-human-decision');
247
+ if (parsed.stage !== undefined)
248
+ rows = rows.filter((r) => r.stage === parsed.stage);
249
+ const selector = parsed.positionals[1];
250
+ if (selector === undefined) {
251
+ renderList(rows, parsed.format, resolved.taskDir);
252
+ }
253
+ else {
254
+ renderDetail(rows, selector, parsed.format, resolved.taskDir, content);
255
+ }
256
+ }
257
+ export { decisions };
258
+ //# sourceMappingURL=decisions.js.map
@@ -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
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import { formatTable } from "../../table.js";
3
3
  import { resolveTaskRef } from "../resolve-ref.js";
4
+ import { parseLedger, HUMAN_DECISION_STATUSES } from "../ledger.js";
4
5
  const USAGE = `Usage: ai task log <N | #N | TASK-id>
5
6
 
6
7
  Renders a task's activity log as a per-step status table. A step's start and
@@ -9,6 +10,10 @@ completion time (or '(in progress)' while still running).
9
10
  <ref> Bare numeric / '#N' short id, or a full TASK-YYYYMMDD-HHMMSS id.
10
11
 
11
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).
12
17
  `;
13
18
  const TABLE_HEADERS = ['#', 'STEP', 'AGENT', 'STARTED', 'DONE', 'NOTE'];
14
19
  // The activity-log H2 heading is language-dependent (zh template / en template).
@@ -23,6 +28,20 @@ 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 ENV_BLOCKED_RE = /\(\+\s*(\d+)\s+env-blocked\)/i;
37
+ // Same match plus any leading whitespace, so folding the count into the verdict
38
+ // text drops the redundant `(+ n env-blocked)` fragment without leaving a gap.
39
+ const ENV_BLOCKED_STRIP_RE = /\s*\(\+\s*\d+\s+env-blocked\)/i;
40
+ const REVIEW_STAGE_PREFIXES = [
41
+ { prefix: 'Review Analysis', stage: 'analysis' },
42
+ { prefix: 'Review Plan', stage: 'plan' },
43
+ { prefix: 'Review Code', stage: 'code' }
44
+ ];
26
45
  function parseActivityLog(content) {
27
46
  const lines = content.split('\n');
28
47
  let i = 0;
@@ -82,6 +101,42 @@ function pairEntries(entries) {
82
101
  }
83
102
  return rows;
84
103
  }
104
+ function countHumanDecisionsByStage(rows) {
105
+ const counts = new Map();
106
+ for (const row of rows) {
107
+ if (!isReviewStage(row.stage) || !HUMAN_DECISION_STATUSES.has(row.status))
108
+ continue;
109
+ counts.set(row.stage, (counts.get(row.stage) ?? 0) + 1);
110
+ }
111
+ return counts;
112
+ }
113
+ function isReviewStage(stage) {
114
+ return stage === 'analysis' || stage === 'plan' || stage === 'code';
115
+ }
116
+ function reviewStageForStep(step) {
117
+ return REVIEW_STAGE_PREFIXES.find(({ prefix }) => step.startsWith(prefix))?.stage;
118
+ }
119
+ function humanValidationCount(note) {
120
+ const match = ENV_BLOCKED_RE.exec(note);
121
+ return match ? Number(match[1]) : 0;
122
+ }
123
+ // A step is human-executed when its agent token is not a known AI token. Take
124
+ // the first whitespace-delimited token and drop any trailing parenthetical
125
+ // annotation (e.g. `张三 (executed on host)` -> `张三`) before the lookup.
126
+ function isHumanAgent(agent) {
127
+ const token = agent.trim().split(/\s+/)[0]?.replace(/\(.*$/, '') ?? '';
128
+ return token !== '' && !KNOWN_AI_AGENTS.has(token);
129
+ }
130
+ // Fold the two human counts into a review row's verdict NOTE: comma-joined, right
131
+ // after the blockers/major/minor list and before the ` → artifact` link, mirroring
132
+ // the review count line. The raw `(+ n env-blocked)` fragment is dropped so the
133
+ // env-blocked number is not shown twice (it becomes the manual-verify count).
134
+ function foldHumanCounts(note, decisions, envBlocked) {
135
+ const base = note.replace(ENV_BLOCKED_STRIP_RE, '');
136
+ const group = `Manual-verify: ${envBlocked}, Human-decision: ${decisions}`;
137
+ const arrow = base.indexOf(' → ');
138
+ return arrow === -1 ? `${base}, ${group}` : `${base.slice(0, arrow)}, ${group}${base.slice(arrow)}`;
139
+ }
85
140
  function log(args = []) {
86
141
  if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
87
142
  process.stdout.write(USAGE);
@@ -108,18 +163,21 @@ function log(args = []) {
108
163
  return;
109
164
  }
110
165
  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
- ]);
166
+ const humanDecisionCounts = countHumanDecisionsByStage(parseLedger(content));
167
+ const rows = steps.map((s, idx) => {
168
+ const stage = reviewStageForStep(s.step);
169
+ const note = stage
170
+ ? foldHumanCounts(s.note, humanDecisionCounts.get(stage) ?? 0, humanValidationCount(s.note))
171
+ : s.note;
172
+ const human = isHumanAgent(s.agent);
173
+ const agent = human ? 'human' : s.agent;
174
+ const started = s.started || (human ? '-' : '');
175
+ return [String(idx + 1), s.step, agent, started, s.done || (s.started ? '(in progress)' : ''), note];
176
+ });
119
177
  for (const line of formatTable(TABLE_HEADERS, rows, { zebra: Boolean(process.stdout.isTTY) })) {
120
178
  process.stdout.write(`${line}\n`);
121
179
  }
122
180
  process.stdout.write(`Total: ${steps.length} steps\n`);
123
181
  }
124
- export { log, parseActivityLog, pairEntries };
182
+ export { log, parseActivityLog, pairEntries, isHumanAgent };
125
183
  //# sourceMappingURL=log.js.map
@@ -2,8 +2,10 @@ const USAGE = `Usage: ai task <command> [options]
2
2
 
3
3
  Commands:
4
4
  cat <ref> <artifact | N> Print a task artifact (by name or number)
5
+ decisions, d <ref> [selector] List human-decision (HD-) items, or show one's detail
5
6
  files <ref> List artifacts in a task dir (numbered)
6
7
  grep <pattern> [ref] [artifact | N] Literal search across task artifacts (omit ref to scan all)
8
+ issue-body <ref> [--template <path>] Print a deterministic Issue body from task.md (描述 + 需求, or an Issue Form)
7
9
  log <ref> Render a task's activity log as a timeline
8
10
  ls [--all | --blocked | --completed] List tasks (default: active)
9
11
  show <N | #N | TASK-id> Print a task.md
@@ -12,9 +14,13 @@ Commands:
12
14
  Examples:
13
15
  ai task cat 11 analysis
14
16
  ai task cat 11 3
17
+ ai task decisions 11
18
+ ai task d 11 HD-3 --format markdown
15
19
  ai task files 11
16
20
  ai task grep resolveArtifact
17
21
  ai task grep resolveArtifact 11
22
+ ai task issue-body 11
23
+ ai task issue-body 11 --template .github/ISSUE_TEMPLATE/05_other.yml
18
24
  ai task log 11
19
25
  ai task ls
20
26
  ai task show 11
@@ -39,6 +45,12 @@ export async function runTask(args) {
39
45
  cat(rest);
40
46
  break;
41
47
  }
48
+ case 'decisions':
49
+ case 'd': {
50
+ const { decisions } = await import("./commands/decisions.js");
51
+ decisions(rest);
52
+ break;
53
+ }
42
54
  case 'files': {
43
55
  const { files } = await import("./commands/files.js");
44
56
  files(rest);
@@ -49,6 +61,11 @@ export async function runTask(args) {
49
61
  grep(rest);
50
62
  break;
51
63
  }
64
+ case 'issue-body': {
65
+ const { issueBody } = await import("./commands/issue-body.js");
66
+ issueBody(rest);
67
+ break;
68
+ }
52
69
  case 'log': {
53
70
  const { log } = await import("./commands/log.js");
54
71
  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,66 @@
1
+ // Shared parser for the task.md `## 审查分歧账本` (Review Disagreement Ledger).
2
+ // Single source of truth for ledger row parsing across `ai task` commands
3
+ // (log, decisions) — see the plan's D1 constraint "no third parser". The gate
4
+ // parser in `.agents/scripts/validate-artifact.js` is a separate concern and
5
+ // must be kept semantically in sync by hand (noted in review-handshake.md).
6
+ // The ledger H2 heading is language-dependent (zh template / en template).
7
+ const LEDGER_HEADING_RE = /^##\s+(审查分歧账本|Review Disagreement Ledger)\s*$/;
8
+ const NEXT_H2_RE = /^##\s/;
9
+ // Terminal statuses the completion gates treat as resolved.
10
+ const LEDGER_TERMINAL = new Set(['confirmed', 'closed', 'human-decided']);
11
+ // Statuses that represent an executor-raised human-decision row (pending or done).
12
+ const HUMAN_DECISION_STATUSES = new Set(['needs-human-decision', 'human-decided']);
13
+ // Parse all rows of the disagreement ledger table. Skips the heading, the
14
+ // header row (`| id | ... |`) and the `|---|` separator; ignores non-`|` lines.
15
+ // Rows with fewer than 6 columns are skipped (mirrors the JS gate parser).
16
+ function parseLedger(content) {
17
+ const lines = content.split('\n');
18
+ let i = 0;
19
+ while (i < lines.length && !LEDGER_HEADING_RE.test(lines[i]))
20
+ i += 1;
21
+ if (i >= lines.length)
22
+ return [];
23
+ const rows = [];
24
+ for (let j = i + 1; j < lines.length; j += 1) {
25
+ if (NEXT_H2_RE.test(lines[j]))
26
+ break;
27
+ const line = lines[j].trim();
28
+ if (!line.startsWith('|'))
29
+ continue;
30
+ const cells = line
31
+ .split('|')
32
+ .slice(1, -1)
33
+ .map((cell) => cell.trim());
34
+ if (cells.length < 6)
35
+ continue;
36
+ if (cells[0] === 'id' || /^-+$/.test(cells[0] ?? ''))
37
+ continue;
38
+ rows.push({
39
+ id: cells[0],
40
+ stage: cells[1],
41
+ round: cells[2],
42
+ severity: cells[3],
43
+ status: cells[4],
44
+ evidence: cells[5]
45
+ });
46
+ }
47
+ return rows;
48
+ }
49
+ // Allocate the next globally-unique human-decision id. Scans every `HD-<n>`
50
+ // already present in the ledger (across all stages) and returns `HD-{max+1}`,
51
+ // or `HD-1` when none exist. Global monotonic allocation prevents id collisions
52
+ // across the analysis / plan / code stages (plan PL-2).
53
+ function nextHdId(rows) {
54
+ let max = 0;
55
+ for (const row of rows) {
56
+ const m = /^HD-(\d+)$/.exec(row.id);
57
+ if (!m)
58
+ continue;
59
+ const n = Number.parseInt(m[1], 10);
60
+ if (n > max)
61
+ max = n;
62
+ }
63
+ return `HD-${max + 1}`;
64
+ }
65
+ export { parseLedger, nextHdId, LEDGER_TERMINAL, HUMAN_DECISION_STATUSES };
66
+ //# sourceMappingURL=ledger.js.map