agentboss 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -0
- package/bin/aboss.js +288 -0
- package/client/dist/assets/index-C1wFD_Vo.css +1 -0
- package/client/dist/assets/index-DBj1Ujlx.js +137 -0
- package/client/dist/index.html +34 -0
- package/package.json +64 -0
- package/server/analysis/daily-aggregator.js +258 -0
- package/server/analysis/difficulty.js +129 -0
- package/server/analysis/dimensions/ai-knowledge.js +172 -0
- package/server/analysis/dimensions/ai-tools.js +161 -0
- package/server/analysis/dimensions/judgement.js +107 -0
- package/server/analysis/dimensions/llm-merge.js +57 -0
- package/server/analysis/dimensions/output-quality.js +167 -0
- package/server/analysis/dimensions/problem-definition.js +104 -0
- package/server/analysis/dimensions/system-thinking.js +225 -0
- package/server/analysis/evidence-builder.js +104 -0
- package/server/analysis/job.js +273 -0
- package/server/analysis/report-builder.js +581 -0
- package/server/analysis/scoring-v2.js +72 -0
- package/server/analysis/text-signals.js +179 -0
- package/server/analysis/thresholds-v2.js +358 -0
- package/server/api/advice.js +124 -0
- package/server/api/analysis.js +141 -0
- package/server/api/execution.js +330 -0
- package/server/api/metrics.js +277 -0
- package/server/api/overview.js +308 -0
- package/server/api/project.js +255 -0
- package/server/api/reports.js +125 -0
- package/server/api/sessions.js +118 -0
- package/server/api/settings.js +119 -0
- package/server/db/connection.js +175 -0
- package/server/db/queries.js +1051 -0
- package/server/db/schema.js +487 -0
- package/server/etl/active-time.js +150 -0
- package/server/etl/backfill-subagents.js +178 -0
- package/server/etl/claude-code.js +826 -0
- package/server/etl/detect.js +341 -0
- package/server/etl/judge-filter.js +117 -0
- package/server/etl/opencode.js +606 -0
- package/server/execution/job.js +662 -0
- package/server/execution/prompt.js +227 -0
- package/server/execution/runner.js +218 -0
- package/server/index.js +94 -0
- package/server/llm/advice-prompt.js +339 -0
- package/server/llm/advice.js +384 -0
- package/server/llm/analysis-prompt.js +162 -0
- package/server/llm/cli-runner.js +249 -0
- package/server/llm/judge-prompts.js +179 -0
- package/server/llm/judge.js +118 -0
- package/server/llm/project-advice-prompt.js +332 -0
- package/server/llm/project-advice.js +491 -0
- package/server/llm/session-analyzer.js +122 -0
- package/server/utils/project.js +80 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execution prompt builder — turns an AdviceItem + its source session
|
|
3
|
+
* into a single self-contained instruction for `opencode run` / `claude -p`
|
|
4
|
+
* to run inside the session's project directory.
|
|
5
|
+
*
|
|
6
|
+
* Spec: docs/superpowers/specs/2026-06-13-advice-execution-design.md §6
|
|
7
|
+
*
|
|
8
|
+
* # Independence
|
|
9
|
+
*
|
|
10
|
+
* Execution has its own sentinel (EXECUTION_SENTINEL). Registered in
|
|
11
|
+
* server/etl/judge-filter.js so the resulting CLI session gets filtered
|
|
12
|
+
* out of the user's own data. This file does NOT import from
|
|
13
|
+
* judge-prompts or advice-prompt; the three families are decoupled.
|
|
14
|
+
*
|
|
15
|
+
* # Versioning
|
|
16
|
+
*
|
|
17
|
+
* EXECUTION_PROMPT_VERSION is informational for the MVP — execution
|
|
18
|
+
* results are immutable facts (something either happened or didn't),
|
|
19
|
+
* so we don't store the version per-run and don't invalidate history
|
|
20
|
+
* on bump. Bump anyway to keep audit trails.
|
|
21
|
+
*
|
|
22
|
+
* @author Felix
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Constants
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
const EXECUTION_SENTINEL = '[ABOSS-EXEC]';
|
|
32
|
+
const EXECUTION_PROMPT_VERSION = 1;
|
|
33
|
+
|
|
34
|
+
/** Max chars per recent-user-message snippet sent to the executor. */
|
|
35
|
+
const MSG_CAP_CHARS = 400;
|
|
36
|
+
const MAX_RECENT_MSGS = 5;
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Helpers
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
function trimTo(s, n) {
|
|
43
|
+
if (!s) return '';
|
|
44
|
+
return s.length > n ? s.slice(0, n) + '…' : s;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function fmtRecent(messages) {
|
|
48
|
+
if (!Array.isArray(messages) || messages.length === 0) return '(无)';
|
|
49
|
+
return messages
|
|
50
|
+
.map((m, i) => `${i + 1}. ${trimTo(String(m.text || '').replace(/\s+/g, ' '), MSG_CAP_CHARS)}`)
|
|
51
|
+
.join('\n');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Builder
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Assemble the executor prompt.
|
|
60
|
+
*
|
|
61
|
+
* @param {object} input
|
|
62
|
+
* @param {object} input.advice AdviceItem (with category attached)
|
|
63
|
+
* @param {string} input.advice.category e.g. 'cost'
|
|
64
|
+
* @param {string} input.advice.severity
|
|
65
|
+
* @param {string} input.advice.title
|
|
66
|
+
* @param {string} input.advice.why
|
|
67
|
+
* @param {string} input.advice.action
|
|
68
|
+
* @param {string} input.advice.evidence
|
|
69
|
+
* @param {object} input.session
|
|
70
|
+
* @param {string} input.session.project
|
|
71
|
+
* @param {string} input.session.title
|
|
72
|
+
* @param {string} input.session.model
|
|
73
|
+
* @param {number} input.session.durationMinutes
|
|
74
|
+
* @param {number} input.session.messageCount
|
|
75
|
+
* @param {{role:string, text:string}[]} input.recentUserMessages
|
|
76
|
+
* @returns {string}
|
|
77
|
+
*/
|
|
78
|
+
function buildExecutionPrompt({ advice, session, recentUserMessages }) {
|
|
79
|
+
const a = advice || {};
|
|
80
|
+
const s = session || {};
|
|
81
|
+
return `${EXECUTION_SENTINEL}(内部标记,忽略本行)
|
|
82
|
+
|
|
83
|
+
# 任务
|
|
84
|
+
|
|
85
|
+
我是 Agent Boss,在分析一段开发者与 AI 助手过去的会话时,得出了一条
|
|
86
|
+
可执行的改进建议。请在当前项目里实施这条建议,然后按指定 JSON 格式
|
|
87
|
+
报告你的行动。
|
|
88
|
+
|
|
89
|
+
# 来源会话
|
|
90
|
+
|
|
91
|
+
项目: ${s.project || '未知'}
|
|
92
|
+
标题: ${s.title || '(无标题)'}
|
|
93
|
+
模型: ${s.model || '未知'}
|
|
94
|
+
时长: ${s.durationMinutes ?? '?'} 分钟
|
|
95
|
+
消息: ${s.messageCount ?? '?'} 条
|
|
96
|
+
|
|
97
|
+
# 改进建议
|
|
98
|
+
|
|
99
|
+
类别: ${a.category || '?'}
|
|
100
|
+
严重度: ${a.severity || '?'}
|
|
101
|
+
标题: ${a.title || '(无)'}
|
|
102
|
+
|
|
103
|
+
现状: ${a.why || '(无)'}
|
|
104
|
+
动作: ${a.action || '(无)'}
|
|
105
|
+
依据: ${a.evidence || '(无)'}
|
|
106
|
+
|
|
107
|
+
# 最近 ${Math.min((recentUserMessages || []).length, MAX_RECENT_MSGS)} 条用户消息(供你定位上下文,不代表当前任务)
|
|
108
|
+
|
|
109
|
+
${fmtRecent(recentUserMessages)}
|
|
110
|
+
|
|
111
|
+
# 报告格式
|
|
112
|
+
|
|
113
|
+
完成后(无论成功失败),请在你的最后一条消息里输出一段严格 JSON:
|
|
114
|
+
|
|
115
|
+
{
|
|
116
|
+
"ok": true | false,
|
|
117
|
+
"summary": "≤80 字 总结你做了什么(或为什么没做)",
|
|
118
|
+
"files_changed": ["相对路径", ...],
|
|
119
|
+
"notes": "可选,任何补充"
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
如果你判断这条建议不应该执行(过时、与代码冲突、需要更多信息),
|
|
123
|
+
直接返回 ok=false 并在 notes 里说明,不要硬改。`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// Project-level execution prompt
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Build a prompt for executing a PROJECT-LEVEL AdviceItem.
|
|
132
|
+
*
|
|
133
|
+
* The big differences vs. session-level:
|
|
134
|
+
* • the AdviceItem describes a cross-session pattern, not a single
|
|
135
|
+
* conversation problem — so we don't have "recent user messages" to
|
|
136
|
+
* give as context. Instead we pass the cross-session patterns the
|
|
137
|
+
* analyser surfaced.
|
|
138
|
+
* • the action is typically "build a skill", "change a config", "set
|
|
139
|
+
* up a workflow" — durable changes to the project, not single-shot
|
|
140
|
+
* code edits.
|
|
141
|
+
*
|
|
142
|
+
* Re-uses the same EXECUTION_SENTINEL so judge-filter.js keeps filtering
|
|
143
|
+
* the executor's own session out of the user's ETL.
|
|
144
|
+
*
|
|
145
|
+
* @param {object} input
|
|
146
|
+
* @param {object} input.advice AdviceItem (with .category)
|
|
147
|
+
* @param {object} input.project
|
|
148
|
+
* @param {string} input.project.path
|
|
149
|
+
* @param {string} input.project.scope 'daily'|'weekly'|'all'
|
|
150
|
+
* @param {string} input.project.windowFrom
|
|
151
|
+
* @param {string} input.project.windowTo
|
|
152
|
+
* @param {number} input.project.sessionCount number of sessions analysed
|
|
153
|
+
* @param {string[]} input.crossSessionPatterns
|
|
154
|
+
* @returns {string}
|
|
155
|
+
*/
|
|
156
|
+
function buildProjectExecutionPrompt({ advice, project, crossSessionPatterns }) {
|
|
157
|
+
const a = advice || {};
|
|
158
|
+
const p = project || {};
|
|
159
|
+
const windowLabel =
|
|
160
|
+
p.scope === 'all' ? '全部历史'
|
|
161
|
+
: (p.windowFrom && p.windowTo)
|
|
162
|
+
? (p.windowFrom === p.windowTo ? p.windowFrom : `${p.windowFrom} → ${p.windowTo}`)
|
|
163
|
+
: '?';
|
|
164
|
+
const patterns = Array.isArray(crossSessionPatterns) && crossSessionPatterns.length
|
|
165
|
+
? crossSessionPatterns.map((s, i) => `${i + 1}. ${s}`).join('\n')
|
|
166
|
+
: '(无)';
|
|
167
|
+
|
|
168
|
+
return `${EXECUTION_SENTINEL}(内部标记,忽略本行)
|
|
169
|
+
|
|
170
|
+
# 任务
|
|
171
|
+
|
|
172
|
+
我是 Agent Boss,在分析这个项目最近多次开发会话之后,得出了一条
|
|
173
|
+
**项目级**的可执行改进建议。请在当前项目里实施这条建议。
|
|
174
|
+
|
|
175
|
+
项目级建议通常是**持久化的改变**——新增 / 修改一个 opencode skill、
|
|
176
|
+
调整某个配置、写一份惯例文档、补一段脚本——目的是让以后所有会话都
|
|
177
|
+
能从中受益,而不是修一段具体的业务代码。
|
|
178
|
+
|
|
179
|
+
# 项目
|
|
180
|
+
|
|
181
|
+
路径: ${p.path || '?'}
|
|
182
|
+
分析范围: ${p.scope || '?'} · ${windowLabel}
|
|
183
|
+
覆盖会话数: ${p.sessionCount ?? '?'}
|
|
184
|
+
|
|
185
|
+
# 跨会话观察到的模式
|
|
186
|
+
|
|
187
|
+
${patterns}
|
|
188
|
+
|
|
189
|
+
# 改进建议
|
|
190
|
+
|
|
191
|
+
类别: ${a.category || '?'}
|
|
192
|
+
严重度: ${a.severity || '?'}
|
|
193
|
+
标题: ${a.title || '(无)'}
|
|
194
|
+
|
|
195
|
+
现状: ${a.why || '(无)'}
|
|
196
|
+
动作: ${a.action || '(无)'}
|
|
197
|
+
依据: ${a.evidence || '(无)'}
|
|
198
|
+
|
|
199
|
+
# 实施原则
|
|
200
|
+
|
|
201
|
+
1. 优先做**可复用的**改变(skill / 配置 / 文档),而不是某次具体业务代码。
|
|
202
|
+
2. 如果建议是"建立 skill",在 \`.opencode/skills/\` 或 \`.claude/\` 之类的
|
|
203
|
+
惯例位置下创建合适文件;不存在的目录请自行创建。
|
|
204
|
+
3. 不要为了执行这条建议去修改正在跑的应用功能代码,除非建议明确要求。
|
|
205
|
+
4. 不要 push / commit;只修改工作区文件。
|
|
206
|
+
|
|
207
|
+
# 报告格式
|
|
208
|
+
|
|
209
|
+
完成后(无论成功失败),请在你的最后一条消息里输出一段严格 JSON:
|
|
210
|
+
|
|
211
|
+
{
|
|
212
|
+
"ok": true | false,
|
|
213
|
+
"summary": "≤80 字 总结你做了什么(或为什么没做)",
|
|
214
|
+
"files_changed": ["相对路径", ...],
|
|
215
|
+
"notes": "可选,任何补充"
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
如果你判断这条建议不应该执行(过时、与项目现状冲突、需要更多信息),
|
|
219
|
+
直接返回 ok=false 并在 notes 里说明,不要硬改。`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
module.exports = {
|
|
223
|
+
EXECUTION_SENTINEL,
|
|
224
|
+
EXECUTION_PROMPT_VERSION,
|
|
225
|
+
buildExecutionPrompt,
|
|
226
|
+
buildProjectExecutionPrompt,
|
|
227
|
+
};
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Long-running CLI runner for advice execution.
|
|
3
|
+
*
|
|
4
|
+
* Spawns `opencode run` or `claude -p` in a given cwd, feeds the prompt
|
|
5
|
+
* via stdin, streams stdout/stderr, and resolves with a result blob.
|
|
6
|
+
* NEVER throws on normal failures (no-cli / timeout / spawn-error / non-zero
|
|
7
|
+
* exit) — the caller (server/execution/job.js) just inspects `ok`.
|
|
8
|
+
*
|
|
9
|
+
* Why NOT cli-runner.js / runJudge:
|
|
10
|
+
* • runJudge is contracted for short (<30s) JSON-strict calls with a
|
|
11
|
+
* 2-wide global semaphore — perfect for E1/O1/advice but wrong here.
|
|
12
|
+
* • execution is open-ended (minutes), can produce megabytes, and uses
|
|
13
|
+
* a 1-wide global lock managed by job.js (not by runner).
|
|
14
|
+
*
|
|
15
|
+
* Spec: docs/superpowers/specs/2026-06-13-advice-execution-design.md §7
|
|
16
|
+
*
|
|
17
|
+
* @author Felix
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const { spawn } = require('child_process');
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Defaults
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
const DEFAULT_TIMEOUT_MS = 10 * 60_000; // 10 minutes
|
|
29
|
+
const DEFAULT_MAX_BYTES = 256 * 1024; // 256 KB per stream
|
|
30
|
+
|
|
31
|
+
const CLI_TO_ARGV = {
|
|
32
|
+
opencode: () => ['run'],
|
|
33
|
+
claude: () => ['-p'],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Best-effort CLI detection
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Try `bin --version`; resolves true on exit code 0 within 5 s.
|
|
42
|
+
* Caches per-binary forever (PATH changes during a single aboss process
|
|
43
|
+
* are rare).
|
|
44
|
+
*/
|
|
45
|
+
const _detectCache = new Map();
|
|
46
|
+
|
|
47
|
+
function canSpawn(bin) {
|
|
48
|
+
if (_detectCache.has(bin)) return _detectCache.get(bin);
|
|
49
|
+
const p = new Promise((resolve) => {
|
|
50
|
+
let done = false;
|
|
51
|
+
const settle = (v) => { if (!done) { done = true; resolve(v); } };
|
|
52
|
+
try {
|
|
53
|
+
const proc = spawn(bin, ['--version'], {
|
|
54
|
+
stdio: 'ignore',
|
|
55
|
+
shell: process.platform === 'win32',
|
|
56
|
+
});
|
|
57
|
+
proc.on('error', () => settle(false));
|
|
58
|
+
proc.on('exit', (code) => settle(code === 0));
|
|
59
|
+
setTimeout(() => { try { proc.kill('SIGKILL'); } catch {} settle(false); }, 5000);
|
|
60
|
+
} catch { settle(false); }
|
|
61
|
+
});
|
|
62
|
+
_detectCache.set(bin, p);
|
|
63
|
+
return p;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Public: clear detection cache (tests / settings reload). */
|
|
67
|
+
function _resetDetectCache() { _detectCache.clear(); }
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Core runner
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Run the chosen executor in `cwd`, feed `prompt` via stdin, collect output.
|
|
75
|
+
*
|
|
76
|
+
* @param {object} opts
|
|
77
|
+
* @param {'opencode'|'claude'} opts.executor
|
|
78
|
+
* @param {string} opts.prompt
|
|
79
|
+
* @param {string} opts.cwd absolute path, must exist
|
|
80
|
+
* @param {number} [opts.timeoutMs=600_000] SIGKILL after this many ms
|
|
81
|
+
* @param {number} [opts.maxBytes=262_144] truncate each of stdout/stderr
|
|
82
|
+
* @param {(cancel:Function)=>void} [opts.onSpawn] receives a cancel fn once
|
|
83
|
+
* the process is alive; calling
|
|
84
|
+
* it SIGKILLs the child and
|
|
85
|
+
* resolves with reason='cancelled'
|
|
86
|
+
*
|
|
87
|
+
* @returns {Promise<
|
|
88
|
+
* { ok: true, exitCode, stdout, stderr, durationMs, executor }
|
|
89
|
+
* | { ok: false, reason: 'no-cli'|'spawn-error'|'timeout'|'cancelled'|'exit-non-zero',
|
|
90
|
+
* exitCode, stdout, stderr, durationMs, executor, error }
|
|
91
|
+
* >}
|
|
92
|
+
*/
|
|
93
|
+
async function runExecutor(opts) {
|
|
94
|
+
const {
|
|
95
|
+
executor,
|
|
96
|
+
prompt,
|
|
97
|
+
cwd,
|
|
98
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
99
|
+
maxBytes = DEFAULT_MAX_BYTES,
|
|
100
|
+
onSpawn,
|
|
101
|
+
} = opts;
|
|
102
|
+
|
|
103
|
+
if (!CLI_TO_ARGV[executor]) {
|
|
104
|
+
return _fail('spawn-error', 0, '', '', 0, executor, `unknown executor: ${executor}`);
|
|
105
|
+
}
|
|
106
|
+
if (!prompt || typeof prompt !== 'string') {
|
|
107
|
+
return _fail('spawn-error', 0, '', '', 0, executor, 'empty prompt');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const available = await canSpawn(executor);
|
|
111
|
+
if (!available) {
|
|
112
|
+
return _fail('no-cli', 0, '', '', 0, executor, `${executor} not on PATH`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return new Promise((resolve) => {
|
|
116
|
+
let done = false;
|
|
117
|
+
const start = Date.now();
|
|
118
|
+
const settle = (v) => { if (!done) { done = true; clearTimeout(timer); resolve(v); } };
|
|
119
|
+
|
|
120
|
+
let proc;
|
|
121
|
+
try {
|
|
122
|
+
proc = spawn(executor, CLI_TO_ARGV[executor](), {
|
|
123
|
+
cwd,
|
|
124
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
125
|
+
shell: process.platform === 'win32',
|
|
126
|
+
windowsHide: true,
|
|
127
|
+
});
|
|
128
|
+
} catch (err) {
|
|
129
|
+
return settle(_fail('spawn-error', 0, '', '', Date.now() - start, executor, err.message));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Expose a cancel handle for the job layer.
|
|
133
|
+
let cancelled = false;
|
|
134
|
+
if (typeof onSpawn === 'function') {
|
|
135
|
+
onSpawn(() => {
|
|
136
|
+
cancelled = true;
|
|
137
|
+
try { proc.kill('SIGKILL'); } catch {}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let stdout = '';
|
|
142
|
+
let stderr = '';
|
|
143
|
+
let stdoutTrunc = false;
|
|
144
|
+
let stderrTrunc = false;
|
|
145
|
+
let _timedOut = false;
|
|
146
|
+
|
|
147
|
+
proc.stdout.on('data', (chunk) => {
|
|
148
|
+
if (stdoutTrunc) return;
|
|
149
|
+
stdout += chunk.toString('utf8');
|
|
150
|
+
if (stdout.length > maxBytes) {
|
|
151
|
+
stdout = stdout.slice(0, maxBytes) + `\n[…truncated at ${maxBytes} B…]`;
|
|
152
|
+
stdoutTrunc = true;
|
|
153
|
+
// Don't kill here — let stderr still flow. Kill only if BOTH overflow.
|
|
154
|
+
if (stderrTrunc) try { proc.kill('SIGKILL'); } catch {}
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
proc.stderr.on('data', (chunk) => {
|
|
158
|
+
if (stderrTrunc) return;
|
|
159
|
+
stderr += chunk.toString('utf8');
|
|
160
|
+
if (stderr.length > maxBytes) {
|
|
161
|
+
stderr = stderr.slice(0, maxBytes) + `\n[…truncated at ${maxBytes} B…]`;
|
|
162
|
+
stderrTrunc = true;
|
|
163
|
+
if (stdoutTrunc) try { proc.kill('SIGKILL'); } catch {}
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
proc.on('error', (err) => settle(
|
|
168
|
+
_fail('spawn-error', 0, stdout, stderr, Date.now() - start, executor, err.message)
|
|
169
|
+
));
|
|
170
|
+
|
|
171
|
+
proc.on('exit', (code, signal) => {
|
|
172
|
+
const durationMs = Date.now() - start;
|
|
173
|
+
if (cancelled) {
|
|
174
|
+
return settle(_fail('cancelled', code ?? 0, stdout, stderr, durationMs, executor,
|
|
175
|
+
signal ? `killed by ${signal}` : 'cancelled'));
|
|
176
|
+
}
|
|
177
|
+
if (signal === 'SIGKILL' && _timedOut) {
|
|
178
|
+
return settle(_fail('timeout', code ?? 0, stdout, stderr, durationMs, executor,
|
|
179
|
+
`killed after ${timeoutMs}ms`));
|
|
180
|
+
}
|
|
181
|
+
if (code !== 0) {
|
|
182
|
+
return settle(_fail('exit-non-zero', code, stdout, stderr, durationMs, executor,
|
|
183
|
+
`exit ${code}`));
|
|
184
|
+
}
|
|
185
|
+
settle({
|
|
186
|
+
ok: true, exitCode: code, stdout, stderr, durationMs, executor,
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Pipe prompt via stdin then close — opencode/claude both read from
|
|
191
|
+
// stdin when no positional prompt is given after run/-p.
|
|
192
|
+
try {
|
|
193
|
+
proc.stdin.on('error', () => {}); // EPIPE if CLI exits before we're done
|
|
194
|
+
proc.stdin.end(prompt, 'utf8');
|
|
195
|
+
} catch {
|
|
196
|
+
// Already handled by 'error' event.
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Timeout (declared above, set here so the close-handler distinguishes
|
|
200
|
+
// a SIGKILL from a 10-min wall-clock kill versus user cancel).
|
|
201
|
+
const timer = setTimeout(() => {
|
|
202
|
+
_timedOut = true;
|
|
203
|
+
try { proc.kill('SIGKILL'); } catch {}
|
|
204
|
+
}, timeoutMs);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function _fail(reason, exitCode, stdout, stderr, durationMs, executor, error) {
|
|
209
|
+
return { ok: false, reason, exitCode, stdout, stderr, durationMs, executor, error };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = {
|
|
213
|
+
runExecutor,
|
|
214
|
+
canSpawn,
|
|
215
|
+
_resetDetectCache,
|
|
216
|
+
DEFAULT_TIMEOUT_MS,
|
|
217
|
+
DEFAULT_MAX_BYTES,
|
|
218
|
+
};
|
package/server/index.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express server for Agent Boss.
|
|
3
|
+
* Serves REST API and static frontend files.
|
|
4
|
+
*
|
|
5
|
+
* @author Felix
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const express = require('express');
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Route factories
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
const createReportsRouter = require('./api/reports');
|
|
16
|
+
const createMetricsRouter = require('./api/metrics');
|
|
17
|
+
const createSessionsRouter = require('./api/sessions');
|
|
18
|
+
const createSettingsRouter = require('./api/settings');
|
|
19
|
+
const createAnalysisRouter = require('./api/analysis');
|
|
20
|
+
const createOverviewRouter = require('./api/overview');
|
|
21
|
+
const createAdviceRouter = require('./api/advice');
|
|
22
|
+
const createProjectRouter = require('./api/project');
|
|
23
|
+
const createExecutionRouter = require('./api/execution');
|
|
24
|
+
|
|
25
|
+
const { cleanupOrphans } = require('./execution/job');
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Server factory
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create and configure the Express application.
|
|
33
|
+
*
|
|
34
|
+
* @param {object} db sql.js Database instance (boss.db)
|
|
35
|
+
* @returns {import('express').Express}
|
|
36
|
+
*/
|
|
37
|
+
function createServer(db) {
|
|
38
|
+
// Mark any execution_run rows left in 'pending' / 'running' by a crashed
|
|
39
|
+
// previous process as failed. Has to happen before any API mounts so a
|
|
40
|
+
// user opening the page right after restart doesn't see a stale "still
|
|
41
|
+
// running" badge. See server/execution/job.js::cleanupOrphans.
|
|
42
|
+
const orphans = cleanupOrphans(db);
|
|
43
|
+
if (orphans > 0) {
|
|
44
|
+
console.log(`[execution] cleaned ${orphans} orphan run(s) from previous process`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const app = express();
|
|
48
|
+
|
|
49
|
+
// --- Middleware -----------------------------------------------------------
|
|
50
|
+
app.use(express.json({ limit: '512kb' }));
|
|
51
|
+
|
|
52
|
+
// --- API routes -----------------------------------------------------------
|
|
53
|
+
app.use('/api/overview', createOverviewRouter(db));
|
|
54
|
+
app.use('/api/reports', createReportsRouter(db));
|
|
55
|
+
app.use('/api/metrics', createMetricsRouter(db));
|
|
56
|
+
app.use('/api/sessions', createSessionsRouter(db));
|
|
57
|
+
app.use('/api/settings', createSettingsRouter(db));
|
|
58
|
+
app.use('/api/analysis', createAnalysisRouter(db));
|
|
59
|
+
app.use('/api/advice', createAdviceRouter(db));
|
|
60
|
+
app.use('/api/project', createProjectRouter(db));
|
|
61
|
+
app.use('/api/execution', createExecutionRouter(db));
|
|
62
|
+
|
|
63
|
+
// --- Static files ---------------------------------------------------------
|
|
64
|
+
const clientDist = path.join(__dirname, '..', 'client', 'dist');
|
|
65
|
+
app.use(express.static(clientDist));
|
|
66
|
+
|
|
67
|
+
// --- SPA fallback ---------------------------------------------------------
|
|
68
|
+
app.get('*', (_req, res) => {
|
|
69
|
+
res.sendFile(path.join(clientDist, 'index.html'));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return app;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Start the Express server on the given port.
|
|
77
|
+
*
|
|
78
|
+
* @param {object} db sql.js Database instance (boss.db)
|
|
79
|
+
* @param {number} port TCP port to listen on
|
|
80
|
+
* @returns {Promise<import('http').Server>}
|
|
81
|
+
*/
|
|
82
|
+
function startServer(db, port) {
|
|
83
|
+
const app = createServer(db);
|
|
84
|
+
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
const server = app.listen(port, () => {
|
|
87
|
+
console.log(`Agent Boss server listening on http://localhost:${port}`);
|
|
88
|
+
resolve(server);
|
|
89
|
+
});
|
|
90
|
+
server.on('error', (err) => reject(err));
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = { createServer, startServer };
|