mocode-ai 1.6.9 → 1.6.10
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/dist/agent/model-turn.js +6 -1
- package/dist/agent/run-coordinator.js +215 -149
- package/dist/agent/stages/tool-dispatcher.js +150 -126
- package/dist/headless.js +27 -1
- package/dist/jobs/launch.js +14 -7
- package/dist/jobs/worktree.js +20 -0
- package/dist/llm/index.js +33 -1
- package/dist/llm/proxy.js +24 -1
- package/dist/repl/commands/diff.js +189 -0
- package/dist/repl/commands/jobs.js +2 -2
- package/dist/repl/commands/registry.js +2 -0
- package/dist/repl/commands.js +1 -0
- package/dist/repl/runtime.js +29 -10
- package/dist/rollback/store.js +7 -0
- package/dist/runtime/dev-server-manager.js +27 -5
- package/dist/schedule/cli.js +1 -1
- package/dist/schedule/daemon.js +3 -3
- package/dist/skills/builtin-skills.js +99 -59
- package/dist/tools/builtins/index.js +6 -2
- package/dist/tools/builtins/run-command.js +32 -10
- package/dist/tools/policy.js +2 -2
- package/dist/tools/resource-lock.js +5 -1
- package/dist/tools/ripgrep-client.js +9 -1
- package/dist/ui/batch.js +5 -4
- package/package.json +1 -1
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工作区命令组:/diff
|
|
3
|
+
*
|
|
4
|
+
* `/diff` = 最近一轮 agent turn 的快照(before) vs 工作区当前内容(after),
|
|
5
|
+
* 逐文件渲染 unified diff 到内容区;`/diff <N>` 选第 N 轮(1-based,与 /rollback
|
|
6
|
+
* 轮次口径一致);`/diff all` = 全部轮次的累计改动(最早的 before vs 现在)。
|
|
7
|
+
*
|
|
8
|
+
* 数据源:rollback 快照(每轮文件修改前捕获,不依赖 git)——好处是「没有 git 的目录」
|
|
9
|
+
* 也能看 agent 改了什么,且口径与 /rollback 完全一致(所见即可撤)。
|
|
10
|
+
* 限制:run_command 等不透明工具的工作区扫描快照在文件超 CAPTURE_FILE_LIMIT 时只留
|
|
11
|
+
* 指纹(contentUnavailable),这些文件显示「快照不可用」而非 diff。
|
|
12
|
+
*
|
|
13
|
+
* diff 算法:前后缀等行消除 + 单 hunk 输出,不引依赖(与 agent/index.ts lineDelta
|
|
14
|
+
* 同思路)。文件较少、行数中等的会话内 diff 足够快。
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
import * as layout from '../../ui/layout.js';
|
|
18
|
+
import { ui } from '../../ui/theme.js';
|
|
19
|
+
import { getActiveRollbackStore } from '../../rollback/index.js';
|
|
20
|
+
import { unhandled, next } from './types.js';
|
|
21
|
+
const MAX_DIFF_LINES = 400; // 单文件输出上限:防止把万行生成文件刷满内容区
|
|
22
|
+
const MAX_FILES = 60; // 单次命令最多渲染文件数(超出提示收窄)
|
|
23
|
+
const CONTEXT = 3; // hunk 上下文行数
|
|
24
|
+
/** 快照 before 内容 → 文本(null → 无内容/不可用;base64 解码失败按 null)。 */
|
|
25
|
+
function snapshotText(snapshot) {
|
|
26
|
+
if (snapshot.before === null)
|
|
27
|
+
return null;
|
|
28
|
+
if (snapshot.encoding === 'base64') {
|
|
29
|
+
try {
|
|
30
|
+
return Buffer.from(snapshot.before, 'base64').toString('utf8');
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return snapshot.before; // v1 快照:UTF-8 原文
|
|
37
|
+
}
|
|
38
|
+
/** 是否可当文本 diff:kind=file 且非 contentUnavailable(目录/软链/超预算跳过)。 */
|
|
39
|
+
function diffable(snapshot) {
|
|
40
|
+
return snapshot.kind === 'file' && snapshot.contentUnavailable !== true;
|
|
41
|
+
}
|
|
42
|
+
/** 按行求公共前缀/后缀长度(与 lineDelta 同思路,但这里要保留行号做 hunk)。 */
|
|
43
|
+
function hunkBoundaries(before, after) {
|
|
44
|
+
let head = 0;
|
|
45
|
+
while (head < before.length && head < after.length && before[head] === after[head])
|
|
46
|
+
head++;
|
|
47
|
+
let tail = 0;
|
|
48
|
+
while (tail < before.length - head &&
|
|
49
|
+
tail < after.length - head &&
|
|
50
|
+
before[before.length - 1 - tail] === after[after.length - 1 - tail])
|
|
51
|
+
tail++;
|
|
52
|
+
return { head, tail };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 行级 unified diff 单文件段(纯文本,颜色渲染层加)。
|
|
56
|
+
* 策略:前后缀等行消除后,中间区整段输出为「先删后加」的单 hunk。对单文件一次
|
|
57
|
+
* 编辑(agent 编辑的绝大多数形态:改一处/几处)等价于标准 diff;全文重写时输出
|
|
58
|
+
* 体积与正确 diff 同量级,只是 hunk 划分更粗。无变化返回 ''(指纹差异但文本相同)。
|
|
59
|
+
*/
|
|
60
|
+
export function unifiedFileDiff(pathRel, beforeText, afterText) {
|
|
61
|
+
const before = beforeText === null ? [] : beforeText.split('\n');
|
|
62
|
+
const after = afterText === null ? [] : afterText.split('\n');
|
|
63
|
+
const { head, tail } = hunkBoundaries(before, after);
|
|
64
|
+
const midBefore = before.slice(head, before.length - tail);
|
|
65
|
+
const midAfter = after.slice(head, after.length - tail);
|
|
66
|
+
if (midBefore.length === 0 && midAfter.length === 0)
|
|
67
|
+
return '';
|
|
68
|
+
const contextStart = Math.max(0, head - CONTEXT);
|
|
69
|
+
const tailStartBefore = before.length - tail;
|
|
70
|
+
const tailStartAfter = after.length - tail;
|
|
71
|
+
const contextEndBefore = Math.min(before.length, tailStartBefore + CONTEXT);
|
|
72
|
+
const contextEndAfter = Math.min(after.length, tailStartAfter + CONTEXT);
|
|
73
|
+
const lines = [];
|
|
74
|
+
lines.push(`--- a/${pathRel}`);
|
|
75
|
+
lines.push(`+++ b/${pathRel}`);
|
|
76
|
+
lines.push(`@@ -${contextStart + 1},${contextEndBefore - contextStart} +${contextStart + 1},${contextEndAfter - contextStart} @@`);
|
|
77
|
+
for (let i = contextStart; i < head; i++)
|
|
78
|
+
lines.push(` ${before[i]}`);
|
|
79
|
+
for (const line of midBefore)
|
|
80
|
+
lines.push(`-${line}`);
|
|
81
|
+
for (const line of midAfter)
|
|
82
|
+
lines.push(`+${line}`);
|
|
83
|
+
for (let i = tailStartBefore; i < contextEndBefore; i++)
|
|
84
|
+
lines.push(` ${before[i]}`);
|
|
85
|
+
return lines.join('\n');
|
|
86
|
+
}
|
|
87
|
+
/** 渲染一条 diff 行:加绿/删红/hunk 头青/上下文 dim。 */
|
|
88
|
+
function writeDiffLine(line) {
|
|
89
|
+
if (line.startsWith('+'))
|
|
90
|
+
layout.contentWrite(`${ui.green}${line}${ui.reset}\n`);
|
|
91
|
+
else if (line.startsWith('-'))
|
|
92
|
+
layout.contentWrite(`${ui.red}${line}${ui.reset}\n`);
|
|
93
|
+
else if (line.startsWith('@@'))
|
|
94
|
+
layout.contentWrite(`${ui.cyan}${line}${ui.reset}\n`);
|
|
95
|
+
else
|
|
96
|
+
layout.contentWrite(`${ui.dim}${line}${ui.reset}\n`);
|
|
97
|
+
}
|
|
98
|
+
/** 读当前文件(不存在→null=「删除」;读失败→null)。 */
|
|
99
|
+
function readCurrent(full) {
|
|
100
|
+
try {
|
|
101
|
+
return readFileSync(full, 'utf8');
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export const diffCommands = [
|
|
108
|
+
(ctx) => {
|
|
109
|
+
if (ctx.line !== '/diff' && !ctx.line.startsWith('/diff '))
|
|
110
|
+
return unhandled();
|
|
111
|
+
const arg = ctx.line === '/diff' ? '' : ctx.line.slice('/diff '.length).trim();
|
|
112
|
+
const store = getActiveRollbackStore();
|
|
113
|
+
const turns = store.listTurns();
|
|
114
|
+
if (turns.length === 0) {
|
|
115
|
+
layout.contentWrite(`${ui.dim}(本会话尚无文件改动记录——rollback 快照为空)${ui.reset}\n`);
|
|
116
|
+
return next();
|
|
117
|
+
}
|
|
118
|
+
// 轮次选择:/diff = 最近一轮;/diff N = 第 N 轮(1-based,与 /rollback 同口径);/diff all = 全部累计。
|
|
119
|
+
let label;
|
|
120
|
+
let minTurnIdExclusive; // 只统计 turnId > 此值的快照
|
|
121
|
+
if (arg === '') {
|
|
122
|
+
const last = turns[turns.length - 1];
|
|
123
|
+
minTurnIdExclusive = last.turnId - 1;
|
|
124
|
+
label = `第 ${turns.length} 轮(最近)`;
|
|
125
|
+
}
|
|
126
|
+
else if (arg === 'all') {
|
|
127
|
+
minTurnIdExclusive = 0;
|
|
128
|
+
label = '全部轮次累计';
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
const n = Number(arg);
|
|
132
|
+
if (!Number.isInteger(n) || n < 1 || n > turns.length) {
|
|
133
|
+
layout.contentWrite(`${ui.yellow}(轮次无效:本会话共 ${turns.length} 轮,可用 /diff 1..${turns.length} 或 /diff all)${ui.reset}\n`);
|
|
134
|
+
return next();
|
|
135
|
+
}
|
|
136
|
+
minTurnIdExclusive = (turns[n - 1].turnId ?? 0) - 1;
|
|
137
|
+
label = `第 ${n} 轮`;
|
|
138
|
+
}
|
|
139
|
+
const byPath = new Map();
|
|
140
|
+
for (const snapshot of store.snapshotsSince(minTurnIdExclusive)) {
|
|
141
|
+
const existing = byPath.get(snapshot.path);
|
|
142
|
+
if (!existing || snapshot.turnId < existing.snapshot.turnId) {
|
|
143
|
+
byPath.set(snapshot.path, {
|
|
144
|
+
snapshot,
|
|
145
|
+
unavailable: (existing?.unavailable ?? false) || !diffable(snapshot),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
else if (!diffable(snapshot)) {
|
|
149
|
+
existing.unavailable = true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (byPath.size === 0) {
|
|
153
|
+
layout.contentWrite(`${ui.dim}(${label}:无文件改动记录)${ui.reset}\n`);
|
|
154
|
+
return next();
|
|
155
|
+
}
|
|
156
|
+
const root = process.cwd();
|
|
157
|
+
const files = [...byPath.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
158
|
+
const shown = files.slice(0, MAX_FILES);
|
|
159
|
+
layout.contentWrite(`${ui.bold}${ui.accent}● ${label} 文件改动${ui.reset} ${ui.dim}${shown.length}/${files.length} 文件${ui.reset}\n`);
|
|
160
|
+
for (const [rel, entry] of shown) {
|
|
161
|
+
if (entry.unavailable) {
|
|
162
|
+
layout.contentWrite(`${ui.yellow} ⚠ ${rel}(快照不可用:超出捕获预算或非文本,无法展示 diff)${ui.reset}\n`);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const beforeText = snapshotText(entry.snapshot);
|
|
166
|
+
const afterText = readCurrent(`${root}${rel.startsWith('/') || rel.startsWith('\\') ? '' : '/'}${rel}`);
|
|
167
|
+
const diffText = unifiedFileDiff(rel, beforeText, afterText);
|
|
168
|
+
if (!diffText) {
|
|
169
|
+
// 快照记录了变更但当前内容已与 before 相同(后续轮改回/用户手改回):
|
|
170
|
+
// 仍列文件名,让用户知道它被动过,只是无净变化。
|
|
171
|
+
layout.contentWrite(`${ui.dim} = ${rel}(已与轮前状态一致,无净变化)${ui.reset}\n`);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
layout.contentWrite(`\n${ui.bold}${rel}${ui.reset}\n`);
|
|
175
|
+
const lines = diffText.split('\n');
|
|
176
|
+
const truncated = lines.length > MAX_DIFF_LINES;
|
|
177
|
+
for (const line of lines.slice(0, MAX_DIFF_LINES))
|
|
178
|
+
writeDiffLine(line);
|
|
179
|
+
if (truncated) {
|
|
180
|
+
layout.contentWrite(`${ui.dim} …(diff 超过 ${MAX_DIFF_LINES} 行已截断,如需完整内容请直接 read_file)${ui.reset}\n`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (files.length > MAX_FILES) {
|
|
184
|
+
layout.contentWrite(`${ui.dim} …(${files.length - MAX_FILES} 个文件未展示;改动面过大,建议 /diff <N> 收窄到单轮)${ui.reset}\n`);
|
|
185
|
+
}
|
|
186
|
+
layout.contentWrite('\n');
|
|
187
|
+
return next();
|
|
188
|
+
},
|
|
189
|
+
];
|
|
@@ -62,7 +62,7 @@ export const jobsCommands = [
|
|
|
62
62
|
return next();
|
|
63
63
|
},
|
|
64
64
|
// /jobs kill [id前缀]
|
|
65
|
-
(ctx) => {
|
|
65
|
+
async (ctx) => {
|
|
66
66
|
if (ctx.cmd !== '/jobs')
|
|
67
67
|
return unhandled();
|
|
68
68
|
const parts = ctx.line.split(/\s+/);
|
|
@@ -77,7 +77,7 @@ export const jobsCommands = [
|
|
|
77
77
|
layout.contentWrite(`${ui.dim}任务 ${job.id} 已是 ${job.status},无需 kill${ui.reset}\n`);
|
|
78
78
|
return next();
|
|
79
79
|
}
|
|
80
|
-
const ok = killBackgroundJob(job);
|
|
80
|
+
const ok = await killBackgroundJob(job);
|
|
81
81
|
layout.contentWrite(ok
|
|
82
82
|
? `${ui.yellow}已 kill 任务 ${job.id}${ui.reset}\n`
|
|
83
83
|
: `${ui.red}kill 失败(进程可能已退出;PID=${job.pid ?? '?'})${ui.reset}\n`);
|
|
@@ -17,6 +17,7 @@ import { effortCommands } from './effort.js';
|
|
|
17
17
|
import { routerCommands } from './router.js';
|
|
18
18
|
import { toolGroupCommands } from './tool-group.js';
|
|
19
19
|
import { jobsCommands } from './jobs.js';
|
|
20
|
+
import { diffCommands } from './diff.js';
|
|
20
21
|
import { scheduleCommands } from './schedule.js';
|
|
21
22
|
import { botsCommands } from './bots.js';
|
|
22
23
|
/**
|
|
@@ -44,6 +45,7 @@ export const commandHandlers = [
|
|
|
44
45
|
...routerCommands,
|
|
45
46
|
...toolGroupCommands,
|
|
46
47
|
...jobsCommands,
|
|
48
|
+
...diffCommands,
|
|
47
49
|
...scheduleCommands,
|
|
48
50
|
...botsCommands,
|
|
49
51
|
];
|
package/dist/repl/commands.js
CHANGED
|
@@ -29,6 +29,7 @@ export function buildSlashCommands() {
|
|
|
29
29
|
{ name: '/resume', desc: d('commands.sessionResume') },
|
|
30
30
|
{ name: '/sessions', desc: d('commands.sessionBrowse') },
|
|
31
31
|
{ name: '/rollback', desc: d('commands.sessionRollback') },
|
|
32
|
+
{ name: '/diff', desc: '查看 agent 文件改动 diff(/diff [N]|all,快照 vs 工作区)' },
|
|
32
33
|
{ name: '/ssearch <关键词>', value: '/ssearch ', submit: false, desc: '跨会话搜索归档会话' },
|
|
33
34
|
{ name: '/jobs', desc: '后台任务列表(/jobs log/kill <id前缀>)' },
|
|
34
35
|
{ name: '/schedules', desc: '查看定时计划与调度守护状态' },
|
package/dist/repl/runtime.js
CHANGED
|
@@ -173,10 +173,17 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
173
173
|
const runtime = defaultRuntime;
|
|
174
174
|
await runtime.start();
|
|
175
175
|
const runtimeContext = runtime.context;
|
|
176
|
-
// MCP
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
176
|
+
// MCP 异步并行连接(不阻塞首帧):旧序「await initializeAllMcp() → banner」在
|
|
177
|
+
// npx/stdio 类 server 上把 REPL 首屏卡住数秒(黑屏等待)。现在 banner/欢迎块先画,
|
|
178
|
+
// MCP 在后台连;完成后注册工具 + refreshChatTools + 在内容区补连接结果行。
|
|
179
|
+
// runTurn 前 mcpReady barrier 兜底:用户在连接完成前就发消息时等它就绪(见 runTurn)。
|
|
180
|
+
const mcpReady = (async () => {
|
|
181
|
+
const report = await initializeAllMcp();
|
|
182
|
+
registerToolsExtension('mcp', getMcpTools());
|
|
183
|
+
refreshChatTools();
|
|
184
|
+
return report;
|
|
185
|
+
})();
|
|
186
|
+
void mcpReady.catch(() => undefined); // 连接失败不产生 unhandledRejection;报告行由 then 写
|
|
180
187
|
// --resume:读回该会话的轮次/快照;无文件则从 history 重建 turns(无快照→旧轮次文件改动不可撤销)
|
|
181
188
|
let currentSessionId = sessionId;
|
|
182
189
|
if (!currentSessionId)
|
|
@@ -270,12 +277,16 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
270
277
|
else {
|
|
271
278
|
layout.writeBanner(bannerLines(banner()));
|
|
272
279
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
280
|
+
// MCP 连接结果在就绪后补写(后台并行连接,不再阻塞上方首帧);用户已滚动时仍
|
|
281
|
+
// 落在内容区尾部,与启动期其它提示行为一致。
|
|
282
|
+
void mcpReady.then((mcpReport) => {
|
|
283
|
+
if (mcpReport.connected.length > 0) {
|
|
284
|
+
layout.contentWrite(`${ui.dim} ↳ 已连接 MCP: ${mcpReport.connected.join(', ')} (${getMcpTools().length} 个工具;外部工具每次均需授权)${ui.reset}\n`);
|
|
285
|
+
}
|
|
286
|
+
for (const warning of mcpReport.warnings) {
|
|
287
|
+
layout.contentWrite(`${ui.yellow} ⚠ ${warning}${ui.reset}\n`);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
279
290
|
if (!isModelConfigured()) {
|
|
280
291
|
// 未配置 baseURL/apiKey:醒目提示引导 /model(不退出,REPL 仍可用;发消息会失败但不崩)。
|
|
281
292
|
layout.contentWrite(`${ui.yellow} ⚠ 未配置大模型。输入 ${ui.cyan}/model${ui.yellow} 配置 baseURL / apiKey / model(即时生效),或退出后运行 ${ui.cyan}mocode config${ui.yellow} 走向导。${ui.reset}\n`);
|
|
@@ -456,6 +467,9 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
456
467
|
let routingDone = toolPolicy != null;
|
|
457
468
|
try {
|
|
458
469
|
signal = startRunningListener(placeholder);
|
|
470
|
+
// MCP 后台连接兜底:启动后在连接完成前就发消息的极端窗口,先等就绪再路由,
|
|
471
|
+
// 保证路由看到的工具表含 MCP 工具(否则本 turn 工具簇不含 mcp__,模型路由失真)。
|
|
472
|
+
await mcpReady.catch(() => undefined);
|
|
459
473
|
// 入口设定本轮初始模式(合成执行轮传 false→auto;用户轮传当前 mode)。
|
|
460
474
|
// setAgentMode 触发 listener 重写 history[0];模式只由用户面切换,runAgent 每步读取当前值。
|
|
461
475
|
setAgentMode(planMode ? 'plan' : 'auto');
|
|
@@ -533,6 +547,11 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
533
547
|
refreshStatusBase(history);
|
|
534
548
|
layout.drawStatusBar();
|
|
535
549
|
}, toolPolicy, initialToolRoute, runtime);
|
|
550
|
+
// 撞限 salvage 摘要要在 TUI 落地:runAgent 只把 finalText 带回不渲染,主线撞限的
|
|
551
|
+
// 收尾摘要若不打印,这笔收尾请求的 token 就白花(用户只看到「达到最大步数」一行)。
|
|
552
|
+
if (result.terminationReason === 'max_steps' && result.finalText) {
|
|
553
|
+
layout.contentWrite(result.finalText.endsWith('\n') ? result.finalText : `${result.finalText}\n`);
|
|
554
|
+
}
|
|
536
555
|
if (toolPolicy)
|
|
537
556
|
lastToolGroups = toolPolicy.groupNames;
|
|
538
557
|
// 本轮 token 累计(底栏模式 chip 右边显示)。undefined = 后端不开 include_usage。
|
package/dist/rollback/store.js
CHANGED
|
@@ -392,6 +392,13 @@ export class RollbackStore {
|
|
|
392
392
|
listTurns() {
|
|
393
393
|
return this.turns.slice();
|
|
394
394
|
}
|
|
395
|
+
/**
|
|
396
|
+
* 只读侧门(供 /diff 渲染):返回 turnId > sinceTurnId 的全部快照,浅拷贝数组。
|
|
397
|
+
* 快照内容含 before 原文,调用方不得 mutate 返回值;用于展示,不影响回滚语义。
|
|
398
|
+
*/
|
|
399
|
+
snapshotsSince(sinceTurnId) {
|
|
400
|
+
return this.snapshots.filter((snapshot) => snapshot.turnId > sinceTurnId);
|
|
401
|
+
}
|
|
395
402
|
findCutoffIndex(n, history) {
|
|
396
403
|
let seen = 0;
|
|
397
404
|
for (let i = 0; i < history.length; i++) {
|
|
@@ -127,7 +127,29 @@ async function probeReady(url) {
|
|
|
127
127
|
clearTimeout(timer);
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
|
-
|
|
130
|
+
/**
|
|
131
|
+
* 异步树杀(运行期路径):spawnSync('taskkill') 会同步阻塞事件循环(扫树期间流式渲染/
|
|
132
|
+
* 并行工具停摆)。发出 kill 后等 taskkill 退出,不牺牲「进程已终止」语义。
|
|
133
|
+
* 退出路径(stopAllDevServersSync)必须同步,用 killTreeSync 保留原行为。
|
|
134
|
+
*/
|
|
135
|
+
async function killTree(pid) {
|
|
136
|
+
if (!IS_WINDOWS) {
|
|
137
|
+
try {
|
|
138
|
+
process.kill(pid, 'SIGTERM');
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// 已退出或无权限:best-effort。
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
await new Promise((resolve) => {
|
|
146
|
+
const killer = spawn('taskkill', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore' });
|
|
147
|
+
killer.on('close', () => resolve());
|
|
148
|
+
killer.on('error', () => resolve());
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/** 同步树杀:仅供进程退出钩子(stopAllDevServersSync)使用,运行期请走异步版。 */
|
|
152
|
+
function killTreeSync(pid) {
|
|
131
153
|
try {
|
|
132
154
|
if (IS_WINDOWS) {
|
|
133
155
|
spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore' });
|
|
@@ -266,13 +288,13 @@ export async function startDevServer(opts) {
|
|
|
266
288
|
catch (error) {
|
|
267
289
|
// 启动失败 / 被中断:不留孤儿进程。
|
|
268
290
|
record.stopRequested = true;
|
|
269
|
-
killTree(record.pid);
|
|
291
|
+
await killTree(record.pid);
|
|
270
292
|
finalizeExit(record, record.child.exitCode, record.child.signalCode);
|
|
271
293
|
throw error;
|
|
272
294
|
}
|
|
273
295
|
if (wantsReadySignal && !ready) {
|
|
274
296
|
record.stopRequested = true;
|
|
275
|
-
killTree(record.pid);
|
|
297
|
+
await killTree(record.pid);
|
|
276
298
|
finalizeExit(record, record.child.exitCode, record.child.signalCode);
|
|
277
299
|
throw new DevServerManagerError('TIMEOUT', `Dev server did not become ready within ${timeoutMs}ms and was stopped. Recent output:\n${record.recentLog.trim() || '(no output)'}`);
|
|
278
300
|
}
|
|
@@ -328,7 +350,7 @@ export async function stopDevServer(id) {
|
|
|
328
350
|
return { ...snapshot(record), alreadyStopped: true };
|
|
329
351
|
}
|
|
330
352
|
record.stopRequested = true;
|
|
331
|
-
killTree(record.pid);
|
|
353
|
+
await killTree(record.pid);
|
|
332
354
|
const deadline = Date.now() + STOP_GRACE_MS;
|
|
333
355
|
while (Date.now() < deadline && isAlive(record.pid)) {
|
|
334
356
|
await sleep(50);
|
|
@@ -350,7 +372,7 @@ export function stopAllDevServersSync() {
|
|
|
350
372
|
if (record.state !== 'running')
|
|
351
373
|
continue;
|
|
352
374
|
record.stopRequested = true;
|
|
353
|
-
|
|
375
|
+
killTreeSync(record.pid);
|
|
354
376
|
finalizeExit(record, record.child.exitCode, record.child.signalCode);
|
|
355
377
|
}
|
|
356
378
|
}
|
package/dist/schedule/cli.js
CHANGED
|
@@ -83,7 +83,7 @@ export async function runScheduleCli(rawArgs) {
|
|
|
83
83
|
return 0;
|
|
84
84
|
}
|
|
85
85
|
case 'stop':
|
|
86
|
-
process.stdout.write(stopDaemon() ? 'Scheduler stopped\n' : 'Scheduler was not running\n');
|
|
86
|
+
process.stdout.write((await stopDaemon()) ? 'Scheduler stopped\n' : 'Scheduler was not running\n');
|
|
87
87
|
return 0;
|
|
88
88
|
case 'status': {
|
|
89
89
|
const state = getDaemonState();
|
package/dist/schedule/daemon.js
CHANGED
|
@@ -150,12 +150,12 @@ export function startDaemon(port = DEFAULT_PORT) {
|
|
|
150
150
|
fs.writeFileSync(daemonStatePath(), `${JSON.stringify(state, null, 2)}\n`);
|
|
151
151
|
return { started: true, state };
|
|
152
152
|
}
|
|
153
|
-
/**
|
|
154
|
-
export function stopDaemon() {
|
|
153
|
+
/** 停止守护进程并清状态。异步:树杀不再阻塞事件循环(见 jobs/launch.ts killTree)。 */
|
|
154
|
+
export async function stopDaemon() {
|
|
155
155
|
const state = getDaemonState();
|
|
156
156
|
if (!state)
|
|
157
157
|
return false;
|
|
158
|
-
const ok = killTree(state.pid);
|
|
158
|
+
const ok = await killTree(state.pid);
|
|
159
159
|
try {
|
|
160
160
|
fs.rmSync(daemonStatePath());
|
|
161
161
|
}
|
|
@@ -42,94 +42,135 @@ CLI dump 一般是 \`path/to/file.ts:line:col symbol\` 形式,与 grep 一样
|
|
|
42
42
|
`;
|
|
43
43
|
const BRAINSTORM_BODY = `# Brainstorming(需求澄清)
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
开工前的需求澄清:当需求可能被理解错、信息不足或只给了粗略想法时,先用对话把需求收敛成用户认可的设计,再动手。
|
|
46
46
|
|
|
47
|
-
##
|
|
47
|
+
## 铁律
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
- 缺少关键信息(目标、范围、约束、输入输出、验收标准),且靠仓库证据无法补全;
|
|
51
|
-
- 改动涉及不可逆操作 / 公共 API / 多个会实质改变产品行为的方案;
|
|
52
|
-
- 用户只给了粗略想法(如「帮我做个 X」),期望先讨论。
|
|
49
|
+
开始写产品代码之前,必须完成所选路径对应的确认门。**批准是分阶段的**:用户认可想法 ≠ 批准写代码;对方案的口头认可只推进到下一步,不越级。
|
|
53
50
|
|
|
54
|
-
##
|
|
51
|
+
## 第一步:分级(说出来,让用户可纠正)
|
|
55
52
|
|
|
56
|
-
|
|
57
|
-
- 只是命名或实现细节 → 遵循仓库惯例自主决定;
|
|
58
|
-
- 用户已明确表示「直接做 / 别问我」。
|
|
53
|
+
对需求先分类并明说——「这个需求看起来是有界改动,我会先在聊天里给短设计」:
|
|
59
54
|
|
|
60
|
-
|
|
55
|
+
- **探针(Spike)**:可行性问题(「能不能…」「先快速试试」),产出是结论不是保留的代码。2-3 句说清要验证什么,口头确认即可动手;产出物明确标注一次性。
|
|
56
|
+
- **有界(Bounded)**:改动仓库里**已存在**的流程(新 flag、小端点、单文件修复)。问关键问题后,在聊天里给出几段短设计,停下等确认;不写 spec 文件。
|
|
57
|
+
- **架构级(Architectural)**:新项目、新子系统、改变组件间接口。走完整流程:澄清问题 → 分节设计 → 落盘 spec 文档 → 用户审阅 spec → 再谈实施计划。
|
|
58
|
+
|
|
59
|
+
判断不了时取更重的级别。**棘轮单向**:任务中途发现隐藏复杂度 → 停下明说,升级路径;永不中途降级。
|
|
60
|
+
|
|
61
|
+
## 澄清阶段怎么问
|
|
62
|
+
|
|
63
|
+
1. **最小探查**:只读提出好问题所必需的代码(有 .codegraph/ 时优先 codegraph skill),不做大范围调研。
|
|
64
|
+
2. **复述理解**:一两句复述你对需求的理解,区分「用户说的」和「我假设的」,邀请纠正。
|
|
65
|
+
3. **一次一个问题**:每轮 ask_human 只问当前最关键的一个,自由文本优先(options: []);仅当答案能收敛为少量明确选项时才给 2-4 项。回答引出新歧义时先澄清新歧义,不重复问已回答的。
|
|
66
|
+
|
|
67
|
+
## 收敛确认
|
|
61
68
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
4. **据回答继续**:必要时追问下一个最关键的问题;回答引出新歧义时先澄清新歧义。不要重复问已回答的内容。
|
|
66
|
-
5. **收敛确认**:需求清晰后,用一次 \`ask_human\` 分节呈现方案——目标 / 范围 / 做法与受影响文件 / 不做什么 / 验证方式,选项固定为「确认,开始做」「再调整」「取消」。
|
|
67
|
-
6. **转入执行**:用户确认后再开始改动。
|
|
69
|
+
需求清晰后,用一次 ask_human 分节呈现方案——目标 / 范围 / 做法与受影响文件 / 不做什么 / 验证方式,选项固定为「确认,开始做」「再调整」「取消」。
|
|
70
|
+
|
|
71
|
+
**呈现前自查一遍(静默)**:有没有 TBD 或模糊措辞?各节是否互相矛盾?有没有可双解的需求点(有则当场选定并写明)?修完再呈现,自查过程不必展示。
|
|
68
72
|
|
|
69
73
|
## 边界
|
|
70
74
|
|
|
71
|
-
-
|
|
75
|
+
- 澄清期 ask_human 不计入「执行阶段每轮最多 2 次」的预算,但每个问题必须实质推进理解;用户不耐烦或说「你决定」时,取最安全的可逆默认并说明。
|
|
72
76
|
- 用户取消:停止,不要按自己的猜测继续。
|
|
73
|
-
-
|
|
77
|
+
- 问题与方案用用户的语言。
|
|
74
78
|
- 澄清期间保持只读:不改文件、不跑有副作用的命令。
|
|
75
79
|
`;
|
|
76
80
|
const DEBUGGING_BODY = `# Systematic Debugging(系统化调试)
|
|
77
81
|
|
|
78
|
-
遇到 bug
|
|
82
|
+
遇到 bug、测试失败或报错时,用「先理解再修」的流程。**违反本流程的字面,就是违反调试的精神。**
|
|
83
|
+
|
|
84
|
+
## 铁律
|
|
85
|
+
|
|
86
|
+
> 未定位根因,不得提出修复。
|
|
79
87
|
|
|
80
|
-
|
|
88
|
+
症状不是原因。「让报错消失」和「修好」是两回事——说不清为什么有效,就只是掩盖。
|
|
81
89
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
-
|
|
90
|
+
## 最容易跳过的时刻(恰恰必须用本流程)
|
|
91
|
+
|
|
92
|
+
- 时间紧、线上着急、「就改一行」;
|
|
93
|
+
- 已经连试 2 次以上修复仍失败——这不是「再试一次」的信号,是前提或架构有问题的信号,回到第 1 步质疑假设;
|
|
94
|
+
- 感觉「我大概知道问题在哪」;
|
|
95
|
+
- 报错看起来很简单——简单 bug 也有根因。
|
|
85
96
|
|
|
86
97
|
## 流程
|
|
87
98
|
|
|
88
|
-
1.
|
|
89
|
-
2.
|
|
90
|
-
3.
|
|
91
|
-
4.
|
|
92
|
-
5.
|
|
93
|
-
6.
|
|
99
|
+
1. **复现**:先稳定复现。复现不了就先弄清触发条件(输入、环境、顺序),必要时向用户索取复现步骤或报错全文。
|
|
100
|
+
2. **收集证据**:读完整报错和堆栈,定位到具体文件/行;用日志或最小用例确认数据在**哪一步**开始出错。多组件系统(如 CI→构建→签名、API→DB)先在组件边界加诊断输出,跑一次看断在哪一层,再深入该层。有 .codegraph/ 时用它追调用链。
|
|
101
|
+
3. **形成单一假设**:一句话说清「X 是根因,因为 Y」,并指出能证伪它的检查点。同时只验证这一个假设,不许一次改多处。
|
|
102
|
+
4. **最小验证**:先做最小检查确认假设成立;证据与假设矛盾 → 回到第 2 步,不要硬修。承认「我没看懂这段」比假装看懂更好。
|
|
103
|
+
5. **最小修复**:只改根因,不动无关代码;不加吞异常、放宽校验这类绕症状的补丁。
|
|
104
|
+
6. **回归验证**:复现用例通过;按红绿法补回归测试(见 verification-before-completion skill);检查同类路径是否同样受影响。
|
|
94
105
|
7. **复盘**:说明根因、改动、验证方式;仍有不确定就如实说。
|
|
95
106
|
|
|
96
|
-
##
|
|
107
|
+
## 合理化借口对照
|
|
108
|
+
|
|
109
|
+
| 借口 | 现实 |
|
|
110
|
+
|------|------|
|
|
111
|
+
| 「问题很简单,不用走流程」 | 简单问题也有根因;流程对简单问题很快 |
|
|
112
|
+
| 「紧急,没时间走流程」 | 系统化比反复乱改快 |
|
|
113
|
+
| 「先随手修一下再查原因」 | 第一次随手修会定下错误方向 |
|
|
114
|
+
| 「多改几处一起试,省时间」 | 无法隔离哪个改动起效,还引入新变量 |
|
|
115
|
+
| 「我知道问题在哪,直接修」 | 看到症状 ≠ 理解根因 |
|
|
116
|
+
| 「再试一次就好了」 | 3+ 次失败说明该质疑前提,不是再试 |
|
|
97
117
|
|
|
98
|
-
|
|
99
|
-
- 同一处反复改了 3 次仍失败却不换思路;
|
|
100
|
-
- 修复让错误「消失」但说不清机制(往往只是被掩盖);
|
|
101
|
-
- 顺手重构与 bug 无关的代码。
|
|
118
|
+
如果系统化调查后确认问题确实源于环境/时序/外部依赖、没有代码层根因:记录调查过程,实现恰当处理(重试、超时、报错信息)——这本身是完成流程,不是失败。
|
|
102
119
|
`;
|
|
103
120
|
const CODE_REVIEW_BODY = `# Code Review(自审 diff)
|
|
104
121
|
|
|
105
122
|
提交/收尾前,对**自己本次的改动**做一遍审查。改动越多越要做;几行的机械改动可快速过。
|
|
106
123
|
|
|
107
|
-
##
|
|
124
|
+
## 独立视角优先
|
|
125
|
+
|
|
126
|
+
自审最大的敌人是实现思维惯性——评估和实现在同一个上下文里互相污染。**若工具面可用 sub-agent,优先派子代理审查**:给它精心构造的上下文(任务目标、验收要求、diff 范围),不要给它你的会话历史;只收回结论。sub-agent 不可用或改动很小时自查,按清单逐项过,不要凭整体印象。
|
|
127
|
+
|
|
128
|
+
## 清单
|
|
108
129
|
|
|
109
|
-
1.
|
|
110
|
-
2.
|
|
111
|
-
3.
|
|
112
|
-
4.
|
|
113
|
-
5.
|
|
114
|
-
6.
|
|
115
|
-
7. **完整性**:文档、调用方、测试是否需要同步;用户要求的场景是否都覆盖。
|
|
116
|
-
8. **修复后复审**:发现问题就改,改完重新看那部分 diff,确认没有引入新问题。
|
|
130
|
+
1. **意图对齐**:每处改动是否都服务于任务?有没有夹带无关重构、调试残留(console.log、注释掉的代码、临时文件)?
|
|
131
|
+
2. **正确性**:逻辑分支是否完整;边界(空值、空数组、并发/重复调用、错误路径);条件取反、off-by-one;异步是否有未 await 或未处理的 rejection。
|
|
132
|
+
3. **错误处理**:失败时是否给出有用信息而非静默吞掉;资源(文件句柄、锁、子进程)是否正确释放。
|
|
133
|
+
4. **兼容与契约**:公共 API、类型、配置键、默认值是否保持兼容;是否违背仓库既有约定(如纯 ESM 的 .js 扩展名、工具契约)。
|
|
134
|
+
5. **安全与副作用**:是否引入不可逆操作、权限绕过、注入面;写入/执行是否走了既有护栏。
|
|
135
|
+
6. **完整性**:文档、调用方、测试是否需要同步;用户要求的场景是否都覆盖。
|
|
117
136
|
|
|
118
|
-
##
|
|
137
|
+
## 处理与输出
|
|
119
138
|
|
|
120
|
-
|
|
121
|
-
|
|
139
|
+
- 重要问题:立即修复,修完重看那部分 diff,确认没有引入新问题;
|
|
140
|
+
- 次要问题:记录,不阻塞收尾;
|
|
141
|
+
- 无实质问题:简要说明审过哪些点。
|
|
142
|
+
- 不要为了凑数报风格问题——格式交给 Prettier。
|
|
122
143
|
`;
|
|
123
144
|
const VERIFICATION_BODY = `# Verification Before Completion(完成前证据验证)
|
|
124
145
|
|
|
125
146
|
宣布任务「完成」之前,必须用实际运行的证据确认改动真的生效。禁止把「代码看起来对 / 应该能跑」当作完成。
|
|
126
147
|
|
|
127
|
-
##
|
|
148
|
+
## 铁律
|
|
149
|
+
|
|
150
|
+
**没有本次修改之后产生的运行证据,不得做任何完成/成功/通过类声称。**上一次运行的旧结果、部分检查、别的检查碰巧通过,都不算证据。
|
|
151
|
+
|
|
152
|
+
## 声称 → 必须的证据 → 不算数
|
|
153
|
+
|
|
154
|
+
| 声称 | 必须有 | 不算数 |
|
|
155
|
+
|------|--------|--------|
|
|
156
|
+
| 测试通过 | 测试命令输出 0 failures | 上次运行、「应该通过」 |
|
|
157
|
+
| 构建成功 | build 命令 exit 0 | lint 通过、日志看着没报错 |
|
|
158
|
+
| bug 已修 | 用能复现该 bug 的输入跑过且通过 | 改完代码就当修好 |
|
|
159
|
+
| 回归测试有效 | 红绿循环验证过(见下) | 测试只绿过一次 |
|
|
160
|
+
| 子 agent 完成 | 核查其落盘的改动 | 子 agent 自报 success |
|
|
161
|
+
| 需求全部满足 | 逐条对照用户的验收要求 | 相关测试都绿 |
|
|
162
|
+
|
|
163
|
+
## 红绿回归验证
|
|
164
|
+
|
|
165
|
+
补回归测试时,只绿过一次不算数:写测试 → 确认它在修复前的代码上会红(临时回滚修复,或注入原触发输入) → 恢复修复 → 确认转绿。这样才能证明测试测的就是这个 bug,而不是碰巧通过。
|
|
166
|
+
|
|
167
|
+
## 红旗(出现即停)
|
|
128
168
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
169
|
+
- 「应该没问题」「大概可以」「看起来对」;
|
|
170
|
+
- 还没跑验证就表达满意(「搞定」「完美」);
|
|
171
|
+
- 引用改动前的旧运行结果;
|
|
172
|
+
- 拿与改动无关的检查冒充验证;
|
|
173
|
+
- 测试失败或被跳过,却报告成功。
|
|
133
174
|
|
|
134
175
|
## 怎么选验证(按成本,够小即可)
|
|
135
176
|
|
|
@@ -138,12 +179,9 @@ const VERIFICATION_BODY = `# Verification Before Completion(完成前证据验
|
|
|
138
179
|
- UI/交互类改动:能自动化就自动化(浏览器工具/webapp 测试),否则明确告知未做可视化验证;
|
|
139
180
|
- 纯文档、注释等零行为改动:可不跑验证。
|
|
140
181
|
|
|
141
|
-
##
|
|
182
|
+
## 验证不了就如实说
|
|
142
183
|
|
|
143
|
-
|
|
144
|
-
- 测试其实失败/被跳过,却报告成功;
|
|
145
|
-
- 跑了一个与改动无关的检查,用它冒充验证;
|
|
146
|
-
- 改完代码不重新运行,沿用改动前的旧结果。
|
|
184
|
+
环境缺失、需要用户凭据/设备、或验证成本明显不合理时:说明已验证到哪一步、什么没验证、用户可以怎么验证。如实报告未验证项不是失败;假称验证才是。
|
|
147
185
|
`;
|
|
148
186
|
/** 单一事实源:加新内置 skill 在此数组加一条。 */
|
|
149
187
|
export const builtinSkills = [
|
|
@@ -175,14 +213,16 @@ export const builtinSkills = [
|
|
|
175
213
|
},
|
|
176
214
|
{
|
|
177
215
|
name: 'systematic-debugging',
|
|
178
|
-
description: '系统化调试:遇到 bug
|
|
179
|
-
'
|
|
216
|
+
description: '系统化调试:遇到 bug、测试失败或报错时,在提出任何修复之前先稳定复现并定位根因(读完堆栈、用证据验证假设),' +
|
|
217
|
+
'再做最小修复并回归验证。禁止没找到根因就靠猜测反复改代码。',
|
|
180
218
|
body: DEBUGGING_BODY,
|
|
181
219
|
dir: 'builtin',
|
|
182
220
|
skillMdPath: 'builtin',
|
|
183
221
|
context: 'inline',
|
|
184
222
|
modelInvocable: true,
|
|
185
223
|
origin: 'builtin',
|
|
224
|
+
// debug 需要深度推理:激活期把 effort 提到 high(仅未显式设置时生效)。
|
|
225
|
+
effort: 'high',
|
|
186
226
|
warnings: [],
|
|
187
227
|
},
|
|
188
228
|
{
|