@hcflabs/pi-subagent 27.11.0 → 28.1.0
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 +27 -3
- package/dist/full-suite.d.ts +34 -0
- package/dist/full-suite.js +142 -0
- package/dist/herdr-runtime.d.ts +7 -1
- package/dist/herdr-runtime.js +11 -1
- package/dist/index.d.ts +13 -0
- package/dist/index.js +37 -0
- package/dist/launch-runtime.d.ts +2 -0
- package/dist/launch-runtime.js +4 -1
- package/dist/runner.d.ts +32 -5
- package/dist/runner.js +45 -35
- package/dist/schema.d.ts +38 -3
- package/dist/schema.js +23 -1
- package/dist/store.d.ts +10 -2
- package/dist/store.js +20 -11
- package/docs/orchestration.md +2 -2
- package/extensions/isolated.ts +177 -63
- package/extensions/subagent-command.ts +12 -3
- package/extensions/subagent.ts +4 -2
- package/extensions/workflow.ts +8 -5
- package/package.json +1 -1
- package/skills/pi-subagent/SKILL.md +1 -1
package/dist/runner.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { inspectDiscard, primaryCheckout, removeDiscardResources } from "./discard.js";
|
|
4
4
|
import { utf8Prefix } from "./ephemeral.js";
|
|
5
|
+
import { assertFullSuiteSource, configuredSuiteFor, resolveFullSuite, sameCheck } from "./full-suite.js";
|
|
5
6
|
import { createChildWorktree, finalizeChildWorktree, inspectWorktreeDirty } from "./worktree.js";
|
|
6
7
|
import { runGit } from "./git-process.js";
|
|
7
8
|
import { IntegrationGit } from "./integration-git.js";
|
|
@@ -19,23 +20,8 @@ export const CLEANUP_SAFETY_BUDGET_MS = 30_000;
|
|
|
19
20
|
export const TERMINATION_SAFETY_BUDGET_MS = 15_000;
|
|
20
21
|
export const STATUS_INSPECTION_BUDGET_MS = 5_000;
|
|
21
22
|
const ALLOCATION_KINDS = ["worktree", "workspace", "worker_tab", "agent"];
|
|
22
|
-
const FULL_SUITE = { command: "pnpm", args: ["test"] };
|
|
23
23
|
const MAX_RETAINED_INTEGRATION_GENERATIONS = 2;
|
|
24
|
-
|
|
25
|
-
let manifest;
|
|
26
|
-
try {
|
|
27
|
-
manifest = JSON.parse(readFileSync(`${root}/package.json`, "utf8"));
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
throw new Error("Canonical root package.json with a test script is required for isolated changesets.");
|
|
31
|
-
}
|
|
32
|
-
if (!manifest || typeof manifest !== "object" || !('scripts' in manifest)
|
|
33
|
-
|| !manifest.scripts || typeof manifest.scripts !== "object"
|
|
34
|
-
|| !('test' in manifest.scripts) || typeof manifest.scripts.test !== "string"
|
|
35
|
-
|| !manifest.scripts.test.trim())
|
|
36
|
-
throw new Error("Canonical root test script is missing.");
|
|
37
|
-
return manifest.scripts.test;
|
|
38
|
-
}
|
|
24
|
+
const MAX_FINAL_CHECKS = 32;
|
|
39
25
|
/** Run one launch action and surface cleanup failure, including after action failure or abort. */
|
|
40
26
|
export async function withTransientLaunch(handle, operation) {
|
|
41
27
|
let failed = false;
|
|
@@ -616,12 +602,15 @@ export class IsolatedRunner {
|
|
|
616
602
|
integrationGit;
|
|
617
603
|
textExecutor;
|
|
618
604
|
currentPolicy;
|
|
605
|
+
/** The configured `repoSuites`, read at admission only. */
|
|
606
|
+
repoSuites;
|
|
619
607
|
productiveLifecycles = new Map();
|
|
620
608
|
followupControls = new Map();
|
|
621
609
|
constructor(coordinatorRuntime, hostRuntime, gitRuntime, store = new FileRunStore(), textExecutor, policy = {
|
|
622
610
|
maxSubagents: 5, maxTurns: 50, childIdleMs: 600_000, childMaxMs: 1_800_000,
|
|
623
611
|
maxCorrections: 1,
|
|
624
|
-
}, currentPolicy = () => policy, integrationGit = new IntegrationGit()) {
|
|
612
|
+
}, currentPolicy = () => policy, integrationGit = new IntegrationGit(), repoSuites = () => ({})) {
|
|
613
|
+
this.repoSuites = repoSuites;
|
|
625
614
|
this.coordinatorRuntime = coordinatorRuntime;
|
|
626
615
|
this.hostRuntime = hostRuntime;
|
|
627
616
|
this.gitRuntime = gitRuntime;
|
|
@@ -825,19 +814,30 @@ export class IsolatedRunner {
|
|
|
825
814
|
throw error;
|
|
826
815
|
}
|
|
827
816
|
/** Every execute check before durable state; a dry run also skips the Herdr host preflight. */
|
|
828
|
-
async admitExecute(value, cwd, scope, dryRun) {
|
|
817
|
+
async admitExecute(value, cwd, scope, dryRun, targeted) {
|
|
829
818
|
const request = parseExecuteRequest(value);
|
|
830
819
|
const policy = Object.freeze({ ...this.currentPolicy() });
|
|
831
820
|
const canonicalCwd = realpathSync.native(cwd);
|
|
832
|
-
const prepared = await scope.call(async (context) => await this.coordinatorRuntime.preflight({
|
|
821
|
+
const prepared = await scope.call(async (context) => await this.coordinatorRuntime.preflight({
|
|
822
|
+
request, cwd: canonicalCwd, ...(dryRun ? { dryRun } : {}), ...(targeted ? { targeted } : {}),
|
|
823
|
+
}, context));
|
|
833
824
|
const root = realpathSync.native(prepared.root);
|
|
834
825
|
if (root !== prepared.root)
|
|
835
826
|
throw new Error("Preflight repository root must be canonical.");
|
|
827
|
+
// A Repository target is its own root. A different root means the checkout changed after the target check.
|
|
828
|
+
if (targeted && root !== canonicalCwd)
|
|
829
|
+
throw new Error(`Repository target ${canonicalCwd} now resolves to ${root}; nothing was saved. Retry the request.`);
|
|
830
|
+
let fullSuite;
|
|
836
831
|
if (request.tasks.some((task) => task.kind === "changeset")) {
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
832
|
+
// Main is clean and committed here, so the working files are the committed sources.
|
|
833
|
+
const repoSuites = this.repoSuites();
|
|
834
|
+
const configured = Object.keys(repoSuites).length ? configuredSuiteFor(repoSuites, await primaryCheckout(root)) : undefined;
|
|
835
|
+
fullSuite = resolveFullSuite(root, configured);
|
|
836
|
+
if (!request.finalChecks.some((check) => sameCheck(check, fullSuite.check))) {
|
|
837
|
+
if (request.finalChecks.length >= MAX_FINAL_CHECKS) {
|
|
838
|
+
throw new Error(`The Full suite ${formatCheckCommand(fullSuite.check)} cannot be added: the request already has ${MAX_FINAL_CHECKS} final checks.`);
|
|
839
|
+
}
|
|
840
|
+
request.finalChecks = [fullSuite.check, ...request.finalChecks];
|
|
841
841
|
}
|
|
842
842
|
}
|
|
843
843
|
const preparedMain = runtimeIdentity(prepared.main, "Preflight Main identity");
|
|
@@ -846,22 +846,24 @@ export class IsolatedRunner {
|
|
|
846
846
|
if (this.hasAnyActiveControl(root)) {
|
|
847
847
|
throw new Error("Another Pi Subagent request has active changeset work in this repository.");
|
|
848
848
|
}
|
|
849
|
-
return { request, policy, root, preparedMain, routes: prepared.routes ?? [] };
|
|
849
|
+
return { request, policy, root, preparedMain, routes: prepared.routes ?? [], ...(fullSuite ? { fullSuite } : {}) };
|
|
850
850
|
}
|
|
851
851
|
/** Dry run of execute: no state, lock, lease, worktree, or Herdr call. */
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
852
|
+
/** `options.targeted` marks `cwd` as a Repository target that the caller selected with `repo`. */
|
|
853
|
+
async plan(value, cwd, outerSignal, options = {}) {
|
|
854
|
+
const { request, root, preparedMain, routes, fullSuite } = await this.admitExecute(value, cwd, new ProductiveScope(outerSignal), true, options.targeted === true);
|
|
855
|
+
return { request, root, main: preparedMain, routes, ...(fullSuite ? { fullSuite } : {}) };
|
|
855
856
|
}
|
|
856
|
-
async execute(value, cwd, outerSignal) {
|
|
857
|
+
async execute(value, cwd, outerSignal, options = {}) {
|
|
857
858
|
const scope = new ProductiveScope(outerSignal);
|
|
858
|
-
const { request, policy, root, preparedMain } = await this.admitExecute(value, cwd, scope, false);
|
|
859
|
+
const { request, policy, root, preparedMain, fullSuite } = await this.admitExecute(value, cwd, scope, false, options.targeted === true);
|
|
859
860
|
return await this.withProductiveRun(root, async (lifecycle) => {
|
|
860
861
|
const createdAt = this.coordinatorRuntime.now();
|
|
861
862
|
const state = {
|
|
862
863
|
version: RUN_STATE_VERSION,
|
|
863
864
|
request,
|
|
864
865
|
policy,
|
|
866
|
+
...(fullSuite ? { fullSuite } : {}),
|
|
865
867
|
correctionCount: 0,
|
|
866
868
|
root,
|
|
867
869
|
requestStartMain: preparedMain,
|
|
@@ -1478,12 +1480,12 @@ export class IsolatedRunner {
|
|
|
1478
1480
|
generation.status = "validating";
|
|
1479
1481
|
await this.saveProductive(handle); // Record the exact validation intent before running checks.
|
|
1480
1482
|
try {
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
throw new Error("
|
|
1484
|
-
if (requireRootSuite(generation.worktree.path) !== requireRootSuite(root)) {
|
|
1485
|
-
throw new Error("Combined tip changed the admitted canonical root test script.");
|
|
1483
|
+
const fullSuite = state.fullSuite;
|
|
1484
|
+
if (!fullSuite || !request.finalChecks.some((check) => sameCheck(check, fullSuite.check))) {
|
|
1485
|
+
throw new Error("The Full suite was not recorded at admission.");
|
|
1486
1486
|
}
|
|
1487
|
+
// The saved suite runs as admitted. A later config edit does not change it.
|
|
1488
|
+
assertFullSuiteSource(fullSuite, generation.worktree.path);
|
|
1487
1489
|
await this.requireExactIntegration(handle, generation, receipts, scope);
|
|
1488
1490
|
const sources = stagedCheckSources(generation, state.integration.candidates);
|
|
1489
1491
|
const checks = await this.runCheckBatch(handle, request.finalChecks, action.expectedTip, "final", scope, undefined, generation.worktree.path, request.finalChecks.map((check) => reusableCheckSource(check, action.expectedTip, sources)));
|
|
@@ -1792,6 +1794,14 @@ export class IsolatedRunner {
|
|
|
1792
1794
|
directory: dirname(handle.path), root: handle.state.root, orphaned: true, tasks: [],
|
|
1793
1795
|
})) };
|
|
1794
1796
|
}
|
|
1797
|
+
/** Orphaned requests of every primary checkout in one state scan, each with the primary checkout that owns it. */
|
|
1798
|
+
async listAllOrphanRequests() {
|
|
1799
|
+
const { states, invalidIds, primaries } = await this.store.listOrphans();
|
|
1800
|
+
return { invalidIds, requests: states.map((handle) => ({
|
|
1801
|
+
id: handle.state.request.id, name: handle.state.request.goal, status: inventoryStatus(handle.state, true),
|
|
1802
|
+
directory: dirname(handle.path), root: handle.state.root, orphaned: true, tasks: [], primary: primaries.get(handle),
|
|
1803
|
+
})) };
|
|
1804
|
+
}
|
|
1795
1805
|
discardGuards(directory, root, guards) {
|
|
1796
1806
|
return { ...guards, blocked: async () => {
|
|
1797
1807
|
if (this.productiveLifecycles.has(root))
|
package/dist/schema.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Type, type Static } from "typebox";
|
|
2
|
-
export declare const RUN_STATE_VERSION =
|
|
2
|
+
export declare const RUN_STATE_VERSION = 6;
|
|
3
3
|
export declare const MAX_TASKS = 8;
|
|
4
4
|
export declare const MAX_EXECUTE_REQUEST_BYTES: number;
|
|
5
5
|
export declare const MAX_PERSISTED_RUNTIME_TEXT_BYTES: number;
|
|
@@ -10,6 +10,24 @@ export declare const CheckCommandSchema: Type.TObject<{
|
|
|
10
10
|
command: Type.TString;
|
|
11
11
|
args: Type.TArray<Type.TString>;
|
|
12
12
|
}>;
|
|
13
|
+
/** The Full suite fixed at admission, with its source. Validation checks the same source again. */
|
|
14
|
+
export declare const FullSuiteSchema: Type.TObject<{
|
|
15
|
+
check: Type.TObject<{
|
|
16
|
+
command: Type.TString;
|
|
17
|
+
args: Type.TArray<Type.TString>;
|
|
18
|
+
}>;
|
|
19
|
+
source: Type.TUnion<[Type.TObject<{
|
|
20
|
+
kind: Type.TLiteral<"config">;
|
|
21
|
+
}>, Type.TObject<{
|
|
22
|
+
kind: Type.TLiteral<"package-json">;
|
|
23
|
+
testScript: Type.TString;
|
|
24
|
+
}>, Type.TObject<{
|
|
25
|
+
kind: Type.TLiteral<"makefile">;
|
|
26
|
+
file: Type.TUnion<[Type.TLiteral<"GNUmakefile">, Type.TLiteral<"makefile">, Type.TLiteral<"Makefile">]>;
|
|
27
|
+
sha256: Type.TString;
|
|
28
|
+
}>]>;
|
|
29
|
+
}>;
|
|
30
|
+
export type AdmittedFullSuite = Static<typeof FullSuiteSchema>;
|
|
13
31
|
/** One display form for a check command; quoted args keep argv boundaries visible. */
|
|
14
32
|
export declare function formatCheckCommand({ command, args }: {
|
|
15
33
|
command: string;
|
|
@@ -1756,7 +1774,7 @@ export type IntegrationState = Static<typeof IntegrationStateSchema>;
|
|
|
1756
1774
|
declare const TerminalRunStatusSchema: Type.TUnion<[Type.TLiteral<"completed">, Type.TLiteral<"final_failed">, Type.TLiteral<"superseded">, Type.TLiteral<"aborted">]>;
|
|
1757
1775
|
export declare const TERMINAL_RUN_STATUSES: readonly Static<typeof TerminalRunStatusSchema>[];
|
|
1758
1776
|
declare const RunStateSchema: Type.TObject<{
|
|
1759
|
-
version: Type.TLiteral<
|
|
1777
|
+
version: Type.TLiteral<6>;
|
|
1760
1778
|
request: Type.TObject<{
|
|
1761
1779
|
mode: Type.TLiteral<"isolated">;
|
|
1762
1780
|
id: Type.TString;
|
|
@@ -1807,6 +1825,23 @@ declare const RunStateSchema: Type.TObject<{
|
|
|
1807
1825
|
childMaxMs: Type.TNumber;
|
|
1808
1826
|
maxCorrections: Type.TInteger;
|
|
1809
1827
|
}>;
|
|
1828
|
+
/** Present exactly when the request has a changeset task. */
|
|
1829
|
+
fullSuite: Type.TOptional<Type.TObject<{
|
|
1830
|
+
check: Type.TObject<{
|
|
1831
|
+
command: Type.TString;
|
|
1832
|
+
args: Type.TArray<Type.TString>;
|
|
1833
|
+
}>;
|
|
1834
|
+
source: Type.TUnion<[Type.TObject<{
|
|
1835
|
+
kind: Type.TLiteral<"config">;
|
|
1836
|
+
}>, Type.TObject<{
|
|
1837
|
+
kind: Type.TLiteral<"package-json">;
|
|
1838
|
+
testScript: Type.TString;
|
|
1839
|
+
}>, Type.TObject<{
|
|
1840
|
+
kind: Type.TLiteral<"makefile">;
|
|
1841
|
+
file: Type.TUnion<[Type.TLiteral<"GNUmakefile">, Type.TLiteral<"makefile">, Type.TLiteral<"Makefile">]>;
|
|
1842
|
+
sha256: Type.TString;
|
|
1843
|
+
}>]>;
|
|
1844
|
+
}>>;
|
|
1810
1845
|
correctionCount: Type.TInteger;
|
|
1811
1846
|
root: Type.TString;
|
|
1812
1847
|
requestStartMain: Type.TObject<{
|
|
@@ -2429,7 +2464,7 @@ declare const RunStateSchema: Type.TObject<{
|
|
|
2429
2464
|
*/
|
|
2430
2465
|
export declare const StatusOutputSchema: Type.TObject<{
|
|
2431
2466
|
state: Type.TObject<{
|
|
2432
|
-
version: Type.TLiteral<
|
|
2467
|
+
version: Type.TLiteral<6>;
|
|
2433
2468
|
id: Type.TString;
|
|
2434
2469
|
status: Type.TUnion<[Type.TLiteral<"pending">, Type.TLiteral<"running">, Type.TLiteral<"needs_attention">, Type.TLiteral<"completed">, Type.TLiteral<"final_failed">, Type.TLiteral<"superseded">, Type.TLiteral<"aborted">]>;
|
|
2435
2470
|
accepted: Type.TBoolean;
|
package/dist/schema.js
CHANGED
|
@@ -2,7 +2,7 @@ import { isAbsolute } from "node:path";
|
|
|
2
2
|
import { parseRoleName } from "./index.js";
|
|
3
3
|
import { Type } from "typebox";
|
|
4
4
|
import { Check, Errors } from "typebox/value";
|
|
5
|
-
export const RUN_STATE_VERSION =
|
|
5
|
+
export const RUN_STATE_VERSION = 6;
|
|
6
6
|
export const MAX_TASKS = 8;
|
|
7
7
|
export const MAX_EXECUTE_REQUEST_BYTES = 256 * 1024;
|
|
8
8
|
export const MAX_PERSISTED_RUNTIME_TEXT_BYTES = 8 * 1024;
|
|
@@ -26,6 +26,19 @@ export const CheckCommandSchema = Type.Object({
|
|
|
26
26
|
command: TextSchema,
|
|
27
27
|
args: Type.Array(Type.String({ maxLength: 16_000 }), { maxItems: 128 }),
|
|
28
28
|
}, { additionalProperties: false });
|
|
29
|
+
/** The Full suite fixed at admission, with its source. Validation checks the same source again. */
|
|
30
|
+
export const FullSuiteSchema = Type.Object({
|
|
31
|
+
check: CheckCommandSchema,
|
|
32
|
+
source: Type.Union([
|
|
33
|
+
Type.Object({ kind: Type.Literal("config") }, { additionalProperties: false }),
|
|
34
|
+
Type.Object({ kind: Type.Literal("package-json"), testScript: Type.String({ minLength: 1, maxLength: 16_000 }) }, { additionalProperties: false }),
|
|
35
|
+
Type.Object({
|
|
36
|
+
kind: Type.Literal("makefile"),
|
|
37
|
+
file: Type.Union([Type.Literal("GNUmakefile"), Type.Literal("makefile"), Type.Literal("Makefile")]),
|
|
38
|
+
sha256: Type.String({ pattern: "^[0-9a-f]{64}$" }),
|
|
39
|
+
}, { additionalProperties: false }),
|
|
40
|
+
]),
|
|
41
|
+
}, { additionalProperties: false });
|
|
29
42
|
/** One display form for a check command; quoted args keep argv boundaries visible. */
|
|
30
43
|
export function formatCheckCommand({ command, args }) {
|
|
31
44
|
return [command, ...args.map((arg) => JSON.stringify(arg))].join(" ");
|
|
@@ -420,6 +433,8 @@ const RunStateSchema = Type.Object({
|
|
|
420
433
|
version: Type.Literal(RUN_STATE_VERSION),
|
|
421
434
|
request: ExecuteRequestSchema,
|
|
422
435
|
policy: ExecutionPolicySnapshotSchema,
|
|
436
|
+
/** Present exactly when the request has a changeset task. */
|
|
437
|
+
fullSuite: Type.Optional(FullSuiteSchema),
|
|
423
438
|
correctionCount: Type.Integer({ minimum: 0 }),
|
|
424
439
|
root: TextSchema,
|
|
425
440
|
requestStartMain: WorkspaceSchema,
|
|
@@ -1015,6 +1030,13 @@ export function parseRunState(value) {
|
|
|
1015
1030
|
const state = value;
|
|
1016
1031
|
const request = parseExecuteRequest(state.request);
|
|
1017
1032
|
parseIntegrationState(state.integration, request);
|
|
1033
|
+
const changeset = request.tasks.some((task) => task.kind === "changeset");
|
|
1034
|
+
if (changeset !== (state.fullSuite !== undefined))
|
|
1035
|
+
throw new Error("A Full suite must be recorded exactly for a request with changeset tasks.");
|
|
1036
|
+
if (state.fullSuite && !request.finalChecks.some(({ command, args }) => command === state.fullSuite.check.command
|
|
1037
|
+
&& args.length === state.fullSuite.check.args.length && args.every((arg, index) => arg === state.fullSuite.check.args[index]))) {
|
|
1038
|
+
throw new Error("The recorded Full suite is not in the request final checks.");
|
|
1039
|
+
}
|
|
1018
1040
|
if (state.status !== "aborted" && state.integration.generations.some((generation) => generation.supersededFrom === "discarded")) {
|
|
1019
1041
|
throw new Error("User-discarded generations require an aborted request.");
|
|
1020
1042
|
}
|
package/dist/store.d.ts
CHANGED
|
@@ -47,13 +47,21 @@ export declare class FileRunStore {
|
|
|
47
47
|
private loadFromValidatedDirectory;
|
|
48
48
|
hasProductiveRunLeaseInDirectory(directory: string): Promise<boolean>;
|
|
49
49
|
withDirectoryLock<T>(directory: string, operation: () => Promise<T>): Promise<T>;
|
|
50
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Missing roots only; repoRoot records the stable primary checkout, not the removed parent.
|
|
52
|
+
* Without `primary`, return the orphans of every primary checkout, each with its primary.
|
|
53
|
+
*/
|
|
51
54
|
listOrphans(primary: string): Promise<{
|
|
52
55
|
states: RunStateHandle[];
|
|
53
56
|
invalidIds: string[];
|
|
54
57
|
}>;
|
|
58
|
+
listOrphans(primary?: undefined): Promise<{
|
|
59
|
+
states: RunStateHandle[];
|
|
60
|
+
invalidIds: string[];
|
|
61
|
+
primaries: Map<RunStateHandle, string>;
|
|
62
|
+
}>;
|
|
55
63
|
private stateEntries;
|
|
56
|
-
/** Read only version and request status; never migrate or interpret v4 worker evidence. */
|
|
64
|
+
/** Read only version and request status; never migrate or interpret v4 or v5 worker evidence. */
|
|
57
65
|
assertLegacyAdmissionSafe(root: string): Promise<void>;
|
|
58
66
|
list(root: string): Promise<{
|
|
59
67
|
states: RunStateHandle[];
|
package/dist/store.js
CHANGED
|
@@ -18,8 +18,10 @@ const INITIAL_STATE_MAX_BYTES = 2 * 1024 * 1024;
|
|
|
18
18
|
const STATE_MAX_BYTES = 128 * 1024 * 1024;
|
|
19
19
|
const ADMISSION_MAX_FILES = 256;
|
|
20
20
|
const ADMISSION_MAX_BYTES = STATE_MAX_BYTES;
|
|
21
|
-
|
|
22
|
-
const
|
|
21
|
+
// v4 and v5 use the same request statuses. Admission reads only the version and the status of older files.
|
|
22
|
+
const LEGACY_VERSIONS = new Set([4, 5]);
|
|
23
|
+
const LEGACY_TERMINAL_STATUSES = new Set(["completed", "final_failed", "superseded", "aborted"]);
|
|
24
|
+
const LEGACY_UNFINISHED_STATUSES = new Set(["pending", "running", "needs_attention"]);
|
|
23
25
|
const LOCK_OPTIONS = { realpath: false, stale: 30_000, update: 5_000, retries: 0 };
|
|
24
26
|
const LIFECYCLE_LOCK_OPTIONS = {
|
|
25
27
|
...LOCK_OPTIONS,
|
|
@@ -253,8 +255,8 @@ export class FileRunStore {
|
|
|
253
255
|
await release();
|
|
254
256
|
}
|
|
255
257
|
}
|
|
256
|
-
/** Missing roots only; repoRoot records the stable primary checkout, not the removed parent. */
|
|
257
258
|
async listOrphans(primary) {
|
|
259
|
+
const primaries = new Map();
|
|
258
260
|
const parent = join(extensionConfigDir("pi-subagent", this.agentDir), "state");
|
|
259
261
|
let directories;
|
|
260
262
|
try {
|
|
@@ -262,7 +264,7 @@ export class FileRunStore {
|
|
|
262
264
|
}
|
|
263
265
|
catch (error) {
|
|
264
266
|
if (isMissing(error))
|
|
265
|
-
return { states: [], invalidIds: [] };
|
|
267
|
+
return { states: [], invalidIds: [], ...(primary === undefined ? { primaries } : {}) };
|
|
266
268
|
throw error;
|
|
267
269
|
}
|
|
268
270
|
const states = [];
|
|
@@ -289,7 +291,14 @@ export class FileRunStore {
|
|
|
289
291
|
throw error;
|
|
290
292
|
}
|
|
291
293
|
const worktrees = discardResources(handle.state).flatMap((resource) => resource.worktree ? [resource.worktree] : []);
|
|
292
|
-
|
|
294
|
+
const owner = worktrees[0]?.repoRoot;
|
|
295
|
+
if (owner === undefined || !worktrees.every((item) => item.repoRoot === owner))
|
|
296
|
+
continue;
|
|
297
|
+
if (primary === undefined) {
|
|
298
|
+
primaries.set(handle, owner);
|
|
299
|
+
states.push(handle);
|
|
300
|
+
}
|
|
301
|
+
else if (owner === primary)
|
|
293
302
|
states.push(handle);
|
|
294
303
|
}
|
|
295
304
|
catch {
|
|
@@ -303,7 +312,7 @@ export class FileRunStore {
|
|
|
303
312
|
invalidIds.push(directory);
|
|
304
313
|
}
|
|
305
314
|
}
|
|
306
|
-
return { states, invalidIds };
|
|
315
|
+
return { states, invalidIds, ...(primary === undefined ? { primaries } : {}) };
|
|
307
316
|
}
|
|
308
317
|
async stateEntries(root) {
|
|
309
318
|
try {
|
|
@@ -317,7 +326,7 @@ export class FileRunStore {
|
|
|
317
326
|
throw error;
|
|
318
327
|
}
|
|
319
328
|
}
|
|
320
|
-
/** Read only version and request status; never migrate or interpret v4 worker evidence. */
|
|
329
|
+
/** Read only version and request status; never migrate or interpret v4 or v5 worker evidence. */
|
|
321
330
|
async assertLegacyAdmissionSafe(root) {
|
|
322
331
|
const directory = this.stateDirectory(root);
|
|
323
332
|
await this.assertSafeDestination(root, directory);
|
|
@@ -348,12 +357,12 @@ export class FileRunStore {
|
|
|
348
357
|
}
|
|
349
358
|
if (value.version === RUN_STATE_VERSION)
|
|
350
359
|
continue;
|
|
351
|
-
if (value.version !==
|
|
352
|
-
|| (!
|
|
360
|
+
if (typeof value.version !== "number" || !LEGACY_VERSIONS.has(value.version) || !("status" in value) || typeof value.status !== "string"
|
|
361
|
+
|| (!LEGACY_TERMINAL_STATUSES.has(value.status) && !LEGACY_UNFINISHED_STATUSES.has(value.status))) {
|
|
353
362
|
throw new Error(`Pi Subagent malformed or unsupported legacy state ${id} blocks safe admission. ${recovery}`);
|
|
354
363
|
}
|
|
355
|
-
if (
|
|
356
|
-
throw new Error(`Pi Subagent unfinished
|
|
364
|
+
if (LEGACY_UNFINISHED_STATUSES.has(value.status)) {
|
|
365
|
+
throw new Error(`Pi Subagent unfinished v${value.version} request ${id} (${value.status}) blocks new isolated work. ${recovery}`);
|
|
357
366
|
}
|
|
358
367
|
}
|
|
359
368
|
}
|
package/docs/orchestration.md
CHANGED
|
@@ -81,7 +81,7 @@ Direct `changeset` is rejected; use isolated mode for checked integration. Resou
|
|
|
81
81
|
|
|
82
82
|
### Dry run
|
|
83
83
|
|
|
84
|
-
Set `"dryRun": true` on a direct or isolated `delegate_task` request to check it without a launch. The dry run does the same checks as a launch: request shape, Role lookup, route and model with thinking level, Skills, tools, MCP allowlist, Role extensions, config, and `continueFrom` eligibility. For an isolated request, it also checks the task graph and that Main is clean and committed. If the request has a changeset task, it also
|
|
84
|
+
Set `"dryRun": true` on a direct or isolated `delegate_task` request to check it without a launch. The dry run does the same checks as a launch: request shape, Role lookup, route and model with thinking level, Skills, tools, MCP allowlist, Role extensions, config, and `continueFrom` eligibility. For an isolated request, it also checks the task graph and that Main is clean and committed. If the request has a changeset task, it also selects the Full suite, adds it to the final checks, and shows it. A failed check gives the same error as a launch.
|
|
85
85
|
|
|
86
86
|
The result is a bounded plan. It shows each task's Role, resolved `provider/model`, thinking level, access or kind, dependencies, and checks. An isolated plan also shows the final checks. The dry run makes no Herdr call, Herdr tab, session record, saved state, worktree, lease, checkout admission, or follow-up. The plan lists each check it cannot do without such an effect as `Not checked: <reason>`. Examples are the Herdr host preflight, checkout admission, and the stop of a continued agent.
|
|
87
87
|
|
|
@@ -257,7 +257,7 @@ Every task declares `id`, `kind`, `role`, `modelClass`, `requirements`, `deliver
|
|
|
257
257
|
- A `changeset` task requires focused checks and owns one worktree, Herdr workspace, worker tab, and retained worker.
|
|
258
258
|
- `dependsOn` controls ready waves; dependent tasks can run from a selected staged snapshot only after Main explicitly advances it.
|
|
259
259
|
- `contextFrom` may reference completed text tasks only, implies scheduling dependencies, and preserves declaration order. It must not overlap `dependsOn`.
|
|
260
|
-
- A graph containing changesets requires final checks.
|
|
260
|
+
- A graph containing changesets requires final checks. Admission selects one Full suite: the `repoSuites` config entry, then `pnpm test` for a root `package.json` test script, then `make test` for a literal Makefile `test` target. It prepends the suite to the final checks if absent and saves the suite with its source in state v6. Combined validation rejects a changed root test script or Makefile. A config change after admission does not change the saved suite.
|
|
261
261
|
|
|
262
262
|
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.**
|
|
263
263
|
|