@unifan/pi-unifan-zh 1.0.35 → 1.0.37
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/extensions/workflow/index.ts +312 -96
- package/extensions/workflow/package.json +1 -1
- package/extensions/workflow/skills/00-next/SKILL.md +2 -5
- package/extensions/workflow/src/driver/trigger-matcher.ts +39 -0
- package/extensions/workflow/src/driver/work-loop-driver.ts +19 -1
- package/extensions/workflow/src/tools/workflow-dashboard.ts +325 -0
- package/extensions/workflow/tests/workflow.test.mjs +294 -2
- package/package.json +1 -1
|
@@ -1,52 +1,103 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ArtifactType } from "./src/tools/artifact-helper.js";
|
|
3
|
+
import type { WorkLoopDriver } from "./src/driver/work-loop-driver.js";
|
|
4
|
+
import { isExplicit03WorkTrigger } from "./src/driver/trigger-matcher.js";
|
|
5
|
+
|
|
6
|
+
let workDriverInstance: WorkLoopDriver | null = null;
|
|
7
|
+
async function getWorkDriver(): Promise<WorkLoopDriver> {
|
|
8
|
+
if (!workDriverInstance) {
|
|
9
|
+
const { WorkLoopDriver: DriverClass } = await import("./src/driver/work-loop-driver.js");
|
|
10
|
+
workDriverInstance = new DriverClass();
|
|
11
|
+
}
|
|
12
|
+
return workDriverInstance;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const workflowStateParams = {
|
|
16
|
+
type: "object",
|
|
17
|
+
required: ["repoRoot"],
|
|
18
|
+
properties: {
|
|
19
|
+
repoRoot: {
|
|
20
|
+
type: "string",
|
|
21
|
+
description: "Repository root path to scan for workflow artifacts",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
} as any;
|
|
25
|
+
|
|
26
|
+
const artifactHelperParams = {
|
|
27
|
+
type: "object",
|
|
28
|
+
required: ["repoRoot", "artifactType"],
|
|
29
|
+
properties: {
|
|
30
|
+
repoRoot: {
|
|
31
|
+
type: "string",
|
|
32
|
+
description: "Repository root where workflow artifacts are stored",
|
|
33
|
+
},
|
|
34
|
+
artifactType: {
|
|
35
|
+
anyOf: [
|
|
36
|
+
{ type: "string", const: "brainstorm" },
|
|
37
|
+
{ type: "string", const: "plan" },
|
|
38
|
+
{ type: "string", const: "solution" },
|
|
39
|
+
{ type: "string", const: "checkpoint" },
|
|
40
|
+
],
|
|
41
|
+
description: "Target artifact category",
|
|
42
|
+
},
|
|
43
|
+
topic: {
|
|
44
|
+
type: "string",
|
|
45
|
+
description: "Topic or feature name for the artifact",
|
|
46
|
+
},
|
|
47
|
+
date: {
|
|
48
|
+
type: "string",
|
|
49
|
+
description: "Date prefix formatted as YYYY-MM-DD",
|
|
50
|
+
},
|
|
51
|
+
ensureDir: {
|
|
52
|
+
type: "boolean",
|
|
53
|
+
description: "Whether to create directory if missing",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
} as any;
|
|
57
|
+
|
|
58
|
+
const sessionCheckpointParams = {
|
|
59
|
+
type: "object",
|
|
60
|
+
required: ["operation", "repoRoot"],
|
|
61
|
+
properties: {
|
|
62
|
+
operation: {
|
|
63
|
+
anyOf: [
|
|
64
|
+
{ type: "string", const: "save" },
|
|
65
|
+
{ type: "string", const: "load" },
|
|
66
|
+
{ type: "string", const: "list" },
|
|
67
|
+
{ type: "string", const: "fail" },
|
|
68
|
+
{ type: "string", const: "retry" },
|
|
69
|
+
],
|
|
70
|
+
description: "Checkpoint action to execute",
|
|
71
|
+
},
|
|
72
|
+
repoRoot: {
|
|
73
|
+
type: "string",
|
|
74
|
+
description: "Repository root path",
|
|
75
|
+
},
|
|
76
|
+
planPath: {
|
|
77
|
+
type: "string",
|
|
78
|
+
description: "Path to the plan markdown artifact",
|
|
79
|
+
},
|
|
80
|
+
planSlug: {
|
|
81
|
+
type: "string",
|
|
82
|
+
description: "Slug identifier for the execution plan",
|
|
83
|
+
},
|
|
84
|
+
completedUnits: {
|
|
85
|
+
type: "array",
|
|
86
|
+
items: {
|
|
87
|
+
type: "string",
|
|
88
|
+
},
|
|
89
|
+
description: "List of completed unit names",
|
|
90
|
+
},
|
|
91
|
+
failedUnit: {
|
|
92
|
+
type: "string",
|
|
93
|
+
description: "Unit name that encountered a failure",
|
|
94
|
+
},
|
|
95
|
+
error: {
|
|
96
|
+
type: "string",
|
|
97
|
+
description: "Error description or failure details",
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
} as any;
|
|
50
101
|
|
|
51
102
|
export default function workflowExtension(pi: ExtensionAPI) {
|
|
52
103
|
// 1. Register workflow_state tool (Core engine for 00-next)
|
|
@@ -56,7 +107,8 @@ export default function workflowExtension(pi: ExtensionAPI) {
|
|
|
56
107
|
description:
|
|
57
108
|
"Scan repository artifacts (brainstorms, plans, checkpoints, solutions) and determine current stage and recommended next skill.",
|
|
58
109
|
parameters: workflowStateParams,
|
|
59
|
-
async execute(_toolCallId, params) {
|
|
110
|
+
async execute(_toolCallId, params: any) {
|
|
111
|
+
const { detectWorkflowState } = await import("./src/tools/workflow-state.js");
|
|
60
112
|
const result = await detectWorkflowState(params.repoRoot);
|
|
61
113
|
return {
|
|
62
114
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
@@ -72,7 +124,8 @@ export default function workflowExtension(pi: ExtensionAPI) {
|
|
|
72
124
|
description:
|
|
73
125
|
"Resolve and optionally create standard Compound Engineering artifact paths under docs/ or .context/.",
|
|
74
126
|
parameters: artifactHelperParams,
|
|
75
|
-
async execute(_toolCallId, params) {
|
|
127
|
+
async execute(_toolCallId, params: any) {
|
|
128
|
+
const { resolveArtifactPath } = await import("./src/tools/artifact-helper.js");
|
|
76
129
|
const result = await resolveArtifactPath({
|
|
77
130
|
repoRoot: params.repoRoot,
|
|
78
131
|
artifactType: params.artifactType as ArtifactType,
|
|
@@ -94,7 +147,8 @@ export default function workflowExtension(pi: ExtensionAPI) {
|
|
|
94
147
|
description:
|
|
95
148
|
"Manage plan execution checkpoints: save completed units, load breakpoints, record errors, and retry failed units.",
|
|
96
149
|
parameters: sessionCheckpointParams,
|
|
97
|
-
async execute(_toolCallId, params) {
|
|
150
|
+
async execute(_toolCallId, params: any) {
|
|
151
|
+
const { executeSessionCheckpoint } = await import("./src/tools/session-checkpoint.js");
|
|
98
152
|
const result = await executeSessionCheckpoint({
|
|
99
153
|
operation: params.operation,
|
|
100
154
|
repoRoot: params.repoRoot,
|
|
@@ -125,6 +179,7 @@ export default function workflowExtension(pi: ExtensionAPI) {
|
|
|
125
179
|
if (textBlocks.length === 0) return undefined;
|
|
126
180
|
|
|
127
181
|
const output = textBlocks.map((b) => b.text ?? "").join("");
|
|
182
|
+
const { filterBashOutput } = await import("./src/filters/bash-output-filter.js");
|
|
128
183
|
const result = filterBashOutput({
|
|
129
184
|
command,
|
|
130
185
|
output,
|
|
@@ -163,6 +218,7 @@ export default function workflowExtension(pi: ExtensionAPI) {
|
|
|
163
218
|
const isImage =
|
|
164
219
|
(event.content as Array<{ type: string }>)?.some((b) => b.type === "image") ?? false;
|
|
165
220
|
|
|
221
|
+
const { filterReadOutput } = await import("./src/filters/read-output-filter.js");
|
|
166
222
|
const result = filterReadOutput({
|
|
167
223
|
path: filePath,
|
|
168
224
|
output,
|
|
@@ -185,58 +241,143 @@ export default function workflowExtension(pi: ExtensionAPI) {
|
|
|
185
241
|
};
|
|
186
242
|
});
|
|
187
243
|
|
|
188
|
-
// 6. User Command: /workflow (
|
|
244
|
+
// 6. User Command: /workflow (查看工作流全阶段看板与自主循环控制)
|
|
189
245
|
pi.registerCommand("workflow", {
|
|
190
|
-
description: "
|
|
191
|
-
async handler(
|
|
246
|
+
description: "查看复合工程工作流全流程步骤状态看板与自主干活循环控制",
|
|
247
|
+
async handler(args, ctx) {
|
|
248
|
+
const sub = (args || "").trim().toLowerCase();
|
|
249
|
+
const workDriver = await getWorkDriver();
|
|
250
|
+
|
|
251
|
+
if (sub === "pause" || sub === "暂停") {
|
|
252
|
+
if (workDriver.getStatus().isActive) {
|
|
253
|
+
detachEscapeListener();
|
|
254
|
+
await workDriver.pause("用户执行 /workflow pause");
|
|
255
|
+
ctx.ui?.setStatus?.("workflow", undefined);
|
|
256
|
+
ctx.ui?.notify?.("⏸️ 03-work 自主循环已安全暂停。", "info");
|
|
257
|
+
} else {
|
|
258
|
+
ctx.ui?.notify?.("⚪ 03-work 当前未处于活跃运行状态。", "info");
|
|
259
|
+
}
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (sub === "resume" || sub === "继续") {
|
|
264
|
+
workDriver.setRepoRoot(ctx.cwd || process.cwd());
|
|
265
|
+
const res = await workDriver.start();
|
|
266
|
+
if (res.success && res.status.isActive) {
|
|
267
|
+
attachEscapeListener(ctx);
|
|
268
|
+
ctx.ui?.setStatus?.(
|
|
269
|
+
"workflow",
|
|
270
|
+
`🔄 03-work 自主干活 [${res.status.completedUnits.length}/${res.status.allUnits.length}]`,
|
|
271
|
+
);
|
|
272
|
+
ctx.ui?.notify?.("🚀 03-work 自主循环已恢复驱动。", "info");
|
|
273
|
+
} else {
|
|
274
|
+
ctx.ui?.notify?.(res.message, "info");
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (sub === "hide" || sub === "off" || sub === "close" || sub === "关闭" || sub === "隐藏") {
|
|
280
|
+
ctx.ui?.setWidget?.("workflow", undefined);
|
|
281
|
+
ctx.ui?.setStatus?.("workflow", undefined);
|
|
282
|
+
ctx.ui?.notify?.("工作流状态栏已隐藏。", "info");
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const { detectWorkflowState } = await import("./src/tools/workflow-state.js");
|
|
287
|
+
const { buildWorkflowDashboard, buildWorkflowWidgetLines } = await import(
|
|
288
|
+
"./src/tools/workflow-dashboard.js"
|
|
289
|
+
);
|
|
192
290
|
const repoRoot = ctx.cwd || process.cwd();
|
|
193
291
|
const state = await detectWorkflowState(repoRoot);
|
|
194
292
|
const workStatus = workDriver.getStatus();
|
|
195
293
|
|
|
196
|
-
const
|
|
197
|
-
`🎯 **复合工程工作流状态 (Compound Engineering)**`,
|
|
198
|
-
`📁 仓库路径: \`${state.repoRoot}\``,
|
|
199
|
-
`📊 当前阶段: **${state.stage.toUpperCase()}**`,
|
|
200
|
-
`🚀 推荐下一步: \`/skill:${state.recommendedSkill}\``,
|
|
201
|
-
`💡 理由: ${state.recommendationReason}`,
|
|
202
|
-
``,
|
|
203
|
-
workStatus.isActive
|
|
204
|
-
? `⚡ **03-work 自主循环运行中**: [${workStatus.completedUnits.length}/${workStatus.allUnits.length}] 当前推进: **${workStatus.currentUnit || "全部完成"}**`
|
|
205
|
-
: `⚡ **03-work 自主循环状态**: ⚪ 空闲(调用 /skill:03-work 即可自动循环驱动)`,
|
|
206
|
-
``,
|
|
207
|
-
`📋 **产物概览**:`,
|
|
208
|
-
`- 需求文档 (Brainstorms): ${state.brainstorms.length} 个 ${state.latestBrainstorm ? `(最新: ${state.latestBrainstorm.filename})` : ""}`,
|
|
209
|
-
`- 执行计划 (Plans): ${state.plans.length} 个 ${state.latestPlan ? `(最新: ${state.latestPlan.filename})` : ""}`,
|
|
210
|
-
`- 运行断点 (Checkpoints): ${state.checkpoints.length} 个 ${state.activeCheckpoint ? `(已完成: ${state.activeCheckpoint.completedUnits.length} 单元)` : ""}`,
|
|
211
|
-
`- 避坑经验 (Solutions): ${state.solutions.length} 个 ${state.latestSolution ? `(最新: ${state.latestSolution.filename})` : ""}`,
|
|
212
|
-
].join("\n");
|
|
213
|
-
|
|
294
|
+
const dashboard = buildWorkflowDashboard(state, workStatus);
|
|
214
295
|
if (ctx.hasUI) {
|
|
215
|
-
ctx.ui.notify?.(
|
|
296
|
+
ctx.ui.notify?.(dashboard, "info");
|
|
297
|
+
const widgetLines = buildWorkflowWidgetLines(state, workStatus);
|
|
298
|
+
ctx.ui.setWidget?.("workflow", widgetLines, { placement: "aboveEditor" });
|
|
216
299
|
}
|
|
217
300
|
},
|
|
218
301
|
});
|
|
219
302
|
|
|
220
|
-
|
|
221
|
-
|
|
303
|
+
pi.registerCommand("workflow-pause", {
|
|
304
|
+
description: "暂停当前正在运行的 03-work 自主干活循环 (同 Esc / 输入 '暂停')",
|
|
305
|
+
async handler(_args, ctx) {
|
|
306
|
+
const workDriver = await getWorkDriver();
|
|
307
|
+
if (workDriver.getStatus().isActive) {
|
|
308
|
+
detachEscapeListener();
|
|
309
|
+
await workDriver.pause("用户执行 /workflow-pause");
|
|
310
|
+
ctx.ui?.setStatus?.("workflow", undefined);
|
|
311
|
+
ctx.ui?.notify?.("⏸️ 03-work 自主循环已安全暂停。", "info");
|
|
312
|
+
} else {
|
|
313
|
+
ctx.ui?.notify?.("⚪ 03-work 当前未处于活跃运行状态。", "info");
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
});
|
|
222
317
|
|
|
223
|
-
|
|
318
|
+
pi.registerCommand("workflow-resume", {
|
|
319
|
+
description: "恢复当前计划的 03-work 自主干活循环 (同 /skill:03-work / 输入 '继续干活')",
|
|
320
|
+
async handler(_args, ctx) {
|
|
321
|
+
const workDriver = await getWorkDriver();
|
|
322
|
+
workDriver.setRepoRoot(ctx.cwd || process.cwd());
|
|
323
|
+
const res = await workDriver.start();
|
|
324
|
+
if (res.success && res.status.isActive) {
|
|
325
|
+
attachEscapeListener(ctx);
|
|
326
|
+
ctx.ui?.setStatus?.(
|
|
327
|
+
"workflow",
|
|
328
|
+
`🔄 03-work 自主干活 [${res.status.completedUnits.length}/${res.status.allUnits.length}]`,
|
|
329
|
+
);
|
|
330
|
+
ctx.ui?.notify?.("🚀 03-work 自主循环已恢复驱动。", "info");
|
|
331
|
+
} else {
|
|
332
|
+
ctx.ui?.notify?.(res.message, "info");
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// 7. 03-work 原生自主循环驱动引擎与极轻量按需键盘中断监听
|
|
338
|
+
// 仅在 03-work 自主循环运行时按需挂载 Esc 监听,暂停或结束时立即脱钩,日常打字 0 开销
|
|
339
|
+
let terminalInputUnsub: (() => void) | null = null;
|
|
340
|
+
async function attachEscapeListener(ctx: ExtensionContext) {
|
|
341
|
+
if (ctx.hasUI && !terminalInputUnsub && ctx.ui?.onTerminalInput) {
|
|
342
|
+
const { matchesKey } = await import("@earendil-works/pi-tui");
|
|
343
|
+
terminalInputUnsub = ctx.ui.onTerminalInput((data: string) => {
|
|
344
|
+
if (
|
|
345
|
+
(data === "\x1b" || matchesKey(data, "escape")) &&
|
|
346
|
+
workDriverInstance &&
|
|
347
|
+
workDriverInstance.getStatus().isActive
|
|
348
|
+
) {
|
|
349
|
+
detachEscapeListener();
|
|
350
|
+
workDriverInstance.pause("用户按 Esc 中断");
|
|
351
|
+
ctx.ui?.setStatus?.("workflow", undefined);
|
|
352
|
+
ctx.ui?.notify?.(
|
|
353
|
+
"⏸️ 03-work 自主循环已响应 Esc 安全暂停。随时输入“继续”或调用 /skill:03-work 即可恢复。",
|
|
354
|
+
"info",
|
|
355
|
+
);
|
|
356
|
+
// 不消费按键,透传给宿主让 Pi 终止当前正在运行的模型回合或工具命令
|
|
357
|
+
return undefined;
|
|
358
|
+
}
|
|
359
|
+
return undefined;
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function detachEscapeListener() {
|
|
365
|
+
if (terminalInputUnsub) {
|
|
366
|
+
terminalInputUnsub();
|
|
367
|
+
terminalInputUnsub = null;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 监听用户输入与技能启动:只有显式触发 03-work 时才启动自主循环(绝不读取 systemPrompt)
|
|
224
372
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
(promptLower.includes("03") &&
|
|
230
|
-
(promptLower.includes("work") ||
|
|
231
|
-
promptLower.includes("继续") ||
|
|
232
|
-
promptLower.includes("恢复") ||
|
|
233
|
-
promptLower.includes("resume") ||
|
|
234
|
-
promptLower.includes("干活")));
|
|
235
|
-
|
|
236
|
-
if (is03Work && !workDriver.getStatus().isActive) {
|
|
373
|
+
const is03Work = isExplicit03WorkTrigger(event.prompt);
|
|
374
|
+
|
|
375
|
+
if (is03Work && (!workDriverInstance || !workDriverInstance.getStatus().isActive)) {
|
|
376
|
+
const workDriver = await getWorkDriver();
|
|
237
377
|
workDriver.setRepoRoot(ctx.cwd || process.cwd());
|
|
238
378
|
const res = await workDriver.start();
|
|
239
379
|
if (res.success && res.status.isActive) {
|
|
380
|
+
attachEscapeListener(ctx);
|
|
240
381
|
const nextUnit = res.status.currentUnit ? `下一个: ${res.status.currentUnit}` : "准备就绪";
|
|
241
382
|
ctx.ui?.setStatus?.(
|
|
242
383
|
"workflow",
|
|
@@ -246,14 +387,36 @@ export default function workflowExtension(pi: ExtensionAPI) {
|
|
|
246
387
|
}
|
|
247
388
|
});
|
|
248
389
|
|
|
249
|
-
|
|
390
|
+
const WORKFLOW_SHORTCUTS: Record<string, string> = {
|
|
391
|
+
"00": "00-next",
|
|
392
|
+
"/00": "00-next",
|
|
393
|
+
"01": "01-brainstorm",
|
|
394
|
+
"/01": "01-brainstorm",
|
|
395
|
+
"02": "02-plan",
|
|
396
|
+
"/02": "02-plan",
|
|
397
|
+
"03": "03-work",
|
|
398
|
+
"/03": "03-work",
|
|
399
|
+
"04": "04-review",
|
|
400
|
+
"/04": "04-review",
|
|
401
|
+
"05": "05-learn",
|
|
402
|
+
"/05": "05-learn",
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
// 监听用户打断指令与极简快捷指令(如输入 01 自动转为 /skill:01-brainstorm)
|
|
250
406
|
pi.on("input", async (event, ctx) => {
|
|
251
|
-
|
|
407
|
+
if (event.source === "extension") return { action: "continue" };
|
|
408
|
+
|
|
409
|
+
const raw = event.text.trim();
|
|
410
|
+
const lower = raw.toLowerCase();
|
|
411
|
+
|
|
412
|
+
// 1. 暂停/打断指令:用户若输入“暂停”或“停止”,自动安全挂起 03 自主循环
|
|
252
413
|
if (
|
|
253
|
-
|
|
254
|
-
(
|
|
414
|
+
workDriverInstance &&
|
|
415
|
+
workDriverInstance.getStatus().isActive &&
|
|
416
|
+
(lower === "暂停" || lower === "停止" || lower === "pause" || lower === "stop")
|
|
255
417
|
) {
|
|
256
|
-
|
|
418
|
+
detachEscapeListener();
|
|
419
|
+
await workDriverInstance.pause("用户手动输入暂停");
|
|
257
420
|
ctx.ui?.setStatus?.("workflow", undefined);
|
|
258
421
|
ctx.ui?.notify?.(
|
|
259
422
|
"⏸️ 03-work 自主循环已暂停。随时输入“继续”或调用 /skill:03-work 即可恢复。",
|
|
@@ -261,15 +424,68 @@ export default function workflowExtension(pi: ExtensionAPI) {
|
|
|
261
424
|
);
|
|
262
425
|
return { action: "handled" };
|
|
263
426
|
}
|
|
427
|
+
|
|
428
|
+
// 2. 阶段编号快捷直达:输入 00~05 自动转换为 /skill:0x-xxx
|
|
429
|
+
const match = raw.match(/^(\/?0[0-5])(?:\s+(.*))?$/);
|
|
430
|
+
if (match) {
|
|
431
|
+
const shortcutKey = match[1];
|
|
432
|
+
const skillName = WORKFLOW_SHORTCUTS[shortcutKey];
|
|
433
|
+
if (skillName) {
|
|
434
|
+
// 若当前正在 03 自主干活中,切换阶段时先优雅暂停 03
|
|
435
|
+
if (skillName !== "03-work" && workDriverInstance && workDriverInstance.getStatus().isActive) {
|
|
436
|
+
detachEscapeListener();
|
|
437
|
+
await workDriverInstance.pause(`用户通过快捷键切换至 ${skillName}`);
|
|
438
|
+
ctx.ui?.setStatus?.("workflow", undefined);
|
|
439
|
+
}
|
|
440
|
+
const rest = match[2] ? ` ${match[2]}` : "";
|
|
441
|
+
return { action: "transform", text: `/skill:${skillName}${rest}` };
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
264
445
|
return { action: "continue" };
|
|
265
446
|
});
|
|
266
447
|
|
|
448
|
+
// 监听单轮结束 (turn_end):若因 Ctrl+C / Esc 导致当前轮次被中止,立即安全挂起
|
|
449
|
+
pi.on("turn_end", async (event, ctx) => {
|
|
450
|
+
const msg = event.message as any;
|
|
451
|
+
if (
|
|
452
|
+
workDriverInstance &&
|
|
453
|
+
workDriverInstance.getStatus().isActive &&
|
|
454
|
+
msg?.role === "assistant" &&
|
|
455
|
+
msg?.stopReason === "aborted"
|
|
456
|
+
) {
|
|
457
|
+
detachEscapeListener();
|
|
458
|
+
await workDriverInstance.pause("检测到助理回合中断 (stopReason: aborted)");
|
|
459
|
+
ctx.ui?.setStatus?.("workflow", undefined);
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// 监听整段运行结束 (agent_end):检测 abort 信号或中止消息并挂起,防止 runaway 循环
|
|
464
|
+
pi.on("agent_end", async (event, ctx) => {
|
|
465
|
+
if (workDriverInstance && workDriverInstance.getStatus().isActive) {
|
|
466
|
+
const hasAborted =
|
|
467
|
+
ctx.signal?.aborted ||
|
|
468
|
+
event.messages.some((m: any) => m?.role === "assistant" && m?.stopReason === "aborted");
|
|
469
|
+
if (hasAborted) {
|
|
470
|
+
detachEscapeListener();
|
|
471
|
+
await workDriverInstance.pause("检测到会话中断信号 (Ctrl+C / Esc)");
|
|
472
|
+
ctx.ui?.setStatus?.("workflow", undefined);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
|
|
267
477
|
// 核心事件循环:每个回合结束后,若仍有未完成单元,自动注入下一回合实现“不做完不停机”
|
|
268
478
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
269
|
-
|
|
479
|
+
if (workDriverInstance && workDriverInstance.getStatus().isActive) {
|
|
480
|
+
await workDriverInstance.onAgentSettled(ctx, pi);
|
|
481
|
+
if (!workDriverInstance.getStatus().isActive) {
|
|
482
|
+
detachEscapeListener();
|
|
483
|
+
}
|
|
484
|
+
}
|
|
270
485
|
});
|
|
271
486
|
|
|
272
487
|
pi.on("session_shutdown", async () => {
|
|
273
|
-
|
|
488
|
+
detachEscapeListener();
|
|
489
|
+
workDriverInstance?.cancelTimer();
|
|
274
490
|
});
|
|
275
491
|
}
|
|
@@ -34,8 +34,5 @@ description: "检查工作流产物并推荐/导航到最佳的下一步技能
|
|
|
34
34
|
## 执行流程
|
|
35
35
|
|
|
36
36
|
1. 调用 `workflow_state({ repoRoot: "." })`。
|
|
37
|
-
2.
|
|
38
|
-
|
|
39
|
-
- 🚀 推荐技能(如 `/skill:01-brainstorm`、`/skill:02-plan` 等)
|
|
40
|
-
- 💡 推荐理由
|
|
41
|
-
3. 询问用户是否立刻进入推荐技能。
|
|
37
|
+
2. 输出状态概览与推荐技能(亦可随时运行 `/workflow` 查看全量看板)。
|
|
38
|
+
3. 询问用户是否进入推荐技能。
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, lightweight trigger matcher for 03-work.
|
|
3
|
+
* Has ZERO external imports to ensure near-zero cold startup overhead.
|
|
4
|
+
*/
|
|
5
|
+
export function isExplicit03WorkTrigger(prompt: string): boolean {
|
|
6
|
+
if (!prompt) return false;
|
|
7
|
+
const trimmed = prompt.trim();
|
|
8
|
+
const lower = trimmed.toLowerCase();
|
|
9
|
+
|
|
10
|
+
// 1. Direct skill invocation or slash command
|
|
11
|
+
if (
|
|
12
|
+
lower.startsWith("/skill:03-work") ||
|
|
13
|
+
lower === "/skill:03" ||
|
|
14
|
+
lower.startsWith("03-work")
|
|
15
|
+
) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 2. Strict keywords and natural language commands to start/resume 03 work
|
|
20
|
+
const strictPatterns = [
|
|
21
|
+
/^(\/)?(03|03-work)$/i,
|
|
22
|
+
/^开始(03|干活|编码|实现|写代码)/,
|
|
23
|
+
/^执行(03|03-work|干活)/,
|
|
24
|
+
/^自主(干活|工作|编码)/,
|
|
25
|
+
/^(继续|恢复)(干活|03|工作|work)/i,
|
|
26
|
+
/^resume\s+(03|work)/i,
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
if (strictPatterns.some((p) => p.test(trimmed))) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 3. Autonomous continuation follow-up prompt generated by the driver itself
|
|
34
|
+
if (trimmed.startsWith("【03-work 自主循环驱动引擎")) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
@@ -95,6 +95,8 @@ export async function findLatestPlanFile(repoRoot: string): Promise<string | nul
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
export { isExplicit03WorkTrigger } from "./trigger-matcher.js";
|
|
99
|
+
|
|
98
100
|
/**
|
|
99
101
|
* Autonomous Loop Driver for 03-work.
|
|
100
102
|
* Ensures the agent drives continuously across turns until all planned units are 100% finished.
|
|
@@ -282,13 +284,29 @@ export class WorkLoopDriver {
|
|
|
282
284
|
async onAgentSettled(ctx: ExtensionContext, pi: ExtensionAPI): Promise<void> {
|
|
283
285
|
if (!this.isActive || !this.planPath) return;
|
|
284
286
|
|
|
285
|
-
// 1. Check if user aborted (Esc pressed or
|
|
287
|
+
// 1. Check if user aborted (Esc pressed, abort signal, or aborted assistant message)
|
|
286
288
|
if (ctx.signal?.aborted) {
|
|
287
289
|
await this.pause("检测到用户中断信号 (Esc/Abort)");
|
|
290
|
+
ctx.ui?.setStatus?.("workflow", undefined);
|
|
288
291
|
ctx.ui?.notify?.("⏸️ 03-work 自主循环已响应中断信号安全暂停。", "info");
|
|
289
292
|
return;
|
|
290
293
|
}
|
|
291
294
|
|
|
295
|
+
try {
|
|
296
|
+
const branch = ctx.sessionManager?.getBranch?.() ?? [];
|
|
297
|
+
const lastMsgEntry = branch
|
|
298
|
+
.slice()
|
|
299
|
+
.reverse()
|
|
300
|
+
.find((e: any) => e?.type === "message" && e?.message?.role === "assistant");
|
|
301
|
+
const lastAssistantMsg = (lastMsgEntry as any)?.message;
|
|
302
|
+
if (lastAssistantMsg?.stopReason === "aborted") {
|
|
303
|
+
await this.pause("检测到会话中断 (stopReason: aborted)");
|
|
304
|
+
ctx.ui?.setStatus?.("workflow", undefined);
|
|
305
|
+
ctx.ui?.notify?.("⏸️ 03-work 自主循环已响应中断信号安全暂停。", "info");
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
} catch {}
|
|
309
|
+
|
|
292
310
|
// 2. Re-read checkpoint from disk to see what the agent achieved in this turn
|
|
293
311
|
const cpResult = await executeSessionCheckpoint({
|
|
294
312
|
operation: "load",
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import type { WorkflowStateResult } from "./workflow-state.js";
|
|
2
|
+
import type { WorkStatus } from "../driver/work-loop-driver.js";
|
|
3
|
+
|
|
4
|
+
export interface WorkflowStepItem {
|
|
5
|
+
number: string;
|
|
6
|
+
id: "01-brainstorm" | "02-plan" | "03-work" | "04-review" | "05-learn";
|
|
7
|
+
title: string;
|
|
8
|
+
status: "completed" | "in_progress" | "ready" | "pending" | "idle";
|
|
9
|
+
statusLabel: string;
|
|
10
|
+
detail: string;
|
|
11
|
+
isCurrent: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generates an ASCII progress bar, e.g. [██████░░░░░░░░░░] 38%
|
|
16
|
+
*/
|
|
17
|
+
export function renderProgressBar(completed: number, total: number, barLength = 12): string {
|
|
18
|
+
if (total <= 0) return "░".repeat(barLength);
|
|
19
|
+
const ratio = Math.min(1, Math.max(0, completed / total));
|
|
20
|
+
const filled = Math.round(ratio * barLength);
|
|
21
|
+
const empty = barLength - filled;
|
|
22
|
+
return "█".repeat(filled) + "░".repeat(empty);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Derives the full 5-step status array from workflow state and driver status.
|
|
27
|
+
*/
|
|
28
|
+
export function deriveWorkflowSteps(
|
|
29
|
+
state: WorkflowStateResult,
|
|
30
|
+
workStatus: WorkStatus,
|
|
31
|
+
): WorkflowStepItem[] {
|
|
32
|
+
// Determine current active step
|
|
33
|
+
let currentStepId: WorkflowStepItem["id"] = state.recommendedSkill || "01-brainstorm";
|
|
34
|
+
if (workStatus.isActive) {
|
|
35
|
+
currentStepId = "03-work";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const steps: WorkflowStepItem[] = [];
|
|
39
|
+
|
|
40
|
+
// Step 01: 01-brainstorm
|
|
41
|
+
const hasBrainstorm = state.brainstorms.length > 0;
|
|
42
|
+
steps.push({
|
|
43
|
+
number: "01",
|
|
44
|
+
id: "01-brainstorm",
|
|
45
|
+
title: "需求发现与规格说明",
|
|
46
|
+
status: hasBrainstorm ? "completed" : "ready",
|
|
47
|
+
statusLabel: hasBrainstorm ? "[已完成]" : "[就绪]",
|
|
48
|
+
detail: hasBrainstorm
|
|
49
|
+
? `${state.brainstorms.length} 个需求文档 (最新: ${state.latestBrainstorm?.filename || "-"})`
|
|
50
|
+
: "建议梳理目标、边界与成功标准",
|
|
51
|
+
isCurrent: currentStepId === "01-brainstorm",
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Step 02: 02-plan
|
|
55
|
+
const hasPlan = state.plans.length > 0;
|
|
56
|
+
steps.push({
|
|
57
|
+
number: "02",
|
|
58
|
+
id: "02-plan",
|
|
59
|
+
title: "架构设计与TDD计划",
|
|
60
|
+
status: hasPlan ? "completed" : hasBrainstorm ? "ready" : "pending",
|
|
61
|
+
statusLabel: hasPlan ? "[已完成]" : hasBrainstorm ? "[就绪]" : "[待办]",
|
|
62
|
+
detail: hasPlan
|
|
63
|
+
? `${state.plans.length} 个计划文档 (最新: ${state.latestPlan?.filename || "-"})`
|
|
64
|
+
: hasBrainstorm
|
|
65
|
+
? "可基于需求拆解架构与 TDD 实施单元"
|
|
66
|
+
: "待梳理需求文档",
|
|
67
|
+
isCurrent: currentStepId === "02-plan",
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Step 03: 03-work
|
|
71
|
+
const totalUnits = state.totalUnitsCount ?? workStatus.allUnits.length;
|
|
72
|
+
const completedUnits = state.completedUnitsCount ?? workStatus.completedUnits.length;
|
|
73
|
+
const remainingUnits = state.remainingUnitsCount ?? workStatus.remainingUnits.length;
|
|
74
|
+
|
|
75
|
+
let workStepStatus: WorkflowStepItem["status"] = "idle";
|
|
76
|
+
let workStepLabel = "[空闲]";
|
|
77
|
+
let workStepDetail = "待制定带实施单元的计划文档";
|
|
78
|
+
|
|
79
|
+
if (workStatus.isActive) {
|
|
80
|
+
workStepStatus = "in_progress";
|
|
81
|
+
workStepLabel = "[⚡ 运行中]";
|
|
82
|
+
const percent = totalUnits > 0 ? Math.round((completedUnits / totalUnits) * 100) : 0;
|
|
83
|
+
workStepDetail = `单元进度: [${renderProgressBar(completedUnits, totalUnits, 8)}] ${completedUnits}/${totalUnits} (${percent}%) 当前: ${workStatus.currentUnit || "就绪"}`;
|
|
84
|
+
} else if (workStatus.lastMessage && workStatus.lastMessage.includes("暂停")) {
|
|
85
|
+
workStepStatus = "in_progress";
|
|
86
|
+
workStepLabel = "[⏸️ 已暂停]";
|
|
87
|
+
const percent = totalUnits > 0 ? Math.round((completedUnits / totalUnits) * 100) : 0;
|
|
88
|
+
workStepDetail = `单元进度: [${renderProgressBar(completedUnits, totalUnits, 8)}] ${completedUnits}/${totalUnits} (${percent}%),已安全暂存`;
|
|
89
|
+
} else if (state.activeCheckpoint?.failedUnit) {
|
|
90
|
+
workStepStatus = "in_progress";
|
|
91
|
+
workStepLabel = "[🛑 遇错暂停]";
|
|
92
|
+
workStepDetail = `失败单元: ${state.activeCheckpoint.failedUnit},建议排查报错`;
|
|
93
|
+
} else if (totalUnits > 0) {
|
|
94
|
+
if (remainingUnits === 0) {
|
|
95
|
+
workStepStatus = "completed";
|
|
96
|
+
workStepLabel = "[已完成]";
|
|
97
|
+
workStepDetail = `全部 ${totalUnits} 个规划单元测试均已绿灯通过`;
|
|
98
|
+
} else if (completedUnits > 0) {
|
|
99
|
+
workStepStatus = "in_progress";
|
|
100
|
+
workStepLabel = "[进行中]";
|
|
101
|
+
const percent = Math.round((completedUnits / totalUnits) * 100);
|
|
102
|
+
workStepDetail = `单元进度: [${renderProgressBar(completedUnits, totalUnits, 8)}] ${completedUnits}/${totalUnits} (${percent}%) 下一待办: ${state.nextUnitId || "-"}`;
|
|
103
|
+
} else {
|
|
104
|
+
workStepStatus = "ready";
|
|
105
|
+
workStepLabel = "[就绪]";
|
|
106
|
+
workStepDetail = `计划已就绪,共 ${totalUnits} 个单元待编码`;
|
|
107
|
+
}
|
|
108
|
+
} else if (hasPlan) {
|
|
109
|
+
workStepStatus = "ready";
|
|
110
|
+
workStepLabel = "[就绪]";
|
|
111
|
+
workStepDetail = "计划文档已存在,随时可启动自主干活";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
steps.push({
|
|
115
|
+
number: "03",
|
|
116
|
+
id: "03-work",
|
|
117
|
+
title: "自主编码与循环推进",
|
|
118
|
+
status: workStepStatus,
|
|
119
|
+
statusLabel: workStepLabel,
|
|
120
|
+
detail: workStepDetail,
|
|
121
|
+
isCurrent: currentStepId === "03-work",
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Step 04: 04-review
|
|
125
|
+
const isWorkDone = totalUnits > 0 && remainingUnits === 0;
|
|
126
|
+
steps.push({
|
|
127
|
+
number: "04",
|
|
128
|
+
id: "04-review",
|
|
129
|
+
title: "代码质量与规格审查",
|
|
130
|
+
status: isWorkDone ? "ready" : state.stage === "completed" ? "completed" : "pending",
|
|
131
|
+
statusLabel: state.stage === "completed" ? "[已完成]" : isWorkDone ? "[就绪]" : "[待办]",
|
|
132
|
+
detail: isWorkDone
|
|
133
|
+
? "单元实现已完毕,可启动全量代码审查与回归验证"
|
|
134
|
+
: state.stage === "completed"
|
|
135
|
+
? "审查已完成,项目规格与质量达标"
|
|
136
|
+
: "建议在 Implementation Units 验证后审查",
|
|
137
|
+
isCurrent: currentStepId === "04-review",
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Step 05: 05-learn
|
|
141
|
+
const hasSolutions = state.solutions.length > 0;
|
|
142
|
+
steps.push({
|
|
143
|
+
number: "05",
|
|
144
|
+
id: "05-learn",
|
|
145
|
+
title: "知识复盘与经验沉淀",
|
|
146
|
+
status: hasSolutions ? "completed" : state.stage === "completed" ? "ready" : "pending",
|
|
147
|
+
statusLabel: hasSolutions ? "[已沉淀]" : state.stage === "completed" ? "[就绪]" : "[待办]",
|
|
148
|
+
detail: hasSolutions
|
|
149
|
+
? `${state.solutions.length} 个避坑指南 (最新: ${state.latestSolution?.filename || "-"})`
|
|
150
|
+
: state.stage === "completed"
|
|
151
|
+
? "闭环已完成,可沉淀高价值非平凡解决方案"
|
|
152
|
+
: "在开发与审查完毕后进行",
|
|
153
|
+
isCurrent: currentStepId === "05-learn",
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
return steps;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Helper to calculate terminal display width considering CJK full-width characters and ANSI codes.
|
|
161
|
+
*/
|
|
162
|
+
export function getVisibleWidth(str: string): number {
|
|
163
|
+
let width = 0;
|
|
164
|
+
for (let i = 0; i < str.length; i++) {
|
|
165
|
+
const code = str.charCodeAt(i);
|
|
166
|
+
if (code === 0x1b && str[i + 1] === "[") {
|
|
167
|
+
const mIdx = str.indexOf("m", i);
|
|
168
|
+
if (mIdx !== -1) {
|
|
169
|
+
i = mIdx;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (
|
|
174
|
+
(code >= 0x1100 && code <= 0x115f) ||
|
|
175
|
+
(code >= 0x2e80 && code <= 0xa4cf) ||
|
|
176
|
+
(code >= 0xac00 && code <= 0xd7a3) ||
|
|
177
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
178
|
+
(code >= 0xfe10 && code <= 0xfe19) ||
|
|
179
|
+
(code >= 0xfe30 && code <= 0xfe6f) ||
|
|
180
|
+
(code >= 0xff00 && code <= 0xff60) ||
|
|
181
|
+
(code >= 0xffe0 && code <= 0xffe6) ||
|
|
182
|
+
code >= 0x10000
|
|
183
|
+
) {
|
|
184
|
+
width += 2;
|
|
185
|
+
} else {
|
|
186
|
+
width += 1;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return width;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Truncates string to a maximum visible width, appending an ellipsis if truncated.
|
|
194
|
+
*/
|
|
195
|
+
export function truncateToVisibleWidth(str: string, maxWidth: number): string {
|
|
196
|
+
if (getVisibleWidth(str) <= maxWidth) return str;
|
|
197
|
+
let currentWidth = 0;
|
|
198
|
+
let result = "";
|
|
199
|
+
for (let i = 0; i < str.length; i++) {
|
|
200
|
+
const char = str[i];
|
|
201
|
+
const code = str.charCodeAt(i);
|
|
202
|
+
const charWidth =
|
|
203
|
+
(code >= 0x1100 && code <= 0x115f) ||
|
|
204
|
+
(code >= 0x2e80 && code <= 0xa4cf) ||
|
|
205
|
+
(code >= 0xac00 && code <= 0xd7a3) ||
|
|
206
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
207
|
+
(code >= 0xfe10 && code <= 0xfe19) ||
|
|
208
|
+
(code >= 0xfe30 && code <= 0xfe6f) ||
|
|
209
|
+
(code >= 0xff00 && code <= 0xff60) ||
|
|
210
|
+
(code >= 0xffe0 && code <= 0xffe6) ||
|
|
211
|
+
code >= 0x10000
|
|
212
|
+
? 2
|
|
213
|
+
: 1;
|
|
214
|
+
|
|
215
|
+
if (currentWidth + charWidth + 1 > maxWidth) {
|
|
216
|
+
result += "…";
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
result += char;
|
|
220
|
+
currentWidth += charWidth;
|
|
221
|
+
}
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function renderBoxHeader(left: string, right = "", width = 76): string {
|
|
226
|
+
const inner = width - 2;
|
|
227
|
+
const l = `─ ${left} `;
|
|
228
|
+
const r = right ? ` ${right} ─` : "─";
|
|
229
|
+
const fixed = getVisibleWidth(l) + getVisibleWidth(r);
|
|
230
|
+
const fill = Math.max(1, inner - fixed);
|
|
231
|
+
return `╭${l}${"─".repeat(fill)}${r}╮`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function renderBoxLine(content: string, width = 76): string {
|
|
235
|
+
const inner = width - 4;
|
|
236
|
+
const truncated = truncateToVisibleWidth(content, inner);
|
|
237
|
+
const visibleLen = getVisibleWidth(truncated);
|
|
238
|
+
const pad = Math.max(0, inner - visibleLen);
|
|
239
|
+
return `│ ${truncated}${" ".repeat(pad)} │`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function renderBoxFooter(text = "", width = 76): string {
|
|
243
|
+
const inner = width - 2;
|
|
244
|
+
if (!text) return `╰${"─".repeat(inner)}╯`;
|
|
245
|
+
const t = `─ ${text} `;
|
|
246
|
+
const fill = Math.max(1, inner - getVisibleWidth(t));
|
|
247
|
+
return `╰${t}${"─".repeat(fill)}╯`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Builds a structured, visually polished compact ASCII status dashboard (Goal-style, 7 lines).
|
|
252
|
+
*/
|
|
253
|
+
export function buildWorkflowDashboard(
|
|
254
|
+
state: WorkflowStateResult,
|
|
255
|
+
workStatus: WorkStatus,
|
|
256
|
+
boxWidth = 76,
|
|
257
|
+
): string {
|
|
258
|
+
const steps = deriveWorkflowSteps(state, workStatus);
|
|
259
|
+
const currentStep = steps.find((s) => s.isCurrent) || steps[0];
|
|
260
|
+
|
|
261
|
+
const totalUnits = state.totalUnitsCount ?? workStatus.allUnits.length;
|
|
262
|
+
const completedUnits = state.completedUnitsCount ?? workStatus.completedUnits.length;
|
|
263
|
+
const percent = totalUnits > 0 ? Math.round((completedUnits / totalUnits) * 100) : 0;
|
|
264
|
+
|
|
265
|
+
const lines: string[] = [];
|
|
266
|
+
|
|
267
|
+
// 1. Header: 1 line
|
|
268
|
+
const progressSummary =
|
|
269
|
+
totalUnits > 0
|
|
270
|
+
? `[${renderProgressBar(completedUnits, totalUnits, 8)}] ${completedUnits}/${totalUnits} (${percent}%)`
|
|
271
|
+
: "";
|
|
272
|
+
const leftTitle = `Workflow Dashboard ─ [${currentStep.id} ${currentStep.statusLabel}]`;
|
|
273
|
+
lines.push(renderBoxHeader(leftTitle, progressSummary, boxWidth));
|
|
274
|
+
|
|
275
|
+
// 2. Exactly 5 steps: 5 lines (1 clean line per step)
|
|
276
|
+
for (const step of steps) {
|
|
277
|
+
const marker = step.isCurrent ? "▶" : step.status === "completed" ? "✓" : "·";
|
|
278
|
+
const rowContent = ` ${marker} [${step.number}] ${step.id.padEnd(14)} ${step.statusLabel.padEnd(8)} · ${step.detail}`;
|
|
279
|
+
lines.push(renderBoxLine(rowContent, boxWidth));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// 3. Footer: 1 line
|
|
283
|
+
lines.push(renderBoxFooter("Esc/Ctrl+C: 暂停 · 继续: 恢复 · /workflow", boxWidth));
|
|
284
|
+
|
|
285
|
+
return lines.join("\n");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Builds ultra-compact 1-line display for status bar or above-editor widget.
|
|
290
|
+
*/
|
|
291
|
+
export function buildWorkflowWidgetLines(
|
|
292
|
+
state: WorkflowStateResult,
|
|
293
|
+
workStatus: WorkStatus,
|
|
294
|
+
): string[] {
|
|
295
|
+
const steps = deriveWorkflowSteps(state, workStatus);
|
|
296
|
+
const currentStep = steps.find((s) => s.isCurrent) || steps[0];
|
|
297
|
+
|
|
298
|
+
const pipeline = steps
|
|
299
|
+
.map((s) => {
|
|
300
|
+
const sym = s.isCurrent ? "▶" : s.status === "completed" ? "✓" : "·";
|
|
301
|
+
return `${s.number}${sym}`;
|
|
302
|
+
})
|
|
303
|
+
.join(" ");
|
|
304
|
+
|
|
305
|
+
const totalUnits = state.totalUnitsCount ?? workStatus.allUnits.length;
|
|
306
|
+
const completedUnits = state.completedUnitsCount ?? workStatus.completedUnits.length;
|
|
307
|
+
const pct = totalUnits > 0 ? Math.round((completedUnits / totalUnits) * 100) : 0;
|
|
308
|
+
|
|
309
|
+
if (workStatus.isActive) {
|
|
310
|
+
const cur = workStatus.currentUnit ? ` 当前: ${workStatus.currentUnit}` : "";
|
|
311
|
+
return [
|
|
312
|
+
`⚡ [${pipeline}] 03-work [${renderProgressBar(completedUnits, totalUnits, 6)}] ${completedUnits}/${totalUnits} (${pct}%)${cur} | [Esc 暂停]`,
|
|
313
|
+
];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (workStatus.lastMessage && workStatus.lastMessage.includes("暂停")) {
|
|
317
|
+
return [
|
|
318
|
+
`⏸️ [${pipeline}] 03-work 已暂停 [${completedUnits}/${totalUnits} 单元] | 输入“继续”恢复 | /workflow 查看看板`,
|
|
319
|
+
];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return [
|
|
323
|
+
`🎯 [${pipeline}] 当前: [${currentStep.number}] ${currentStep.id} ${currentStep.statusLabel} | /workflow 查看看板`,
|
|
324
|
+
];
|
|
325
|
+
}
|
|
@@ -22,8 +22,10 @@ const tsFiles = [
|
|
|
22
22
|
"src/tools/artifact-helper",
|
|
23
23
|
"src/tools/session-checkpoint",
|
|
24
24
|
"src/tools/workflow-state",
|
|
25
|
+
"src/tools/workflow-dashboard",
|
|
25
26
|
"src/filters/bash-output-filter",
|
|
26
27
|
"src/filters/read-output-filter",
|
|
28
|
+
"src/driver/trigger-matcher",
|
|
27
29
|
"src/driver/work-loop-driver",
|
|
28
30
|
];
|
|
29
31
|
|
|
@@ -49,6 +51,13 @@ const { detectWorkflowState } = await import(
|
|
|
49
51
|
pathToFileURL(path.join(tempBuildDir, "src/tools/workflow-state.js"))
|
|
50
52
|
);
|
|
51
53
|
|
|
54
|
+
const {
|
|
55
|
+
deriveWorkflowSteps,
|
|
56
|
+
buildWorkflowDashboard,
|
|
57
|
+
buildWorkflowWidgetLines,
|
|
58
|
+
renderProgressBar,
|
|
59
|
+
} = await import(pathToFileURL(path.join(tempBuildDir, "src/tools/workflow-dashboard.js")));
|
|
60
|
+
|
|
52
61
|
const { filterBashOutput } = await import(
|
|
53
62
|
pathToFileURL(path.join(tempBuildDir, "src/filters/bash-output-filter.js"))
|
|
54
63
|
);
|
|
@@ -61,7 +70,7 @@ const { default: workflowExtension } = await import(
|
|
|
61
70
|
pathToFileURL(path.join(tempBuildDir, "index.js"))
|
|
62
71
|
);
|
|
63
72
|
|
|
64
|
-
const { parsePlanUnits, WorkLoopDriver } = await import(
|
|
73
|
+
const { parsePlanUnits, WorkLoopDriver, isExplicit03WorkTrigger } = await import(
|
|
65
74
|
pathToFileURL(path.join(tempBuildDir, "src/driver/work-loop-driver.js"))
|
|
66
75
|
);
|
|
67
76
|
|
|
@@ -253,7 +262,7 @@ test("read_output_filter: compresses lockfiles and large files", () => {
|
|
|
253
262
|
assert.ok(r1.output.includes("totalPackagesCount"));
|
|
254
263
|
});
|
|
255
264
|
|
|
256
|
-
test("workflowExtension: registers tools and command cleanly", () => {
|
|
265
|
+
test("workflowExtension: registers tools and command cleanly", async () => {
|
|
257
266
|
const registeredTools = new Map();
|
|
258
267
|
const registeredCommands = new Map();
|
|
259
268
|
const listeners = new Map();
|
|
@@ -280,6 +289,20 @@ test("workflowExtension: registers tools and command cleanly", () => {
|
|
|
280
289
|
assert.ok(listeners.has("before_agent_start"));
|
|
281
290
|
assert.ok(listeners.has("input"));
|
|
282
291
|
assert.ok(listeners.has("agent_settled"));
|
|
292
|
+
|
|
293
|
+
// Verify 00~05 shortcut transforms
|
|
294
|
+
const inputHandler = listeners.get("input");
|
|
295
|
+
const res01 = await inputHandler({ text: "01", source: "interactive" }, {});
|
|
296
|
+
assert.deepEqual(res01, { action: "transform", text: "/skill:01-brainstorm" });
|
|
297
|
+
|
|
298
|
+
const res01WithText = await inputHandler({ text: "01 补充需求边界", source: "interactive" }, {});
|
|
299
|
+
assert.deepEqual(res01WithText, { action: "transform", text: "/skill:01-brainstorm 补充需求边界" });
|
|
300
|
+
|
|
301
|
+
const res00 = await inputHandler({ text: "00", source: "interactive" }, {});
|
|
302
|
+
assert.deepEqual(res00, { action: "transform", text: "/skill:00-next" });
|
|
303
|
+
|
|
304
|
+
const resNormal = await inputHandler({ text: "1. 选第一个方案", source: "interactive" }, {});
|
|
305
|
+
assert.deepEqual(resNormal, { action: "continue" });
|
|
283
306
|
});
|
|
284
307
|
|
|
285
308
|
test("parsePlanUnits: correctly extracts various unit headers and check-boxes", () => {
|
|
@@ -452,3 +475,272 @@ test("WorkLoopDriver: records failure history and triggers Stop-The-Line valve o
|
|
|
452
475
|
|
|
453
476
|
driver.cancelTimer();
|
|
454
477
|
});
|
|
478
|
+
|
|
479
|
+
test("isExplicit03WorkTrigger: accurately identifies 03-work invocations vs harmless inputs", async () => {
|
|
480
|
+
// Should match explicit 03 triggers
|
|
481
|
+
assert.equal(isExplicit03WorkTrigger("/skill:03-work"), true);
|
|
482
|
+
assert.equal(isExplicit03WorkTrigger("/skill:03"), true);
|
|
483
|
+
assert.equal(isExplicit03WorkTrigger("03-work"), true);
|
|
484
|
+
assert.equal(isExplicit03WorkTrigger("03-work docs/plans/plan.md"), true);
|
|
485
|
+
assert.equal(isExplicit03WorkTrigger("03"), true);
|
|
486
|
+
assert.equal(isExplicit03WorkTrigger("/03"), true);
|
|
487
|
+
assert.equal(isExplicit03WorkTrigger("开始干活"), true);
|
|
488
|
+
assert.equal(isExplicit03WorkTrigger("开始03"), true);
|
|
489
|
+
assert.equal(isExplicit03WorkTrigger("开始实现"), true);
|
|
490
|
+
assert.equal(isExplicit03WorkTrigger("执行03"), true);
|
|
491
|
+
assert.equal(isExplicit03WorkTrigger("继续干活"), true);
|
|
492
|
+
assert.equal(isExplicit03WorkTrigger("恢复干活"), true);
|
|
493
|
+
assert.equal(isExplicit03WorkTrigger("resume work"), true);
|
|
494
|
+
assert.equal(isExplicit03WorkTrigger("【03-work 自主循环驱动引擎 · 自动化续跑指令】"), true);
|
|
495
|
+
|
|
496
|
+
// Must NEVER match other skills or normal dialogue (prevents false auto-starts!)
|
|
497
|
+
assert.equal(isExplicit03WorkTrigger("00"), false);
|
|
498
|
+
assert.equal(isExplicit03WorkTrigger("/skill:00-next"), false);
|
|
499
|
+
assert.equal(isExplicit03WorkTrigger("01"), false);
|
|
500
|
+
assert.equal(isExplicit03WorkTrigger("/skill:01-brainstorm"), false);
|
|
501
|
+
assert.equal(isExplicit03WorkTrigger("02"), false);
|
|
502
|
+
assert.equal(isExplicit03WorkTrigger("/skill:02-plan"), false);
|
|
503
|
+
assert.equal(isExplicit03WorkTrigger("04"), false);
|
|
504
|
+
assert.equal(isExplicit03WorkTrigger("/skill:04-review"), false);
|
|
505
|
+
assert.equal(isExplicit03WorkTrigger("05"), false);
|
|
506
|
+
assert.equal(isExplicit03WorkTrigger("/skill:05-learn"), false);
|
|
507
|
+
assert.equal(isExplicit03WorkTrigger("我想了解一下03步骤是什么"), false);
|
|
508
|
+
assert.equal(isExplicit03WorkTrigger("继续讨论一下需求细节"), false);
|
|
509
|
+
assert.equal(isExplicit03WorkTrigger("请帮我恢复之前被删除的代码"), false);
|
|
510
|
+
assert.equal(isExplicit03WorkTrigger(""), false);
|
|
511
|
+
assert.equal(isExplicit03WorkTrigger(" "), false);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
test("workflow-dashboard: derives all 5 steps and renders goal-style status dashboard", async () => {
|
|
515
|
+
const mockState = {
|
|
516
|
+
repoRoot: "/test/repo",
|
|
517
|
+
hasArtifacts: true,
|
|
518
|
+
stage: "planned",
|
|
519
|
+
recommendedSkill: "03-work",
|
|
520
|
+
recommendationReason: "计划已就绪",
|
|
521
|
+
brainstorms: [{ filename: "requirements.md", relativePath: "docs/brainstorms/requirements.md", mtimeMs: 100 }],
|
|
522
|
+
plans: [{ filename: "plan.md", relativePath: "docs/plans/plan.md", mtimeMs: 200 }],
|
|
523
|
+
solutions: [],
|
|
524
|
+
checkpoints: [],
|
|
525
|
+
latestBrainstorm: { filename: "requirements.md", relativePath: "docs/brainstorms/requirements.md", mtimeMs: 100 },
|
|
526
|
+
latestPlan: { filename: "plan.md", relativePath: "docs/plans/plan.md", mtimeMs: 200 },
|
|
527
|
+
totalUnitsCount: 4,
|
|
528
|
+
completedUnitsCount: 1,
|
|
529
|
+
remainingUnitsCount: 3,
|
|
530
|
+
nextUnitId: "Unit 1",
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
const mockWorkStatus = {
|
|
534
|
+
isActive: true,
|
|
535
|
+
planPath: "/test/repo/docs/plans/plan.md",
|
|
536
|
+
planSlug: "plan",
|
|
537
|
+
allUnits: ["Unit 0", "Unit 1", "Unit 2", "Unit 3"],
|
|
538
|
+
completedUnits: ["Unit 0"],
|
|
539
|
+
remainingUnits: ["Unit 1", "Unit 2", "Unit 3"],
|
|
540
|
+
currentUnit: "Unit 1",
|
|
541
|
+
failedUnit: null,
|
|
542
|
+
consecutiveFailures: 0,
|
|
543
|
+
currentRunCount: 1,
|
|
544
|
+
maxRuns: 50,
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
// 1. Check all 5 steps derivation
|
|
548
|
+
const steps = deriveWorkflowSteps(mockState, mockWorkStatus);
|
|
549
|
+
assert.equal(steps.length, 5);
|
|
550
|
+
assert.equal(steps[0].number, "01");
|
|
551
|
+
assert.equal(steps[0].id, "01-brainstorm");
|
|
552
|
+
assert.equal(steps[0].status, "completed");
|
|
553
|
+
|
|
554
|
+
assert.equal(steps[1].number, "02");
|
|
555
|
+
assert.equal(steps[1].id, "02-plan");
|
|
556
|
+
assert.equal(steps[1].status, "completed");
|
|
557
|
+
|
|
558
|
+
assert.equal(steps[2].number, "03");
|
|
559
|
+
assert.equal(steps[2].id, "03-work");
|
|
560
|
+
assert.equal(steps[2].status, "in_progress");
|
|
561
|
+
assert.equal(steps[2].isCurrent, true);
|
|
562
|
+
|
|
563
|
+
assert.equal(steps[3].number, "04");
|
|
564
|
+
assert.equal(steps[3].id, "04-review");
|
|
565
|
+
|
|
566
|
+
assert.equal(steps[4].number, "05");
|
|
567
|
+
assert.equal(steps[4].id, "05-learn");
|
|
568
|
+
|
|
569
|
+
// 2. Check full dashboard rendering
|
|
570
|
+
const dashboard = buildWorkflowDashboard(mockState, mockWorkStatus);
|
|
571
|
+
assert.ok(dashboard.includes("Workflow Dashboard"));
|
|
572
|
+
assert.ok(dashboard.includes("▶ [03] 03-work"));
|
|
573
|
+
assert.ok(dashboard.includes("[01] 01-brainstorm"));
|
|
574
|
+
assert.ok(dashboard.includes("[02] 02-plan"));
|
|
575
|
+
assert.ok(dashboard.includes("[04] 04-review"));
|
|
576
|
+
assert.ok(dashboard.includes("[05] 05-learn"));
|
|
577
|
+
assert.ok(dashboard.includes("Esc/Ctrl+C"));
|
|
578
|
+
assert.ok(dashboard.includes("单元进度:"));
|
|
579
|
+
|
|
580
|
+
// 3. Check progress bar helper
|
|
581
|
+
assert.equal(renderProgressBar(2, 4, 8), "████░░░░");
|
|
582
|
+
assert.equal(renderProgressBar(4, 4, 8), "████████");
|
|
583
|
+
assert.equal(renderProgressBar(0, 4, 8), "░░░░░░░░");
|
|
584
|
+
|
|
585
|
+
// 4. Check widget lines
|
|
586
|
+
const widgetLines = buildWorkflowWidgetLines(mockState, mockWorkStatus);
|
|
587
|
+
assert.ok(widgetLines[0].includes("03-work"));
|
|
588
|
+
assert.ok(widgetLines[0].includes("Unit 1"));
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
test("WorkLoopDriver: Esc/Abort interruption safely disarms autonomous driving", async () => {
|
|
592
|
+
const testRoot = path.join(tempBuildDir, "loop-interrupt-test-" + Date.now());
|
|
593
|
+
const plansDir = path.join(testRoot, "docs", "plans");
|
|
594
|
+
await mkdir(plansDir, { recursive: true });
|
|
595
|
+
|
|
596
|
+
const planFile = path.join(plansDir, "interrupt-plan.md");
|
|
597
|
+
await writeFile(planFile, "# Plan\n### Unit 0 — Alpha\n### Unit 1 — Beta", "utf-8");
|
|
598
|
+
|
|
599
|
+
const driver = new WorkLoopDriver(testRoot);
|
|
600
|
+
await driver.start(planFile);
|
|
601
|
+
assert.equal(driver.getStatus().isActive, true);
|
|
602
|
+
|
|
603
|
+
// 1. Simulate pause triggered by Esc / user action
|
|
604
|
+
await driver.pause("用户按 Esc 中断");
|
|
605
|
+
assert.equal(driver.getStatus().isActive, false);
|
|
606
|
+
assert.ok(driver.getStatus().lastMessage.includes("用户按 Esc 中断"));
|
|
607
|
+
|
|
608
|
+
// 2. Simulate agent_settled called while paused -> must NOT start next turn
|
|
609
|
+
let sendCalled = false;
|
|
610
|
+
const mockPi = {
|
|
611
|
+
sendUserMessage() {
|
|
612
|
+
sendCalled = true;
|
|
613
|
+
},
|
|
614
|
+
};
|
|
615
|
+
const mockCtx = {
|
|
616
|
+
signal: undefined,
|
|
617
|
+
ui: { notify() {}, setStatus() {} },
|
|
618
|
+
};
|
|
619
|
+
await driver.onAgentSettled(mockCtx, mockPi);
|
|
620
|
+
assert.equal(sendCalled, false);
|
|
621
|
+
assert.equal(driver.getStatus().isActive, false);
|
|
622
|
+
|
|
623
|
+
// 3. Test onAgentSettled with ctx.signal.aborted
|
|
624
|
+
await driver.start(planFile);
|
|
625
|
+
assert.equal(driver.getStatus().isActive, true);
|
|
626
|
+
const abortedCtx = {
|
|
627
|
+
signal: { aborted: true },
|
|
628
|
+
ui: { notify() {}, setStatus() {} },
|
|
629
|
+
};
|
|
630
|
+
await driver.onAgentSettled(abortedCtx, mockPi);
|
|
631
|
+
assert.equal(driver.getStatus().isActive, false);
|
|
632
|
+
assert.equal(sendCalled, false);
|
|
633
|
+
|
|
634
|
+
// 4. Test onAgentSettled with last assistant message aborted (Ctrl+C / Esc aftermath)
|
|
635
|
+
await driver.start(planFile);
|
|
636
|
+
assert.equal(driver.getStatus().isActive, true);
|
|
637
|
+
const sessionAbortedCtx = {
|
|
638
|
+
signal: undefined,
|
|
639
|
+
sessionManager: {
|
|
640
|
+
getBranch() {
|
|
641
|
+
return [
|
|
642
|
+
{ type: "message", message: { role: "user", content: "hello" } },
|
|
643
|
+
{ type: "message", message: { role: "assistant", stopReason: "aborted", content: "stopping..." } },
|
|
644
|
+
];
|
|
645
|
+
},
|
|
646
|
+
},
|
|
647
|
+
ui: { notify() {}, setStatus() {} },
|
|
648
|
+
};
|
|
649
|
+
await driver.onAgentSettled(sessionAbortedCtx, mockPi);
|
|
650
|
+
assert.equal(driver.getStatus().isActive, false);
|
|
651
|
+
assert.equal(sendCalled, false);
|
|
652
|
+
assert.ok(driver.getStatus().lastMessage.includes("中断"));
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
test("E2E Lifecycle: 01 brainstorm -> 02 plan -> 01 rollback -> 03 work -> compact dashboard & widget", async () => {
|
|
656
|
+
const testRoot = path.join(tempBuildDir, "e2e-workflow-" + Date.now());
|
|
657
|
+
const brainstormsDir = path.join(testRoot, "docs", "brainstorms");
|
|
658
|
+
const plansDir = path.join(testRoot, "docs", "plans");
|
|
659
|
+
await mkdir(brainstormsDir, { recursive: true });
|
|
660
|
+
await mkdir(plansDir, { recursive: true });
|
|
661
|
+
|
|
662
|
+
// 1. Initial State: No artifacts -> recommends 01-brainstorm
|
|
663
|
+
let state = await detectWorkflowState(testRoot);
|
|
664
|
+
assert.equal(state.recommendedSkill, "01-brainstorm");
|
|
665
|
+
|
|
666
|
+
// 2. User creates 01 brainstorm artifact
|
|
667
|
+
const bsFile = path.join(brainstormsDir, "2026-09-18-auth-requirements.md");
|
|
668
|
+
await writeFile(bsFile, "# Auth Requirements\nScope: login, register", "utf-8");
|
|
669
|
+
|
|
670
|
+
state = await detectWorkflowState(testRoot);
|
|
671
|
+
assert.equal(state.recommendedSkill, "02-plan");
|
|
672
|
+
assert.equal(state.brainstorms.length, 1);
|
|
673
|
+
|
|
674
|
+
// 3. User creates 02 plan artifact
|
|
675
|
+
const planFile = path.join(plansDir, "2026-09-18-auth-plan.md");
|
|
676
|
+
await writeFile(
|
|
677
|
+
planFile,
|
|
678
|
+
"# Auth Plan\n### Unit 0 — DB Schema\n### Unit 1 — API Route\n### Unit 2 — Tests",
|
|
679
|
+
"utf-8",
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
state = await detectWorkflowState(testRoot);
|
|
683
|
+
assert.equal(state.recommendedSkill, "03-work");
|
|
684
|
+
assert.equal(state.totalUnitsCount, 3);
|
|
685
|
+
|
|
686
|
+
// 4. User is in 02, realizes requirements need changes -> Rolls back to 01!
|
|
687
|
+
// (Simulate input event transform)
|
|
688
|
+
const listeners = new Map();
|
|
689
|
+
const mockPi = {
|
|
690
|
+
registerTool() {},
|
|
691
|
+
registerCommand() {},
|
|
692
|
+
on(event, handler) {
|
|
693
|
+
listeners.set(event, handler);
|
|
694
|
+
},
|
|
695
|
+
};
|
|
696
|
+
workflowExtension(mockPi);
|
|
697
|
+
const inputHandler = listeners.get("input");
|
|
698
|
+
|
|
699
|
+
// User types "01 增加第三方登录"
|
|
700
|
+
const transformRes = await inputHandler({ text: "01 增加第三方登录", source: "interactive" }, {});
|
|
701
|
+
assert.deepEqual(transformRes, {
|
|
702
|
+
action: "transform",
|
|
703
|
+
text: "/skill:01-brainstorm 增加第三方登录",
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
// User updates brainstorm file
|
|
707
|
+
await writeFile(bsFile, "# Auth Requirements\nScope: login, register, oauth2", "utf-8");
|
|
708
|
+
|
|
709
|
+
// 5. User returns to 02
|
|
710
|
+
const returnTo02 = await inputHandler({ text: "02", source: "interactive" }, {});
|
|
711
|
+
assert.deepEqual(returnTo02, {
|
|
712
|
+
action: "transform",
|
|
713
|
+
text: "/skill:02-plan",
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
// 6. User enters 03 -> starts autonomous work
|
|
717
|
+
const driver = new WorkLoopDriver(testRoot);
|
|
718
|
+
const startRes = await driver.start(planFile);
|
|
719
|
+
assert.equal(startRes.success, true);
|
|
720
|
+
assert.equal(startRes.status.isActive, true);
|
|
721
|
+
|
|
722
|
+
// 7. Verify Dashboard height is strictly 7 lines
|
|
723
|
+
const dashboard = buildWorkflowDashboard(state, driver.getStatus());
|
|
724
|
+
const dashboardLines = dashboard.trim().split("\n");
|
|
725
|
+
assert.equal(dashboardLines.length, 7, "Dashboard must strictly occupy exactly 7 lines");
|
|
726
|
+
assert.ok(dashboard.includes("▶ [03] 03-work"));
|
|
727
|
+
assert.ok(dashboard.includes("✓ [01] 01-brainstorm"));
|
|
728
|
+
assert.ok(dashboard.includes("✓ [02] 02-plan"));
|
|
729
|
+
|
|
730
|
+
// 8. Verify Widget height is strictly 1 line
|
|
731
|
+
const widgetLines = buildWorkflowWidgetLines(state, driver.getStatus());
|
|
732
|
+
assert.equal(widgetLines.length, 1, "Widget must strictly occupy exactly 1 line");
|
|
733
|
+
assert.ok(widgetLines[0].includes("03-work"));
|
|
734
|
+
assert.ok(widgetLines[0].includes("01✓ 02✓ 03▶"));
|
|
735
|
+
|
|
736
|
+
// 9. Manual interruption (Esc) stops the loop
|
|
737
|
+
await driver.pause("User hit Esc");
|
|
738
|
+
assert.equal(driver.getStatus().isActive, false);
|
|
739
|
+
|
|
740
|
+
// Post-pause widget is also strictly 1 line
|
|
741
|
+
const pausedWidgetLines = buildWorkflowWidgetLines(state, driver.getStatus());
|
|
742
|
+
assert.equal(pausedWidgetLines.length, 1);
|
|
743
|
+
assert.ok(pausedWidgetLines[0].includes("已暂停"));
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
|