@zhushanwen/pi-base-tool-enhance 0.5.4 → 0.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-base-tool-enhance",
3
- "version": "0.5.4",
3
+ "version": "0.6.0",
4
4
  "description": "Base tool enhancement - override builtin bash tool: foreground delegates to pi official factory, incremental background mode (design decisions documented in source comments)",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -28,11 +28,11 @@
28
28
  "skills/"
29
29
  ],
30
30
  "dependencies": {
31
- "@taiji/extension-protocol": "0.11.0",
32
- "@zhushanwen/pi-ext-guards": "0.4.0",
33
- "@zhushanwen/pi-extension-logger": "0.6.0",
34
- "@zhushanwen/pi-file-lock": "0.4.0",
35
- "@zhushanwen/pi-llm-shared": "0.8.0"
31
+ "@zhushanwen/extension-protocol": "0.12.0",
32
+ "@zhushanwen/pi-ext-guards": "0.4.1",
33
+ "@zhushanwen/pi-extension-logger": "0.6.1",
34
+ "@zhushanwen/pi-file-lock": "0.4.1",
35
+ "@zhushanwen/pi-llm-shared": "0.8.1"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@vitest/coverage-v8": "^4.1.9",
@@ -41,7 +41,7 @@
41
41
  "peerDependencies": {
42
42
  "@earendil-works/pi-coding-agent": "^0.84.4",
43
43
  "typebox": "*",
44
- "@zhushanwen/pi-pending-notifications": "0.7.3"
44
+ "@zhushanwen/pi-pending-notifications": "0.7.5"
45
45
  },
46
46
  "peerDependenciesMeta": {
47
47
  "@zhushanwen/pi-pending-notifications": {
@@ -20,9 +20,9 @@ vi.mock("@earendil-works/pi-coding-agent", () => ({
20
20
  // 进程原语(ext-simplify-13 后单点在 protocol 子出口)转发真实杀 + 记录调用
21
21
  //(timeout 分支断言 killProcessTree 被调)
22
22
  const { killTreeCalls } = vi.hoisted(() => ({ killTreeCalls: [] as number[] }));
23
- vi.mock("@taiji/extension-protocol/background-task", async (importOriginal) => {
23
+ vi.mock("@zhushanwen/extension-protocol/background-task", async (importOriginal) => {
24
24
  const orig = await importOriginal<
25
- typeof import("@taiji/extension-protocol/background-task")
25
+ typeof import("@zhushanwen/extension-protocol/background-task")
26
26
  >();
27
27
  return {
28
28
  ...orig,
@@ -33,7 +33,7 @@ vi.mock("@taiji/extension-protocol/background-task", async (importOriginal) => {
33
33
  };
34
34
  });
35
35
 
36
- import { isPidAlive } from "@taiji/extension-protocol/background-task";
36
+ import { isPidAlive } from "@zhushanwen/extension-protocol/background-task";
37
37
 
38
38
  import { createBashKillToolDefinition } from "../bash-kill-tool.ts";
39
39
  import { createBashOutputToolDefinition } from "../bash-output-tool.ts";
@@ -237,6 +237,12 @@ describe("real spawn lifecycle (poll edge finalization)", () => {
237
237
  });
238
238
 
239
239
  describe("bash_output tool", () => {
240
+ it("description pins the auto-notify contract (on-demand checks only, DO NOT bash sleep)", () => {
241
+ // 与 bash description / 启动回执同一契约话术(统一 "DO NOT bash sleep")——删改必须红灯
242
+ expect(bashOutput.description).toContain("completion is auto-notified");
243
+ expect(bashOutput.description).toContain("DO NOT bash sleep");
244
+ });
245
+
240
246
  it("list merges store + registry terminal entries, store wins on same id", async () => {
241
247
  // 单例表:一个 running 真任务
242
248
  const spawned = spawnBg("sleep 5");
@@ -139,6 +139,10 @@ describe("createBashOverrideToolDefinition", () => {
139
139
  // 白名单自动转后台 + timeout 显式值被尊重(除白名单强转后台例外)
140
140
  expect(tool.description).toContain("whitelist");
141
141
  expect(tool.description).toContain("timeout in seconds");
142
+ // 自动通知契约("DO NOT bash sleep" 统一话术是 background-bash-notify 主线主张——
143
+ // 实测大量 agent bash sleep 空等后加的硬契约,删改必须红灯)
144
+ expect(tool.description).toContain("Completion is auto-notified");
145
+ expect(tool.description).toContain("DO NOT bash sleep");
142
146
  });
143
147
 
144
148
  it("passes through official promptSnippet/promptGuidelines (system-prompt parity)", () => {
@@ -281,6 +285,9 @@ describe("background branch routing (M2)", () => {
281
285
  expect(text).toContain("pid: 12345");
282
286
  expect(text).toContain("Output file:");
283
287
  expect(text).toContain('bash_output {task_id:"bt-1724589012-a3f7"}');
288
+ // 启动回执同钉自动通知契约(与 description 同一话术,startBackgroundAndReply 单点拼装)
289
+ expect(text).toContain("Completion is auto-notified");
290
+ expect(text).toContain("DO NOT bash sleep");
284
291
  });
285
292
 
286
293
  it("background spawn failure surfaces the error", async () => {
@@ -1,7 +1,6 @@
1
1
  // src/__tests__/maintenance-once.test.ts —— 收殓下沉后的 session_start 维护链
2
- // 语义(实施单元 u-bte-remove,设计 docs/architecture/file-lock-unification-and-
3
- // reaper-sink.md §3.2 D2「extension 删 session_start reaper」/ §3.3 D3 粒度段 / §4 S6
4
- // 「批 2 后 reap 类操作不再执行」):
2
+ // 语义(实施单元 u-bte-remove:extension 删 session_start reaper,批 2 后
3
+ // reap 类操作不再执行):
5
4
  // - reconcilePendingEntries 是 session 级豁免类,每 session_start 派发都执行
6
5
  // (startup/resume/new 多派发 ×N——含 factory 二调 handler 累积的真实形态)
7
6
  // - 触发面消失:reaper.ts 模块文件已删除(文件级断言,见首条用例)+ 维护链不再触发
@@ -8,10 +8,11 @@ import { join } from "node:path";
8
8
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
9
9
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
10
10
 
11
- import { killProcessTree } from "@taiji/extension-protocol/background-task";
11
+ import { killProcessTree } from "@zhushanwen/extension-protocol/background-task";
12
12
  import {
13
13
  BACKGROUND_BASH_CUSTOM_TYPE,
14
14
  buildNotificationContent,
15
+ buildNotifyDetails,
15
16
  emitPendingRegister,
16
17
  handleTaskExit,
17
18
  refreshPiReference,
@@ -46,6 +47,12 @@ function attach(pi: MockPi): void {
46
47
  refreshPiReference(pi as unknown as ExtensionAPI);
47
48
  }
48
49
 
50
+ /** 取第一条 sendMessage 的首参数对象(D1 details 断言入口)。 */
51
+ function sentMessage(pi: MockPi): { content: string; details: unknown } {
52
+ const [message] = pi.sendMessage.mock.calls[0] as [{ content: string; details: unknown }, unknown];
53
+ return message;
54
+ }
55
+
49
56
  function sleep(ms: number): Promise<void> {
50
57
  return new Promise((resolve) => setTimeout(resolve, ms));
51
58
  }
@@ -187,7 +194,7 @@ describe("exit-edge notification (⑧⑨, poll edge wiring)", () => {
187
194
  });
188
195
  expect(pi.sendMessage).toHaveBeenCalledTimes(1);
189
196
  const [message, options] = pi.sendMessage.mock.calls[0] as [
190
- { customType: string; content: string; display: boolean },
197
+ { customType: string; content: string; display: boolean; details: Record<string, unknown> },
191
198
  { deliverAs: string; triggerTurn: boolean },
192
199
  ];
193
200
  expect(message.customType).toBe(BACKGROUND_BASH_CUSTOM_TYPE);
@@ -196,6 +203,15 @@ describe("exit-edge notification (⑧⑨, poll edge wiring)", () => {
196
203
  expect(message.content).toContain("exit 0");
197
204
  expect(message.content).toContain("done"); // tail 摘要
198
205
  expect(options).toEqual({ deliverAs: "steer", triggerTurn: true });
206
+ // U1(D1):真实轮询边沿投递的 details 与终态条目逐字段一致(非 fixture 常量)
207
+ const finalizedEntry = getTask(task.taskId);
208
+ expect(message.details).toMatchObject({
209
+ taskId: task.taskId,
210
+ command: "sleep 0.3 && echo done",
211
+ endReason: "natural",
212
+ exitCode: 0,
213
+ });
214
+ expect((message.details as { durationMs: number }).durationMs).toBe(finalizedEntry?.durationMs);
199
215
  });
200
216
 
201
217
  it("killed: emits unregister reason 'cancelled' but does NOT sendMessage (single-point rule)", async () => {
@@ -285,6 +301,70 @@ describe("notification content (§3.1 sample)", () => {
285
301
  });
286
302
  });
287
303
 
304
+ describe("notification details (D1 schema: taskId/command/durationMs/endReason/exitCode)", () => {
305
+ it("natural: details mirror the terminal task fields; content stays byte-identical (LLM payload)", () => {
306
+ const pi = createMockPi();
307
+ attach(pi);
308
+ const task = finalizedTask();
309
+ handleTaskExit(task);
310
+
311
+ const message = sentMessage(pi);
312
+ expect(message.details).toEqual({
313
+ taskId: task.taskId,
314
+ command: task.command,
315
+ durationMs: task.durationMs,
316
+ endReason: "natural",
317
+ exitCode: task.exitCode,
318
+ });
319
+ // 键级断言:D1 恰五字段,防字段悄悄混入(register emit 同款范式)
320
+ expect(Object.keys(message.details as Record<string, unknown>).sort()).toEqual([
321
+ "command",
322
+ "durationMs",
323
+ "endReason",
324
+ "exitCode",
325
+ "taskId",
326
+ ]);
327
+ // content 是 triggerTurn:true 给 LLM 接力的载荷:挂 details 不得改动一个字节
328
+ expect(message.content).toBe(
329
+ "[background-bash] bt-1700000000-test01 finished (exit 0, 3m12s): pnpm test\n" +
330
+ "Last lines: Tests: 42 passed\n" +
331
+ 'Full output: /tmp/out/bt-1700000000-test01.log; use bash_output {task_id:"bt-1700000000-test01"} for details.',
332
+ );
333
+ });
334
+
335
+ it("natural nonzero exit: exitCode passes through unchanged, endReason stays 'natural'", () => {
336
+ const pi = createMockPi();
337
+ attach(pi);
338
+ handleTaskExit(finalizedTask({ exitCode: 3, durationMs: 5000 }));
339
+
340
+ expect(sentMessage(pi).details).toEqual({
341
+ taskId: "bt-1700000000-test01",
342
+ command: "pnpm test",
343
+ durationMs: 5000,
344
+ endReason: "natural",
345
+ exitCode: 3,
346
+ });
347
+ });
348
+
349
+ it("timeout: endReason 'timeout' + exitCode null (D1 可空字段的 timeout 形态)", () => {
350
+ const pi = createMockPi();
351
+ attach(pi);
352
+ handleTaskExit(finalizedTask({ reason: "timeout", exitCode: null, durationMs: 15_000 }));
353
+
354
+ expect(sentMessage(pi).details).toEqual({
355
+ taskId: "bt-1700000000-test01",
356
+ command: "pnpm test",
357
+ durationMs: 15_000,
358
+ endReason: "timeout",
359
+ exitCode: null,
360
+ });
361
+ });
362
+
363
+ it("durationMs 缺省归 0:必需字段恒为 number(消费侧解析器才不判 null)", () => {
364
+ expect(buildNotifyDetails(finalizedTask({ durationMs: undefined })).durationMs).toBe(0);
365
+ });
366
+ });
367
+
288
368
  describe("D17: pi reference refresh (session replacement takeover)", () => {
289
369
  it("second load wins: notification goes through the NEW reference, old one untouched", () => {
290
370
  const piA = createMockPi();
@@ -10,7 +10,7 @@ import {
10
10
  BACKGROUND_TASK_ID_PREFIX,
11
11
  collectActivePendingIds,
12
12
  mapReasonToStatus,
13
- } from "@taiji/extension-protocol";
13
+ } from "@zhushanwen/extension-protocol";
14
14
  import { afterEach, describe, expect, it, vi } from "vitest";
15
15
 
16
16
  import {
@@ -5,7 +5,7 @@ import { join } from "node:path";
5
5
 
6
6
  import { afterEach, describe, expect, it } from "vitest";
7
7
 
8
- import { MAX_TERMINAL_REGISTRY_ENTRIES } from "@taiji/extension-protocol";
8
+ import { MAX_TERMINAL_REGISTRY_ENTRIES } from "@zhushanwen/extension-protocol";
9
9
 
10
10
  import {
11
11
  getRegistryPath,
@@ -48,6 +48,16 @@ export const BACKGROUND_BASH_CUSTOM_TYPE = "background-bash";
48
48
  */
49
49
  const PENDING_NAME_LIMIT = 80;
50
50
 
51
+ /**
52
+ * 命令文案截断(pending register 的 name 与完成通知 head 行共用同一口径):
53
+ * 超 PENDING_NAME_LIMIT 取前 80 字符 + 省略号。
54
+ */
55
+ function truncateCommand(command: string): string {
56
+ return command.length > PENDING_NAME_LIMIT
57
+ ? `${command.slice(0, PENDING_NAME_LIMIT)}…`
58
+ : command;
59
+ }
60
+
51
61
  /** 模块级「当前 pi 引用」(D17 核心可变状态,见文件头)。 */
52
62
  let currentPi: ExtensionAPI | undefined;
53
63
 
@@ -100,10 +110,7 @@ export function toPendingReason(
100
110
  export function emitPendingRegister(task: BackgroundTask): void {
101
111
  const pi = currentPi;
102
112
  if (pi === undefined) return;
103
- const name =
104
- task.command.length > PENDING_NAME_LIMIT
105
- ? `${task.command.slice(0, PENDING_NAME_LIMIT)}…`
106
- : task.command;
113
+ const name = truncateCommand(task.command);
107
114
  try {
108
115
  pi.events.emit("pending:register", { id: task.taskId, type: "bash", name });
109
116
  } catch (err) {
@@ -153,7 +160,12 @@ function sendTaskFinishedMessage(task: BackgroundTask): void {
153
160
  if (pi === undefined) return;
154
161
  try {
155
162
  pi.sendMessage(
156
- { customType: BACKGROUND_BASH_CUSTOM_TYPE, content: buildNotificationContent(task), display: true },
163
+ {
164
+ customType: BACKGROUND_BASH_CUSTOM_TYPE,
165
+ content: buildNotificationContent(task),
166
+ display: true,
167
+ details: buildNotifyDetails(task),
168
+ },
157
169
  { deliverAs: "steer", triggerTurn: true },
158
170
  );
159
171
  } catch (err) {
@@ -179,10 +191,7 @@ function sendTaskFinishedMessage(task: BackgroundTask): void {
179
191
  */
180
192
  export function buildNotificationContent(task: BackgroundTask): string {
181
193
  const duration = formatDurationMs(task.durationMs ?? 0);
182
- const command =
183
- task.command.length > PENDING_NAME_LIMIT
184
- ? `${task.command.slice(0, PENDING_NAME_LIMIT)}…`
185
- : task.command;
194
+ const command = truncateCommand(task.command);
186
195
  let head: string;
187
196
  if (task.reason === "timeout") {
188
197
  head = `[background-bash] ${task.taskId} timed out (${duration}): ${command}`;
@@ -199,6 +208,49 @@ export function buildNotificationContent(task: BackgroundTask): string {
199
208
  return lines.join("\n");
200
209
  }
201
210
 
211
+ /**
212
+ * sendMessage details 载荷(设计 §3.3 D1 schema)。
213
+ *
214
+ * 为什么另立 details 而非让上层解析 content:content 是本模块内部模板字符串、写给
215
+ * LLM 接力的日志行,格式漂移即静默打破解析;details 经既有透传管道(event-adapter
216
+ * → registry → reducer → Message.details)结构化到达渲染层,content 原文对 LLM 逐
217
+ * 字节不变。
218
+ *
219
+ * 字面量镜像:消费侧解析器 `packages/shared/src/message.ts` 的
220
+ * `parseBackgroundBashDetails`(本包是独立 npm 包,不 import taiji 内部 shared)——
221
+ * 字段名、可空性、枚举值三处必须逐字对齐,改动须同步另一侧。
222
+ *
223
+ * 枚举收敛:`endReason` 只落 natural | timeout——killed 在 handleTaskExit 提前返回、
224
+ * process-exit 走收殓路径(只 emit 不发消息、根本不进本函数),两者均不可达(D1
225
+ * 源码核实),不为不可达形态造死值。
226
+ */
227
+ export interface BackgroundBashDetails {
228
+ taskId: string;
229
+ command: string;
230
+ /** 存活时长(毫秒),与 content 中的耗时同源同口径。 */
231
+ durationMs: number;
232
+ endReason: "natural" | "timeout";
233
+ /** 无退出码时为 null(timeout / 被信号终止语义,D1 可空字段)。 */
234
+ exitCode: number | null;
235
+ }
236
+
237
+ /**
238
+ * 完成通知 details(D1):终态字段与 buildNotificationContent 同源——durationMs
239
+ * 同用 `?? 0` 归一(终态条目必已物化,缺省仅防御)。
240
+ *
241
+ * command 取原始终态值不截断:截断(PENDING_NAME_LIMIT)是 content 行宽约束,结构化
242
+ * 消费方的行宽由渲染层自己决定(旧数据无 details 时仍走 content 原文兜底)。
243
+ */
244
+ export function buildNotifyDetails(task: BackgroundTask): BackgroundBashDetails {
245
+ return {
246
+ taskId: task.taskId,
247
+ command: task.command,
248
+ durationMs: task.durationMs ?? 0,
249
+ endReason: task.reason === "timeout" ? "timeout" : "natural",
250
+ exitCode: task.exitCode ?? null,
251
+ };
252
+ }
253
+
202
254
  /** 耗时换算常量(毫秒/秒/分/时 + 时分两位补零宽度)。 */
203
255
  const MS_PER_SECOND = 1000;
204
256
  const SECONDS_PER_MINUTE = 60;
@@ -8,7 +8,7 @@
8
8
  * - readTailSummary:exit 边沿的 tail 摘要(存条目 / M3 通知用)
9
9
  */
10
10
 
11
- import { readOutputTail as readOutputTailPrimitive } from "@taiji/extension-protocol/background-task";
11
+ import { readOutputTail as readOutputTailPrimitive } from "@zhushanwen/extension-protocol/background-task";
12
12
 
13
13
  /** pi 内置 bash 同款截断上限(last 2000 lines / 50KB = 51200 bytes,先到为准)。 */
14
14
  export const TAIL_MAX_LINES = 2000;
@@ -35,8 +35,8 @@ import {
35
35
  BACKGROUND_TASK_ID_PREFIX,
36
36
  collectActivePendingIds,
37
37
  mapReasonToStatus,
38
- } from "@taiji/extension-protocol";
39
- import { isPidAlive } from "@taiji/extension-protocol/background-task";
38
+ } from "@zhushanwen/extension-protocol";
39
+ import { isPidAlive } from "@zhushanwen/extension-protocol/background-task";
40
40
  import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
41
41
  import { getLogger } from "@zhushanwen/pi-extension-logger";
42
42
 
@@ -13,7 +13,7 @@
13
13
  * 可查、对账可补),不加同步握手。
14
14
  */
15
15
 
16
- import { isPidAlive } from "@taiji/extension-protocol/background-task";
16
+ import { isPidAlive } from "@zhushanwen/extension-protocol/background-task";
17
17
 
18
18
  import { readTailSummary } from "./output-tail.ts";
19
19
  import { readRegistry, taskToRegistryEntry, writeRegistryEntry } from "./registry.ts";
@@ -27,7 +27,7 @@
27
27
  * 本单元不管。
28
28
  */
29
29
 
30
- import { killProcessTree } from "@taiji/extension-protocol/background-task";
30
+ import { killProcessTree } from "@zhushanwen/extension-protocol/background-task";
31
31
  import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
32
32
  import { getLogger } from "@zhushanwen/pi-extension-logger";
33
33
 
@@ -7,7 +7,7 @@
7
7
  * 写入。
8
8
  *
9
9
  * 行为原语单点(ext-simplify-13):解析防御 / corrupt 隔离 / 原子写 / 序列化 /
10
- * 终态 LRU 裁剪自 @taiji/extension-protocol background-task 子出口引入——此前
10
+ * 终态 LRU 裁剪自 @zhushanwen/extension-protocol background-task 子出口引入——此前
11
11
  * 本地实现与 runtime 侧逐字同构、靠注释对齐;本模块只保留 bte 形态薄壳:Map 投影
12
12
  * readRegistry 与锁内 RMW 写壳。
13
13
  *
@@ -26,14 +26,14 @@ import { join } from "node:path";
26
26
 
27
27
  // 契约常量(LRU 上限)走 index 出口(background-task.ts 契约面);行为原语走
28
28
  // background-task 子出口(含 node 内建依赖,不进 index——renderer/core 零触达)
29
- import { MAX_TERMINAL_REGISTRY_ENTRIES } from "@taiji/extension-protocol";
29
+ import { MAX_TERMINAL_REGISTRY_ENTRIES } from "@zhushanwen/extension-protocol";
30
30
  import {
31
31
  atomicWriteRegistry,
32
32
  readRegistry as readRegistryEntries,
33
33
  serializeRegistryFile,
34
34
  trimTerminalEntries,
35
35
  type RegistryFileLogFn,
36
- } from "@taiji/extension-protocol/background-task";
36
+ } from "@zhushanwen/extension-protocol/background-task";
37
37
  import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
38
38
  import { getLogger } from "@zhushanwen/pi-extension-logger";
39
39
  import { withFileLockSync } from "@zhushanwen/pi-file-lock";
@@ -17,7 +17,7 @@ import { mkdirSync, openSync, closeSync, accessSync, constants } from "node:fs";
17
17
  import { dirname, join } from "node:path";
18
18
  import { randomBytes } from "node:crypto";
19
19
 
20
- import { getProcessStartTimeSec, killProcessTree, pidStartMatchesRegistered } from "@taiji/extension-protocol/background-task";
20
+ import { getProcessStartTimeSec, killProcessTree, pidStartMatchesRegistered } from "@zhushanwen/extension-protocol/background-task";
21
21
  import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
22
22
  import { getLogger } from "@zhushanwen/pi-extension-logger";
23
23
 
@@ -28,7 +28,7 @@
28
28
  * 未来新增写路径(如 maintenance 类)必须保持该顺序,违反即 intent 可丢失。
29
29
  */
30
30
 
31
- import { trimTerminalEntries } from "@taiji/extension-protocol/background-task";
31
+ import { trimTerminalEntries } from "@zhushanwen/extension-protocol/background-task";
32
32
 
33
33
  import {
34
34
  isActiveState,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * background 任务核心数据模型(设计文档 docs/design/base-tool-enhance.md §3.5)。
2
+ * background 任务核心数据模型。
3
3
  *
4
4
  * 两层存储分工:
5
5
  * - 单例任务表(task-store.ts,模块级 Map)= 运行时权威;条目唯一来源 = 本进程
@@ -8,7 +8,7 @@
8
8
  * 条目记 ownerPiPid(M2 只负责写入)
9
9
  *
10
10
  * 契约改引(u-bte-remove):registry 持久化形状(RegistryEntry / 状态机 / reason
11
- * 枚举 / active·terminal 判定)与 @taiji/extension-protocol background-task.ts
11
+ * 枚举 / active·terminal 判定)与 @zhushanwen/extension-protocol background-task.ts
12
12
  * 契约逐字段一致(契约文档头声明基线即本文件 + registry.ts),本地副本已删除、
13
13
  * 此处 re-export 并保留包内短名——消费方 import 路径与字段语义零变化。运行时
14
14
  * 对象类型(BackgroundTask / KillingIntent)不在契约内(剥离非持久字段前的形状),
@@ -19,18 +19,18 @@ import type { ChildProcess } from "node:child_process";
19
19
  import type {
20
20
  BackgroundTaskEndReason as ContractEndReason,
21
21
  BackgroundTaskState as ContractState,
22
- } from "@taiji/extension-protocol";
22
+ } from "@zhushanwen/extension-protocol";
23
23
 
24
24
  /** 状态机与 reason 枚举 / 持久化条目 / active·terminal 判定:契约 SSOT re-export。 */
25
25
  export type {
26
26
  BackgroundTaskRegistryEntry as RegistryEntry,
27
27
  BackgroundTaskState,
28
28
  BackgroundTaskEndReason,
29
- } from "@taiji/extension-protocol";
29
+ } from "@zhushanwen/extension-protocol";
30
30
  export {
31
31
  isActiveBackgroundTaskState as isActiveState,
32
32
  isTerminalBackgroundTaskState as isTerminalState,
33
- } from "@taiji/extension-protocol";
33
+ } from "@zhushanwen/extension-protocol";
34
34
 
35
35
  /**
36
36
  * killing intent:bash_kill / 后台 timeout 已发令、轮询器 exit 边沿未确认的瞬态标记。
@@ -18,7 +18,7 @@
18
18
 
19
19
  import type { AgentToolResult, ExtensionContext } from "@earendil-works/pi-coding-agent";
20
20
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
21
- import { getProcessStartTimeSec, isPidAlive, killProcessTree } from "@taiji/extension-protocol/background-task";
21
+ import { getProcessStartTimeSec, isPidAlive, killProcessTree } from "@zhushanwen/extension-protocol/background-task";
22
22
  import { Type } from "typebox";
23
23
 
24
24
  import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
@@ -14,7 +14,7 @@
14
14
 
15
15
  import type { AgentToolResult, ExtensionContext } from "@earendil-works/pi-coding-agent";
16
16
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
17
- import { readOutputTail } from "@taiji/extension-protocol/background-task";
17
+ import { readOutputTail } from "@zhushanwen/extension-protocol/background-task";
18
18
  import { Type } from "typebox";
19
19
 
20
20
  import { TAIL_MAX_BYTES, TAIL_MAX_LINES } from "./background/output-tail.ts";
@@ -32,6 +32,7 @@ const JSON_INDENT = 2;
32
32
 
33
33
  const BASH_OUTPUT_DESCRIPTION = [
34
34
  "Fetch output and status of a background bash task started with bash {background:true}.",
35
+ "On-demand checks only: completion is auto-notified (steer wakes your next turn), so DO NOT bash sleep or call this tool in a wait loop.",
35
36
  "Provide task_id to get task detail: state (running|killing|exited|orphaned), exitCode, reason, duration and tail output (last 2000 lines / 50KB).",
36
37
  'reason "killed" means the task was terminated by the user from the UI — do not restart the command unless explicitly asked.',
37
38
  'reason "orphaned" means the owner process died before the task finished — the task is no longer supervised.',
package/src/bash-tool.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * bash 工具同名 override 定义(设计文档 docs/design/base-tool-enhance.md §3.2 方案 B / §3.5)。
2
+ * bash 工具同名 override 定义。
3
3
  *
4
4
  * 前台行为 100% 委托 pi 官方工厂 createBashToolDefinition——override 后工具的全部
5
5
  * 行为归本包负责,前台语义必须跟随 pi 版本升级而不是自研复刻(自研 spawn 会与
@@ -54,6 +54,9 @@ const enhancedBashSchema = Type.Object({
54
54
  * 1. background:true 语义(立即返回 task_id,bash_output 查询 / bash_kill 终止)
55
55
  * 2. 白名单命中命令自动转后台(即使未要求 background)
56
56
  * 3. timeout 单位秒、显式填写会被尊重(唯一例外:白名单强转后台时忽略,D13)
57
+ * 4. 自动通知契约(统一话术 "DO NOT bash sleep"):任务完成 steer 自动唤醒新
58
+ * turn(notify.ts),LLM 文案必须传达「不要 sleep 空等」——实测大量 agent
59
+ * 启动后台任务后跑 bash sleep 轮询空等,机制在而契约不在所致
57
60
  *
58
61
  * bash_output / bash_kill 工具由 M2 单元注册,description 先行提及(D2/§3.5 要求)。
59
62
  */
@@ -62,10 +65,11 @@ const ENHANCED_BASH_DESCRIPTION = [
62
65
  "Output is truncated to last 2000 lines or 50KB (whichever is hit first). If truncated, full output is saved to a temp file.",
63
66
  "Optionally provide a timeout in seconds; an explicit timeout is respected, except when a whitelisted long-running command is force-routed to background. If no timeout is given, a configured default timeout applies when set.",
64
67
  "",
65
- "Background mode: set background: true to start the command without waiting for it. The tool returns immediately with a task_id, the pid, and an output file path, and you can continue other work while it runs.",
66
- "Poll progress and fetch output with bash_output {task_id} (omit task_id to list known background tasks); terminate a task with bash_kill {task_id}.",
68
+ "Background mode: set background: true to start the command without waiting for it. The tool returns immediately with a task_id, the pid, and an output file path.",
69
+ "Completion is auto-notified: when the task finishes, a steer message with its output tail wakes your next turn — DO NOT bash sleep (e.g. run `sleep 30` in another bash call) or busy-wait while it runs. Do useful non-overlapping work, otherwise just stop and let the notification arrive.",
70
+ "bash_output {task_id} is for on-demand progress checks only (omit task_id to list known background tasks); never use it as a wait loop. Terminate a task with bash_kill {task_id}.",
67
71
  "",
68
- "Commands matching the force-background whitelist (test suites, dev servers, watch jobs and similar long-running commands) are automatically routed to background even when background was not requested; in that case the result carries a task_id to poll.",
72
+ "Commands matching the force-background whitelist (test suites, dev servers, watch jobs and similar long-running commands) are automatically routed to background even when background was not requested; the result carries a task_id and completion auto-notifies — the same DO NOT bash sleep rule applies.",
69
73
  ].join("\n");
70
74
 
71
75
  /**
@@ -101,7 +105,7 @@ function startBackgroundAndReply(
101
105
  `task_id: ${task.taskId} pid: ${task.pid}`,
102
106
  `Output file: ${task.outputFile}`,
103
107
  ...extraNotes,
104
- `Poll with bash_output {task_id:"${task.taskId}"} or omit task_id to list all tasks; terminate with bash_kill {task_id:"${task.taskId}"}.`,
108
+ `Completion is auto-notified — DO NOT bash sleep while waiting. On-demand check: bash_output {task_id:"${task.taskId}"} (or omit task_id to list all tasks); terminate with bash_kill {task_id:"${task.taskId}"}.`,
105
109
  ].join("\n"),
106
110
  },
107
111
  ],