@ferris1225/pi-subagents 4.3.4 → 4.3.5

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +59 -66
  3. package/agents/artisan.md +0 -1
  4. package/agents/steward.md +1 -2
  5. package/{src/index.ts → index.ts} +19 -19
  6. package/package.json +4 -3
  7. package/src/{config.ts → configuration/config.ts} +19 -24
  8. package/src/configuration/setup.ts +375 -0
  9. package/src/configuration/ui.ts +245 -0
  10. package/src/{agents.ts → delegation/agents.ts} +3 -3
  11. package/src/{dispatch.ts → delegation/dispatch.ts} +12 -18
  12. package/src/{prompt.ts → delegation/prompt.ts} +3 -8
  13. package/src/{background.ts → execution/background.ts} +3 -6
  14. package/src/execution/rpc-control.ts +200 -0
  15. package/src/{rpc-run.ts → execution/rpc-run.ts} +11 -199
  16. package/src/{session-fork.ts → execution/session-fork.ts} +1 -1
  17. package/src/{spawn.ts → execution/spawn.ts} +10 -8
  18. package/src/isolation/git-command.ts +147 -0
  19. package/src/{recovery.ts → isolation/recovery.ts} +1 -1
  20. package/src/{worktree.ts → isolation/worktree.ts} +10 -147
  21. package/src/{completion.ts → lifecycle/completion.ts} +2 -2
  22. package/src/{durable.ts → lifecycle/durable.ts} +3 -3
  23. package/src/{runtime.ts → lifecycle/runtime.ts} +7 -7
  24. package/src/{thread-lifecycle.ts → lifecycle/thread-lifecycle.ts} +25 -519
  25. package/src/lifecycle/thread-restore.ts +250 -0
  26. package/src/lifecycle/thread-shared.ts +269 -0
  27. package/src/{tools.ts → lifecycle/tools.ts} +8 -8
  28. package/src/{announcements.ts → presentation/announcements.ts} +4 -4
  29. package/src/{format.ts → presentation/format.ts} +3 -3
  30. package/src/{monitor.ts → presentation/monitor.ts} +2 -2
  31. package/src/{widget.ts → presentation/widget.ts} +1 -1
  32. package/agents/sentinel.md +0 -16
  33. package/src/setup.ts +0 -344
  34. package/src/ui.ts +0 -160
  35. /package/src/{models.ts → configuration/models.ts} +0 -0
  36. /package/src/{temp-hygiene.ts → isolation/temp-hygiene.ts} +0 -0
  37. /package/src/{status.ts → presentation/status.ts} +0 -0
@@ -8,10 +8,17 @@
8
8
  * Failed integration deliberately retains both the worktree and patch.
9
9
  */
10
10
 
11
- import { spawn, type ChildProcess } from "node:child_process";
12
11
  import { existsSync, symlinkSync } from "node:fs";
13
12
  import { copyFile, mkdir, mkdtemp, realpath, rm, stat, writeFile } from "node:fs/promises";
14
13
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
14
+ import {
15
+ GIT_COMMAND_TIMEOUT_MS,
16
+ GIT_OUTPUT_MAX_BYTES,
17
+ runCommand,
18
+ type CommandResult,
19
+ type CommandRunner,
20
+ WORKTREE_PATCH_MAX_BYTES,
21
+ } from "./git-command.ts";
15
22
  import { writeTempOwnerMarker } from "./temp-hygiene.ts";
16
23
 
17
24
  export type IsolationMode = "shared" | "worktree";
@@ -28,151 +35,6 @@ export function worktreeGroupId(worktree: Pick<WorktreeIsolation, "tempDir">): s
28
35
  : base;
29
36
  }
30
37
 
31
- export interface CommandRunOptions {
32
- cwd: string;
33
- input?: Buffer;
34
- signal?: AbortSignal;
35
- timeoutMs?: number;
36
- maxOutputBytes?: number;
37
- /** Extra environment entries merged over the inherited environment. */
38
- env?: Record<string, string>;
39
- }
40
-
41
- export interface CommandResult {
42
- code: number;
43
- stdout: Buffer;
44
- stderr: Buffer;
45
- }
46
-
47
- /** Injectable, shell-free command runner used by every Git operation. */
48
- export type CommandRunner = (
49
- command: string,
50
- args: readonly string[],
51
- options: CommandRunOptions,
52
- ) => Promise<CommandResult>;
53
-
54
- export const GIT_COMMAND_TIMEOUT_MS = 120_000;
55
- export const GIT_COMMAND_KILL_GRACE_MS = 2_000;
56
- export const GIT_OUTPUT_MAX_BYTES = 64 * 1024 * 1024;
57
- export const WORKTREE_PATCH_MAX_BYTES = GIT_OUTPUT_MAX_BYTES;
58
-
59
- /** Git apply validates and writes in one process. The repository lane
60
- * (thread-lifecycle) already serializes every finalize against all writers of
61
- * the same canonical checkout, so applies never race each other here. */
62
- function terminateCommandTree(child: ChildProcess, force: boolean, processGroup: boolean): void {
63
- if (process.platform === "win32" && child.pid !== undefined) {
64
- const fallback = (): void => {
65
- try {
66
- child.kill(force ? "SIGKILL" : "SIGTERM");
67
- } catch {
68
- /* process may already be gone */
69
- }
70
- };
71
- const killer = spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], {
72
- stdio: "ignore",
73
- windowsHide: true,
74
- });
75
- killer.once("error", fallback);
76
- killer.once("close", (code) => {
77
- if (code !== 0) fallback();
78
- });
79
- return;
80
- }
81
- try {
82
- if (processGroup && child.pid !== undefined) {
83
- process.kill(-child.pid, force ? "SIGKILL" : "SIGTERM");
84
- } else {
85
- child.kill(force ? "SIGKILL" : "SIGTERM");
86
- }
87
- } catch {
88
- /* process may already be gone */
89
- }
90
- }
91
-
92
- /** Default argument-safe runner. Output is bounded before binary patches enter
93
- * memory, and timeout/abort terminates the complete checkout-filter process tree. */
94
- export const runCommand: CommandRunner = (command, args, options) =>
95
- new Promise<CommandResult>((resolveResult, reject) => {
96
- if (options.signal?.aborted) {
97
- reject(new Error(`Command aborted before start: ${command}`));
98
- return;
99
- }
100
- const usePosixProcessGroup = process.platform !== "win32";
101
- const child = spawn(command, [...args], {
102
- cwd: options.cwd,
103
- shell: false,
104
- windowsHide: true,
105
- stdio: ["pipe", "pipe", "pipe"],
106
- detached: usePosixProcessGroup,
107
- ...(options.env ? { env: { ...process.env, ...options.env } } : {}),
108
- });
109
- const stdout: Buffer[] = [];
110
- const stderr: Buffer[] = [];
111
- const maxOutputBytes = options.maxOutputBytes ?? GIT_OUTPUT_MAX_BYTES;
112
- let outputBytes = 0;
113
- let finished = false;
114
- let failure: Error | undefined;
115
- let timeout: ReturnType<typeof setTimeout> | undefined;
116
- let forceKillTimer: ReturnType<typeof setTimeout> | undefined;
117
-
118
- const terminate = (): void => {
119
- terminateCommandTree(child, false, usePosixProcessGroup);
120
- if (!forceKillTimer) {
121
- forceKillTimer = setTimeout(
122
- () => terminateCommandTree(child, true, usePosixProcessGroup),
123
- GIT_COMMAND_KILL_GRACE_MS,
124
- );
125
- if (typeof forceKillTimer.unref === "function") forceKillTimer.unref();
126
- }
127
- };
128
- const fail = (error: Error): void => {
129
- if (failure || finished) return;
130
- failure = error;
131
- terminate();
132
- };
133
- const append = (target: Buffer[], chunk: Buffer | string): void => {
134
- if (failure || finished) return;
135
- const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
136
- outputBytes += value.length;
137
- if (outputBytes > maxOutputBytes) {
138
- fail(new Error(`Command output exceeded ${maxOutputBytes} bytes: ${command}`));
139
- return;
140
- }
141
- target.push(value);
142
- };
143
- const onAbort = (): void => fail(new Error(`Command aborted: ${command}`));
144
- options.signal?.addEventListener("abort", onAbort, { once: true });
145
- if (options.timeoutMs !== undefined && options.timeoutMs > 0) {
146
- timeout = setTimeout(
147
- () => fail(new Error(`Command timed out after ${options.timeoutMs}ms: ${command}`)),
148
- options.timeoutMs,
149
- );
150
- if (typeof timeout.unref === "function") timeout.unref();
151
- }
152
-
153
- child.stdout?.on("data", (chunk: Buffer | string) => append(stdout, chunk));
154
- child.stderr?.on("data", (chunk: Buffer | string) => append(stderr, chunk));
155
- child.once("error", (error) => fail(error));
156
- child.once("close", (code) => {
157
- if (finished) return;
158
- finished = true;
159
- if (timeout) clearTimeout(timeout);
160
- if (forceKillTimer) clearTimeout(forceKillTimer);
161
- options.signal?.removeEventListener("abort", onAbort);
162
- if (failure) {
163
- reject(failure);
164
- return;
165
- }
166
- resolveResult({
167
- code: code ?? 1,
168
- stdout: Buffer.concat(stdout),
169
- stderr: Buffer.concat(stderr),
170
- });
171
- });
172
- child.stdin?.once("error", () => undefined);
173
- child.stdin?.end(options.input);
174
- });
175
-
176
38
  export interface WorktreeTarget {
177
39
  /** Canonical cwd requested by the caller. */
178
40
  originalCwd: string;
@@ -663,7 +525,8 @@ class GitWorktreeIsolation implements WorktreeIsolation {
663
525
  * that index, so everything runs against a private copy of the checkout's
664
526
  * index: the copy first absorbs the current unstaged state (`add -A`),
665
527
  * making the working tree "ours" of the merge, and the user's real staged
666
- * state is never touched. */
528
+ * state is never touched. The repository lane serializes finalization, and
529
+ * each `git apply` validates and writes in one process. */
667
530
  private async applyPatchThreeWay(): Promise<boolean> {
668
531
  const indexCopy = join(this.tempDir, "apply-index");
669
532
  try {
@@ -7,8 +7,8 @@
7
7
  * failure directly so it is never delayed.
8
8
  */
9
9
 
10
- import { formatUsageCompact, sumUsage, type RunWaitReason } from "./monitor.ts";
11
- import type { UsageStats } from "./rpc-run.ts";
10
+ import { formatUsageCompact, sumUsage, type RunWaitReason } from "../presentation/monitor.ts";
11
+ import type { UsageStats } from "../execution/rpc-control.ts";
12
12
 
13
13
  export interface CompletionBatchTimings {
14
14
  debounceMs: number;
@@ -18,9 +18,9 @@ import { existsSync, type Dirent, readdirSync, statSync } from "node:fs";
18
18
  import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
19
19
  import { uptime } from "node:os";
20
20
  import { dirname, join } from "node:path";
21
- import type { UsageStats } from "./rpc-run.ts";
21
+ import type { UsageStats } from "../execution/rpc-control.ts";
22
22
  import type { SubagentThread } from "./runtime.ts";
23
- import { getResultOutput, isFailedResult, getProjectRoot, getSubagentsRoot, type SingleResult } from "./spawn.ts";
23
+ import { getResultOutput, isFailedResult, getProjectRoot, getSubagentsRoot, type SingleResult } from "../execution/spawn.ts";
24
24
  import {
25
25
  isPathInside,
26
26
  restoreWorktreeIsolation,
@@ -28,7 +28,7 @@ import {
28
28
  normalizeWorktreeSnapshot,
29
29
  worktreeSnapshot,
30
30
  type WorktreeSnapshot,
31
- } from "./worktree.ts";
31
+ } from "../isolation/worktree.ts";
32
32
 
33
33
  export const THREADS_MANIFEST_FILE_NAME = "pi-subagents-threads.json";
34
34
  const THREADS_MANIFEST_VERSION = 1;
@@ -10,7 +10,7 @@
10
10
 
11
11
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
12
12
  import { rmSync } from "node:fs";
13
- import { resolveSubagentConcurrency, BackgroundTaskQueue } from "./background.ts";
13
+ import { resolveSubagentConcurrency, BackgroundTaskQueue } from "../execution/background.ts";
14
14
  import {
15
15
  createCompletionBatcher,
16
16
  formatActiveRunsFooter,
@@ -18,13 +18,13 @@ import {
18
18
  type CompletionBatcher,
19
19
  type CompletionMessageItem,
20
20
  } from "./completion.ts";
21
- import { type ThinkingLevel } from "./config.ts";
21
+ import { type ThinkingLevel } from "../configuration/config.ts";
22
22
  import { removeThreadRecord, threadRecordFromThread, upsertThreadRecord, type ThreadRecord } from "./durable.ts";
23
- import { isRunActiveStatus, monitor } from "./monitor.ts";
24
- import type { RpcRunControl } from "./rpc-run.ts";
25
- import type { StartBackgroundInternal } from "./thread-lifecycle.ts";
26
- import { isFailedResult, type SingleResult } from "./spawn.ts";
27
- import type { IsolationMode, WorktreeFinalization, WorktreeIsolation } from "./worktree.ts";
23
+ import { isRunActiveStatus, monitor } from "../presentation/monitor.ts";
24
+ import type { RpcRunControl } from "../execution/rpc-control.ts";
25
+ import type { StartBackgroundInternal } from "./thread-shared.ts";
26
+ import { isFailedResult, type SingleResult } from "../execution/spawn.ts";
27
+ import type { IsolationMode, WorktreeFinalization, WorktreeIsolation } from "../isolation/worktree.ts";
28
28
 
29
29
  export type ThreadState =
30
30
  | "queued"