@zhushanwen/pi-subagent-workflow 7.3.2 → 7.3.4

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-subagent-workflow",
3
- "version": "7.3.2",
3
+ "version": "7.3.4",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "description": "Unified subagent execution and multi-agent workflow orchestration for Pi — spawned-process agent runtime with sync/background modes, stateful workflow management with persistence, state machine, and execution tracing.",
@@ -44,14 +44,14 @@
44
44
  "@xyz-agent/extension-protocol": "^0.4.0",
45
45
  "ajv": "^8.20.0",
46
46
  "yaml": "^2.9.0",
47
- "@zhushanwen/pi-extension-logger": "0.2.0"
47
+ "@zhushanwen/pi-extension-logger": "0.2.1"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "@earendil-works/pi-ai": "*",
51
51
  "@earendil-works/pi-coding-agent": "*",
52
52
  "@earendil-works/pi-tui": "*",
53
53
  "typebox": "*",
54
- "@zhushanwen/pi-pending-notifications": "0.3.1",
54
+ "@zhushanwen/pi-pending-notifications": "0.3.2",
55
55
  "@zhushanwen/pi-structured-output": "5.0.1"
56
56
  },
57
57
  "peerDependenciesMeta": {
@@ -764,6 +764,26 @@ export async function runSpawn(
764
764
  // abortRunningControllers 跳过它,靠本 Set 兜底)。close/error 后移除(已退出无需再 kill)。
765
765
  spawnedChildren.add(child);
766
766
 
767
+ // [worktree-reaper-fix] 同步补全注册表 pid:spawn 返回后 child.pid 立即可得(Node.js
768
+ // 同步属性),无需等任何 stdout 事件。原补全点挂在 header 分支(下方 stdout handler 内),
769
+ // 而 RPC mode(buildSpawnArgs 固定 --mode rpc)不输出 header 行——pid 恒为 0,超
770
+ // SPAWN_GRACE_MS 后被 reaper 当孤儿误删活 worktree(2026-08-11 cw 递归编排整树失活事故)。
771
+ // header 分支调用保留:json mode 回切时仍能补全,updatePid 同 branch 覆盖写幂等,无副作用。
772
+ // [S1] 防御:必须放在 spawnedChildren.add 之后(onWorktreePid 抛错时子进程已被跟踪,
773
+ // dispose 兜底 kill 不会泄漏),且包 try/catch(补全失败不阻断 spawn 主流程——
774
+ // 注册表写失败最坏后果是条目停留 pid=0,由 reaper 宽限回收兜底)。
775
+ if (opts.worktree && child.pid) {
776
+ try {
777
+ ctx.onWorktreePid?.(opts.worktree.branch, child.pid);
778
+ } catch (err) {
779
+ logger.warn("[worktree] worktree pid registration failed (defensive)", {
780
+ branch: opts.worktree.branch,
781
+ pid: child.pid,
782
+ err: err instanceof Error ? err.message : String(err),
783
+ });
784
+ }
785
+ }
786
+
767
787
  // stdout/stderr 用 utf8 编码:stream 自动按字符边界切分,避免多字节
768
788
  // UTF-8(CJK/emoji)跨 chunk 时 toString() 产生 U+FFFD 替换符导致 JSON.parse 失败。
769
789
  // [m2] 先 setEncoding 再注册 signal listener/watchdog:若 setEncoding 抛错,try/finally
@@ -997,9 +1017,12 @@ export async function runSpawn(
997
1017
  // [worktree-reaper-fix] 拼 spawnCwd 进错误消息:ENOENT 的 err.message 只含 command 名,
998
1018
  // 无 cwd 线索(worktree 被 reaper 误删后 cwd 指向虚空)会导致误诊——2026-08-11 事故
999
1019
  // AI 误判"node 被卸载"的直接原因。
1020
+ // [S3] code 读取带运行时 guard:非 ErrnoException(普通 Error)时 code 为 undefined,
1021
+ // 不加 cwd hint(行为与修复前一致);仅 ENOENT 才拼 cwd。
1000
1022
  spawnedChildren.delete(child);
1001
1023
  const errno = err as NodeJS.ErrnoException;
1002
- const cwdHint = errno.code === "ENOENT" ? ` (cwd: ${spawnCwd})` : "";
1024
+ const errCode = "code" in err ? errno.code : undefined;
1025
+ const cwdHint = errCode === "ENOENT" ? ` (cwd: ${spawnCwd})` : "";
1003
1026
  record.lastError = `${err.message}${cwdHint}`;
1004
1027
  resolve(SIGNAL_EXIT_CODE_THRESHOLD); // 非零退出
1005
1028
  });
@@ -351,7 +351,7 @@ export class SubagentService {
351
351
  // 基线是 createRecordForMode / 护栏读 ALS store 失败时的权威回退。
352
352
  this.execCtxBaseline = { recordId: envSelfRecord, depth: nestingDepth };
353
353
  this.execCtxAls.enterWith({ recordId: envSelfRecord, depth: nestingDepth });
354
- if (process.env.PI_EXT_DEBUG) {
354
+ if (process.env.XYZ_AGENT_DEBUG) {
355
355
  logger.debug(
356
356
  `[subagents] execCtxAls initialized: recordId=${envSelfRecord} depth=${nestingDepth} rootSessionId=${envRoot ?? init.sessionId}`,
357
357
  );
package/src/index.ts CHANGED
@@ -14,7 +14,6 @@
14
14
  */
15
15
 
16
16
  import * as fs from "node:fs";
17
- import * as os from "node:os";
18
17
  import * as path from "node:path";
19
18
 
20
19
  import type { ExtensionAPI, ExtensionContext, SessionShutdownEvent, SessionStartEvent, SessionTreeEvent } from "@earendil-works/pi-coding-agent";
@@ -166,9 +165,10 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
166
165
  }
167
166
 
168
167
  function resolveSessionDir(): string {
169
- const defaultDir = path.join(os.homedir(), ".pi", "agent");
168
+ const defaultDir = getAgentDir();
170
169
  const sessionSlug = `--${process.cwd().replace(/^\//, "").replace(/\//g, "-")}--`;
171
- const sessionScopedDir = path.join(os.homedir(), ".pi", "agent", "sessions", sessionSlug);
170
+ // F2:根改 getAgentDir() 派生(实例隔离);保留 sessionScopedDir 存在则用之的探测语义
171
+ const sessionScopedDir = path.join(getAgentDir(), "sessions", sessionSlug);
172
172
  return fs.existsSync(sessionScopedDir) ? sessionScopedDir : defaultDir;
173
173
  }
174
174
 
@@ -310,6 +310,7 @@ export function formatToolCall(
310
310
  theme: ThemeLike,
311
311
  ): string {
312
312
  const shortenPath = (p: string): string => {
313
+ // 仅用于显示层路径缩写(~ 替换 home 前缀),不读取 pi 目录(TC9 合法命中)
313
314
  const home = os.homedir();
314
315
  return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
315
316
  };
@@ -242,7 +242,7 @@ A subagent MAY call the \`subagent\` tool itself (each level spawns its own chil
242
242
 
243
243
  // ponytail: renderCall 每次 TUI invalidate 都触发。streaming 中 args 是 partial JSON
244
244
  // 解析结果(如 model="deep" 来自未流完的 "deepseek-router/ds-pro"),解析失败是预期。
245
- // 不走 appendEntry(非真实错误),只走 logger.debug(默认 no-op,PI_EXT_DEBUG=1 写文件)。
245
+ // 不走 appendEntry(非真实错误),只走 logger.debug(默认 no-op,XYZ_AGENT_DEBUG=1 写文件)。
246
246
  const renderCallLogger = getLogger("subagents");
247
247
 
248
248
  const subagentRenderCall: SubagentRenderCallCb = (args, theme, ctx) => {
@@ -262,7 +262,7 @@ const subagentRenderCall: SubagentRenderCallCb = (args, theme, ctx) => {
262
262
  if (r) resolved = { model: `${r.model.provider}/${r.model.id}`, thinkingLevel: r.thinkingLevel };
263
263
  } catch (err) {
264
264
  // streaming 中间态(partial JSON)或 service 未就绪 → 降级不显示 model(renderCall 不应崩)。
265
- // 不阻断渲染,不污染 TUI。开发期开 PI_EXT_DEBUG=1 可写文件日志排查。
265
+ // 不阻断渲染,不污染 TUI。开发期开 XYZ_AGENT_DEBUG=1 可写文件日志排查。
266
266
  renderCallLogger.debug("renderCall model resolution failed, degrading", {
267
267
  reason: err instanceof Error ? err.message : String(err),
268
268
  });
@@ -23,10 +23,10 @@
23
23
  */
24
24
 
25
25
  import { promises as fsPromises } from "node:fs";
26
- import { homedir } from "node:os";
27
26
  import { join as pathJoin } from "node:path";
28
27
 
29
28
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
29
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
30
30
  import { Key, matchesKey } from "@earendil-works/pi-tui";
31
31
 
32
32
  import {
@@ -858,11 +858,11 @@ const TRACE_ACTIVITY_WIDTH = 80;
858
858
 
859
859
  /**
860
860
  * 导出完整 workflow trace 到 Markdown 文件。
861
- * 路径:~/.pi/agent/workflow-traces/{runId}.md
861
+ * 路径:<agentDir>/workflow-traces/{runId}.md(agentDir = getAgentDir(),实例隔离)
862
862
  * 对齐 main 的 saveTraceToFile(WorkflowsView.ts:365-396)。
863
863
  */
864
864
  function saveTraceToFile(run: WorkflowRun, ctx: ExtensionContext): void {
865
- const dir = pathJoin(homedir(), ".pi", "agent", "workflow-traces");
865
+ const dir = pathJoin(getAgentDir(), "workflow-traces");
866
866
  const filePath = pathJoin(dir, `${run.runId}.md`);
867
867
  const lines: string[] = [];
868
868
  lines.push(`# Workflow Trace: ${run.spec.scriptName} (${run.runId})`, "");
@@ -6,9 +6,10 @@
6
6
  */
7
7
 
8
8
  import * as fs from "node:fs";
9
- import * as os from "node:os";
10
9
  import * as path from "node:path";
11
10
 
11
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
12
+
12
13
  // ── Skill path resolution (with npm dir cache) ─────────────────────
13
14
 
14
15
  const skillCandidatesCache = new Map<string, string[]>();
@@ -32,8 +33,9 @@ function getNpmSkillCandidates(npmSkillsDir: string): string[] {
32
33
  * Resolve a skill name to its directory or SKILL.md path.
33
34
  * Search order:
34
35
  * 1. Project-level: .agents/skills/<name>/
35
- * 2. Global: ~/.pi/agent/skills/<name>/
36
- * 3. npm packages: ~/.pi/agent/npm/node_modules/<pkg>/skills/<name>/
36
+ * 2. Global: <agentDir>/skills/<name>/(agentDir = getAgentDir(),实例隔离:
37
+ * PI_CODING_AGENT_DIR 场景读隔离目录,不碰 ~/.pi/agent
38
+ * 3. npm packages: <agentDir>/npm/node_modules/<pkg>/skills/<name>/
37
39
  * Returns the directory path if found, undefined otherwise.
38
40
  */
39
41
  export function resolveSkillPath(skillName: string): string | undefined {
@@ -41,11 +43,11 @@ export function resolveSkillPath(skillName: string): string | undefined {
41
43
  // Project-level
42
44
  path.resolve(process.cwd(), ".agents/skills", skillName),
43
45
  // Global user skills
44
- path.join(os.homedir(), ".pi/agent/skills", skillName),
46
+ path.join(getAgentDir(), "skills", skillName),
45
47
  ];
46
48
 
47
49
  // npm package skills (cached)
48
- const npmSkillsDir = path.join(os.homedir(), ".pi/agent/npm/node_modules");
50
+ const npmSkillsDir = path.join(getAgentDir(), "npm/node_modules");
49
51
  for (const pkgSkillsBase of getNpmSkillCandidates(npmSkillsDir)) {
50
52
  candidates.push(path.join(pkgSkillsBase, skillName));
51
53
  }