@fitlab-ai/agent-infra 0.8.3 → 0.8.5
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 +9 -0
- package/dist/lib/init.js +5 -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 +68 -26
- package/dist/lib/sandbox/engine.js +33 -3
- package/dist/lib/sandbox/engines/colima.js +2 -2
- package/dist/lib/sandbox/runtimes/base.dockerfile +7 -0
- package/dist/lib/sandbox/shell.js +9 -1
- package/dist/lib/sandbox/tools.js +2 -2
- package/dist/lib/server/daemon.js +9 -0
- package/dist/lib/server/process-control.js +66 -73
- package/dist/lib/server/process-state.js +168 -0
- package/dist/lib/update.js +2 -0
- package/lib/cp.ts +56 -15
- package/lib/defaults.json +9 -0
- package/lib/init.ts +9 -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 +80 -27
- package/lib/sandbox/engine.ts +37 -3
- package/lib/sandbox/engines/colima.ts +2 -2
- package/lib/sandbox/runtimes/base.dockerfile +7 -0
- package/lib/sandbox/shell.ts +11 -2
- package/lib/sandbox/tools.ts +5 -5
- package/lib/server/daemon.ts +9 -0
- package/lib/server/process-control.ts +70 -71
- package/lib/server/process-state.ts +183 -0
- package/lib/update.ts +6 -0
- package/package.json +2 -1
- package/runtime/platform-adapters/platform-sync.github.js +1501 -0
- package/templates/.agents/README.en.md +5 -0
- package/templates/.agents/README.zh-CN.md +5 -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 +5 -4
- package/templates/.agents/rules/review-handshake.zh-CN.md +5 -4
- 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/lib/agent-infra-package.js +149 -0
- package/templates/.agents/scripts/lib/post-review-commit.js +5 -0
- package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +25 -1459
- package/templates/.agents/scripts/review-diff-fingerprint.js +95 -24
- package/templates/.agents/scripts/validate-artifact.js +144 -38
- 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 +6 -4
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -4
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +35 -8
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +35 -8
- 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 +3 -3
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
- 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 +5 -4
- package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +6 -5
- package/templates/.agents/skills/init-milestones/scripts/init-milestones.github.sh +170 -45
- 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 +3 -1
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +3 -1
- 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 +3 -2
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +3 -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.en.md +5 -1
- package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +203 -142
- 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
|
@@ -44,8 +44,8 @@ function createBuiltinTools(home, project) {
|
|
|
44
44
|
// codex churns ~/.codex/logs_2.sqlite heavily (upstream openai/codex#24275);
|
|
45
45
|
// a bind-mount would write-amplify onto the host SSD via virtiofs. Mount the
|
|
46
46
|
// codex home as tmpfs so those logs stay in RAM and die with the container.
|
|
47
|
-
//
|
|
48
|
-
//
|
|
47
|
+
// Seeded config (config.toml, model-catalogs) is copied into the tmpfs at
|
|
48
|
+
// startup; runtime files like logs_2.sqlite stay in RAM.
|
|
49
49
|
tmpfs: { size: '512m', seed: ['config.toml', 'model-catalogs'] },
|
|
50
50
|
hostLiveMounts: [
|
|
51
51
|
{ hostPath: hostJoin(home, '.codex', 'auth.json'), containerSubpath: 'auth.json' }
|
|
@@ -8,6 +8,7 @@ import { runAi } from './runner.js';
|
|
|
8
8
|
import { streamCommand } from './streamer.js';
|
|
9
9
|
import { markdownMessage, replyOutbound, textMessage } from './display.js';
|
|
10
10
|
import { buildStatusModel, statusModelToDisplay } from '../task/commands/status.js';
|
|
11
|
+
import { getProcessStartTime, removePidRecordIfMatches } from './process-state.js';
|
|
11
12
|
function errorMessage(error) {
|
|
12
13
|
return error instanceof Error ? error.message : String(error);
|
|
13
14
|
}
|
|
@@ -85,6 +86,7 @@ export async function runDaemon() {
|
|
|
85
86
|
}
|
|
86
87
|
const logger = createLogger(config.log);
|
|
87
88
|
logger.info(`daemon starting agent-infra ${VERSION} pid=${process.pid}`);
|
|
89
|
+
const ownStartTime = getProcessStartTime(process.pid);
|
|
88
90
|
const abortController = new AbortController();
|
|
89
91
|
let resolveShutdown = () => { };
|
|
90
92
|
const shutdown = new Promise((resolve) => {
|
|
@@ -106,6 +108,13 @@ export async function runDaemon() {
|
|
|
106
108
|
await unloadAdapters(adapters);
|
|
107
109
|
clearInterval(heartbeat);
|
|
108
110
|
logger.close();
|
|
111
|
+
if (ownStartTime !== null) {
|
|
112
|
+
removePidRecordIfMatches(config.pidFile, {
|
|
113
|
+
version: 1,
|
|
114
|
+
pid: process.pid,
|
|
115
|
+
startTime: ownStartTime
|
|
116
|
+
});
|
|
117
|
+
}
|
|
109
118
|
resolveShutdown();
|
|
110
119
|
})();
|
|
111
120
|
};
|
|
@@ -1,66 +1,44 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
2
|
import { spawn, execFileSync } from 'node:child_process';
|
|
4
3
|
import { loadServerConfig } from './config.js';
|
|
5
4
|
import { runDaemon } from './daemon.js';
|
|
5
|
+
import { getProcessStartTime, isProcessAlive, processIdentityMatches, readProcessState, removePidFileIfMatches, writePidRecord } from './process-state.js';
|
|
6
|
+
export { isProcessAlive } from './process-state.js';
|
|
6
7
|
export function buildStopCommand(pid, platform) {
|
|
7
8
|
if (platform === 'win32') {
|
|
8
9
|
return { kind: 'exec', command: 'taskkill', args: ['/PID', String(pid), '/T', '/F'] };
|
|
9
10
|
}
|
|
10
11
|
return { kind: 'signal', signal: 'SIGTERM' };
|
|
11
12
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
// reaps the zombie. On Linux we therefore also check /proc state so status/stop
|
|
17
|
-
// /start don't treat an already-exited daemon as running. macOS reaps orphans
|
|
18
|
-
// via launchd and Windows has no zombies, so the kill(pid, 0) result is enough.
|
|
19
|
-
export function isProcessAlive(pid) {
|
|
20
|
-
try {
|
|
21
|
-
process.kill(pid, 0);
|
|
22
|
-
}
|
|
23
|
-
catch (error) {
|
|
24
|
-
// EPERM means the process exists but we may not signal it → still alive.
|
|
25
|
-
return error.code === 'EPERM';
|
|
26
|
-
}
|
|
27
|
-
if (process.platform === 'linux') {
|
|
28
|
-
try {
|
|
29
|
-
const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
30
|
-
// The process state is the first token after the parenthesised comm.
|
|
31
|
-
const state = stat.slice(stat.lastIndexOf(')') + 1).trim().charAt(0);
|
|
32
|
-
if (state === 'Z')
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
return false; // /proc entry vanished → not alive
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return true;
|
|
13
|
+
function delay(ms) {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
setTimeout(resolve, ms);
|
|
16
|
+
});
|
|
40
17
|
}
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
18
|
+
async function waitForProcessIdentity(pid, timeoutMs = 2000) {
|
|
19
|
+
const deadline = Date.now() + timeoutMs;
|
|
20
|
+
while (Date.now() < deadline) {
|
|
21
|
+
const startTime = getProcessStartTime(pid);
|
|
22
|
+
if (startTime !== null)
|
|
23
|
+
return { version: 1, pid, startTime };
|
|
24
|
+
await delay(25);
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
50
27
|
}
|
|
51
|
-
function
|
|
28
|
+
function terminateIfMatching(record) {
|
|
29
|
+
if (!processIdentityMatches(record))
|
|
30
|
+
return;
|
|
31
|
+
const command = buildStopCommand(record.pid, process.platform);
|
|
52
32
|
try {
|
|
53
|
-
|
|
33
|
+
if (command.kind === 'exec')
|
|
34
|
+
execFileSync(command.command, command.args);
|
|
35
|
+
else
|
|
36
|
+
process.kill(record.pid, command.signal);
|
|
54
37
|
}
|
|
55
38
|
catch {
|
|
56
|
-
//
|
|
39
|
+
// Best effort cleanup for a daemon whose PID record could not be published.
|
|
57
40
|
}
|
|
58
41
|
}
|
|
59
|
-
function delay(ms) {
|
|
60
|
-
return new Promise((resolve) => {
|
|
61
|
-
setTimeout(resolve, ms);
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
42
|
function enabledAdapterNames(config) {
|
|
65
43
|
return Object.entries(config.adapters)
|
|
66
44
|
.filter(([, adapter]) => adapter?.enabled === true)
|
|
@@ -69,15 +47,13 @@ function enabledAdapterNames(config) {
|
|
|
69
47
|
export async function start({ foreground = false } = {}) {
|
|
70
48
|
const config = loadServerConfig();
|
|
71
49
|
const pidPath = config.pidFile;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (existing !== null && isProcessAlive(existing)) {
|
|
76
|
-
process.stdout.write(`server already running (pid ${existing})\n`);
|
|
50
|
+
const existing = readProcessState(pidPath);
|
|
51
|
+
if (existing.kind === 'running') {
|
|
52
|
+
process.stdout.write(`server already running (pid ${existing.record.pid})\n`);
|
|
77
53
|
return;
|
|
78
54
|
}
|
|
79
|
-
if (existing !==
|
|
80
|
-
|
|
55
|
+
if (existing.kind !== 'missing') {
|
|
56
|
+
removePidFileIfMatches(pidPath, existing.snapshot);
|
|
81
57
|
}
|
|
82
58
|
if (foreground) {
|
|
83
59
|
await runDaemon();
|
|
@@ -98,47 +74,64 @@ export async function start({ foreground = false } = {}) {
|
|
|
98
74
|
if (typeof child.pid !== 'number') {
|
|
99
75
|
throw new Error('server: failed to spawn daemon process');
|
|
100
76
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
77
|
+
// The initial check, spawn, and record publication are not a mutually
|
|
78
|
+
// exclusive transaction. Atomic publication keeps the record complete, but
|
|
79
|
+
// a separate locking design is required to serialize concurrent starts.
|
|
80
|
+
const record = await waitForProcessIdentity(child.pid);
|
|
81
|
+
if (record === null || !processIdentityMatches(record)) {
|
|
82
|
+
throw new Error('server: unable to verify spawned daemon process identity');
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
writePidRecord(pidPath, record);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
terminateIfMatching(record);
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
process.stdout.write(`server started (pid ${record.pid})\n`);
|
|
104
92
|
}
|
|
105
93
|
export async function stop() {
|
|
106
94
|
const config = loadServerConfig();
|
|
107
|
-
const
|
|
108
|
-
if (
|
|
95
|
+
const state = readProcessState(config.pidFile);
|
|
96
|
+
if (state.kind === 'missing') {
|
|
109
97
|
process.stdout.write('server is not running (no pid file)\n');
|
|
110
98
|
return;
|
|
111
99
|
}
|
|
112
|
-
if (
|
|
113
|
-
|
|
100
|
+
if (state.kind !== 'running') {
|
|
101
|
+
removePidFileIfMatches(config.pidFile, state.snapshot);
|
|
114
102
|
process.stdout.write('server is not running (removed stale pid file)\n');
|
|
115
103
|
return;
|
|
116
104
|
}
|
|
117
|
-
const
|
|
105
|
+
const { record } = state;
|
|
106
|
+
if (!processIdentityMatches(record)) {
|
|
107
|
+
removePidFileIfMatches(config.pidFile, state.snapshot);
|
|
108
|
+
process.stdout.write('server is not running (removed stale pid file)\n');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const command = buildStopCommand(record.pid, process.platform);
|
|
118
112
|
if (command.kind === 'exec') {
|
|
119
113
|
execFileSync(command.command, command.args);
|
|
120
114
|
}
|
|
121
115
|
else {
|
|
122
|
-
process.kill(pid, command.signal);
|
|
116
|
+
process.kill(record.pid, command.signal);
|
|
123
117
|
const deadline = Date.now() + 5000;
|
|
124
|
-
while (isProcessAlive(pid) && Date.now() < deadline) {
|
|
118
|
+
while (isProcessAlive(record.pid) && Date.now() < deadline) {
|
|
125
119
|
await delay(100);
|
|
126
120
|
}
|
|
127
|
-
if (isProcessAlive(pid)) {
|
|
128
|
-
process.kill(pid, 'SIGKILL');
|
|
121
|
+
if (isProcessAlive(record.pid) && processIdentityMatches(record)) {
|
|
122
|
+
process.kill(record.pid, 'SIGKILL');
|
|
129
123
|
}
|
|
130
124
|
}
|
|
131
|
-
|
|
132
|
-
process.stdout.write(`server stopped (pid ${pid})\n`);
|
|
125
|
+
removePidFileIfMatches(config.pidFile, state.snapshot);
|
|
126
|
+
process.stdout.write(`server stopped (pid ${record.pid})\n`);
|
|
133
127
|
}
|
|
134
128
|
export function status() {
|
|
135
129
|
const config = loadServerConfig();
|
|
136
|
-
const
|
|
137
|
-
if (
|
|
130
|
+
const state = readProcessState(config.pidFile);
|
|
131
|
+
if (state.kind !== 'running') {
|
|
132
|
+
if (state.kind !== 'missing')
|
|
133
|
+
removePidFileIfMatches(config.pidFile, state.snapshot);
|
|
138
134
|
process.stdout.write('server: stopped\n');
|
|
139
|
-
if (pid !== null) {
|
|
140
|
-
process.stdout.write(` (stale pid file references pid ${pid})\n`);
|
|
141
|
-
}
|
|
142
135
|
return;
|
|
143
136
|
}
|
|
144
137
|
let startedAt = 'unknown';
|
|
@@ -150,7 +143,7 @@ export function status() {
|
|
|
150
143
|
}
|
|
151
144
|
const adapters = enabledAdapterNames(config);
|
|
152
145
|
process.stdout.write(`server: running\n` +
|
|
153
|
-
` pid: ${pid}\n` +
|
|
146
|
+
` pid: ${state.record.pid}\n` +
|
|
154
147
|
` started: ${startedAt}\n` +
|
|
155
148
|
` adapters: ${adapters.length > 0 ? adapters.join(', ') : '(none)'}\n` +
|
|
156
149
|
` pid file: ${config.pidFile}\n` +
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
export function parseLinuxProcessStat(raw) {
|
|
5
|
+
const closeParen = raw.lastIndexOf(')');
|
|
6
|
+
if (closeParen < 0)
|
|
7
|
+
return null;
|
|
8
|
+
const fields = raw.slice(closeParen + 1).trim().split(/\s+/);
|
|
9
|
+
const state = fields[0];
|
|
10
|
+
const startTime = fields[19];
|
|
11
|
+
if (typeof state !== 'string' || state.length !== 1 || !/^\d+$/.test(startTime ?? '')) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return { state, startTime: startTime };
|
|
15
|
+
}
|
|
16
|
+
export function buildProcessStartTimeQuery(pid, platform) {
|
|
17
|
+
if (platform === 'darwin') {
|
|
18
|
+
return { command: 'ps', args: ['-p', String(pid), '-o', 'lstart='] };
|
|
19
|
+
}
|
|
20
|
+
if (platform === 'win32') {
|
|
21
|
+
const script = `$process = Get-CimInstance Win32_Process -Filter 'ProcessId = ${pid}'; ` +
|
|
22
|
+
`if ($null -ne $process) { $process.CreationDate.ToUniversalTime().ToString('o') }`;
|
|
23
|
+
return {
|
|
24
|
+
command: 'powershell.exe',
|
|
25
|
+
args: ['-NoProfile', '-NonInteractive', '-Command', script]
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
export function isProcessAlive(pid, platform = process.platform) {
|
|
31
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
32
|
+
return false;
|
|
33
|
+
if (platform === 'linux') {
|
|
34
|
+
try {
|
|
35
|
+
const stat = parseLinuxProcessStat(fs.readFileSync(`/proc/${pid}/stat`, 'utf8'));
|
|
36
|
+
return stat !== null && stat.state !== 'Z';
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
process.kill(pid, 0);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
return error.code === 'EPERM';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function getProcessStartTime(pid, platform = process.platform) {
|
|
51
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
52
|
+
return null;
|
|
53
|
+
if (platform === 'linux') {
|
|
54
|
+
try {
|
|
55
|
+
const stat = parseLinuxProcessStat(fs.readFileSync(`/proc/${pid}/stat`, 'utf8'));
|
|
56
|
+
return stat !== null && stat.state !== 'Z' ? stat.startTime : null;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (!isProcessAlive(pid, platform))
|
|
63
|
+
return null;
|
|
64
|
+
const query = buildProcessStartTimeQuery(pid, platform);
|
|
65
|
+
if (query === null)
|
|
66
|
+
return null;
|
|
67
|
+
try {
|
|
68
|
+
const output = execFileSync(query.command, query.args, { encoding: 'utf8' }).trim();
|
|
69
|
+
if (output.length === 0)
|
|
70
|
+
return null;
|
|
71
|
+
if (platform === 'win32') {
|
|
72
|
+
const timestamp = new Date(output);
|
|
73
|
+
return Number.isNaN(timestamp.getTime()) ? null : timestamp.toISOString();
|
|
74
|
+
}
|
|
75
|
+
return output;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function parsePidRecord(raw) {
|
|
82
|
+
const trimmed = raw.trim();
|
|
83
|
+
if (/^[1-9]\d*$/.test(trimmed)) {
|
|
84
|
+
const pid = Number(trimmed);
|
|
85
|
+
return Number.isSafeInteger(pid) ? { kind: 'legacy', pid } : { kind: 'invalid' };
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const value = JSON.parse(trimmed);
|
|
89
|
+
if (value !== null &&
|
|
90
|
+
value.version === 1 &&
|
|
91
|
+
Number.isSafeInteger(value.pid) &&
|
|
92
|
+
(value.pid ?? 0) > 0 &&
|
|
93
|
+
typeof value.startTime === 'string' &&
|
|
94
|
+
value.startTime.length > 0) {
|
|
95
|
+
return {
|
|
96
|
+
kind: 'record',
|
|
97
|
+
record: { version: 1, pid: value.pid, startTime: value.startTime }
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Invalid JSON is an invalid PID record.
|
|
103
|
+
}
|
|
104
|
+
return { kind: 'invalid' };
|
|
105
|
+
}
|
|
106
|
+
export function readProcessState(pidFile) {
|
|
107
|
+
let snapshot;
|
|
108
|
+
try {
|
|
109
|
+
snapshot = fs.readFileSync(pidFile, 'utf8');
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (error.code === 'ENOENT')
|
|
113
|
+
return { kind: 'missing', snapshot: null };
|
|
114
|
+
return { kind: 'invalid', snapshot: '' };
|
|
115
|
+
}
|
|
116
|
+
const parsed = parsePidRecord(snapshot);
|
|
117
|
+
if (parsed.kind === 'invalid')
|
|
118
|
+
return { kind: 'invalid', snapshot };
|
|
119
|
+
if (parsed.kind === 'legacy')
|
|
120
|
+
return { kind: 'legacy', snapshot, pid: parsed.pid };
|
|
121
|
+
const currentStartTime = getProcessStartTime(parsed.record.pid);
|
|
122
|
+
if (currentStartTime === null || currentStartTime !== parsed.record.startTime) {
|
|
123
|
+
return { kind: 'stale', snapshot, record: parsed.record };
|
|
124
|
+
}
|
|
125
|
+
return { kind: 'running', snapshot, record: parsed.record };
|
|
126
|
+
}
|
|
127
|
+
export function writePidRecord(pidFile, record) {
|
|
128
|
+
fs.mkdirSync(path.dirname(pidFile), { recursive: true });
|
|
129
|
+
const temporary = `${pidFile}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
|
|
130
|
+
try {
|
|
131
|
+
fs.writeFileSync(temporary, `${JSON.stringify(record)}\n`);
|
|
132
|
+
fs.renameSync(temporary, pidFile);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
try {
|
|
136
|
+
fs.unlinkSync(temporary);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// The temporary file was never created or was already renamed.
|
|
140
|
+
}
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export function removePidFileIfMatches(pidFile, expectedSnapshot) {
|
|
145
|
+
if (expectedSnapshot === null)
|
|
146
|
+
return false;
|
|
147
|
+
try {
|
|
148
|
+
if (fs.readFileSync(pidFile, 'utf8') !== expectedSnapshot)
|
|
149
|
+
return false;
|
|
150
|
+
fs.unlinkSync(pidFile);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export function removePidRecordIfMatches(pidFile, expected) {
|
|
158
|
+
const state = readProcessState(pidFile);
|
|
159
|
+
if (state.kind !== 'running')
|
|
160
|
+
return false;
|
|
161
|
+
if (state.record.pid !== expected.pid || state.record.startTime !== expected.startTime)
|
|
162
|
+
return false;
|
|
163
|
+
return removePidFileIfMatches(pidFile, state.snapshot);
|
|
164
|
+
}
|
|
165
|
+
export function processIdentityMatches(record) {
|
|
166
|
+
return getProcessStartTime(record.pid) === record.startTime;
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=process-state.js.map
|
package/dist/lib/update.js
CHANGED
|
@@ -145,6 +145,8 @@ async function cmdUpdate() {
|
|
|
145
145
|
// update skill
|
|
146
146
|
copySkillDir(path.join(templateDir, '.agents', 'skills', 'update-agent-infra'), path.join('.agents', 'skills', 'update-agent-infra'), replacements, language, platformType);
|
|
147
147
|
ok('Updated .agents/skills/update-agent-infra/');
|
|
148
|
+
renderFile(path.join(templateDir, '.agents', 'scripts', 'lib', 'agent-infra-package.js'), path.join('.agents', 'scripts', 'lib', 'agent-infra-package.js'), replacements);
|
|
149
|
+
ok('Updated .agents/scripts/lib/agent-infra-package.js');
|
|
148
150
|
try {
|
|
149
151
|
fs.unlinkSync(path.join('.agents', 'skills', 'update-agent-infra', 'scripts', 'sync-templates.cjs'));
|
|
150
152
|
}
|
package/lib/cp.ts
CHANGED
|
@@ -5,8 +5,9 @@ import { platform as currentPlatform, tmpdir as defaultTmpdir } from 'node:os';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { parseArgs } from 'node:util';
|
|
7
7
|
import { createClipboardAdapter, type ClipboardAdapter } from './sandbox/clipboard/index.ts';
|
|
8
|
+
import { RECEIVER_CAPABILITY, receiveRemoteClipboardPng } from './sandbox/clipboard/inbox.ts';
|
|
8
9
|
|
|
9
|
-
const USAGE = 'Usage: ai cp <ssh-alias>\n\nCopy the local clipboard image (PNG) to a remote macOS
|
|
10
|
+
const USAGE = 'Usage: ai cp <ssh-alias>\n\nCopy the local clipboard image (PNG) to a remote macOS clipboard or Linux sandbox over SSH.\n';
|
|
10
11
|
const COMMAND_TIMEOUT_MS = 30_000;
|
|
11
12
|
|
|
12
13
|
export type SpawnResult = {
|
|
@@ -65,6 +66,21 @@ export async function cmdCp(args: string[], deps: CpDeps = {}): Promise<number>
|
|
|
65
66
|
writeStderr = (chunk: string) => process.stderr.write(chunk)
|
|
66
67
|
} = deps;
|
|
67
68
|
|
|
69
|
+
if (args.length === 1 && args[0] === '--remote-receive-capability') {
|
|
70
|
+
if (platform === 'linux') writeStdout(`${RECEIVER_CAPABILITY}\n`);
|
|
71
|
+
return platform === 'linux' ? 0 : 1;
|
|
72
|
+
}
|
|
73
|
+
if (args.length === 2 && args[0] === '--remote-receive-v1') {
|
|
74
|
+
try {
|
|
75
|
+
const result = receiveRemoteClipboardPng(process.env.HOME || process.env.USERPROFILE || '', args[1]!, platform);
|
|
76
|
+
writeStdout(`${result}\n`);
|
|
77
|
+
return 0;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
writeStderr(`${error instanceof Error ? error.message : 'clipboard receive failed'}\n`);
|
|
80
|
+
return 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
68
84
|
if (args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
|
|
69
85
|
writeStdout(USAGE);
|
|
70
86
|
return 0;
|
|
@@ -110,6 +126,35 @@ export async function cmdCp(args: string[], deps: CpDeps = {}): Promise<number>
|
|
|
110
126
|
writeFileFn(localPng, png);
|
|
111
127
|
|
|
112
128
|
remotePath = `/tmp/agent-infra-cp-${randomId()}.png`;
|
|
129
|
+
const remoteSystem = spawnFn('ssh', [
|
|
130
|
+
'-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', alias, 'uname', '-s'
|
|
131
|
+
]);
|
|
132
|
+
if (remoteSystem.status !== 0) {
|
|
133
|
+
writeStderr(`failed to detect remote platform on ${alias}:\n${commandDetail(remoteSystem)}\n`);
|
|
134
|
+
return 1;
|
|
135
|
+
}
|
|
136
|
+
const remotePlatform = remoteSystem.stdout.trim();
|
|
137
|
+
if (remotePlatform !== 'Darwin' && remotePlatform !== 'Linux') {
|
|
138
|
+
writeStderr(`unsupported remote platform on ${alias}: ${remotePlatform || 'unknown'}\n`);
|
|
139
|
+
return 1;
|
|
140
|
+
}
|
|
141
|
+
if (remotePlatform === 'Linux') {
|
|
142
|
+
const homeCheck = spawnFn('ssh', [
|
|
143
|
+
'-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', alias, 'test', '-d', '$HOME/.agent-infra'
|
|
144
|
+
]);
|
|
145
|
+
if (homeCheck.status !== 0) {
|
|
146
|
+
writeStderr(`remote agent-infra installation is missing on ${alias}\n`);
|
|
147
|
+
return 1;
|
|
148
|
+
}
|
|
149
|
+
const capability = spawnFn('ssh', [
|
|
150
|
+
'-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', alias, 'ai', 'cp', '--remote-receive-capability'
|
|
151
|
+
]);
|
|
152
|
+
if (capability.status !== 0 || capability.stderr !== '' || capability.stdout.replace(/\n$/u, '') !== RECEIVER_CAPABILITY) {
|
|
153
|
+
writeStderr(`remote agent-infra clipboard receiver is unavailable or incompatible on ${alias}; update agent-infra\n`);
|
|
154
|
+
return 1;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
113
158
|
const upload = spawnFn('scp', [
|
|
114
159
|
'-o',
|
|
115
160
|
'BatchMode=yes',
|
|
@@ -124,25 +169,21 @@ export async function cmdCp(args: string[], deps: CpDeps = {}): Promise<number>
|
|
|
124
169
|
}
|
|
125
170
|
uploaded = true;
|
|
126
171
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
'-o',
|
|
135
|
-
'ConnectTimeout=10',
|
|
136
|
-
alias,
|
|
137
|
-
'osascript',
|
|
138
|
-
'-'
|
|
139
|
-
], remoteSetScript(remotePath));
|
|
172
|
+
const setRemote = remotePlatform === 'Darwin'
|
|
173
|
+
? spawnFn('ssh', [
|
|
174
|
+
'-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', alias, 'osascript', '-'
|
|
175
|
+
], remoteSetScript(remotePath))
|
|
176
|
+
: spawnFn('ssh', [
|
|
177
|
+
'-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', alias, 'ai', 'cp', '--remote-receive-v1', remotePath
|
|
178
|
+
]);
|
|
140
179
|
if (setRemote.status !== 0) {
|
|
141
180
|
writeStderr(`failed to set remote clipboard on ${alias}:\n${commandDetail(setRemote)}\n`);
|
|
142
181
|
return 1;
|
|
143
182
|
}
|
|
144
183
|
|
|
145
|
-
writeStdout(
|
|
184
|
+
writeStdout(remotePlatform === 'Linux'
|
|
185
|
+
? `uploaded clipboard image to ${alias}; return to the SSH session and press Ctrl+V\n`
|
|
186
|
+
: `copied clipboard image to ${alias}\n`);
|
|
146
187
|
return 0;
|
|
147
188
|
} finally {
|
|
148
189
|
if (uploaded && remotePath) {
|
package/lib/defaults.json
CHANGED
|
@@ -44,14 +44,23 @@
|
|
|
44
44
|
".gemini/commands/",
|
|
45
45
|
".git-hooks/check-version-format.sh",
|
|
46
46
|
".github/scripts/",
|
|
47
|
+
".github/workflows/metadata-sync.yml",
|
|
48
|
+
".github/workflows/pr-label.yml",
|
|
49
|
+
".github/workflows/status-label.yml",
|
|
47
50
|
".opencode/commands/"
|
|
48
51
|
],
|
|
52
|
+
"guardedManaged": [
|
|
53
|
+
".github/workflows/metadata-sync.yml",
|
|
54
|
+
".github/workflows/pr-label.yml",
|
|
55
|
+
".github/workflows/status-label.yml"
|
|
56
|
+
],
|
|
49
57
|
"merged": [
|
|
50
58
|
"**/post-release.*",
|
|
51
59
|
"**/release.*",
|
|
52
60
|
"**/test-integration.*",
|
|
53
61
|
"**/test.*",
|
|
54
62
|
"**/upgrade-dependency.*",
|
|
63
|
+
".agents/rules/testing-discipline.*",
|
|
55
64
|
".agents/skills/post-release/SKILL.*",
|
|
56
65
|
".agents/skills/release/SKILL.*",
|
|
57
66
|
".agents/skills/test-integration/SKILL.*",
|
package/lib/init.ts
CHANGED
|
@@ -278,6 +278,12 @@ async function cmdInit(): Promise<void> {
|
|
|
278
278
|
platformType
|
|
279
279
|
);
|
|
280
280
|
ok('Installed .agents/skills/update-agent-infra/');
|
|
281
|
+
renderFile(
|
|
282
|
+
path.join(templateDir, '.agents', 'scripts', 'lib', 'agent-infra-package.js'),
|
|
283
|
+
path.join('.agents', 'scripts', 'lib', 'agent-infra-package.js'),
|
|
284
|
+
replacements
|
|
285
|
+
);
|
|
286
|
+
ok('Installed .agents/scripts/lib/agent-infra-package.js');
|
|
281
287
|
|
|
282
288
|
// install Claude command (only if enabled)
|
|
283
289
|
if (enabledTUISet.has('claude-code')) {
|
|
@@ -347,6 +353,9 @@ async function cmdInit(): Promise<void> {
|
|
|
347
353
|
console.log('');
|
|
348
354
|
ok('Project initialized successfully!');
|
|
349
355
|
console.log('');
|
|
356
|
+
console.log(' If this init used npx, install agent-infra persistently before update or validation:');
|
|
357
|
+
console.log(' npm install -g @fitlab-ai/agent-infra');
|
|
358
|
+
console.log('');
|
|
350
359
|
if (enabledTUISet.size === 0) {
|
|
351
360
|
console.log(' No built-in TUI selected.');
|
|
352
361
|
console.log(` Configure "customTUIs" in ${configPath} before running update-agent-infra.`);
|
package/lib/sandbox/capture.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { loadConfig } from './config.ts';
|
|
3
3
|
import { containerNameCandidates, sandboxBranchLabel, sandboxLabel } from './constants.ts';
|
|
4
4
|
import { detectEngine } from './engine.ts';
|
|
5
|
+
import { commandForEngine } from './shell.ts';
|
|
5
6
|
import { hostTimezoneEnvFlags, terminalEnvFlags } from './commands/enter.ts';
|
|
6
7
|
import {
|
|
7
8
|
fetchSandboxRows,
|
|
@@ -95,12 +96,12 @@ function buildRunScript(params: {
|
|
|
95
96
|
return `#!/bin/sh
|
|
96
97
|
set -u
|
|
97
98
|
cd /workspace
|
|
98
|
-
date "+%Y-%m-%d %H:%M:%S
|
|
99
|
+
date "+%Y-%m-%d %H:%M:%S%z" | sed 's/\\([+-][0-9][0-9]\\)\\([0-9][0-9]\\)$/\\1:\\2/' > ${runDir}/started_at
|
|
99
100
|
printf '%s\\n' running > ${runDir}/status
|
|
100
101
|
${command}
|
|
101
102
|
code=$?
|
|
102
103
|
printf '%s\\n' "$code" > ${runDir}/exit_code
|
|
103
|
-
date "+%Y-%m-%d %H:%M:%S
|
|
104
|
+
date "+%Y-%m-%d %H:%M:%S%z" | sed 's/\\([+-][0-9][0-9]\\)\\([0-9][0-9]\\)$/\\1:\\2/' > ${runDir}/finished_at
|
|
104
105
|
if [ "$code" -eq 0 ]; then
|
|
105
106
|
printf '%s\\n' completed > ${runDir}/status
|
|
106
107
|
else
|
|
@@ -202,7 +203,8 @@ export async function runInSandbox(
|
|
|
202
203
|
'-lc',
|
|
203
204
|
buildTmuxLauncher({ request, runId })
|
|
204
205
|
];
|
|
205
|
-
const
|
|
206
|
+
const command = commandForEngine(engine, 'docker', dockerArgs);
|
|
207
|
+
const result = await (options.spawn ?? spawnCapture)(command.cmd, command.args);
|
|
206
208
|
return {
|
|
207
209
|
...result,
|
|
208
210
|
run: {
|