@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.
- package/dist/herdr-runtime.d.ts +3 -0
- package/dist/herdr-runtime.js +18 -2
- package/dist/integration-git.js +7 -4
- package/dist/runner.js +14 -7
- package/dist/worktree.d.ts +1 -1
- package/dist/worktree.js +2 -2
- package/extensions/isolated.ts +7 -6
- package/package.json +1 -1
package/dist/herdr-runtime.d.ts
CHANGED
|
@@ -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: {
|
package/dist/herdr-runtime.js
CHANGED
|
@@ -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}`,
|
|
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).
|
|
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));
|
package/dist/integration-git.js
CHANGED
|
@@ -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
|
|
49
|
-
const primary =
|
|
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 !==
|
|
54
|
+
|| info.repoRoot !== repoRoot || info.cwd !== info.path
|
|
52
55
|
|| !/^pi-subagent\/subagent-[0-9a-f]{24}$/.test(info.branch)
|
|
53
|
-
|| info.path !== join(
|
|
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
|
-
|
|
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;
|
package/dist/worktree.d.ts
CHANGED
|
@@ -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,
|
|
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,
|
|
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 } : {}),
|
package/extensions/isolated.ts
CHANGED
|
@@ -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
|
|
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,
|