@fitlab-ai/agent-infra 0.7.6 → 0.8.0
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 +2 -0
- package/README.zh-CN.md +2 -0
- package/bin/cli.ts +53 -18
- package/dist/bin/cli.js +56 -19
- package/dist/lib/decide.js +110 -0
- package/dist/lib/run/host.js +39 -0
- package/dist/lib/run/index.js +102 -0
- package/dist/lib/run/prompt.js +2 -0
- package/dist/lib/run/skills.js +29 -0
- package/dist/lib/run/tui.js +35 -0
- package/dist/lib/sandbox/capture.js +49 -0
- package/dist/lib/sandbox/commands/create.js +55 -6
- package/dist/lib/sandbox/commands/ls.js +4 -33
- package/dist/lib/sandbox/commands/show.js +67 -0
- package/dist/lib/sandbox/index.js +7 -0
- package/dist/lib/server/adapters/_contract.js +9 -0
- package/dist/lib/server/adapters/feishu/index.js +40 -0
- package/dist/lib/server/adapters/feishu/transport.js +95 -0
- package/dist/lib/server/auth.js +19 -0
- package/dist/lib/server/config.js +183 -0
- package/dist/lib/server/daemon.js +96 -0
- package/dist/lib/server/index.js +47 -0
- package/dist/lib/server/logger.js +34 -0
- package/dist/lib/server/plugin-loader.js +73 -0
- package/dist/lib/server/process-control.js +200 -0
- package/dist/lib/server/protocol.js +69 -0
- package/dist/lib/server/redact.js +12 -0
- package/dist/lib/server/runner.js +75 -0
- package/dist/lib/server/streamer.js +44 -0
- package/dist/lib/task/commands/decisions.js +258 -0
- package/dist/lib/task/commands/issue-body.js +94 -0
- package/dist/lib/task/commands/log.js +67 -9
- package/dist/lib/task/index.js +17 -0
- package/dist/lib/task/issue-form.js +66 -0
- package/dist/lib/task/ledger.js +66 -0
- package/dist/lib/task/sections.js +73 -0
- package/lib/decide.ts +124 -0
- package/lib/run/host.ts +47 -0
- package/lib/run/index.ts +124 -0
- package/lib/run/prompt.ts +1 -0
- package/lib/run/skills.ts +34 -0
- package/lib/run/tui.ts +40 -0
- package/lib/sandbox/capture.ts +84 -0
- package/lib/sandbox/commands/create.ts +61 -9
- package/lib/sandbox/commands/ls.ts +4 -36
- package/lib/sandbox/commands/show.ts +80 -0
- package/lib/sandbox/index.ts +7 -0
- package/lib/server/adapters/_contract.ts +42 -0
- package/lib/server/adapters/feishu/index.ts +47 -0
- package/lib/server/adapters/feishu/transport.ts +128 -0
- package/lib/server/auth.ts +28 -0
- package/lib/server/config.ts +228 -0
- package/lib/server/daemon.ts +110 -0
- package/lib/server/index.ts +50 -0
- package/lib/server/logger.ts +48 -0
- package/lib/server/plugin-loader.ts +84 -0
- package/lib/server/process-control.ts +225 -0
- package/lib/server/protocol.ts +79 -0
- package/lib/server/redact.ts +12 -0
- package/lib/server/runner.ts +95 -0
- package/lib/server/server.schema.json +127 -0
- package/lib/server/streamer.ts +57 -0
- package/lib/task/commands/decisions.ts +272 -0
- package/lib/task/commands/issue-body.ts +102 -0
- package/lib/task/commands/log.ts +73 -9
- package/lib/task/index.ts +17 -0
- package/lib/task/issue-form.ts +77 -0
- package/lib/task/ledger.ts +73 -0
- package/lib/task/sections.ts +73 -0
- package/package.json +9 -4
- package/templates/.agents/QUICKSTART.zh-CN.md +2 -2
- package/templates/.agents/README.en.md +3 -1
- package/templates/.agents/README.zh-CN.md +3 -1
- package/templates/.agents/rules/create-issue.github.en.md +20 -29
- package/templates/.agents/rules/create-issue.github.zh-CN.md +20 -29
- package/templates/.agents/rules/issue-fields.github.en.md +1 -1
- package/templates/.agents/rules/issue-fields.github.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-pr-commands.github.en.md +6 -0
- package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +6 -0
- package/templates/.agents/rules/next-step-output.en.md +30 -1
- package/templates/.agents/rules/next-step-output.zh-CN.md +30 -1
- package/templates/.agents/rules/no-mid-flow-questions.en.md +7 -2
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +7 -2
- package/templates/.agents/rules/review-handshake.en.md +5 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +5 -1
- package/templates/.agents/rules/task-management.en.md +3 -1
- package/templates/.agents/rules/task-management.zh-CN.md +3 -1
- package/templates/.agents/scripts/lib/post-review-commit.js +15 -18
- package/templates/.agents/scripts/validate-artifact.js +8 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +18 -4
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +18 -4
- package/templates/.agents/skills/analyze-task/config/verify.en.json +2 -1
- package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +2 -1
- package/templates/.agents/skills/archive-tasks/SKILL.en.md +3 -1
- package/templates/.agents/skills/archive-tasks/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/block-task/SKILL.en.md +3 -1
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -1
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/check-task/SKILL.en.md +30 -81
- package/templates/.agents/skills/check-task/SKILL.zh-CN.md +30 -80
- package/templates/.agents/skills/close-codescan/SKILL.en.md +3 -1
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +3 -1
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +4 -2
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +4 -2
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +7 -2
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +7 -2
- package/templates/.agents/skills/code-task/scripts/detect-mode.js +6 -14
- package/templates/.agents/skills/commit/SKILL.en.md +27 -5
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +27 -5
- package/templates/.agents/skills/complete-task/SKILL.en.md +4 -1
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -1
- package/templates/.agents/skills/complete-task/config/verify.en.json +2 -1
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +3 -1
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/create-release-note/SKILL.en.md +3 -1
- package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/create-task/SKILL.en.md +4 -4
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/import-codescan/SKILL.en.md +3 -4
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +3 -4
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +3 -2
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +3 -2
- package/templates/.agents/skills/import-issue/SKILL.en.md +20 -4
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +20 -4
- package/templates/.agents/skills/init-labels/SKILL.en.md +3 -1
- package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/init-milestones/SKILL.en.md +3 -1
- package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +4 -3
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -3
- package/templates/.agents/skills/post-release/SKILL.en.md +3 -1
- package/templates/.agents/skills/post-release/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/refine-title/SKILL.en.md +3 -1
- package/templates/.agents/skills/refine-title/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/release/SKILL.en.md +3 -1
- package/templates/.agents/skills/release/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/restore-task/SKILL.en.md +3 -1
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +6 -2
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +1 -1
- package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +1 -1
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +1 -1
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +1 -2
- package/templates/.agents/skills/review-code/SKILL.en.md +5 -2
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -2
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +1 -1
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +1 -1
- package/templates/.agents/skills/review-code/reference/report-template.en.md +1 -1
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +1 -2
- package/templates/.agents/skills/review-plan/SKILL.en.md +6 -2
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/review-plan/reference/output-templates.en.md +1 -1
- package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +1 -1
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +1 -1
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +1 -2
- package/templates/.agents/skills/test/SKILL.en.md +3 -1
- package/templates/.agents/skills/test/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/test-integration/SKILL.en.md +3 -1
- package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/update-agent-infra/SKILL.en.md +3 -1
- package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +3 -1
- package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/watch-pr/SKILL.en.md +3 -1
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +3 -1
- package/templates/.agents/templates/task.en.md +3 -3
- package/templates/.agents/templates/task.zh-CN.md +3 -3
- package/templates/.github/workflows/metadata-sync.yml +0 -18
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
function escapeRegExp(value) {
|
|
2
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Return the body of the first `## {alias}` section (any alias matches), from
|
|
6
|
+
* the heading line to the next `## ` heading or EOF. Lines are preserved
|
|
7
|
+
* verbatim (checkbox text is never normalized); only leading/trailing blank
|
|
8
|
+
* lines are trimmed. Returns '' when no alias heading is present.
|
|
9
|
+
*/
|
|
10
|
+
function extractSection(content, aliases) {
|
|
11
|
+
const lines = content.split('\n');
|
|
12
|
+
let start = -1;
|
|
13
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
14
|
+
const line = lines[i].trim();
|
|
15
|
+
if (aliases.some((alias) => new RegExp(`^##\\s+${escapeRegExp(alias)}\\s*$`).test(line))) {
|
|
16
|
+
start = i + 1;
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (start === -1)
|
|
21
|
+
return '';
|
|
22
|
+
let end = lines.length;
|
|
23
|
+
for (let i = start; i < lines.length; i += 1) {
|
|
24
|
+
if (/^##\s+/.test(lines[i])) {
|
|
25
|
+
end = i;
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return lines.slice(start, end).join('\n').replace(/^\n+/, '').replace(/\n+$/, '');
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Return the heading alias that actually appears as a `## {alias}` line, so a
|
|
33
|
+
* rendered section can mirror the source language. Falls back to the first
|
|
34
|
+
* alias when none is present.
|
|
35
|
+
*/
|
|
36
|
+
function findSectionHeading(content, aliases) {
|
|
37
|
+
for (const alias of aliases) {
|
|
38
|
+
if (new RegExp(`^##\\s+${escapeRegExp(alias)}\\s*$`, 'm').test(content))
|
|
39
|
+
return alias;
|
|
40
|
+
}
|
|
41
|
+
return aliases[0];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Return the body of the first `### {headingPrefix}` sub-section, from the
|
|
45
|
+
* heading line (inclusive) to the next `### ` / `## ` heading or EOF. Used to
|
|
46
|
+
* pull a single `### HD-N` human-decision detail block out of an artifact. The
|
|
47
|
+
* prefix must be followed by a word boundary so `HD-1` does not match `HD-10`
|
|
48
|
+
* (e.g. `### HD-1`, `### HD-1:标题`, `### HD-1 [needs-human-decision]`). Leading
|
|
49
|
+
* and trailing blank lines are trimmed. Returns '' when no match is present.
|
|
50
|
+
*/
|
|
51
|
+
function extractSubSection(content, headingPrefix) {
|
|
52
|
+
const lines = content.split('\n');
|
|
53
|
+
const headRe = new RegExp(`^###\\s+${escapeRegExp(headingPrefix)}(?![\\w-])`);
|
|
54
|
+
let start = -1;
|
|
55
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
56
|
+
if (headRe.test(lines[i].trim())) {
|
|
57
|
+
start = i;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (start === -1)
|
|
62
|
+
return '';
|
|
63
|
+
let end = lines.length;
|
|
64
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
65
|
+
if (/^###?\s+/.test(lines[i])) {
|
|
66
|
+
end = i;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return lines.slice(start, end).join('\n').replace(/^\n+/, '').replace(/\n+$/, '');
|
|
71
|
+
}
|
|
72
|
+
export { extractSection, findSectionHeading, extractSubSection };
|
|
73
|
+
//# sourceMappingURL=sections.js.map
|
package/lib/decide.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { VERSION } from './version.ts';
|
|
5
|
+
import { resolveTaskRef } from './task/resolve-ref.ts';
|
|
6
|
+
|
|
7
|
+
const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
|
|
8
|
+
|
|
9
|
+
type DecideOptions = {
|
|
10
|
+
repoRoot?: string;
|
|
11
|
+
now?: () => string;
|
|
12
|
+
version?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function detectRepoRoot(): string {
|
|
16
|
+
return execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
17
|
+
encoding: 'utf8',
|
|
18
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
19
|
+
}).trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function defaultNow(): string {
|
|
23
|
+
return new Intl.DateTimeFormat('sv-SE', {
|
|
24
|
+
timeZoneName: 'longOffset',
|
|
25
|
+
year: 'numeric',
|
|
26
|
+
month: '2-digit',
|
|
27
|
+
day: '2-digit',
|
|
28
|
+
hour: '2-digit',
|
|
29
|
+
minute: '2-digit',
|
|
30
|
+
second: '2-digit',
|
|
31
|
+
hour12: false
|
|
32
|
+
})
|
|
33
|
+
.format(new Date())
|
|
34
|
+
.replace(' GMT', '');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function taskPath(repoRoot: string, ref: string): string {
|
|
38
|
+
if (!TASK_ID_RE.test(ref)) {
|
|
39
|
+
const resolved = resolveTaskRef(ref);
|
|
40
|
+
if (!resolved.ok) throw new Error(resolved.message);
|
|
41
|
+
if (!resolved.taskDir.includes(`${path.join('.agents', 'workspace', 'active')}${path.sep}`)) {
|
|
42
|
+
throw new Error(`task ${resolved.taskId} is not active`);
|
|
43
|
+
}
|
|
44
|
+
return resolved.taskMdPath;
|
|
45
|
+
}
|
|
46
|
+
const candidate = path.join(repoRoot, '.agents', 'workspace', 'active', ref, 'task.md');
|
|
47
|
+
if (!fs.existsSync(candidate)) throw new Error(`active task not found: ${ref}`);
|
|
48
|
+
return candidate;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function replaceFrontmatterField(content: string, field: string, value: string): string {
|
|
52
|
+
const re = new RegExp(`^${field}:.*$`, 'm');
|
|
53
|
+
if (re.test(content)) return content.replace(re, `${field}: ${value}`);
|
|
54
|
+
return content.replace(/^---\n/, `---\n${field}: ${value}\n`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function replaceLedgerRow(content: string, hdId: string): { content: string; found: boolean; pending: boolean } {
|
|
58
|
+
const lines = content.split('\n');
|
|
59
|
+
let found = false;
|
|
60
|
+
let pending = false;
|
|
61
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
62
|
+
const line = lines[i] as string;
|
|
63
|
+
if (!line.trim().startsWith(`| ${hdId} |`)) continue;
|
|
64
|
+
found = true;
|
|
65
|
+
const cells = line.split('|').slice(1, -1).map((cell) => cell.trim());
|
|
66
|
+
if (cells[4] !== 'needs-human-decision') break;
|
|
67
|
+
pending = true;
|
|
68
|
+
cells[4] = 'human-decided';
|
|
69
|
+
cells[5] = `task.md#${hdId}`;
|
|
70
|
+
lines[i] = `| ${cells.join(' | ')} |`;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
return { content: lines.join('\n'), found, pending };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function appendUnderHeading(content: string, heading: string, block: string): string {
|
|
77
|
+
if (!content.includes(`${heading}\n`)) {
|
|
78
|
+
return `${content.trimEnd()}\n\n${heading}\n\n${block}\n`;
|
|
79
|
+
}
|
|
80
|
+
const idx = content.indexOf(`${heading}\n`) + heading.length + 1;
|
|
81
|
+
const before = content.slice(0, idx);
|
|
82
|
+
const after = content.slice(idx);
|
|
83
|
+
return `${before}\n${block}\n${after.replace(/^\n/, '')}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function decide(args: string[], options: DecideOptions = {}): Promise<number> {
|
|
87
|
+
const [taskRef, hdId, ...decisionParts] = args;
|
|
88
|
+
if (!taskRef || !hdId || decisionParts.length === 0) {
|
|
89
|
+
process.stderr.write('Usage: ai decide <task-ref> <HD-id> <decision>\n');
|
|
90
|
+
return 1;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const repoRoot = options.repoRoot ?? detectRepoRoot();
|
|
94
|
+
const file = taskPath(repoRoot, taskRef);
|
|
95
|
+
let content = fs.readFileSync(file, 'utf8');
|
|
96
|
+
const replaced = replaceLedgerRow(content, hdId);
|
|
97
|
+
if (!replaced.found) throw new Error(`${hdId} not found in review ledger`);
|
|
98
|
+
if (!replaced.pending) throw new Error(`${hdId} is not needs-human-decision`);
|
|
99
|
+
content = replaced.content;
|
|
100
|
+
const now = (options.now ?? defaultNow)();
|
|
101
|
+
content = replaceFrontmatterField(content, 'updated_at', now);
|
|
102
|
+
content = replaceFrontmatterField(content, 'agent_infra_version', options.version ?? VERSION);
|
|
103
|
+
const decision = decisionParts.join(' ');
|
|
104
|
+
content = appendUnderHeading(
|
|
105
|
+
content,
|
|
106
|
+
'## 人工裁决',
|
|
107
|
+
`### ${hdId}\n\n- **裁决时间**:${now}\n- **裁决结果**:${decision}`
|
|
108
|
+
);
|
|
109
|
+
content = appendUnderHeading(
|
|
110
|
+
content,
|
|
111
|
+
'## 活动日志',
|
|
112
|
+
`- ${now} — **Human Decision** by human — ${hdId} decided`
|
|
113
|
+
);
|
|
114
|
+
fs.writeFileSync(file, content);
|
|
115
|
+
return 0;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
118
|
+
return 1;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function cmdDecide(args: string[]): Promise<void> {
|
|
123
|
+
process.exitCode = await decide(args);
|
|
124
|
+
}
|
package/lib/run/host.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
export type RunProcessResult = {
|
|
6
|
+
exitCode: number | null;
|
|
7
|
+
signal?: NodeJS.Signals | null;
|
|
8
|
+
stdout?: string;
|
|
9
|
+
stderr?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function resolveCommand(file: string): string {
|
|
13
|
+
if (process.platform !== 'win32' || path.extname(file)) {
|
|
14
|
+
return file;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const pathValue = process.env.Path || process.env.PATH || '';
|
|
18
|
+
const extensions = (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean);
|
|
19
|
+
for (const dir of pathValue.split(path.delimiter).filter(Boolean)) {
|
|
20
|
+
for (const extension of extensions) {
|
|
21
|
+
const lowerCandidate = path.join(dir, `${file}${extension.toLowerCase()}`);
|
|
22
|
+
if (fs.existsSync(lowerCandidate)) return lowerCandidate;
|
|
23
|
+
const upperCandidate = path.join(dir, `${file}${extension.toUpperCase()}`);
|
|
24
|
+
if (fs.existsSync(upperCandidate)) return upperCandidate;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return file;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function needsShell(file: string): boolean {
|
|
32
|
+
return process.platform === 'win32' && /\.(?:bat|cmd)$/i.test(file);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function runHostCommand(command: string[]): Promise<RunProcessResult> {
|
|
36
|
+
const [file, ...args] = command;
|
|
37
|
+
if (!file) throw new Error('run: missing command');
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const resolvedFile = resolveCommand(file);
|
|
40
|
+
const child = spawn(resolvedFile, args, {
|
|
41
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
42
|
+
shell: needsShell(resolvedFile)
|
|
43
|
+
});
|
|
44
|
+
child.on('error', reject);
|
|
45
|
+
child.on('close', (exitCode, signal) => resolve({ exitCode, signal }));
|
|
46
|
+
});
|
|
47
|
+
}
|
package/lib/run/index.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { loadServerConfig } from '../server/config.ts';
|
|
2
|
+
import { resolveTaskBranch } from '../sandbox/task-resolver.ts';
|
|
3
|
+
import { runInSandbox } from '../sandbox/capture.ts';
|
|
4
|
+
import { buildTuiCommand, renderPrompt, selectTui } from './tui.ts';
|
|
5
|
+
import { getSkillRunSpec } from './skills.ts';
|
|
6
|
+
import { runHostCommand, type RunProcessResult } from './host.ts';
|
|
7
|
+
|
|
8
|
+
export type ParsedRunArgs = {
|
|
9
|
+
skill: string;
|
|
10
|
+
taskRef: string | null;
|
|
11
|
+
args: string[];
|
|
12
|
+
tui: string | null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type SandboxRunRequest = {
|
|
16
|
+
taskRef: string;
|
|
17
|
+
branch: string;
|
|
18
|
+
command: string[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type RunSkillOptions = {
|
|
22
|
+
command?: Record<string, unknown>;
|
|
23
|
+
repoRoot?: string;
|
|
24
|
+
runHost?: (command: string[]) => Promise<RunProcessResult>;
|
|
25
|
+
runSandbox?: (request: SandboxRunRequest) => Promise<RunProcessResult>;
|
|
26
|
+
writeStdout?: (chunk: string) => void;
|
|
27
|
+
writeStderr?: (chunk: string) => void;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const USAGE = `Usage: ai run <skill> [task-ref] [args...] [--tui <name>]
|
|
31
|
+
|
|
32
|
+
Examples:
|
|
33
|
+
ai run create-task "describe the task" --tui codex
|
|
34
|
+
ai run code-task #7 --tui codex`;
|
|
35
|
+
|
|
36
|
+
function extractTui(args: string[]): { rest: string[]; tui: string | null } {
|
|
37
|
+
const rest: string[] = [];
|
|
38
|
+
let tui: string | null = null;
|
|
39
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
40
|
+
const arg = args[i] as string;
|
|
41
|
+
if (arg === '--tui') {
|
|
42
|
+
const value = args[i + 1];
|
|
43
|
+
if (!value) throw new Error('--tui requires a value');
|
|
44
|
+
tui = value;
|
|
45
|
+
i += 1;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
rest.push(arg);
|
|
49
|
+
}
|
|
50
|
+
return { rest, tui };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function parseRunArgs(args: string[]): ParsedRunArgs {
|
|
54
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
55
|
+
throw new Error(USAGE);
|
|
56
|
+
}
|
|
57
|
+
const { rest, tui } = extractTui(args);
|
|
58
|
+
const [skill = '', maybeTaskRef, ...remaining] = rest;
|
|
59
|
+
const spec = getSkillRunSpec(skill);
|
|
60
|
+
if (!spec) throw new Error(`Unknown skill '${skill}'`);
|
|
61
|
+
if (spec.kind === 'create') {
|
|
62
|
+
const createArgs = rest.slice(1);
|
|
63
|
+
if (createArgs.length === 0) throw new Error('create-task requires a description');
|
|
64
|
+
return { skill, taskRef: null, args: createArgs, tui };
|
|
65
|
+
}
|
|
66
|
+
if (!maybeTaskRef) throw new Error(`${skill} requires a task-ref`);
|
|
67
|
+
return { skill, taskRef: maybeTaskRef, args: remaining, tui };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function assertAllowedByConfig(skill: string, commandConfig: Record<string, unknown>): void {
|
|
71
|
+
const allowed = commandConfig.allowedSkills;
|
|
72
|
+
if (!Array.isArray(allowed)) return;
|
|
73
|
+
if (!allowed.every((entry) => typeof entry === 'string')) {
|
|
74
|
+
throw new Error('command.allowedSkills must be an array of skill names');
|
|
75
|
+
}
|
|
76
|
+
if (!allowed.includes(skill)) {
|
|
77
|
+
throw new Error(`Skill '${skill}' is not allowed by command.allowedSkills`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function runSkill(args: string[], options: RunSkillOptions = {}): Promise<number> {
|
|
82
|
+
const parsed = parseRunArgs(args);
|
|
83
|
+
const config = options.command ? null : loadServerConfig({ rootDir: options.repoRoot });
|
|
84
|
+
const commandConfig = options.command ?? config?.command ?? {};
|
|
85
|
+
assertAllowedByConfig(parsed.skill, commandConfig);
|
|
86
|
+
const tui = selectTui(parsed.skill, { cliTui: parsed.tui, command: commandConfig });
|
|
87
|
+
const promptArgs = parsed.taskRef === null ? parsed.args : [parsed.taskRef, ...parsed.args];
|
|
88
|
+
const prompt = renderPrompt({ tui, skill: parsed.skill, args: promptArgs });
|
|
89
|
+
const [file, argv] = buildTuiCommand(tui, prompt);
|
|
90
|
+
const command = [file, ...argv];
|
|
91
|
+
|
|
92
|
+
if (parsed.taskRef === null) {
|
|
93
|
+
const result = await (options.runHost ?? runHostCommand)(command);
|
|
94
|
+
return result.exitCode ?? (result.signal ? 1 : 0);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const repoRoot = options.repoRoot ?? config?.repoRoot ?? process.cwd();
|
|
98
|
+
const branch = resolveTaskBranch(parsed.taskRef, repoRoot);
|
|
99
|
+
const runSandbox = options.runSandbox ?? ((request: SandboxRunRequest) => runInSandbox(request));
|
|
100
|
+
const result = await runSandbox({ taskRef: parsed.taskRef, branch, command });
|
|
101
|
+
if (result.stdout) {
|
|
102
|
+
(options.writeStdout ?? ((chunk: string) => process.stdout.write(chunk)))(result.stdout);
|
|
103
|
+
}
|
|
104
|
+
if (result.stderr) {
|
|
105
|
+
(options.writeStderr ?? ((chunk: string) => process.stderr.write(chunk)))(result.stderr);
|
|
106
|
+
}
|
|
107
|
+
return result.exitCode ?? (result.signal ? 1 : 0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function cmdRun(args: string[]): Promise<void> {
|
|
111
|
+
try {
|
|
112
|
+
const code = await runSkill(args);
|
|
113
|
+
process.exitCode = code;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
116
|
+
if (message.startsWith('Usage:')) {
|
|
117
|
+
process.stdout.write(`${message}\n`);
|
|
118
|
+
process.exitCode = args.length === 0 ? 1 : 0;
|
|
119
|
+
} else {
|
|
120
|
+
process.stderr.write(`Error: ${message}\n`);
|
|
121
|
+
process.exitCode = 1;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { renderPrompt } from './tui.ts';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type SkillRunSpec =
|
|
2
|
+
| { kind: 'task'; skill: string; role: 'exec'; requiresSandbox: true }
|
|
3
|
+
| { kind: 'create'; skill: 'create-task'; role: 'exec'; requiresSandbox: false };
|
|
4
|
+
|
|
5
|
+
const TASK_SKILLS = new Set([
|
|
6
|
+
'analyze-task',
|
|
7
|
+
'block-task',
|
|
8
|
+
'cancel-task',
|
|
9
|
+
'code-task',
|
|
10
|
+
'commit',
|
|
11
|
+
'complete-task',
|
|
12
|
+
'create-pr',
|
|
13
|
+
'plan-task',
|
|
14
|
+
'review-analysis',
|
|
15
|
+
'review-code',
|
|
16
|
+
'review-plan',
|
|
17
|
+
'test',
|
|
18
|
+
'test-integration',
|
|
19
|
+
'watch-pr'
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
export function getSkillRunSpec(skill: string): SkillRunSpec | null {
|
|
23
|
+
if (skill === 'create-task') {
|
|
24
|
+
return { kind: 'create', skill: 'create-task', role: 'exec', requiresSandbox: false };
|
|
25
|
+
}
|
|
26
|
+
if (TASK_SKILLS.has(skill)) {
|
|
27
|
+
return { kind: 'task', skill, role: 'exec', requiresSandbox: true };
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function allowedSkillNames(): string[] {
|
|
33
|
+
return ['create-task', ...TASK_SKILLS].sort();
|
|
34
|
+
}
|
package/lib/run/tui.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export type TuiName = 'claude' | 'codex' | 'gemini' | 'opencode';
|
|
2
|
+
|
|
3
|
+
const TUI_NAMES = new Set(['claude', 'codex', 'gemini', 'opencode']);
|
|
4
|
+
|
|
5
|
+
export type CommandConfig = {
|
|
6
|
+
defaultTui?: unknown;
|
|
7
|
+
skillTuiDefaults?: unknown;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function isTuiName(value: unknown): value is TuiName {
|
|
11
|
+
return typeof value === 'string' && TUI_NAMES.has(value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function selectTui(
|
|
15
|
+
skill: string,
|
|
16
|
+
options: { cliTui?: string | null; command?: CommandConfig }
|
|
17
|
+
): TuiName {
|
|
18
|
+
if (isTuiName(options.cliTui)) return options.cliTui;
|
|
19
|
+
const defaults = options.command?.skillTuiDefaults;
|
|
20
|
+
if (defaults && typeof defaults === 'object' && !Array.isArray(defaults)) {
|
|
21
|
+
const value = (defaults as Record<string, unknown>)[skill];
|
|
22
|
+
if (isTuiName(value)) return value;
|
|
23
|
+
}
|
|
24
|
+
if (isTuiName(options.command?.defaultTui)) return options.command.defaultTui;
|
|
25
|
+
return 'codex';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function renderPrompt(params: { tui: TuiName; skill: string; args: string[] }): string {
|
|
29
|
+
const suffix = [params.skill, ...params.args].join(' ').trim();
|
|
30
|
+
if (params.tui === 'codex') return `$${suffix}`;
|
|
31
|
+
if (params.tui === 'gemini') return `/agent-infra:${suffix}`;
|
|
32
|
+
return `/${suffix}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function buildTuiCommand(tui: TuiName, prompt: string): [string, string[]] {
|
|
36
|
+
if (tui === 'claude') return ['claude', ['--dangerously-skip-permissions', '--print', prompt]];
|
|
37
|
+
if (tui === 'gemini') return ['gemini', ['--approval-mode', 'yolo', '--prompt', prompt]];
|
|
38
|
+
if (tui === 'opencode') return ['opencode', ['run', '--dangerously-skip-permissions', prompt]];
|
|
39
|
+
return ['codex', ['exec', '--dangerously-bypass-approvals-and-sandbox', prompt]];
|
|
40
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { loadConfig } from './config.ts';
|
|
3
|
+
import { containerNameCandidates, sandboxBranchLabel, sandboxLabel } from './constants.ts';
|
|
4
|
+
import { detectEngine } from './engine.ts';
|
|
5
|
+
import { hostTimezoneEnvFlags, terminalEnvFlags } from './commands/enter.ts';
|
|
6
|
+
import {
|
|
7
|
+
fetchSandboxRows,
|
|
8
|
+
selectSandboxContainer,
|
|
9
|
+
startSandboxContainer,
|
|
10
|
+
type SandboxRow
|
|
11
|
+
} from './commands/list-running.ts';
|
|
12
|
+
|
|
13
|
+
export type SandboxCaptureRequest = {
|
|
14
|
+
taskRef: string;
|
|
15
|
+
branch: string;
|
|
16
|
+
command: string[];
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type SandboxCaptureResult = {
|
|
21
|
+
exitCode: number | null;
|
|
22
|
+
signal: NodeJS.Signals | null;
|
|
23
|
+
stdout: string;
|
|
24
|
+
stderr: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type SandboxCaptureOptions = {
|
|
28
|
+
engine?: string;
|
|
29
|
+
repoRoot?: string;
|
|
30
|
+
containerCandidates?: string[];
|
|
31
|
+
rows?: SandboxRow[];
|
|
32
|
+
startContainer?: (name: string) => void;
|
|
33
|
+
spawn?: (file: string, args: string[]) => Promise<SandboxCaptureResult>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
async function spawnCapture(file: string, args: string[]): Promise<SandboxCaptureResult> {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const child = spawn(file, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
39
|
+
let stdout = '';
|
|
40
|
+
let stderr = '';
|
|
41
|
+
child.stdout.setEncoding('utf8');
|
|
42
|
+
child.stderr.setEncoding('utf8');
|
|
43
|
+
child.stdout.on('data', (chunk) => {
|
|
44
|
+
stdout += chunk;
|
|
45
|
+
});
|
|
46
|
+
child.stderr.on('data', (chunk) => {
|
|
47
|
+
stderr += chunk;
|
|
48
|
+
});
|
|
49
|
+
child.on('error', reject);
|
|
50
|
+
child.on('close', (exitCode, signal) => resolve({ exitCode, signal, stdout, stderr }));
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function runInSandbox(
|
|
55
|
+
request: SandboxCaptureRequest,
|
|
56
|
+
options: SandboxCaptureOptions = {}
|
|
57
|
+
): Promise<SandboxCaptureResult> {
|
|
58
|
+
const config = options.engine ? null : loadConfig();
|
|
59
|
+
const engine = options.engine ?? detectEngine(config!);
|
|
60
|
+
const rows =
|
|
61
|
+
options.rows ??
|
|
62
|
+
(() => {
|
|
63
|
+
const fetched = fetchSandboxRows(engine, sandboxLabel(config!), sandboxBranchLabel(config!));
|
|
64
|
+
return [...fetched.running, ...fetched.nonRunning];
|
|
65
|
+
})();
|
|
66
|
+
const candidates = options.containerCandidates ?? containerNameCandidates(config!, request.branch);
|
|
67
|
+
const found = selectSandboxContainer(rows, candidates);
|
|
68
|
+
if (!found) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Sandbox for ${request.branch} not found. Create it first with ai sandbox create ${request.taskRef}.`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
if (!found.running) {
|
|
74
|
+
(options.startContainer ?? ((name: string) => startSandboxContainer(engine, name)))(found.name);
|
|
75
|
+
}
|
|
76
|
+
const dockerArgs = [
|
|
77
|
+
'exec',
|
|
78
|
+
...terminalEnvFlags(),
|
|
79
|
+
...hostTimezoneEnvFlags(),
|
|
80
|
+
found.name,
|
|
81
|
+
...request.command
|
|
82
|
+
];
|
|
83
|
+
return (options.spawn ?? spawnCapture)('docker', dockerArgs);
|
|
84
|
+
}
|
|
@@ -740,6 +740,63 @@ function readHostJsonSafe(filePath: string): JsonObject | null {
|
|
|
740
740
|
}
|
|
741
741
|
}
|
|
742
742
|
|
|
743
|
+
const CLAUDE_SETTINGS_INHERIT_TOP_LEVEL_KEYS = [
|
|
744
|
+
'model',
|
|
745
|
+
'fallbackModel',
|
|
746
|
+
'availableModels',
|
|
747
|
+
'modelOverrides',
|
|
748
|
+
'enforceAvailableModels',
|
|
749
|
+
'advisorModel',
|
|
750
|
+
'apiKeyHelper',
|
|
751
|
+
'effortLevel'
|
|
752
|
+
];
|
|
753
|
+
|
|
754
|
+
function isJsonObjectRecord(value: unknown): value is JsonObject {
|
|
755
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function mergeMissingStringEnvFields(target: JsonObject, source: JsonObject): boolean {
|
|
759
|
+
if (!isJsonObjectRecord(source.env)) {
|
|
760
|
+
return false;
|
|
761
|
+
}
|
|
762
|
+
if (Object.hasOwn(target, 'env') && !isJsonObjectRecord(target.env)) {
|
|
763
|
+
return false;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
let targetEnv = target.env as JsonObject | undefined;
|
|
767
|
+
let changed = false;
|
|
768
|
+
for (const [key, value] of Object.entries(source.env)) {
|
|
769
|
+
if (typeof value !== 'string' || value === '') {
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
if (!targetEnv) {
|
|
773
|
+
targetEnv = {};
|
|
774
|
+
target.env = targetEnv;
|
|
775
|
+
}
|
|
776
|
+
if (!Object.hasOwn(targetEnv, key)) {
|
|
777
|
+
targetEnv[key] = value;
|
|
778
|
+
changed = true;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return changed;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function mergeMissingTopLevelSettings(target: JsonObject, source: JsonObject): boolean {
|
|
785
|
+
let changed = false;
|
|
786
|
+
for (const key of CLAUDE_SETTINGS_INHERIT_TOP_LEVEL_KEYS) {
|
|
787
|
+
if (!Object.hasOwn(source, key) || Object.hasOwn(target, key)) {
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
const value = source[key];
|
|
791
|
+
if (value === null || value === undefined || value === '') {
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
target[key] = value;
|
|
795
|
+
changed = true;
|
|
796
|
+
}
|
|
797
|
+
return changed;
|
|
798
|
+
}
|
|
799
|
+
|
|
743
800
|
export function ensureClaudeOnboarding(toolDir: string, hostHomeDir?: string): void {
|
|
744
801
|
const claudeJsonPath = path.join(toolDir, '.claude.json');
|
|
745
802
|
let data: JsonObject & {
|
|
@@ -810,7 +867,7 @@ export function ensureClaudeOnboarding(toolDir: string, hostHomeDir?: string): v
|
|
|
810
867
|
|
|
811
868
|
export function ensureClaudeSettings(toolDir: string, hostHomeDir?: string): void {
|
|
812
869
|
const settingsPath = path.join(toolDir, 'settings.json');
|
|
813
|
-
let data: JsonObject & { skipDangerousModePermissionPrompt?: boolean
|
|
870
|
+
let data: JsonObject & { skipDangerousModePermissionPrompt?: boolean } = {};
|
|
814
871
|
if (fs.existsSync(settingsPath)) {
|
|
815
872
|
try {
|
|
816
873
|
data = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as typeof data;
|
|
@@ -825,14 +882,9 @@ export function ensureClaudeSettings(toolDir: string, hostHomeDir?: string): voi
|
|
|
825
882
|
}
|
|
826
883
|
if (hostHomeDir) {
|
|
827
884
|
const hostSettings = readHostJsonSafe(path.join(hostHomeDir, '.claude', 'settings.json'));
|
|
828
|
-
if (
|
|
829
|
-
hostSettings
|
|
830
|
-
|
|
831
|
-
&& hostSettings.effortLevel !== ''
|
|
832
|
-
&& !Object.hasOwn(data, 'effortLevel')
|
|
833
|
-
) {
|
|
834
|
-
data.effortLevel = hostSettings.effortLevel;
|
|
835
|
-
changed = true;
|
|
885
|
+
if (hostSettings) {
|
|
886
|
+
changed = mergeMissingStringEnvFields(data, hostSettings) || changed;
|
|
887
|
+
changed = mergeMissingTopLevelSettings(data, hostSettings) || changed;
|
|
836
888
|
}
|
|
837
889
|
}
|
|
838
890
|
if (changed) {
|