@fitlab-ai/agent-infra 0.8.3 → 0.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.zh-CN.md +10 -0
- package/bin/cli.ts +1 -1
- package/dist/bin/cli.js +1 -1
- package/dist/lib/cp.js +56 -15
- package/dist/lib/defaults.json +1 -0
- package/dist/lib/sandbox/capture.js +5 -3
- package/dist/lib/sandbox/clipboard/bridge.js +12 -9
- package/dist/lib/sandbox/clipboard/inbox.js +82 -0
- package/dist/lib/sandbox/commands/create.js +39 -23
- package/dist/lib/sandbox/runtimes/base.dockerfile +3 -0
- package/dist/lib/sandbox/shell.js +9 -1
- package/dist/lib/sandbox/tools.js +2 -2
- package/lib/cp.ts +56 -15
- package/lib/defaults.json +1 -0
- package/lib/sandbox/capture.ts +5 -3
- package/lib/sandbox/clipboard/bridge.ts +14 -12
- package/lib/sandbox/clipboard/inbox.ts +83 -0
- package/lib/sandbox/commands/create.ts +48 -24
- package/lib/sandbox/runtimes/base.dockerfile +3 -0
- package/lib/sandbox/shell.ts +11 -2
- package/lib/sandbox/tools.ts +5 -5
- package/package.json +1 -1
- package/templates/.agents/README.en.md +3 -0
- package/templates/.agents/README.zh-CN.md +3 -0
- package/templates/.agents/rules/create-issue.github.en.md +1 -1
- package/templates/.agents/rules/create-issue.github.zh-CN.md +1 -1
- package/templates/.agents/rules/review-handshake.en.md +2 -2
- package/templates/.agents/rules/review-handshake.zh-CN.md +2 -2
- package/templates/.agents/rules/testing-discipline.en.md +3 -41
- package/templates/.agents/rules/testing-discipline.zh-CN.md +3 -41
- package/templates/.agents/scripts/validate-artifact.js +93 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/archive-tasks/scripts/archive-tasks.sh +1 -1
- package/templates/.agents/skills/block-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/cancel-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/check-task/SKILL.zh-CN.md +9 -9
- package/templates/.agents/skills/close-codescan/SKILL.en.md +1 -1
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +1 -1
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/branch-management.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/reference/fix-mode.en.md +2 -2
- package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/SKILL.en.md +1 -1
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +6 -6
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +6 -6
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +27 -3
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +27 -3
- 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/create-pr/SKILL.en.md +4 -4
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
- package/templates/.agents/skills/create-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -2
- 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 +3 -3
- package/templates/.agents/skills/import-issue/SKILL.en.md +2 -2
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/init-milestones/SKILL.en.md +3 -2
- package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +4 -3
- package/templates/.agents/skills/init-milestones/scripts/init-milestones.github.sh +165 -41
- package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/restore-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +5 -5
- package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +5 -5
- package/templates/.agents/skills/review-code/SKILL.en.md +8 -4
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +6 -3
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -0
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -0
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +5 -5
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +5 -5
- package/templates/.agents/skills/review-code/reference/report-template.en.md +2 -2
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +2 -2
- package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-plan/reference/output-templates.en.md +5 -5
- package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +5 -5
- package/templates/.agents/skills/test/SKILL.zh-CN.md +26 -8
- package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +29 -6
- package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +8 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +8 -1
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
pruneClipboardDir,
|
|
10
10
|
writeClipboardPngAtomic
|
|
11
11
|
} from './paths.ts';
|
|
12
|
+
import { isClipboardInboxReadable, readPendingClipboardPath } from './inbox.ts';
|
|
12
13
|
import { commandForEngine, restoreTerminal, runInteractiveEngine, runOkEngine } from '../shell.ts';
|
|
13
14
|
import { loadNodePty, type NodePty, type PtyProcess } from './node-pty.ts';
|
|
14
15
|
|
|
@@ -85,13 +86,9 @@ export async function runInteractiveWithClipboardBridge(options: BridgeOptions):
|
|
|
85
86
|
if (!stdin.isTTY || !stdout.isTTY) {
|
|
86
87
|
return fallback('host stdin/stdout is not a TTY');
|
|
87
88
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const available = adapter.available();
|
|
92
|
-
if (!available.ok) {
|
|
93
|
-
return fallback(available.reason);
|
|
94
|
-
}
|
|
89
|
+
const available = adapter?.available() ?? { ok: false as const, reason: 'no clipboard adapter available on this platform' };
|
|
90
|
+
const inboxReady = isClipboardInboxReadable(home);
|
|
91
|
+
if (!available.ok && !inboxReady) return fallback(available.reason);
|
|
95
92
|
if (!runOk(engine, 'docker', ['exec', container, 'sh', '-c', '[ -d /clipboard ] && [ -r /clipboard ]'])) {
|
|
96
93
|
return fallback('container /clipboard mount is missing; rebuild the sandbox to enable image paste');
|
|
97
94
|
}
|
|
@@ -119,7 +116,7 @@ export async function runInteractiveWithClipboardBridge(options: BridgeOptions):
|
|
|
119
116
|
return runBridge({
|
|
120
117
|
child,
|
|
121
118
|
home,
|
|
122
|
-
adapter,
|
|
119
|
+
adapter: available.ok ? adapter : null,
|
|
123
120
|
writeStderr,
|
|
124
121
|
stdin,
|
|
125
122
|
stdout,
|
|
@@ -138,7 +135,7 @@ async function runBridge({
|
|
|
138
135
|
}: {
|
|
139
136
|
child: PtyProcess;
|
|
140
137
|
home: string;
|
|
141
|
-
adapter: ClipboardAdapter;
|
|
138
|
+
adapter: ClipboardAdapter | null;
|
|
142
139
|
writeStderr: (chunk: string) => unknown;
|
|
143
140
|
stdin: NodeJS.ReadStream;
|
|
144
141
|
stdout: NodeJS.WriteStream;
|
|
@@ -182,9 +179,9 @@ async function runBridge({
|
|
|
182
179
|
}
|
|
183
180
|
|
|
184
181
|
function handleText(raw: string, target: PtyProcess): void {
|
|
185
|
-
const textAdapter = adapter as TextImageClipboardAdapter;
|
|
182
|
+
const textAdapter = adapter as TextImageClipboardAdapter | null;
|
|
186
183
|
const pastedText = extractSinglePathPaste(raw);
|
|
187
|
-
if (!textAdapter.readImageFromText || pastedText === null) {
|
|
184
|
+
if (!textAdapter || !textAdapter.readImageFromText || pastedText === null) {
|
|
188
185
|
target.write(raw);
|
|
189
186
|
return;
|
|
190
187
|
}
|
|
@@ -207,12 +204,17 @@ async function runBridge({
|
|
|
207
204
|
|
|
208
205
|
function handleCtrlV(match: CtrlVMatch, target: PtyProcess): void {
|
|
209
206
|
try {
|
|
207
|
+
const pendingPath = readPendingClipboardPath(home);
|
|
208
|
+
if (pendingPath) {
|
|
209
|
+
target.write(buildBracketedPaste(pendingPath));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
210
212
|
// readImagePng returns null both for "no image on clipboard" and for
|
|
211
213
|
// unexpected read failures; both cases forward the original Ctrl+V so
|
|
212
214
|
// the container app handles it as a regular keystroke. The throw branch
|
|
213
215
|
// below only fires on truly unexpected exceptions (e.g. fs write
|
|
214
216
|
// errors writing to the host clipboard dir).
|
|
215
|
-
const png = adapter
|
|
217
|
+
const png = adapter?.readImagePng() ?? null;
|
|
216
218
|
if (!png) {
|
|
217
219
|
target.write(match.raw);
|
|
218
220
|
return;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
clipboardHostDir,
|
|
5
|
+
containerClipboardPath,
|
|
6
|
+
pngClipboardFilename,
|
|
7
|
+
pruneClipboardDir,
|
|
8
|
+
writeClipboardPngAtomic
|
|
9
|
+
} from './paths.ts';
|
|
10
|
+
|
|
11
|
+
export const RECEIVER_CAPABILITY = 'AGENT_INFRA_CLIPBOARD_RECEIVER:v1';
|
|
12
|
+
const MARKER = '.pending.json';
|
|
13
|
+
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
14
|
+
const HASHED_PNG = /^[a-f0-9]{16}\.png$/u;
|
|
15
|
+
const REMOTE_TEMP = /^agent-infra-cp-[0-9a-f-]{36}\.png$/u;
|
|
16
|
+
|
|
17
|
+
export function isClipboardInboxReadable(home: string): boolean {
|
|
18
|
+
const dir = clipboardHostDir(home);
|
|
19
|
+
try {
|
|
20
|
+
fs.accessSync(dir, fs.constants.R_OK);
|
|
21
|
+
return fs.statSync(dir).isDirectory();
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function receiveRemoteClipboardPng(home: string, tempPath: string, platformName: NodeJS.Platform = process.platform): string {
|
|
28
|
+
if (platformName !== 'linux') throw new Error('remote clipboard receiver requires Linux');
|
|
29
|
+
if (path.dirname(tempPath) !== '/tmp' || !REMOTE_TEMP.test(path.basename(tempPath))) {
|
|
30
|
+
throw new Error('invalid clipboard upload path');
|
|
31
|
+
}
|
|
32
|
+
const signature = Buffer.alloc(PNG_SIGNATURE.length);
|
|
33
|
+
const fd = fs.openSync(tempPath, 'r');
|
|
34
|
+
let signatureBytes: number;
|
|
35
|
+
try {
|
|
36
|
+
signatureBytes = fs.readSync(fd, signature, 0, signature.length, 0);
|
|
37
|
+
} finally {
|
|
38
|
+
fs.closeSync(fd);
|
|
39
|
+
}
|
|
40
|
+
if (signatureBytes !== PNG_SIGNATURE.length || !signature.equals(PNG_SIGNATURE)) {
|
|
41
|
+
throw new Error('uploaded clipboard file is not a PNG');
|
|
42
|
+
}
|
|
43
|
+
const png = fs.readFileSync(tempPath);
|
|
44
|
+
const dir = clipboardHostDir(home);
|
|
45
|
+
if (!fs.existsSync(path.dirname(dir))) throw new Error('agent-infra is not installed on this host');
|
|
46
|
+
const filename = pngClipboardFilename(png);
|
|
47
|
+
writeClipboardPngAtomic(dir, filename, png);
|
|
48
|
+
pruneClipboardDir(dir);
|
|
49
|
+
const markerTmp = path.join(dir, `.${MARKER}.${process.pid}.tmp`);
|
|
50
|
+
fs.writeFileSync(markerTmp, JSON.stringify({ version: 1, filename, createdAt: new Date().toISOString() }) + '\n', { mode: 0o600 });
|
|
51
|
+
fs.renameSync(markerTmp, path.join(dir, MARKER));
|
|
52
|
+
return containerClipboardPath(filename);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function readPendingClipboardPath(home: string): string | null {
|
|
56
|
+
const dir = clipboardHostDir(home);
|
|
57
|
+
const markerPath = path.join(dir, MARKER);
|
|
58
|
+
let markerText: string;
|
|
59
|
+
try {
|
|
60
|
+
markerText = fs.readFileSync(markerPath, 'utf8');
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
|
|
63
|
+
throw new Error('invalid clipboard pending marker', { cause: error });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let marker: { version?: unknown; filename?: unknown };
|
|
67
|
+
try {
|
|
68
|
+
marker = JSON.parse(markerText) as { version?: unknown; filename?: unknown };
|
|
69
|
+
} catch (error) {
|
|
70
|
+
throw new Error('invalid clipboard pending marker', { cause: error });
|
|
71
|
+
}
|
|
72
|
+
if (marker.version !== 1 || typeof marker.filename !== 'string' || !HASHED_PNG.test(marker.filename)) {
|
|
73
|
+
throw new Error('invalid clipboard pending marker');
|
|
74
|
+
}
|
|
75
|
+
const filePath = path.join(dir, marker.filename);
|
|
76
|
+
try {
|
|
77
|
+
const stat = fs.lstatSync(filePath);
|
|
78
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('pending clipboard image is not a regular file');
|
|
79
|
+
} catch (error) {
|
|
80
|
+
throw new Error('invalid clipboard pending marker', { cause: error });
|
|
81
|
+
}
|
|
82
|
+
return containerClipboardPath(marker.filename);
|
|
83
|
+
}
|
|
@@ -98,6 +98,11 @@ Host aliases:
|
|
|
98
98
|
type SandboxCreateConfig = ReturnType<typeof loadConfig>;
|
|
99
99
|
type PreparedDockerfile = ReturnType<typeof prepareDockerfile>;
|
|
100
100
|
type ResolvedTool = { tool: SandboxTool; dir: string };
|
|
101
|
+
type TmpfsSeedPlanEntry = {
|
|
102
|
+
stagingPath: string;
|
|
103
|
+
targetPath: string;
|
|
104
|
+
volumeArgs: string[];
|
|
105
|
+
};
|
|
101
106
|
type RuntimeCheck = { name: string; cmd: string[] };
|
|
102
107
|
type JsonObject = Record<string, unknown>;
|
|
103
108
|
type GpgCache = { pub: Buffer; sec: Buffer } | null;
|
|
@@ -322,12 +327,14 @@ export function writeSanitizedGitconfig({
|
|
|
322
327
|
const SHELL_CONFIG_SYMLINKS = ['.gitconfig', '.gitignore_global', '.stCommitMsg', '.bash_aliases'];
|
|
323
328
|
|
|
324
329
|
export function ensureShellConfigSymlinks(engine: string, container: string, execFn: EngineExecFn = execEngine): void {
|
|
325
|
-
// Idempotent symlink setup.
|
|
326
|
-
//
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
330
|
+
// Idempotent symlink setup. Avoid a shell command here because Windows .cmd
|
|
331
|
+
// engine shims would interpret metacharacters in a `bash -lc` script before
|
|
332
|
+
// forwarding it to Docker.
|
|
333
|
+
for (const file of SHELL_CONFIG_SYMLINKS) {
|
|
334
|
+
execFn(engine, 'docker', [
|
|
335
|
+
'exec', container, 'ln', '-sf', `.host-shell-config/${file}`, `${CONTAINER_HOME}/${file}`
|
|
336
|
+
], { stdio: 'ignore' });
|
|
337
|
+
}
|
|
331
338
|
}
|
|
332
339
|
|
|
333
340
|
export function prepareHostShellConfig({
|
|
@@ -629,6 +636,7 @@ export function buildContainerEnvFile(
|
|
|
629
636
|
chmodFn?: typeof fs.chmodSync;
|
|
630
637
|
rmFn?: typeof fs.rmSync;
|
|
631
638
|
tmpDir?: string;
|
|
639
|
+
runSafeFn?: DirectRunSafeFn;
|
|
632
640
|
} = {}
|
|
633
641
|
): { dockerArgs: string[]; cleanup: () => void } {
|
|
634
642
|
const {
|
|
@@ -636,11 +644,15 @@ export function buildContainerEnvFile(
|
|
|
636
644
|
writeFileFn = fs.writeFileSync,
|
|
637
645
|
chmodFn = fs.chmodSync,
|
|
638
646
|
rmFn = fs.rmSync,
|
|
639
|
-
tmpDir = os.tmpdir()
|
|
647
|
+
tmpDir = os.tmpdir(),
|
|
648
|
+
runSafeFn = runSafe
|
|
640
649
|
} = options;
|
|
641
650
|
|
|
642
651
|
const entries: Array<[string, string]> = resolvedTools.flatMap(({ tool }) => Object.entries(tool.envVars ?? {}));
|
|
643
|
-
|
|
652
|
+
let ghToken = runSafeEngineFn(engine, 'gh', ['auth', 'token']);
|
|
653
|
+
if (!ghToken && engine === 'wsl2') {
|
|
654
|
+
ghToken = runSafeFn('gh', ['auth', 'token']);
|
|
655
|
+
}
|
|
644
656
|
if (ghToken) {
|
|
645
657
|
entries.push(['GH_TOKEN', ghToken]);
|
|
646
658
|
}
|
|
@@ -1493,6 +1505,7 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1493
1505
|
: null;
|
|
1494
1506
|
const envFile = buildContainerEnvFile(effectiveResolvedTools, engine);
|
|
1495
1507
|
let hostShellConfig: HostShellConfig;
|
|
1508
|
+
let tmpfsSeedPlan: TmpfsSeedPlanEntry[] = [];
|
|
1496
1509
|
try {
|
|
1497
1510
|
const claudeCodeEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'claude-code');
|
|
1498
1511
|
if (claudeCodeEntry) {
|
|
@@ -1533,22 +1546,24 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1533
1546
|
'-v',
|
|
1534
1547
|
volumeArg(engine, hostPath, containerPath, ':ro')
|
|
1535
1548
|
]);
|
|
1536
|
-
//
|
|
1537
|
-
//
|
|
1538
|
-
// the
|
|
1539
|
-
//
|
|
1540
|
-
//
|
|
1541
|
-
//
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
// sessions/ from a previous bind-mount era) must NOT be re-mounted,
|
|
1545
|
-
// or the high-churn writes would land on the host SSD again.
|
|
1546
|
-
const tmpfsSeedVolumes = effectiveResolvedTools.flatMap(({ tool, dir }) =>
|
|
1547
|
-
(tool.tmpfs?.seed ?? []).flatMap((entry) => {
|
|
1549
|
+
// Mount each declared seed read-only at an isolated staging path.
|
|
1550
|
+
// After docker run mounts the empty tmpfs, the container's default
|
|
1551
|
+
// user copies these entries into it so the runtime targets are normal,
|
|
1552
|
+
// writable files rather than nested mount points. The allowlist keeps
|
|
1553
|
+
// stale runtime files (logs_2.sqlite, sessions, etc.) on the host from
|
|
1554
|
+
// being exposed or written through.
|
|
1555
|
+
tmpfsSeedPlan = effectiveResolvedTools.flatMap(({ tool, dir }) =>
|
|
1556
|
+
(tool.tmpfs?.seed ?? []).flatMap((entry, index) => {
|
|
1548
1557
|
const hostPath = path.join(dir, entry);
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1558
|
+
if (!fs.existsSync(hostPath)) {
|
|
1559
|
+
return [];
|
|
1560
|
+
}
|
|
1561
|
+
const stagingPath = path.posix.join('/run/agent-infra/tmpfs-seeds', tool.id, String(index));
|
|
1562
|
+
return [{
|
|
1563
|
+
stagingPath,
|
|
1564
|
+
targetPath: path.posix.join(tool.containerMount, entry),
|
|
1565
|
+
volumeArgs: ['-v', volumeArg(engine, hostPath, stagingPath, ':ro')]
|
|
1566
|
+
}];
|
|
1552
1567
|
})
|
|
1553
1568
|
);
|
|
1554
1569
|
const liveMountVolumes = effectiveResolvedTools.flatMap(({ tool }) =>
|
|
@@ -1604,7 +1619,7 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1604
1619
|
...dotfilesMount,
|
|
1605
1620
|
...toolVolumes,
|
|
1606
1621
|
...tmpfsArgs,
|
|
1607
|
-
...
|
|
1622
|
+
...tmpfsSeedPlan.flatMap(({ volumeArgs }) => volumeArgs),
|
|
1608
1623
|
...liveMountVolumes,
|
|
1609
1624
|
...shellConfigVolumes,
|
|
1610
1625
|
...envFile.dockerArgs,
|
|
@@ -1617,6 +1632,15 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1617
1632
|
envFile.cleanup();
|
|
1618
1633
|
}
|
|
1619
1634
|
|
|
1635
|
+
for (const { stagingPath, targetPath } of tmpfsSeedPlan) {
|
|
1636
|
+
runEngineTaskCommand(engine, 'docker', [
|
|
1637
|
+
'exec', container, 'mkdir', '-p', path.posix.dirname(targetPath)
|
|
1638
|
+
]);
|
|
1639
|
+
runEngineTaskCommand(engine, 'docker', [
|
|
1640
|
+
'exec', container, 'cp', '-R', '--', stagingPath, targetPath
|
|
1641
|
+
]);
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1620
1644
|
// Belt-and-suspenders: re-create the four shell-config symlinks at
|
|
1621
1645
|
// runtime so users with a custom `sandbox.dockerfile` (which won't
|
|
1622
1646
|
// include the ai-tools.dockerfile symlink fragment) still get
|
|
@@ -12,6 +12,9 @@ RUN if [ "${HOST_UID}" = "0" ]; then \
|
|
|
12
12
|
(groupadd -o -g ${HOST_GID} devuser || true) && \
|
|
13
13
|
useradd -o -u ${HOST_UID} -g ${HOST_GID} -m -s /bin/bash devuser; \
|
|
14
14
|
else \
|
|
15
|
+
if ubuntu_uid="$(id -u ubuntu 2>/dev/null)" && [ "${ubuntu_uid}" = "${HOST_UID}" ]; then \
|
|
16
|
+
userdel -r ubuntu || exit 1; \
|
|
17
|
+
fi; \
|
|
15
18
|
(groupadd -g ${HOST_GID} devuser || true) && \
|
|
16
19
|
useradd -u ${HOST_UID} -g ${HOST_GID} -m -s /bin/bash devuser; \
|
|
17
20
|
fi
|
package/lib/sandbox/shell.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { execFileSync, spawnSync } from 'node:child_process';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import type { ExecFileSyncOptions, StdioOptions, SpawnSyncOptions, SpawnSyncReturns } from 'node:child_process';
|
|
5
|
+
import { getAdapter } from './engines/index.ts';
|
|
5
6
|
|
|
6
7
|
const DEFAULT_TIMEOUT_MS = 60 * 60 * 1000;
|
|
7
8
|
|
|
@@ -52,7 +53,7 @@ function resolveCommand(cmd: string): string {
|
|
|
52
53
|
return cmd;
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
function commandOptions<T extends
|
|
56
|
+
function commandOptions<T extends object>(cmd: string, opts: T): T | (T & { shell: true }) {
|
|
56
57
|
if (process.platform === 'win32' && /\.(?:bat|cmd)$/i.test(cmd)) {
|
|
57
58
|
return { ...opts, shell: true };
|
|
58
59
|
}
|
|
@@ -143,6 +144,13 @@ export function commandForEngine(engine: string, cmd: string, args: string[] = [
|
|
|
143
144
|
return { cmd: resolvedWrapper, args: ['--', cmd, ...args] };
|
|
144
145
|
}
|
|
145
146
|
|
|
147
|
+
if (cmd === 'docker') {
|
|
148
|
+
const context = getAdapter(engine).dockerContext;
|
|
149
|
+
if (context) {
|
|
150
|
+
return { cmd, args: ['--context', context, ...args] };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
146
154
|
return { cmd, args };
|
|
147
155
|
}
|
|
148
156
|
|
|
@@ -153,7 +161,8 @@ export function runEngine(engine: string, cmd: string, args: string[], opts: Com
|
|
|
153
161
|
|
|
154
162
|
export function execEngine(engine: string, cmd: string, args: string[], opts: ExecFileSyncOptions = {}) {
|
|
155
163
|
const command = commandForEngine(engine, cmd, args);
|
|
156
|
-
|
|
164
|
+
const resolved = resolveCommand(command.cmd);
|
|
165
|
+
return execFileSync(resolved, command.args, commandOptions(resolved, opts));
|
|
157
166
|
}
|
|
158
167
|
|
|
159
168
|
export function runOkEngine(engine: string, cmd: string, args: string[], opts: CommandOptions = {}): boolean {
|
package/lib/sandbox/tools.ts
CHANGED
|
@@ -22,9 +22,9 @@ export type SandboxTool = {
|
|
|
22
22
|
// When set, containerMount is mounted as an in-container tmpfs (RAM) instead
|
|
23
23
|
// of bind-mounting the host config dir, keeping high-churn tool logs off the
|
|
24
24
|
// host disk. `seed` lists the host-dir entries (relative to the tool's config
|
|
25
|
-
// dir) to
|
|
26
|
-
//
|
|
27
|
-
//
|
|
25
|
+
// dir) to copy into the tmpfs when the container starts. It is an explicit
|
|
26
|
+
// allowlist so runtime files (e.g. logs_2.sqlite, sessions) left in the host
|
|
27
|
+
// dir stay out of the container and seeded config cannot write back.
|
|
28
28
|
tmpfs?: { size?: string; seed?: string[] };
|
|
29
29
|
};
|
|
30
30
|
|
|
@@ -80,8 +80,8 @@ function createBuiltinTools(home: string, project: string): Record<string, Sandb
|
|
|
80
80
|
// codex churns ~/.codex/logs_2.sqlite heavily (upstream openai/codex#24275);
|
|
81
81
|
// a bind-mount would write-amplify onto the host SSD via virtiofs. Mount the
|
|
82
82
|
// codex home as tmpfs so those logs stay in RAM and die with the container.
|
|
83
|
-
//
|
|
84
|
-
//
|
|
83
|
+
// Seeded config (config.toml, model-catalogs) is copied into the tmpfs at
|
|
84
|
+
// startup; runtime files like logs_2.sqlite stay in RAM.
|
|
85
85
|
tmpfs: { size: '512m', seed: ['config.toml', 'model-catalogs'] },
|
|
86
86
|
hostLiveMounts: [
|
|
87
87
|
{ hostPath: hostJoin(home, '.codex', 'auth.json'), containerSubpath: 'auth.json' }
|
package/package.json
CHANGED
|
@@ -165,11 +165,14 @@ Recommended frontmatter:
|
|
|
165
165
|
name: enforce-style
|
|
166
166
|
description: "Apply the team style guide before code review. Use when you need to align the team's code style before review"
|
|
167
167
|
args: "<task-id>" # optional
|
|
168
|
+
disable-model-invocation: true # optional; consumed by supporting TUI adapters
|
|
168
169
|
---
|
|
169
170
|
```
|
|
170
171
|
|
|
171
172
|
Write `description` as "one-line responsibility + scenario clause": after the short responsibility, append "Use when …" (the Chinese `SKILL.zh-CN.md` variant uses "当……时使用") as the cross-TUI trigger semantics, so Agent Skills-capable tools can self-discover the skill in natural conversation. Do **not** add a separate `triggers` (or similar) frontmatter field for this.
|
|
172
173
|
|
|
174
|
+
Set the generic optional `disable-model-invocation` field to `true` when generated TUI commands should disable model invocation; each TUI adapter consumes it according to its capabilities. To preserve line breaks in a multiline `description`, use a YAML literal block (`|`); sync emits the corresponding multiline metadata format for each TUI.
|
|
175
|
+
|
|
173
176
|
After adding or updating a custom skill, run `update-agent-infra` again. The sync step detects non-built-in skills and generates matching commands for Claude Code, Gemini CLI, and OpenCode automatically.
|
|
174
177
|
|
|
175
178
|
### Shared skill sources
|
|
@@ -165,11 +165,14 @@
|
|
|
165
165
|
name: enforce-style
|
|
166
166
|
description: "在代码审查前应用团队风格规范。当需要在评审前统一团队代码风格时使用"
|
|
167
167
|
args: "<task-id>" # 可选
|
|
168
|
+
disable-model-invocation: true # 可选;由支持该能力的 TUI 适配器消费
|
|
168
169
|
---
|
|
169
170
|
```
|
|
170
171
|
|
|
171
172
|
`description` 采用「一句话职责 + 场景触发子句」写法:在简短职责描述后补充「当……时使用」(英文 `SKILL.en.md` 用「Use when …」),作为跨 TUI 的触发语义,供支持 Agent Skills 的工具在自然对话中自发现该 skill。**不要**为此新增 `triggers` 等额外 frontmatter 字段。
|
|
172
173
|
|
|
174
|
+
当生成的 TUI 命令需要禁用模型调用时,将通用可选字段 `disable-model-invocation` 设为 `true`;各 TUI 适配器按自身能力决定是否消费。多行 `description` 如需保留换行,使用 YAML literal block(`|`);同步器会为各 TUI 输出对应的多行元数据格式。
|
|
175
|
+
|
|
173
176
|
新增或修改自定义 skill 后,再执行一次 `update-agent-infra`。同步过程会自动检测非内置 skill,并为 Claude Code、Gemini CLI、OpenCode 生成对应命令。
|
|
174
177
|
|
|
175
178
|
### 共享 skill 源
|
|
@@ -169,7 +169,7 @@ Field write failures are non-blocking.
|
|
|
169
169
|
Update task.md:
|
|
170
170
|
|
|
171
171
|
- Write `issue_number: {n}` into the frontmatter (replace if it exists; append at the end of the frontmatter otherwise)
|
|
172
|
-
- Update `updated_at` to the current time (command: `date "+%Y-%m-%d %H:%M:%S
|
|
172
|
+
- Update `updated_at` to the current time (command: `date "+%Y-%m-%d %H:%M:%S%z" | sed 's/\([+-][0-9][0-9]\)\([0-9][0-9]\)$/\1:\2/'`)
|
|
173
173
|
|
|
174
174
|
> Do NOT append an Activity Log entry here. The Issue creation event is already captured by the GitHub Issue itself and by the frontmatter `issue_number` field; the Activity Log only records the single `create-task` skill execution anchor (`Create Task`), written by the caller SKILL step 3.
|
|
175
175
|
|
|
@@ -169,7 +169,7 @@ gh api "repos/$upstream_repo/issues/{issue-number}" -X PATCH \
|
|
|
169
169
|
更新 task.md:
|
|
170
170
|
|
|
171
171
|
- 把 `issue_number: {n}` 写入 frontmatter(已存在则替换;不存在则在 frontmatter 末尾追加)
|
|
172
|
-
- 更新 `updated_at` 为当前时间(命令:`date "+%Y-%m-%d %H:%M:%S
|
|
172
|
+
- 更新 `updated_at` 为当前时间(命令:`date "+%Y-%m-%d %H:%M:%S%z" | sed 's/\([+-][0-9][0-9]\)\([0-9][0-9]\)$/\1:\2/'`)
|
|
173
173
|
|
|
174
174
|
> 不要在此追加 Activity Log 条目。Issue 创建事件已由 GitHub Issue 自身和 frontmatter `issue_number` 承载;Activity Log 仅记录 `create-task` skill 一次执行的整体锚点(`Create Task`),由调用方 SKILL 步骤 3 写入。
|
|
175
175
|
|
|
@@ -85,8 +85,8 @@ When an executor judges an item to be a key design decision that needs human rul
|
|
|
85
85
|
|
|
86
86
|
## post-review commit gate (code stage only)
|
|
87
87
|
|
|
88
|
-
-
|
|
89
|
-
- `commit` reads only the highest-round `review-code` artifact. When that artifact is Approved, the pre-commit HEAD equals R, and the staged diff fingerprint equals F, task.md
|
|
88
|
+
- `review-code` captures `Review Baseline Commit` (R, `git rev-parse HEAD`) once, computes `Reviewed Diff Fingerprint` (F, the full worktree diff fingerprint) from that same R, and records both in the highest-round report. When the round is Approved it also writes `last_reviewed_commit: R` (B) to task.md; a non-Approved round preserves the existing B.
|
|
89
|
+
- `commit` reads only the highest-round `review-code` artifact. When that artifact is Approved, the pre-commit HEAD equals R, and the staged diff fingerprint equals F, it advances task.md B to the new commit SHA.
|
|
90
90
|
- The `complete-task` `post-review-commit` gate prefers B; when B is absent or invalid, it falls back to R from the highest-round `review-code` artifact.
|
|
91
91
|
- If new commits touch code / rule paths after B / R, the gate blocks and requires a fresh `review-code`.
|
|
92
92
|
- **Exemption**: append a ledger row `| PRC-1 | post-review-commit | - | - | human-decided | <ruling note> |` recording that a human explicitly allowed those commits without re-review.
|
|
@@ -85,8 +85,8 @@
|
|
|
85
85
|
|
|
86
86
|
## post-review commit 门禁(仅 code 阶段)
|
|
87
87
|
|
|
88
|
-
- `review-code`
|
|
89
|
-
- `commit` 只读取最高轮 `review-code` 产物;当该产物 Approved、提交前 HEAD 等于 R、且 staged diff fingerprint 等于 F
|
|
88
|
+
- `review-code` 一次性捕获 `审查基线提交`(R,`git rev-parse HEAD`),以同一 R 计算 `审查差异指纹`(F,完整工作区 diff fingerprint)并写入最高轮报告;本轮 Approved 时同时在 task.md 写入 `last_reviewed_commit: R`(B),非 Approved 时保留既有 B。
|
|
89
|
+
- `commit` 只读取最高轮 `review-code` 产物;当该产物 Approved、提交前 HEAD 等于 R、且 staged diff fingerprint 等于 F 时,把 task.md 的 B 推进为新提交 SHA。
|
|
90
90
|
- `complete-task` 的 `post-review-commit` gate 优先使用 B;B 缺失或非法时回退最高轮 `review-code` 的 R。
|
|
91
91
|
- 若 B / R 之后代码 / 规则路径出现新提交,gate 会拦截,要求重新 `review-code`。
|
|
92
92
|
- **豁免**:在账本追加一行 `| PRC-1 | post-review-commit | - | - | human-decided | <裁定说明> |`,记录人工明确允许该批提交免复审。
|
|
@@ -39,46 +39,8 @@ This mirrors "Goal-Driven Execution" in AGENTS.md: define a verifiable success c
|
|
|
39
39
|
- **Testing implementation details**: Prefer assertions on public APIs, artifacts, state changes, or error results; avoid assertions on private functions, internal call order, or temporary data structures.
|
|
40
40
|
- **Insufficient assertions**: Assertions must pin down concrete expected values; do not replace checks on key fields, counts, and boundaries with "does not throw" or "result exists".
|
|
41
41
|
|
|
42
|
-
##
|
|
42
|
+
## Project-specific test policy
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
Keep repository-specific commands, directory conventions, coverage thresholds, CI integration, and reporting services in the project's own test documentation. Before adding a test, inspect that local policy and place the test in the layer that matches its observable scope and runtime cost.
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
npm run test:coverage
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
The tail of stdout prints per-file line / branch / function coverage and the uncovered line numbers.
|
|
53
|
-
|
|
54
|
-
### CI display
|
|
55
|
-
|
|
56
|
-
`.github/workflows/unit-tests.yml` writes the coverage block into the GitHub Actions step summary on the ubuntu-latest shard (visible on the PR Checks page). Windows / macOS shards do not duplicate this output.
|
|
57
|
-
|
|
58
|
-
The Codecov badge at the top of the README is generated by Codecov after `.github/workflows/unit-tests.yml` uploads `coverage.lcov` from the ubuntu-latest shard.
|
|
59
|
-
|
|
60
|
-
### Boundaries
|
|
61
|
-
|
|
62
|
-
- **No percentage thresholds**: Threshold flags such as `--test-coverage-lines/branches/functions` are forbidden. Goodhart's law reminds us that once coverage becomes a metric, developers write "coverage-friendly but behavior-weak" tests.
|
|
63
|
-
- **Third-party services for the badge only**: Codecov hosts the README coverage badge, but the root `codecov.yml` explicitly disables its project/patch status checks and PR comments — Codecov only displays a number here and does not participate in merge decisions. Do not integrate other services such as coveralls.
|
|
64
|
-
- **No tier split**: Coverage is currently emitted only for the full `test` tier; smoke / core tier coverage has no independent value.
|
|
65
|
-
- **Does not block PRs**: The CI step uses `continue-on-error: true`, so a failed coverage collection does not affect merges.
|
|
66
|
-
|
|
67
|
-
### Where New Tests Go
|
|
68
|
-
|
|
69
|
-
The test directory determines which npm script runs a test file:
|
|
70
|
-
|
|
71
|
-
- `tests/unit/<module>/`: fast structural or pure-function tests; does not spawn the real CLI process or depend on external tools, suitable for `test:smoke`.
|
|
72
|
-
- `tests/integration/<module>/`: combines multiple modules, runs CLI subprocesses, touches temporary filesystems, or verifies template sync flows while staying stable and reasonably fast, suitable for `test:core`.
|
|
73
|
-
- `tests/e2e/<module>/`: slower contract, platform-sync, packaged-output, cross-process, or end-to-end workflow tests, run only by full `npm test`.
|
|
74
|
-
|
|
75
|
-
Modules remain as second-level directories such as `cli`, `core`, `scripts`, and `templates`. Shared helpers and fixtures stay in `tests/helpers/`, `tests/helpers.ts`, and `tests/fixtures/`; do not place them under a tier.
|
|
76
|
-
|
|
77
|
-
### Relation to "test tier coverage"
|
|
78
|
-
|
|
79
|
-
Distinguish two concepts:
|
|
80
|
-
|
|
81
|
-
- **Test tier coverage** (`tests/unit/core/test-tier-coverage.test.ts` checks test directory placement and npm script tier mapping): governs "which test files belong to which tier" and is orthogonal to source-line coverage.
|
|
82
|
-
- **Source-line coverage** (this section): governs "which lines of production source are exercised by tests".
|
|
83
|
-
|
|
84
|
-
The two serve different purposes and should not substitute for each other.
|
|
46
|
+
If the project does not define a layered test suite, use its complete test command for RED and GREEN verification. Do not invent a test hierarchy or coverage gate as part of an unrelated change.
|
|
@@ -39,46 +39,8 @@ assert.match(content, /^name: code-task$/m); // 正向断言已足够
|
|
|
39
39
|
- **测试实现细节**:优先断言公开接口、产物、状态变化或错误结果;避免断言私有函数、内部调用顺序、临时数据结构。
|
|
40
40
|
- **断言不充分**:断言必须锁定具体期望值;不要用"只要不抛异常""结果存在即可"替代对关键字段、数量和边界的验证。
|
|
41
41
|
|
|
42
|
-
##
|
|
42
|
+
## 项目级测试策略
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
仓库特定的命令、目录约定、覆盖率阈值、CI 集成和报告服务应记录在项目自己的测试文档中。新增测试前先读取该策略,并按测试的可观察范围与运行成本选择对应层级。
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
npm run test:coverage
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
stdout 末尾会打印按文件粒度的行 / 分支 / 函数覆盖率以及未覆盖行号。
|
|
53
|
-
|
|
54
|
-
### CI 展示
|
|
55
|
-
|
|
56
|
-
`.github/workflows/unit-tests.yml` 在 ubuntu-latest 分片上把覆盖率块写入 GitHub Actions 的 step summary(PR Checks 页可见)。Windows / macOS 分片不重复输出。
|
|
57
|
-
|
|
58
|
-
README 顶部的 Codecov 徽章由 `.github/workflows/unit-tests.yml` 在 ubuntu-latest 分片上传 `coverage.lcov` 后由 Codecov 生成。
|
|
59
|
-
|
|
60
|
-
### 边界
|
|
61
|
-
|
|
62
|
-
- **不设置百分比阈值**:`--test-coverage-lines/branches/functions` 等阈值参数禁止加入;Goodhart's law 提醒我们一旦把覆盖率作为指标,开发者会写"覆盖率友好但行为弱"的测试。
|
|
63
|
-
- **第三方服务仅用于徽章**:已接入 Codecov 托管 README 覆盖率徽章,但通过根 `codecov.yml` 显式关闭其 project/patch status check 与 PR 评论——Codecov 在本项目只展示数字,不参与 merge 决策。不接入 coveralls 等其他服务。
|
|
64
|
-
- **不区分 tier**:当前只对 full `test` tier 输出覆盖率;smoke / core tier 的覆盖率没有独立价值。
|
|
65
|
-
- **不阻塞 PR**:CI 步骤 `continue-on-error: true`,即便覆盖率采集失败也不影响 merge。
|
|
66
|
-
|
|
67
|
-
### 新测试该放哪一层
|
|
68
|
-
|
|
69
|
-
测试文件放入哪一层决定它会被哪些 npm script 自动执行:
|
|
70
|
-
|
|
71
|
-
- `tests/unit/<module>/`:快速、结构性或纯函数类测试;不启动真实 CLI 子进程,不依赖外部工具,适合 `test:smoke`。
|
|
72
|
-
- `tests/integration/<module>/`:会组合多个模块、运行 CLI 子进程、触达临时文件系统或验证模板同步流程,但仍应保持稳定和相对快速,适合 `test:core`。
|
|
73
|
-
- `tests/e2e/<module>/`:较慢的契约、平台同步、打包产物、跨进程或端到端流程测试,只在完整 `npm test` 中运行。
|
|
74
|
-
|
|
75
|
-
模块继续作为第二级目录(如 `cli`、`core`、`scripts`、`templates`)。共享 helper 和 fixtures 保持在 `tests/helpers/`、`tests/helpers.ts`、`tests/fixtures/`,不要放入任一 tier。
|
|
76
|
-
|
|
77
|
-
### 与"测试 tier 覆盖"的关系
|
|
78
|
-
|
|
79
|
-
注意区分两个概念:
|
|
80
|
-
|
|
81
|
-
- **测试 tier 覆盖**(`tests/unit/core/test-tier-coverage.test.ts` 校验测试文件目录归属与 npm script tier 映射):管的是"哪些测试文件被纳入哪一 tier",与代码行覆盖率正交。
|
|
82
|
-
- **代码行覆盖率**(本节):管的是"业务源码哪些行被测试触达"。
|
|
83
|
-
|
|
84
|
-
两者目的不同,不要相互替代。
|
|
46
|
+
如果项目没有定义分层测试套件,RED 与 GREEN 验证都使用项目的完整测试命令。不要在无关改动中自行引入测试层级或覆盖率门禁。
|