@fitlab-ai/agent-infra 0.7.2 → 0.7.4

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 (126) hide show
  1. package/README.md +35 -787
  2. package/README.zh-CN.md +37 -762
  3. package/bin/cli.ts +1 -1
  4. package/dist/bin/cli.js +1 -1
  5. package/dist/lib/defaults.json +0 -1
  6. package/dist/lib/init.js +0 -3
  7. package/dist/lib/sandbox/commands/create.js +44 -3
  8. package/dist/lib/sandbox/commands/enter.js +13 -15
  9. package/dist/lib/sandbox/commands/list-running.js +36 -1
  10. package/dist/lib/sandbox/commands/ls.js +9 -4
  11. package/dist/lib/sandbox/commands/rm.js +99 -19
  12. package/dist/lib/sandbox/commands/start.js +36 -0
  13. package/dist/lib/sandbox/index.js +11 -1
  14. package/dist/lib/sandbox/readme-scaffold.js +6 -6
  15. package/dist/lib/table.js +11 -2
  16. package/dist/lib/task/artifacts.js +58 -0
  17. package/dist/lib/task/commands/cat.js +38 -0
  18. package/dist/lib/task/commands/files.js +47 -0
  19. package/dist/lib/task/commands/grep.js +143 -0
  20. package/dist/lib/task/commands/log.js +75 -0
  21. package/dist/lib/task/commands/ls.js +1 -1
  22. package/dist/lib/task/commands/show.js +5 -114
  23. package/dist/lib/task/commands/status.js +239 -0
  24. package/dist/lib/task/index.js +37 -0
  25. package/dist/lib/task/resolve-ref.js +150 -0
  26. package/dist/lib/task/short-id.js +10 -0
  27. package/dist/lib/update.js +25 -8
  28. package/lib/defaults.json +0 -1
  29. package/lib/init.ts +0 -10
  30. package/lib/sandbox/commands/create.ts +47 -4
  31. package/lib/sandbox/commands/enter.ts +33 -14
  32. package/lib/sandbox/commands/list-running.ts +43 -1
  33. package/lib/sandbox/commands/ls.ts +12 -4
  34. package/lib/sandbox/commands/rm.ts +128 -19
  35. package/lib/sandbox/commands/start.ts +61 -0
  36. package/lib/sandbox/index.ts +11 -1
  37. package/lib/sandbox/readme-scaffold.ts +6 -6
  38. package/lib/table.ts +14 -2
  39. package/lib/task/artifacts.ts +72 -0
  40. package/lib/task/commands/cat.ts +39 -0
  41. package/lib/task/commands/files.ts +53 -0
  42. package/lib/task/commands/grep.ts +147 -0
  43. package/lib/task/commands/log.ts +80 -0
  44. package/lib/task/commands/ls.ts +1 -1
  45. package/lib/task/commands/show.ts +5 -117
  46. package/lib/task/commands/status.ts +302 -0
  47. package/lib/task/index.ts +37 -0
  48. package/lib/task/resolve-ref.ts +160 -0
  49. package/lib/task/short-id.ts +10 -0
  50. package/lib/update.ts +28 -10
  51. package/package.json +1 -1
  52. package/templates/.agents/README.en.md +1 -0
  53. package/templates/.agents/README.zh-CN.md +1 -0
  54. package/templates/.agents/hooks/auto-resume.sh +21 -4
  55. package/templates/.agents/rules/README.en.md +41 -0
  56. package/templates/.agents/rules/README.zh-CN.md +40 -0
  57. package/templates/.agents/rules/debugging-guide.en.md +25 -0
  58. package/templates/.agents/rules/debugging-guide.zh-CN.md +25 -0
  59. package/templates/.agents/rules/next-step-output.en.md +6 -3
  60. package/templates/.agents/rules/next-step-output.zh-CN.md +6 -3
  61. package/templates/.agents/rules/pr-checks-commands.en.md +5 -0
  62. package/templates/.agents/rules/pr-checks-commands.github.en.md +62 -0
  63. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +62 -0
  64. package/templates/.agents/rules/pr-checks-commands.zh-CN.md +5 -0
  65. package/templates/.agents/rules/pr-sync.github.en.md +7 -0
  66. package/templates/.agents/rules/pr-sync.github.zh-CN.md +7 -0
  67. package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
  68. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -1
  69. package/templates/.agents/skills/block-task/SKILL.en.md +8 -1
  70. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +8 -1
  71. package/templates/.agents/skills/cancel-task/SKILL.en.md +8 -1
  72. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +8 -1
  73. package/templates/.agents/skills/check-task/SKILL.en.md +1 -1
  74. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +1 -1
  75. package/templates/.agents/skills/close-codescan/SKILL.en.md +8 -1
  76. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +8 -1
  77. package/templates/.agents/skills/close-dependabot/SKILL.en.md +8 -1
  78. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +8 -1
  79. package/templates/.agents/skills/code-task/SKILL.en.md +3 -1
  80. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -1
  81. package/templates/.agents/skills/commit/SKILL.en.md +2 -3
  82. package/templates/.agents/skills/commit/SKILL.zh-CN.md +2 -3
  83. package/templates/.agents/skills/commit/reference/task-status-update.en.md +31 -23
  84. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +31 -23
  85. package/templates/.agents/skills/complete-task/SKILL.en.md +36 -3
  86. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +36 -3
  87. package/templates/.agents/skills/create-pr/SKILL.en.md +16 -7
  88. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +16 -7
  89. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +1 -0
  90. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +1 -0
  91. package/templates/.agents/skills/create-task/SKILL.en.md +1 -1
  92. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
  93. package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
  94. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  95. package/templates/.agents/skills/import-dependabot/SKILL.en.md +1 -1
  96. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
  97. package/templates/.agents/skills/import-issue/SKILL.en.md +1 -1
  98. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
  99. package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
  100. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -1
  101. package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
  102. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
  103. package/templates/.agents/skills/review-code/SKILL.en.md +1 -1
  104. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +1 -1
  105. package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
  106. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
  107. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +0 -1
  108. package/templates/.agents/skills/watch-pr/SKILL.en.md +131 -0
  109. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +131 -0
  110. package/templates/.agents/skills/watch-pr/config/verify.json +22 -0
  111. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +43 -0
  112. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +43 -0
  113. package/templates/.agents/templates/task.en.md +1 -0
  114. package/templates/.agents/templates/task.zh-CN.md +1 -0
  115. package/templates/.agents/workflows/bug-fix.en.yaml +6 -4
  116. package/templates/.agents/workflows/bug-fix.zh-CN.yaml +5 -4
  117. package/templates/.agents/workflows/feature-development.en.yaml +6 -4
  118. package/templates/.agents/workflows/feature-development.zh-CN.yaml +5 -4
  119. package/templates/.agents/workflows/refactoring.en.yaml +6 -4
  120. package/templates/.agents/workflows/refactoring.zh-CN.yaml +5 -4
  121. package/templates/.claude/commands/watch-pr.en.md +8 -0
  122. package/templates/.claude/commands/watch-pr.zh-CN.md +8 -0
  123. package/templates/.gemini/commands/_project_/watch-pr.en.toml +8 -0
  124. package/templates/.gemini/commands/_project_/watch-pr.zh-CN.toml +8 -0
  125. package/templates/.opencode/commands/watch-pr.en.md +11 -0
  126. package/templates/.opencode/commands/watch-pr.zh-CN.md +11 -0
@@ -0,0 +1,239 @@
1
+ import fs from 'node:fs';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { resolveTaskRef } from "../resolve-ref.js";
4
+ import { enumerateArtifacts } from "../artifacts.js";
5
+ import { parseTaskFrontmatter, extractTitle } from "../frontmatter.js";
6
+ import { loadShortIdByTaskId } from "../short-id.js";
7
+ const USAGE = `Usage: ai task status <N | #N | TASK-id>
8
+
9
+ Prints an aggregated "health check" view for a task: header, metadata, an
10
+ artifacts summary, git branch state, and best-effort GitHub issue/PR status.
11
+ <ref> Bare numeric / '#N' short id, or a full TASK-YYYYMMDD-HHMMSS id.
12
+
13
+ Git and Platform rows are best-effort: a failed git/gh call degrades that row to
14
+ '-' without failing the command.
15
+ `;
16
+ const DASH = '-';
17
+ function makeRunner(cwd) {
18
+ return (file, args) => execFileSync(file, args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
19
+ }
20
+ // Run `run` and swallow any failure into null, so a single failing git/gh call
21
+ // degrades only its own field instead of aborting the whole view.
22
+ function tryRun(run, file, args) {
23
+ try {
24
+ return run(file, args);
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ }
30
+ // Frontmatter keys shown in the Metadata section, in a fixed display order.
31
+ const METADATA_KEYS = [
32
+ 'type',
33
+ 'status',
34
+ 'current_step',
35
+ 'priority',
36
+ 'effort',
37
+ 'branch',
38
+ 'assigned_to',
39
+ 'created_at',
40
+ 'updated_at',
41
+ 'issue_number',
42
+ 'pr_status'
43
+ ];
44
+ function collectMetadata(fm) {
45
+ return METADATA_KEYS.map((key) => [key, fm[key] ? fm[key] : DASH]);
46
+ }
47
+ // Workflow stages in timeline order; artifacts are bucketed by filename prefix.
48
+ // `review-*` prefixes are matched before their bare counterparts so that, e.g.,
49
+ // `review-analysis.md` is never swallowed by the `analysis` bucket.
50
+ const STAGE_ORDER = [
51
+ 'analysis',
52
+ 'review-analysis',
53
+ 'plan',
54
+ 'review-plan',
55
+ 'code',
56
+ 'review-code',
57
+ 'task',
58
+ 'other'
59
+ ];
60
+ function stageOf(name) {
61
+ const stem = name.replace(/\.md$/, '');
62
+ if (stem === 'task')
63
+ return 'task';
64
+ if (stem.startsWith('review-analysis'))
65
+ return 'review-analysis';
66
+ if (stem.startsWith('review-plan'))
67
+ return 'review-plan';
68
+ if (stem.startsWith('review-code'))
69
+ return 'review-code';
70
+ if (stem.startsWith('analysis'))
71
+ return 'analysis';
72
+ if (stem.startsWith('plan'))
73
+ return 'plan';
74
+ if (stem.startsWith('code'))
75
+ return 'code';
76
+ return 'other';
77
+ }
78
+ // Group artifacts by workflow stage, preserving the input order (mtime ascending
79
+ // from enumerateArtifacts) within each stage and dropping empty stages.
80
+ function groupArtifacts(artifacts) {
81
+ const byStage = new Map();
82
+ for (const artifact of artifacts) {
83
+ const stage = stageOf(artifact.name);
84
+ const bucket = byStage.get(stage);
85
+ if (bucket)
86
+ bucket.push(artifact.name);
87
+ else
88
+ byStage.set(stage, [artifact.name]);
89
+ }
90
+ const groups = [];
91
+ for (const stage of STAGE_ORDER) {
92
+ const files = byStage.get(stage);
93
+ if (files && files.length > 0)
94
+ groups.push({ stage, files });
95
+ }
96
+ return groups;
97
+ }
98
+ // `frontmatterBranch` is the task.md `branch` field (caller passes '' when absent).
99
+ // It is read straight from frontmatter and never depends on a subprocess, so it
100
+ // keeps its value even when every git call fails. All other fields degrade to '-'
101
+ // on failure of their own command.
102
+ function collectGit(frontmatterBranch, run) {
103
+ const frontmatter = frontmatterBranch ? frontmatterBranch : DASH;
104
+ let current = DASH;
105
+ const cur = tryRun(run, 'git', ['rev-parse', '--abbrev-ref', 'HEAD']);
106
+ if (cur !== null && cur.trim())
107
+ current = cur.trim();
108
+ let match = DASH;
109
+ if (current !== DASH && frontmatter !== DASH) {
110
+ match = current === frontmatter ? 'yes' : 'no';
111
+ }
112
+ let exists = DASH;
113
+ if (frontmatter !== DASH) {
114
+ const verified = tryRun(run, 'git', ['rev-parse', '--verify', '--quiet', `refs/heads/${frontmatter}`]);
115
+ exists = verified === null ? 'no' : 'yes';
116
+ }
117
+ let uncommitted = DASH;
118
+ const porcelain = tryRun(run, 'git', ['status', '--porcelain']);
119
+ if (porcelain !== null) {
120
+ const changed = porcelain.split('\n').filter((line) => line.trim() !== '');
121
+ uncommitted = changed.length === 0 ? 'clean' : `${changed.length} file(s)`;
122
+ }
123
+ let aheadBehind = DASH;
124
+ if (frontmatter !== DASH) {
125
+ const counts = tryRun(run, 'git', [
126
+ 'rev-list',
127
+ '--left-right',
128
+ '--count',
129
+ `${frontmatter}...${frontmatter}@{upstream}`
130
+ ]);
131
+ if (counts !== null) {
132
+ const parts = counts.trim().split(/\s+/);
133
+ if (parts.length === 2)
134
+ aheadBehind = `${parts[0]} ahead / ${parts[1]} behind`;
135
+ }
136
+ }
137
+ return { current, frontmatter, match, exists, uncommitted, aheadBehind };
138
+ }
139
+ function collectPlatform(fm, run) {
140
+ let issue = DASH;
141
+ if (fm.issue_number && /^\d+$/.test(fm.issue_number)) {
142
+ const out = tryRun(run, 'gh', ['issue', 'view', fm.issue_number, '--json', 'state,labels']);
143
+ if (out !== null) {
144
+ try {
145
+ const data = JSON.parse(out);
146
+ const labels = Array.isArray(data.labels)
147
+ ? data.labels.map((label) => label.name).join(', ')
148
+ : '';
149
+ issue = labels ? `${data.state} [${labels}]` : `${data.state}`;
150
+ }
151
+ catch {
152
+ issue = DASH;
153
+ }
154
+ }
155
+ }
156
+ let pr = DASH;
157
+ if (fm.pr_status === 'created' && fm.pr_number && /^\d+$/.test(fm.pr_number)) {
158
+ const out = tryRun(run, 'gh', ['pr', 'view', fm.pr_number, '--json', 'state,statusCheckRollup']);
159
+ if (out !== null) {
160
+ try {
161
+ const data = JSON.parse(out);
162
+ const rollup = Array.isArray(data.statusCheckRollup) ? data.statusCheckRollup : [];
163
+ const passed = rollup.filter((check) => check.conclusion === 'SUCCESS' || check.state === 'SUCCESS').length;
164
+ pr = rollup.length > 0 ? `${data.state}, checks: ${passed}/${rollup.length}` : `${data.state}`;
165
+ }
166
+ catch {
167
+ pr = DASH;
168
+ }
169
+ }
170
+ }
171
+ return { issue, pr };
172
+ }
173
+ // Indent each label/value pair by two spaces and pad labels to a common width so
174
+ // every section reads as an aligned "key value" block.
175
+ function renderPairs(rows) {
176
+ const width = rows.reduce((max, [label]) => Math.max(max, label.length), 0);
177
+ return rows.map(([label, value]) => ` ${label.padEnd(width)} ${value}`.trimEnd());
178
+ }
179
+ function renderStatus(model) {
180
+ const lines = [];
181
+ lines.push(`Task ${model.taskId} (${model.shortId})`);
182
+ if (model.title)
183
+ lines.push(model.title);
184
+ lines.push('', 'Metadata', ...renderPairs(model.metadata));
185
+ lines.push('', `Artifacts (${model.artifacts.count})`);
186
+ if (model.artifacts.groups.length === 0) {
187
+ lines.push(' (none)');
188
+ }
189
+ else {
190
+ lines.push(...renderPairs(model.artifacts.groups.map((group) => [group.stage, group.files.join(', ')])));
191
+ }
192
+ lines.push('', 'Git', ...renderPairs([
193
+ ['current', model.git.current],
194
+ ['frontmatter', model.git.frontmatter],
195
+ ['match', model.git.match],
196
+ ['exists', model.git.exists],
197
+ ['uncommitted', model.git.uncommitted],
198
+ ['ahead/behind', model.git.aheadBehind]
199
+ ]));
200
+ const issueLabel = model.issueNumber ? `issue #${model.issueNumber}` : 'issue';
201
+ lines.push('', 'Platform', ...renderPairs([
202
+ [issueLabel, model.platform.issue],
203
+ ['pr', model.platform.pr]
204
+ ]));
205
+ return lines;
206
+ }
207
+ function status(args = []) {
208
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
209
+ process.stdout.write(USAGE);
210
+ if (args.length === 0)
211
+ process.exitCode = 1;
212
+ return;
213
+ }
214
+ const resolved = resolveTaskRef(args[0]);
215
+ if (!resolved.ok) {
216
+ process.stderr.write(`ai task status: ${resolved.message}\n`);
217
+ process.exitCode = 1;
218
+ return;
219
+ }
220
+ const content = fs.readFileSync(resolved.taskMdPath, 'utf8');
221
+ const fm = parseTaskFrontmatter(content);
222
+ const run = makeRunner(resolved.repoRoot);
223
+ const artifacts = enumerateArtifacts(resolved.taskDir);
224
+ const model = {
225
+ taskId: resolved.taskId,
226
+ shortId: loadShortIdByTaskId(resolved.repoRoot).get(resolved.taskId) ?? DASH,
227
+ title: extractTitle(content),
228
+ issueNumber: fm.issue_number && /^\d+$/.test(fm.issue_number) ? fm.issue_number : '',
229
+ metadata: collectMetadata(fm),
230
+ artifacts: { count: artifacts.length, groups: groupArtifacts(artifacts) },
231
+ git: collectGit(fm.branch ?? '', run),
232
+ platform: collectPlatform(fm, run)
233
+ };
234
+ for (const line of renderStatus(model)) {
235
+ process.stdout.write(`${line}\n`);
236
+ }
237
+ }
238
+ export { status, makeRunner, collectMetadata, groupArtifacts, collectGit, collectPlatform, renderStatus, METADATA_KEYS };
239
+ //# sourceMappingURL=status.js.map
@@ -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
  export async function runTask(args) {
@@ -32,6 +44,31 @@ export async function runTask(args) {
32
44
  show(rest);
33
45
  break;
34
46
  }
47
+ case 'files': {
48
+ const { files } = await import("./commands/files.js");
49
+ files(rest);
50
+ break;
51
+ }
52
+ case 'cat': {
53
+ const { cat } = await import("./commands/cat.js");
54
+ cat(rest);
55
+ break;
56
+ }
57
+ case 'grep': {
58
+ const { grep } = await import("./commands/grep.js");
59
+ grep(rest);
60
+ break;
61
+ }
62
+ case 'log': {
63
+ const { log } = await import("./commands/log.js");
64
+ log(rest);
65
+ break;
66
+ }
67
+ case 'status': {
68
+ const { status } = await import("./commands/status.js");
69
+ status(rest);
70
+ break;
71
+ }
35
72
  default:
36
73
  process.stderr.write(`Unknown task command: ${subcommand}\n\n`);
37
74
  process.stdout.write(`${USAGE}\n`);
@@ -0,0 +1,150 @@
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.js";
5
+ const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
6
+ // Flat-structured workspace dirs that hold tasks under `{dir}/{taskId}/task.md`.
7
+ // Note: `archive` uses a three-level YYYY/MM/DD layout and is handled separately.
8
+ const FLAT_WORKSPACE_DIRS = ['active', 'blocked', 'completed'];
9
+ function detectRepoRoot() {
10
+ try {
11
+ return execFileSync('git', ['rev-parse', '--show-toplevel'], {
12
+ encoding: 'utf8',
13
+ stdio: ['pipe', 'pipe', 'pipe']
14
+ }).trim();
15
+ }
16
+ catch {
17
+ throw new Error('ai task: current directory is not inside a git repository');
18
+ }
19
+ }
20
+ function readShortIdLength(repoRoot) {
21
+ try {
22
+ const cfg = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8'));
23
+ const v = cfg?.task?.shortIdLength;
24
+ if (typeof v === 'number' && Number.isFinite(v) && v >= 1)
25
+ return v;
26
+ }
27
+ catch {
28
+ // fall through to default
29
+ }
30
+ return 2;
31
+ }
32
+ function resolveShortIdToTaskId(arg, repoRoot) {
33
+ const scriptPath = path.join(repoRoot, '.agents', 'scripts', 'task-short-id.js');
34
+ if (!fs.existsSync(scriptPath)) {
35
+ throw new Error(`task-short-id.js not found at ${scriptPath}`);
36
+ }
37
+ const result = spawnSync('node', [scriptPath, 'resolve', arg], {
38
+ encoding: 'utf8',
39
+ cwd: repoRoot
40
+ });
41
+ if (result.status !== 0) {
42
+ throw new Error((result.stderr || '').trim() || `failed to resolve '${arg}'`);
43
+ }
44
+ return result.stdout.trim();
45
+ }
46
+ function listSortedNumeric(dir, width) {
47
+ if (!fs.existsSync(dir))
48
+ return [];
49
+ const pattern = new RegExp(`^\\d{${width}}$`);
50
+ return fs
51
+ .readdirSync(dir)
52
+ .filter((entry) => pattern.test(entry))
53
+ .sort()
54
+ .reverse();
55
+ }
56
+ function findInArchive(repoRoot, taskId) {
57
+ // archive-tasks SKILL writes to .agents/workspace/archive/YYYY/MM/DD/{taskId}/task.md
58
+ // where YYYY/MM/DD comes from completed_at (or updated_at fallback) — NOT from
59
+ // the task id's creation date. So we cannot derive the path from taskId alone;
60
+ // walk the bounded YYYY/MM/DD tree instead. Newest-first to favor recent archives.
61
+ const archiveDir = path.join(repoRoot, '.agents', 'workspace', 'archive');
62
+ for (const year of listSortedNumeric(archiveDir, 4)) {
63
+ const yearDir = path.join(archiveDir, year);
64
+ for (const month of listSortedNumeric(yearDir, 2)) {
65
+ const monthDir = path.join(yearDir, month);
66
+ for (const day of listSortedNumeric(monthDir, 2)) {
67
+ const candidate = path.join(monthDir, day, taskId, 'task.md');
68
+ if (fs.existsSync(candidate))
69
+ return candidate;
70
+ }
71
+ }
72
+ }
73
+ return null;
74
+ }
75
+ function findTaskMd(repoRoot, taskId) {
76
+ for (const sub of FLAT_WORKSPACE_DIRS) {
77
+ const candidate = path.join(repoRoot, '.agents', 'workspace', sub, taskId, 'task.md');
78
+ if (fs.existsSync(candidate))
79
+ return candidate;
80
+ }
81
+ return findInArchive(repoRoot, taskId);
82
+ }
83
+ /**
84
+ * Enumerate every task directory under the flat workspace states
85
+ * (active / blocked / completed) — archive is intentionally excluded so a
86
+ * full-tree scan never pulls in cold data. Ordered by state, then task id
87
+ * ascending, giving callers a deterministic traversal.
88
+ */
89
+ function enumerateTaskDirs(repoRoot) {
90
+ const out = [];
91
+ for (const sub of FLAT_WORKSPACE_DIRS) {
92
+ const base = path.join(repoRoot, '.agents', 'workspace', sub);
93
+ if (!fs.existsSync(base))
94
+ continue;
95
+ for (const entry of fs.readdirSync(base).sort()) {
96
+ if (!TASK_ID_RE.test(entry))
97
+ continue;
98
+ const taskDir = path.join(base, entry);
99
+ if (!fs.existsSync(path.join(taskDir, 'task.md')))
100
+ continue;
101
+ out.push({ taskId: entry, taskDir });
102
+ }
103
+ }
104
+ return out;
105
+ }
106
+ /**
107
+ * Resolve a task ref (bare short id, `#N`, or `TASK-YYYYMMDD-HHMMSS`) to its
108
+ * task directory across active / blocked / completed / archive.
109
+ *
110
+ * The returned `message` on failure is command-agnostic (no `ai task <cmd>:`
111
+ * prefix); callers prepend their own prefix so each command keeps its existing
112
+ * stderr wording byte-for-byte.
113
+ */
114
+ function resolveTaskRef(arg) {
115
+ const repoRoot = detectRepoRoot();
116
+ let taskId;
117
+ if (TASK_ID_RE.test(arg)) {
118
+ taskId = arg;
119
+ }
120
+ else {
121
+ const shortIdLength = readShortIdLength(repoRoot);
122
+ const normalized = normalizeShortIdInput(arg, { shortIdLength });
123
+ if (normalized.kind === 'error') {
124
+ return { ok: false, message: normalized.message };
125
+ }
126
+ if (normalized.kind === 'pass') {
127
+ return {
128
+ ok: false,
129
+ message: `'${arg}' is not a valid short id or TASK-id; ` +
130
+ `expected bare digits, '#N', or 'TASK-YYYYMMDD-HHMMSS'`
131
+ };
132
+ }
133
+ try {
134
+ taskId = resolveShortIdToTaskId(normalized.value, repoRoot);
135
+ }
136
+ catch (e) {
137
+ return { ok: false, message: e.message };
138
+ }
139
+ }
140
+ const taskMdPath = findTaskMd(repoRoot, taskId);
141
+ if (!taskMdPath) {
142
+ return {
143
+ ok: false,
144
+ message: `task ${taskId} not found in active / blocked / completed / archive`
145
+ };
146
+ }
147
+ return { ok: true, repoRoot, taskId, taskDir: path.dirname(taskMdPath), taskMdPath };
148
+ }
149
+ export { resolveTaskRef, detectRepoRoot, enumerateTaskDirs, TASK_ID_RE };
150
+ //# sourceMappingURL=resolve-ref.js.map
@@ -58,6 +58,16 @@ function loadShortIdByTaskId(repoRoot) {
58
58
  }
59
59
  return map;
60
60
  }
61
+ /**
62
+ * Resolve a branch to its active-task short id (`#NN`), or `null` when no
63
+ * active task is bound to that branch.
64
+ *
65
+ * Two-state semantics: this only consults the active registry
66
+ * (`active/.short-ids.json`) plus each `active/{taskId}/task.md`. Tasks moved
67
+ * to completed/blocked/cancelled/archive have already released their short id,
68
+ * so their branches return `null` — in `ai sandbox ls` that surfaces as `-`,
69
+ * meaning the sandbox is free to remove.
70
+ */
61
71
  function lookupShortIdByBranch(branch, repoRoot, _opts) {
62
72
  const registry = readRegistry(repoRoot);
63
73
  if (!registry)
@@ -7,6 +7,24 @@ import { isPathOwnedByDisabledTUI, resolveEnabledTUIs } from "./builtin-tuis.js"
7
7
  const defaults = JSON.parse(fs.readFileSync(new URL('./defaults.json', import.meta.url), 'utf8'));
8
8
  const CONFIG_DIR = '.agents';
9
9
  const CONFIG_PATH = path.join(CONFIG_DIR, '.airc.json');
10
+ // One-time migration of the legacy project-level PR switch to the three-state
11
+ // `prFlow` preference. `true` (the old default / "PR flow on") maps to the
12
+ // strong constraint `required`; `false` maps to `disabled`. A missing or
13
+ // already-migrated config is left untouched (idempotent). Returns the new
14
+ // prFlow value when a migration happened, otherwise null.
15
+ function migratePrFlow(config) {
16
+ if (config.requiresPullRequest === true) {
17
+ delete config.requiresPullRequest;
18
+ config.prFlow = 'required';
19
+ return 'required';
20
+ }
21
+ if (config.requiresPullRequest === false) {
22
+ delete config.requiresPullRequest;
23
+ config.prFlow = 'disabled';
24
+ return 'disabled';
25
+ }
26
+ return null;
27
+ }
10
28
  function isPathOwnedByOtherPlatform(relativePath, platformType) {
11
29
  const top = String(relativePath || '').replace(/\\/g, '/').replace(/^\.\//, '').split('/')[0] ?? '';
12
30
  if (!top.startsWith('.'))
@@ -134,7 +152,7 @@ async function cmdUpdate() {
134
152
  const sandboxAdded = !config.sandbox;
135
153
  const taskAdded = !config.task;
136
154
  const labelsAdded = !config.labels;
137
- const requiresPullRequestAdded = config.requiresPullRequest === undefined;
155
+ const prFlowMigrated = migratePrFlow(config);
138
156
  let configChanged = changed;
139
157
  if (platformAdded) {
140
158
  config.platform = structuredClone(defaults.platform);
@@ -152,8 +170,7 @@ async function cmdUpdate() {
152
170
  config.labels = structuredClone(defaults.labels);
153
171
  configChanged = true;
154
172
  }
155
- if (requiresPullRequestAdded) {
156
- config.requiresPullRequest = defaults.requiresPullRequest;
173
+ if (prFlowMigrated) {
157
174
  configChanged = true;
158
175
  }
159
176
  if (configChanged) {
@@ -167,7 +184,7 @@ async function cmdUpdate() {
167
184
  ok(` merged: ${entry}`);
168
185
  }
169
186
  }
170
- else if (platformAdded || sandboxAdded || taskAdded || labelsAdded || requiresPullRequestAdded) {
187
+ else if (platformAdded || sandboxAdded || taskAdded || labelsAdded || prFlowMigrated) {
171
188
  if (platformAdded) {
172
189
  info(`Default platform config added to ${CONFIG_PATH}.`);
173
190
  }
@@ -180,8 +197,8 @@ async function cmdUpdate() {
180
197
  if (labelsAdded) {
181
198
  info(`Default labels.in config added to ${CONFIG_PATH}.`);
182
199
  }
183
- if (requiresPullRequestAdded) {
184
- info(`Default requiresPullRequest=${defaults.requiresPullRequest} added to ${CONFIG_PATH}.`);
200
+ if (prFlowMigrated) {
201
+ info(`Migrated legacy requiresPullRequest to prFlow="${prFlowMigrated}" in ${CONFIG_PATH}.`);
185
202
  }
186
203
  }
187
204
  else {
@@ -199,8 +216,8 @@ async function cmdUpdate() {
199
216
  if (hasNewEntries && platformAdded) {
200
217
  info(`Default platform config added to ${CONFIG_PATH}.`);
201
218
  }
202
- if (hasNewEntries && requiresPullRequestAdded) {
203
- info(`Default requiresPullRequest=${defaults.requiresPullRequest} added to ${CONFIG_PATH}.`);
219
+ if (hasNewEntries && prFlowMigrated) {
220
+ info(`Migrated legacy requiresPullRequest to prFlow="${prFlowMigrated}" in ${CONFIG_PATH}.`);
204
221
  }
205
222
  fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', 'utf8');
206
223
  ok(`Updated ${CONFIG_PATH}`);
package/lib/defaults.json CHANGED
@@ -2,7 +2,6 @@
2
2
  "platform": {
3
3
  "type": "github"
4
4
  },
5
- "requiresPullRequest": true,
6
5
  "sandbox": {
7
6
  "engine": null,
8
7
  "runtimes": [
package/lib/init.ts CHANGED
@@ -32,7 +32,6 @@ type Defaults = {
32
32
  sandbox: Record<string, unknown>;
33
33
  task: { shortIdLength: number };
34
34
  labels: Record<string, unknown>;
35
- requiresPullRequest: boolean;
36
35
  };
37
36
 
38
37
  type AgentConfig = {
@@ -40,7 +39,6 @@ type AgentConfig = {
40
39
  org: string;
41
40
  language: string;
42
41
  platform: { type: string };
43
- requiresPullRequest: boolean;
44
42
  templateVersion: string;
45
43
  sandbox: Record<string, unknown>;
46
44
  task: { shortIdLength: number };
@@ -224,13 +222,6 @@ async function cmdInit(): Promise<void> {
224
222
  );
225
223
  }
226
224
 
227
- const requiresPRChoice = await select(
228
- 'Require Pull Request flow?',
229
- ['yes', 'no'],
230
- 'yes'
231
- );
232
- const requiresPullRequest = requiresPRChoice !== 'no';
233
-
234
225
  let enabledTUIs: string[];
235
226
  try {
236
227
  enabledTUIs = await multiSelect(
@@ -324,7 +315,6 @@ async function cmdInit(): Promise<void> {
324
315
  org: orgName,
325
316
  language,
326
317
  platform: { type: platformType },
327
- requiresPullRequest,
328
318
  templateVersion: VERSION,
329
319
  sandbox: structuredClone(defaults.sandbox),
330
320
  task: structuredClone(defaults.task),
@@ -840,7 +840,34 @@ export function ensureClaudeSettings(toolDir: string, hostHomeDir?: string): voi
840
840
  }
841
841
  }
842
842
 
843
- export function ensureCodexModelInheritance(toolDir: string, hostHomeDir?: string): void {
843
+ function resolveHostCatalogPath(value: unknown, hostHomeDir: string): string | null {
844
+ if (typeof value !== 'string' || value === '') {
845
+ return null;
846
+ }
847
+ let resolved: string;
848
+ if (value === '~' || value.startsWith('~/') || value.startsWith('~\\')) {
849
+ resolved = path.join(hostHomeDir, value.slice(1).replace(/^[/\\]+/, ''));
850
+ } else if (path.isAbsolute(value)) {
851
+ resolved = value;
852
+ } else {
853
+ resolved = path.join(hostHomeDir, '.codex', value);
854
+ }
855
+ try {
856
+ if (!fs.statSync(resolved).isFile()) {
857
+ return null;
858
+ }
859
+ fs.accessSync(resolved, fs.constants.R_OK);
860
+ return resolved;
861
+ } catch {
862
+ return null;
863
+ }
864
+ }
865
+
866
+ export function ensureCodexModelInheritance(
867
+ toolDir: string,
868
+ hostHomeDir?: string,
869
+ containerCodexDir: string = '/home/devuser/.codex'
870
+ ): void {
844
871
  if (!hostHomeDir) {
845
872
  return;
846
873
  }
@@ -890,6 +917,22 @@ export function ensureCodexModelInheritance(toolDir: string, hostHomeDir?: strin
890
917
  changed = true;
891
918
  }
892
919
 
920
+ if (!Object.hasOwn(sandboxParsed, 'model_catalog_json')) {
921
+ const hostCatalogPath = resolveHostCatalogPath(hostParsed['model_catalog_json'], hostHomeDir);
922
+ if (hostCatalogPath) {
923
+ try {
924
+ const basename = path.basename(hostCatalogPath);
925
+ const destDir = path.join(toolDir, 'model-catalogs');
926
+ fs.mkdirSync(destDir, { recursive: true });
927
+ fs.copyFileSync(hostCatalogPath, path.join(destDir, basename));
928
+ sandboxParsed['model_catalog_json'] = path.posix.join(containerCodexDir, 'model-catalogs', basename);
929
+ changed = true;
930
+ } catch {
931
+ // Copy failed (e.g. permissions): skip catalog, keep scalar inheritance intact.
932
+ }
933
+ }
934
+ }
935
+
893
936
  if (changed) {
894
937
  fs.writeFileSync(sandboxConfigPath, `${toml.stringify(sandboxParsed)}\n`, 'utf8');
895
938
  }
@@ -1042,7 +1085,7 @@ function runEngineTaskCommand(engine: string, cmd: string, args: string[], opts:
1042
1085
  }
1043
1086
 
1044
1087
  export function buildImage(
1045
- config: SandboxCreateConfig,
1088
+ config: Pick<SandboxCreateConfig, 'project' | 'imageName' | 'repoRoot'> & { engine?: string | null },
1046
1089
  tools: SandboxTool[],
1047
1090
  dockerfilePath: string,
1048
1091
  imageSignature: string,
@@ -1060,7 +1103,7 @@ export function buildImage(
1060
1103
  env?: NodeJS.ProcessEnv;
1061
1104
  } = {}
1062
1105
  ): void {
1063
- const selectedEngine = engine ?? detectEngine(config);
1106
+ const selectedEngine = engine ?? detectEngine({ engine: config.engine });
1064
1107
  const { uid: hostUid, gid: hostGid } = resolveBuildUid({
1065
1108
  engine: selectedEngine,
1066
1109
  runFn,
@@ -1342,7 +1385,7 @@ export async function create(args: string[]): Promise<void> {
1342
1385
  }
1343
1386
  const codexEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'codex');
1344
1387
  if (codexEntry) {
1345
- ensureCodexModelInheritance(codexEntry.dir, effectiveConfig.home);
1388
+ ensureCodexModelInheritance(codexEntry.dir, effectiveConfig.home, codexEntry.tool.containerMount);
1346
1389
  ensureCodexWorkspaceTrust(codexEntry.dir);
1347
1390
  }
1348
1391
  const geminiEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'gemini-cli');