@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.
@@ -1,52 +1,103 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { Type } from "typebox";
3
- import { resolveArtifactPath, type ArtifactType } from "./src/tools/artifact-helper.js";
4
- import { detectWorkflowState } from "./src/tools/workflow-state.js";
5
- import { executeSessionCheckpoint } from "./src/tools/session-checkpoint.js";
6
- import { filterBashOutput } from "./src/filters/bash-output-filter.js";
7
- import { filterReadOutput } from "./src/filters/read-output-filter.js";
8
- import { WorkLoopDriver } from "./src/driver/work-loop-driver.js";
9
-
10
- const workflowStateParams = Type.Object({
11
- repoRoot: Type.String({ description: "Repository root path to scan for workflow artifacts" }),
12
- });
13
-
14
- const artifactHelperParams = Type.Object({
15
- repoRoot: Type.String({ description: "Repository root where workflow artifacts are stored" }),
16
- artifactType: Type.Union(
17
- [
18
- Type.Literal("brainstorm"),
19
- Type.Literal("plan"),
20
- Type.Literal("solution"),
21
- Type.Literal("checkpoint"),
22
- ],
23
- { description: "Target artifact category" },
24
- ),
25
- topic: Type.Optional(Type.String({ description: "Topic or feature name for the artifact" })),
26
- date: Type.Optional(Type.String({ description: "Date prefix formatted as YYYY-MM-DD" })),
27
- ensureDir: Type.Optional(Type.Boolean({ description: "Whether to create directory if missing" })),
28
- });
29
-
30
- const sessionCheckpointParams = Type.Object({
31
- operation: Type.Union(
32
- [
33
- Type.Literal("save"),
34
- Type.Literal("load"),
35
- Type.Literal("list"),
36
- Type.Literal("fail"),
37
- Type.Literal("retry"),
38
- ],
39
- { description: "Checkpoint action to execute" },
40
- ),
41
- repoRoot: Type.String({ description: "Repository root path" }),
42
- planPath: Type.Optional(Type.String({ description: "Path to the plan markdown artifact" })),
43
- planSlug: Type.Optional(Type.String({ description: "Slug identifier for the execution plan" })),
44
- completedUnits: Type.Optional(
45
- Type.Array(Type.String(), { description: "List of completed unit names" }),
46
- ),
47
- failedUnit: Type.Optional(Type.String({ description: "Unit name that encountered a failure" })),
48
- error: Type.Optional(Type.String({ description: "Error description or failure details" })),
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 (查看工作流总览与 03-work 自主循环状态)
244
+ // 6. User Command: /workflow (查看工作流全阶段看板与自主循环控制)
189
245
  pi.registerCommand("workflow", {
190
- description: "查看当前项目的复合工程流状态与下一步推荐技能",
191
- async handler(_args, ctx) {
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 msg = [
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?.(msg, "info");
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
- // 7. 03-work 原生自主循环驱动引擎 (无须额外命令,直接在 03-work 中原生生效)
221
- const workDriver = new WorkLoopDriver();
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
- // 监听用户输入与技能启动:当调用 03-work 或请求恢复执行时,自动启动自主循环驱动
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 promptLower = event.prompt.toLowerCase();
226
- const is03Work =
227
- event.prompt.includes("03-work") ||
228
- event.systemPrompt.includes("03-work") ||
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
- // 监听用户打断指令:用户若输入“暂停”或“停止”,自动安全挂起 03 自主循环
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
- const text = event.text.trim().toLowerCase();
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
- workDriver.getStatus().isActive &&
254
- (text === "暂停" || text === "停止" || text === "pause" || text === "stop")
414
+ workDriverInstance &&
415
+ workDriverInstance.getStatus().isActive &&
416
+ (lower === "暂停" || lower === "停止" || lower === "pause" || lower === "stop")
255
417
  ) {
256
- await workDriver.pause("用户手动输入暂停");
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
- await workDriver.onAgentSettled(ctx, pi);
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
- workDriver.cancelTimer();
488
+ detachEscapeListener();
489
+ workDriverInstance?.cancelTimer();
274
490
  });
275
491
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unifan/pi-workflow-zh",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Pi 复合工程工作流引擎(状态感知 / 断点续跑 / 输出压缩 / 自动化工具集)",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -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 abort signal)
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
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unifan/pi-unifan-zh",
3
- "version": "1.0.35",
3
+ "version": "1.0.37",
4
4
  "description": "Pi Coding Agent 中文扩展全家桶与独立插件集",
5
5
  "type": "module",
6
6
  "main": "extensions/sessions/index.ts",