@fitlab-ai/agent-infra 0.7.6 → 0.7.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.ts +11 -3
- package/dist/bin/cli.js +10 -3
- 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/task/commands/issue-body.js +94 -0
- package/dist/lib/task/commands/log.js +92 -9
- package/dist/lib/task/index.js +8 -0
- package/dist/lib/task/issue-form.js +66 -0
- package/dist/lib/task/sections.js +44 -0
- 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/task/commands/issue-body.ts +102 -0
- package/lib/task/commands/log.ts +96 -9
- package/lib/task/index.ts +8 -0
- package/lib/task/issue-form.ts +77 -0
- package/lib/task/sections.ts +44 -0
- package/package.json +3 -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-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 +26 -1
- package/templates/.agents/rules/next-step-output.zh-CN.md +26 -1
- package/templates/.agents/rules/task-management.en.md +2 -0
- package/templates/.agents/rules/task-management.zh-CN.md +2 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -1
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -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 +3 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -1
- 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 +3 -1
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -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 +3 -1
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/import-codescan/SKILL.en.md +3 -1
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +3 -1
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +19 -2
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +19 -2
- 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 +3 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -1
- 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
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import * as p from '@clack/prompts';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import type { SandboxConfig } from '../config.ts';
|
|
5
|
+
import { loadConfig } from '../config.ts';
|
|
6
|
+
import { assertValidBranchName, worktreeDirCandidates } from '../constants.ts';
|
|
7
|
+
import { resolveBranchArg } from './list-running.ts';
|
|
8
|
+
import { resolveTools, toolConfigDirCandidates } from '../tools.ts';
|
|
9
|
+
|
|
10
|
+
const USAGE = `Usage: ai sandbox show <branch | TASK-id | N | '#N'>
|
|
11
|
+
|
|
12
|
+
Shows one sandbox's worktree path and per-tool state paths (Claude Code,
|
|
13
|
+
Codex, Gemini CLI, OpenCode). The argument follows the same contract as
|
|
14
|
+
'ai sandbox exec' and 'ai sandbox start': N (bare) is the recommended form
|
|
15
|
+
for task short ids (e.g. 'ai sandbox show 11'); '#N', a TASK-id, or a plain
|
|
16
|
+
branch name are also accepted. Use 'ai sandbox ls' for the container list.`;
|
|
17
|
+
|
|
18
|
+
export type SandboxDetail = {
|
|
19
|
+
worktrees: string[];
|
|
20
|
+
toolStates: { name: string; entries: string[] }[];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function existingDirs(candidates: string[]): string[] {
|
|
24
|
+
const seen = new Set<string>();
|
|
25
|
+
const result: string[] = [];
|
|
26
|
+
for (const candidate of candidates) {
|
|
27
|
+
if (!seen.has(candidate) && fs.existsSync(candidate)) {
|
|
28
|
+
seen.add(candidate);
|
|
29
|
+
result.push(candidate);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function collectSandboxDetail(config: SandboxConfig, branch: string): SandboxDetail {
|
|
36
|
+
const worktrees = existingDirs(worktreeDirCandidates(config, branch));
|
|
37
|
+
const toolStates = resolveTools(config).map((tool) => ({
|
|
38
|
+
name: tool.name,
|
|
39
|
+
entries: existingDirs(toolConfigDirCandidates(tool, config.project, branch))
|
|
40
|
+
}));
|
|
41
|
+
return { worktrees, toolStates };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function show(args: string[] = []): void {
|
|
45
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
46
|
+
process.stdout.write(`${USAGE}\n`);
|
|
47
|
+
if (args.length === 0) {
|
|
48
|
+
process.exitCode = 1;
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const config = loadConfig();
|
|
54
|
+
const branch = resolveBranchArg(args[0]!, { repoRoot: config.repoRoot });
|
|
55
|
+
assertValidBranchName(branch);
|
|
56
|
+
|
|
57
|
+
const detail = collectSandboxDetail(config, branch);
|
|
58
|
+
|
|
59
|
+
p.intro(pc.cyan(`Sandbox detail for ${config.project} · ${branch}`));
|
|
60
|
+
|
|
61
|
+
p.log.step('Worktree');
|
|
62
|
+
if (detail.worktrees.length === 0) {
|
|
63
|
+
p.log.warn(' No worktree for this branch');
|
|
64
|
+
} else {
|
|
65
|
+
for (const worktree of detail.worktrees) {
|
|
66
|
+
process.stdout.write(` ${worktree}\n`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const tool of detail.toolStates) {
|
|
71
|
+
p.log.step(`${tool.name} state`);
|
|
72
|
+
if (tool.entries.length === 0) {
|
|
73
|
+
p.log.warn(` No ${tool.name} sandbox state`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
for (const entry of tool.entries) {
|
|
77
|
+
process.stdout.write(` ${entry}\n`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
package/lib/sandbox/index.ts
CHANGED
|
@@ -9,6 +9,8 @@ Commands:
|
|
|
9
9
|
ls List sandboxes for the current project (the '#'
|
|
10
10
|
column is a display-only row number; the 'SHORT'
|
|
11
11
|
column shows the active task short id, '-' if none)
|
|
12
|
+
show <branch | TASK-id | N | '#N'>
|
|
13
|
+
Show one sandbox's worktree and per-tool state paths
|
|
12
14
|
prune [--dry-run] Remove orphaned per-branch state dirs
|
|
13
15
|
rebuild [--quiet] [--refresh]
|
|
14
16
|
Rebuild the sandbox image (--refresh pulls base + tools)
|
|
@@ -56,6 +58,11 @@ export async function runSandbox(args: string[]): Promise<void> {
|
|
|
56
58
|
ls(rest);
|
|
57
59
|
break;
|
|
58
60
|
}
|
|
61
|
+
case 'show': {
|
|
62
|
+
const { show } = await import('./commands/show.ts');
|
|
63
|
+
show(rest);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
59
66
|
case 'prune': {
|
|
60
67
|
const { prune } = await import('./commands/prune.ts');
|
|
61
68
|
await prune(rest);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { resolveTaskRef } from '../resolve-ref.ts';
|
|
3
|
+
import { extractTitle } from '../frontmatter.ts';
|
|
4
|
+
import { extractSection, findSectionHeading } from '../sections.ts';
|
|
5
|
+
import { renderTemplateBody, PLACEHOLDER } from '../issue-form.ts';
|
|
6
|
+
import type { TaskFields } from '../issue-form.ts';
|
|
7
|
+
|
|
8
|
+
const USAGE = `Usage: ai task issue-body <N | #N | TASK-id> [--template <path>]
|
|
9
|
+
|
|
10
|
+
Print a deterministic Issue body extracted from a task's task.md.
|
|
11
|
+
<ref> Bare numeric / '#N' short id, or a full TASK-YYYYMMDD-HHMMSS id.
|
|
12
|
+
--template <path> Render the final body for the given GitHub Issue Form (scenario A);
|
|
13
|
+
without it, print the default '描述 + 需求' body (scenario B).
|
|
14
|
+
|
|
15
|
+
Only the task title, '## 描述' and '## 需求' sections are ever emitted; the rest of
|
|
16
|
+
task.md (scaffolding sections, placeholders) is never written to the body.
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
const DESCRIPTION_ALIASES = ['描述', 'Description'];
|
|
20
|
+
const REQUIREMENTS_ALIASES = ['需求', 'Requirements'];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Build the scenario B default body, mirroring whichever heading language the
|
|
24
|
+
* task.md actually uses, with empty sections falling back to `N/A`.
|
|
25
|
+
*/
|
|
26
|
+
function buildDefaultBody(content: string): string {
|
|
27
|
+
const descHeading = findSectionHeading(content, DESCRIPTION_ALIASES);
|
|
28
|
+
const reqHeading = findSectionHeading(content, REQUIREMENTS_ALIASES);
|
|
29
|
+
const description = extractSection(content, DESCRIPTION_ALIASES) || PLACEHOLDER;
|
|
30
|
+
const requirements = extractSection(content, REQUIREMENTS_ALIASES) || PLACEHOLDER;
|
|
31
|
+
return `## ${descHeading}\n\n${description}\n\n## ${reqHeading}\n\n${requirements}\n`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readTaskFields(content: string): TaskFields {
|
|
35
|
+
return {
|
|
36
|
+
title: extractTitle(content),
|
|
37
|
+
description: extractSection(content, DESCRIPTION_ALIASES),
|
|
38
|
+
requirements: extractSection(content, REQUIREMENTS_ALIASES)
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function issueBody(args: string[] = []): void {
|
|
43
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
44
|
+
process.stdout.write(USAGE);
|
|
45
|
+
if (args.length === 0) process.exitCode = 1;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let ref: string | undefined;
|
|
50
|
+
let templatePath: string | undefined;
|
|
51
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
52
|
+
const arg = args[i]!;
|
|
53
|
+
if (arg === '--template') {
|
|
54
|
+
templatePath = args[i + 1];
|
|
55
|
+
i += 1;
|
|
56
|
+
} else if (ref === undefined) {
|
|
57
|
+
ref = arg;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!ref) {
|
|
62
|
+
process.stderr.write('ai task issue-body: missing task ref\n');
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (templatePath === undefined && args.includes('--template')) {
|
|
67
|
+
process.stderr.write('ai task issue-body: --template requires a path\n');
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const resolved = resolveTaskRef(ref);
|
|
73
|
+
if (!resolved.ok) {
|
|
74
|
+
process.stderr.write(`ai task issue-body: ${resolved.message}\n`);
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const content = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
80
|
+
|
|
81
|
+
if (templatePath !== undefined) {
|
|
82
|
+
let formText: string;
|
|
83
|
+
try {
|
|
84
|
+
formText = fs.readFileSync(templatePath, 'utf8');
|
|
85
|
+
} catch (e) {
|
|
86
|
+
process.stderr.write(`ai task issue-body: cannot read template '${templatePath}': ${(e as Error).message}\n`);
|
|
87
|
+
process.exitCode = 1;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
process.stdout.write(renderTemplateBody(formText, readTaskFields(content)));
|
|
92
|
+
} catch (e) {
|
|
93
|
+
process.stderr.write(`ai task issue-body: cannot render template '${templatePath}': ${(e as Error).message}\n`);
|
|
94
|
+
process.exitCode = 1;
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
process.stdout.write(buildDefaultBody(content));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export { issueBody, buildDefaultBody };
|
package/lib/task/commands/log.ts
CHANGED
|
@@ -10,12 +10,17 @@ completion time (or '(in progress)' while still running).
|
|
|
10
10
|
<ref> Bare numeric / '#N' short id, or a full TASK-YYYYMMDD-HHMMSS id.
|
|
11
11
|
|
|
12
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).
|
|
13
17
|
`;
|
|
14
18
|
|
|
15
19
|
const TABLE_HEADERS = ['#', 'STEP', 'AGENT', 'STARTED', 'DONE', 'NOTE'] as const;
|
|
16
20
|
|
|
17
21
|
// The activity-log H2 heading is language-dependent (zh template / en template).
|
|
18
22
|
const HEADING_RE = /^##\s+(活动日志|Activity Log)\s*$/;
|
|
23
|
+
const LEDGER_HEADING_RE = /^##\s+(审查分歧账本|Review Disagreement Ledger)\s*$/;
|
|
19
24
|
const NEXT_H2_RE = /^##\s/;
|
|
20
25
|
// `- {time} — **{step}** by {agent} — {note}` ; the separator is an em-dash
|
|
21
26
|
// (U+2014). STEP/AGENT are non-greedy so a note that itself contains ' — ' or
|
|
@@ -24,6 +29,8 @@ 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';
|
|
33
|
+
type LedgerRow = { stage: string; status: string };
|
|
27
34
|
|
|
28
35
|
// One rendered row = one step instance. `started`/`done` are timestamps; an empty
|
|
29
36
|
// `done` with a non-empty `started` means the step is still in flight, while an
|
|
@@ -35,6 +42,21 @@ type StepRow = { step: string; agent: string; started: string; done: string; not
|
|
|
35
42
|
// without the suffix. Pairing therefore keys on the base action (including any
|
|
36
43
|
// `(Round N)`), so every round and every repeated execution pairs on its own.
|
|
37
44
|
const STARTED_SUFFIX_RE = /\s*\[started\]\s*$/;
|
|
45
|
+
// Short agent tokens that actually appear in activity logs for AI executors
|
|
46
|
+
// (workflow recommended_agents claude/codex/gemini/cursor + enabled TUI opencode;
|
|
47
|
+
// note these differ from the `.airc.json` long names claude-code/gemini-cli).
|
|
48
|
+
// Any other executor token (a human name, possibly CJK) is treated as human.
|
|
49
|
+
const KNOWN_AI_AGENTS = new Set(['claude', 'codex', 'gemini', 'opencode', 'cursor']);
|
|
50
|
+
const HUMAN_DECISION_STATUSES = new Set(['needs-human-decision', 'human-decided']);
|
|
51
|
+
const ENV_BLOCKED_RE = /\(\+\s*(\d+)\s+env-blocked\)/i;
|
|
52
|
+
// Same match plus any leading whitespace, so folding the count into the verdict
|
|
53
|
+
// text drops the redundant `(+ n env-blocked)` fragment without leaving a gap.
|
|
54
|
+
const ENV_BLOCKED_STRIP_RE = /\s*\(\+\s*\d+\s+env-blocked\)/i;
|
|
55
|
+
const REVIEW_STAGE_PREFIXES: { prefix: string; stage: ReviewStage }[] = [
|
|
56
|
+
{ prefix: 'Review Analysis', stage: 'analysis' },
|
|
57
|
+
{ prefix: 'Review Plan', stage: 'plan' },
|
|
58
|
+
{ prefix: 'Review Code', stage: 'code' }
|
|
59
|
+
];
|
|
38
60
|
|
|
39
61
|
function parseActivityLog(content: string): { sectionFound: boolean; entries: LogEntry[] } {
|
|
40
62
|
const lines = content.split('\n');
|
|
@@ -89,6 +111,68 @@ function pairEntries(entries: LogEntry[]): StepRow[] {
|
|
|
89
111
|
return rows;
|
|
90
112
|
}
|
|
91
113
|
|
|
114
|
+
function parseReviewLedger(content: string): LedgerRow[] {
|
|
115
|
+
const lines = content.split('\n');
|
|
116
|
+
let i = 0;
|
|
117
|
+
while (i < lines.length && !LEDGER_HEADING_RE.test(lines[i]!)) i += 1;
|
|
118
|
+
if (i >= lines.length) return [];
|
|
119
|
+
|
|
120
|
+
const rows: LedgerRow[] = [];
|
|
121
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
122
|
+
if (NEXT_H2_RE.test(lines[j]!)) break;
|
|
123
|
+
const line = lines[j]!.trim();
|
|
124
|
+
if (!line.startsWith('|')) continue;
|
|
125
|
+
const cells = line
|
|
126
|
+
.split('|')
|
|
127
|
+
.slice(1, -1)
|
|
128
|
+
.map((cell) => cell.trim());
|
|
129
|
+
if (cells.length < 6 || cells[0] === 'id' || /^-+$/.test(cells[0] ?? '')) continue;
|
|
130
|
+
rows.push({ stage: cells[1]!, status: cells[4]! });
|
|
131
|
+
}
|
|
132
|
+
return rows;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function countHumanDecisionsByStage(rows: LedgerRow[]): Map<ReviewStage, number> {
|
|
136
|
+
const counts = new Map<ReviewStage, number>();
|
|
137
|
+
for (const row of rows) {
|
|
138
|
+
if (!isReviewStage(row.stage) || !HUMAN_DECISION_STATUSES.has(row.status)) continue;
|
|
139
|
+
counts.set(row.stage, (counts.get(row.stage) ?? 0) + 1);
|
|
140
|
+
}
|
|
141
|
+
return counts;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isReviewStage(stage: string): stage is ReviewStage {
|
|
145
|
+
return stage === 'analysis' || stage === 'plan' || stage === 'code';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function reviewStageForStep(step: string): ReviewStage | undefined {
|
|
149
|
+
return REVIEW_STAGE_PREFIXES.find(({ prefix }) => step.startsWith(prefix))?.stage;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function humanValidationCount(note: string): number {
|
|
153
|
+
const match = ENV_BLOCKED_RE.exec(note);
|
|
154
|
+
return match ? Number(match[1]) : 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// A step is human-executed when its agent token is not a known AI token. Take
|
|
158
|
+
// the first whitespace-delimited token and drop any trailing parenthetical
|
|
159
|
+
// annotation (e.g. `张三 (executed on host)` -> `张三`) before the lookup.
|
|
160
|
+
function isHumanAgent(agent: string): boolean {
|
|
161
|
+
const token = agent.trim().split(/\s+/)[0]?.replace(/\(.*$/, '') ?? '';
|
|
162
|
+
return token !== '' && !KNOWN_AI_AGENTS.has(token);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Fold the two human counts into a review row's verdict NOTE: comma-joined, right
|
|
166
|
+
// after the blockers/major/minor list and before the ` → artifact` link, mirroring
|
|
167
|
+
// the review count line. The raw `(+ n env-blocked)` fragment is dropped so the
|
|
168
|
+
// env-blocked number is not shown twice (it becomes the manual-verify count).
|
|
169
|
+
function foldHumanCounts(note: string, decisions: number, envBlocked: number): string {
|
|
170
|
+
const base = note.replace(ENV_BLOCKED_STRIP_RE, '');
|
|
171
|
+
const group = `Manual-verify: ${envBlocked}, Human-decision: ${decisions}`;
|
|
172
|
+
const arrow = base.indexOf(' → ');
|
|
173
|
+
return arrow === -1 ? `${base}, ${group}` : `${base.slice(0, arrow)}, ${group}${base.slice(arrow)}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
92
176
|
function log(args: string[] = []): void {
|
|
93
177
|
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
94
178
|
process.stdout.write(USAGE);
|
|
@@ -116,18 +200,21 @@ function log(args: string[] = []): void {
|
|
|
116
200
|
return;
|
|
117
201
|
}
|
|
118
202
|
const steps = pairEntries(entries);
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
s.step
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
s.
|
|
126
|
-
|
|
203
|
+
const humanDecisionCounts = countHumanDecisionsByStage(parseReviewLedger(content));
|
|
204
|
+
const rows = steps.map((s, idx) => {
|
|
205
|
+
const stage = reviewStageForStep(s.step);
|
|
206
|
+
const note = stage
|
|
207
|
+
? foldHumanCounts(s.note, humanDecisionCounts.get(stage) ?? 0, humanValidationCount(s.note))
|
|
208
|
+
: s.note;
|
|
209
|
+
const human = isHumanAgent(s.agent);
|
|
210
|
+
const agent = human ? 'human' : s.agent;
|
|
211
|
+
const started = s.started || (human ? '-' : '');
|
|
212
|
+
return [String(idx + 1), s.step, agent, started, s.done || (s.started ? '(in progress)' : ''), note];
|
|
213
|
+
});
|
|
127
214
|
for (const line of formatTable(TABLE_HEADERS, rows, { zebra: Boolean(process.stdout.isTTY) })) {
|
|
128
215
|
process.stdout.write(`${line}\n`);
|
|
129
216
|
}
|
|
130
217
|
process.stdout.write(`Total: ${steps.length} steps\n`);
|
|
131
218
|
}
|
|
132
219
|
|
|
133
|
-
export { log, parseActivityLog, pairEntries };
|
|
220
|
+
export { log, parseActivityLog, pairEntries, isHumanAgent };
|
package/lib/task/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ Commands:
|
|
|
4
4
|
cat <ref> <artifact | N> Print a task artifact (by name or number)
|
|
5
5
|
files <ref> List artifacts in a task dir (numbered)
|
|
6
6
|
grep <pattern> [ref] [artifact | N] Literal search across task artifacts (omit ref to scan all)
|
|
7
|
+
issue-body <ref> [--template <path>] Print a deterministic Issue body from task.md (描述 + 需求, or an Issue Form)
|
|
7
8
|
log <ref> Render a task's activity log as a timeline
|
|
8
9
|
ls [--all | --blocked | --completed] List tasks (default: active)
|
|
9
10
|
show <N | #N | TASK-id> Print a task.md
|
|
@@ -15,6 +16,8 @@ Examples:
|
|
|
15
16
|
ai task files 11
|
|
16
17
|
ai task grep resolveArtifact
|
|
17
18
|
ai task grep resolveArtifact 11
|
|
19
|
+
ai task issue-body 11
|
|
20
|
+
ai task issue-body 11 --template .github/ISSUE_TEMPLATE/05_other.yml
|
|
18
21
|
ai task log 11
|
|
19
22
|
ai task ls
|
|
20
23
|
ai task show 11
|
|
@@ -53,6 +56,11 @@ export async function runTask(args: string[]): Promise<void> {
|
|
|
53
56
|
grep(rest);
|
|
54
57
|
break;
|
|
55
58
|
}
|
|
59
|
+
case 'issue-body': {
|
|
60
|
+
const { issueBody } = await import('./commands/issue-body.ts');
|
|
61
|
+
issueBody(rest);
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
56
64
|
case 'log': {
|
|
57
65
|
const { log } = await import('./commands/log.ts');
|
|
58
66
|
log(rest);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { parse } from 'yaml';
|
|
2
|
+
|
|
3
|
+
type TaskFields = {
|
|
4
|
+
title: string;
|
|
5
|
+
description: string;
|
|
6
|
+
requirements: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// Field id -> task value mapping (single source of truth, per HD-1).
|
|
10
|
+
// Only ids that map cleanly to title / description / requirements get a value;
|
|
11
|
+
// every other text field gets `N/A`. This deliberately tightens the older
|
|
12
|
+
// "suggested" mapping table (impact / context / alternatives / steps / expected
|
|
13
|
+
// no longer flow into requirements) so the requirements checklist is never
|
|
14
|
+
// pushed into an unrelated field.
|
|
15
|
+
const TITLE_IDS = new Set(['summary', 'title']);
|
|
16
|
+
const DESCRIPTION_IDS = new Set([
|
|
17
|
+
'description',
|
|
18
|
+
'problem',
|
|
19
|
+
'what-happened',
|
|
20
|
+
'question',
|
|
21
|
+
'current-content',
|
|
22
|
+
'issue-description',
|
|
23
|
+
'detailed-description'
|
|
24
|
+
]);
|
|
25
|
+
const REQUIREMENTS_IDS = new Set(['requirements', 'solution', 'suggested-content']);
|
|
26
|
+
|
|
27
|
+
// Only free-text field types carry task content; structural / static fields are skipped.
|
|
28
|
+
const TEXT_FIELD_TYPES = new Set(['input', 'textarea']);
|
|
29
|
+
const PLACEHOLDER = 'N/A';
|
|
30
|
+
|
|
31
|
+
function mapFieldValue(id: string, fields: TaskFields): string {
|
|
32
|
+
if (TITLE_IDS.has(id)) return fields.title;
|
|
33
|
+
if (DESCRIPTION_IDS.has(id)) return fields.description;
|
|
34
|
+
if (REQUIREMENTS_IDS.has(id)) return fields.requirements;
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type IssueFormField = {
|
|
39
|
+
type?: unknown;
|
|
40
|
+
id?: unknown;
|
|
41
|
+
attributes?: { label?: unknown };
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Render the final Issue body for a GitHub Issue Form (scenario A).
|
|
46
|
+
*
|
|
47
|
+
* Walks the form's `body[]` in order, skips `markdown` / `dropdown` /
|
|
48
|
+
* `checkboxes`, and renders each `input` / `textarea` as `### {label}` followed
|
|
49
|
+
* by the deterministically-mapped task value (or `N/A` when the field has no
|
|
50
|
+
* reliable source). The template structure is preserved; the whole task.md and
|
|
51
|
+
* its scaffolding sections are never emitted.
|
|
52
|
+
*
|
|
53
|
+
* Throws on unreadable / non-object YAML or a missing `body[]` list so the
|
|
54
|
+
* caller can fall back to the default body.
|
|
55
|
+
*/
|
|
56
|
+
function renderTemplateBody(formText: string, fields: TaskFields): string {
|
|
57
|
+
const doc = parse(formText) as { body?: unknown } | null;
|
|
58
|
+
if (!doc || typeof doc !== 'object' || !Array.isArray(doc.body)) {
|
|
59
|
+
throw new Error('Issue Form has no body[] list');
|
|
60
|
+
}
|
|
61
|
+
const sections: string[] = [];
|
|
62
|
+
for (const raw of doc.body as IssueFormField[]) {
|
|
63
|
+
if (!raw || typeof raw !== 'object') continue;
|
|
64
|
+
const type = typeof raw.type === 'string' ? raw.type : '';
|
|
65
|
+
if (!TEXT_FIELD_TYPES.has(type)) continue;
|
|
66
|
+
const label = typeof raw.attributes?.label === 'string' ? raw.attributes.label.trim() : '';
|
|
67
|
+
if (!label) continue;
|
|
68
|
+
const id = typeof raw.id === 'string' ? raw.id : '';
|
|
69
|
+
const mapped = mapFieldValue(id, fields);
|
|
70
|
+
const value = mapped.trim() === '' ? PLACEHOLDER : mapped;
|
|
71
|
+
sections.push(`### ${label}\n\n${value}`);
|
|
72
|
+
}
|
|
73
|
+
return `${sections.join('\n\n')}\n`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export { renderTemplateBody, mapFieldValue, PLACEHOLDER };
|
|
77
|
+
export type { TaskFields };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
function escapeRegExp(value: string): string {
|
|
2
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Return the body of the first `## {alias}` section (any alias matches), from
|
|
7
|
+
* the heading line to the next `## ` heading or EOF. Lines are preserved
|
|
8
|
+
* verbatim (checkbox text is never normalized); only leading/trailing blank
|
|
9
|
+
* lines are trimmed. Returns '' when no alias heading is present.
|
|
10
|
+
*/
|
|
11
|
+
function extractSection(content: string, aliases: string[]): string {
|
|
12
|
+
const lines = content.split('\n');
|
|
13
|
+
let start = -1;
|
|
14
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
15
|
+
const line = lines[i]!.trim();
|
|
16
|
+
if (aliases.some((alias) => new RegExp(`^##\\s+${escapeRegExp(alias)}\\s*$`).test(line))) {
|
|
17
|
+
start = i + 1;
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (start === -1) return '';
|
|
22
|
+
let end = lines.length;
|
|
23
|
+
for (let i = start; i < lines.length; i += 1) {
|
|
24
|
+
if (/^##\s+/.test(lines[i]!)) {
|
|
25
|
+
end = i;
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return lines.slice(start, end).join('\n').replace(/^\n+/, '').replace(/\n+$/, '');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Return the heading alias that actually appears as a `## {alias}` line, so a
|
|
34
|
+
* rendered section can mirror the source language. Falls back to the first
|
|
35
|
+
* alias when none is present.
|
|
36
|
+
*/
|
|
37
|
+
function findSectionHeading(content: string, aliases: string[]): string {
|
|
38
|
+
for (const alias of aliases) {
|
|
39
|
+
if (new RegExp(`^##\\s+${escapeRegExp(alias)}\\s*$`, 'm').test(content)) return alias;
|
|
40
|
+
}
|
|
41
|
+
return aliases[0]!;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { extractSection, findSectionHeading };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fitlab-ai/agent-infra",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.7",
|
|
4
4
|
"description": "Bootstrap tool for AI multi-tool collaboration infrastructure — works with Claude Code, Codex, Gemini CLI, and OpenCode",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -47,7 +47,8 @@
|
|
|
47
47
|
"cross-spawn": "^7.0.6",
|
|
48
48
|
"picocolors": "1.1.1",
|
|
49
49
|
"semver": "^7.8.1",
|
|
50
|
-
"smol-toml": "^1.6.1"
|
|
50
|
+
"smol-toml": "^1.6.1",
|
|
51
|
+
"yaml": "^2.9.0"
|
|
51
52
|
},
|
|
52
53
|
"scripts": {
|
|
53
54
|
"build": "tsc -p tsconfig.json && node scripts/build.js && node scripts/build-inline.js",
|
|
@@ -163,11 +163,13 @@ Recommended frontmatter:
|
|
|
163
163
|
```yaml
|
|
164
164
|
---
|
|
165
165
|
name: enforce-style
|
|
166
|
-
description: "Apply the team style guide before code review"
|
|
166
|
+
description: "Apply the team style guide before code review. Use when you need to align the team's code style before review"
|
|
167
167
|
args: "<task-id>" # optional
|
|
168
168
|
---
|
|
169
169
|
```
|
|
170
170
|
|
|
171
|
+
Write `description` as "one-line responsibility + scenario clause": after the short responsibility, append "Use when …" (the Chinese `SKILL.zh-CN.md` variant uses "当……时使用") as the cross-TUI trigger semantics, so Agent Skills-capable tools can self-discover the skill in natural conversation. Do **not** add a separate `triggers` (or similar) frontmatter field for this.
|
|
172
|
+
|
|
171
173
|
After adding or updating a custom skill, run `update-agent-infra` again. The sync step detects non-built-in skills and generates matching commands for Claude Code, Gemini CLI, and OpenCode automatically.
|
|
172
174
|
|
|
173
175
|
### Shared skill sources
|
|
@@ -163,11 +163,13 @@
|
|
|
163
163
|
```yaml
|
|
164
164
|
---
|
|
165
165
|
name: enforce-style
|
|
166
|
-
description: "
|
|
166
|
+
description: "在代码审查前应用团队风格规范。当需要在评审前统一团队代码风格时使用"
|
|
167
167
|
args: "<task-id>" # 可选
|
|
168
168
|
---
|
|
169
169
|
```
|
|
170
170
|
|
|
171
|
+
`description` 采用「一句话职责 + 场景触发子句」写法:在简短职责描述后补充「当……时使用」(英文 `SKILL.en.md` 用「Use when …」),作为跨 TUI 的触发语义,供支持 Agent Skills 的工具在自然对话中自发现该 skill。**不要**为此新增 `triggers` 等额外 frontmatter 字段。
|
|
172
|
+
|
|
171
173
|
新增或修改自定义 skill 后,再执行一次 `update-agent-infra`。同步过程会自动检测非内置 skill,并为 Claude Code、Gemini CLI、OpenCode 生成对应命令。
|
|
172
174
|
|
|
173
175
|
### 共享 skill 源
|
|
@@ -22,11 +22,11 @@ After `create-task` writes the local `task.md`, follow this rule to cascade Issu
|
|
|
22
22
|
|
|
23
23
|
Pull the following from `task.md`:
|
|
24
24
|
|
|
25
|
-
- Task title (the first `# ` heading, stripped of `任务:` / `Task:` prefixes)
|
|
26
|
-
- The `## Description` / `## 描述` section
|
|
27
|
-
- The `## Requirements` / `## 需求` section
|
|
25
|
+
- Task title (the first `# ` heading, stripped of `任务:` / `Task:` prefixes) — used to build the Issue title
|
|
28
26
|
- frontmatter fields `type` and (optionally) `milestone`
|
|
29
27
|
|
|
28
|
+
> The Issue **body** is not extracted by hand here. It is generated deterministically in §3 by the `ai task issue-body` command from the `## 描述` / `## 需求` sections; callers must not assemble it themselves.
|
|
29
|
+
|
|
30
30
|
Build the Issue title:
|
|
31
31
|
|
|
32
32
|
| task.md `type` | Conventional Commits type |
|
|
@@ -41,45 +41,35 @@ Scope inference: read known module names from `.agents/.airc.json`'s `labels.in`
|
|
|
41
41
|
|
|
42
42
|
### 3. Build the Issue Body
|
|
43
43
|
|
|
44
|
+
> **Mechanization boundary (mandatory)**: the Issue body is always produced deterministically by the `ai task issue-body` command; callers only pass the command's stdout to `gh issue create` via `--body-file` and must **not** assemble, rewrite, or truncate the body themselves. The command emits only the task title / `## 描述` / `## 需求` content; every other task.md scaffolding section never enters the body.
|
|
45
|
+
|
|
44
46
|
Issue Form detection: follow the "Issue Template Detection" section in `.agents/rules/issue-pr-commands.md` to scan `.github/ISSUE_TEMPLATE/*.yml` (excluding `config.yml`).
|
|
45
47
|
|
|
46
48
|
#### Scenario A: A matching template was detected
|
|
47
49
|
|
|
48
50
|
Pick the form whose `name` (or filename) best matches the task type (e.g., a task with `type: bugfix` prefers a form whose name contains `bug`); if no match, fall back to a generic form like `other.yml`; if none, take the first form in the directory.
|
|
49
51
|
|
|
50
|
-
|
|
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
|