@zhushanwen/pi-subagent-workflow 7.4.0 → 8.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/package.json +7 -4
- package/scripts/rfl.mjs +308 -0
- package/skills/workflow-script-format/SKILL.md +1 -1
- package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +10 -4
- package/src/execution/__tests__/before-agent-start-injection.test.ts +132 -0
- package/src/execution/__tests__/bg-notify-render.test.ts +15 -15
- package/src/execution/__tests__/chatmode-first-round-closure-service.test.ts +365 -0
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
- package/src/execution/__tests__/chatmode-round-notify-real-chain.test.ts +215 -0
- package/src/execution/__tests__/conversation-wiring.test.ts +198 -0
- package/src/execution/__tests__/crash-recovery.test.ts +8 -2
- package/src/execution/__tests__/delivery-methods.test.ts +385 -0
- package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +49 -2
- package/src/execution/__tests__/execute-nesting.test.ts +20 -72
- package/src/execution/__tests__/execution-record.test.ts +199 -0
- package/src/execution/__tests__/finalize-record.test.ts +197 -15
- package/src/execution/__tests__/format.test.ts +131 -7
- package/src/execution/__tests__/gc-timer.test.ts +184 -0
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
- package/src/execution/__tests__/helpers/spawn-mock.ts +37 -10
- package/src/execution/__tests__/index-session-start-identity.test.ts +371 -0
- package/src/execution/__tests__/index-session-start.test.ts +257 -5
- package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +337 -0
- package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
- package/src/execution/__tests__/list-component.test.ts +59 -5
- package/src/execution/__tests__/list-fields.test.ts +109 -0
- package/src/execution/__tests__/model-resolver.test.ts +38 -1
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
- package/src/execution/__tests__/nested-visibility.test.ts +325 -0
- package/src/execution/__tests__/notifier-flush.test.ts +209 -7
- package/src/execution/__tests__/one-shot-upgrade.test.ts +205 -0
- package/src/execution/__tests__/parent-child-matrix.test.ts +336 -0
- package/src/execution/__tests__/record-store.test.ts +442 -54
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +11 -12
- package/src/execution/__tests__/recursive-visibility-env.test.ts +18 -20
- package/src/execution/__tests__/resource-policy.test.ts +109 -0
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +18 -25
- package/src/execution/__tests__/run-spawn-integration.test.ts +29 -25
- package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
- package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +14 -11
- package/src/execution/__tests__/session-pending.test.ts +61 -2
- package/src/execution/__tests__/session-reconstructor.test.ts +4 -4
- package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
- package/src/execution/__tests__/session-runner-schema-env.test.ts +15 -21
- package/src/execution/__tests__/session-start-reaper.test.ts +10 -8
- package/src/execution/__tests__/spawn-args.test.ts +127 -49
- package/src/execution/__tests__/spawn-worktree-guidance.test.ts +1 -0
- package/src/execution/__tests__/spawned-children.test.ts +92 -0
- package/src/execution/__tests__/status-refactor.test.ts +345 -0
- package/src/execution/__tests__/stdin-writer.test.ts +97 -0
- package/src/execution/__tests__/subagent-service-message-close.test.ts +629 -0
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
- package/src/execution/__tests__/subagent-service.test.ts +49 -11
- package/src/execution/__tests__/timeout-integration.test.ts +27 -13
- package/src/execution/__tests__/tool-action.test.ts +12 -10
- package/src/execution/__tests__/truncline-snapshot.test.ts +81 -0
- package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
- package/src/execution/__tests__/worktree-manager.test.ts +300 -90
- package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +17 -13
- package/src/execution/__tests__/worktree-reconcile.integration.test.ts +181 -0
- package/src/execution/__tests__/worktree-registry.test.ts +72 -34
- package/src/execution/agent-result-mapper.ts +4 -1
- package/src/execution/argv-mirror.ts +21 -2
- package/src/execution/channel-registry-access.ts +3 -1
- package/src/execution/execution-record.ts +126 -9
- package/src/execution/finalize-record.ts +99 -13
- package/src/execution/idle-gc.ts +47 -0
- package/src/execution/lifecycle-manager.ts +491 -0
- package/src/execution/lifecycle-predicates.ts +65 -0
- package/src/execution/manifest-store.ts +61 -16
- package/src/execution/model-resolver.ts +26 -5
- package/src/execution/notifier.ts +69 -12
- package/src/execution/pi-invocation.ts +21 -1
- package/src/execution/record-entry.ts +118 -0
- package/src/execution/record-store.ts +844 -108
- package/src/execution/session-pending.ts +121 -49
- package/src/execution/session-reconstructor.ts +224 -7
- package/src/execution/session-runner.ts +713 -316
- package/src/execution/sessions-index.ts +304 -0
- package/src/execution/stdin-writer.ts +93 -7
- package/src/execution/stream-sink.ts +20 -3
- package/src/execution/subagent-service.ts +917 -138
- package/src/execution/temp-prompt.ts +8 -3
- package/src/execution/turn-limiter.ts +14 -0
- package/src/execution/types.ts +218 -19
- package/src/execution/worktree-manager.ts +449 -59
- package/src/execution/worktree-registry.ts +97 -29
- package/src/index.ts +318 -20
- package/src/injectors/subagent-list-injector.ts +26 -8
- package/src/injectors/workflow-list-injector.ts +25 -8
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +17 -4
- package/src/interface/__tests__/tool-render.test.ts +10 -8
- package/src/interface/__tests__/tool-workflow-script-generate.test.ts +103 -26
- package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +179 -0
- package/src/interface/bg-notify-render.ts +32 -8
- package/src/interface/command-actions.ts +26 -7
- package/src/interface/commands.ts +21 -22
- package/src/interface/format.ts +53 -20
- package/src/interface/gui-mappers.ts +6 -8
- package/src/interface/helpers.ts +170 -10
- package/src/interface/list-component.ts +53 -14
- package/src/interface/subagent-actions.ts +235 -17
- package/src/interface/subagent-tool.ts +81 -16
- package/src/interface/subagents.ts +2 -1
- package/src/interface/tool-render.ts +21 -27
- package/src/interface/tool-workflow-script.ts +29 -33
- package/src/interface/tool-workflow.ts +67 -100
- package/src/interface/views/WorkflowsView.ts +89 -32
- package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +264 -0
- package/src/interface/views/detail-content.ts +1 -1
- package/src/interface/views/format.ts +3 -3
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +333 -0
- package/src/orchestration/__tests__/args-validator.test.ts +1 -1
- package/src/orchestration/__tests__/config-loader.test.ts +38 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +394 -4
- package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +4 -4
- package/src/orchestration/__tests__/execute-agent-call.test.ts +144 -1
- package/src/orchestration/__tests__/jsonl-run-store-loadall-sources.test.ts +171 -0
- package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +820 -19
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -2
- package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +96 -0
- package/src/orchestration/__tests__/lifecycle.test.ts +332 -149
- package/src/orchestration/__tests__/review-fix-loop-e2e.test.ts +1108 -19
- package/src/orchestration/__tests__/skill-discovery.test.ts +130 -61
- package/src/orchestration/__tests__/test-mocks.ts +197 -0
- package/src/orchestration/__tests__/worker-returnmeta-passthrough.test.ts +164 -0
- package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +110 -0
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +0 -2
- package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
- package/src/orchestration/__tests__/workflows-e2e.test.ts +38 -40
- package/src/orchestration/agent-opts-resolver.ts +4 -1
- package/src/orchestration/args-validator.ts +2 -2
- package/src/orchestration/config-loader.ts +30 -1
- package/src/orchestration/error-recovery.ts +137 -30
- package/src/orchestration/execute-agent-call.ts +44 -10
- package/src/orchestration/jsonl-run-store.ts +397 -71
- package/src/orchestration/launcher.ts +7 -1
- package/src/orchestration/lifecycle.ts +145 -133
- package/src/orchestration/models/__tests__/trace.test.ts +408 -0
- package/src/orchestration/models/budget.ts +1 -1
- package/src/orchestration/models/run-runtime.ts +15 -17
- package/src/orchestration/models/run-spec.ts +2 -2
- package/src/orchestration/models/run-state.ts +3 -3
- package/src/orchestration/models/trace.ts +95 -15
- package/src/orchestration/models/types.ts +8 -9
- package/src/orchestration/models/workflow-run.ts +50 -71
- package/src/orchestration/models/workflow-script.ts +32 -1
- package/src/orchestration/skill-discovery.ts +30 -0
- package/src/orchestration/worker-handle.ts +1 -1
- package/src/orchestration/worker-host.ts +1 -1
- package/src/orchestration/worker-script-builder.ts +38 -11
- package/src/shared/__tests__/agent-ref.test.ts +34 -0
- package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +280 -0
- package/src/shared/__tests__/resource-discovery.test.ts +79 -0
- package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
- package/src/shared/agent-ref.ts +22 -1
- package/src/shared/resource-discovery.ts +162 -59
- package/src/shared/schema-jsonify.ts +56 -0
- package/workflows/review-fix-loop-utils.cjs +542 -32
- package/workflows/review-fix-loop.js +462 -109
|
@@ -51,6 +51,19 @@ const logger = getLogger("injector");
|
|
|
51
51
|
*/
|
|
52
52
|
let agentCache: AgentEntry[] | null = null;
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* 注入块渲染缓存(与 agentCache 同步更新):before_agent_start 每个 turn 都要注入,
|
|
56
|
+
* formatAgentList(escapeXml 多趟正则 × 全部字段)在数据不变时输出完全相同——渲染
|
|
57
|
+
* 一次随缓存复用,turn 热路径零重复计算。
|
|
58
|
+
*/
|
|
59
|
+
let agentInjectionCache: string | null = null;
|
|
60
|
+
|
|
61
|
+
/** agentCache 唯一写点:数据与渲染缓存同步更新(null 清空两者)。 */
|
|
62
|
+
function setAgentCache(entries: AgentEntry[] | null): void {
|
|
63
|
+
agentCache = entries;
|
|
64
|
+
agentInjectionCache = entries !== null ? formatAgentList(entries) : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
54
67
|
/** 从 .md frontmatter 提取的最小 agent 信息(m5:+ when/examples 路由样本;S1:+ path) */
|
|
55
68
|
export interface AgentEntry {
|
|
56
69
|
name: string;
|
|
@@ -189,9 +202,11 @@ export function setupSubagentListInjector(pi: ExtensionAPI): void {
|
|
|
189
202
|
"session_start",
|
|
190
203
|
async (_event: SessionStartEvent, ctx: ExtensionContext): Promise<void> => {
|
|
191
204
|
try {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
205
|
+
setAgentCache(
|
|
206
|
+
await discoverAllAgents(
|
|
207
|
+
findWorkspaceRoot(ctx.cwd),
|
|
208
|
+
getAgentDir(),
|
|
209
|
+
),
|
|
195
210
|
);
|
|
196
211
|
} catch (err) {
|
|
197
212
|
// fail-safe:发现异常不阻断 session,缓存保持 null(before_agent_start 会 fallback)
|
|
@@ -211,12 +226,15 @@ export function setupSubagentListInjector(pi: ExtensionAPI): void {
|
|
|
211
226
|
try {
|
|
212
227
|
// 读缓存;miss(session_start 未触发/缓存被清)则 fallback 重新发现+赋值
|
|
213
228
|
if (agentCache === null) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
229
|
+
setAgentCache(
|
|
230
|
+
await discoverAllAgents(
|
|
231
|
+
findWorkspaceRoot(ctx.cwd),
|
|
232
|
+
getAgentDir(),
|
|
233
|
+
),
|
|
217
234
|
);
|
|
218
235
|
}
|
|
219
|
-
|
|
236
|
+
// agentInjectionCache 与 agentCache 不变量同步(setAgentCache 保证),直接复用
|
|
237
|
+
const injection = agentInjectionCache;
|
|
220
238
|
if (!injection) return;
|
|
221
239
|
return { systemPrompt: event.systemPrompt + injection };
|
|
222
240
|
} catch (err) {
|
|
@@ -230,7 +248,7 @@ export function setupSubagentListInjector(pi: ExtensionAPI): void {
|
|
|
230
248
|
pi.on(
|
|
231
249
|
"session_shutdown",
|
|
232
250
|
(_event: SessionShutdownEvent, _ctx: ExtensionContext): void => {
|
|
233
|
-
|
|
251
|
+
setAgentCache(null);
|
|
234
252
|
},
|
|
235
253
|
);
|
|
236
254
|
}
|
|
@@ -43,6 +43,18 @@ const logger = getLogger("injector");
|
|
|
43
43
|
*/
|
|
44
44
|
let workflowCache: WorkflowEntry[] | null = null;
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* 注入块渲染缓存(与 workflowCache 同步更新):before_agent_start 每个 turn 都要注入,
|
|
48
|
+
* formatWorkflowList 在数据不变时输出完全相同——渲染一次随缓存复用。
|
|
49
|
+
*/
|
|
50
|
+
let workflowInjectionCache: string | null = null;
|
|
51
|
+
|
|
52
|
+
/** workflowCache 唯一写点:数据与渲染缓存同步更新(null 清空两者)。 */
|
|
53
|
+
function setWorkflowCache(entries: WorkflowEntry[] | null): void {
|
|
54
|
+
workflowCache = entries;
|
|
55
|
+
workflowInjectionCache = entries !== null ? formatWorkflowList(entries) : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
46
58
|
/** 注入段中单个 workflow 的最大描述长度(控制每 turn prompt 体积) */
|
|
47
59
|
const MAX_DESC_LEN = 160;
|
|
48
60
|
|
|
@@ -172,9 +184,11 @@ export function setupWorkflowListInjector(pi: ExtensionAPI): void {
|
|
|
172
184
|
"session_start",
|
|
173
185
|
async (_event: SessionStartEvent, ctx: ExtensionContext): Promise<void> => {
|
|
174
186
|
try {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
187
|
+
setWorkflowCache(
|
|
188
|
+
await discoverAllWorkflows(
|
|
189
|
+
findWorkspaceRoot(ctx.cwd),
|
|
190
|
+
getAgentDir(),
|
|
191
|
+
),
|
|
178
192
|
);
|
|
179
193
|
} catch (err) {
|
|
180
194
|
// fail-safe:发现异常不阻断 session,缓存保持 null(before_agent_start 会 fallback)
|
|
@@ -194,12 +208,15 @@ export function setupWorkflowListInjector(pi: ExtensionAPI): void {
|
|
|
194
208
|
try {
|
|
195
209
|
// 读缓存;miss(session_start 未触发/缓存被清)则 fallback 重新发现+赋值
|
|
196
210
|
if (workflowCache === null) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
211
|
+
setWorkflowCache(
|
|
212
|
+
await discoverAllWorkflows(
|
|
213
|
+
findWorkspaceRoot(ctx.cwd),
|
|
214
|
+
getAgentDir(),
|
|
215
|
+
),
|
|
200
216
|
);
|
|
201
217
|
}
|
|
202
|
-
|
|
218
|
+
// workflowInjectionCache 与 workflowCache 不变量同步(setWorkflowCache 保证),直接复用
|
|
219
|
+
const injection = workflowInjectionCache;
|
|
203
220
|
if (!injection) return;
|
|
204
221
|
return { systemPrompt: event.systemPrompt + injection };
|
|
205
222
|
} catch (err) {
|
|
@@ -213,7 +230,7 @@ export function setupWorkflowListInjector(pi: ExtensionAPI): void {
|
|
|
213
230
|
pi.on(
|
|
214
231
|
"session_shutdown",
|
|
215
232
|
(_event: SessionShutdownEvent, _ctx: ExtensionContext): void => {
|
|
216
|
-
|
|
233
|
+
setWorkflowCache(null);
|
|
217
234
|
},
|
|
218
235
|
);
|
|
219
236
|
}
|
|
@@ -31,12 +31,16 @@ function extractDescription(src: string): string {
|
|
|
31
31
|
const DESCRIPTION = extractDescription(SUBAGENT_TOOL_SRC);
|
|
32
32
|
|
|
33
33
|
describe("subagent tool description — 行为约束器(非功能说明书)", () => {
|
|
34
|
-
it("词数 ≤
|
|
34
|
+
it("词数 ≤ 800(高风险 description 密度上限)", () => {
|
|
35
35
|
// 高风险 tool 的 description 应聚焦约束而非功能铺陈;过长会稀释信号。
|
|
36
|
-
//
|
|
37
|
-
//
|
|
36
|
+
// 上限演进:400 → 550(补 start/list/cancel 三 action 完整 JSON 正例)→
|
|
37
|
+
// 650(M2-B3 新增 message/close 两 action + conversation 对话模式 + 例子)→
|
|
38
|
+
// 800(递归指导:树形判据 + 自包含 task + 独立验收 + fork 成本警告)。
|
|
39
|
+
// 800 是断言的真实预算(标题与本值一致);留 ≥60 词余量供后续措辞微调,
|
|
40
|
+
// 逼近上限时优先精简正文而非再放宽断言。
|
|
41
|
+
// 每次 action 扩展必然增加必需描述;正例对弱模型首次用对参数的价值 > 节省 description 预算。
|
|
38
42
|
const words = DESCRIPTION.trim().split(/\s+/).filter(Boolean).length;
|
|
39
|
-
expect(words).toBeLessThanOrEqual(
|
|
43
|
+
expect(words).toBeLessThanOrEqual(800);
|
|
40
44
|
});
|
|
41
45
|
|
|
42
46
|
it("含 'When to delegate' 调用条件段(何时委派 vs 自己做)", () => {
|
|
@@ -78,6 +82,15 @@ describe("subagent tool description — 行为约束器(非功能说明书)"
|
|
|
78
82
|
expect(DESCRIPTION).toMatch(/Depth: N\/10/);
|
|
79
83
|
});
|
|
80
84
|
|
|
85
|
+
it("递归指导含树形判据 + 自包含 task + 独立验收 + 深度定位", () => {
|
|
86
|
+
// 递归不是「能套就套」,必须有明确使用判据,否则 LLM 会滥用深递归。
|
|
87
|
+
const lower = DESCRIPTION.toLowerCase();
|
|
88
|
+
expect(lower).toContain("tree-shaped"); // 树形判据
|
|
89
|
+
expect(lower).toContain("self-contained"); // task 自包含
|
|
90
|
+
expect(lower).toContain("acceptance criteria"); // 独立验收
|
|
91
|
+
expect(lower).toContain("safety rail"); // 深度定位:10 是护栏非预算
|
|
92
|
+
});
|
|
93
|
+
|
|
81
94
|
it("保留 executionMode sequential 的 CRITICAL 说明", () => {
|
|
82
95
|
// sequential 是关键执行语义,删了会导致 LLM 误以为并行可用。
|
|
83
96
|
expect(DESCRIPTION).toMatch(/CRITICAL/i);
|
|
@@ -80,39 +80,41 @@ describe("renderSubagentCall — 拍平形态提取(regression for wave 3 flat
|
|
|
80
80
|
expect(out).toContain("fix-login");
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
-
it("
|
|
83
|
+
it("task 不再渲染为 preview 行(标题块只有单行)", () => {
|
|
84
84
|
const out = renderText(renderSubagentCall(
|
|
85
85
|
{ action: "start", agent: "coder", task: "Analyze the bug in parser", slug: "fix-parser" },
|
|
86
86
|
makeTheme() as never,
|
|
87
87
|
CTX,
|
|
88
88
|
));
|
|
89
|
-
// task
|
|
90
|
-
expect(out).toContain("Analyze the bug in parser");
|
|
89
|
+
// task 预览行已移除——task 内容不进对话流(完整 task 在 /subagents 详情可见)
|
|
90
|
+
expect(out).not.toContain("Analyze the bug in parser");
|
|
91
|
+
expect(out).not.toContain("\n");
|
|
91
92
|
});
|
|
92
93
|
|
|
93
|
-
it("task
|
|
94
|
+
it("task 含换行时不进渲染(无 preview 泄漏)", () => {
|
|
94
95
|
const out = renderText(renderSubagentCall(
|
|
95
96
|
{ action: "start", task: "first line\nsecond line", slug: "x" },
|
|
96
97
|
makeTheme() as never,
|
|
97
98
|
CTX,
|
|
98
99
|
));
|
|
99
|
-
expect(out).toContain("first line");
|
|
100
|
+
expect(out).not.toContain("first line");
|
|
100
101
|
expect(out).not.toContain("second line");
|
|
102
|
+
expect(out).not.toContain("\n");
|
|
101
103
|
});
|
|
102
104
|
|
|
103
105
|
// 关键回归:若有人把提取路径改回 args.startParam,这些顶层调用都会失败
|
|
104
|
-
// (agent/slug
|
|
106
|
+
// (agent/slug 取不到,全用默认值)。此测试用顶层数据形态锁住 flatten。
|
|
107
|
+
// task 不在断言里——task 已不渲染(上方用例锁定)。
|
|
105
108
|
it("REGRESSION: 顶层 args 形态完整提取(防止回退到 startParam envelope)", () => {
|
|
106
109
|
const out = renderText(renderSubagentCall(
|
|
107
110
|
{ action: "start", agent: "researcher", task: "search docs", slug: "search-docs" },
|
|
108
111
|
makeTheme() as never,
|
|
109
112
|
CTX,
|
|
110
113
|
));
|
|
111
|
-
//
|
|
114
|
+
// agent + slug 都应被提取(默认值 fallback 也能过单字段断言,但同时命中的
|
|
112
115
|
// 概率只有联合 fallback 才有——researcher/search-docs 都不是默认值)
|
|
113
116
|
expect(out).toContain("researcher");
|
|
114
117
|
expect(out).toContain("search-docs");
|
|
115
|
-
expect(out).toContain("search docs");
|
|
116
118
|
});
|
|
117
119
|
|
|
118
120
|
it("args 缺所有字段时不崩(最防御)", () => {
|
|
@@ -10,13 +10,19 @@ import { mkdirSync, writeFileSync } from "node:fs";
|
|
|
10
10
|
|
|
11
11
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
12
12
|
|
|
13
|
-
import { actionGenerate, type ScriptParams, type TextContent } from "../tool-workflow-script.ts";
|
|
13
|
+
import { actionGenerate, type ScriptParams, type TextContent, registerWorkflowScriptTool } from "../tool-workflow-script.ts";
|
|
14
|
+
import { deleteWorkflow, saveWorkflow } from "../../orchestration/workflow-files.ts";
|
|
14
15
|
|
|
15
16
|
vi.mock("node:fs", () => ({
|
|
16
17
|
mkdirSync: vi.fn(),
|
|
17
18
|
writeFileSync: vi.fn(),
|
|
18
19
|
}));
|
|
19
20
|
|
|
21
|
+
vi.mock("../../orchestration/workflow-files.ts", () => ({
|
|
22
|
+
saveWorkflow: vi.fn(),
|
|
23
|
+
deleteWorkflow: vi.fn(),
|
|
24
|
+
}));
|
|
25
|
+
|
|
20
26
|
const mockedWriteFileSync = vi.mocked(writeFileSync);
|
|
21
27
|
const mockedMkdirSync = vi.mocked(mkdirSync);
|
|
22
28
|
|
|
@@ -106,6 +112,12 @@ describe("actionGenerate (m0: @pi-meta 认可 + round-trip)", () => {
|
|
|
106
112
|
vi.clearAllMocks();
|
|
107
113
|
});
|
|
108
114
|
|
|
115
|
+
/**
|
|
116
|
+
* W4b:generate 校验族错误路径从 return {isError:true} 改为 throw(pi 只对
|
|
117
|
+
* execute throw 置 isError:true,返回值 isError 被 agent-loop 丢弃)。
|
|
118
|
+
* actionGenerate 是同步函数——断言用同步 toThrow。
|
|
119
|
+
*/
|
|
120
|
+
|
|
109
121
|
it("TC1: 合法 @pi-meta → ready + writeFileSync 被调用", () => {
|
|
110
122
|
const r = actionGenerate(gen(PI_META_VALID), undefined);
|
|
111
123
|
expect(r.isError).toBeFalsy();
|
|
@@ -113,10 +125,10 @@ describe("actionGenerate (m0: @pi-meta 认可 + round-trip)", () => {
|
|
|
113
125
|
expect(mockedWriteFileSync).toHaveBeenCalledTimes(1);
|
|
114
126
|
});
|
|
115
127
|
|
|
116
|
-
it("TC2: malformed @pi-meta YAML →
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
128
|
+
it("TC2: malformed @pi-meta YAML → throw + writeFileSync 未调用 [P-generate-roundtrip]", () => {
|
|
129
|
+
expect(() => actionGenerate(gen(PI_META_MALFORMED), undefined)).toThrow(
|
|
130
|
+
/cannot be parsed/i,
|
|
131
|
+
);
|
|
120
132
|
expect(mockedWriteFileSync).not.toHaveBeenCalled();
|
|
121
133
|
});
|
|
122
134
|
|
|
@@ -126,38 +138,103 @@ describe("actionGenerate (m0: @pi-meta 认可 + round-trip)", () => {
|
|
|
126
138
|
expect(mockedWriteFileSync).toHaveBeenCalledTimes(1);
|
|
127
139
|
});
|
|
128
140
|
|
|
129
|
-
it("TC4: 无 meta →
|
|
130
|
-
|
|
131
|
-
expect(r.isError).toBe(true);
|
|
132
|
-
expect(textOf(r)).toMatch(/meta declaration/i);
|
|
133
|
-
expect(textOf(r)).toMatch(/pi-meta/i);
|
|
141
|
+
it("TC4: 无 meta → throw 提及 @pi-meta 新格式", () => {
|
|
142
|
+
expect(() => actionGenerate(gen(NO_META), undefined)).toThrow(/meta declaration/i);
|
|
134
143
|
expect(mockedWriteFileSync).not.toHaveBeenCalled();
|
|
135
144
|
});
|
|
136
145
|
|
|
137
|
-
it("TC5: @pi-meta 单反斜杠正则 →
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
146
|
+
it("TC5: @pi-meta 单反斜杠正则 → throw(LLM 高频错)[P-generate-roundtrip]", () => {
|
|
147
|
+
expect(() => actionGenerate(gen(PI_META_REGEX_SINGLE_BS), undefined)).toThrow(
|
|
148
|
+
/escape|cannot be parsed/i,
|
|
149
|
+
);
|
|
141
150
|
expect(mockedWriteFileSync).not.toHaveBeenCalled();
|
|
142
151
|
});
|
|
143
152
|
|
|
144
|
-
it("TC6: ESM import →
|
|
145
|
-
|
|
146
|
-
expect(r.isError).toBe(true);
|
|
147
|
-
expect(textOf(r)).toMatch(/ESM|import/i);
|
|
153
|
+
it("TC6: ESM import → throw(保留现有行为)", () => {
|
|
154
|
+
expect(() => actionGenerate(gen(ESM_IMPORT), undefined)).toThrow(/ESM|import/i);
|
|
148
155
|
});
|
|
149
156
|
|
|
150
|
-
it("TC7: 无 agent() →
|
|
151
|
-
|
|
152
|
-
expect(r.isError).toBe(true);
|
|
153
|
-
expect(textOf(r)).toMatch(/agent\(\)/i);
|
|
157
|
+
it("TC7: 无 agent() → throw(保留现有行为)", () => {
|
|
158
|
+
expect(() => actionGenerate(gen(NO_AGENT), undefined)).toThrow(/agent\(\)/i);
|
|
154
159
|
});
|
|
155
160
|
|
|
156
|
-
it("TC8: signal aborted →
|
|
161
|
+
it("TC8: signal aborted → throw(保留现有行为)", () => {
|
|
157
162
|
const controller = new AbortController();
|
|
158
163
|
controller.abort();
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
164
|
+
expect(() => actionGenerate(gen(PI_META_VALID), controller.signal)).toThrow(/abort/i);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("TC9: 缺 name/script 参数 → throw 'generate requires'(防御性,schema 先拦)", () => {
|
|
168
|
+
expect(() => actionGenerate({ action: "generate" } as ScriptParams, undefined)).toThrow(
|
|
169
|
+
"generate requires 'name' and 'script' parameters",
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("TC10: ESM export(非 meta)→ throw(W4b 收敛路径)", () => {
|
|
174
|
+
const script = `/* @pi-meta
|
|
175
|
+
name: x
|
|
176
|
+
description: d
|
|
177
|
+
phases: [a]
|
|
178
|
+
*/
|
|
179
|
+
const agent = require("./agent");
|
|
180
|
+
export const foo = 1;
|
|
181
|
+
agent("w");
|
|
182
|
+
`;
|
|
183
|
+
expect(() => actionGenerate(gen(script), undefined)).toThrow(/ESM 'export'/i);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("actionSave/actionDelete error paths (W4: throw 范式)", () => {
|
|
188
|
+
/**
|
|
189
|
+
* W4:save/delete 失败路径从 return {isError:true} 改为 throw——pi 只对 execute
|
|
190
|
+
* throw 置 isError:true(agent-loop.js:453-483 丢弃返回值里的 isError)。
|
|
191
|
+
* 经 registerWorkflowScriptTool 注册层测(mock workflow-files 的 FS 依赖)。
|
|
192
|
+
*/
|
|
193
|
+
interface CapturedTool {
|
|
194
|
+
execute: (
|
|
195
|
+
toolCallId: string,
|
|
196
|
+
params: Record<string, unknown>,
|
|
197
|
+
signal: AbortSignal | undefined,
|
|
198
|
+
onUpdate: unknown,
|
|
199
|
+
ctx: unknown,
|
|
200
|
+
) => Promise<TextContent>;
|
|
201
|
+
}
|
|
202
|
+
function captureTool(): CapturedTool {
|
|
203
|
+
const tools: CapturedTool[] = [];
|
|
204
|
+
const pi = { registerTool: (t: unknown) => tools.push(t as CapturedTool) };
|
|
205
|
+
// registry 最小 stub:delete 成功路径会调 invalidate(失败路径 throw 前不触达)
|
|
206
|
+
const registry = { invalidate: vi.fn() };
|
|
207
|
+
registerWorkflowScriptTool(pi as never, registry as never, () => false);
|
|
208
|
+
if (!tools[0]) throw new Error("registerWorkflowScriptTool did not register");
|
|
209
|
+
return tools[0];
|
|
210
|
+
}
|
|
211
|
+
const ctx = { mode: "tui" as const, hasUI: true };
|
|
212
|
+
|
|
213
|
+
it("save 失败 → throw 'Save failed: <原因>'(pi catch 后置 isError:true)", async () => {
|
|
214
|
+
vi.mocked(saveWorkflow).mockRejectedValueOnce(new Error("disk full"));
|
|
215
|
+
const tool = captureTool();
|
|
216
|
+
await expect(
|
|
217
|
+
tool.execute("id", { action: "save", name: "tmp-wf" }, undefined, undefined, ctx),
|
|
218
|
+
).rejects.toThrow("Save failed: disk full");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("delete 失败 → throw 'Delete failed: <原因>'", async () => {
|
|
222
|
+
// deleteWorkflow 是同步函数——mock 用同步 throw(mockRejectedValue 不会被
|
|
223
|
+
// actionDelete 的同步 try/catch 捕获)
|
|
224
|
+
vi.mocked(deleteWorkflow).mockImplementationOnce(() => {
|
|
225
|
+
throw new Error("script is running");
|
|
226
|
+
});
|
|
227
|
+
const tool = captureTool();
|
|
228
|
+
await expect(
|
|
229
|
+
tool.execute("id", { action: "delete", name: "tmp-wf" }, undefined, undefined, ctx),
|
|
230
|
+
).rejects.toThrow("Delete failed: script is running");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("save 成功路径不受影响(ok details 正常返回)", async () => {
|
|
234
|
+
vi.mocked(saveWorkflow).mockResolvedValueOnce("saved tmp-wf");
|
|
235
|
+
const tool = captureTool();
|
|
236
|
+
const r = await tool.execute("id", { action: "save", name: "tmp-wf" }, undefined, undefined, ctx);
|
|
237
|
+
expect(r.isError).toBeFalsy();
|
|
238
|
+
expect(r.details).toMatchObject({ action: "save", name: "tmp-wf", ok: true });
|
|
162
239
|
});
|
|
163
240
|
});
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W4b:workflow tool 错误路径 throw 语义(代表路径)。
|
|
3
|
+
*
|
|
4
|
+
* pi 只对 execute throw 置 isError:true——返回值里的 isError 被 agent-loop 丢弃
|
|
5
|
+
* (agent-loop.js:453-483),故错误路径必须 throw。本文件抽 4 个代表路径
|
|
6
|
+
* (W4b 验收交付物 3):
|
|
7
|
+
* - not_found(abort 目标 runId 不存在)
|
|
8
|
+
* - reentry-busy(guard 占用,throw 前未 acquire)
|
|
9
|
+
* - 平铺检测(args 子字段平铺到顶层,LLM 常见错误)
|
|
10
|
+
* - slug 护栏(schema maxLength 之后的运行时第二道)
|
|
11
|
+
* generate 校验族(第 5 代表路径)见 tool-workflow-script-generate.test.ts。
|
|
12
|
+
*
|
|
13
|
+
* 经 registerWorkflowTool 注册层测(capture tool + 最小 deps stub),
|
|
14
|
+
* 与 W4 save/delete 测试同范式。框架:vitest。
|
|
15
|
+
*/
|
|
16
|
+
import { describe, expect, it, vi } from "vitest";
|
|
17
|
+
|
|
18
|
+
import { SLUG_MAX_LENGTH } from "../../execution/execute-options-mapper.ts";
|
|
19
|
+
import { REENTRY_BUSY_MESSAGE, type ReentryGuardRef } from "../reentry-guard.ts";
|
|
20
|
+
import { registerWorkflowTool } from "../tool-workflow.ts";
|
|
21
|
+
|
|
22
|
+
// ── capture helper ──
|
|
23
|
+
|
|
24
|
+
interface CapturedTool {
|
|
25
|
+
name: string;
|
|
26
|
+
execute: (
|
|
27
|
+
toolCallId: string,
|
|
28
|
+
params: Record<string, unknown>,
|
|
29
|
+
signal: AbortSignal | undefined,
|
|
30
|
+
onUpdate: unknown,
|
|
31
|
+
ctx: unknown,
|
|
32
|
+
) => Promise<unknown>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function captureTool(deps: unknown, reentryRef: ReentryGuardRef): CapturedTool {
|
|
36
|
+
const tools: CapturedTool[] = [];
|
|
37
|
+
const pi = { registerTool: (t: unknown) => tools.push(t as CapturedTool) };
|
|
38
|
+
registerWorkflowTool(pi as never, deps as never, reentryRef);
|
|
39
|
+
if (!tools[0] || tools[0].name !== "workflow") {
|
|
40
|
+
throw new Error("registerWorkflowTool did not register the workflow tool");
|
|
41
|
+
}
|
|
42
|
+
return tools[0];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 最小 LauncherDeps stub:被测路径只触 registry / runs。 */
|
|
46
|
+
function makeDeps(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
|
47
|
+
return {
|
|
48
|
+
runs: new Map(),
|
|
49
|
+
registry: {
|
|
50
|
+
getPath: vi.fn().mockResolvedValue(undefined),
|
|
51
|
+
loadAll: vi.fn().mockResolvedValue([]),
|
|
52
|
+
},
|
|
53
|
+
...overrides,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 平铺检测 / slug 护栏路径的 registry script stub(meta.parameters 驱动已知键集)。 */
|
|
58
|
+
function makeScript(parameters?: object): Record<string, unknown> {
|
|
59
|
+
return {
|
|
60
|
+
name: "demo-wf",
|
|
61
|
+
path: "/abs/demo-wf.js",
|
|
62
|
+
available: true,
|
|
63
|
+
sourceCode: "// demo",
|
|
64
|
+
meta: { description: "demo workflow", parameters },
|
|
65
|
+
toExecutable: () => "// demo",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── 代表路径 ──
|
|
70
|
+
|
|
71
|
+
describe("W4b: workflow tool 错误路径 throw 语义", () => {
|
|
72
|
+
it("not_found:abort 目标 runId 不存在 → throw(pi catch 后置 isError:true)", async () => {
|
|
73
|
+
const tool = captureTool(makeDeps(), { isProcessing: false });
|
|
74
|
+
await expect(
|
|
75
|
+
tool.execute("id", { action: "abort", runId: "no-such-run" }, undefined, undefined, {}),
|
|
76
|
+
).rejects.toThrow(
|
|
77
|
+
"Workflow 'no-such-run' not found. Use action:status to list active runs and their runIds.",
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("W4c not_found:run 目标路径不可读(getPath 返回 available:false stub 而非 undefined)→ throw,不假启动", async () => {
|
|
82
|
+
// config-loader.toCachedMeta 对不存在/不可读文件返回 available:false 的空壳实体
|
|
83
|
+
// (非 undefined)——旧判定仅 !script 绕过 not_found,空 sourceCode 的 run 假启动
|
|
84
|
+
// (W4b verifier 探针实测复现)。registry 层真行为见 config-loader.ts:143-151。
|
|
85
|
+
const deps = makeDeps({
|
|
86
|
+
registry: {
|
|
87
|
+
getPath: vi.fn().mockResolvedValue({
|
|
88
|
+
name: "ghost-wf",
|
|
89
|
+
path: "/tmp/no-such-workflow.js",
|
|
90
|
+
available: false,
|
|
91
|
+
sourceCode: "",
|
|
92
|
+
meta: { description: "", parameters: undefined },
|
|
93
|
+
toExecutable: () => "",
|
|
94
|
+
}),
|
|
95
|
+
loadAll: vi.fn().mockResolvedValue([]),
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
const tool = captureTool(deps, { isProcessing: false });
|
|
99
|
+
await expect(
|
|
100
|
+
tool.execute("id", { action: "run", name: "/tmp/no-such-workflow.js" }, undefined, undefined, {}),
|
|
101
|
+
).rejects.toThrow("Workflow '/tmp/no-such-workflow.js' not found.");
|
|
102
|
+
// 未假启动:runs 注册表不出现该 run
|
|
103
|
+
expect(deps.runs).toBeInstanceOf(Map);
|
|
104
|
+
expect((deps.runs as Map<string, unknown>).size).toBe(0);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("reentry-busy:guard 占用 → throw REENTRY_BUSY_MESSAGE,且 guard 状态不被污染", async () => {
|
|
108
|
+
const guard: ReentryGuardRef = { isProcessing: true };
|
|
109
|
+
const tool = captureTool(makeDeps(), guard);
|
|
110
|
+
await expect(
|
|
111
|
+
tool.execute("id", { action: "status" }, undefined, undefined, {}),
|
|
112
|
+
).rejects.toThrow(REENTRY_BUSY_MESSAGE);
|
|
113
|
+
// throw 发生在 acquire 之前——占用方语义保持,不产生双重 release
|
|
114
|
+
expect(guard.isProcessing).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("平铺检测:args 子字段提到顶层 → throw 'Detected ... at top level'(含 Correct 正例)", async () => {
|
|
118
|
+
const deps = makeDeps({
|
|
119
|
+
registry: {
|
|
120
|
+
getPath: vi.fn().mockResolvedValue(
|
|
121
|
+
makeScript({
|
|
122
|
+
type: "object",
|
|
123
|
+
properties: { task: { type: "string" }, items: { type: "array" } },
|
|
124
|
+
required: ["task"],
|
|
125
|
+
}),
|
|
126
|
+
),
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
const tool = captureTool(deps, { isProcessing: false });
|
|
130
|
+
await expect(
|
|
131
|
+
tool.execute(
|
|
132
|
+
"id",
|
|
133
|
+
{ action: "run", name: "/abs/demo-wf.js", task: "do work" },
|
|
134
|
+
undefined,
|
|
135
|
+
undefined,
|
|
136
|
+
{},
|
|
137
|
+
),
|
|
138
|
+
).rejects.toThrow(
|
|
139
|
+
/Detected task at top level — they belong inside 'args'\. Correct: \{"action":"run","name":"\/abs\/demo-wf\.js","args":\{"task": "<value>"\}\}/,
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("slug 护栏:slug 超 SLUG_MAX_LENGTH → throw 'slug exceeds ...'(运行时第二道)", async () => {
|
|
144
|
+
const deps = makeDeps({
|
|
145
|
+
registry: {
|
|
146
|
+
getPath: vi.fn().mockResolvedValue(
|
|
147
|
+
makeScript({
|
|
148
|
+
type: "object",
|
|
149
|
+
properties: { task: { type: "string" } },
|
|
150
|
+
required: ["task"],
|
|
151
|
+
}),
|
|
152
|
+
),
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
const tool = captureTool(deps, { isProcessing: false });
|
|
156
|
+
const longSlug = "a".repeat(SLUG_MAX_LENGTH + 1);
|
|
157
|
+
await expect(
|
|
158
|
+
tool.execute(
|
|
159
|
+
"id",
|
|
160
|
+
{ action: "run", name: "/abs/demo-wf.js", slug: longSlug, args: {} },
|
|
161
|
+
undefined,
|
|
162
|
+
undefined,
|
|
163
|
+
{},
|
|
164
|
+
),
|
|
165
|
+
).rejects.toThrow(
|
|
166
|
+
`slug exceeds ${SLUG_MAX_LENGTH} chars (got ${longSlug.length}). Shorten to a kebab-case label, e.g. "fix-login", "extract-urls".`,
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("throw 后 reentry guard 经 finally 正常释放(成功路径回归)", async () => {
|
|
171
|
+
// abort not_found throw 穿透 execute try/finally:guard 必须复位,否则后续命令全部 busy
|
|
172
|
+
const guard: ReentryGuardRef = { isProcessing: false };
|
|
173
|
+
const tool = captureTool(makeDeps(), guard);
|
|
174
|
+
await expect(
|
|
175
|
+
tool.execute("id", { action: "abort", runId: "no-such-run" }, undefined, undefined, {}),
|
|
176
|
+
).rejects.toThrow(/not found/);
|
|
177
|
+
expect(guard.isProcessing).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import type { Component } from "@earendil-works/pi-tui";
|
|
23
23
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
24
24
|
|
|
25
|
+
import { displayAgentName } from "../shared/agent-ref.ts";
|
|
25
26
|
import {
|
|
26
27
|
firstLine,
|
|
27
28
|
padToVisible,
|
|
@@ -52,11 +53,16 @@ const MIN_BORDER_WIDTH = BORDER_CHARS + INNER_PAD_TOTAL + 1;
|
|
|
52
53
|
*/
|
|
53
54
|
interface BgNotifyRecord {
|
|
54
55
|
id: string;
|
|
55
|
-
|
|
56
|
+
/** v4 B-1: closed(终态,含 cancelled)或 running(对话模式轮次完成,旧 idle)。 */
|
|
57
|
+
status: "running" | "closed";
|
|
58
|
+
/** L2 关闭原因子枚举(仅 status="closed" 时有意义)。 */
|
|
59
|
+
closedReason?: string;
|
|
56
60
|
agent: string;
|
|
57
61
|
model?: string;
|
|
58
62
|
result?: string;
|
|
59
63
|
error?: string;
|
|
64
|
+
/** 对话轮次计数(仅 idle 有意义)。 */
|
|
65
|
+
round?: number;
|
|
60
66
|
/** [MF#1] worktree background 完成通知携带的 patch 文件路径。 */
|
|
61
67
|
patchFile?: string;
|
|
62
68
|
}
|
|
@@ -207,17 +213,34 @@ function sanitizeAnsiForBg(text: string): string {
|
|
|
207
213
|
function renderRecordLines(record: BgNotifyRecord, t: ThemeLike): string[] {
|
|
208
214
|
const glyph = statusGlyph(record.status);
|
|
209
215
|
const icon = glyph.icon ?? "•";
|
|
210
|
-
const agent = truncLine(record.agent, AGENT_MAX_WIDTH);
|
|
216
|
+
const agent = truncLine(displayAgentName(record.agent), AGENT_MAX_WIDTH);
|
|
211
217
|
// model 段:agent 后、状态描述前,accent 色。空则省略(向后兼容旧 record)。
|
|
212
218
|
const modelPart = record.model
|
|
213
219
|
? ` ${t.fg("dim", "·")} ${t.fg("accent", truncLine(record.model, MODEL_MAX_WIDTH))}`
|
|
214
220
|
: "";
|
|
215
|
-
|
|
221
|
+
// v4 B-1: closed 统一终态(含 cancelled)。按 closedReason 派生文案。
|
|
222
|
+
const reason = record.closedReason ?? "gc";
|
|
223
|
+
let verb: string;
|
|
224
|
+
if (reason === "cancelled") {
|
|
225
|
+
verb = "cancelled";
|
|
226
|
+
} else if (reason === "gc" && record.error) {
|
|
227
|
+
verb = "failed";
|
|
228
|
+
} else {
|
|
229
|
+
verb = "finished";
|
|
230
|
+
}
|
|
216
231
|
const idShort = shortId(record.id);
|
|
217
232
|
const head = `${t.fg(glyph.color, icon)} ${t.bold(agent)}${modelPart}${t.fg("dim", ` — background subagent ${verb} - ${idShort}`)}`;
|
|
218
233
|
|
|
219
234
|
switch (record.status) {
|
|
220
|
-
case "
|
|
235
|
+
case "closed": {
|
|
236
|
+
// v4 B-1: closed 统一终态(含 cancelled)。cancelled 无正文;失败显示错误;否则结果/patch。
|
|
237
|
+
const r = record.closedReason ?? "gc";
|
|
238
|
+
if (r === "cancelled") {
|
|
239
|
+
return [head];
|
|
240
|
+
}
|
|
241
|
+
if (r === "gc" && record.error) {
|
|
242
|
+
return [head, t.fg("dim", truncLine(`Error: ${firstLineSanitized(record.error)}`, BODY_MAX_WIDTH))];
|
|
243
|
+
}
|
|
221
244
|
if (!record.result && !record.patchFile) return [head];
|
|
222
245
|
const lines: string[] = [];
|
|
223
246
|
if (record.result) {
|
|
@@ -229,10 +252,9 @@ function renderRecordLines(record: BgNotifyRecord, t: ThemeLike): string[] {
|
|
|
229
252
|
}
|
|
230
253
|
return [head, ...lines];
|
|
231
254
|
}
|
|
232
|
-
case "
|
|
233
|
-
return [head, t.fg("dim", truncLine(`Error: ${record.error ? firstLineSanitized(record.error) : "(unknown)"}`, BODY_MAX_WIDTH))];
|
|
234
|
-
case "cancelled":
|
|
255
|
+
case "running":
|
|
235
256
|
default:
|
|
257
|
+
// v4 B-1: 对话模式轮次完成(旧 idle 折入 running)——仅标题行。
|
|
236
258
|
return [head];
|
|
237
259
|
}
|
|
238
260
|
}
|
|
@@ -263,7 +285,7 @@ function extractBgNotifyRecord(details: unknown): BgNotifyRecord | undefined {
|
|
|
263
285
|
const status = d.status;
|
|
264
286
|
const agent = d.agent;
|
|
265
287
|
if (
|
|
266
|
-
(status !== "
|
|
288
|
+
(status !== "closed" && status !== "running") ||
|
|
267
289
|
typeof agent !== "string"
|
|
268
290
|
) {
|
|
269
291
|
return undefined;
|
|
@@ -275,6 +297,8 @@ function extractBgNotifyRecord(details: unknown): BgNotifyRecord | undefined {
|
|
|
275
297
|
model: typeof d.model === "string" ? d.model : undefined,
|
|
276
298
|
result: typeof d.result === "string" ? d.result : undefined,
|
|
277
299
|
error: typeof d.error === "string" ? d.error : undefined,
|
|
300
|
+
closedReason: typeof d.closedReason === "string" ? d.closedReason : undefined,
|
|
301
|
+
round: typeof d.round === "number" ? d.round : undefined,
|
|
278
302
|
// [MF#1] 提取 patchFile(worktree background 完成通知携带)。
|
|
279
303
|
patchFile: typeof d.patchFile === "string" ? d.patchFile : undefined,
|
|
280
304
|
};
|