@fitlab-ai/agent-infra 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.ts +1 -1
- package/dist/bin/cli.js +1 -1
- package/dist/lib/builtin-tuis.js +45 -0
- package/dist/lib/defaults.json +3 -0
- package/dist/lib/init.js +62 -23
- package/dist/lib/prompt.js +49 -1
- package/dist/lib/sandbox/commands/enter.js +1 -1
- package/dist/lib/sandbox/commands/list-running.js +58 -13
- package/dist/lib/sandbox/commands/rebuild.js +3 -11
- package/dist/lib/sandbox/commands/rm.js +2 -0
- package/dist/lib/sandbox/image-prune.js +18 -0
- package/dist/lib/sandbox/task-resolver.js +18 -0
- package/dist/lib/update.js +59 -18
- package/lib/builtin-tuis.ts +55 -0
- package/lib/defaults.json +3 -0
- package/lib/init.ts +87 -35
- package/lib/prompt.ts +54 -1
- package/lib/sandbox/commands/enter.ts +1 -1
- package/lib/sandbox/commands/list-running.ts +69 -16
- package/lib/sandbox/commands/rebuild.ts +3 -12
- package/lib/sandbox/commands/rm.ts +3 -0
- package/lib/sandbox/image-prune.ts +23 -0
- package/lib/sandbox/task-resolver.ts +23 -1
- package/lib/update.ts +71 -30
- package/package.json +1 -1
- package/templates/.agents/README.en.md +32 -0
- package/templates/.agents/README.zh-CN.md +32 -0
- package/templates/.agents/rules/task-short-id.en.md +141 -0
- package/templates/.agents/rules/task-short-id.zh-CN.md +124 -0
- package/templates/.agents/scripts/task-short-id.js +713 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +4 -0
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +4 -1
- package/templates/.agents/skills/block-task/SKILL.en.md +12 -0
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +12 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +12 -0
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +12 -1
- package/templates/.agents/skills/check-task/SKILL.en.md +4 -0
- package/templates/.agents/skills/check-task/SKILL.zh-CN.md +4 -1
- package/templates/.agents/skills/close-codescan/SKILL.en.md +11 -0
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +11 -0
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +11 -0
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +11 -0
- package/templates/.agents/skills/code-task/SKILL.en.md +4 -0
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +4 -1
- package/templates/.agents/skills/commit/SKILL.en.md +4 -0
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +4 -0
- package/templates/.agents/skills/complete-task/SKILL.en.md +12 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +12 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +4 -0
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -0
- package/templates/.agents/skills/create-task/SKILL.en.md +14 -0
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +14 -1
- package/templates/.agents/skills/import-codescan/SKILL.en.md +14 -0
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +14 -0
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +14 -0
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +14 -0
- package/templates/.agents/skills/import-issue/SKILL.en.md +14 -0
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +14 -0
- package/templates/.agents/skills/plan-task/SKILL.en.md +4 -0
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -1
- package/templates/.agents/skills/restore-task/SKILL.en.md +12 -0
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +12 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +4 -0
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +4 -1
- package/templates/.agents/skills/review-code/SKILL.en.md +4 -0
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -1
- package/templates/.agents/skills/review-plan/SKILL.en.md +4 -0
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +4 -1
- package/templates/.agents/skills/update-agent-infra/SKILL.en.md +1 -0
- package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +1 -0
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +112 -21
- package/templates/.agents/templates/task.en.md +1 -0
- package/templates/.agents/templates/task.zh-CN.md +1 -0
package/bin/cli.ts
CHANGED
package/dist/bin/cli.js
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const BUILTIN_TUI_IDS = ['claude-code', 'codex', 'gemini-cli', 'opencode'];
|
|
2
|
+
const BUILTIN_TUI_DISPLAY = {
|
|
3
|
+
'claude-code': 'Claude Code',
|
|
4
|
+
'codex': 'Codex',
|
|
5
|
+
'gemini-cli': 'Gemini CLI',
|
|
6
|
+
'opencode': 'OpenCode'
|
|
7
|
+
};
|
|
8
|
+
const BUILTIN_TUI_OWNED_PATH_PREFIXES = {
|
|
9
|
+
'claude-code': ['.claude/'],
|
|
10
|
+
'codex': ['.codex/'],
|
|
11
|
+
'gemini-cli': ['.gemini/'],
|
|
12
|
+
'opencode': ['.opencode/']
|
|
13
|
+
};
|
|
14
|
+
function isBuiltinTUIId(value) {
|
|
15
|
+
return typeof value === 'string' && BUILTIN_TUI_IDS.includes(value);
|
|
16
|
+
}
|
|
17
|
+
function resolveEnabledTUIs(value) {
|
|
18
|
+
// Missing field / null / non-array → full set (backward compat for legacy
|
|
19
|
+
// .airc.json predating the `tuis` field).
|
|
20
|
+
if (!Array.isArray(value))
|
|
21
|
+
return new Set(BUILTIN_TUI_IDS);
|
|
22
|
+
// Empty array is a meaningful, user-set value: "no built-in TUI managed".
|
|
23
|
+
// This supports the customTUI-only project layout.
|
|
24
|
+
const set = new Set();
|
|
25
|
+
for (const v of value) {
|
|
26
|
+
if (isBuiltinTUIId(v))
|
|
27
|
+
set.add(v);
|
|
28
|
+
}
|
|
29
|
+
return set;
|
|
30
|
+
}
|
|
31
|
+
function isPathOwnedByDisabledTUI(rel, enabled) {
|
|
32
|
+
const normalized = String(rel || '').replace(/\\/g, '/').replace(/^\.\//, '');
|
|
33
|
+
for (const tui of BUILTIN_TUI_IDS) {
|
|
34
|
+
if (enabled.has(tui))
|
|
35
|
+
continue;
|
|
36
|
+
for (const prefix of BUILTIN_TUI_OWNED_PATH_PREFIXES[tui]) {
|
|
37
|
+
const trimmed = prefix.replace(/\/$/, '');
|
|
38
|
+
if (normalized === trimmed || normalized.startsWith(prefix))
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
export { BUILTIN_TUI_IDS, BUILTIN_TUI_DISPLAY, BUILTIN_TUI_OWNED_PATH_PREFIXES, isBuiltinTUIId, resolveEnabledTUIs, isPathOwnedByDisabledTUI };
|
|
45
|
+
//# sourceMappingURL=builtin-tuis.js.map
|
package/dist/lib/defaults.json
CHANGED
package/dist/lib/init.js
CHANGED
|
@@ -3,11 +3,12 @@ import path from 'node:path';
|
|
|
3
3
|
import { execSync } from 'node:child_process';
|
|
4
4
|
import { platform } from 'node:os';
|
|
5
5
|
import { info, ok, err } from "./log.js";
|
|
6
|
-
import { prompt, select, closePrompt } from "./prompt.js";
|
|
6
|
+
import { prompt, select, multiSelect, closePrompt } from "./prompt.js";
|
|
7
7
|
import { resolveTemplateDir } from "./paths.js";
|
|
8
8
|
import { renderFile, copySkillDir, KNOWN_PLATFORMS } from "./render.js";
|
|
9
9
|
import { enginesForPlatform } from "./sandbox/engines/index.js";
|
|
10
10
|
import { VERSION } from "./version.js";
|
|
11
|
+
import { BUILTIN_TUI_IDS, BUILTIN_TUI_DISPLAY, isPathOwnedByDisabledTUI, resolveEnabledTUIs } from "./builtin-tuis.js";
|
|
11
12
|
const defaults = JSON.parse(fs.readFileSync(new URL('./defaults.json', import.meta.url), 'utf8'));
|
|
12
13
|
const PLATFORM_DEFAULT_ENGINES = Object.freeze({
|
|
13
14
|
linux: 'native',
|
|
@@ -23,10 +24,11 @@ function isPathOwnedByOtherPlatform(relativePath, platformType) {
|
|
|
23
24
|
return false;
|
|
24
25
|
return candidate !== platformType;
|
|
25
26
|
}
|
|
26
|
-
function buildDefaultFiles(platformType) {
|
|
27
|
+
function buildDefaultFiles(platformType, enabledTUIs) {
|
|
28
|
+
const ownedByDisabled = (entry) => isPathOwnedByDisabledTUI(entry, enabledTUIs);
|
|
27
29
|
return {
|
|
28
|
-
managed: (defaults.files.managed || []).filter((entry) => !isPathOwnedByOtherPlatform(entry, platformType)),
|
|
29
|
-
merged: (defaults.files.merged || []).filter((entry) => !isPathOwnedByOtherPlatform(entry, platformType)),
|
|
30
|
+
managed: (defaults.files.managed || []).filter((entry) => !isPathOwnedByOtherPlatform(entry, platformType) && !ownedByDisabled(entry)),
|
|
31
|
+
merged: (defaults.files.merged || []).filter((entry) => !isPathOwnedByOtherPlatform(entry, platformType) && !ownedByDisabled(entry)),
|
|
30
32
|
ejected: structuredClone(defaults.files.ejected || [])
|
|
31
33
|
};
|
|
32
34
|
}
|
|
@@ -158,6 +160,17 @@ async function cmdInit() {
|
|
|
158
160
|
}
|
|
159
161
|
const requiresPRChoice = await select('Require Pull Request flow?', ['yes', 'no'], 'yes');
|
|
160
162
|
const requiresPullRequest = requiresPRChoice !== 'no';
|
|
163
|
+
let enabledTUIs;
|
|
164
|
+
try {
|
|
165
|
+
enabledTUIs = await multiSelect('Built-in TUI command files to install/manage', BUILTIN_TUI_IDS.map((id) => ({ id, label: BUILTIN_TUI_DISPLAY[id] })));
|
|
166
|
+
}
|
|
167
|
+
catch (e) {
|
|
168
|
+
err(e instanceof Error ? e.message : String(e));
|
|
169
|
+
closePrompt();
|
|
170
|
+
process.exitCode = 1;
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const enabledTUISet = resolveEnabledTUIs(enabledTUIs);
|
|
161
174
|
const templateSources = parseLocalSources(await prompt('Template sources (optional, comma-separated local paths, e.g. ~/my-templates; Enter to skip)', ''));
|
|
162
175
|
const skillSources = parseLocalSources(await prompt('Skill sources (optional, comma-separated local paths, e.g. ~/my-skills; Enter to skip)', ''));
|
|
163
176
|
closePrompt();
|
|
@@ -186,15 +199,21 @@ async function cmdInit() {
|
|
|
186
199
|
// install skill
|
|
187
200
|
copySkillDir(path.join(templateDir, '.agents', 'skills', 'update-agent-infra'), path.join('.agents', 'skills', 'update-agent-infra'), replacements, language, platformType);
|
|
188
201
|
ok('Installed .agents/skills/update-agent-infra/');
|
|
189
|
-
// install Claude command
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
202
|
+
// install Claude command (only if enabled)
|
|
203
|
+
if (enabledTUISet.has('claude-code')) {
|
|
204
|
+
renderFile(path.join(templateDir, '.claude', 'commands', claudeSrc), path.join('.claude', 'commands', 'update-agent-infra.md'), replacements);
|
|
205
|
+
ok('Installed .claude/commands/update-agent-infra.md');
|
|
206
|
+
}
|
|
207
|
+
// install Gemini command (only if enabled)
|
|
208
|
+
if (enabledTUISet.has('gemini-cli')) {
|
|
209
|
+
renderFile(path.join(templateDir, '.gemini', 'commands', '_project_', geminiSrc), path.join('.gemini', 'commands', project, 'update-agent-infra.toml'), replacements);
|
|
210
|
+
ok(`Installed .gemini/commands/${project}/update-agent-infra.toml`);
|
|
211
|
+
}
|
|
212
|
+
// install OpenCode command (only if enabled)
|
|
213
|
+
if (enabledTUISet.has('opencode')) {
|
|
214
|
+
renderFile(path.join(templateDir, '.opencode', 'commands', opencodeSrc), path.join('.opencode', 'commands', 'update-agent-infra.md'), replacements);
|
|
215
|
+
ok('Installed .opencode/commands/update-agent-infra.md');
|
|
216
|
+
}
|
|
198
217
|
// generate .agents/.airc.json
|
|
199
218
|
const config = {
|
|
200
219
|
project: projectName,
|
|
@@ -204,8 +223,10 @@ async function cmdInit() {
|
|
|
204
223
|
requiresPullRequest,
|
|
205
224
|
templateVersion: VERSION,
|
|
206
225
|
sandbox: structuredClone(defaults.sandbox),
|
|
226
|
+
task: structuredClone(defaults.task),
|
|
207
227
|
labels: structuredClone(defaults.labels),
|
|
208
|
-
files: buildDefaultFiles(platformType)
|
|
228
|
+
files: buildDefaultFiles(platformType, enabledTUISet),
|
|
229
|
+
tuis: enabledTUIs
|
|
209
230
|
};
|
|
210
231
|
if (sandboxEngine) {
|
|
211
232
|
config.sandbox.engine = sandboxEngine;
|
|
@@ -227,15 +248,33 @@ async function cmdInit() {
|
|
|
227
248
|
console.log('');
|
|
228
249
|
ok('Project initialized successfully!');
|
|
229
250
|
console.log('');
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
251
|
+
if (enabledTUISet.size === 0) {
|
|
252
|
+
console.log(' No built-in TUI selected.');
|
|
253
|
+
console.log(` Configure "customTUIs" in ${configPath} before running update-agent-infra.`);
|
|
254
|
+
console.log('');
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
console.log(' Next step: open this project in any AI TUI and run:');
|
|
258
|
+
console.log('');
|
|
259
|
+
const claudeOrOpencode = [];
|
|
260
|
+
if (enabledTUISet.has('claude-code'))
|
|
261
|
+
claudeOrOpencode.push('Claude Code');
|
|
262
|
+
if (enabledTUISet.has('opencode'))
|
|
263
|
+
claudeOrOpencode.push('OpenCode');
|
|
264
|
+
if (claudeOrOpencode.length > 0) {
|
|
265
|
+
console.log(` ${claudeOrOpencode.join(' / ')}: /update-agent-infra`);
|
|
266
|
+
}
|
|
267
|
+
if (enabledTUISet.has('gemini-cli')) {
|
|
268
|
+
console.log(` Gemini CLI: /${project}:update-agent-infra`);
|
|
269
|
+
}
|
|
270
|
+
if (enabledTUISet.has('codex')) {
|
|
271
|
+
console.log(' Codex CLI: $update-agent-infra');
|
|
272
|
+
}
|
|
273
|
+
console.log('');
|
|
274
|
+
console.log(' This will render all templates and set up the full');
|
|
275
|
+
console.log(' AI collaboration infrastructure.');
|
|
276
|
+
console.log('');
|
|
277
|
+
}
|
|
239
278
|
}
|
|
240
279
|
export { cmdInit };
|
|
241
280
|
//# sourceMappingURL=init.js.map
|
package/dist/lib/prompt.js
CHANGED
|
@@ -74,6 +74,54 @@ async function select(question, choices, defaultValue) {
|
|
|
74
74
|
}
|
|
75
75
|
return trimmed;
|
|
76
76
|
}
|
|
77
|
+
async function multiSelect(question, choices) {
|
|
78
|
+
process.stdout.write(` ${question}:\n`);
|
|
79
|
+
const idWidth = Math.max(...choices.map((c) => c.id.length));
|
|
80
|
+
choices.forEach((c, i) => {
|
|
81
|
+
process.stdout.write(` ${i + 1}) ${c.id.padEnd(idWidth)} (${c.label})\n`);
|
|
82
|
+
});
|
|
83
|
+
ask('Enter comma-separated numbers or ids to keep, or "none" to select nothing [default: all]: ');
|
|
84
|
+
setupInterface();
|
|
85
|
+
const line = await nextLine();
|
|
86
|
+
// Strictly distinguish bare Enter (null/empty string) from whitespace input.
|
|
87
|
+
if (line === null || line === '')
|
|
88
|
+
return choices.map((c) => c.id);
|
|
89
|
+
// Explicit empty selection: "none" means deliberately zero built-in choices.
|
|
90
|
+
if (line.trim().toLowerCase() === 'none')
|
|
91
|
+
return [];
|
|
92
|
+
const tokens = line.split(',').map((t) => t.trim());
|
|
93
|
+
if (tokens.some((t) => t === '')) {
|
|
94
|
+
throw new Error(`Invalid selection input: "${line}" (empty token)`);
|
|
95
|
+
}
|
|
96
|
+
const idSet = new Set(choices.map((c) => c.id));
|
|
97
|
+
const seenIds = new Set();
|
|
98
|
+
for (const t of tokens) {
|
|
99
|
+
let resolvedId;
|
|
100
|
+
if (/^[0-9]+$/.test(t)) {
|
|
101
|
+
const n = Number.parseInt(t, 10);
|
|
102
|
+
if (n < 1 || n > choices.length) {
|
|
103
|
+
throw new Error(`Selection out of range: "${t}" (expected 1..${choices.length})`);
|
|
104
|
+
}
|
|
105
|
+
resolvedId = choices[n - 1].id;
|
|
106
|
+
}
|
|
107
|
+
else if (idSet.has(t)) {
|
|
108
|
+
resolvedId = t;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
throw new Error(`Unknown TUI selection token: "${t}"`);
|
|
112
|
+
}
|
|
113
|
+
if (seenIds.has(resolvedId)) {
|
|
114
|
+
throw new Error(`Duplicate selection: "${t}" resolves to already-selected "${resolvedId}"`);
|
|
115
|
+
}
|
|
116
|
+
seenIds.add(resolvedId);
|
|
117
|
+
}
|
|
118
|
+
// Normalize to prompt order: users can type tokens in any order, but the
|
|
119
|
+
// persisted array follows the canonical choices order to keep .airc.json
|
|
120
|
+
// diffs stable. An empty result here is impossible (tokens.length > 0 and
|
|
121
|
+
// every token resolves to an id), so no separate empty guard is needed —
|
|
122
|
+
// explicit "none" handled above.
|
|
123
|
+
return choices.map((c) => c.id).filter((id) => seenIds.has(id));
|
|
124
|
+
}
|
|
77
125
|
function closePrompt() {
|
|
78
126
|
if (_rl) {
|
|
79
127
|
_rl.close();
|
|
@@ -81,5 +129,5 @@ function closePrompt() {
|
|
|
81
129
|
_stdinDone = true;
|
|
82
130
|
}
|
|
83
131
|
}
|
|
84
|
-
export { prompt, select, closePrompt };
|
|
132
|
+
export { prompt, select, multiSelect, closePrompt };
|
|
85
133
|
//# sourceMappingURL=prompt.js.map
|
|
@@ -87,7 +87,7 @@ export async function enter(args) {
|
|
|
87
87
|
let branch;
|
|
88
88
|
if (isTaskShortRef(firstArg)) {
|
|
89
89
|
const { running } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
|
|
90
|
-
branch = resolveTaskShortRef(firstArg, { running });
|
|
90
|
+
branch = resolveTaskShortRef(firstArg, { running, repoRoot: config.repoRoot });
|
|
91
91
|
}
|
|
92
92
|
else {
|
|
93
93
|
branch = resolveTaskBranch(firstArg, config.repoRoot);
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
1
4
|
import { runSafeEngine } from "../shell.js";
|
|
2
5
|
export function containerListFormat() {
|
|
3
6
|
return '{{.Names}}\t{{.Status}}\t{{.Labels}}';
|
|
@@ -74,25 +77,47 @@ export function isTaskShortRef(arg) {
|
|
|
74
77
|
return /^#\d+$/.test(arg);
|
|
75
78
|
}
|
|
76
79
|
/**
|
|
77
|
-
*
|
|
80
|
+
* Try to resolve a short ref against the global task-short-id registry.
|
|
78
81
|
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* Precondition: callers MUST gate on isTaskShortRef(arg) === true before
|
|
86
|
-
* constructing ctx and calling this function. Throws when arg is a valid
|
|
87
|
-
* short ref but cannot be resolved (out of range, no running sandboxes,
|
|
88
|
-
* etc.); the caller surfaces the error to the user.
|
|
82
|
+
* Tri-state semantics (review-code Round 1 M-1 fix):
|
|
83
|
+
* - 'miss' → script reports no entry (or registry script missing). Caller may fall back.
|
|
84
|
+
* - 'hit' → registry resolved to a task id and branch is found in task.md.
|
|
85
|
+
* - throws → registry hit but task.md is missing or branch metadata is unparseable;
|
|
86
|
+
* surfacing this error is critical — never silently fall back to running index.
|
|
89
87
|
*/
|
|
90
|
-
|
|
88
|
+
function tryResolveFromRegistry(arg, repoRoot) {
|
|
89
|
+
const scriptPath = path.join(repoRoot, '.agents', 'scripts', 'task-short-id.js');
|
|
90
|
+
if (!fs.existsSync(scriptPath))
|
|
91
|
+
return { status: 'miss' };
|
|
92
|
+
const result = spawnSync('node', [scriptPath, 'resolve', arg], { encoding: 'utf8', cwd: repoRoot });
|
|
93
|
+
if (result.status !== 0)
|
|
94
|
+
return { status: 'miss' };
|
|
95
|
+
const taskId = (result.stdout || '').trim();
|
|
96
|
+
if (!/^TASK-\d{8}-\d{6}$/.test(taskId)) {
|
|
97
|
+
throw new Error(`Registry returned malformed task id for '${arg}': ${JSON.stringify(taskId)}`);
|
|
98
|
+
}
|
|
99
|
+
for (const sub of ['active', 'completed', 'blocked', 'archive']) {
|
|
100
|
+
const taskMdPath = path.join(repoRoot, '.agents', 'workspace', sub, taskId, 'task.md');
|
|
101
|
+
if (!fs.existsSync(taskMdPath))
|
|
102
|
+
continue;
|
|
103
|
+
const content = fs.readFileSync(taskMdPath, 'utf8');
|
|
104
|
+
const fm = content.match(/^branch:\s*(.+)$/m);
|
|
105
|
+
if (fm?.[1]?.trim()) {
|
|
106
|
+
return { status: 'hit', branch: fm[1].trim().replace(/^(["'])(.*)\1$/, '$2') };
|
|
107
|
+
}
|
|
108
|
+
const ctx = content.match(/^- \*\*(?:分支|Branch)\*\*:[ \t]*`?([^`\n]+)`?$/m);
|
|
109
|
+
if (ctx?.[1]?.trim()) {
|
|
110
|
+
return { status: 'hit', branch: ctx[1].trim().replace(/^(["'])(.*)\1$/, '$2') };
|
|
111
|
+
}
|
|
112
|
+
throw new Error(`Short ref '${arg}' resolved to task ${taskId} but task.md has no branch field`);
|
|
113
|
+
}
|
|
114
|
+
throw new Error(`Short ref '${arg}' resolved to task ${taskId} but task.md was not found under any workspace dir`);
|
|
115
|
+
}
|
|
116
|
+
function resolveByRunningIndex(arg, running) {
|
|
91
117
|
const n = Number(arg.slice(1));
|
|
92
118
|
if (n < 1) {
|
|
93
119
|
throw new Error(`Invalid sandbox index '${arg}': must be >= 1`);
|
|
94
120
|
}
|
|
95
|
-
const { running } = ctx;
|
|
96
121
|
if (running.length === 0) {
|
|
97
122
|
throw new Error(`No running sandbox to reference with '${arg}'`);
|
|
98
123
|
}
|
|
@@ -105,4 +130,24 @@ export function resolveTaskShortRef(arg, ctx) {
|
|
|
105
130
|
}
|
|
106
131
|
return row.branch;
|
|
107
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Resolve a task short reference ('#N') to a branch name for the sandbox entrypoint.
|
|
135
|
+
*
|
|
136
|
+
* Resolution order (sandbox fallback mode, plan-r7 C2):
|
|
137
|
+
* 1. Try the global task-short-id registry under repoRoot. If hit, look up the
|
|
138
|
+
* branch from the matching task.md.
|
|
139
|
+
* 2. Fallback to the running-sandbox list index (preserves the #414 ls-index
|
|
140
|
+
* behaviour; long-term contract per analysis-r5).
|
|
141
|
+
*
|
|
142
|
+
* Precondition: callers MUST gate on isTaskShortRef(arg) === true.
|
|
143
|
+
*/
|
|
144
|
+
export function resolveTaskShortRef(arg, ctx) {
|
|
145
|
+
if (ctx.repoRoot) {
|
|
146
|
+
const lookup = tryResolveFromRegistry(arg, ctx.repoRoot);
|
|
147
|
+
if (lookup.status === 'hit')
|
|
148
|
+
return lookup.branch;
|
|
149
|
+
// 'miss' falls through to ls-index fallback (preserves #414 behaviour); 'hit-but-invalid' already threw above.
|
|
150
|
+
}
|
|
151
|
+
return resolveByRunningIndex(arg, ctx.running);
|
|
152
|
+
}
|
|
108
153
|
//# sourceMappingURL=list-running.js.map
|
|
@@ -6,7 +6,8 @@ import { loadConfig } from "../config.js";
|
|
|
6
6
|
import { prepareDockerfile } from "../dockerfile.js";
|
|
7
7
|
import { sandboxImageConfigLabel, sandboxLabel } from "../constants.js";
|
|
8
8
|
import { detectEngine, ensureDocker } from "../engine.js";
|
|
9
|
-
import { runEngine,
|
|
9
|
+
import { runEngine, runSafeEngine, runVerboseEngine } from "../shell.js";
|
|
10
|
+
import { pruneSandboxDanglingImages } from "../image-prune.js";
|
|
10
11
|
import { imageSignatureFields, resolveTools, toolNpmPackagesArg, toolShellInstallScriptBase64 } from "../tools.js";
|
|
11
12
|
import { toEnginePath } from "../engines/wsl2-paths.js";
|
|
12
13
|
import { resolveBuildUid } from "../engines/native.js";
|
|
@@ -53,11 +54,6 @@ export function buildArgs(config, tools, dockerfilePath, imageSignature, { engin
|
|
|
53
54
|
}
|
|
54
55
|
return args;
|
|
55
56
|
}
|
|
56
|
-
function removeImageIfPresent(imageName, engine) {
|
|
57
|
-
if (runOkEngine(engine, 'docker', ['image', 'inspect', imageName])) {
|
|
58
|
-
runEngine(engine, 'docker', ['rmi', imageName]);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
57
|
export async function rebuild(args) {
|
|
62
58
|
const { values } = parseArgs({
|
|
63
59
|
args,
|
|
@@ -85,9 +81,6 @@ export async function rebuild(args) {
|
|
|
85
81
|
try {
|
|
86
82
|
if (quiet) {
|
|
87
83
|
const spinner = p.spinner();
|
|
88
|
-
spinner.start(`Removing old image ${config.imageName}...`);
|
|
89
|
-
removeImageIfPresent(config.imageName, engine);
|
|
90
|
-
spinner.stop('Old image removed');
|
|
91
84
|
spinner.start('Building image...');
|
|
92
85
|
runEngine(engine, 'docker', buildArgs(config, tools, preparedDockerfile.path, imageSignature, { engine, refresh }), {
|
|
93
86
|
cwd: config.repoRoot
|
|
@@ -95,12 +88,11 @@ export async function rebuild(args) {
|
|
|
95
88
|
spinner.stop(pc.green('Sandbox image rebuilt'));
|
|
96
89
|
}
|
|
97
90
|
else {
|
|
98
|
-
p.log.step(`Removing old image ${config.imageName}`);
|
|
99
|
-
removeImageIfPresent(config.imageName, engine);
|
|
100
91
|
p.log.step('Building image');
|
|
101
92
|
runVerboseEngine(engine, 'docker', buildArgs(config, tools, preparedDockerfile.path, imageSignature, { engine, refresh }), { cwd: config.repoRoot });
|
|
102
93
|
p.log.success(pc.green('Sandbox image rebuilt'));
|
|
103
94
|
}
|
|
95
|
+
pruneSandboxDanglingImages(config, engine);
|
|
104
96
|
}
|
|
105
97
|
finally {
|
|
106
98
|
preparedDockerfile.cleanup();
|
|
@@ -6,6 +6,7 @@ import pc from 'picocolors';
|
|
|
6
6
|
import { loadConfig } from "../config.js";
|
|
7
7
|
import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel, shareBranchDir, shellConfigDirCandidates, worktreeDirCandidates } from "../constants.js";
|
|
8
8
|
import { ENGINES, detectEngine, engineDisplayName, isManagedEngine, stopManagedVm } from "../engine.js";
|
|
9
|
+
import { pruneSandboxDanglingImages } from "../image-prune.js";
|
|
9
10
|
import { removeManagedDir, removeWorktreeDir } from "../managed-fs.js";
|
|
10
11
|
import { runOk, runSafe, runSafeEngine } from "../shell.js";
|
|
11
12
|
import { resolveTaskBranch } from "../task-resolver.js";
|
|
@@ -174,6 +175,7 @@ async function rmAll(config, tools) {
|
|
|
174
175
|
if (!p.isCancel(shouldRemoveImage) && shouldRemoveImage) {
|
|
175
176
|
runSafeEngine(engine, 'docker', ['rmi', config.imageName]);
|
|
176
177
|
}
|
|
178
|
+
pruneSandboxDanglingImages(config, engine);
|
|
177
179
|
if (isManagedEngine(engine)) {
|
|
178
180
|
if (engine === ENGINES.WSL2) {
|
|
179
181
|
p.log.warn('Windows uses Docker Desktop with WSL2. Stop it from Docker Desktop or run "wsl --shutdown" manually.');
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import { sandboxLabel } from "./constants.js";
|
|
3
|
+
import { runEngine } from "./shell.js";
|
|
4
|
+
export function pruneSandboxDanglingImages(config, engine) {
|
|
5
|
+
try {
|
|
6
|
+
runEngine(engine, 'docker', [
|
|
7
|
+
'image',
|
|
8
|
+
'prune',
|
|
9
|
+
'-f',
|
|
10
|
+
'--filter',
|
|
11
|
+
`label=${sandboxLabel(config)}`
|
|
12
|
+
]);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
p.log.warn(`Failed to prune dangling sandbox images (label=${sandboxLabel(config)}); leaving them in place.`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=image-prune.js.map
|
|
@@ -1,7 +1,20 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
|
|
5
|
+
const SHORT_ID_RE = /^#\d+$/;
|
|
4
6
|
const WORKSPACE_DIRS = ['active', 'completed', 'blocked', 'archive'];
|
|
7
|
+
function resolveShortIdStrict(arg, repoRoot) {
|
|
8
|
+
const scriptPath = path.join(repoRoot, '.agents', 'scripts', 'task-short-id.js');
|
|
9
|
+
if (!fs.existsSync(scriptPath)) {
|
|
10
|
+
throw new Error(`Short id '${arg}' provided but task-short-id.js script is missing at ${scriptPath}`);
|
|
11
|
+
}
|
|
12
|
+
const result = spawnSync('node', [scriptPath, 'resolve', arg], { encoding: 'utf8', cwd: repoRoot });
|
|
13
|
+
if (result.status !== 0) {
|
|
14
|
+
throw new Error(`Short id '${arg}' not found in active task registry: ${(result.stderr || '').trim()}`);
|
|
15
|
+
}
|
|
16
|
+
return result.stdout.trim();
|
|
17
|
+
}
|
|
5
18
|
function stripQuotes(value) {
|
|
6
19
|
return value.replace(/^(["'])(.*)\1$/, '$2');
|
|
7
20
|
}
|
|
@@ -26,6 +39,11 @@ function resolveBranchFromTaskContent(content, taskId) {
|
|
|
26
39
|
throw new Error(`Task ${taskId} has no branch field in task.md`);
|
|
27
40
|
}
|
|
28
41
|
export function resolveTaskBranch(arg, repoRoot) {
|
|
42
|
+
if (SHORT_ID_RE.test(arg)) {
|
|
43
|
+
const taskId = resolveShortIdStrict(arg, repoRoot);
|
|
44
|
+
const content = readTaskContent(repoRoot, taskId);
|
|
45
|
+
return resolveBranchFromTaskContent(content, taskId);
|
|
46
|
+
}
|
|
29
47
|
if (!TASK_ID_RE.test(arg)) {
|
|
30
48
|
return arg;
|
|
31
49
|
}
|
package/dist/lib/update.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { info, ok, err } from "./log.js";
|
|
4
4
|
import { resolveTemplateDir } from "./paths.js";
|
|
5
5
|
import { renderFile, copySkillDir, KNOWN_PLATFORMS } from "./render.js";
|
|
6
|
+
import { isPathOwnedByDisabledTUI, resolveEnabledTUIs } from "./builtin-tuis.js";
|
|
6
7
|
const defaults = JSON.parse(fs.readFileSync(new URL('./defaults.json', import.meta.url), 'utf8'));
|
|
7
8
|
const CONFIG_DIR = '.agents';
|
|
8
9
|
const CONFIG_PATH = path.join(CONFIG_DIR, '.airc.json');
|
|
@@ -15,7 +16,7 @@ function isPathOwnedByOtherPlatform(relativePath, platformType) {
|
|
|
15
16
|
return false;
|
|
16
17
|
return candidate !== platformType;
|
|
17
18
|
}
|
|
18
|
-
function syncFileRegistry(config, platformType) {
|
|
19
|
+
function syncFileRegistry(config, platformType, enabledTUIs) {
|
|
19
20
|
config.files ||= {};
|
|
20
21
|
const before = JSON.stringify({
|
|
21
22
|
files: {
|
|
@@ -36,6 +37,8 @@ function syncFileRegistry(config, platformType) {
|
|
|
36
37
|
for (const entry of defaults.files.managed) {
|
|
37
38
|
if (isPathOwnedByOtherPlatform(entry, platformType))
|
|
38
39
|
continue;
|
|
40
|
+
if (isPathOwnedByDisabledTUI(entry, enabledTUIs))
|
|
41
|
+
continue;
|
|
39
42
|
if (!allExisting.includes(entry)) {
|
|
40
43
|
config.files.managed.push(entry);
|
|
41
44
|
added.managed.push(entry);
|
|
@@ -44,6 +47,8 @@ function syncFileRegistry(config, platformType) {
|
|
|
44
47
|
for (const entry of defaults.files.merged) {
|
|
45
48
|
if (isPathOwnedByOtherPlatform(entry, platformType))
|
|
46
49
|
continue;
|
|
50
|
+
if (isPathOwnedByDisabledTUI(entry, enabledTUIs))
|
|
51
|
+
continue;
|
|
47
52
|
if (!allExisting.includes(entry)) {
|
|
48
53
|
config.files.merged.push(entry);
|
|
49
54
|
added.merged.push(entry);
|
|
@@ -82,6 +87,7 @@ async function cmdUpdate() {
|
|
|
82
87
|
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
83
88
|
const { project, org, language } = config;
|
|
84
89
|
const platformType = config.platform?.type || defaults.platform.type;
|
|
90
|
+
const enabledTUIs = resolveEnabledTUIs(config.tuis);
|
|
85
91
|
const replacements = { project, org };
|
|
86
92
|
info(`Updating seed files for: ${project}`);
|
|
87
93
|
console.log('');
|
|
@@ -106,20 +112,27 @@ async function cmdUpdate() {
|
|
|
106
112
|
catch {
|
|
107
113
|
// Ignore missing legacy script from pre-ESM installs.
|
|
108
114
|
}
|
|
109
|
-
// update Claude command
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
// update Claude command (only if enabled)
|
|
116
|
+
if (enabledTUIs.has('claude-code')) {
|
|
117
|
+
renderFile(path.join(templateDir, '.claude', 'commands', claudeSrc), path.join('.claude', 'commands', 'update-agent-infra.md'), replacements);
|
|
118
|
+
ok('Updated .claude/commands/update-agent-infra.md');
|
|
119
|
+
}
|
|
120
|
+
// update Gemini command (only if enabled)
|
|
121
|
+
if (enabledTUIs.has('gemini-cli')) {
|
|
122
|
+
renderFile(path.join(templateDir, '.gemini', 'commands', '_project_', geminiSrc), path.join('.gemini', 'commands', project, 'update-agent-infra.toml'), replacements);
|
|
123
|
+
ok(`Updated .gemini/commands/${project}/update-agent-infra.toml`);
|
|
124
|
+
}
|
|
125
|
+
// update OpenCode command (only if enabled)
|
|
126
|
+
if (enabledTUIs.has('opencode')) {
|
|
127
|
+
renderFile(path.join(templateDir, '.opencode', 'commands', opencodeSrc), path.join('.opencode', 'commands', 'update-agent-infra.md'), replacements);
|
|
128
|
+
ok('Updated .opencode/commands/update-agent-infra.md');
|
|
129
|
+
}
|
|
118
130
|
// sync file registry
|
|
119
|
-
const { added, changed } = syncFileRegistry(config, platformType);
|
|
131
|
+
const { added, changed } = syncFileRegistry(config, platformType, enabledTUIs);
|
|
120
132
|
const hasNewEntries = added.managed.length > 0 || added.merged.length > 0;
|
|
121
133
|
const platformAdded = !config.platform;
|
|
122
134
|
const sandboxAdded = !config.sandbox;
|
|
135
|
+
const taskAdded = !config.task;
|
|
123
136
|
const labelsAdded = !config.labels;
|
|
124
137
|
const requiresPullRequestAdded = config.requiresPullRequest === undefined;
|
|
125
138
|
let configChanged = changed;
|
|
@@ -131,6 +144,10 @@ async function cmdUpdate() {
|
|
|
131
144
|
config.sandbox = structuredClone(defaults.sandbox);
|
|
132
145
|
configChanged = true;
|
|
133
146
|
}
|
|
147
|
+
if (taskAdded) {
|
|
148
|
+
config.task = structuredClone(defaults.task);
|
|
149
|
+
configChanged = true;
|
|
150
|
+
}
|
|
134
151
|
if (labelsAdded) {
|
|
135
152
|
config.labels = structuredClone(defaults.labels);
|
|
136
153
|
configChanged = true;
|
|
@@ -150,13 +167,16 @@ async function cmdUpdate() {
|
|
|
150
167
|
ok(` merged: ${entry}`);
|
|
151
168
|
}
|
|
152
169
|
}
|
|
153
|
-
else if (platformAdded || sandboxAdded || labelsAdded || requiresPullRequestAdded) {
|
|
170
|
+
else if (platformAdded || sandboxAdded || taskAdded || labelsAdded || requiresPullRequestAdded) {
|
|
154
171
|
if (platformAdded) {
|
|
155
172
|
info(`Default platform config added to ${CONFIG_PATH}.`);
|
|
156
173
|
}
|
|
157
174
|
if (sandboxAdded) {
|
|
158
175
|
info(`Default sandbox config added to ${CONFIG_PATH}.`);
|
|
159
176
|
}
|
|
177
|
+
if (taskAdded) {
|
|
178
|
+
info(`Default task.shortIdLength=${defaults.task.shortIdLength} added to ${CONFIG_PATH}.`);
|
|
179
|
+
}
|
|
160
180
|
if (labelsAdded) {
|
|
161
181
|
info(`Default labels.in config added to ${CONFIG_PATH}.`);
|
|
162
182
|
}
|
|
@@ -170,6 +190,9 @@ async function cmdUpdate() {
|
|
|
170
190
|
if (hasNewEntries && sandboxAdded) {
|
|
171
191
|
info(`Default sandbox config added to ${CONFIG_PATH}.`);
|
|
172
192
|
}
|
|
193
|
+
if (hasNewEntries && taskAdded) {
|
|
194
|
+
info(`Default task.shortIdLength=${defaults.task.shortIdLength} added to ${CONFIG_PATH}.`);
|
|
195
|
+
}
|
|
173
196
|
if (hasNewEntries && labelsAdded) {
|
|
174
197
|
info(`Default labels.in config added to ${CONFIG_PATH}.`);
|
|
175
198
|
}
|
|
@@ -186,12 +209,30 @@ async function cmdUpdate() {
|
|
|
186
209
|
console.log('');
|
|
187
210
|
ok('Seed files updated successfully!');
|
|
188
211
|
console.log('');
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
212
|
+
if (enabledTUIs.size === 0) {
|
|
213
|
+
console.log(' No built-in TUI enabled (tuis: []).');
|
|
214
|
+
console.log(` Configure "customTUIs" in ${CONFIG_PATH} if needed.`);
|
|
215
|
+
console.log('');
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
console.log(' Next step: run the full update in your AI TUI:');
|
|
219
|
+
console.log('');
|
|
220
|
+
const claudeOrOpencode = [];
|
|
221
|
+
if (enabledTUIs.has('claude-code'))
|
|
222
|
+
claudeOrOpencode.push('Claude Code');
|
|
223
|
+
if (enabledTUIs.has('opencode'))
|
|
224
|
+
claudeOrOpencode.push('OpenCode');
|
|
225
|
+
if (claudeOrOpencode.length > 0) {
|
|
226
|
+
console.log(` ${claudeOrOpencode.join(' / ')}: /update-agent-infra`);
|
|
227
|
+
}
|
|
228
|
+
if (enabledTUIs.has('gemini-cli')) {
|
|
229
|
+
console.log(` Gemini CLI: /${project}:update-agent-infra`);
|
|
230
|
+
}
|
|
231
|
+
if (enabledTUIs.has('codex')) {
|
|
232
|
+
console.log(' Codex CLI: $update-agent-infra');
|
|
233
|
+
}
|
|
234
|
+
console.log('');
|
|
235
|
+
}
|
|
195
236
|
}
|
|
196
237
|
export { cmdUpdate };
|
|
197
238
|
//# sourceMappingURL=update.js.map
|