@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
package/lib/task/commands/log.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { formatTable } from '../../table.ts';
|
|
3
3
|
import { resolveTaskRef } from '../resolve-ref.ts';
|
|
4
|
+
import { parseLedger, HUMAN_DECISION_STATUSES, type LedgerRow } from '../ledger.ts';
|
|
4
5
|
|
|
5
6
|
const USAGE = `Usage: ai task log <N | #N | TASK-id>
|
|
6
7
|
|
|
@@ -10,6 +11,10 @@ completion time (or '(in progress)' while still running).
|
|
|
10
11
|
<ref> Bare numeric / '#N' short id, or a full TASK-YYYYMMDD-HHMMSS id.
|
|
11
12
|
|
|
12
13
|
Columns: # (row) / STEP / AGENT / STARTED / DONE / NOTE
|
|
14
|
+
A human-executed step shows AGENT as 'human' and, when it has no start marker,
|
|
15
|
+
a '-' STARTED placeholder. Review-step NOTE also carries two human counts in
|
|
16
|
+
the verdict list, right after blockers/major/minor: manual-verify (env-blocked)
|
|
17
|
+
and human-decision (current ledger stage total).
|
|
13
18
|
`;
|
|
14
19
|
|
|
15
20
|
const TABLE_HEADERS = ['#', 'STEP', 'AGENT', 'STARTED', 'DONE', 'NOTE'] as const;
|
|
@@ -24,6 +29,7 @@ const ENTRY_RE =
|
|
|
24
29
|
/^- (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}) — \*\*(.+?)\*\* by (.+?) — (.*)$/;
|
|
25
30
|
|
|
26
31
|
type LogEntry = { time: string; step: string; agent: string; note: string };
|
|
32
|
+
type ReviewStage = 'analysis' | 'plan' | 'code';
|
|
27
33
|
|
|
28
34
|
// One rendered row = one step instance. `started`/`done` are timestamps; an empty
|
|
29
35
|
// `done` with a non-empty `started` means the step is still in flight, while an
|
|
@@ -35,6 +41,20 @@ type StepRow = { step: string; agent: string; started: string; done: string; not
|
|
|
35
41
|
// without the suffix. Pairing therefore keys on the base action (including any
|
|
36
42
|
// `(Round N)`), so every round and every repeated execution pairs on its own.
|
|
37
43
|
const STARTED_SUFFIX_RE = /\s*\[started\]\s*$/;
|
|
44
|
+
// Short agent tokens that actually appear in activity logs for AI executors
|
|
45
|
+
// (workflow recommended_agents claude/codex/gemini/cursor + enabled TUI opencode;
|
|
46
|
+
// note these differ from the `.airc.json` long names claude-code/gemini-cli).
|
|
47
|
+
// Any other executor token (a human name, possibly CJK) is treated as human.
|
|
48
|
+
const KNOWN_AI_AGENTS = new Set(['claude', 'codex', 'gemini', 'opencode', 'cursor']);
|
|
49
|
+
const ENV_BLOCKED_RE = /\(\+\s*(\d+)\s+env-blocked\)/i;
|
|
50
|
+
// Same match plus any leading whitespace, so folding the count into the verdict
|
|
51
|
+
// text drops the redundant `(+ n env-blocked)` fragment without leaving a gap.
|
|
52
|
+
const ENV_BLOCKED_STRIP_RE = /\s*\(\+\s*\d+\s+env-blocked\)/i;
|
|
53
|
+
const REVIEW_STAGE_PREFIXES: { prefix: string; stage: ReviewStage }[] = [
|
|
54
|
+
{ prefix: 'Review Analysis', stage: 'analysis' },
|
|
55
|
+
{ prefix: 'Review Plan', stage: 'plan' },
|
|
56
|
+
{ prefix: 'Review Code', stage: 'code' }
|
|
57
|
+
];
|
|
38
58
|
|
|
39
59
|
function parseActivityLog(content: string): { sectionFound: boolean; entries: LogEntry[] } {
|
|
40
60
|
const lines = content.split('\n');
|
|
@@ -89,6 +109,47 @@ function pairEntries(entries: LogEntry[]): StepRow[] {
|
|
|
89
109
|
return rows;
|
|
90
110
|
}
|
|
91
111
|
|
|
112
|
+
function countHumanDecisionsByStage(rows: LedgerRow[]): Map<ReviewStage, number> {
|
|
113
|
+
const counts = new Map<ReviewStage, number>();
|
|
114
|
+
for (const row of rows) {
|
|
115
|
+
if (!isReviewStage(row.stage) || !HUMAN_DECISION_STATUSES.has(row.status)) continue;
|
|
116
|
+
counts.set(row.stage, (counts.get(row.stage) ?? 0) + 1);
|
|
117
|
+
}
|
|
118
|
+
return counts;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isReviewStage(stage: string): stage is ReviewStage {
|
|
122
|
+
return stage === 'analysis' || stage === 'plan' || stage === 'code';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function reviewStageForStep(step: string): ReviewStage | undefined {
|
|
126
|
+
return REVIEW_STAGE_PREFIXES.find(({ prefix }) => step.startsWith(prefix))?.stage;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function humanValidationCount(note: string): number {
|
|
130
|
+
const match = ENV_BLOCKED_RE.exec(note);
|
|
131
|
+
return match ? Number(match[1]) : 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// A step is human-executed when its agent token is not a known AI token. Take
|
|
135
|
+
// the first whitespace-delimited token and drop any trailing parenthetical
|
|
136
|
+
// annotation (e.g. `张三 (executed on host)` -> `张三`) before the lookup.
|
|
137
|
+
function isHumanAgent(agent: string): boolean {
|
|
138
|
+
const token = agent.trim().split(/\s+/)[0]?.replace(/\(.*$/, '') ?? '';
|
|
139
|
+
return token !== '' && !KNOWN_AI_AGENTS.has(token);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Fold the two human counts into a review row's verdict NOTE: comma-joined, right
|
|
143
|
+
// after the blockers/major/minor list and before the ` → artifact` link, mirroring
|
|
144
|
+
// the review count line. The raw `(+ n env-blocked)` fragment is dropped so the
|
|
145
|
+
// env-blocked number is not shown twice (it becomes the manual-verify count).
|
|
146
|
+
function foldHumanCounts(note: string, decisions: number, envBlocked: number): string {
|
|
147
|
+
const base = note.replace(ENV_BLOCKED_STRIP_RE, '');
|
|
148
|
+
const group = `Manual-verify: ${envBlocked}, Human-decision: ${decisions}`;
|
|
149
|
+
const arrow = base.indexOf(' → ');
|
|
150
|
+
return arrow === -1 ? `${base}, ${group}` : `${base.slice(0, arrow)}, ${group}${base.slice(arrow)}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
92
153
|
function log(args: string[] = []): void {
|
|
93
154
|
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
94
155
|
process.stdout.write(USAGE);
|
|
@@ -116,18 +177,21 @@ function log(args: string[] = []): void {
|
|
|
116
177
|
return;
|
|
117
178
|
}
|
|
118
179
|
const steps = pairEntries(entries);
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
s.step
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
s.
|
|
126
|
-
|
|
180
|
+
const humanDecisionCounts = countHumanDecisionsByStage(parseLedger(content));
|
|
181
|
+
const rows = steps.map((s, idx) => {
|
|
182
|
+
const stage = reviewStageForStep(s.step);
|
|
183
|
+
const note = stage
|
|
184
|
+
? foldHumanCounts(s.note, humanDecisionCounts.get(stage) ?? 0, humanValidationCount(s.note))
|
|
185
|
+
: s.note;
|
|
186
|
+
const human = isHumanAgent(s.agent);
|
|
187
|
+
const agent = human ? 'human' : s.agent;
|
|
188
|
+
const started = s.started || (human ? '-' : '');
|
|
189
|
+
return [String(idx + 1), s.step, agent, started, s.done || (s.started ? '(in progress)' : ''), note];
|
|
190
|
+
});
|
|
127
191
|
for (const line of formatTable(TABLE_HEADERS, rows, { zebra: Boolean(process.stdout.isTTY) })) {
|
|
128
192
|
process.stdout.write(`${line}\n`);
|
|
129
193
|
}
|
|
130
194
|
process.stdout.write(`Total: ${steps.length} steps\n`);
|
|
131
195
|
}
|
|
132
196
|
|
|
133
|
-
export { log, parseActivityLog, pairEntries };
|
|
197
|
+
export { log, parseActivityLog, pairEntries, isHumanAgent };
|
package/lib/task/index.ts
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
|
|
@@ -43,6 +49,12 @@ export async function runTask(args: string[]): Promise<void> {
|
|
|
43
49
|
cat(rest);
|
|
44
50
|
break;
|
|
45
51
|
}
|
|
52
|
+
case 'decisions':
|
|
53
|
+
case 'd': {
|
|
54
|
+
const { decisions } = await import('./commands/decisions.ts');
|
|
55
|
+
decisions(rest);
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
46
58
|
case 'files': {
|
|
47
59
|
const { files } = await import('./commands/files.ts');
|
|
48
60
|
files(rest);
|
|
@@ -53,6 +65,11 @@ export async function runTask(args: string[]): Promise<void> {
|
|
|
53
65
|
grep(rest);
|
|
54
66
|
break;
|
|
55
67
|
}
|
|
68
|
+
case 'issue-body': {
|
|
69
|
+
const { issueBody } = await import('./commands/issue-body.ts');
|
|
70
|
+
issueBody(rest);
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
56
73
|
case 'log': {
|
|
57
74
|
const { log } = await import('./commands/log.ts');
|
|
58
75
|
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,73 @@
|
|
|
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
|
+
|
|
7
|
+
// The ledger H2 heading is language-dependent (zh template / en template).
|
|
8
|
+
const LEDGER_HEADING_RE = /^##\s+(审查分歧账本|Review Disagreement Ledger)\s*$/;
|
|
9
|
+
const NEXT_H2_RE = /^##\s/;
|
|
10
|
+
|
|
11
|
+
type LedgerRow = {
|
|
12
|
+
id: string;
|
|
13
|
+
stage: string;
|
|
14
|
+
round: string;
|
|
15
|
+
severity: string;
|
|
16
|
+
status: string;
|
|
17
|
+
evidence: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Terminal statuses the completion gates treat as resolved.
|
|
21
|
+
const LEDGER_TERMINAL = new Set(['confirmed', 'closed', 'human-decided']);
|
|
22
|
+
// Statuses that represent an executor-raised human-decision row (pending or done).
|
|
23
|
+
const HUMAN_DECISION_STATUSES = new Set(['needs-human-decision', 'human-decided']);
|
|
24
|
+
|
|
25
|
+
// Parse all rows of the disagreement ledger table. Skips the heading, the
|
|
26
|
+
// header row (`| id | ... |`) and the `|---|` separator; ignores non-`|` lines.
|
|
27
|
+
// Rows with fewer than 6 columns are skipped (mirrors the JS gate parser).
|
|
28
|
+
function parseLedger(content: string): LedgerRow[] {
|
|
29
|
+
const lines = content.split('\n');
|
|
30
|
+
let i = 0;
|
|
31
|
+
while (i < lines.length && !LEDGER_HEADING_RE.test(lines[i]!)) i += 1;
|
|
32
|
+
if (i >= lines.length) return [];
|
|
33
|
+
|
|
34
|
+
const rows: LedgerRow[] = [];
|
|
35
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
36
|
+
if (NEXT_H2_RE.test(lines[j]!)) break;
|
|
37
|
+
const line = lines[j]!.trim();
|
|
38
|
+
if (!line.startsWith('|')) continue;
|
|
39
|
+
const cells = line
|
|
40
|
+
.split('|')
|
|
41
|
+
.slice(1, -1)
|
|
42
|
+
.map((cell) => cell.trim());
|
|
43
|
+
if (cells.length < 6) continue;
|
|
44
|
+
if (cells[0] === 'id' || /^-+$/.test(cells[0] ?? '')) continue;
|
|
45
|
+
rows.push({
|
|
46
|
+
id: cells[0]!,
|
|
47
|
+
stage: cells[1]!,
|
|
48
|
+
round: cells[2]!,
|
|
49
|
+
severity: cells[3]!,
|
|
50
|
+
status: cells[4]!,
|
|
51
|
+
evidence: cells[5]!
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return rows;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Allocate the next globally-unique human-decision id. Scans every `HD-<n>`
|
|
58
|
+
// already present in the ledger (across all stages) and returns `HD-{max+1}`,
|
|
59
|
+
// or `HD-1` when none exist. Global monotonic allocation prevents id collisions
|
|
60
|
+
// across the analysis / plan / code stages (plan PL-2).
|
|
61
|
+
function nextHdId(rows: readonly LedgerRow[]): string {
|
|
62
|
+
let max = 0;
|
|
63
|
+
for (const row of rows) {
|
|
64
|
+
const m = /^HD-(\d+)$/.exec(row.id);
|
|
65
|
+
if (!m) continue;
|
|
66
|
+
const n = Number.parseInt(m[1]!, 10);
|
|
67
|
+
if (n > max) max = n;
|
|
68
|
+
}
|
|
69
|
+
return `HD-${max + 1}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export { parseLedger, nextHdId, LEDGER_TERMINAL, HUMAN_DECISION_STATUSES };
|
|
73
|
+
export type { LedgerRow };
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
/**
|
|
45
|
+
* Return the body of the first `### {headingPrefix}` sub-section, from the
|
|
46
|
+
* heading line (inclusive) to the next `### ` / `## ` heading or EOF. Used to
|
|
47
|
+
* pull a single `### HD-N` human-decision detail block out of an artifact. The
|
|
48
|
+
* prefix must be followed by a word boundary so `HD-1` does not match `HD-10`
|
|
49
|
+
* (e.g. `### HD-1`, `### HD-1:标题`, `### HD-1 [needs-human-decision]`). Leading
|
|
50
|
+
* and trailing blank lines are trimmed. Returns '' when no match is present.
|
|
51
|
+
*/
|
|
52
|
+
function extractSubSection(content: string, headingPrefix: string): string {
|
|
53
|
+
const lines = content.split('\n');
|
|
54
|
+
const headRe = new RegExp(`^###\\s+${escapeRegExp(headingPrefix)}(?![\\w-])`);
|
|
55
|
+
let start = -1;
|
|
56
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
57
|
+
if (headRe.test(lines[i]!.trim())) {
|
|
58
|
+
start = i;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (start === -1) return '';
|
|
63
|
+
let end = lines.length;
|
|
64
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
65
|
+
if (/^###?\s+/.test(lines[i]!)) {
|
|
66
|
+
end = i;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return lines.slice(start, end).join('\n').replace(/^\n+/, '').replace(/\n+$/, '');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export { extractSection, findSectionHeading, extractSubSection };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fitlab-ai/agent-infra",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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",
|
|
@@ -43,11 +43,13 @@
|
|
|
43
43
|
"installer"
|
|
44
44
|
],
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@clack/prompts": "1.
|
|
46
|
+
"@clack/prompts": "1.6.0",
|
|
47
|
+
"@larksuiteoapi/node-sdk": "^1.68.0",
|
|
47
48
|
"cross-spawn": "^7.0.6",
|
|
48
49
|
"picocolors": "1.1.1",
|
|
49
50
|
"semver": "^7.8.1",
|
|
50
|
-
"smol-toml": "^1.6.1"
|
|
51
|
+
"smol-toml": "^1.6.1",
|
|
52
|
+
"yaml": "^2.9.0"
|
|
51
53
|
},
|
|
52
54
|
"scripts": {
|
|
53
55
|
"build": "tsc -p tsconfig.json && node scripts/build.js && node scripts/build-inline.js",
|
|
@@ -62,11 +64,14 @@
|
|
|
62
64
|
},
|
|
63
65
|
"devDependencies": {
|
|
64
66
|
"@types/cross-spawn": "^6.0.6",
|
|
65
|
-
"@types/node": "^
|
|
67
|
+
"@types/node": "^24.0.0",
|
|
66
68
|
"@types/semver": "^7.7.1",
|
|
67
69
|
"typescript": "~6.0"
|
|
68
70
|
},
|
|
69
71
|
"optionalDependencies": {
|
|
70
72
|
"@lydell/node-pty": "^1.2.0-beta.12"
|
|
73
|
+
},
|
|
74
|
+
"overrides": {
|
|
75
|
+
"axios": "1.18.1"
|
|
71
76
|
}
|
|
72
77
|
}
|
|
@@ -47,8 +47,8 @@ cp .agents/templates/task.md .agents/workspace/active/task-001.md
|
|
|
47
47
|
|
|
48
48
|
```yaml
|
|
49
49
|
id: task-001
|
|
50
|
-
type: feature # feature | bugfix | refactor | docs |
|
|
51
|
-
status:
|
|
50
|
+
type: feature # feature | bugfix | refactor | docs | chore
|
|
51
|
+
status: active # active | blocked | completed
|
|
52
52
|
assigned_to: claude # claude | codex | gemini | opencode | human
|
|
53
53
|
```
|
|
54
54
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
+
```bash
|
|
65
|
+
ai task issue-body {task-id} > "{body-file}"
|
|
66
|
+
```
|
|
75
67
|
|
|
76
|
-
|
|
68
|
+
#### Red line: never use the entire task.md as the body
|
|
77
69
|
|
|
78
|
-
-
|
|
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
|
-
|
|
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
|