@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.
Files changed (107) hide show
  1. package/README.md +32 -790
  2. package/README.zh-CN.md +32 -763
  3. package/bin/cli.ts +13 -11
  4. package/dist/bin/cli.js +13 -11
  5. package/dist/lib/init.js +1 -1
  6. package/dist/lib/merge.js +1 -1
  7. package/dist/lib/sandbox/commands/create.js +44 -3
  8. package/dist/lib/sandbox/commands/rm.js +99 -19
  9. package/dist/lib/sandbox/index.js +24 -22
  10. package/dist/lib/sandbox/readme-scaffold.js +6 -6
  11. package/dist/lib/task/artifacts.js +58 -0
  12. package/dist/lib/task/commands/cat.js +38 -0
  13. package/dist/lib/task/commands/files.js +47 -0
  14. package/dist/lib/task/commands/grep.js +143 -0
  15. package/dist/lib/task/commands/log.js +75 -0
  16. package/dist/lib/task/commands/show.js +5 -114
  17. package/dist/lib/task/commands/status.js +239 -0
  18. package/dist/lib/task/index.js +37 -0
  19. package/dist/lib/task/resolve-ref.js +150 -0
  20. package/dist/lib/update.js +1 -1
  21. package/lib/init.ts +1 -1
  22. package/lib/merge.ts +1 -1
  23. package/lib/sandbox/commands/create.ts +47 -4
  24. package/lib/sandbox/commands/rm.ts +128 -19
  25. package/lib/sandbox/index.ts +24 -22
  26. package/lib/sandbox/readme-scaffold.ts +6 -6
  27. package/lib/task/artifacts.ts +72 -0
  28. package/lib/task/commands/cat.ts +39 -0
  29. package/lib/task/commands/files.ts +53 -0
  30. package/lib/task/commands/grep.ts +147 -0
  31. package/lib/task/commands/log.ts +80 -0
  32. package/lib/task/commands/show.ts +5 -117
  33. package/lib/task/commands/status.ts +302 -0
  34. package/lib/task/index.ts +37 -0
  35. package/lib/task/resolve-ref.ts +160 -0
  36. package/lib/update.ts +1 -1
  37. package/package.json +1 -1
  38. package/templates/.agents/README.en.md +1 -0
  39. package/templates/.agents/README.zh-CN.md +1 -0
  40. package/templates/.agents/rules/README.en.md +45 -0
  41. package/templates/.agents/rules/README.zh-CN.md +44 -0
  42. package/templates/.agents/rules/cli-help-format.en.md +49 -0
  43. package/templates/.agents/rules/cli-help-format.zh-CN.md +49 -0
  44. package/templates/.agents/rules/debugging-guide.en.md +25 -0
  45. package/templates/.agents/rules/debugging-guide.zh-CN.md +25 -0
  46. package/templates/.agents/rules/no-mid-flow-questions.en.md +14 -2
  47. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +14 -2
  48. package/templates/.agents/rules/pr-sync.github.en.md +8 -6
  49. package/templates/.agents/rules/pr-sync.github.zh-CN.md +8 -6
  50. package/templates/.agents/rules/review-handshake.en.md +83 -0
  51. package/templates/.agents/rules/review-handshake.zh-CN.md +83 -0
  52. package/templates/.agents/scripts/lib/post-review-commit.js +56 -0
  53. package/templates/.agents/scripts/lib/review-artifacts.js +117 -0
  54. package/templates/.agents/scripts/review-diff-fingerprint.js +99 -0
  55. package/templates/.agents/scripts/validate-artifact.js +240 -0
  56. package/templates/.agents/skills/analyze-task/SKILL.en.md +52 -6
  57. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +52 -6
  58. package/templates/.agents/skills/code-task/SKILL.en.md +2 -0
  59. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -0
  60. package/templates/.agents/skills/code-task/config/verify.en.json +3 -0
  61. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +3 -0
  62. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +5 -3
  63. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +5 -3
  64. package/templates/.agents/skills/code-task/reference/report-template.en.md +4 -4
  65. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +4 -4
  66. package/templates/.agents/skills/code-task/scripts/detect-mode.js +2 -107
  67. package/templates/.agents/skills/commit/SKILL.en.md +6 -0
  68. package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -0
  69. package/templates/.agents/skills/commit/reference/task-status-update.en.md +8 -0
  70. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +8 -0
  71. package/templates/.agents/skills/complete-task/SKILL.en.md +10 -0
  72. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +10 -0
  73. package/templates/.agents/skills/complete-task/config/verify.en.json +2 -0
  74. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -0
  75. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +1 -1
  76. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +1 -1
  77. package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
  78. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -1
  79. package/templates/.agents/skills/plan-task/config/verify.en.json +3 -0
  80. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +3 -0
  81. package/templates/.agents/skills/review-analysis/config/verify.en.json +2 -1
  82. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +2 -1
  83. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +5 -4
  84. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +5 -4
  85. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +4 -0
  86. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +4 -0
  87. package/templates/.agents/skills/review-code/SKILL.en.md +4 -1
  88. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -1
  89. package/templates/.agents/skills/review-code/config/verify.en.json +5 -2
  90. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +5 -2
  91. package/templates/.agents/skills/review-code/reference/output-templates.en.md +5 -4
  92. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +5 -4
  93. package/templates/.agents/skills/review-code/reference/report-template.en.md +6 -0
  94. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +6 -0
  95. package/templates/.agents/skills/review-plan/config/verify.en.json +2 -1
  96. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +2 -1
  97. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +5 -4
  98. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +5 -4
  99. package/templates/.agents/skills/review-plan/reference/report-template.en.md +4 -0
  100. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +4 -0
  101. package/templates/.agents/skills/watch-pr/SKILL.en.md +1 -1
  102. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +1 -1
  103. package/templates/.agents/templates/task.en.md +7 -0
  104. package/templates/.agents/templates/task.zh-CN.md +7 -0
  105. package/templates/.github/workflows/metadata-sync.yml +1 -1
  106. package/templates/.github/workflows/pr-label.yml +1 -1
  107. package/templates/.github/workflows/status-label.yml +1 -1
@@ -0,0 +1,72 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ type Artifact = {
5
+ index: number;
6
+ name: string;
7
+ path: string;
8
+ size: number;
9
+ mtimeMs: number;
10
+ };
11
+
12
+ /**
13
+ * Enumerate a task directory's artifacts ordered by modification time, oldest
14
+ * first, so the listing reads like the task's timeline. Filename ascending is a
15
+ * deterministic tiebreak when two files share the same mtime (e.g. written in
16
+ * the same millisecond).
17
+ *
18
+ * Only top-level regular files are included; subdirectories and dotfiles are
19
+ * skipped so every entry is something `cat` can print. The returned 1-based
20
+ * `index` is the source of truth shared by `files` and `cat`.
21
+ */
22
+ function enumerateArtifacts(taskDir: string): Artifact[] {
23
+ const entries = fs
24
+ .readdirSync(taskDir, { withFileTypes: true })
25
+ .filter((dirent) => dirent.isFile() && !dirent.name.startsWith('.'))
26
+ .map((dirent) => {
27
+ const abs = path.join(taskDir, dirent.name);
28
+ const stat = fs.statSync(abs);
29
+ return { name: dirent.name, path: abs, size: stat.size, mtimeMs: stat.mtimeMs };
30
+ });
31
+
32
+ entries.sort((a, b) => {
33
+ if (a.mtimeMs !== b.mtimeMs) return a.mtimeMs - b.mtimeMs;
34
+ return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
35
+ });
36
+
37
+ return entries.map((entry, i) => ({ index: i + 1, ...entry }));
38
+ }
39
+
40
+ /**
41
+ * Resolve an artifact selector to an absolute path within `taskDir`. The
42
+ * selector is either a 1-based index `N` (as listed by `files`) or a filename
43
+ * (with or without the `.md` suffix). Throws with a clear message on failure.
44
+ */
45
+ function resolveArtifact(taskDir: string, artifactOrN: string): string {
46
+ if (path.basename(artifactOrN) !== artifactOrN) {
47
+ throw new Error('artifact name must not contain path separators');
48
+ }
49
+
50
+ if (/^\d+$/.test(artifactOrN)) {
51
+ const n = Number(artifactOrN);
52
+ const match = enumerateArtifacts(taskDir).find((a) => a.index === n);
53
+ if (!match) {
54
+ throw new Error(`invalid artifact index ${n} (run 'ai task files <ref>' to list)`);
55
+ }
56
+ return match.path;
57
+ }
58
+
59
+ const candidates = artifactOrN.endsWith('.md')
60
+ ? [artifactOrN]
61
+ : [artifactOrN, `${artifactOrN}.md`];
62
+ for (const candidate of candidates) {
63
+ const abs = path.join(taskDir, candidate);
64
+ if (fs.existsSync(abs) && fs.statSync(abs).isFile()) {
65
+ return abs;
66
+ }
67
+ }
68
+ throw new Error(`artifact '${artifactOrN}' not found in task directory`);
69
+ }
70
+
71
+ export { enumerateArtifacts, resolveArtifact };
72
+ export type { Artifact };
@@ -0,0 +1,39 @@
1
+ import fs from 'node:fs';
2
+ import { resolveTaskRef } from '../resolve-ref.ts';
3
+ import { resolveArtifact } from '../artifacts.ts';
4
+
5
+ const USAGE = `Usage: ai task cat <N | #N | TASK-id> <artifact | N>
6
+
7
+ Prints a task artifact's raw content to stdout.
8
+ <ref> Bare numeric / '#N' short id, or a full TASK-YYYYMMDD-HHMMSS id.
9
+ <artifact | N> Artifact filename (with or without '.md'), or the number from 'ai task files'.
10
+ `;
11
+
12
+ function cat(args: string[] = []): void {
13
+ if (args[0] === '--help' || args[0] === '-h') {
14
+ process.stdout.write(USAGE);
15
+ return;
16
+ }
17
+ if (args.length < 2) {
18
+ process.stdout.write(USAGE);
19
+ process.exitCode = 1;
20
+ return;
21
+ }
22
+ const resolved = resolveTaskRef(args[0]!);
23
+ if (!resolved.ok) {
24
+ process.stderr.write(`ai task cat: ${resolved.message}\n`);
25
+ process.exitCode = 1;
26
+ return;
27
+ }
28
+ let artifactPath: string;
29
+ try {
30
+ artifactPath = resolveArtifact(resolved.taskDir, args[1]!);
31
+ } catch (e) {
32
+ process.stderr.write(`ai task cat: ${(e as Error).message}\n`);
33
+ process.exitCode = 1;
34
+ return;
35
+ }
36
+ process.stdout.write(fs.readFileSync(artifactPath, 'utf8'));
37
+ }
38
+
39
+ export { cat };
@@ -0,0 +1,53 @@
1
+ import { formatTable } from '../../table.ts';
2
+ import { resolveTaskRef } from '../resolve-ref.ts';
3
+ import { enumerateArtifacts } from '../artifacts.ts';
4
+
5
+ const USAGE = `Usage: ai task files <N | #N | TASK-id>
6
+
7
+ Lists the artifacts in a task directory with stable numbers.
8
+ <ref> Bare numeric / '#N' short id, or a full TASK-YYYYMMDD-HHMMSS id.
9
+
10
+ Columns: # (artifact number, usable with 'ai task cat') / NAME / SIZE (bytes) / MTIME
11
+ `;
12
+
13
+ const TABLE_HEADERS = ['#', 'NAME', 'SIZE', 'MTIME'] as const;
14
+
15
+ function pad2(n: number): string {
16
+ return String(n).padStart(2, '0');
17
+ }
18
+
19
+ function formatMtime(mtimeMs: number): string {
20
+ const d = new Date(mtimeMs);
21
+ return (
22
+ `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ` +
23
+ `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`
24
+ );
25
+ }
26
+
27
+ function files(args: string[] = []): void {
28
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
29
+ process.stdout.write(USAGE);
30
+ if (args.length === 0) process.exitCode = 1;
31
+ return;
32
+ }
33
+ const resolved = resolveTaskRef(args[0]!);
34
+ if (!resolved.ok) {
35
+ process.stderr.write(`ai task files: ${resolved.message}\n`);
36
+ process.exitCode = 1;
37
+ return;
38
+ }
39
+ const artifacts = enumerateArtifacts(resolved.taskDir);
40
+ // Show the name without the `.md` suffix so the NAME column is exactly what
41
+ // `ai task cat <ref> <name>` accepts (the resolver re-adds `.md`).
42
+ const rows = artifacts.map((a) => [
43
+ String(a.index),
44
+ a.name.replace(/\.md$/, ''),
45
+ String(a.size),
46
+ formatMtime(a.mtimeMs)
47
+ ]);
48
+ for (const line of formatTable(TABLE_HEADERS, rows, { zebra: Boolean(process.stdout.isTTY) })) {
49
+ process.stdout.write(`${line}\n`);
50
+ }
51
+ }
52
+
53
+ export { files };
@@ -0,0 +1,147 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { resolveTaskRef, detectRepoRoot, enumerateTaskDirs } from '../resolve-ref.ts';
4
+ import { enumerateArtifacts, resolveArtifact } from '../artifacts.ts';
5
+ import { loadShortIdByTaskId } from '../short-id.ts';
6
+
7
+ const USAGE = `Usage: ai task grep <pattern> [ref] [artifact | N]
8
+
9
+ Literal (non-regex) line search across task artifacts.
10
+ <pattern> Literal substring to match (NOT a regex). Case-sensitive by default.
11
+ [ref] Bare numeric / '#N' short id, or a full TASK-YYYYMMDD-HHMMSS id.
12
+ Omit to scan every task under active / blocked / completed
13
+ (archive is skipped). With a ref, narrows to that single task
14
+ (a TASK-id ref can also resolve an archived task).
15
+ [artifact | N] Only valid with <ref>. Artifact filename (with or without '.md')
16
+ or the number from 'ai task files'. Narrows to a single artifact.
17
+
18
+ Options:
19
+ -i, --ignore-case Case-insensitive matching.
20
+ -- Treat the rest as positional (use for patterns starting with '-').
21
+
22
+ Output: '{taskId} [#short] {fileStem}:{line}: {matched-line}' (short id only for active tasks).
23
+ Exits 1 with no output when nothing matches.
24
+ `;
25
+
26
+ function makeMatcher(pattern: string, ignoreCase: boolean): (line: string) => boolean {
27
+ if (ignoreCase) {
28
+ const needle = pattern.toLowerCase();
29
+ return (line) => line.toLowerCase().includes(needle);
30
+ }
31
+ return (line) => line.includes(pattern);
32
+ }
33
+
34
+ // Split content into lines with grep-like semantics: a trailing newline does
35
+ // not yield a phantom final empty line, but genuine interior blank lines stay.
36
+ function splitLines(content: string): string[] {
37
+ const lines = content.split(/\r?\n/);
38
+ if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
39
+ return lines;
40
+ }
41
+
42
+ function scanArtifact(
43
+ taskId: string,
44
+ shortToken: string | undefined,
45
+ artifactPath: string,
46
+ matcher: (line: string) => boolean,
47
+ emit: (line: string) => void
48
+ ): number {
49
+ const content = fs.readFileSync(artifactPath, 'utf8');
50
+ const stem = path.basename(artifactPath).replace(/\.md$/, '');
51
+ const prefix = shortToken ? `${taskId} ${shortToken}` : taskId;
52
+ let count = 0;
53
+ splitLines(content).forEach((line, i) => {
54
+ if (matcher(line)) {
55
+ emit(`${prefix} ${stem}:${i + 1}: ${line}\n`);
56
+ count++;
57
+ }
58
+ });
59
+ return count;
60
+ }
61
+
62
+ function grep(args: string[] = []): void {
63
+ const positional: string[] = [];
64
+ let ignoreCase = false;
65
+ let optsEnded = false;
66
+ for (const a of args) {
67
+ if (!optsEnded && a === '--') { optsEnded = true; continue; }
68
+ if (!optsEnded && (a === '-h' || a === '--help')) {
69
+ process.stdout.write(USAGE);
70
+ return;
71
+ }
72
+ if (!optsEnded && (a === '-i' || a === '--ignore-case')) { ignoreCase = true; continue; }
73
+ if (!optsEnded && a.startsWith('-') && a !== '-') {
74
+ process.stderr.write(`ai task grep: unknown flag: ${a}\n`);
75
+ process.exitCode = 1;
76
+ return;
77
+ }
78
+ positional.push(a);
79
+ }
80
+
81
+ if (positional.length === 0) {
82
+ process.stdout.write(USAGE);
83
+ process.exitCode = 1;
84
+ return;
85
+ }
86
+ if (positional.length > 3) {
87
+ process.stderr.write('ai task grep: too many arguments\n');
88
+ process.exitCode = 1;
89
+ return;
90
+ }
91
+
92
+ const [pattern, ref, artifactOrN] = positional;
93
+ const matcher = makeMatcher(pattern!, ignoreCase);
94
+ const chunks: string[] = [];
95
+ const emit = (line: string) => chunks.push(line);
96
+ let total = 0;
97
+
98
+ if (ref === undefined) {
99
+ // No ref: full scan across active / blocked / completed (no archive).
100
+ let repoRoot: string;
101
+ try {
102
+ repoRoot = detectRepoRoot();
103
+ } catch (e) {
104
+ process.stderr.write(`ai task grep: ${(e as Error).message}\n`);
105
+ process.exitCode = 1;
106
+ return;
107
+ }
108
+ const shortMap = loadShortIdByTaskId(repoRoot);
109
+ for (const { taskId, taskDir } of enumerateTaskDirs(repoRoot)) {
110
+ const shortToken = shortMap.get(taskId);
111
+ for (const a of enumerateArtifacts(taskDir)) {
112
+ total += scanArtifact(taskId, shortToken, a.path, matcher, emit);
113
+ }
114
+ }
115
+ } else {
116
+ const resolved = resolveTaskRef(ref);
117
+ if (!resolved.ok) {
118
+ process.stderr.write(`ai task grep: ${resolved.message}\n`);
119
+ process.exitCode = 1;
120
+ return;
121
+ }
122
+ const shortToken = loadShortIdByTaskId(resolved.repoRoot).get(resolved.taskId);
123
+ if (artifactOrN !== undefined) {
124
+ let artifactPath: string;
125
+ try {
126
+ artifactPath = resolveArtifact(resolved.taskDir, artifactOrN);
127
+ } catch (e) {
128
+ process.stderr.write(`ai task grep: ${(e as Error).message}\n`);
129
+ process.exitCode = 1;
130
+ return;
131
+ }
132
+ total += scanArtifact(resolved.taskId, shortToken, artifactPath, matcher, emit);
133
+ } else {
134
+ for (const a of enumerateArtifacts(resolved.taskDir)) {
135
+ total += scanArtifact(resolved.taskId, shortToken, a.path, matcher, emit);
136
+ }
137
+ }
138
+ }
139
+
140
+ if (total === 0) {
141
+ process.exitCode = 1;
142
+ return;
143
+ }
144
+ process.stdout.write(chunks.join(''));
145
+ }
146
+
147
+ export { grep };
@@ -0,0 +1,80 @@
1
+ import fs from 'node:fs';
2
+ import { formatTable } from '../../table.ts';
3
+ import { resolveTaskRef } from '../resolve-ref.ts';
4
+
5
+ const USAGE = `Usage: ai task log <N | #N | TASK-id>
6
+
7
+ Renders a task's activity log as a chronological timeline table.
8
+ <ref> Bare numeric / '#N' short id, or a full TASK-YYYYMMDD-HHMMSS id.
9
+
10
+ Columns: # (timeline position) / TIME / STEP / AGENT / NOTE
11
+ `;
12
+
13
+ const TABLE_HEADERS = ['#', 'TIME', 'STEP', 'AGENT', 'NOTE'] as const;
14
+
15
+ // The activity-log H2 heading is language-dependent (zh template / en template).
16
+ const HEADING_RE = /^##\s+(活动日志|Activity Log)\s*$/;
17
+ const NEXT_H2_RE = /^##\s/;
18
+ // `- {time} — **{step}** by {agent} — {note}` ; the separator is an em-dash
19
+ // (U+2014). STEP/AGENT are non-greedy so a note that itself contains ' — ' or
20
+ // '→' is not mis-split; NOTE greedily takes the rest of the line.
21
+ const ENTRY_RE =
22
+ /^- (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}) — \*\*(.+?)\*\* by (.+?) — (.*)$/;
23
+
24
+ type LogEntry = { time: string; step: string; agent: string; note: string };
25
+
26
+ function parseActivityLog(content: string): { sectionFound: boolean; entries: LogEntry[] } {
27
+ const lines = content.split('\n');
28
+ let i = 0;
29
+ while (i < lines.length && !HEADING_RE.test(lines[i]!)) i += 1;
30
+ if (i >= lines.length) return { sectionFound: false, entries: [] };
31
+ const parsed: { entry: LogEntry; epoch: number; order: number }[] = [];
32
+ for (let j = i + 1; j < lines.length; j += 1) {
33
+ if (NEXT_H2_RE.test(lines[j]!)) break;
34
+ const m = ENTRY_RE.exec(lines[j]!);
35
+ if (!m) continue; // skip blank / non-entry / malformed lines
36
+ parsed.push({
37
+ entry: { time: m[1]!, step: m[2]!, agent: m[3]!, note: m[4]! },
38
+ epoch: Date.parse(m[1]!.replace(' ', 'T')),
39
+ order: parsed.length
40
+ });
41
+ }
42
+ // Ascending by time; stable tie-break on original order for equal timestamps.
43
+ parsed.sort((a, b) => a.epoch - b.epoch || a.order - b.order);
44
+ return { sectionFound: true, entries: parsed.map((p) => p.entry) };
45
+ }
46
+
47
+ function log(args: string[] = []): void {
48
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
49
+ process.stdout.write(USAGE);
50
+ if (args.length === 0) process.exitCode = 1;
51
+ return;
52
+ }
53
+ const resolved = resolveTaskRef(args[0]!);
54
+ if (!resolved.ok) {
55
+ process.stderr.write(`ai task log: ${resolved.message}\n`);
56
+ process.exitCode = 1;
57
+ return;
58
+ }
59
+ const content = fs.readFileSync(resolved.taskMdPath, 'utf8');
60
+ const { sectionFound, entries } = parseActivityLog(content);
61
+ if (!sectionFound) {
62
+ process.stderr.write(
63
+ `ai task log: no activity log section ('## 活动日志' or '## Activity Log') found in task ${resolved.taskId}\n`
64
+ );
65
+ process.exitCode = 1;
66
+ return;
67
+ }
68
+ if (entries.length === 0) {
69
+ process.stderr.write(`ai task log: no activity log entries found in task ${resolved.taskId}\n`);
70
+ process.exitCode = 1;
71
+ return;
72
+ }
73
+ const rows = entries.map((e, idx) => [String(idx + 1), e.time, e.step, e.agent, e.note]);
74
+ for (const line of formatTable(TABLE_HEADERS, rows, { zebra: Boolean(process.stdout.isTTY) })) {
75
+ process.stdout.write(`${line}\n`);
76
+ }
77
+ process.stdout.write(`Total: ${entries.length} entries\n`);
78
+ }
79
+
80
+ export { log, parseActivityLog };
@@ -1,7 +1,5 @@
1
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';
2
+ import { resolveTaskRef } from '../resolve-ref.ts';
5
3
 
6
4
  const USAGE = `Usage: ai task show <N | #N | TASK-id>
7
5
 
@@ -11,129 +9,19 @@ Prints the task.md content for the matching task.
11
9
  TASK-YYYYMMDD-HHMMSS Locates a task in active / blocked / completed / archive.
12
10
  `;
13
11
 
14
- const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
15
- // Flat-structured workspace dirs that hold tasks under `{dir}/{taskId}/task.md`.
16
- // Note: `archive` uses a three-level YYYY/MM/DD layout and is handled separately.
17
- const FLAT_WORKSPACE_DIRS = ['active', 'blocked', 'completed'] as const;
18
-
19
- function detectRepoRoot(): string {
20
- try {
21
- return execFileSync('git', ['rev-parse', '--show-toplevel'], {
22
- encoding: 'utf8',
23
- stdio: ['pipe', 'pipe', 'pipe']
24
- }).trim();
25
- } catch {
26
- throw new Error('ai task: current directory is not inside a git repository');
27
- }
28
- }
29
-
30
- function readShortIdLength(repoRoot: string): number {
31
- try {
32
- const cfg = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8'));
33
- const v = cfg?.task?.shortIdLength;
34
- if (typeof v === 'number' && Number.isFinite(v) && v >= 1) return v;
35
- } catch {
36
- // fall through to default
37
- }
38
- return 2;
39
- }
40
-
41
- function resolveShortIdToTaskId(arg: string, repoRoot: string): string {
42
- const scriptPath = path.join(repoRoot, '.agents', 'scripts', 'task-short-id.js');
43
- if (!fs.existsSync(scriptPath)) {
44
- throw new Error(`task-short-id.js not found at ${scriptPath}`);
45
- }
46
- const result = spawnSync('node', [scriptPath, 'resolve', arg], {
47
- encoding: 'utf8',
48
- cwd: repoRoot
49
- });
50
- if (result.status !== 0) {
51
- throw new Error((result.stderr || '').trim() || `failed to resolve '${arg}'`);
52
- }
53
- return result.stdout.trim();
54
- }
55
-
56
- function listSortedNumeric(dir: string, width: number): string[] {
57
- if (!fs.existsSync(dir)) return [];
58
- const pattern = new RegExp(`^\\d{${width}}$`);
59
- return fs
60
- .readdirSync(dir)
61
- .filter((entry) => pattern.test(entry))
62
- .sort()
63
- .reverse();
64
- }
65
-
66
- function findInArchive(repoRoot: string, taskId: string): string | null {
67
- // archive-tasks SKILL writes to .agents/workspace/archive/YYYY/MM/DD/{taskId}/task.md
68
- // where YYYY/MM/DD comes from completed_at (or updated_at fallback) — NOT from
69
- // the task id's creation date. So we cannot derive the path from taskId alone;
70
- // walk the bounded YYYY/MM/DD tree instead. Newest-first to favor recent archives.
71
- const archiveDir = path.join(repoRoot, '.agents', 'workspace', 'archive');
72
- for (const year of listSortedNumeric(archiveDir, 4)) {
73
- const yearDir = path.join(archiveDir, year);
74
- for (const month of listSortedNumeric(yearDir, 2)) {
75
- const monthDir = path.join(yearDir, month);
76
- for (const day of listSortedNumeric(monthDir, 2)) {
77
- const candidate = path.join(monthDir, day, taskId, 'task.md');
78
- if (fs.existsSync(candidate)) return candidate;
79
- }
80
- }
81
- }
82
- return null;
83
- }
84
-
85
- function findTaskMd(repoRoot: string, taskId: string): string | null {
86
- for (const sub of FLAT_WORKSPACE_DIRS) {
87
- const candidate = path.join(repoRoot, '.agents', 'workspace', sub, taskId, 'task.md');
88
- if (fs.existsSync(candidate)) return candidate;
89
- }
90
- return findInArchive(repoRoot, taskId);
91
- }
92
-
93
12
  function show(args: string[] = []): void {
94
13
  if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
95
14
  process.stdout.write(USAGE);
96
15
  if (args.length === 0) process.exitCode = 1;
97
16
  return;
98
17
  }
99
- const repoRoot = detectRepoRoot();
100
- const arg = args[0]!;
101
- let taskId: string;
102
- if (TASK_ID_RE.test(arg)) {
103
- taskId = arg;
104
- } else {
105
- const shortIdLength = readShortIdLength(repoRoot);
106
- const normalized = normalizeShortIdInput(arg, { shortIdLength });
107
- if (normalized.kind === 'error') {
108
- process.stderr.write(`ai task show: ${normalized.message}\n`);
109
- process.exitCode = 1;
110
- return;
111
- }
112
- if (normalized.kind === 'pass') {
113
- process.stderr.write(
114
- `ai task show: '${arg}' is not a valid short id or TASK-id; ` +
115
- `expected bare digits, '#N', or 'TASK-YYYYMMDD-HHMMSS'\n`
116
- );
117
- process.exitCode = 1;
118
- return;
119
- }
120
- try {
121
- taskId = resolveShortIdToTaskId(normalized.value, repoRoot);
122
- } catch (e) {
123
- process.stderr.write(`ai task show: ${(e as Error).message}\n`);
124
- process.exitCode = 1;
125
- return;
126
- }
127
- }
128
- const taskMdPath = findTaskMd(repoRoot, taskId);
129
- if (!taskMdPath) {
130
- process.stderr.write(
131
- `ai task show: task ${taskId} not found in active / blocked / completed / archive\n`
132
- );
18
+ const resolved = resolveTaskRef(args[0]!);
19
+ if (!resolved.ok) {
20
+ process.stderr.write(`ai task show: ${resolved.message}\n`);
133
21
  process.exitCode = 1;
134
22
  return;
135
23
  }
136
- process.stdout.write(fs.readFileSync(taskMdPath, 'utf8'));
24
+ process.stdout.write(fs.readFileSync(resolved.taskMdPath, 'utf8'));
137
25
  }
138
26
 
139
27
  export { show };