@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.
- package/README.md +2 -0
- package/README.zh-CN.md +2 -0
- package/bin/cli.ts +53 -18
- package/dist/bin/cli.js +56 -19
- package/dist/lib/decide.js +110 -0
- package/dist/lib/run/host.js +39 -0
- package/dist/lib/run/index.js +102 -0
- package/dist/lib/run/prompt.js +2 -0
- package/dist/lib/run/skills.js +29 -0
- package/dist/lib/run/tui.js +35 -0
- package/dist/lib/sandbox/capture.js +49 -0
- package/dist/lib/sandbox/commands/create.js +55 -6
- package/dist/lib/sandbox/commands/ls.js +4 -33
- package/dist/lib/sandbox/commands/show.js +67 -0
- package/dist/lib/sandbox/index.js +7 -0
- package/dist/lib/server/adapters/_contract.js +9 -0
- package/dist/lib/server/adapters/feishu/index.js +40 -0
- package/dist/lib/server/adapters/feishu/transport.js +95 -0
- package/dist/lib/server/auth.js +19 -0
- package/dist/lib/server/config.js +183 -0
- package/dist/lib/server/daemon.js +96 -0
- package/dist/lib/server/index.js +47 -0
- package/dist/lib/server/logger.js +34 -0
- package/dist/lib/server/plugin-loader.js +73 -0
- package/dist/lib/server/process-control.js +200 -0
- package/dist/lib/server/protocol.js +69 -0
- package/dist/lib/server/redact.js +12 -0
- package/dist/lib/server/runner.js +75 -0
- package/dist/lib/server/streamer.js +44 -0
- package/dist/lib/task/commands/decisions.js +258 -0
- package/dist/lib/task/commands/issue-body.js +94 -0
- package/dist/lib/task/commands/log.js +67 -9
- package/dist/lib/task/index.js +17 -0
- package/dist/lib/task/issue-form.js +66 -0
- package/dist/lib/task/ledger.js +66 -0
- package/dist/lib/task/sections.js +73 -0
- package/lib/decide.ts +124 -0
- package/lib/run/host.ts +47 -0
- package/lib/run/index.ts +124 -0
- package/lib/run/prompt.ts +1 -0
- package/lib/run/skills.ts +34 -0
- package/lib/run/tui.ts +40 -0
- package/lib/sandbox/capture.ts +84 -0
- package/lib/sandbox/commands/create.ts +61 -9
- package/lib/sandbox/commands/ls.ts +4 -36
- package/lib/sandbox/commands/show.ts +80 -0
- package/lib/sandbox/index.ts +7 -0
- package/lib/server/adapters/_contract.ts +42 -0
- package/lib/server/adapters/feishu/index.ts +47 -0
- package/lib/server/adapters/feishu/transport.ts +128 -0
- package/lib/server/auth.ts +28 -0
- package/lib/server/config.ts +228 -0
- package/lib/server/daemon.ts +110 -0
- package/lib/server/index.ts +50 -0
- package/lib/server/logger.ts +48 -0
- package/lib/server/plugin-loader.ts +84 -0
- package/lib/server/process-control.ts +225 -0
- package/lib/server/protocol.ts +79 -0
- package/lib/server/redact.ts +12 -0
- package/lib/server/runner.ts +95 -0
- package/lib/server/server.schema.json +127 -0
- package/lib/server/streamer.ts +57 -0
- package/lib/task/commands/decisions.ts +272 -0
- package/lib/task/commands/issue-body.ts +102 -0
- package/lib/task/commands/log.ts +73 -9
- package/lib/task/index.ts +17 -0
- package/lib/task/issue-form.ts +77 -0
- package/lib/task/ledger.ts +73 -0
- package/lib/task/sections.ts +73 -0
- package/package.json +9 -4
- package/templates/.agents/QUICKSTART.zh-CN.md +2 -2
- package/templates/.agents/README.en.md +3 -1
- package/templates/.agents/README.zh-CN.md +3 -1
- package/templates/.agents/rules/create-issue.github.en.md +20 -29
- package/templates/.agents/rules/create-issue.github.zh-CN.md +20 -29
- package/templates/.agents/rules/issue-fields.github.en.md +1 -1
- package/templates/.agents/rules/issue-fields.github.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-pr-commands.github.en.md +6 -0
- package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +6 -0
- package/templates/.agents/rules/next-step-output.en.md +30 -1
- package/templates/.agents/rules/next-step-output.zh-CN.md +30 -1
- package/templates/.agents/rules/no-mid-flow-questions.en.md +7 -2
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +7 -2
- package/templates/.agents/rules/review-handshake.en.md +5 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +5 -1
- package/templates/.agents/rules/task-management.en.md +3 -1
- package/templates/.agents/rules/task-management.zh-CN.md +3 -1
- package/templates/.agents/scripts/lib/post-review-commit.js +15 -18
- package/templates/.agents/scripts/validate-artifact.js +8 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +18 -4
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +18 -4
- package/templates/.agents/skills/analyze-task/config/verify.en.json +2 -1
- package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +2 -1
- package/templates/.agents/skills/archive-tasks/SKILL.en.md +3 -1
- package/templates/.agents/skills/archive-tasks/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/block-task/SKILL.en.md +3 -1
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -1
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/check-task/SKILL.en.md +30 -81
- package/templates/.agents/skills/check-task/SKILL.zh-CN.md +30 -80
- package/templates/.agents/skills/close-codescan/SKILL.en.md +3 -1
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +3 -1
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +4 -2
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +4 -2
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +7 -2
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +7 -2
- package/templates/.agents/skills/code-task/scripts/detect-mode.js +6 -14
- package/templates/.agents/skills/commit/SKILL.en.md +27 -5
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +27 -5
- package/templates/.agents/skills/complete-task/SKILL.en.md +4 -1
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -1
- package/templates/.agents/skills/complete-task/config/verify.en.json +2 -1
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +3 -1
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/create-release-note/SKILL.en.md +3 -1
- package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/create-task/SKILL.en.md +4 -4
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/import-codescan/SKILL.en.md +3 -4
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +3 -4
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +3 -2
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +3 -2
- package/templates/.agents/skills/import-issue/SKILL.en.md +20 -4
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +20 -4
- package/templates/.agents/skills/init-labels/SKILL.en.md +3 -1
- package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/init-milestones/SKILL.en.md +3 -1
- package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +4 -3
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -3
- package/templates/.agents/skills/post-release/SKILL.en.md +3 -1
- package/templates/.agents/skills/post-release/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/refine-title/SKILL.en.md +3 -1
- package/templates/.agents/skills/refine-title/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/release/SKILL.en.md +3 -1
- package/templates/.agents/skills/release/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/restore-task/SKILL.en.md +3 -1
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +6 -2
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +1 -1
- package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +1 -1
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +1 -1
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +1 -2
- package/templates/.agents/skills/review-code/SKILL.en.md +5 -2
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -2
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +1 -1
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +1 -1
- package/templates/.agents/skills/review-code/reference/report-template.en.md +1 -1
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +1 -2
- package/templates/.agents/skills/review-plan/SKILL.en.md +6 -2
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/review-plan/reference/output-templates.en.md +1 -1
- package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +1 -1
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +1 -1
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +1 -2
- package/templates/.agents/skills/test/SKILL.en.md +3 -1
- package/templates/.agents/skills/test/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/test-integration/SKILL.en.md +3 -1
- package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/update-agent-infra/SKILL.en.md +3 -1
- package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +3 -1
- package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/watch-pr/SKILL.en.md +3 -1
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +3 -1
- package/templates/.agents/templates/task.en.md +3 -3
- package/templates/.agents/templates/task.zh-CN.md +3 -3
- 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
|
|
112
|
-
|
|
113
|
-
s.step
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
s.
|
|
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
|
package/dist/lib/task/index.js
CHANGED
|
@@ -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
|