@henryqw/pi-subagent 16.0.1 → 16.0.2

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 CHANGED
@@ -21,6 +21,7 @@ Run `/task-models` and configure the `fast` profile before delegating. Open `/ta
21
21
  | --- | --- | --- |
22
22
  | [`@henryqw/pi-orchestrator`](https://pi.henry.wang/extensions/pi-orchestrator) | Consumer | Owns durable checked local implementation graphs. |
23
23
  | [`@henryqw/pi-task-models`](https://pi.henry.wang/extensions/pi-task-models) | Required | Supplies `fast`, `balanced`, `frontier`, and `fav` model routes. |
24
+ | [`@henryqw/pi-process`](https://pi.henry.wang/packages/pi-process) | Required | Runs bounded captured Git commands. |
24
25
 
25
26
  Routes come from `~/.pi/agent/config/pi-task-models/config.json`. It stores explicit task overrides. Missing shared model config warns once because delegation needs a route.
26
27
 
@@ -0,0 +1,8 @@
1
+ export type GitResult = {
2
+ code: number;
3
+ stdout: string;
4
+ stderr: string;
5
+ };
6
+ export type GitRunner = (args: string[], cwd: string, signal?: AbortSignal) => Promise<GitResult>;
7
+ /** Run one ordinary Git command with bounded output; transport failures never throw. */
8
+ export declare const runGit: GitRunner;
@@ -0,0 +1,32 @@
1
+ import { spawnBounded } from "@henryqw/pi-process";
2
+ const GIT_TIMEOUT_MS = 30_000;
3
+ const GIT_OUTPUT_LIMIT_BYTES = 1 * 1024 * 1024;
4
+ const GIT_DIAGNOSTIC_LIMIT = 200;
5
+ /** Run one ordinary Git command with bounded output; transport failures never throw. */
6
+ export const runGit = async (args, cwd, signal) => {
7
+ try {
8
+ const result = await spawnBounded("git", ["--no-pager", ...args], {
9
+ cwd,
10
+ signal,
11
+ timeoutMs: GIT_TIMEOUT_MS,
12
+ stdoutLimitBytes: GIT_OUTPUT_LIMIT_BYTES,
13
+ stderrLimitBytes: GIT_OUTPUT_LIMIT_BYTES,
14
+ });
15
+ if (result.killed) {
16
+ return {
17
+ code: -1,
18
+ stdout: "",
19
+ stderr: result.stderr.trim().slice(0, GIT_DIAGNOSTIC_LIMIT) || "git was killed",
20
+ };
21
+ }
22
+ return { code: result.code, stdout: result.stdout, stderr: result.stderr };
23
+ }
24
+ catch (error) {
25
+ const reason = error instanceof Error ? error.message : String(error);
26
+ return {
27
+ code: -1,
28
+ stdout: "",
29
+ stderr: reason.slice(0, GIT_DIAGNOSTIC_LIMIT) || "git execution failed",
30
+ };
31
+ }
32
+ };
@@ -1,28 +1,20 @@
1
- import { execFile, spawn } from "node:child_process";
1
+ import { spawn } from "node:child_process";
2
2
  import fs from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { isAbsolute, join } from "node:path";
5
5
  import { inspectIndexFlags } from "./worktree.js";
6
+ import { runGit as runCapturedGit } from "./git-process.js";
6
7
  const GIT_TIMEOUT_MS = 30_000;
7
8
  export const REVIEW_MAX_PATHS = 1_000;
8
9
  export const REVIEW_MAX_PATCH_BYTES = 512 * 1024;
9
10
  const STDERR_LIMIT = 200;
10
- const git = (args, cwd, signal) => new Promise((resolve) => {
11
- execFile("git", ["--no-pager", ...args], { cwd, signal, timeout: GIT_TIMEOUT_MS }, (error, stdout, stderr) => {
12
- resolve({
13
- code: error ? (typeof error.code === "number" ? error.code : -1) : 0,
14
- stdout: String(stdout),
15
- stderr: String(stderr).slice(0, STDERR_LIMIT),
16
- });
17
- });
18
- });
19
11
  function failure(args, result) {
20
- const detail = result.stderr.trim();
12
+ const detail = result.stderr.slice(0, STDERR_LIMIT).trim();
21
13
  return new Error(`git ${args.join(" ")} failed with exit ${result.code}${detail ? `: ${detail}` : ""}`);
22
14
  }
23
15
  async function runGit(args, cwd, signal) {
24
16
  signal?.throwIfAborted();
25
- const result = await git(args, cwd, signal);
17
+ const result = await runCapturedGit(args, cwd, signal);
26
18
  signal?.throwIfAborted();
27
19
  if (result.code !== 0)
28
20
  throw failure(args, result);
@@ -126,7 +118,7 @@ async function assertCleanRegisteredWorktree(worktree, tip, signal) {
126
118
  if (await runGit(["status", "--porcelain=v1", "--untracked-files=all", "--ignore-submodules=none"], worktree, signal)) {
127
119
  throw new Error("Review evidence worktree is not clean.");
128
120
  }
129
- const flags = await inspectIndexFlags(worktree, git, signal);
121
+ const flags = await inspectIndexFlags(worktree, runCapturedGit, signal);
130
122
  if (flags.failure)
131
123
  throw new Error(`Review evidence index inspection failed: ${flags.failure}`);
132
124
  if (flags.hidden)
@@ -1,3 +1,4 @@
1
+ import { type GitRunner } from "./git-process.ts";
1
2
  export interface WorktreeInfo {
2
3
  path: string;
3
4
  cwd: string;
@@ -23,11 +24,6 @@ export type WorktreePayload = {
23
24
  commits?: number;
24
25
  dirty?: boolean;
25
26
  };
26
- export type GitRunner = (args: string[], cwd: string, signal?: AbortSignal) => Promise<{
27
- code: number;
28
- stdout: string;
29
- stderr: string;
30
- }>;
31
27
  export declare class WorktreeSetupError extends Error {
32
28
  name: string;
33
29
  readonly worktree: WorktreeInfo;
package/dist/worktree.js CHANGED
@@ -1,9 +1,8 @@
1
- import { execFile } from "node:child_process";
2
1
  import { createHash } from "node:crypto";
3
2
  import { existsSync, lstatSync } from "node:fs";
4
3
  import { appendFile, mkdir, readFile } from "node:fs/promises";
5
4
  import { dirname, isAbsolute, join, resolve } from "node:path";
6
- const GIT_TIMEOUT_MS = 30_000;
5
+ import { runGit } from "./git-process.js";
7
6
  const WORKTREES_DIRNAME = ".worktrees";
8
7
  const BRANCH_NAMESPACE = "pi-subagent";
9
8
  export class WorktreeSetupError extends Error {
@@ -14,16 +13,6 @@ export class WorktreeSetupError extends Error {
14
13
  this.worktree = worktree;
15
14
  }
16
15
  }
17
- /** Runs git, capturing output; never throws on non-zero exit or spawn failure. */
18
- const runGit = (args, cwd, signal) => new Promise((resolve) => {
19
- execFile("git", args, { cwd, timeout: GIT_TIMEOUT_MS, signal }, (error, stdout, stderr) => {
20
- resolve({
21
- code: error ? (typeof error.code === "number" ? error.code : -1) : 0,
22
- stdout: String(stdout),
23
- stderr: String(stderr),
24
- });
25
- });
26
- });
27
16
  const sanitizeShortId = (childId) => createHash("sha256").update(childId).digest("hex").slice(0, 24);
28
17
  const stripGitLineEnd = (value) => value.replace(/\r?\n$/, "");
29
18
  function hasRepositoryMarker(cwd) {
@@ -71,10 +60,15 @@ async function ensureLocalExclude(gitDir) {
71
60
  */
72
61
  export async function createChildWorktree(cwd, childId, run = runGit, signal, onPrepared) {
73
62
  const root = await run(["rev-parse", "--show-toplevel"], cwd, signal);
63
+ signal?.throwIfAborted();
64
+ if (root.code === -1)
65
+ throw new Error(`git rev-parse failed (${root.stderr.trim().slice(0, 200)})`);
74
66
  if (root.code !== 0) {
75
67
  signal?.throwIfAborted();
76
68
  const repository = await run(["-c", "safe.directory=*", "rev-parse", "--show-toplevel"], cwd, signal);
77
69
  signal?.throwIfAborted();
70
+ if (repository.code === -1)
71
+ throw new Error(`git rev-parse failed (${repository.stderr.trim().slice(0, 200)})`);
78
72
  if (repository.code !== 0 && !hasRepositoryMarker(cwd) && !process.env.GIT_DIR && !process.env.GIT_WORK_TREE)
79
73
  return undefined;
80
74
  throw new Error(`git rev-parse failed (${root.stderr.trim().slice(0, 200)})`); // dubious ownership, timeout, …
@@ -87,6 +81,9 @@ export async function createChildWorktree(cwd, childId, run = runGit, signal, on
87
81
  throw new Error(`git rev-parse --show-prefix failed (${prefix.stderr.trim().slice(0, 200)})`);
88
82
  const relativeCwd = stripGitLineEnd(prefix.stdout);
89
83
  const base = await run(["rev-parse", "HEAD"], repoRoot, signal);
84
+ signal?.throwIfAborted();
85
+ if (base.code === -1)
86
+ throw new Error(`git rev-parse HEAD failed (${base.stderr.trim().slice(0, 200)})`);
90
87
  if (base.code !== 0) {
91
88
  signal?.throwIfAborted();
92
89
  const head = await run(["symbolic-ref", "--quiet", "HEAD"], repoRoot, signal);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-subagent",
3
- "version": "16.0.1",
3
+ "version": "16.0.2",
4
4
  "description": "Delegate bounded single, parallel, or chained tasks to isolated Pi roles.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -63,6 +63,7 @@
63
63
  "dependencies": {
64
64
  "@henryqw/pi-config-store": "^1.0.0",
65
65
  "@henryqw/pi-multi-codex": "^1.0.0",
66
+ "@henryqw/pi-process": "^0.1.0",
66
67
  "@henryqw/pi-task-models": "^5.0.0"
67
68
  }
68
69
  }