specdacular 0.12.1 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/specd.js +168 -36
- package/package.json +1 -5
- package/runner/main/agent/parser.js +0 -39
- package/runner/main/agent/runner.js +0 -137
- package/runner/main/agent/template.js +0 -16
- package/runner/main/bootstrap.js +0 -69
- package/runner/main/db.js +0 -45
- package/runner/main/index.js +0 -108
- package/runner/main/ipc.js +0 -72
- package/runner/main/notifications/telegram.js +0 -45
- package/runner/main/orchestrator.js +0 -193
- package/runner/main/paths.js +0 -36
- package/runner/main/pipeline/resolver.js +0 -20
- package/runner/main/pipeline/sequencer.js +0 -42
- package/runner/main/server/api.js +0 -125
- package/runner/main/server/index.js +0 -33
- package/runner/main/server/websocket.js +0 -24
- package/runner/main/state/manager.js +0 -83
- package/runner/main/template-manager.js +0 -41
- package/runner/main/test/agent-parser.test.js +0 -44
- package/runner/main/test/bootstrap.test.js +0 -58
- package/runner/main/test/db.test.js +0 -72
- package/runner/main/test/paths.test.js +0 -29
- package/runner/main/test/state-manager.test.js +0 -72
- package/runner/main/test/template-manager.test.js +0 -66
- package/runner/main/worktree/manager.js +0 -95
- package/runner/package.json +0 -20
- package/runner/preload.js +0 -19
- package/runner/renderer/dist/assets/index-B1mwF1ao.js +0 -50
- package/runner/renderer/dist/index.html +0 -12
package/bin/specd.js
CHANGED
|
@@ -3,23 +3,22 @@
|
|
|
3
3
|
// bin/specd.js — unified CLI entry point
|
|
4
4
|
// Usage:
|
|
5
5
|
// specd llm-init [--local] — install commands/agents/workflows
|
|
6
|
-
// specd install-runner —
|
|
7
|
-
// specd runner — launch
|
|
6
|
+
// specd install-runner — download Specd Runner app from GitHub Releases
|
|
7
|
+
// specd runner — launch Specd Runner app
|
|
8
8
|
// specd runner register <path> — register a folder
|
|
9
9
|
// specd runner unregister <id> — remove a project
|
|
10
10
|
// specd runner projects — list projects
|
|
11
11
|
// specd runner status — show task status
|
|
12
12
|
|
|
13
13
|
import { resolve, join } from 'path';
|
|
14
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
15
|
-
import { homedir, platform } from 'os';
|
|
14
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, createWriteStream, unlinkSync } from 'fs';
|
|
15
|
+
import { homedir, platform, arch } from 'os';
|
|
16
16
|
import { execSync, spawn } from 'child_process';
|
|
17
|
+
import { get } from 'https';
|
|
17
18
|
|
|
18
19
|
const args = process.argv.slice(2);
|
|
19
20
|
const command = args[0];
|
|
20
21
|
|
|
21
|
-
const runnerDir = join(import.meta.dirname, '..', 'runner');
|
|
22
|
-
|
|
23
22
|
function getAppDataDir() {
|
|
24
23
|
if (platform() === 'darwin') {
|
|
25
24
|
return join(homedir(), 'Library', 'Application Support', 'Specd');
|
|
@@ -27,6 +26,27 @@ function getAppDataDir() {
|
|
|
27
26
|
return join(homedir(), '.config', 'specd');
|
|
28
27
|
}
|
|
29
28
|
|
|
29
|
+
function getAppInstallDir() {
|
|
30
|
+
if (platform() === 'darwin') {
|
|
31
|
+
return '/Applications';
|
|
32
|
+
}
|
|
33
|
+
return join(getAppDataDir(), 'app');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getRunnerAppPath() {
|
|
37
|
+
if (platform() === 'darwin') {
|
|
38
|
+
return join(getAppInstallDir(), 'Specd Runner.app');
|
|
39
|
+
}
|
|
40
|
+
if (platform() === 'linux') {
|
|
41
|
+
return join(getAppInstallDir(), 'Specd Runner.AppImage');
|
|
42
|
+
}
|
|
43
|
+
return join(getAppInstallDir(), 'Specd Runner.exe');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isRunnerInstalled() {
|
|
47
|
+
return existsSync(getRunnerAppPath());
|
|
48
|
+
}
|
|
49
|
+
|
|
30
50
|
function getDbPath() {
|
|
31
51
|
return join(getAppDataDir(), 'db.json');
|
|
32
52
|
}
|
|
@@ -43,15 +63,139 @@ function saveDb(data) {
|
|
|
43
63
|
writeFileSync(dbPath, JSON.stringify(data, null, 2));
|
|
44
64
|
}
|
|
45
65
|
|
|
46
|
-
function
|
|
47
|
-
return
|
|
66
|
+
function fetchJson(url) {
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
get(url, { headers: { 'User-Agent': 'specd-cli' } }, (res) => {
|
|
69
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
70
|
+
return fetchJson(res.headers.location).then(resolve, reject);
|
|
71
|
+
}
|
|
72
|
+
let data = '';
|
|
73
|
+
res.on('data', (chunk) => data += chunk);
|
|
74
|
+
res.on('end', () => {
|
|
75
|
+
try { resolve(JSON.parse(data)); }
|
|
76
|
+
catch (e) { reject(new Error(`Failed to parse response: ${data.slice(0, 200)}`)); }
|
|
77
|
+
});
|
|
78
|
+
}).on('error', reject);
|
|
79
|
+
});
|
|
48
80
|
}
|
|
49
81
|
|
|
50
|
-
function
|
|
51
|
-
|
|
52
|
-
|
|
82
|
+
function download(url, dest) {
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
const follow = (url) => {
|
|
85
|
+
get(url, { headers: { 'User-Agent': 'specd-cli' } }, (res) => {
|
|
86
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
87
|
+
return follow(res.headers.location);
|
|
88
|
+
}
|
|
89
|
+
if (res.statusCode !== 200) {
|
|
90
|
+
return reject(new Error(`Download failed: HTTP ${res.statusCode}`));
|
|
91
|
+
}
|
|
92
|
+
const total = parseInt(res.headers['content-length'], 10);
|
|
93
|
+
let downloaded = 0;
|
|
94
|
+
const file = createWriteStream(dest);
|
|
95
|
+
res.on('data', (chunk) => {
|
|
96
|
+
downloaded += chunk.length;
|
|
97
|
+
if (total) {
|
|
98
|
+
const pct = Math.round(downloaded / total * 100);
|
|
99
|
+
process.stdout.write(`\rDownloading... ${pct}%`);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
res.pipe(file);
|
|
103
|
+
file.on('finish', () => {
|
|
104
|
+
file.close();
|
|
105
|
+
console.log('\rDownload complete. ');
|
|
106
|
+
resolve();
|
|
107
|
+
});
|
|
108
|
+
}).on('error', reject);
|
|
109
|
+
};
|
|
110
|
+
follow(url);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function installRunner() {
|
|
115
|
+
const os = platform();
|
|
116
|
+
const cpuArch = arch();
|
|
117
|
+
|
|
118
|
+
console.log(`Fetching latest Specd Runner release...`);
|
|
119
|
+
|
|
120
|
+
const release = await fetchJson(
|
|
121
|
+
'https://api.github.com/repos/victorbalan/specdacular/releases/latest'
|
|
122
|
+
).catch(() => null);
|
|
123
|
+
|
|
124
|
+
// Also check for runner-specific tags
|
|
125
|
+
const releases = await fetchJson(
|
|
126
|
+
'https://api.github.com/repos/victorbalan/specdacular/releases?per_page=10'
|
|
127
|
+
).catch(() => []);
|
|
128
|
+
|
|
129
|
+
const runnerRelease = releases.find(r => r.tag_name?.startsWith('runner-v')) || release;
|
|
130
|
+
|
|
131
|
+
if (!runnerRelease || !runnerRelease.assets?.length) {
|
|
132
|
+
console.error('No runner release found. The Specd Runner app has not been published yet.');
|
|
133
|
+
console.error('For local development, run from the repo: cd runner && npm install && npm run dev');
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Find the right asset for this platform
|
|
138
|
+
let assetName;
|
|
139
|
+
if (os === 'darwin') {
|
|
140
|
+
// Prefer dmg, fall back to zip
|
|
141
|
+
const dmg = runnerRelease.assets.find(a => a.name.endsWith('.dmg') && (
|
|
142
|
+
a.name.includes('arm64') ? cpuArch === 'arm64' : cpuArch === 'x64'
|
|
143
|
+
));
|
|
144
|
+
const universalDmg = runnerRelease.assets.find(a => a.name.endsWith('.dmg') && a.name.includes('universal'));
|
|
145
|
+
const anyDmg = runnerRelease.assets.find(a => a.name.endsWith('.dmg'));
|
|
146
|
+
assetName = dmg || universalDmg || anyDmg;
|
|
147
|
+
} else if (os === 'linux') {
|
|
148
|
+
assetName = runnerRelease.assets.find(a => a.name.endsWith('.AppImage'));
|
|
149
|
+
} else if (os === 'win32') {
|
|
150
|
+
assetName = runnerRelease.assets.find(a => a.name.endsWith('.exe'));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!assetName) {
|
|
154
|
+
console.error(`No runner build found for ${os}/${cpuArch}.`);
|
|
155
|
+
console.error('Available assets:', runnerRelease.assets.map(a => a.name).join(', '));
|
|
53
156
|
process.exit(1);
|
|
54
157
|
}
|
|
158
|
+
|
|
159
|
+
const tmpDir = join(getAppDataDir(), 'tmp');
|
|
160
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
161
|
+
const tmpFile = join(tmpDir, assetName.name);
|
|
162
|
+
|
|
163
|
+
console.log(`Downloading ${assetName.name} (${(assetName.size / 1024 / 1024).toFixed(1)} MB)...`);
|
|
164
|
+
await download(assetName.browser_download_url, tmpFile);
|
|
165
|
+
|
|
166
|
+
if (os === 'darwin') {
|
|
167
|
+
if (assetName.name.endsWith('.dmg')) {
|
|
168
|
+
console.log('Mounting DMG and copying app...');
|
|
169
|
+
const mountOutput = execSync(`hdiutil attach "${tmpFile}" -nobrowse -quiet`, { encoding: 'utf-8' });
|
|
170
|
+
const mountPoint = mountOutput.trim().split('\t').pop().trim();
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
execSync(`cp -R "${mountPoint}/Specd Runner.app" "/Applications/"`, { stdio: 'pipe' });
|
|
174
|
+
} finally {
|
|
175
|
+
execSync(`hdiutil detach "${mountPoint}" -quiet`, { stdio: 'pipe' });
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
// zip
|
|
179
|
+
execSync(`unzip -o "${tmpFile}" -d "/Applications/"`, { stdio: 'pipe' });
|
|
180
|
+
}
|
|
181
|
+
console.log('Specd Runner installed to /Applications/Specd Runner.app');
|
|
182
|
+
} else if (os === 'linux') {
|
|
183
|
+
const installDir = getAppInstallDir();
|
|
184
|
+
mkdirSync(installDir, { recursive: true });
|
|
185
|
+
const dest = join(installDir, 'Specd Runner.AppImage');
|
|
186
|
+
execSync(`cp "${tmpFile}" "${dest}" && chmod +x "${dest}"`);
|
|
187
|
+
console.log(`Specd Runner installed to ${dest}`);
|
|
188
|
+
} else {
|
|
189
|
+
const installDir = getAppInstallDir();
|
|
190
|
+
mkdirSync(installDir, { recursive: true });
|
|
191
|
+
execSync(`cp "${tmpFile}" "${join(installDir, 'Specd Runner.exe')}"`);
|
|
192
|
+
console.log(`Specd Runner installed to ${installDir}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Cleanup
|
|
196
|
+
try { unlinkSync(tmpFile); } catch {}
|
|
197
|
+
|
|
198
|
+
console.log('Done! Run: specd runner');
|
|
55
199
|
}
|
|
56
200
|
|
|
57
201
|
if (command === 'llm-init') {
|
|
@@ -61,19 +205,7 @@ if (command === 'llm-init') {
|
|
|
61
205
|
await import(installScript);
|
|
62
206
|
|
|
63
207
|
} else if (command === 'install-runner') {
|
|
64
|
-
|
|
65
|
-
execSync('npm install --omit=dev', { cwd: runnerDir, stdio: 'inherit' });
|
|
66
|
-
|
|
67
|
-
// Also install renderer deps and build
|
|
68
|
-
const rendererDir = join(runnerDir, 'renderer');
|
|
69
|
-
if (existsSync(join(rendererDir, 'package.json'))) {
|
|
70
|
-
console.log('Installing renderer dependencies...');
|
|
71
|
-
execSync('npm install', { cwd: rendererDir, stdio: 'inherit' });
|
|
72
|
-
console.log('Building renderer...');
|
|
73
|
-
execSync('npm run build', { cwd: rendererDir, stdio: 'inherit' });
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
console.log('Runner installed. Run: specd runner');
|
|
208
|
+
await installRunner();
|
|
77
209
|
|
|
78
210
|
} else if (command === 'runner') {
|
|
79
211
|
const subcommand = args[1];
|
|
@@ -133,26 +265,26 @@ if (command === 'llm-init') {
|
|
|
133
265
|
console.error('Runner not running. Start it with: specd runner');
|
|
134
266
|
}
|
|
135
267
|
} else {
|
|
136
|
-
// No subcommand — launch
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const electronPath = join(runnerDir, 'node_modules', '.bin', 'electron');
|
|
140
|
-
if (!existsSync(electronPath)) {
|
|
141
|
-
console.error('Electron not installed. Run: specd install-runner');
|
|
268
|
+
// No subcommand — launch the app
|
|
269
|
+
if (!isRunnerInstalled()) {
|
|
270
|
+
console.error('Specd Runner not installed. Run: specd install-runner');
|
|
142
271
|
process.exit(1);
|
|
143
272
|
}
|
|
144
273
|
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
stdio: 'ignore'
|
|
148
|
-
})
|
|
149
|
-
|
|
274
|
+
const appPath = getRunnerAppPath();
|
|
275
|
+
if (platform() === 'darwin') {
|
|
276
|
+
spawn('open', ['-a', appPath], { detached: true, stdio: 'ignore' }).unref();
|
|
277
|
+
} else if (platform() === 'linux') {
|
|
278
|
+
spawn(appPath, [], { detached: true, stdio: 'ignore' }).unref();
|
|
279
|
+
} else {
|
|
280
|
+
spawn(appPath, [], { detached: true, stdio: 'ignore' }).unref();
|
|
281
|
+
}
|
|
150
282
|
console.log('Specd Runner launched.');
|
|
151
283
|
}
|
|
152
284
|
} else {
|
|
153
285
|
console.log('Usage:');
|
|
154
286
|
console.log(' specd llm-init [--local] Install Claude Code commands/agents');
|
|
155
|
-
console.log(' specd install-runner
|
|
287
|
+
console.log(' specd install-runner Download and install the Specd Runner app');
|
|
156
288
|
console.log(' specd runner Launch the Specd Runner app');
|
|
157
289
|
console.log(' specd runner register <path> Register a project folder');
|
|
158
290
|
console.log(' specd runner unregister <id> Remove a project');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specdacular",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "Feature planning system for existing codebases. Map, understand, and plan features in large projects.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"specd": "bin/specd.js"
|
|
@@ -11,10 +11,6 @@
|
|
|
11
11
|
"agents",
|
|
12
12
|
"specdacular",
|
|
13
13
|
"hooks",
|
|
14
|
-
"runner/main",
|
|
15
|
-
"runner/renderer/dist",
|
|
16
|
-
"runner/preload.js",
|
|
17
|
-
"runner/package.json",
|
|
18
14
|
"README.md"
|
|
19
15
|
],
|
|
20
16
|
"keywords": [
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from 'events';
|
|
2
|
-
|
|
3
|
-
export class StreamParser extends EventEmitter {
|
|
4
|
-
constructor() {
|
|
5
|
-
super();
|
|
6
|
-
this.inBlock = null;
|
|
7
|
-
this.blockLines = [];
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
feed(line) {
|
|
11
|
-
if (line.startsWith('```specd-status')) {
|
|
12
|
-
this.inBlock = 'status';
|
|
13
|
-
this.blockLines = [];
|
|
14
|
-
return;
|
|
15
|
-
}
|
|
16
|
-
if (line.startsWith('```specd-result')) {
|
|
17
|
-
this.inBlock = 'result';
|
|
18
|
-
this.blockLines = [];
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
if (line === '```' && this.inBlock) {
|
|
22
|
-
const content = this.blockLines.join('\n');
|
|
23
|
-
try {
|
|
24
|
-
const parsed = JSON.parse(content);
|
|
25
|
-
this.emit(this.inBlock, parsed);
|
|
26
|
-
} catch (err) {
|
|
27
|
-
this.emit('error', err);
|
|
28
|
-
}
|
|
29
|
-
this.inBlock = null;
|
|
30
|
-
this.blockLines = [];
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
if (this.inBlock) {
|
|
34
|
-
this.blockLines.push(line);
|
|
35
|
-
} else {
|
|
36
|
-
this.emit('output', line);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
|
-
import { EventEmitter } from 'events';
|
|
3
|
-
import { createWriteStream } from 'fs';
|
|
4
|
-
import { StreamParser } from './parser.js';
|
|
5
|
-
|
|
6
|
-
export class AgentRunner extends EventEmitter {
|
|
7
|
-
constructor({ cmd, input_mode, output_format, system_prompt, timeout, stuck_timeout }) {
|
|
8
|
-
super();
|
|
9
|
-
this.cmd = cmd;
|
|
10
|
-
this.inputMode = input_mode || 'stdin';
|
|
11
|
-
this.outputFormat = output_format || 'stream_json';
|
|
12
|
-
this.systemPrompt = system_prompt || '';
|
|
13
|
-
this.timeout = (timeout || 3600) * 1000;
|
|
14
|
-
this.stuckTimeout = (stuck_timeout || 1800) * 1000;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
async run(prompt, { cwd, logPath } = {}) {
|
|
18
|
-
return new Promise((resolve, reject) => {
|
|
19
|
-
const fullPrompt = this.systemPrompt ? `${this.systemPrompt}\n\n${prompt}` : prompt;
|
|
20
|
-
const args = this.cmd.split(' ').slice(1);
|
|
21
|
-
const bin = this.cmd.split(' ')[0];
|
|
22
|
-
|
|
23
|
-
const proc = spawn(bin, args, {
|
|
24
|
-
cwd,
|
|
25
|
-
shell: true,
|
|
26
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
27
|
-
env: { ...process.env },
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
const logStream = logPath ? createWriteStream(logPath, { flags: 'a' }) : null;
|
|
31
|
-
let lastOutputAt = Date.now();
|
|
32
|
-
let result = null;
|
|
33
|
-
|
|
34
|
-
const parser = new StreamParser();
|
|
35
|
-
parser.on('status', (s) => {
|
|
36
|
-
lastOutputAt = Date.now();
|
|
37
|
-
this.emit('status', s);
|
|
38
|
-
});
|
|
39
|
-
parser.on('result', (r) => {
|
|
40
|
-
lastOutputAt = Date.now();
|
|
41
|
-
result = r;
|
|
42
|
-
this.emit('result', r);
|
|
43
|
-
});
|
|
44
|
-
parser.on('output', (line) => {
|
|
45
|
-
lastOutputAt = Date.now();
|
|
46
|
-
this.emit('output', line);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
const handleLine = (line) => {
|
|
50
|
-
if (logStream) logStream.write(line + '\n');
|
|
51
|
-
|
|
52
|
-
if (this.outputFormat === 'stream_json') {
|
|
53
|
-
try {
|
|
54
|
-
const event = JSON.parse(line);
|
|
55
|
-
if (event.type === 'assistant' && event.message?.content) {
|
|
56
|
-
for (const block of event.message.content) {
|
|
57
|
-
if (block.type === 'text') {
|
|
58
|
-
for (const textLine of block.text.split('\n')) {
|
|
59
|
-
parser.feed(textLine);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
} else if (event.type === 'result' && event.result) {
|
|
64
|
-
for (const block of event.result) {
|
|
65
|
-
if (block.type === 'text') {
|
|
66
|
-
for (const textLine of block.text.split('\n')) {
|
|
67
|
-
parser.feed(textLine);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
} catch {
|
|
73
|
-
parser.feed(line);
|
|
74
|
-
}
|
|
75
|
-
} else {
|
|
76
|
-
parser.feed(line);
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
let stdout = '';
|
|
81
|
-
proc.stdout.on('data', (chunk) => {
|
|
82
|
-
stdout += chunk.toString();
|
|
83
|
-
const lines = stdout.split('\n');
|
|
84
|
-
stdout = lines.pop();
|
|
85
|
-
for (const line of lines) {
|
|
86
|
-
if (line.trim()) handleLine(line.trim());
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
proc.stderr.on('data', (chunk) => {
|
|
91
|
-
if (logStream) logStream.write(`[stderr] ${chunk}`);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
if (this.inputMode === 'stdin') {
|
|
95
|
-
proc.stdin.write(fullPrompt);
|
|
96
|
-
proc.stdin.end();
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Global timeout
|
|
100
|
-
const globalTimer = setTimeout(() => {
|
|
101
|
-
proc.kill('SIGTERM');
|
|
102
|
-
setTimeout(() => proc.kill('SIGKILL'), 5000);
|
|
103
|
-
}, this.timeout);
|
|
104
|
-
|
|
105
|
-
// Stuck detection
|
|
106
|
-
const stuckCheck = setInterval(() => {
|
|
107
|
-
if (Date.now() - lastOutputAt > this.stuckTimeout) {
|
|
108
|
-
this.emit('error', new Error('Agent stuck — no output'));
|
|
109
|
-
proc.kill('SIGTERM');
|
|
110
|
-
setTimeout(() => proc.kill('SIGKILL'), 5000);
|
|
111
|
-
}
|
|
112
|
-
}, 30000);
|
|
113
|
-
|
|
114
|
-
proc.on('close', (code) => {
|
|
115
|
-
clearTimeout(globalTimer);
|
|
116
|
-
clearInterval(stuckCheck);
|
|
117
|
-
if (logStream) logStream.end();
|
|
118
|
-
if (stdout.trim()) handleLine(stdout.trim());
|
|
119
|
-
|
|
120
|
-
if (result) {
|
|
121
|
-
resolve(result);
|
|
122
|
-
} else if (code === 0) {
|
|
123
|
-
resolve({ status: 'success', summary: 'Agent completed without explicit result' });
|
|
124
|
-
} else {
|
|
125
|
-
resolve({ status: 'failure', summary: `Agent exited with code ${code}` });
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
proc.on('error', (err) => {
|
|
130
|
-
clearTimeout(globalTimer);
|
|
131
|
-
clearInterval(stuckCheck);
|
|
132
|
-
if (logStream) logStream.end();
|
|
133
|
-
reject(err);
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export function resolveTemplate(template, variables) {
|
|
2
|
-
return template.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
|
|
3
|
-
const value = path.trim().split('.').reduce((obj, key) => obj?.[key], variables);
|
|
4
|
-
return value !== undefined ? String(value) : match;
|
|
5
|
-
});
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function buildTemplateContext(task, stage, pipeline, paths) {
|
|
9
|
-
return {
|
|
10
|
-
task: { id: task.id, name: task.name, spec: task.spec || '' },
|
|
11
|
-
stage: { name: stage.stage, index: stage.index, total: stage.total },
|
|
12
|
-
pipeline: { name: pipeline.name },
|
|
13
|
-
status_file: paths?.statusJson || '',
|
|
14
|
-
log_dir: paths?.logsDir || '',
|
|
15
|
-
};
|
|
16
|
-
}
|
package/runner/main/bootstrap.js
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync, existsSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
3
|
-
|
|
4
|
-
const DEFAULT_CONFIG = {
|
|
5
|
-
server: { port: 3700 },
|
|
6
|
-
notifications: { telegram: { enabled: false } },
|
|
7
|
-
defaults: {
|
|
8
|
-
pipeline: 'default',
|
|
9
|
-
failure_policy: 'skip',
|
|
10
|
-
timeout: 3600,
|
|
11
|
-
stuck_timeout: 1800,
|
|
12
|
-
max_parallel: 1,
|
|
13
|
-
},
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const DEFAULT_AGENTS = {
|
|
17
|
-
'claude-planner': {
|
|
18
|
-
cmd: 'claude -p --dangerously-skip-permissions',
|
|
19
|
-
input_mode: 'stdin',
|
|
20
|
-
output_format: 'stream_json',
|
|
21
|
-
system_prompt: 'You are a feature planner working on: {{task.name}} ({{task.id}})\nPipeline: {{pipeline.name}} | Stage: {{stage.name}} ({{stage.index}}/{{stage.total}})\n\nResearch the codebase thoroughly, then create a detailed implementation plan.\n\nEmit progress:\n```specd-status\n{"task_id":"{{task.id}}","stage":"{{stage.name}}","progress":"...","percent":0}\n```\n\nWhen done:\n```specd-result\n{"status":"success","summary":"...","files_changed":[],"issues":[]}\n```',
|
|
22
|
-
},
|
|
23
|
-
'claude-implementer': {
|
|
24
|
-
cmd: 'claude -p --dangerously-skip-permissions',
|
|
25
|
-
input_mode: 'stdin',
|
|
26
|
-
output_format: 'stream_json',
|
|
27
|
-
system_prompt: 'You are an implementer working on: {{task.name}} ({{task.id}})\nPipeline: {{pipeline.name}} | Stage: {{stage.name}} ({{stage.index}}/{{stage.total}})\n\nImplement the plan from the previous stage. Write clean, tested code.\n\nEmit progress:\n```specd-status\n{"task_id":"{{task.id}}","stage":"{{stage.name}}","progress":"...","percent":0}\n```\n\nWhen done:\n```specd-result\n{"status":"success","summary":"...","files_changed":[],"issues":[]}\n```',
|
|
28
|
-
},
|
|
29
|
-
'claude-reviewer': {
|
|
30
|
-
cmd: 'claude -p --dangerously-skip-permissions',
|
|
31
|
-
input_mode: 'stdin',
|
|
32
|
-
output_format: 'stream_json',
|
|
33
|
-
system_prompt: 'You are a code reviewer for: {{task.name}} ({{task.id}})\nPipeline: {{pipeline.name}} | Stage: {{stage.name}} ({{stage.index}}/{{stage.total}})\n\nReview the implementation from the previous stage. Check for bugs, style, tests.\n\nEmit progress:\n```specd-status\n{"task_id":"{{task.id}}","stage":"{{stage.name}}","progress":"...","percent":0}\n```\n\nWhen done:\n```specd-result\n{"status":"success","summary":"...","files_changed":[],"issues":[]}\n```',
|
|
34
|
-
},
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const DEFAULT_PIPELINE = {
|
|
38
|
-
name: 'default',
|
|
39
|
-
stages: [
|
|
40
|
-
{ stage: 'plan', agent: 'claude-planner', critical: true },
|
|
41
|
-
{ stage: 'implement', agent: 'claude-implementer', critical: true },
|
|
42
|
-
{ stage: 'review', agent: 'claude-reviewer', on_fail: 'retry', max_retries: 2 },
|
|
43
|
-
],
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
function writeIfMissing(filePath, data) {
|
|
47
|
-
if (!existsSync(filePath)) {
|
|
48
|
-
writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export async function bootstrap(paths) {
|
|
53
|
-
// Create directories
|
|
54
|
-
mkdirSync(paths.agentTemplatesDir, { recursive: true });
|
|
55
|
-
mkdirSync(paths.pipelineTemplatesDir, { recursive: true });
|
|
56
|
-
mkdirSync(paths.projectsDir, { recursive: true });
|
|
57
|
-
|
|
58
|
-
// Write default files
|
|
59
|
-
writeIfMissing(paths.db, { projects: [] });
|
|
60
|
-
writeIfMissing(paths.config, DEFAULT_CONFIG);
|
|
61
|
-
|
|
62
|
-
// Write default agent templates
|
|
63
|
-
for (const [name, agent] of Object.entries(DEFAULT_AGENTS)) {
|
|
64
|
-
writeIfMissing(join(paths.agentTemplatesDir, `${name}.json`), agent);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Write default pipeline template
|
|
68
|
-
writeIfMissing(join(paths.pipelineTemplatesDir, 'default.json'), DEFAULT_PIPELINE);
|
|
69
|
-
}
|
package/runner/main/db.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from 'fs';
|
|
2
|
-
import { randomUUID } from 'crypto';
|
|
3
|
-
|
|
4
|
-
export class ProjectDB {
|
|
5
|
-
constructor(dbPath) {
|
|
6
|
-
this.dbPath = dbPath;
|
|
7
|
-
this.data = JSON.parse(readFileSync(dbPath, 'utf-8'));
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
register(name, folderPath) {
|
|
11
|
-
const project = {
|
|
12
|
-
id: randomUUID().slice(0, 8),
|
|
13
|
-
name,
|
|
14
|
-
path: folderPath,
|
|
15
|
-
active: true,
|
|
16
|
-
registeredAt: new Date().toISOString(),
|
|
17
|
-
};
|
|
18
|
-
this.data.projects.push(project);
|
|
19
|
-
this._save();
|
|
20
|
-
return project;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
unregister(id) {
|
|
24
|
-
this.data.projects = this.data.projects.filter(p => p.id !== id);
|
|
25
|
-
this._save();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
get(id) {
|
|
29
|
-
return this.data.projects.find(p => p.id === id) || null;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
findByPath(folderPath) {
|
|
33
|
-
return this.data.projects.find(p =>
|
|
34
|
-
folderPath === p.path || folderPath.startsWith(p.path + '/')
|
|
35
|
-
) || null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
list() {
|
|
39
|
-
return this.data.projects;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
_save() {
|
|
43
|
-
writeFileSync(this.dbPath, JSON.stringify(this.data, null, 2));
|
|
44
|
-
}
|
|
45
|
-
}
|