@fitlab-ai/agent-infra 0.7.3 → 0.7.5
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 +32 -790
- package/README.zh-CN.md +32 -763
- package/bin/cli.ts +13 -11
- package/dist/bin/cli.js +13 -11
- package/dist/lib/init.js +1 -1
- package/dist/lib/merge.js +1 -1
- package/dist/lib/sandbox/commands/create.js +44 -3
- package/dist/lib/sandbox/commands/rm.js +99 -19
- package/dist/lib/sandbox/index.js +24 -22
- package/dist/lib/sandbox/readme-scaffold.js +6 -6
- package/dist/lib/task/artifacts.js +58 -0
- package/dist/lib/task/commands/cat.js +38 -0
- package/dist/lib/task/commands/files.js +47 -0
- package/dist/lib/task/commands/grep.js +143 -0
- package/dist/lib/task/commands/log.js +75 -0
- package/dist/lib/task/commands/show.js +5 -114
- package/dist/lib/task/commands/status.js +239 -0
- package/dist/lib/task/index.js +37 -0
- package/dist/lib/task/resolve-ref.js +150 -0
- package/dist/lib/update.js +1 -1
- package/lib/init.ts +1 -1
- package/lib/merge.ts +1 -1
- package/lib/sandbox/commands/create.ts +47 -4
- package/lib/sandbox/commands/rm.ts +128 -19
- package/lib/sandbox/index.ts +24 -22
- package/lib/sandbox/readme-scaffold.ts +6 -6
- package/lib/task/artifacts.ts +72 -0
- package/lib/task/commands/cat.ts +39 -0
- package/lib/task/commands/files.ts +53 -0
- package/lib/task/commands/grep.ts +147 -0
- package/lib/task/commands/log.ts +80 -0
- package/lib/task/commands/show.ts +5 -117
- package/lib/task/commands/status.ts +302 -0
- package/lib/task/index.ts +37 -0
- package/lib/task/resolve-ref.ts +160 -0
- package/lib/update.ts +1 -1
- package/package.json +1 -1
- package/templates/.agents/README.en.md +1 -0
- package/templates/.agents/README.zh-CN.md +1 -0
- package/templates/.agents/rules/README.en.md +45 -0
- package/templates/.agents/rules/README.zh-CN.md +44 -0
- package/templates/.agents/rules/cli-help-format.en.md +49 -0
- package/templates/.agents/rules/cli-help-format.zh-CN.md +49 -0
- package/templates/.agents/rules/debugging-guide.en.md +25 -0
- package/templates/.agents/rules/debugging-guide.zh-CN.md +25 -0
- package/templates/.agents/rules/no-mid-flow-questions.en.md +14 -2
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +14 -2
- package/templates/.agents/rules/pr-sync.github.en.md +8 -6
- package/templates/.agents/rules/pr-sync.github.zh-CN.md +8 -6
- package/templates/.agents/rules/review-handshake.en.md +83 -0
- package/templates/.agents/rules/review-handshake.zh-CN.md +83 -0
- package/templates/.agents/scripts/lib/post-review-commit.js +56 -0
- package/templates/.agents/scripts/lib/review-artifacts.js +117 -0
- package/templates/.agents/scripts/review-diff-fingerprint.js +99 -0
- package/templates/.agents/scripts/validate-artifact.js +240 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +52 -6
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +52 -6
- package/templates/.agents/skills/code-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -0
- package/templates/.agents/skills/code-task/config/verify.en.json +3 -0
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +3 -0
- package/templates/.agents/skills/code-task/reference/fix-mode.en.md +5 -3
- package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +5 -3
- package/templates/.agents/skills/code-task/reference/report-template.en.md +4 -4
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +4 -4
- package/templates/.agents/skills/code-task/scripts/detect-mode.js +2 -107
- package/templates/.agents/skills/commit/SKILL.en.md +6 -0
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -0
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +8 -0
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +8 -0
- package/templates/.agents/skills/complete-task/SKILL.en.md +10 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +10 -0
- package/templates/.agents/skills/complete-task/config/verify.en.json +2 -0
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -0
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +1 -1
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/plan-task/config/verify.en.json +3 -0
- package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +3 -0
- package/templates/.agents/skills/review-analysis/config/verify.en.json +2 -1
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +2 -1
- package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +5 -4
- package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +5 -4
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +4 -0
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +4 -0
- package/templates/.agents/skills/review-code/SKILL.en.md +4 -1
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -1
- package/templates/.agents/skills/review-code/config/verify.en.json +5 -2
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +5 -2
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +5 -4
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +5 -4
- package/templates/.agents/skills/review-code/reference/report-template.en.md +6 -0
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +6 -0
- package/templates/.agents/skills/review-plan/config/verify.en.json +2 -1
- package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +2 -1
- package/templates/.agents/skills/review-plan/reference/output-templates.en.md +5 -4
- package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +5 -4
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +4 -0
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +4 -0
- package/templates/.agents/skills/watch-pr/SKILL.en.md +1 -1
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/templates/task.en.md +7 -0
- package/templates/.agents/templates/task.zh-CN.md +7 -0
- package/templates/.github/workflows/metadata-sync.yml +1 -1
- package/templates/.github/workflows/pr-label.yml +1 -1
- package/templates/.github/workflows/status-label.yml +1 -1
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { resolveTaskRef } from '../resolve-ref.ts';
|
|
4
|
+
import { enumerateArtifacts, type Artifact } from '../artifacts.ts';
|
|
5
|
+
import { parseTaskFrontmatter, extractTitle, type Frontmatter } from '../frontmatter.ts';
|
|
6
|
+
import { loadShortIdByTaskId } from '../short-id.ts';
|
|
7
|
+
|
|
8
|
+
const USAGE = `Usage: ai task status <N | #N | TASK-id>
|
|
9
|
+
|
|
10
|
+
Prints an aggregated "health check" view for a task: header, metadata, an
|
|
11
|
+
artifacts summary, git branch state, and best-effort GitHub issue/PR status.
|
|
12
|
+
<ref> Bare numeric / '#N' short id, or a full TASK-YYYYMMDD-HHMMSS id.
|
|
13
|
+
|
|
14
|
+
Git and Platform rows are best-effort: a failed git/gh call degrades that row to
|
|
15
|
+
'-' without failing the command.
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
const DASH = '-';
|
|
19
|
+
|
|
20
|
+
// Subprocess boundary: the single place this command shells out. Injectable so
|
|
21
|
+
// the collectors below can be unit-tested without spawning git/gh. Returns the
|
|
22
|
+
// command's stdout; throws (like execFileSync) on a non-zero exit or spawn error.
|
|
23
|
+
type Runner = (file: string, args: string[]) => string;
|
|
24
|
+
|
|
25
|
+
function makeRunner(cwd: string): Runner {
|
|
26
|
+
return (file, args) =>
|
|
27
|
+
execFileSync(file, args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Run `run` and swallow any failure into null, so a single failing git/gh call
|
|
31
|
+
// degrades only its own field instead of aborting the whole view.
|
|
32
|
+
function tryRun(run: Runner, file: string, args: string[]): string | null {
|
|
33
|
+
try {
|
|
34
|
+
return run(file, args);
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Frontmatter keys shown in the Metadata section, in a fixed display order.
|
|
41
|
+
const METADATA_KEYS = [
|
|
42
|
+
'type',
|
|
43
|
+
'status',
|
|
44
|
+
'current_step',
|
|
45
|
+
'priority',
|
|
46
|
+
'effort',
|
|
47
|
+
'branch',
|
|
48
|
+
'assigned_to',
|
|
49
|
+
'created_at',
|
|
50
|
+
'updated_at',
|
|
51
|
+
'issue_number',
|
|
52
|
+
'pr_status'
|
|
53
|
+
] as const;
|
|
54
|
+
|
|
55
|
+
function collectMetadata(fm: Frontmatter): [string, string][] {
|
|
56
|
+
return METADATA_KEYS.map((key) => [key, fm[key] ? fm[key]! : DASH]);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Workflow stages in timeline order; artifacts are bucketed by filename prefix.
|
|
60
|
+
// `review-*` prefixes are matched before their bare counterparts so that, e.g.,
|
|
61
|
+
// `review-analysis.md` is never swallowed by the `analysis` bucket.
|
|
62
|
+
const STAGE_ORDER = [
|
|
63
|
+
'analysis',
|
|
64
|
+
'review-analysis',
|
|
65
|
+
'plan',
|
|
66
|
+
'review-plan',
|
|
67
|
+
'code',
|
|
68
|
+
'review-code',
|
|
69
|
+
'task',
|
|
70
|
+
'other'
|
|
71
|
+
] as const;
|
|
72
|
+
|
|
73
|
+
function stageOf(name: string): string {
|
|
74
|
+
const stem = name.replace(/\.md$/, '');
|
|
75
|
+
if (stem === 'task') return 'task';
|
|
76
|
+
if (stem.startsWith('review-analysis')) return 'review-analysis';
|
|
77
|
+
if (stem.startsWith('review-plan')) return 'review-plan';
|
|
78
|
+
if (stem.startsWith('review-code')) return 'review-code';
|
|
79
|
+
if (stem.startsWith('analysis')) return 'analysis';
|
|
80
|
+
if (stem.startsWith('plan')) return 'plan';
|
|
81
|
+
if (stem.startsWith('code')) return 'code';
|
|
82
|
+
return 'other';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Group artifacts by workflow stage, preserving the input order (mtime ascending
|
|
86
|
+
// from enumerateArtifacts) within each stage and dropping empty stages.
|
|
87
|
+
function groupArtifacts(artifacts: Artifact[]): { stage: string; files: string[] }[] {
|
|
88
|
+
const byStage = new Map<string, string[]>();
|
|
89
|
+
for (const artifact of artifacts) {
|
|
90
|
+
const stage = stageOf(artifact.name);
|
|
91
|
+
const bucket = byStage.get(stage);
|
|
92
|
+
if (bucket) bucket.push(artifact.name);
|
|
93
|
+
else byStage.set(stage, [artifact.name]);
|
|
94
|
+
}
|
|
95
|
+
const groups: { stage: string; files: string[] }[] = [];
|
|
96
|
+
for (const stage of STAGE_ORDER) {
|
|
97
|
+
const files = byStage.get(stage);
|
|
98
|
+
if (files && files.length > 0) groups.push({ stage, files });
|
|
99
|
+
}
|
|
100
|
+
return groups;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
type GitInfo = {
|
|
104
|
+
current: string;
|
|
105
|
+
frontmatter: string;
|
|
106
|
+
match: string;
|
|
107
|
+
exists: string;
|
|
108
|
+
uncommitted: string;
|
|
109
|
+
aheadBehind: string;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// `frontmatterBranch` is the task.md `branch` field (caller passes '' when absent).
|
|
113
|
+
// It is read straight from frontmatter and never depends on a subprocess, so it
|
|
114
|
+
// keeps its value even when every git call fails. All other fields degrade to '-'
|
|
115
|
+
// on failure of their own command.
|
|
116
|
+
function collectGit(frontmatterBranch: string, run: Runner): GitInfo {
|
|
117
|
+
const frontmatter = frontmatterBranch ? frontmatterBranch : DASH;
|
|
118
|
+
|
|
119
|
+
let current = DASH;
|
|
120
|
+
const cur = tryRun(run, 'git', ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
121
|
+
if (cur !== null && cur.trim()) current = cur.trim();
|
|
122
|
+
|
|
123
|
+
let match = DASH;
|
|
124
|
+
if (current !== DASH && frontmatter !== DASH) {
|
|
125
|
+
match = current === frontmatter ? 'yes' : 'no';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let exists = DASH;
|
|
129
|
+
if (frontmatter !== DASH) {
|
|
130
|
+
const verified = tryRun(run, 'git', ['rev-parse', '--verify', '--quiet', `refs/heads/${frontmatter}`]);
|
|
131
|
+
exists = verified === null ? 'no' : 'yes';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let uncommitted = DASH;
|
|
135
|
+
const porcelain = tryRun(run, 'git', ['status', '--porcelain']);
|
|
136
|
+
if (porcelain !== null) {
|
|
137
|
+
const changed = porcelain.split('\n').filter((line) => line.trim() !== '');
|
|
138
|
+
uncommitted = changed.length === 0 ? 'clean' : `${changed.length} file(s)`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let aheadBehind = DASH;
|
|
142
|
+
if (frontmatter !== DASH) {
|
|
143
|
+
const counts = tryRun(run, 'git', [
|
|
144
|
+
'rev-list',
|
|
145
|
+
'--left-right',
|
|
146
|
+
'--count',
|
|
147
|
+
`${frontmatter}...${frontmatter}@{upstream}`
|
|
148
|
+
]);
|
|
149
|
+
if (counts !== null) {
|
|
150
|
+
const parts = counts.trim().split(/\s+/);
|
|
151
|
+
if (parts.length === 2) aheadBehind = `${parts[0]} ahead / ${parts[1]} behind`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return { current, frontmatter, match, exists, uncommitted, aheadBehind };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
type PlatformInfo = { issue: string; pr: string };
|
|
159
|
+
|
|
160
|
+
function collectPlatform(fm: Frontmatter, run: Runner): PlatformInfo {
|
|
161
|
+
let issue = DASH;
|
|
162
|
+
if (fm.issue_number && /^\d+$/.test(fm.issue_number)) {
|
|
163
|
+
const out = tryRun(run, 'gh', ['issue', 'view', fm.issue_number, '--json', 'state,labels']);
|
|
164
|
+
if (out !== null) {
|
|
165
|
+
try {
|
|
166
|
+
const data = JSON.parse(out);
|
|
167
|
+
const labels = Array.isArray(data.labels)
|
|
168
|
+
? data.labels.map((label: { name: string }) => label.name).join(', ')
|
|
169
|
+
: '';
|
|
170
|
+
issue = labels ? `${data.state} [${labels}]` : `${data.state}`;
|
|
171
|
+
} catch {
|
|
172
|
+
issue = DASH;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let pr = DASH;
|
|
178
|
+
if (fm.pr_status === 'created' && fm.pr_number && /^\d+$/.test(fm.pr_number)) {
|
|
179
|
+
const out = tryRun(run, 'gh', ['pr', 'view', fm.pr_number, '--json', 'state,statusCheckRollup']);
|
|
180
|
+
if (out !== null) {
|
|
181
|
+
try {
|
|
182
|
+
const data = JSON.parse(out);
|
|
183
|
+
const rollup = Array.isArray(data.statusCheckRollup) ? data.statusCheckRollup : [];
|
|
184
|
+
const passed = rollup.filter(
|
|
185
|
+
(check: { conclusion?: string; state?: string }) =>
|
|
186
|
+
check.conclusion === 'SUCCESS' || check.state === 'SUCCESS'
|
|
187
|
+
).length;
|
|
188
|
+
pr = rollup.length > 0 ? `${data.state}, checks: ${passed}/${rollup.length}` : `${data.state}`;
|
|
189
|
+
} catch {
|
|
190
|
+
pr = DASH;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return { issue, pr };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
type StatusModel = {
|
|
199
|
+
taskId: string;
|
|
200
|
+
shortId: string;
|
|
201
|
+
title: string;
|
|
202
|
+
issueNumber: string;
|
|
203
|
+
metadata: [string, string][];
|
|
204
|
+
artifacts: { count: number; groups: { stage: string; files: string[] }[] };
|
|
205
|
+
git: GitInfo;
|
|
206
|
+
platform: PlatformInfo;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Indent each label/value pair by two spaces and pad labels to a common width so
|
|
210
|
+
// every section reads as an aligned "key value" block.
|
|
211
|
+
function renderPairs(rows: [string, string][]): string[] {
|
|
212
|
+
const width = rows.reduce((max, [label]) => Math.max(max, label.length), 0);
|
|
213
|
+
return rows.map(([label, value]) => ` ${label.padEnd(width)} ${value}`.trimEnd());
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function renderStatus(model: StatusModel): string[] {
|
|
217
|
+
const lines: string[] = [];
|
|
218
|
+
|
|
219
|
+
lines.push(`Task ${model.taskId} (${model.shortId})`);
|
|
220
|
+
if (model.title) lines.push(model.title);
|
|
221
|
+
|
|
222
|
+
lines.push('', 'Metadata', ...renderPairs(model.metadata));
|
|
223
|
+
|
|
224
|
+
lines.push('', `Artifacts (${model.artifacts.count})`);
|
|
225
|
+
if (model.artifacts.groups.length === 0) {
|
|
226
|
+
lines.push(' (none)');
|
|
227
|
+
} else {
|
|
228
|
+
lines.push(...renderPairs(model.artifacts.groups.map((group) => [group.stage, group.files.join(', ')])));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
lines.push(
|
|
232
|
+
'',
|
|
233
|
+
'Git',
|
|
234
|
+
...renderPairs([
|
|
235
|
+
['current', model.git.current],
|
|
236
|
+
['frontmatter', model.git.frontmatter],
|
|
237
|
+
['match', model.git.match],
|
|
238
|
+
['exists', model.git.exists],
|
|
239
|
+
['uncommitted', model.git.uncommitted],
|
|
240
|
+
['ahead/behind', model.git.aheadBehind]
|
|
241
|
+
])
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const issueLabel = model.issueNumber ? `issue #${model.issueNumber}` : 'issue';
|
|
245
|
+
lines.push(
|
|
246
|
+
'',
|
|
247
|
+
'Platform',
|
|
248
|
+
...renderPairs([
|
|
249
|
+
[issueLabel, model.platform.issue],
|
|
250
|
+
['pr', model.platform.pr]
|
|
251
|
+
])
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
return lines;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function status(args: string[] = []): void {
|
|
258
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
259
|
+
process.stdout.write(USAGE);
|
|
260
|
+
if (args.length === 0) process.exitCode = 1;
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const resolved = resolveTaskRef(args[0]!);
|
|
265
|
+
if (!resolved.ok) {
|
|
266
|
+
process.stderr.write(`ai task status: ${resolved.message}\n`);
|
|
267
|
+
process.exitCode = 1;
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const content = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
272
|
+
const fm = parseTaskFrontmatter(content);
|
|
273
|
+
const run = makeRunner(resolved.repoRoot);
|
|
274
|
+
const artifacts = enumerateArtifacts(resolved.taskDir);
|
|
275
|
+
|
|
276
|
+
const model: StatusModel = {
|
|
277
|
+
taskId: resolved.taskId,
|
|
278
|
+
shortId: loadShortIdByTaskId(resolved.repoRoot).get(resolved.taskId) ?? DASH,
|
|
279
|
+
title: extractTitle(content),
|
|
280
|
+
issueNumber: fm.issue_number && /^\d+$/.test(fm.issue_number) ? fm.issue_number : '',
|
|
281
|
+
metadata: collectMetadata(fm),
|
|
282
|
+
artifacts: { count: artifacts.length, groups: groupArtifacts(artifacts) },
|
|
283
|
+
git: collectGit(fm.branch ?? '', run),
|
|
284
|
+
platform: collectPlatform(fm, run)
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
for (const line of renderStatus(model)) {
|
|
288
|
+
process.stdout.write(`${line}\n`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export {
|
|
293
|
+
status,
|
|
294
|
+
makeRunner,
|
|
295
|
+
collectMetadata,
|
|
296
|
+
groupArtifacts,
|
|
297
|
+
collectGit,
|
|
298
|
+
collectPlatform,
|
|
299
|
+
renderStatus,
|
|
300
|
+
METADATA_KEYS
|
|
301
|
+
};
|
|
302
|
+
export type { Runner, GitInfo, PlatformInfo, StatusModel };
|
package/lib/task/index.ts
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
const USAGE = `Usage: ai task <command> [options]
|
|
2
2
|
|
|
3
3
|
Commands:
|
|
4
|
+
cat <ref> <artifact | N> Print a task artifact (by name or number)
|
|
5
|
+
files <ref> List artifacts in a task dir (numbered)
|
|
6
|
+
grep <pattern> [ref] [artifact | N] Literal search across task artifacts (omit ref to scan all)
|
|
7
|
+
log <ref> Render a task's activity log as a timeline
|
|
4
8
|
ls [--all | --blocked | --completed] List tasks (default: active)
|
|
5
9
|
show <N | #N | TASK-id> Print a task.md
|
|
10
|
+
status <ref> Aggregated status view (metadata / artifacts / git / platform)
|
|
6
11
|
|
|
7
12
|
Examples:
|
|
13
|
+
ai task cat 11 analysis
|
|
14
|
+
ai task cat 11 3
|
|
15
|
+
ai task files 11
|
|
16
|
+
ai task grep resolveArtifact
|
|
17
|
+
ai task grep resolveArtifact 11
|
|
18
|
+
ai task log 11
|
|
8
19
|
ai task ls
|
|
9
20
|
ai task show 11
|
|
10
21
|
ai task show TASK-20260612-162737
|
|
22
|
+
ai task status 11
|
|
11
23
|
|
|
12
24
|
Run 'ai task <command> --help' for details.`;
|
|
13
25
|
|
|
@@ -26,6 +38,26 @@ export async function runTask(args: string[]): Promise<void> {
|
|
|
26
38
|
}
|
|
27
39
|
|
|
28
40
|
switch (subcommand) {
|
|
41
|
+
case 'cat': {
|
|
42
|
+
const { cat } = await import('./commands/cat.ts');
|
|
43
|
+
cat(rest);
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
case 'files': {
|
|
47
|
+
const { files } = await import('./commands/files.ts');
|
|
48
|
+
files(rest);
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
case 'grep': {
|
|
52
|
+
const { grep } = await import('./commands/grep.ts');
|
|
53
|
+
grep(rest);
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
case 'log': {
|
|
57
|
+
const { log } = await import('./commands/log.ts');
|
|
58
|
+
log(rest);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
29
61
|
case 'ls': {
|
|
30
62
|
const { ls } = await import('./commands/ls.ts');
|
|
31
63
|
ls(rest);
|
|
@@ -36,6 +68,11 @@ export async function runTask(args: string[]): Promise<void> {
|
|
|
36
68
|
show(rest);
|
|
37
69
|
break;
|
|
38
70
|
}
|
|
71
|
+
case 'status': {
|
|
72
|
+
const { status } = await import('./commands/status.ts');
|
|
73
|
+
status(rest);
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
39
76
|
default:
|
|
40
77
|
process.stderr.write(`Unknown task command: ${subcommand}\n\n`);
|
|
41
78
|
process.stdout.write(`${USAGE}\n`);
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
4
|
+
import { normalizeShortIdInput } from './short-id.ts';
|
|
5
|
+
|
|
6
|
+
const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
|
|
7
|
+
// Flat-structured workspace dirs that hold tasks under `{dir}/{taskId}/task.md`.
|
|
8
|
+
// Note: `archive` uses a three-level YYYY/MM/DD layout and is handled separately.
|
|
9
|
+
const FLAT_WORKSPACE_DIRS = ['active', 'blocked', 'completed'] as const;
|
|
10
|
+
|
|
11
|
+
type ResolveRefResult =
|
|
12
|
+
| {
|
|
13
|
+
ok: true;
|
|
14
|
+
repoRoot: string;
|
|
15
|
+
taskId: string;
|
|
16
|
+
taskDir: string;
|
|
17
|
+
taskMdPath: string;
|
|
18
|
+
}
|
|
19
|
+
| { ok: false; message: string };
|
|
20
|
+
|
|
21
|
+
function detectRepoRoot(): string {
|
|
22
|
+
try {
|
|
23
|
+
return execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
24
|
+
encoding: 'utf8',
|
|
25
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
26
|
+
}).trim();
|
|
27
|
+
} catch {
|
|
28
|
+
throw new Error('ai task: current directory is not inside a git repository');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readShortIdLength(repoRoot: string): number {
|
|
33
|
+
try {
|
|
34
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8'));
|
|
35
|
+
const v = cfg?.task?.shortIdLength;
|
|
36
|
+
if (typeof v === 'number' && Number.isFinite(v) && v >= 1) return v;
|
|
37
|
+
} catch {
|
|
38
|
+
// fall through to default
|
|
39
|
+
}
|
|
40
|
+
return 2;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resolveShortIdToTaskId(arg: string, repoRoot: string): string {
|
|
44
|
+
const scriptPath = path.join(repoRoot, '.agents', 'scripts', 'task-short-id.js');
|
|
45
|
+
if (!fs.existsSync(scriptPath)) {
|
|
46
|
+
throw new Error(`task-short-id.js not found at ${scriptPath}`);
|
|
47
|
+
}
|
|
48
|
+
const result = spawnSync('node', [scriptPath, 'resolve', arg], {
|
|
49
|
+
encoding: 'utf8',
|
|
50
|
+
cwd: repoRoot
|
|
51
|
+
});
|
|
52
|
+
if (result.status !== 0) {
|
|
53
|
+
throw new Error((result.stderr || '').trim() || `failed to resolve '${arg}'`);
|
|
54
|
+
}
|
|
55
|
+
return result.stdout.trim();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function listSortedNumeric(dir: string, width: number): string[] {
|
|
59
|
+
if (!fs.existsSync(dir)) return [];
|
|
60
|
+
const pattern = new RegExp(`^\\d{${width}}$`);
|
|
61
|
+
return fs
|
|
62
|
+
.readdirSync(dir)
|
|
63
|
+
.filter((entry) => pattern.test(entry))
|
|
64
|
+
.sort()
|
|
65
|
+
.reverse();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function findInArchive(repoRoot: string, taskId: string): string | null {
|
|
69
|
+
// archive-tasks SKILL writes to .agents/workspace/archive/YYYY/MM/DD/{taskId}/task.md
|
|
70
|
+
// where YYYY/MM/DD comes from completed_at (or updated_at fallback) — NOT from
|
|
71
|
+
// the task id's creation date. So we cannot derive the path from taskId alone;
|
|
72
|
+
// walk the bounded YYYY/MM/DD tree instead. Newest-first to favor recent archives.
|
|
73
|
+
const archiveDir = path.join(repoRoot, '.agents', 'workspace', 'archive');
|
|
74
|
+
for (const year of listSortedNumeric(archiveDir, 4)) {
|
|
75
|
+
const yearDir = path.join(archiveDir, year);
|
|
76
|
+
for (const month of listSortedNumeric(yearDir, 2)) {
|
|
77
|
+
const monthDir = path.join(yearDir, month);
|
|
78
|
+
for (const day of listSortedNumeric(monthDir, 2)) {
|
|
79
|
+
const candidate = path.join(monthDir, day, taskId, 'task.md');
|
|
80
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function findTaskMd(repoRoot: string, taskId: string): string | null {
|
|
88
|
+
for (const sub of FLAT_WORKSPACE_DIRS) {
|
|
89
|
+
const candidate = path.join(repoRoot, '.agents', 'workspace', sub, taskId, 'task.md');
|
|
90
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
91
|
+
}
|
|
92
|
+
return findInArchive(repoRoot, taskId);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Enumerate every task directory under the flat workspace states
|
|
97
|
+
* (active / blocked / completed) — archive is intentionally excluded so a
|
|
98
|
+
* full-tree scan never pulls in cold data. Ordered by state, then task id
|
|
99
|
+
* ascending, giving callers a deterministic traversal.
|
|
100
|
+
*/
|
|
101
|
+
function enumerateTaskDirs(repoRoot: string): { taskId: string; taskDir: string }[] {
|
|
102
|
+
const out: { taskId: string; taskDir: string }[] = [];
|
|
103
|
+
for (const sub of FLAT_WORKSPACE_DIRS) {
|
|
104
|
+
const base = path.join(repoRoot, '.agents', 'workspace', sub);
|
|
105
|
+
if (!fs.existsSync(base)) continue;
|
|
106
|
+
for (const entry of fs.readdirSync(base).sort()) {
|
|
107
|
+
if (!TASK_ID_RE.test(entry)) continue;
|
|
108
|
+
const taskDir = path.join(base, entry);
|
|
109
|
+
if (!fs.existsSync(path.join(taskDir, 'task.md'))) continue;
|
|
110
|
+
out.push({ taskId: entry, taskDir });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Resolve a task ref (bare short id, `#N`, or `TASK-YYYYMMDD-HHMMSS`) to its
|
|
118
|
+
* task directory across active / blocked / completed / archive.
|
|
119
|
+
*
|
|
120
|
+
* The returned `message` on failure is command-agnostic (no `ai task <cmd>:`
|
|
121
|
+
* prefix); callers prepend their own prefix so each command keeps its existing
|
|
122
|
+
* stderr wording byte-for-byte.
|
|
123
|
+
*/
|
|
124
|
+
function resolveTaskRef(arg: string): ResolveRefResult {
|
|
125
|
+
const repoRoot = detectRepoRoot();
|
|
126
|
+
let taskId: string;
|
|
127
|
+
if (TASK_ID_RE.test(arg)) {
|
|
128
|
+
taskId = arg;
|
|
129
|
+
} else {
|
|
130
|
+
const shortIdLength = readShortIdLength(repoRoot);
|
|
131
|
+
const normalized = normalizeShortIdInput(arg, { shortIdLength });
|
|
132
|
+
if (normalized.kind === 'error') {
|
|
133
|
+
return { ok: false, message: normalized.message };
|
|
134
|
+
}
|
|
135
|
+
if (normalized.kind === 'pass') {
|
|
136
|
+
return {
|
|
137
|
+
ok: false,
|
|
138
|
+
message:
|
|
139
|
+
`'${arg}' is not a valid short id or TASK-id; ` +
|
|
140
|
+
`expected bare digits, '#N', or 'TASK-YYYYMMDD-HHMMSS'`
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
taskId = resolveShortIdToTaskId(normalized.value, repoRoot);
|
|
145
|
+
} catch (e) {
|
|
146
|
+
return { ok: false, message: (e as Error).message };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const taskMdPath = findTaskMd(repoRoot, taskId);
|
|
150
|
+
if (!taskMdPath) {
|
|
151
|
+
return {
|
|
152
|
+
ok: false,
|
|
153
|
+
message: `task ${taskId} not found in active / blocked / completed / archive`
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return { ok: true, repoRoot, taskId, taskDir: path.dirname(taskMdPath), taskMdPath };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export { resolveTaskRef, detectRepoRoot, enumerateTaskDirs, TASK_ID_RE };
|
|
160
|
+
export type { ResolveRefResult };
|
package/lib/update.ts
CHANGED
|
@@ -119,7 +119,7 @@ function syncFileRegistry(config: UpdateConfig, platformType: string, enabledTUI
|
|
|
119
119
|
|
|
120
120
|
async function cmdUpdate(): Promise<void> {
|
|
121
121
|
console.log('');
|
|
122
|
-
console.log('
|
|
122
|
+
console.log(' ai update');
|
|
123
123
|
console.log(' ==================================');
|
|
124
124
|
console.log('');
|
|
125
125
|
|
package/package.json
CHANGED
|
@@ -34,6 +34,7 @@ This dual-config approach ensures every AI tool receives appropriate project con
|
|
|
34
34
|
bug-fix.yaml # Bug fix workflow
|
|
35
35
|
code-review.yaml # Code review workflow
|
|
36
36
|
refactoring.yaml # Refactoring workflow
|
|
37
|
+
rules/ # Collaboration rule index (see rules/README.md)
|
|
37
38
|
workspace/ # Runtime workspace (git-ignored)
|
|
38
39
|
active/ # Currently active tasks
|
|
39
40
|
blocked/ # Blocked tasks
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Rules Index
|
|
2
|
+
|
|
3
|
+
`.agents/rules/` holds every collaboration rule in this project. Each SKILL loads the
|
|
4
|
+
relevant few on demand; this index groups all rules by domain with a one-line purpose,
|
|
5
|
+
so you can quickly find "which ones to read" without opening each file.
|
|
6
|
+
|
|
7
|
+
> Maintenance note: when adding or removing `.agents/rules/*.md`, update this index too.
|
|
8
|
+
|
|
9
|
+
## General Principles
|
|
10
|
+
|
|
11
|
+
- [`no-mid-flow-questions.md`](no-mid-flow-questions.md) — Silence during SKILL runs: no user questions by default, plus the exceptions the rule lists.
|
|
12
|
+
- [`next-step-output.md`](next-step-output.md) — "Next step" output rules: task short-id rendering and the `Completed at` trailer.
|
|
13
|
+
- [`version-stamp.md`](version-stamp.md) — How and when to stamp `agent_infra_version`.
|
|
14
|
+
- [`debugging-guide.md`](debugging-guide.md) — Structured debugging flow: gather evidence → form hypothesis → verify hypothesis → fix the root cause; no blind patch-and-retry.
|
|
15
|
+
|
|
16
|
+
## Issue / PR
|
|
17
|
+
|
|
18
|
+
- [`issue-pr-commands.md`](issue-pr-commands.md) — GitHub commands to verify auth and read/write Issues / PRs.
|
|
19
|
+
- [`pr-checks-commands.md`](pr-checks-commands.md) — Commands to watch PR required checks and pull failure logs (`watch-pr`).
|
|
20
|
+
- [`create-issue.md`](create-issue.md) — Cascading Issue creation after `create-task` writes `task.md`.
|
|
21
|
+
- [`issue-sync.md`](issue-sync.md) — Sync markers and flow for task artifacts ↔ Issue comments / labels / fields.
|
|
22
|
+
- [`issue-fields.md`](issue-fields.md) — Read/write flow for Issue Type pinned fields (Priority/Effort/dates).
|
|
23
|
+
- [`pr-sync.md`](pr-sync.md) — Sync rule for the single reviewer-facing PR summary comment.
|
|
24
|
+
|
|
25
|
+
## Task Workflow
|
|
26
|
+
|
|
27
|
+
- [`task-management.md`](task-management.md) — Task intent detection and workflow-command mapping.
|
|
28
|
+
- [`review-handshake.md`](review-handshake.md) — Three-stage bidirectional review handshake: four-state disposition, symmetric evidence, disagreement ledger, convergence and post-review commit gate.
|
|
29
|
+
- [`task-short-id.md`](task-short-id.md) — Resolution, allocation and lifecycle of `#NN` / bare-number short ids.
|
|
30
|
+
- [`milestone-inference.md`](milestone-inference.md) — Milestone inference for create-task / code-task / create-pr.
|
|
31
|
+
- [`label-milestone-setup.md`](label-milestone-setup.md) — Platform commands to initialize labels / milestones.
|
|
32
|
+
- [`security-alerts.md`](security-alerts.md) — Commands to import / close Dependabot and Code Scanning alerts.
|
|
33
|
+
|
|
34
|
+
## Commit & Release
|
|
35
|
+
|
|
36
|
+
- [`commit-and-pr.md`](commit-and-pr.md) — Conventional Commits message and PR conventions.
|
|
37
|
+
- [`release-commands.md`](release-commands.md) — Read past releases, query merged PRs, publish release notes.
|
|
38
|
+
|
|
39
|
+
## Testing
|
|
40
|
+
|
|
41
|
+
- [`testing-discipline.md`](testing-discipline.md) — Test-writing discipline: prefer structural asserts, no brittle wording matches.
|
|
42
|
+
|
|
43
|
+
## CLI
|
|
44
|
+
|
|
45
|
+
- [`cli-help-format.md`](cli-help-format.md) — CLI help text conventions: unify display name on `ai`, `Usage:`+`Commands:` structure, alphabetical command order (top-level and namespace-level help only).
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# 规则索引
|
|
2
|
+
|
|
3
|
+
`.agents/rules/` 收录本项目所有协作规则。各 SKILL 执行时按需加载其中若干篇;
|
|
4
|
+
本索引按业务域列出全部规则及其用途,便于快速定位「该读哪几篇」,无需逐文件翻阅。
|
|
5
|
+
|
|
6
|
+
> 维护提醒:新增或删除 `.agents/rules/*.md` 时,请同步更新本索引。
|
|
7
|
+
|
|
8
|
+
## 通用准则
|
|
9
|
+
|
|
10
|
+
- [`no-mid-flow-questions.md`](no-mid-flow-questions.md) — SKILL 执行期禁言:默认不向用户提问,及规则列明的例外。
|
|
11
|
+
- [`next-step-output.md`](next-step-output.md) — 「下一步」输出规则:任务短号渲染与 `Completed at` 收尾行。
|
|
12
|
+
- [`version-stamp.md`](version-stamp.md) — `agent_infra_version` 版本戳的取值命令与写入时机。
|
|
13
|
+
- [`debugging-guide.md`](debugging-guide.md) — 结构化调试流程:收集证据→形成假设→验证假设→修复根因,禁止盲目改代码重试。
|
|
14
|
+
|
|
15
|
+
## Issue / PR
|
|
16
|
+
|
|
17
|
+
- [`issue-pr-commands.md`](issue-pr-commands.md) — 验证平台认证、读写 Issue / PR 的 GitHub 命令集。
|
|
18
|
+
- [`pr-checks-commands.md`](pr-checks-commands.md) — 监控 PR required checks、拉取失败日志的命令集(`watch-pr`)。
|
|
19
|
+
- [`create-issue.md`](create-issue.md) — `create-task` 落盘后级联创建 Issue 的规则。
|
|
20
|
+
- [`issue-sync.md`](issue-sync.md) — task 产物与 Issue 评论 / 标签 / 字段的同步标记与流程。
|
|
21
|
+
- [`issue-fields.md`](issue-fields.md) — Issue Type pinned 字段(Priority/Effort/日期)的读写流程。
|
|
22
|
+
- [`pr-sync.md`](pr-sync.md) — 面向 reviewer 的唯一 PR 摘要评论的同步规则。
|
|
23
|
+
|
|
24
|
+
## 任务工作流
|
|
25
|
+
|
|
26
|
+
- [`task-management.md`](task-management.md) — 任务语义识别与工作流命令映射。
|
|
27
|
+
- [`review-handshake.md`](review-handshake.md) — 三阶段双向审查握手协议:四态处置、对称证据、分歧账本、收敛与 post-review commit 门禁。
|
|
28
|
+
- [`task-short-id.md`](task-short-id.md) — 任务短号 `#NN` / 裸数字的解析、分配与生命周期。
|
|
29
|
+
- [`milestone-inference.md`](milestone-inference.md) — create-task / code-task / create-pr 的 milestone 推断。
|
|
30
|
+
- [`label-milestone-setup.md`](label-milestone-setup.md) — 初始化 label / milestone 的平台命令集。
|
|
31
|
+
- [`security-alerts.md`](security-alerts.md) — 导入 / 关闭 Dependabot 与 Code Scanning 告警的命令集。
|
|
32
|
+
|
|
33
|
+
## 提交与发布
|
|
34
|
+
|
|
35
|
+
- [`commit-and-pr.md`](commit-and-pr.md) — Conventional Commits 提交信息与 PR 规范。
|
|
36
|
+
- [`release-commands.md`](release-commands.md) — 读取历史 release、查询已合并 PR、发布 Release notes。
|
|
37
|
+
|
|
38
|
+
## 测试
|
|
39
|
+
|
|
40
|
+
- [`testing-discipline.md`](testing-discipline.md) — 测试编写纪律:结构性断言优先,禁止脆弱的措辞匹配。
|
|
41
|
+
|
|
42
|
+
## CLI
|
|
43
|
+
|
|
44
|
+
- [`cli-help-format.md`](cli-help-format.md) — CLI help 文案约定:展示名统一 `ai`、`Usage:`+`Commands:` 结构、命令按字母序(仅顶层与命名空间级 help)。
|