@hcflabs/pi-subagent 28.3.0 → 28.3.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 +1 -1
- package/dist/direct-herdr.js +1 -1
- package/dist/herdr-runtime.d.ts +14 -0
- package/dist/herdr-runtime.js +74 -19
- package/dist/index.js +0 -3
- package/dist/runner.d.ts +21 -0
- package/dist/runner.js +89 -1
- package/docs/orchestration.md +4 -0
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ Commands, prompts, and UI are for you. Tools and the packaged skill are for Main
|
|
|
37
37
|
| `delegate_task` | tool | Start authorized shared-checkout work or a durable isolated checked graph. With `dryRun: true`, check the request and show the resolved plan without a launch. |
|
|
38
38
|
| `subagent_repos` | tool | List the child Git repositories below the Pi working directory and the `repo` selector of each. Read-only. |
|
|
39
39
|
| `subagent_status` | tool | Read saved isolated state, exact identities, failed review verdicts, and allowed continuation. Does not replay work. Supports structured output for `codemode`. |
|
|
40
|
-
| `subagent_resume` | tool | Perform the reported `retry`, `verify`, or `finalize` continuation, not staging or integration. Same-worker correction retries accept optional `instruction` text. |
|
|
40
|
+
| `subagent_resume` | tool | Perform the reported `retry`, `verify`, or `finalize` continuation, not staging or integration. Same-worker correction retries accept optional `instruction` text. `verify` can also adopt a later clean committed candidate from the same settled worker. That candidate must pass its normal checks and judgment. |
|
|
41
41
|
| `subagent_stage` | tool | Stage, resolve, reject, or revise an exact candidate. Rejecting a staged candidate freezes its generation. |
|
|
42
42
|
| `subagent_integrate` | tool | Advance dependents, refresh, validate, correct, promote, reconcile, clean up promotion, or release rejected and superseded resources. |
|
|
43
43
|
| `subagent_abort` | tool | Terminate an isolated request without discarding retained work. Retained candidates and integration work require release or proof of absence. |
|
package/dist/direct-herdr.js
CHANGED
|
@@ -216,7 +216,7 @@ export function createDirectHerdr(pi, cwd, idleMs) {
|
|
|
216
216
|
onTab({ name, tabId, paneId, sessionFile, leasePath, turn, workspaceId, cwd: workingDir });
|
|
217
217
|
signal.throwIfAborted();
|
|
218
218
|
const started = await startPiAgent(herdr, { name, pane: paneId,
|
|
219
|
-
args: [...sessionArgs, "--session", sessionFile], options: options(signal), shouldRetry: () => false });
|
|
219
|
+
args: [...sessionArgs, "--session", sessionFile], options: options(signal), shouldRetry: () => false, freshPane: true });
|
|
220
220
|
if (started.code !== 0 || started.killed)
|
|
221
221
|
throw new Error(`Herdr agent start failed: ${started.stderr.slice(0, 1000)}`);
|
|
222
222
|
if (inspect(JSON.parse(started.stdout), "agent_started", name, paneId, tabId) !== "idle")
|
package/dist/herdr-runtime.d.ts
CHANGED
|
@@ -104,6 +104,20 @@ export declare class HerdrHostRuntime implements HostRuntime {
|
|
|
104
104
|
outcome: "unknown";
|
|
105
105
|
failure: string;
|
|
106
106
|
}>;
|
|
107
|
+
/**
|
|
108
|
+
* Proves that the exact owned worker is settled with a `stop` Final turn on its latest recorded prompt.
|
|
109
|
+
* The method reads the lifecycle and the session one time. It sends no prompt and closes nothing.
|
|
110
|
+
*/
|
|
111
|
+
inspectSettledWorker(input: {
|
|
112
|
+
task: TaskRequest;
|
|
113
|
+
attempt: TaskAttempt;
|
|
114
|
+
workerId: string;
|
|
115
|
+
}, context: OperationContext): Promise<{
|
|
116
|
+
outcome: "settled";
|
|
117
|
+
} | {
|
|
118
|
+
outcome: "active" | "unknown";
|
|
119
|
+
failure: string;
|
|
120
|
+
}>;
|
|
107
121
|
private proveAbsentAgentForCleanup;
|
|
108
122
|
cleanupHost(input: {
|
|
109
123
|
requestId: ExecuteRequest["id"];
|
package/dist/herdr-runtime.js
CHANGED
|
@@ -23,6 +23,8 @@ const STALLED_PROMPT_POLL_MS = 250;
|
|
|
23
23
|
const DEFAULT_SETTLED_IDLE_MS = 600_000;
|
|
24
24
|
const HOST_LAYOUT_POLL_MS = 50;
|
|
25
25
|
const HOST_LAYOUT_MAX_POLLS = 40;
|
|
26
|
+
const SHELL_IDLE_POLL_MS = 100;
|
|
27
|
+
const SHELL_IDLE_MAX_POLLS = 100;
|
|
26
28
|
const DIAGNOSTIC_LIMIT = 8 * 1024;
|
|
27
29
|
const LEASE_MODE = 0o600;
|
|
28
30
|
const DIRECTORY_MODE = 0o700;
|
|
@@ -663,6 +665,41 @@ export class HerdrHostRuntime {
|
|
|
663
665
|
return { outcome: "unknown", failure: safeText(error) };
|
|
664
666
|
}
|
|
665
667
|
}
|
|
668
|
+
/**
|
|
669
|
+
* Proves that the exact owned worker is settled with a `stop` Final turn on its latest recorded prompt.
|
|
670
|
+
* The method reads the lifecycle and the session one time. It sends no prompt and closes nothing.
|
|
671
|
+
*/
|
|
672
|
+
async inspectSettledWorker(input, context) {
|
|
673
|
+
try {
|
|
674
|
+
const allocation = ownedIntent(input.attempt, "agent");
|
|
675
|
+
requireIntentIdentity(allocation, input.attempt);
|
|
676
|
+
assertAgentIntent(allocation, input.attempt);
|
|
677
|
+
if (input.workerId !== allocation.agentName)
|
|
678
|
+
throw new Error("Settled worker inspection does not target the exact saved agent.");
|
|
679
|
+
this.assertLeasePath(allocation.leasePath, allocation.token);
|
|
680
|
+
await assertPrivateLease(allocation.leasePath, false);
|
|
681
|
+
// runWorker adds the prompt count after it records the prompt. So the latest prompt has the identity of the count.
|
|
682
|
+
const index = input.attempt.prompts.length;
|
|
683
|
+
if (!index)
|
|
684
|
+
throw new Error("Settled worker inspection requires a recorded worker prompt.");
|
|
685
|
+
const lifecycle = await this.waitForAgentLifecycle(allocation, context, true);
|
|
686
|
+
if (!SETTLED_AGENT_STATES.has(lifecycle.status)) {
|
|
687
|
+
return { outcome: "active", failure: `The exact worker is not settled: ${lifecycle.status}.` };
|
|
688
|
+
}
|
|
689
|
+
const suffix = `\n\nTurn identity: ${input.attempt.correlationToken}:${index}`;
|
|
690
|
+
const turn = await this.workerSessionTurn(allocation, (text) => text.endsWith(suffix), context.signal);
|
|
691
|
+
if (turn.state === "succeeded")
|
|
692
|
+
return { outcome: "settled" };
|
|
693
|
+
if (turn.state === "failed")
|
|
694
|
+
return { outcome: "unknown", failure: `The latest worker prompt has no stop Final turn. ${turn.failure}` };
|
|
695
|
+
return { outcome: "active", failure: turn.state === "running"
|
|
696
|
+
? "The latest worker prompt has no Final turn yet."
|
|
697
|
+
: "The worker session has no entry for the latest worker prompt." };
|
|
698
|
+
}
|
|
699
|
+
catch (error) {
|
|
700
|
+
return { outcome: "unknown", failure: safeText(error) };
|
|
701
|
+
}
|
|
702
|
+
}
|
|
666
703
|
async proveAbsentAgentForCleanup(allocation, attempt, context) {
|
|
667
704
|
requireIntentIdentity(allocation, attempt);
|
|
668
705
|
assertAgentIntent(allocation, attempt);
|
|
@@ -851,7 +888,7 @@ export class HerdrHostRuntime {
|
|
|
851
888
|
if ((await this.scanLease(allocation.leasePath, allocation.worktreeCwd, context)).length) {
|
|
852
889
|
throw new Error("Agent start requires an empty exact process lease.");
|
|
853
890
|
}
|
|
854
|
-
await this.assertStartableAgentPane(allocation, context, { requireExclusiveTty: false });
|
|
891
|
+
await this.assertStartableAgentPane(allocation, context, { requireExclusiveTty: false, awaitIdleShell: true });
|
|
855
892
|
const options = this.processOptions(allocation.worktreeCwd, context, HERDR_OPERATION_CAP_MS);
|
|
856
893
|
const handle = await acquireLaunch();
|
|
857
894
|
return await withTransientLaunch(handle, async (launch) => {
|
|
@@ -872,6 +909,7 @@ export class HerdrHostRuntime {
|
|
|
872
909
|
args: [...args, "--session", sessionFile],
|
|
873
910
|
options,
|
|
874
911
|
shouldRetry: () => false,
|
|
912
|
+
freshPane: true,
|
|
875
913
|
});
|
|
876
914
|
if (response.code !== 0 || response.killed) {
|
|
877
915
|
const failure = safeText(herdrCommandFailure(["agent", "start"], response));
|
|
@@ -1055,24 +1093,41 @@ export class HerdrHostRuntime {
|
|
|
1055
1093
|
|| (pane.agent !== undefined && pane.agent !== null) || pane.agent_status !== "unknown") {
|
|
1056
1094
|
throw new Error("The exact saved agent pane is not empty and startable in its owned worktree.");
|
|
1057
1095
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1096
|
+
let shellPid;
|
|
1097
|
+
for (let poll = 0;; poll += 1) {
|
|
1098
|
+
const processResponse = await this.herdr.json(["pane", "process-info", "--pane", allocation.paneId], this.processOptions(allocation.worktreeCwd, context, HERDR_OPERATION_CAP_MS));
|
|
1099
|
+
const processResult = resultRecord(processResponse, "Herdr agent pane process-info response");
|
|
1100
|
+
if (processResult.type !== "pane_process_info")
|
|
1101
|
+
throw new Error("Herdr agent pane process-info response has the wrong type.");
|
|
1102
|
+
const processInfo = record(processResult.process_info, "Herdr agent pane process-info");
|
|
1103
|
+
if (exactString(processInfo.pane_id, "Herdr agent process pane ID") !== allocation.paneId) {
|
|
1104
|
+
throw new Error("The exact saved agent pane process state is not an idle foreground shell.");
|
|
1105
|
+
}
|
|
1106
|
+
shellPid = processInfo.shell_pid;
|
|
1107
|
+
const foreground = processInfo.foreground_processes;
|
|
1108
|
+
const idleGroup = Number.isSafeInteger(shellPid) && Number(shellPid) > 0
|
|
1109
|
+
&& processInfo.foreground_process_group_id === shellPid
|
|
1110
|
+
&& Array.isArray(foreground) && foreground.length === 1;
|
|
1111
|
+
const shell = idleGroup ? record(foreground[0], "Herdr agent pane foreground process") : undefined;
|
|
1112
|
+
if (shell && shell.pid === shellPid && shell.cwd === allocation.worktreeCwd) {
|
|
1113
|
+
exactString(shell.name, "Herdr agent pane shell name");
|
|
1114
|
+
break;
|
|
1115
|
+
}
|
|
1116
|
+
if (!options.awaitIdleShell) {
|
|
1117
|
+
if (!idleGroup)
|
|
1118
|
+
throw new Error("The exact saved agent pane process state is not an idle foreground shell.");
|
|
1119
|
+
throw new Error("The exact saved agent pane foreground process is not its owned idle shell.");
|
|
1120
|
+
}
|
|
1121
|
+
if (poll + 1 >= SHELL_IDLE_MAX_POLLS) {
|
|
1122
|
+
const processes = Array.isArray(foreground)
|
|
1123
|
+
? foreground.slice(0, 20).map((process) => `${process?.pid}:${process?.name}`).join(", ")
|
|
1124
|
+
: "malformed";
|
|
1125
|
+
const diagnostic = safeText(`shell_pid=${shellPid}, foreground_process_group_id=${processInfo.foreground_process_group_id}, foreground=[${processes}]`);
|
|
1126
|
+
throw new Error(`The exact saved agent pane process state is not an idle foreground shell. ${diagnostic}`);
|
|
1127
|
+
}
|
|
1128
|
+
// Only fresh allocation waits for shell startup. Recovery requires an immediate safety proof.
|
|
1129
|
+
await this.delay(SHELL_IDLE_POLL_MS, context.signal);
|
|
1130
|
+
}
|
|
1076
1131
|
if (!options.requireExclusiveTty)
|
|
1077
1132
|
return;
|
|
1078
1133
|
// Herdr's foreground list excludes background jobs, so recovery inventories every process on the shell's controlling TTY.
|
package/dist/index.js
CHANGED
|
@@ -12,8 +12,6 @@ export { loadRoleMcpConfig, parseRoleMcpAllowlist } from "./mcp-role.js";
|
|
|
12
12
|
export { addUsage, capEphemeralSubagentOutput, createEphemeralSubagentExecutor, DEFAULT_MAX_TURNS, EphemeralSubagentError, EXECUTION_BUDGET_ENV, formatDuration, } from "./ephemeral.js";
|
|
13
13
|
export { createChildWorktree, finalizeChildWorktree, inspectIndexFlags, inspectWorktreeDirty, WorktreeSetupError, } from "./worktree.js";
|
|
14
14
|
export { prepareExactReviewEvidence, REVIEW_MAX_PATCH_BYTES, REVIEW_MAX_PATHS, } from "./review-evidence.js";
|
|
15
|
-
const CODEX_ALIAS = /^openai-codex-(?:[2-9]|[1-9]\d+)$/;
|
|
16
|
-
const MULTI_CODEX_EXTENSION = fileURLToPath(import.meta.resolve("@hcflabs/pi-multi-codex/extensions/multi-codex.ts"));
|
|
17
15
|
const ROLE_MCP_EXTENSION = fileURLToPath(new URL("../extensions/role-mcp.ts", import.meta.url));
|
|
18
16
|
const ROLE_TOOLS_EXTENSION = fileURLToPath(new URL("../extensions/role-tools.ts", import.meta.url));
|
|
19
17
|
const GIT_READ_EXTENSION = fileURLToPath(new URL("../extensions/git-read.ts", import.meta.url));
|
|
@@ -510,7 +508,6 @@ function prepareRoleLaunchFromSkills(ctx, input, skills) {
|
|
|
510
508
|
}
|
|
511
509
|
const extensions = [
|
|
512
510
|
...selectedExtensions,
|
|
513
|
-
...(CODEX_ALIAS.test(input.route.model.provider) ? [MULTI_CODEX_EXTENSION] : []),
|
|
514
511
|
// Always loaded: it connects the servers that Role extensions register; `mcps` gates only mcp.json.
|
|
515
512
|
ROLE_MCP_EXTENSION,
|
|
516
513
|
...(role.tools.includes("git_read") ? [GIT_READ_EXTENSION] : []),
|
package/dist/runner.d.ts
CHANGED
|
@@ -193,6 +193,20 @@ export interface HostRuntime {
|
|
|
193
193
|
outcome: "unknown";
|
|
194
194
|
failure: string;
|
|
195
195
|
}>;
|
|
196
|
+
/**
|
|
197
|
+
* Optional. Proves that the exact owned worker is settled with a `stop` Final turn on its latest recorded prompt.
|
|
198
|
+
* Without this method, `verify` cannot adopt a later candidate.
|
|
199
|
+
*/
|
|
200
|
+
inspectSettledWorker?(input: {
|
|
201
|
+
task: TaskRequest;
|
|
202
|
+
attempt: TaskAttempt;
|
|
203
|
+
workerId: string;
|
|
204
|
+
}, context: OperationContext): Promise<{
|
|
205
|
+
outcome: "settled";
|
|
206
|
+
} | {
|
|
207
|
+
outcome: "active" | "unknown";
|
|
208
|
+
failure: string;
|
|
209
|
+
}>;
|
|
196
210
|
cleanupHost(input: {
|
|
197
211
|
requestId: ExecuteRequest["id"];
|
|
198
212
|
kind: HostCleanupKind;
|
|
@@ -437,6 +451,13 @@ export declare class IsolatedRunner {
|
|
|
437
451
|
private runCheckBatch;
|
|
438
452
|
private runReview;
|
|
439
453
|
private retainCandidate;
|
|
454
|
+
private lateAdoptionAvailable;
|
|
455
|
+
/**
|
|
456
|
+
* Adopts the later clean committed candidate of the same settled worker. It sends no prompt and uses no correction.
|
|
457
|
+
* A refusal before the candidate is saved throws and changes no state.
|
|
458
|
+
* Returns a response when the adopted candidate needs attention. Returns undefined when readiness can continue.
|
|
459
|
+
*/
|
|
460
|
+
private adoptLateCandidate;
|
|
440
461
|
private verifyRetainedTask;
|
|
441
462
|
private retry;
|
|
442
463
|
private finalize;
|
package/dist/runner.js
CHANGED
|
@@ -412,6 +412,22 @@ function correctionEligible(request, attempt) {
|
|
|
412
412
|
&& sameIdentity(attempt.preliminaryReview.tip, attempt.candidate)
|
|
413
413
|
&& sameIdentity(attempt.preliminaryReview.identityAfter, attempt.candidate)));
|
|
414
414
|
}
|
|
415
|
+
/**
|
|
416
|
+
* True when `verify` can adopt a later candidate of the same settled worker.
|
|
417
|
+
* The latest prompt settled without a candidate, and no candidate exists in the prompt lineage.
|
|
418
|
+
* The worker can still leave a candidate after a false finished reading.
|
|
419
|
+
*/
|
|
420
|
+
function lateAdoptionEligible(task, attempt) {
|
|
421
|
+
const last = attempt.prompts.at(-1);
|
|
422
|
+
return task.status === "needs_attention"
|
|
423
|
+
&& !attempt.superseded
|
|
424
|
+
&& !attempt.candidate && !attempt.candidateBase
|
|
425
|
+
&& last?.status === "settled" && !last.candidate
|
|
426
|
+
&& !attempt.prompts.some((prompt) => prompt.status === "ambiguous" || prompt.status === "submitting")
|
|
427
|
+
&& !attempt.termination && !attempt.readiness && !attempt.preliminaryChecks && !attempt.preliminaryReview
|
|
428
|
+
&& Boolean(allocationByKind(attempt, "agent")?.agentName && allocationByKind(attempt, "worktree")?.worktree)
|
|
429
|
+
&& last.preCandidate.head === attempt.waveBase.head;
|
|
430
|
+
}
|
|
415
431
|
function terminal(state) {
|
|
416
432
|
return TERMINAL_RUN_STATUSES.some((status) => status === state.status);
|
|
417
433
|
}
|
|
@@ -2925,11 +2941,83 @@ export class IsolatedRunner {
|
|
|
2925
2941
|
state.integration.candidates.sort((a, b) => state.tasks.findIndex((item) => item.taskId === a.taskId)
|
|
2926
2942
|
- state.tasks.findIndex((item) => item.taskId === b.taskId));
|
|
2927
2943
|
}
|
|
2944
|
+
lateAdoptionAvailable(state, task, attempt) {
|
|
2945
|
+
return Boolean(this.hostRuntime.inspectSettledWorker) && !state.integration.candidates.length
|
|
2946
|
+
&& lateAdoptionEligible(task, attempt);
|
|
2947
|
+
}
|
|
2948
|
+
/**
|
|
2949
|
+
* Adopts the later clean committed candidate of the same settled worker. It sends no prompt and uses no correction.
|
|
2950
|
+
* A refusal before the candidate is saved throws and changes no state.
|
|
2951
|
+
* Returns a response when the adopted candidate needs attention. Returns undefined when readiness can continue.
|
|
2952
|
+
*/
|
|
2953
|
+
async adoptLateCandidate(handle, task, attempt, scope) {
|
|
2954
|
+
const state = handle.state;
|
|
2955
|
+
const request = changesetTaskRequest(state, task.taskId);
|
|
2956
|
+
const last = attempt.prompts.at(-1);
|
|
2957
|
+
if (last.preCandidate.head !== attempt.waveBase.head) {
|
|
2958
|
+
throw new Error("Late candidate adoption requires a prompt lineage that starts at the wave base.");
|
|
2959
|
+
}
|
|
2960
|
+
const workerId = allocationByKind(attempt, "agent").agentName;
|
|
2961
|
+
const branch = `refs/heads/${allocationByKind(attempt, "worktree").worktree.branch}`;
|
|
2962
|
+
const requireSettled = async () => {
|
|
2963
|
+
const result = await this.callProductive(handle, scope, async (context) => await this.hostRuntime.inspectSettledWorker({
|
|
2964
|
+
task: request, attempt, workerId,
|
|
2965
|
+
}, context));
|
|
2966
|
+
if (result.outcome !== "settled") {
|
|
2967
|
+
throw new Error(`Late candidate adoption requires the exact worker to be settled with a stop Final turn on its latest prompt (${result.outcome}): ${boundedRuntimeText(result.failure, "Settled worker failure")}`);
|
|
2968
|
+
}
|
|
2969
|
+
};
|
|
2970
|
+
await requireSettled();
|
|
2971
|
+
const candidate = runtimeIdentity(await this.callProductive(handle, scope, async (context) => await this.gitRuntime.inspectRetainedTask({
|
|
2972
|
+
root: state.root, task: request, attempt,
|
|
2973
|
+
}, context)), "Late task candidate identity");
|
|
2974
|
+
if (!isCleanCommitted(candidate) || candidate.branch !== branch || candidate.head === attempt.waveBase.head) {
|
|
2975
|
+
throw new Error("Late candidate adoption requires a new clean committed candidate on the owned branch.");
|
|
2976
|
+
}
|
|
2977
|
+
// A second proof after the Git read shows that the worker stayed settled on the same latest prompt.
|
|
2978
|
+
await requireSettled();
|
|
2979
|
+
last.candidate = candidate;
|
|
2980
|
+
attempt.candidate = candidate;
|
|
2981
|
+
attempt.candidateBase = attempt.waveBase;
|
|
2982
|
+
await this.saveProductive(handle);
|
|
2983
|
+
try {
|
|
2984
|
+
attempt.preliminaryChecks = await this.runCheckBatch(handle, request.checks, candidate, "preliminary", scope, task.taskId);
|
|
2985
|
+
await this.saveProductive(handle);
|
|
2986
|
+
if (!attempt.preliminaryChecks.passed) {
|
|
2987
|
+
this.attention(task, sameIdentity(attempt.preliminaryChecks.identityAfter, candidate)
|
|
2988
|
+
? "A declared task check failed on the adopted candidate."
|
|
2989
|
+
: "A declared task check changed the adopted candidate.");
|
|
2990
|
+
await this.saveProductive(handle);
|
|
2991
|
+
return this.response(state);
|
|
2992
|
+
}
|
|
2993
|
+
if (request.judgment) {
|
|
2994
|
+
attempt.preliminaryReview = await this.runReview(handle, request.judgment.criterion, attempt.candidateBase, candidate, request.judgment.role, request.judgment.modelClass, "preliminary", scope, task.taskId);
|
|
2995
|
+
await this.saveProductive(handle);
|
|
2996
|
+
if (!attempt.preliminaryReview.passed) {
|
|
2997
|
+
this.attention(task, "Preliminary Judgment did not return exact PASS on the adopted candidate.");
|
|
2998
|
+
await this.saveProductive(handle);
|
|
2999
|
+
return this.response(state);
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
catch (error) {
|
|
3004
|
+
this.rethrowStopped(error);
|
|
3005
|
+
this.attention(task, `Adopted candidate validation was interrupted: ${errorText(error)}`);
|
|
3006
|
+
await this.saveProductive(handle);
|
|
3007
|
+
throw error;
|
|
3008
|
+
}
|
|
3009
|
+
return undefined;
|
|
3010
|
+
}
|
|
2928
3011
|
async verifyRetainedTask(handle, task, scope) {
|
|
2929
3012
|
const attempt = latestAttempt(task);
|
|
2930
3013
|
if (attempt.prompts.some((prompt) => prompt.status === "ambiguous")) {
|
|
2931
3014
|
throw new Error("An ambiguous delivered prompt is retained for inspection or explicit abort and is never replayed.");
|
|
2932
3015
|
}
|
|
3016
|
+
if (!attempt.candidate && this.lateAdoptionAvailable(handle.state, task, attempt)) {
|
|
3017
|
+
const stopped = await this.adoptLateCandidate(handle, task, attempt, scope);
|
|
3018
|
+
if (stopped)
|
|
3019
|
+
return stopped;
|
|
3020
|
+
}
|
|
2933
3021
|
const candidate = runtimeIdentity(await this.callProductive(handle, scope, async (context) => await this.gitRuntime.inspectRetainedTask({
|
|
2934
3022
|
root: handle.state.root, task: changesetTaskRequest(handle.state, task.taskId), attempt,
|
|
2935
3023
|
}, context)), "Retained task candidate identity");
|
|
@@ -3334,7 +3422,7 @@ export class IsolatedRunner {
|
|
|
3334
3422
|
else if (attempt && this.correctionAllowed(state, changesetTaskRequest(state, attention.taskId), attempt)) {
|
|
3335
3423
|
continuation = { id: state.request.id, action: "retry", taskId: attention.taskId };
|
|
3336
3424
|
}
|
|
3337
|
-
else if (hasEvidence) {
|
|
3425
|
+
else if (hasEvidence || (attempt && this.lateAdoptionAvailable(state, attention, attempt))) {
|
|
3338
3426
|
continuation = { id: state.request.id, action: "verify", taskId: attention.taskId };
|
|
3339
3427
|
}
|
|
3340
3428
|
}
|
package/docs/orchestration.md
CHANGED
|
@@ -281,6 +281,8 @@ Every task declares `id`, `kind`, `role`, `modelClass`, `requirements`, `deliver
|
|
|
281
281
|
|
|
282
282
|
Admission and worker-wave dispatch pin Main's exact committed branch, HEAD, and tree without inspecting or modifying its staged, unstaged, or untracked edits. Worker allocation verifies the same committed identity around worktree creation, including resumed waves; staged-dependent waves verify their clean integration snapshot. A dirty Main can therefore produce a checked retained candidate, but branch/HEAD drift stops new waves. The runner executes ready worker tasks concurrently, inspects clean committed candidates, runs preliminary task checks and optional preliminary judgment, then records ready candidates for Main. **It does not automatically integrate them.**
|
|
283
283
|
|
|
284
|
+
The extension waits up to about 10 seconds for the shell of a new worker pane to finish startup before it starts Pi.
|
|
285
|
+
|
|
284
286
|
Main selects exact candidates through `subagent_stage` into an owned integration checkout in explicit order. Each stage is a committed merge of the candidate, not a mutation of Main. Conflicts retain the checkout for Main to resolve; `resolve` verifies the exact two-parent merge instead of replaying an uncertain merge. Main can explicitly run dependent tasks from an exact staged tip with `subagent_integrate advance` and stage their results afterward. Independent candidates may be staged without re-running workers.
|
|
285
287
|
|
|
286
288
|
Once all selected work is staged, Main calls `subagent_integrate validate` on the exact clean combined tip. It acknowledges the durably saved validation intent and continues in the session-owned background, independent of later turn cancellation; session replacement/shutdown still cancels it. A follow-up reports completion; status remains authoritative if delivery is missed. This runs every final check, including `pnpm test`, unless [exact check reuse](#validate-and-promote) applies, and optional final judgment in the integration checkout before any Main mutation. A failed check/review can be followed by one Main-authored committed correction in that checkout, recorded with `correct`, then another `validate`.
|
|
@@ -406,6 +408,8 @@ Parsing rejects unknown properties, malformed evidence, invalid lineage, and v4
|
|
|
406
408
|
|
|
407
409
|
Isolated workers save private exact Pi sessions under their owned process leases. A proved final answer without a changed clean commit requires attention. Missing or mismatched session proof cannot establish completion and never permits prompt replay.
|
|
408
410
|
|
|
411
|
+
A worker can leave a clean committed candidate after the runner records its turn as finished without one. `verify` can adopt that candidate after it proves that the same owned worker is settled with a `stop` Final turn on its latest prompt. The candidate then passes the task's normal preliminary checks and judgment before readiness. Adoption sends no prompt and uses no correction. It never replaces a saved candidate. While the same-worker correction is available, the reported continuation is `retry`.
|
|
412
|
+
|
|
409
413
|
When a finished turn leaves tracked or untracked changes in a valid owned worktree, the runner sends one commit reminder per dispatch try. It saves the reminder before delivery. This uses neither a correction nor a dispatch try. The runner never commits, stages, stashes, or discards. A resulting commit follows normal candidate checks; remaining dirty work requires attention.
|
|
410
414
|
|
|
411
415
|
`subagent_status` stays read-only and authoritative if delivery fails. Session replacement or shutdown suppresses stale follow-ups and logs the request ID and reason, not prompts or credentials. Synchronous delivery failures report without automatic replay. Pi reports asynchronous `send_message` failures through its runtime.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hcflabs/pi-subagent",
|
|
3
|
-
"version": "28.3.
|
|
3
|
+
"version": "28.3.2",
|
|
4
4
|
"description": "Delegate bounded direct work or durable checked isolated task graphs to Pi roles.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -71,8 +71,7 @@
|
|
|
71
71
|
},
|
|
72
72
|
"dependencies": {
|
|
73
73
|
"@hcflabs/pi-config-store": "^4.0.0",
|
|
74
|
-
"@hcflabs/pi-herdr": "^0.
|
|
75
|
-
"@hcflabs/pi-multi-codex": "^3.0.0",
|
|
74
|
+
"@hcflabs/pi-herdr": "^0.7.0",
|
|
76
75
|
"@hcflabs/pi-process": "^0.3.0",
|
|
77
76
|
"@hcflabs/pi-repo": "^0.1.0",
|
|
78
77
|
"@hcflabs/pi-task-models": "^9.0.0",
|