@fitlab-ai/agent-infra 0.5.6 → 0.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -4
- package/README.zh-CN.md +92 -4
- package/bin/cli.js +28 -4
- package/lib/defaults.json +5 -2
- package/lib/init.js +86 -5
- package/lib/prompt.js +28 -1
- package/lib/render.js +1 -1
- package/lib/sandbox/commands/rm.js +6 -4
- package/lib/sandbox/commands/vm.js +43 -16
- package/lib/sandbox/config.js +5 -0
- package/lib/sandbox/engine.js +144 -16
- package/lib/sandbox/shell.js +36 -2
- package/lib/sandbox/task-resolver.js +13 -6
- package/lib/update.js +14 -3
- package/package.json +5 -5
- package/templates/.agents/QUICKSTART.en.md +19 -2
- package/templates/.agents/QUICKSTART.zh-CN.md +19 -2
- package/templates/.agents/README.en.md +71 -2
- package/templates/.agents/README.zh-CN.md +71 -2
- package/templates/.agents/rules/issue-pr-commands.en.md +5 -0
- package/templates/.agents/rules/issue-pr-commands.github.en.md +60 -0
- package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +60 -0
- package/templates/.agents/rules/issue-pr-commands.zh-CN.md +5 -0
- package/templates/.agents/rules/issue-sync.en.md +19 -0
- package/templates/.agents/rules/issue-sync.github.en.md +14 -0
- package/templates/.agents/rules/issue-sync.github.zh-CN.md +14 -0
- package/templates/.agents/rules/issue-sync.zh-CN.md +19 -0
- package/templates/.agents/rules/label-milestone-setup.en.md +5 -0
- package/templates/.agents/rules/label-milestone-setup.github.en.md +10 -0
- package/templates/.agents/rules/label-milestone-setup.github.zh-CN.md +10 -0
- package/templates/.agents/rules/label-milestone-setup.zh-CN.md +5 -0
- package/templates/.agents/rules/milestone-inference.en.md +5 -0
- package/templates/.agents/rules/milestone-inference.zh-CN.md +5 -0
- package/templates/.agents/rules/pr-sync.en.md +5 -0
- package/templates/.agents/rules/pr-sync.zh-CN.md +5 -0
- package/templates/.agents/rules/release-commands.en.md +5 -0
- package/templates/.agents/rules/release-commands.github.en.md +16 -0
- package/templates/.agents/rules/release-commands.github.zh-CN.md +16 -0
- package/templates/.agents/rules/release-commands.zh-CN.md +5 -0
- package/templates/.agents/rules/security-alerts.en.md +5 -0
- package/templates/.agents/rules/security-alerts.zh-CN.md +5 -0
- package/templates/.agents/scripts/platform-adapters/find-existing-task.github.js +272 -0
- package/templates/.agents/scripts/platform-adapters/find-existing-task.js +5 -0
- package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +88 -8
- package/templates/.agents/scripts/platform-adapters/platform-sync.js +13 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +5 -5
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/analyze-task/config/verify.json +3 -1
- package/templates/.agents/skills/block-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/block-task/config/verify.json +2 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/cancel-task/config/verify.json +2 -1
- package/templates/.agents/skills/check-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/check-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/close-codescan/SKILL.en.md +3 -3
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +1 -1
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/SKILL.en.md +2 -2
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/commit/config/verify.json +2 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-task/config/verify.json +2 -1
- package/templates/.agents/skills/create-issue/SKILL.en.md +10 -10
- package/templates/.agents/skills/create-issue/SKILL.zh-CN.md +10 -10
- package/templates/.agents/skills/create-issue/config/verify.json +2 -1
- package/templates/.agents/skills/create-issue/reference/label-and-type.en.md +3 -3
- package/templates/.agents/skills/create-issue/reference/label-and-type.zh-CN.md +3 -3
- package/templates/.agents/skills/create-issue/reference/template-matching.en.md +6 -34
- package/templates/.agents/skills/create-issue/reference/template-matching.zh-CN.md +8 -36
- package/templates/.agents/skills/create-pr/SKILL.en.md +3 -3
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/create-pr/config/verify.json +2 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +7 -17
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +27 -37
- package/templates/.agents/skills/create-release-note/SKILL.en.md +16 -9
- package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +16 -9
- package/templates/.agents/skills/create-task/SKILL.en.md +5 -5
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/implement-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/implement-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/implement-task/config/verify.json +3 -1
- package/templates/.agents/skills/import-codescan/SKILL.en.md +3 -3
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -2
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/import-issue/SKILL.en.md +41 -11
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +41 -11
- package/templates/.agents/skills/init-labels/SKILL.en.md +10 -10
- package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +10 -10
- package/templates/.agents/skills/init-labels/scripts/init-labels.sh +6 -0
- package/templates/.agents/skills/init-milestones/SKILL.en.md +8 -8
- package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +8 -8
- package/templates/.agents/skills/init-milestones/scripts/init-milestones.sh +6 -0
- package/templates/.agents/skills/plan-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/plan-task/config/verify.json +3 -1
- package/templates/.agents/skills/post-release/SKILL.en.md +95 -0
- package/templates/.agents/skills/post-release/SKILL.zh-CN.md +95 -0
- package/templates/.agents/skills/refine-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/refine-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/refine-task/config/verify.json +3 -1
- package/templates/.agents/skills/refine-title/SKILL.en.md +1 -1
- package/templates/.agents/skills/refine-title/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/release/SKILL.en.md +6 -1
- package/templates/.agents/skills/release/SKILL.zh-CN.md +6 -1
- package/templates/.agents/skills/release/scripts/manage-milestones.sh +6 -0
- package/templates/.agents/skills/restore-task/SKILL.en.md +13 -64
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +13 -64
- package/templates/.agents/skills/review-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/review-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/review-task/config/verify.json +3 -1
- package/templates/.agents/skills/test/SKILL.en.md +1 -1
- package/templates/.agents/skills/test/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/test-integration/SKILL.en.md +1 -1
- package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/update-agent-infra/SKILL.en.md +12 -2
- package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +344 -16
- package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +1 -1
- package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +1 -1
- package/templates/.agents/templates/task.en.md +2 -2
- package/templates/.agents/templates/task.zh-CN.md +2 -2
- package/templates/.claude/commands/create-issue.en.md +1 -1
- package/templates/.claude/commands/create-issue.zh-CN.md +1 -1
- package/templates/.claude/commands/import-issue.en.md +1 -1
- package/templates/.claude/commands/import-issue.zh-CN.md +1 -1
- package/templates/.claude/commands/init-labels.en.md +1 -1
- package/templates/.claude/commands/init-labels.zh-CN.md +1 -1
- package/templates/.claude/commands/init-milestones.en.md +1 -1
- package/templates/.claude/commands/init-milestones.zh-CN.md +1 -1
- package/templates/.claude/commands/post-release.en.md +8 -0
- package/templates/.claude/commands/post-release.zh-CN.md +8 -0
- package/templates/.claude/commands/restore-task.en.md +1 -1
- package/templates/.claude/commands/restore-task.zh-CN.md +1 -1
- package/templates/.claude/hooks/check-version-format.sh +1 -1
- package/templates/.gemini/commands/_project_/create-issue.en.toml +1 -1
- package/templates/.gemini/commands/_project_/create-issue.zh-CN.toml +1 -1
- package/templates/.gemini/commands/_project_/import-issue.en.toml +1 -1
- package/templates/.gemini/commands/_project_/import-issue.zh-CN.toml +1 -1
- package/templates/.gemini/commands/_project_/init-labels.en.toml +2 -2
- package/templates/.gemini/commands/_project_/init-labels.zh-CN.toml +2 -2
- package/templates/.gemini/commands/_project_/init-milestones.en.toml +2 -2
- package/templates/.gemini/commands/_project_/init-milestones.zh-CN.toml +2 -2
- package/templates/.gemini/commands/_project_/post-release.en.toml +6 -0
- package/templates/.gemini/commands/_project_/post-release.zh-CN.toml +6 -0
- package/templates/.gemini/commands/_project_/restore-task.en.toml +1 -1
- package/templates/.gemini/commands/_project_/restore-task.zh-CN.toml +1 -1
- package/templates/{.github/hooks → .git-hooks}/check-version-format.sh +2 -2
- package/templates/.github/workflows/pr-label.yml +1 -1
- package/templates/.opencode/commands/create-issue.en.md +1 -1
- package/templates/.opencode/commands/create-issue.zh-CN.md +1 -1
- package/templates/.opencode/commands/import-issue.en.md +1 -1
- package/templates/.opencode/commands/import-issue.zh-CN.md +1 -1
- package/templates/.opencode/commands/init-labels.en.md +1 -1
- package/templates/.opencode/commands/init-labels.zh-CN.md +1 -1
- package/templates/.opencode/commands/init-milestones.en.md +1 -1
- package/templates/.opencode/commands/init-milestones.zh-CN.md +1 -1
- package/templates/.opencode/commands/post-release.en.md +9 -0
- package/templates/.opencode/commands/post-release.zh-CN.md +9 -0
- package/templates/.opencode/commands/restore-task.en.md +1 -1
- package/templates/.opencode/commands/restore-task.zh-CN.md +1 -1
- /package/templates/{.github/hooks → .git-hooks}/pre-commit +0 -0
|
@@ -3,31 +3,49 @@ import * as p from '@clack/prompts';
|
|
|
3
3
|
import pc from 'picocolors';
|
|
4
4
|
import { loadConfig } from '../config.js';
|
|
5
5
|
import { parsePositiveIntegerOption } from '../constants.js';
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import {
|
|
7
|
+
ENGINES,
|
|
8
|
+
detectEngine,
|
|
9
|
+
engineDisplayName,
|
|
10
|
+
ensureDocker,
|
|
11
|
+
isManagedEngine,
|
|
12
|
+
stopManagedVm
|
|
13
|
+
} from '../engine.js';
|
|
14
|
+
import { runOk, runSafe } from '../shell.js';
|
|
8
15
|
|
|
9
16
|
const USAGE = `Usage: ai sandbox vm <status|start|stop> [--cpu <n>] [--memory <n>]`;
|
|
10
17
|
|
|
11
|
-
function ensureManagedVm() {
|
|
12
|
-
if (!
|
|
13
|
-
throw new Error(`VM management is unavailable
|
|
18
|
+
function ensureManagedVm(engine) {
|
|
19
|
+
if (!isManagedEngine(engine)) {
|
|
20
|
+
throw new Error(`VM management is unavailable for engine '${engineDisplayName(engine)}'.`);
|
|
14
21
|
}
|
|
15
22
|
}
|
|
16
23
|
|
|
17
24
|
function status() {
|
|
18
|
-
|
|
25
|
+
const config = loadConfig();
|
|
26
|
+
const engine = detectEngine(config);
|
|
27
|
+
const name = engineDisplayName(engine);
|
|
28
|
+
ensureManagedVm(engine);
|
|
19
29
|
p.intro(pc.cyan('Sandbox VM status'));
|
|
20
30
|
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
if (engine === ENGINES.COLIMA) {
|
|
32
|
+
if (runOk('colima', ['status'])) {
|
|
33
|
+
process.stdout.write(`${runSafe('colima', ['status'])}\n`);
|
|
34
|
+
} else {
|
|
35
|
+
p.log.warn('Colima VM is not running');
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!runOk('orb', ['status'])) {
|
|
41
|
+
p.log.warn(`${name} VM is not running`);
|
|
42
|
+
return;
|
|
25
43
|
}
|
|
44
|
+
|
|
45
|
+
process.stdout.write(`${runSafe('orb', ['status'])}\n`);
|
|
26
46
|
}
|
|
27
47
|
|
|
28
48
|
async function start(args) {
|
|
29
|
-
ensureManagedVm();
|
|
30
|
-
|
|
31
49
|
const { values } = parseArgs({
|
|
32
50
|
args,
|
|
33
51
|
allowPositionals: true,
|
|
@@ -45,6 +63,8 @@ async function start(args) {
|
|
|
45
63
|
}
|
|
46
64
|
|
|
47
65
|
const config = loadConfig();
|
|
66
|
+
const engine = detectEngine(config);
|
|
67
|
+
ensureManagedVm(engine);
|
|
48
68
|
const effectiveConfig = {
|
|
49
69
|
...config,
|
|
50
70
|
vm: {
|
|
@@ -62,15 +82,22 @@ async function start(args) {
|
|
|
62
82
|
}
|
|
63
83
|
|
|
64
84
|
function stop() {
|
|
65
|
-
|
|
85
|
+
const config = loadConfig();
|
|
86
|
+
const engine = detectEngine(config);
|
|
87
|
+
const name = engineDisplayName(engine);
|
|
88
|
+
ensureManagedVm(engine);
|
|
66
89
|
p.intro(pc.cyan('Stopping sandbox VM'));
|
|
67
90
|
|
|
68
|
-
if (!runOk('colima', ['status'])) {
|
|
69
|
-
p.log.warn(
|
|
91
|
+
if (engine === ENGINES.COLIMA && !runOk('colima', ['status'])) {
|
|
92
|
+
p.log.warn(`${name} VM is not running`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (engine === ENGINES.ORBSTACK && !runOk('orb', ['status'])) {
|
|
96
|
+
p.log.warn(`${name} VM is not running`);
|
|
70
97
|
return;
|
|
71
98
|
}
|
|
72
99
|
|
|
73
|
-
|
|
100
|
+
stopManagedVm(config);
|
|
74
101
|
p.outro(pc.green('VM stopped'));
|
|
75
102
|
}
|
|
76
103
|
|
package/lib/sandbox/config.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { validateSandboxEngine } from './engine.js';
|
|
4
5
|
|
|
5
6
|
const DEFAULTS = Object.freeze({
|
|
7
|
+
engine: null,
|
|
6
8
|
runtimes: ['node20'],
|
|
7
9
|
tools: ['claude-code', 'codex', 'opencode', 'gemini-cli'],
|
|
8
10
|
dockerfile: null,
|
|
@@ -26,6 +28,7 @@ function detectRepoRoot() {
|
|
|
26
28
|
|
|
27
29
|
function cloneDefaults() {
|
|
28
30
|
return {
|
|
31
|
+
engine: DEFAULTS.engine,
|
|
29
32
|
runtimes: [...DEFAULTS.runtimes],
|
|
30
33
|
tools: [...DEFAULTS.tools],
|
|
31
34
|
dockerfile: DEFAULTS.dockerfile,
|
|
@@ -49,6 +52,7 @@ export function loadConfig() {
|
|
|
49
52
|
const airc = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
50
53
|
const defaults = cloneDefaults();
|
|
51
54
|
const sandbox = airc.sandbox ?? {};
|
|
55
|
+
const engine = validateSandboxEngine(sandbox.engine ?? defaults.engine);
|
|
52
56
|
const project = airc.project;
|
|
53
57
|
|
|
54
58
|
if (!project || typeof project !== 'string') {
|
|
@@ -64,6 +68,7 @@ export function loadConfig() {
|
|
|
64
68
|
containerPrefix: `${project}-dev`,
|
|
65
69
|
imageName: `${project}-sandbox:latest`,
|
|
66
70
|
worktreeBase: path.join(home, '.agent-infra', 'worktrees', project),
|
|
71
|
+
engine,
|
|
67
72
|
runtimes: Array.isArray(sandbox.runtimes) && sandbox.runtimes.length > 0
|
|
68
73
|
? [...sandbox.runtimes]
|
|
69
74
|
: defaults.runtimes,
|
package/lib/sandbox/engine.js
CHANGED
|
@@ -2,26 +2,69 @@ import { platform } from 'node:os';
|
|
|
2
2
|
import { detectHostResources } from './constants.js';
|
|
3
3
|
import { run, runOk, runSafe, runVerbose } from './shell.js';
|
|
4
4
|
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
export const ENGINES = Object.freeze({
|
|
6
|
+
COLIMA: 'colima',
|
|
7
|
+
ORBSTACK: 'orbstack',
|
|
8
|
+
DOCKER_DESKTOP: 'docker-desktop'
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const ENGINE_DOCKER_CONTEXT = Object.freeze({
|
|
12
|
+
[ENGINES.COLIMA]: 'colima',
|
|
13
|
+
[ENGINES.ORBSTACK]: 'orbstack',
|
|
14
|
+
[ENGINES.DOCKER_DESKTOP]: 'desktop-linux'
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const VALID_CONFIG_ENGINES = new Set(Object.values(ENGINES));
|
|
18
|
+
|
|
19
|
+
function applyDockerContext(engine) {
|
|
20
|
+
const context = ENGINE_DOCKER_CONTEXT[engine];
|
|
21
|
+
if (context) {
|
|
22
|
+
process.env.DOCKER_CONTEXT = context;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function validateSandboxEngine(engine) {
|
|
27
|
+
if (engine === null || engine === undefined) {
|
|
28
|
+
return null;
|
|
9
29
|
}
|
|
30
|
+
|
|
31
|
+
if (VALID_CONFIG_ENGINES.has(engine)) {
|
|
32
|
+
return engine;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
throw new Error(
|
|
36
|
+
`sandbox: invalid "sandbox.engine" value "${engine}". `
|
|
37
|
+
+ 'Expected one of: null, colima, orbstack, docker-desktop. '
|
|
38
|
+
+ 'This setting only affects macOS sandbox engine selection.'
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function detectEngine(config = {}, { platformFn = platform } = {}) {
|
|
43
|
+
const configured = validateSandboxEngine(config.engine);
|
|
44
|
+
const os = platformFn();
|
|
10
45
|
if (os === 'linux') {
|
|
11
46
|
return 'native';
|
|
12
47
|
}
|
|
13
48
|
if (os === 'win32') {
|
|
14
49
|
return 'wsl2';
|
|
15
50
|
}
|
|
51
|
+
if (os === 'darwin') {
|
|
52
|
+
if (configured) {
|
|
53
|
+
return configured;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return ENGINES.COLIMA;
|
|
57
|
+
}
|
|
16
58
|
return 'unsupported';
|
|
17
59
|
}
|
|
18
60
|
|
|
19
61
|
function colimaArgs(config, runSafeFn = runSafe) {
|
|
20
62
|
const arch = runSafeFn('uname', ['-m']);
|
|
21
63
|
const defaults = detectHostResources();
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
64
|
+
const vm = config.vm ?? {};
|
|
65
|
+
const cpu = vm.cpu ?? defaults.cpu;
|
|
66
|
+
const memory = vm.memory ?? defaults.memory;
|
|
67
|
+
const disk = vm.disk ?? 60;
|
|
25
68
|
const args = ['start', '--cpu', String(cpu), '--memory', String(memory), '--disk', String(disk)];
|
|
26
69
|
|
|
27
70
|
if (arch === 'arm64') {
|
|
@@ -38,6 +81,8 @@ export async function ensureColima(
|
|
|
38
81
|
onMessage,
|
|
39
82
|
{ runOkFn = runOk, runSafeFn = runSafe, runVerboseFn = runVerbose } = {}
|
|
40
83
|
) {
|
|
84
|
+
applyDockerContext(ENGINES.COLIMA);
|
|
85
|
+
|
|
41
86
|
if (!runOkFn('which', ['colima'])) {
|
|
42
87
|
onMessage?.('Installing colima + docker via Homebrew...');
|
|
43
88
|
runVerboseFn('brew', ['install', 'colima', 'docker']);
|
|
@@ -53,14 +98,58 @@ export async function ensureColima(
|
|
|
53
98
|
}
|
|
54
99
|
}
|
|
55
100
|
|
|
101
|
+
export async function ensureOrbStack(
|
|
102
|
+
_config,
|
|
103
|
+
onMessage,
|
|
104
|
+
{ runOkFn = runOk, runVerboseFn = runVerbose } = {}
|
|
105
|
+
) {
|
|
106
|
+
applyDockerContext(ENGINES.ORBSTACK);
|
|
107
|
+
|
|
108
|
+
if (!runOkFn('which', ['orb'])) {
|
|
109
|
+
onMessage?.('Installing OrbStack via Homebrew...');
|
|
110
|
+
runVerboseFn('brew', ['install', '--cask', 'orbstack']);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!runOkFn('docker', ['info'])) {
|
|
114
|
+
onMessage?.('Starting OrbStack...');
|
|
115
|
+
runVerboseFn('orb', ['start']);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!runOkFn('docker', ['info'])) {
|
|
119
|
+
throw new Error('Docker daemon is not available after starting OrbStack');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function ensureDockerDesktop(
|
|
124
|
+
_config,
|
|
125
|
+
onMessage,
|
|
126
|
+
{ runOkFn = runOk } = {}
|
|
127
|
+
) {
|
|
128
|
+
applyDockerContext(ENGINES.DOCKER_DESKTOP);
|
|
129
|
+
|
|
130
|
+
if (!runOkFn('docker', ['info'])) {
|
|
131
|
+
throw new Error('Docker Desktop is not running. Please start Docker Desktop manually.');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
56
135
|
export async function ensureDocker(config, onMessage) {
|
|
57
|
-
const engine = detectEngine();
|
|
136
|
+
const engine = detectEngine(config);
|
|
58
137
|
|
|
59
|
-
if (engine ===
|
|
138
|
+
if (engine === ENGINES.COLIMA) {
|
|
60
139
|
await ensureColima(config, onMessage);
|
|
61
140
|
return;
|
|
62
141
|
}
|
|
63
142
|
|
|
143
|
+
if (engine === ENGINES.ORBSTACK) {
|
|
144
|
+
await ensureOrbStack(config, onMessage);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (engine === ENGINES.DOCKER_DESKTOP) {
|
|
149
|
+
await ensureDockerDesktop(config, onMessage);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
64
153
|
if (engine === 'native') {
|
|
65
154
|
if (!runOk('docker', ['info'])) {
|
|
66
155
|
throw new Error('Docker daemon is not running. Please start Docker first.');
|
|
@@ -75,19 +164,58 @@ export async function ensureDocker(config, onMessage) {
|
|
|
75
164
|
throw new Error(`Unsupported sandbox engine: ${engine}`);
|
|
76
165
|
}
|
|
77
166
|
|
|
78
|
-
export function isVmManaged() {
|
|
79
|
-
|
|
167
|
+
export function isVmManaged(config = {}, dependencies = {}) {
|
|
168
|
+
const engine = detectEngine(config, dependencies);
|
|
169
|
+
return isManagedEngine(engine);
|
|
80
170
|
}
|
|
81
171
|
|
|
82
|
-
export function
|
|
83
|
-
|
|
84
|
-
|
|
172
|
+
export function isManagedEngine(engine) {
|
|
173
|
+
return engine === ENGINES.COLIMA || engine === ENGINES.ORBSTACK;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function engineDisplayName(engine) {
|
|
177
|
+
const names = {
|
|
178
|
+
[ENGINES.COLIMA]: 'Colima',
|
|
179
|
+
[ENGINES.ORBSTACK]: 'OrbStack',
|
|
180
|
+
[ENGINES.DOCKER_DESKTOP]: 'Docker Desktop',
|
|
181
|
+
native: 'native Docker',
|
|
182
|
+
wsl2: 'WSL2'
|
|
183
|
+
};
|
|
184
|
+
return names[engine] ?? engine;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function startManagedVm(
|
|
188
|
+
config,
|
|
189
|
+
{ platformFn = platform, runOkFn = runOk, runVerboseFn = runVerbose } = {}
|
|
190
|
+
) {
|
|
191
|
+
const engine = detectEngine(config, { platformFn });
|
|
192
|
+
if (!isManagedEngine(engine)) {
|
|
193
|
+
throw new Error(`VM management is unavailable for engine '${engineDisplayName(engine)}'.`);
|
|
85
194
|
}
|
|
86
195
|
|
|
87
|
-
if (
|
|
196
|
+
if (engine === ENGINES.COLIMA && runOkFn('colima', ['status'])) {
|
|
197
|
+
return 'already-running';
|
|
198
|
+
}
|
|
199
|
+
if (engine === ENGINES.ORBSTACK && runOkFn('orb', ['status'])) {
|
|
88
200
|
return 'already-running';
|
|
89
201
|
}
|
|
90
202
|
|
|
91
|
-
|
|
203
|
+
if (engine === ENGINES.COLIMA) {
|
|
204
|
+
runVerboseFn('colima', colimaArgs(config));
|
|
205
|
+
} else if (engine === ENGINES.ORBSTACK) {
|
|
206
|
+
runVerboseFn('orb', ['start']);
|
|
207
|
+
}
|
|
92
208
|
return 'started';
|
|
93
209
|
}
|
|
210
|
+
|
|
211
|
+
export function stopManagedVm(config, { platformFn = platform, runFn = run } = {}) {
|
|
212
|
+
const engine = detectEngine(config, { platformFn });
|
|
213
|
+
if (engine === ENGINES.COLIMA) {
|
|
214
|
+
runFn('colima', ['stop']);
|
|
215
|
+
return 'stopped';
|
|
216
|
+
} else if (engine === ENGINES.ORBSTACK) {
|
|
217
|
+
runFn('orb', ['stop']);
|
|
218
|
+
return 'stopped';
|
|
219
|
+
}
|
|
220
|
+
throw new Error(`VM management is unavailable for engine '${engineDisplayName(engine)}'.`);
|
|
221
|
+
}
|
package/lib/sandbox/shell.js
CHANGED
|
@@ -60,10 +60,44 @@ export function runOk(cmd, args, opts = {}) {
|
|
|
60
60
|
return result.status === 0;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
export function restoreTerminal() {
|
|
64
|
+
if (!process.stdout.isTTY) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
process.stdout.write([
|
|
70
|
+
'\x1b[?1049l',
|
|
71
|
+
'\x1b[?25h',
|
|
72
|
+
'\x1b>',
|
|
73
|
+
'\x1b[?1000l',
|
|
74
|
+
'\x1b[?1002l',
|
|
75
|
+
'\x1b[?1003l',
|
|
76
|
+
'\x1b[?1006l'
|
|
77
|
+
].join(''));
|
|
78
|
+
} catch {
|
|
79
|
+
// Best-effort cleanup only; preserve the original command result.
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (process.platform === 'win32') {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
execFileSync('stty', ['sane'], { stdio: 'inherit' });
|
|
88
|
+
} catch {
|
|
89
|
+
// Some environments do not provide stty or reject sane; ANSI reset still helps.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
63
93
|
export function runInteractive(cmd, args, opts = {}) {
|
|
64
94
|
const resolved = resolveCommand(cmd);
|
|
65
|
-
|
|
66
|
-
|
|
95
|
+
try {
|
|
96
|
+
const result = spawnSync(resolved, args, commandOptions(resolved, normalizeOptions(opts, 'inherit')));
|
|
97
|
+
return result.status ?? 1;
|
|
98
|
+
} finally {
|
|
99
|
+
restoreTerminal();
|
|
100
|
+
}
|
|
67
101
|
}
|
|
68
102
|
|
|
69
103
|
export function runVerbose(cmd, args, opts = {}) {
|
|
@@ -2,24 +2,31 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
4
|
const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
|
|
5
|
+
const WORKSPACE_DIRS = ['active', 'completed', 'blocked', 'archive'];
|
|
6
|
+
|
|
7
|
+
function stripQuotes(value) {
|
|
8
|
+
return value.replace(/^(["'])(.*)\1$/, '$2');
|
|
9
|
+
}
|
|
5
10
|
|
|
6
11
|
function readTaskContent(repoRoot, taskId) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
12
|
+
for (const dir of WORKSPACE_DIRS) {
|
|
13
|
+
const taskPath = path.join(repoRoot, '.agents', 'workspace', dir, taskId, 'task.md');
|
|
14
|
+
if (fs.existsSync(taskPath)) {
|
|
15
|
+
return fs.readFileSync(taskPath, 'utf8');
|
|
16
|
+
}
|
|
10
17
|
}
|
|
11
|
-
|
|
18
|
+
throw new Error(`Task not found: ${taskId}`);
|
|
12
19
|
}
|
|
13
20
|
|
|
14
21
|
function resolveBranchFromTaskContent(content, taskId) {
|
|
15
22
|
const frontmatterBranch = content.match(/^branch:\s*(.+)$/m);
|
|
16
23
|
if (frontmatterBranch && frontmatterBranch[1].trim()) {
|
|
17
|
-
return frontmatterBranch[1].trim();
|
|
24
|
+
return stripQuotes(frontmatterBranch[1].trim());
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
const contextBranch = content.match(/^- \*\*(?:分支|Branch)\*\*:[ \t]*`?([^`\n]+)`?$/m);
|
|
21
28
|
if (contextBranch && contextBranch[1].trim()) {
|
|
22
|
-
return contextBranch[1].trim();
|
|
29
|
+
return stripQuotes(contextBranch[1].trim());
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
throw new Error(`Task ${taskId} has no branch field in task.md`);
|
package/lib/update.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { info, ok, err } from './log.js';
|
|
4
4
|
import { resolveTemplateDir } from './paths.js';
|
|
5
|
-
import { renderFile, copySkillDir } from './render.js';
|
|
5
|
+
import { renderFile, copySkillDir, KNOWN_PLATFORMS } from './render.js';
|
|
6
6
|
|
|
7
7
|
const defaults = JSON.parse(
|
|
8
8
|
fs.readFileSync(new URL('./defaults.json', import.meta.url), 'utf8')
|
|
@@ -11,7 +11,16 @@ const defaults = JSON.parse(
|
|
|
11
11
|
const CONFIG_DIR = '.agents';
|
|
12
12
|
const CONFIG_PATH = path.join(CONFIG_DIR, '.airc.json');
|
|
13
13
|
|
|
14
|
-
function
|
|
14
|
+
function isPathOwnedByOtherPlatform(relativePath, platformType) {
|
|
15
|
+
const top = String(relativePath || '').replace(/\\/g, '/').replace(/^\.\//, '').split('/')[0];
|
|
16
|
+
if (!top.startsWith('.')) return false;
|
|
17
|
+
|
|
18
|
+
const candidate = top.slice(1);
|
|
19
|
+
if (!KNOWN_PLATFORMS.has(candidate)) return false;
|
|
20
|
+
return candidate !== platformType;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function syncFileRegistry(config, platformType) {
|
|
15
24
|
config.files ||= {};
|
|
16
25
|
const before = JSON.stringify({
|
|
17
26
|
files: {
|
|
@@ -32,12 +41,14 @@ function syncFileRegistry(config) {
|
|
|
32
41
|
const added = { managed: [], merged: [] };
|
|
33
42
|
|
|
34
43
|
for (const entry of defaults.files.managed) {
|
|
44
|
+
if (isPathOwnedByOtherPlatform(entry, platformType)) continue;
|
|
35
45
|
if (!allExisting.includes(entry)) {
|
|
36
46
|
config.files.managed.push(entry);
|
|
37
47
|
added.managed.push(entry);
|
|
38
48
|
}
|
|
39
49
|
}
|
|
40
50
|
for (const entry of defaults.files.merged) {
|
|
51
|
+
if (isPathOwnedByOtherPlatform(entry, platformType)) continue;
|
|
41
52
|
if (!allExisting.includes(entry)) {
|
|
42
53
|
config.files.merged.push(entry);
|
|
43
54
|
added.merged.push(entry);
|
|
@@ -139,7 +150,7 @@ async function cmdUpdate() {
|
|
|
139
150
|
ok('Updated .opencode/commands/update-agent-infra.md');
|
|
140
151
|
|
|
141
152
|
// sync file registry
|
|
142
|
-
const { added, changed } = syncFileRegistry(config);
|
|
153
|
+
const { added, changed } = syncFileRegistry(config, platformType);
|
|
143
154
|
const hasNewEntries = added.managed.length > 0 || added.merged.length > 0;
|
|
144
155
|
const platformAdded = !config.platform;
|
|
145
156
|
const sandboxAdded = !config.sandbox;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fitlab-ai/agent-infra",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.8",
|
|
4
4
|
"description": "Bootstrap tool for AI multi-tool collaboration infrastructure — works with Claude Code, Codex, Gemini CLI, and OpenCode",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -46,9 +46,9 @@
|
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "node scripts/build-inline.js",
|
|
49
|
-
"demo:regen": "
|
|
50
|
-
"prepare": "git config core.hooksPath .
|
|
51
|
-
"test": "node scripts/build-inline.js --check && node --test tests/cli/*.test.js tests/templates/*.test.js tests/core/*.test.js",
|
|
52
|
-
"prepublishOnly": "node scripts/build-inline.js --check && node --test tests/cli/*.test.js tests/templates/*.test.js tests/core/*.test.js"
|
|
49
|
+
"demo:regen": "sh scripts/demo-regen.sh",
|
|
50
|
+
"prepare": "git config core.hooksPath .git-hooks || true",
|
|
51
|
+
"test": "node scripts/build-inline.js --check && node --test tests/cli/*.test.js tests/templates/*.test.js tests/core/*.test.js tests/scripts/*.test.js",
|
|
52
|
+
"prepublishOnly": "node scripts/build-inline.js --check && node --test tests/cli/*.test.js tests/templates/*.test.js tests/core/*.test.js tests/scripts/*.test.js"
|
|
53
53
|
}
|
|
54
54
|
}
|
|
@@ -13,10 +13,27 @@ This guide walks you through using multiple AI coding assistants together on a p
|
|
|
13
13
|
Enable the shared Git hooks path before relying on the template hook chain:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
git config core.hooksPath .
|
|
16
|
+
git config core.hooksPath .git-hooks
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
This makes Git invoke the hooks
|
|
19
|
+
This makes Git invoke the hooks in the project repository's `.git-hooks/` directory, including `pre-commit` and `check-version-format.sh`.
|
|
20
|
+
|
|
21
|
+
## External Templates And Skills
|
|
22
|
+
|
|
23
|
+
If your team maintains private platform templates or shared custom skills, configure local sources in `.agents/.airc.json`:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"templates": {
|
|
28
|
+
"sources": [{ "type": "local", "path": "~/private-templates" }]
|
|
29
|
+
},
|
|
30
|
+
"skills": {
|
|
31
|
+
"sources": [{ "type": "local", "path": "~/private-skills" }]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Built-in templates take priority over external templates. Later external template sources override earlier external sources, and `update-agent-infra` reports conflicts in `templateSources.conflicts`. Only use trusted local paths because external templates and skills may contain executable scripts.
|
|
20
37
|
|
|
21
38
|
## Creating Your First Task
|
|
22
39
|
|
|
@@ -13,10 +13,27 @@
|
|
|
13
13
|
在依赖模板中的 Git hook 链路前,先启用共享 hooks 路径:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
git config core.hooksPath .
|
|
16
|
+
git config core.hooksPath .git-hooks
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
这样 Git
|
|
19
|
+
这样 Git 才会调用项目仓库 `.git-hooks/` 目录下的 hook,包括 `pre-commit` 和 `check-version-format.sh`。
|
|
20
|
+
|
|
21
|
+
## 外部模板与 Skill
|
|
22
|
+
|
|
23
|
+
如果团队维护私有平台模板或共享自定义 skill,可在 `.agents/.airc.json` 中配置本地源:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"templates": {
|
|
28
|
+
"sources": [{ "type": "local", "path": "~/private-templates" }]
|
|
29
|
+
},
|
|
30
|
+
"skills": {
|
|
31
|
+
"sources": [{ "type": "local", "path": "~/private-skills" }]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
内置模板优先级高于外部模板。多个外部模板源之间,后面的 source 覆盖前面的 source,`update-agent-infra` 会在 `templateSources.conflicts` 中报告冲突。外部模板和 skill 可能包含可执行脚本,只使用可信本地路径。
|
|
20
37
|
|
|
21
38
|
## 创建第一个任务
|
|
22
39
|
|
|
@@ -108,7 +108,7 @@ This project uses the following collaboration label prefixes, each with a define
|
|
|
108
108
|
| `status:` | Yes | — | PRs already have their own state flow (Open / Draft / Merged / Closed); Issues use `status:` labels for project tracking states |
|
|
109
109
|
| `in:` | Yes | Yes | Both Issues and PRs can be filtered by module |
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
Run the `/init-labels` command to initialize these labels via the platform adapter.
|
|
112
112
|
|
|
113
113
|
## Private Platform Extensions
|
|
114
114
|
|
|
@@ -120,6 +120,27 @@ To adapt agent-infra to a private code-hosting platform:
|
|
|
120
120
|
4. If you maintain a fork of the template source, add matching `.{platform}.` template variants before adding that platform identifier to the sync logic.
|
|
121
121
|
5. Validate the customized workflow on a test task before rolling it out broadly.
|
|
122
122
|
|
|
123
|
+
## External Template And Skill Sources
|
|
124
|
+
|
|
125
|
+
Teams can configure external template sources and shared skill sources in `.agents/.airc.json` for private platform templates, private rules, and shared custom skills:
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"templates": {
|
|
130
|
+
"sources": [
|
|
131
|
+
{ "type": "local", "path": "~/private-templates" }
|
|
132
|
+
]
|
|
133
|
+
},
|
|
134
|
+
"skills": {
|
|
135
|
+
"sources": [
|
|
136
|
+
{ "type": "local", "path": "~/private-skills" }
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Built-in templates take priority, and external templates are supplemental. Between multiple external template sources, later sources override earlier sources. The sync report lists ignored same-path files in `templateSources.conflicts`. External templates and skills may contain scripts executed by AI workflows, so only configure trusted local paths.
|
|
143
|
+
|
|
123
144
|
## Custom Skills
|
|
124
145
|
|
|
125
146
|
Projects can add their own skills alongside the built-in task workflow.
|
|
@@ -156,7 +177,7 @@ To reuse centralized team skills, configure `.agents/.airc.json`:
|
|
|
156
177
|
{
|
|
157
178
|
"skills": {
|
|
158
179
|
"sources": [
|
|
159
|
-
{ "type": "local", "path": "~/
|
|
180
|
+
{ "type": "local", "path": "~/private-skills" }
|
|
160
181
|
]
|
|
161
182
|
}
|
|
162
183
|
}
|
|
@@ -172,6 +193,54 @@ Each source should mirror the `.agents/skills/` layout and include `SKILL.md` at
|
|
|
172
193
|
- Built-in skills are not overridable by custom sources; if a source skill name conflicts with a built-in skill, the source copy is skipped
|
|
173
194
|
- Use `files.ejected` if the project must take ownership of a built-in skill or command
|
|
174
195
|
|
|
196
|
+
## Custom TUI Configuration
|
|
197
|
+
|
|
198
|
+
Use the top-level `.agents/.airc.json` `customTUIs` array when your team uses an AI TUI that is not one of the built-in command targets. This config lets agent-infra show the correct next-step commands and generate command files for project custom skills by learning from an existing command in the custom TUI directory.
|
|
199
|
+
|
|
200
|
+
| Field | Required | Meaning |
|
|
201
|
+
|-------|----------|---------|
|
|
202
|
+
| `name` | Yes | Display name shown in reports and next-step guidance, for example `Acme TUI`. |
|
|
203
|
+
| `dir` | Yes | Command directory relative to the project root, for example `.acme/commands`. The path must stay inside the project root. |
|
|
204
|
+
| `invoke` | Yes | User-facing command template used in next-step guidance. |
|
|
205
|
+
|
|
206
|
+
Supported `invoke` placeholders:
|
|
207
|
+
|
|
208
|
+
| Placeholder | Replaced with | Example |
|
|
209
|
+
|-------------|---------------|---------|
|
|
210
|
+
| `${skillName}` | The skill command name, such as `review-task` or `commit`. | `acme ${skillName}` -> `acme review-task` |
|
|
211
|
+
| `${projectName}` | The `.airc.json` `project` value. Use this for namespaced commands. | `/${projectName}:${skillName}` -> `/agent-infra:review-task` |
|
|
212
|
+
|
|
213
|
+
Non-namespaced custom TUI:
|
|
214
|
+
|
|
215
|
+
```json
|
|
216
|
+
{
|
|
217
|
+
"customTUIs": [
|
|
218
|
+
{
|
|
219
|
+
"name": "Acme TUI",
|
|
220
|
+
"dir": ".acme/commands",
|
|
221
|
+
"invoke": "acme ${skillName}"
|
|
222
|
+
}
|
|
223
|
+
]
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Namespaced custom TUI:
|
|
228
|
+
|
|
229
|
+
```json
|
|
230
|
+
{
|
|
231
|
+
"project": "agent-infra",
|
|
232
|
+
"customTUIs": [
|
|
233
|
+
{
|
|
234
|
+
"name": "Internal Gemini",
|
|
235
|
+
"dir": ".internal-gemini/commands",
|
|
236
|
+
"invoke": "/${projectName}:${skillName}"
|
|
237
|
+
}
|
|
238
|
+
]
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
`customTUIs` should contain one entry per custom TUI. To let `update-agent-infra` generate command files for custom skills, keep at least one existing command file in `dir` that references a built-in skill path such as `.agents/skills/analyze-task/SKILL.md`; agent-infra uses that file as the format reference.
|
|
243
|
+
|
|
175
244
|
## Skill Authoring Conventions
|
|
176
245
|
|
|
177
246
|
When writing or updating `.agents/skills/*/SKILL.md` files and their templates, keep step numbering consistent:
|