@henryqw/pi-subagent 22.2.0 → 22.2.1

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.
@@ -1,5 +1,6 @@
1
1
  import { type ExecuteRequest, type HostAllocationIntent, type HostAllocationPlan, type TaskAttempt, type TaskRequest, type WorkspaceIdentity } from "./schema.ts";
2
2
  import { type AllocationReconciliation, type HostAllocationKind, type HostAllocationResult, type HostCleanupKind, type HostRuntime, type InFlightTaskCandidateInspector, type OperationContext, type TextTaskContext, type TransientLaunchHandle, type VerifiedLaunch, type WorkerResult } from "./runner.ts";
3
+ import { type EphemeralSubagentExecutionBudget } from "./ephemeral.ts";
3
4
  export interface HostProcessOptions {
4
5
  cwd: string;
5
6
  signal: AbortSignal;
@@ -21,6 +22,7 @@ export interface HerdrHostRuntimeOptions {
21
22
  env?: NodeJS.ProcessEnv;
22
23
  leaseDirectory?: string;
23
24
  lsofCommand?: string;
25
+ executionBudget?: () => Omit<EphemeralSubagentExecutionBudget, "startedAt">;
24
26
  }
25
27
  export declare function workspaceLabel(attempt: Pick<TaskAttempt, "correlationToken">): string;
26
28
  export declare class HerdrHostRuntime implements HostRuntime {
@@ -34,6 +36,7 @@ export declare class HerdrHostRuntime implements HostRuntime {
34
36
  private readonly env;
35
37
  private readonly leaseDirectory;
36
38
  private readonly lsofCommand;
39
+ private readonly executionBudget?;
37
40
  constructor(options: HerdrHostRuntimeOptions);
38
41
  /** Fail-closed Herdr caller/capability gate. This method creates no state or host resource. */
39
42
  preflightHost(input: {
@@ -8,6 +8,7 @@ import { createHerdrClient, hasHerdrErrorCode, herdrCommandFailure, startPiAgent
8
8
  import { isCleanCommitted, } from "./schema.js";
9
9
  import { buildChangesetTaskPrompt, withTransientLaunch, } from "./runner.js";
10
10
  import { runProcess as defaultRunProcess } from "./process.js";
11
+ import { EXECUTION_BUDGET_ENV } from "./ephemeral.js";
11
12
  const MIN_HERDR_VERSION = [0, 9, 0];
12
13
  const MIN_HERDR_PROTOCOL = 22;
13
14
  const HERDR_OPERATION_CAP_MS = 30_000;
@@ -252,6 +253,7 @@ export class HerdrHostRuntime {
252
253
  env;
253
254
  leaseDirectory;
254
255
  lsofCommand;
256
+ executionBudget;
255
257
  constructor(options) {
256
258
  this.inspectInFlightCandidate = options.inspectInFlightTaskCandidate;
257
259
  this.execute = options.runProcess ?? defaultRunProcess;
@@ -263,6 +265,7 @@ export class HerdrHostRuntime {
263
265
  this.env = options.env ?? process.env;
264
266
  this.leaseDirectory = resolve(options.leaseDirectory ?? join(extensionConfigDir("pi-subagent"), "leases"));
265
267
  this.lsofCommand = options.lsofCommand ?? "lsof";
268
+ this.executionBudget = options.executionBudget;
266
269
  }
267
270
  /** Fail-closed Herdr caller/capability gate. This method creates no state or host resource. */
268
271
  async preflightHost(input, context) {
@@ -741,9 +744,12 @@ export class HerdrHostRuntime {
741
744
  assertWorkerTabIntent(allocation, task, attempt);
742
745
  await this.createPrivateLease(allocation.leasePath, allocation.token);
743
746
  await this.waitForStableWorkspacePanes(allocation.workspaceId, allocation.worktreeCwd, context);
747
+ const budget = this.executionBudget?.();
744
748
  const args = [
745
749
  "tab", "create", "--workspace", allocation.workspaceId, "--cwd", allocation.worktreeCwd,
746
- "--label", allocation.label, "--env", `${PROCESS_LEASE_ENV}=${allocation.leasePath}`, "--no-focus",
750
+ "--label", allocation.label, "--env", `${PROCESS_LEASE_ENV}=${allocation.leasePath}`,
751
+ ...(budget ? ["--env", `${EXECUTION_BUDGET_ENV}=${JSON.stringify({ ...budget, startedAt: this.now() })}`] : []),
752
+ "--no-focus",
747
753
  ];
748
754
  const response = await this.herdr.exec(args, this.processOptions(allocation.worktreeCwd, context, HERDR_OPERATION_CAP_MS));
749
755
  if (response.code !== 0 || response.killed) {
@@ -783,8 +789,9 @@ export class HerdrHostRuntime {
783
789
  return await withTransientLaunch(handle, async (launch) => {
784
790
  if (launch.role !== expectedRole)
785
791
  throw new Error("Agent start acquisition returned the wrong Role.");
786
- if (Object.keys(launch.env).length)
792
+ if (Object.keys(launch.env).some((name) => name !== EXECUTION_BUDGET_ENV || !this.executionBudget)) {
787
793
  throw new Error("Herdr agent launch must not receive caller Role environment variables.");
794
+ }
788
795
  const response = await startPiAgent(this.herdr, {
789
796
  name: allocation.agentName,
790
797
  pane: allocation.paneId,
@@ -827,6 +834,15 @@ export class HerdrHostRuntime {
827
834
  if (lifecycle.status === "unknown") {
828
835
  return { outcome: "unknown", diagnostic: "Delivered prompt lifecycle is unknown." };
829
836
  }
837
+ if (lifecycle.status === "working") {
838
+ try {
839
+ await this.delay(STALLED_PROMPT_POLL_MS, context.signal);
840
+ }
841
+ catch (error) {
842
+ return { outcome: this.contextInterrupted(context) ? "interrupted" : "unknown", diagnostic: `Delivered prompt polling failed: ${safeText(error)}` };
843
+ }
844
+ continue;
845
+ }
830
846
  let inspection;
831
847
  try {
832
848
  inspection = await this.inspectInFlightCandidate({ root: allocation.worktreeCwd, task: input.task, attempt: input.attempt }, this.childContext(context, GIT_INSPECTION_CAP_MS));
@@ -45,12 +45,15 @@ async function sharedHistory(base, tip, cwd, signal) {
45
45
  /** Check helper-derived names even after a safely removed checkout is no longer present. */
46
46
  async function ownershipMetadata(root, info, signal) {
47
47
  const canonical = await realpath(root);
48
- const first = (await requireGit(["worktree", "list", "--porcelain", "-z"], root, signal)).split("\0")[0];
49
- const primary = first.startsWith("worktree ") ? await realpath(first.slice("worktree ".length)) : "";
48
+ const listed = await requireGit(["worktree", "list", "--porcelain", "-z"], root, signal);
49
+ const primary = listed.split("\0", 1)[0];
50
+ if (!primary?.startsWith("worktree "))
51
+ throw new Error("Primary Git worktree is unavailable.");
52
+ const repoRoot = await realpath(primary.slice("worktree ".length));
50
53
  if (await realpath(await requireGit(["rev-parse", "--show-toplevel"], root, signal)) !== canonical
51
- || info.repoRoot !== primary || info.cwd !== info.path
54
+ || info.repoRoot !== repoRoot || info.cwd !== info.path
52
55
  || !/^pi-subagent\/subagent-[0-9a-f]{24}$/.test(info.branch)
53
- || info.path !== join(primary, ".worktrees", basename(info.branch))) {
56
+ || info.path !== join(repoRoot, ".worktrees", basename(info.branch))) {
54
57
  throw new Error("Git worktree is not an exact owned checkout of Main.");
55
58
  }
56
59
  }
package/dist/runner.js CHANGED
@@ -940,6 +940,7 @@ export class IsolatedRunner {
940
940
  if (generation && generation.status !== "staging" && generation.status !== "conflict")
941
941
  throw new Error("Stage action has a stale generation.");
942
942
  const previousStage = generation?.stages.at(-1);
943
+ const pendingAllocation = action.action === "resolve" && previousStage?.status === "pending";
943
944
  const expected = previousStage?.status === "staged"
944
945
  ? previousStage.tip : previousStage?.onto ?? generation?.integrationBase;
945
946
  if (action.action === "stage" && generation?.stages.some((stage) => stage.status !== "staged")) {
@@ -974,6 +975,18 @@ export class IsolatedRunner {
974
975
  const actualMain = await scope.call((context) => this.gitRuntime.inspectMain({ root }, context));
975
976
  if (!sameIdentity(actualMain, state.main))
976
977
  throw new Error("Main changed or became dirty; staging is blocked.");
978
+ if (pendingAllocation) {
979
+ if (!generation?.worktree || generation.status !== "staging" || generation.stages.length !== 1
980
+ || !sameIdentity(action.expectedTip, generation.integrationBase)) {
981
+ throw new Error("Pending integration allocation is not an exact initial stage.");
982
+ }
983
+ const checkout = generation.worktree;
984
+ const base = await scope.call((context) => this.integrationGit.inspectCombined(root, checkout, state.main, [], context.signal));
985
+ if (!sameIdentity(base, generation.integrationBase))
986
+ throw new Error("Pending integration checkout is not the clean recorded base.");
987
+ previousStage.status = "staging";
988
+ await this.saveProductive(handle); // Explicit resolve proves allocation; merge intent precedes the first merge.
989
+ }
977
990
  if (!generation) {
978
991
  if (action.action !== "stage")
979
992
  throw new Error("No integration generation exists to resolve.");
@@ -1018,15 +1031,9 @@ export class IsolatedRunner {
1018
1031
  }
1019
1032
  await this.saveProductive(handle); // Exact candidate and previous tip before merge.
1020
1033
  }
1021
- let outcome = await scope.call((context) => action.action === "stage"
1034
+ const outcome = await scope.call((context) => action.action === "stage" || pendingAllocation
1022
1035
  ? this.integrationGit.stage(root, worktree, state.main, receipts, worker, candidate.tip, context.signal)
1023
1036
  : this.integrationGit.reconcileStage(root, worktree, state.main, receipts, worker, candidate.tip, context.signal));
1024
- if (action.action === "resolve" && stage.status === "pending"
1025
- && outcome.outcome === "ready" && outcome.value === "not_started") {
1026
- stage.status = "staging";
1027
- await this.saveProductive(handle); // Read-only proof before Main's explicit merge request.
1028
- outcome = await scope.call((context) => this.integrationGit.stage(root, worktree, state.main, receipts, worker, candidate.tip, context.signal));
1029
- }
1030
1037
  if (outcome.outcome === "ready" && outcome.value !== "not_started") {
1031
1038
  stage.status = "staged";
1032
1039
  stage.tip = outcome.value.tip;
@@ -46,7 +46,7 @@ export declare function inspectIndexFlags(cwd: string, run?: GitRunner, signal?:
46
46
  hidden: boolean;
47
47
  failure?: string;
48
48
  }>;
49
- /** Proves a worktree has no tracked, untracked, ignored, index-hidden, or nested submodule work. */
49
+ /** Proves a worktree has no tracked, untracked, index-hidden, or nested submodule work; cleanup also checks ignored files. */
50
50
  export declare function inspectWorktreeDirty(cwd: string, run?: GitRunner, includeIgnored?: boolean): Promise<WorktreeDirtyInspection>;
51
51
  /**
52
52
  * Inspects and possibly prunes a child worktree after it finishes. Commit count
package/dist/worktree.js CHANGED
@@ -187,7 +187,7 @@ async function inspectDirty(run, cwd, includeIgnored = true) {
187
187
  return { dirty: false, failure: "assume-unchanged or skip-worktree index entries remain" };
188
188
  return { dirty: false };
189
189
  }
190
- /** Proves a worktree has no tracked, untracked, ignored, index-hidden, or nested submodule work. */
190
+ /** Proves a worktree has no tracked, untracked, index-hidden, or nested submodule work; cleanup also checks ignored files. */
191
191
  export async function inspectWorktreeDirty(cwd, run = runGit, includeIgnored = true) {
192
192
  const root = await inspectDirty(run, cwd, includeIgnored);
193
193
  if (root.dirty || root.failure)
@@ -232,7 +232,7 @@ export async function finalizeChildWorktree(info, run = runGit) {
232
232
  const countFailure = counted.code !== 0
233
233
  ? `rev-list exit ${counted.code}: ${counted.stderr.trim().slice(0, 200)}`
234
234
  : Number.isNaN(commits) ? "rev-list produced non-numeric output" : undefined;
235
- const inspection = checkoutExists ? await inspectDirty(run, info.path) : undefined;
235
+ const inspection = checkoutExists ? await inspectDirty(run, info.path, true) : undefined;
236
236
  const measurements = {
237
237
  ...(countFailure === undefined ? { commits } : {}),
238
238
  ...(inspection?.failure === undefined && inspection !== undefined ? { dirty: inspection.dirty } : {}),
@@ -65,18 +65,19 @@ export const createIsolatedComponents: CreateIsolatedComponents = ({
65
65
  : await directRunProcess(command, args, options);
66
66
  const resolveRoot = createCanonicalGitRootResolver({ runProcess });
67
67
  const git = new CheckedGitRuntime({ runProcess, executeReview: createExactJudgmentExecutor(executor) });
68
- const host = new HerdrHostRuntime({ inspectInFlightTaskCandidate: git.inspectInFlightTaskCandidate.bind(git), runProcess });
68
+ const executionBudget = () => ({
69
+ maxTurns: policy.maxTurns,
70
+ maxMs: null,
71
+ ...(policy.maxTokens === undefined ? {} : { maxTokens: policy.maxTokens }),
72
+ });
73
+ const host = new HerdrHostRuntime({ inspectInFlightTaskCandidate: git.inspectInFlightTaskCandidate.bind(git), runProcess, executionBudget });
69
74
  const coordinator = new RoleLaunchRuntime({
70
75
  pi,
71
76
  context,
72
77
  resolveRoot,
73
78
  preflightHost: async ({ root }, operation) => await host.preflightHost({ root }, operation),
74
79
  inspectMain: async (input, operation) => await git.inspectMain(input, operation),
75
- executionBudget: () => ({
76
- maxTurns: policy.maxTurns,
77
- maxMs: null,
78
- ...(policy.maxTokens === undefined ? {} : { maxTokens: policy.maxTokens }),
79
- }),
80
+ executionBudget,
80
81
  });
81
82
  return {
82
83
  resolveRoot,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-subagent",
3
- "version": "22.2.0",
3
+ "version": "22.2.1",
4
4
  "description": "Delegate bounded direct work or durable checked isolated task graphs to Pi roles.",
5
5
  "keywords": [
6
6
  "pi-package",