pi-herdsman 0.22.1 → 0.22.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/SKILL.md +10 -6
- package/dist/agent-definitions/managed-lead.md +1 -1
- package/dist/agent-definitions/orchestrator-lead.md +1 -1
- package/dist/index.js +344 -105
- package/dist/index.js.map +3 -3
- package/docs/adr/0013-use-herdr-worktree-removal-for-project-retirement.md +1 -1
- package/docs/adr/0015-use-reserved-definition-for-managed-project-leads.md +5 -2
- package/docs/adr/0017-use-project-assignments-for-managed-lead-authority.md +2 -0
- package/docs/adr/0018-use-standalone-lead-execution-profiles.md +4 -2
- package/docs/adr/0019-retire-project-assignments-when-worktrees-are-absent.md +88 -0
- package/docs/concepts/coordination.md +12 -8
- package/docs/coordination-api.md +4 -3
- package/docs/development/product-philosophy.md +7 -0
- package/docs/development/smoke-testing.md +9 -8
- package/docs/guides/customizing-agents.md +13 -9
- package/docs/guides/project-orchestration.md +9 -8
- package/docs/reference/commands.md +3 -2
- package/docs/reference/staff.md +10 -11
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8,14 +8,14 @@ import {
|
|
|
8
8
|
SessionManager as SessionManager2
|
|
9
9
|
} from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { readFileSync as readFileSync9, statSync as statSync7, unlinkSync as unlinkSync9 } from "node:fs";
|
|
11
|
-
import { basename as basename6, dirname as dirname7, resolve as
|
|
11
|
+
import { basename as basename6, dirname as dirname7, resolve as resolve7 } from "node:path";
|
|
12
12
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
13
13
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
14
14
|
|
|
15
15
|
// package.json
|
|
16
16
|
var package_default = {
|
|
17
17
|
name: "pi-herdsman",
|
|
18
|
-
version: "0.22.
|
|
18
|
+
version: "0.22.2",
|
|
19
19
|
type: "module",
|
|
20
20
|
description: "Asynchronous Pi subagents and agent fleet orchestration for parallel coding agents with nested delegation, background work, and supervision in herdr.",
|
|
21
21
|
license: "Apache-2.0",
|
|
@@ -1135,7 +1135,7 @@ function parseControlMarker(text) {
|
|
|
1135
1135
|
return UUID2.test(id) && id.length === 36 ? id : void 0;
|
|
1136
1136
|
}
|
|
1137
1137
|
function waitForState(path, predicate, options) {
|
|
1138
|
-
return new Promise((
|
|
1138
|
+
return new Promise((resolve8, reject) => {
|
|
1139
1139
|
let done = false;
|
|
1140
1140
|
let abortListener;
|
|
1141
1141
|
const finish = (error2, state) => {
|
|
@@ -1145,7 +1145,7 @@ function waitForState(path, predicate, options) {
|
|
|
1145
1145
|
clearInterval(interval);
|
|
1146
1146
|
if (abortListener && options.signal)
|
|
1147
1147
|
options.signal.removeEventListener("abort", abortListener);
|
|
1148
|
-
error2 ? reject(error2) :
|
|
1148
|
+
error2 ? reject(error2) : resolve8(state);
|
|
1149
1149
|
};
|
|
1150
1150
|
const check = () => {
|
|
1151
1151
|
try {
|
|
@@ -15722,6 +15722,7 @@ import { visibleWidth as visibleWidth2 } from "@earendil-works/pi-tui";
|
|
|
15722
15722
|
import { contentText as contentText4, getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
15723
15723
|
import { randomUUID as randomUUID10 } from "node:crypto";
|
|
15724
15724
|
import { realpathSync as realpathSync5, statSync as statSync6, unlinkSync as unlinkSync8 } from "node:fs";
|
|
15725
|
+
import { isAbsolute as isAbsolute5, resolve as resolve6 } from "node:path";
|
|
15725
15726
|
var LEAD_INSTANCE_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
15726
15727
|
function createLeadRuntimeState() {
|
|
15727
15728
|
return {
|
|
@@ -15976,6 +15977,13 @@ function createLeadSessionStartRuntime(host) {
|
|
|
15976
15977
|
...host.ownToolsSnapshot()
|
|
15977
15978
|
});
|
|
15978
15979
|
const activeManager = host.isLead() && host.roleTransitions.activeManager();
|
|
15980
|
+
if (activeManager && !sessionSignal.aborted) {
|
|
15981
|
+
try {
|
|
15982
|
+
await host.roleTransitions.reconcileCurrentManagerAssignments(ctx);
|
|
15983
|
+
} catch (error2) {
|
|
15984
|
+
host.appendRoleError(ctx, error2);
|
|
15985
|
+
}
|
|
15986
|
+
}
|
|
15979
15987
|
if (host.isLead() && (host.roleTransitions.chiefModeActive() || activeManager)) {
|
|
15980
15988
|
host.startSupervisionUI(ctx);
|
|
15981
15989
|
} else host.leadStatusRuntime.start(ctx);
|
|
@@ -16889,6 +16897,7 @@ function registerLeadRuntime(pi, options) {
|
|
|
16889
16897
|
pi,
|
|
16890
16898
|
isCurrentChief: (ctx) => options.leadRuntime.chiefMode === "active" && !!options.leadRuntime.chiefLease && options.leadRuntime.chiefLease.descriptor.piSessionId === ctx.sessionManager.getSessionId(),
|
|
16891
16899
|
currentManager: (...args) => roleTransitions.currentManager(...args),
|
|
16900
|
+
reconcileProjectAssignments: (...args) => roleTransitions.reconcileProjectAssignments(...args),
|
|
16892
16901
|
currentChief: (...args) => roleTransitions.currentChief(...args),
|
|
16893
16902
|
currentChiefAuthority: (...args) => roleTransitions.currentChiefAuthority(...args),
|
|
16894
16903
|
latestMessageText: (ctx) => coordinationRuntime.latestCustomMessageText(
|
|
@@ -17239,6 +17248,7 @@ function registerLeadRuntime(pi, options) {
|
|
|
17239
17248
|
liveAgent,
|
|
17240
17249
|
liveLead,
|
|
17241
17250
|
currentManager: (...args) => roleTransitions.currentManager(...args),
|
|
17251
|
+
reconcileProjectAssignments: (...args) => roleTransitions.reconcileProjectAssignments(...args),
|
|
17242
17252
|
sameManagerDescriptor: projectHost.sameManagerDescriptor,
|
|
17243
17253
|
withProjectWorkLock: (...args) => roleTransitions.withProjectWorkLock(...args),
|
|
17244
17254
|
withProjectAssignmentLock: (...args) => roleTransitions.withProjectAssignmentLock(...args),
|
|
@@ -17713,6 +17723,8 @@ function createLeadSupervisionRuntime(host) {
|
|
|
17713
17723
|
manager.workspaceId,
|
|
17714
17724
|
ctx.signal
|
|
17715
17725
|
);
|
|
17726
|
+
if (group.repoKey !== manager.repoKey || group.primaryWorkspaceId !== manager.workspaceId)
|
|
17727
|
+
throw new Error("Manager workspace is not the assigned project");
|
|
17716
17728
|
const [leads, topology] = await Promise.all([
|
|
17717
17729
|
directReports(ctx, suppliedInventory, suppliedAgents),
|
|
17718
17730
|
host.runHerdr(
|
|
@@ -17722,8 +17734,15 @@ function createLeadSupervisionRuntime(host) {
|
|
|
17722
17734
|
{ signal: ctx.signal }
|
|
17723
17735
|
)
|
|
17724
17736
|
]);
|
|
17725
|
-
if (topology?.source?.repo_key !== group.repoKey || !Array.isArray(topology?.worktrees))
|
|
17737
|
+
if (topology?.source?.repo_key !== group.repoKey || topology?.source?.source_workspace_id !== group.primaryWorkspaceId || !Array.isArray(topology?.worktrees))
|
|
17726
17738
|
throw new Error("Herdr worktree topology is not authoritative");
|
|
17739
|
+
if (typeof topology.source.source_checkout_path === "string")
|
|
17740
|
+
await host.reconcileProjectAssignments?.(
|
|
17741
|
+
ctx,
|
|
17742
|
+
group.repoKey,
|
|
17743
|
+
group.primaryWorkspaceId,
|
|
17744
|
+
topology.source.source_checkout_path
|
|
17745
|
+
);
|
|
17727
17746
|
const assignments = host.listProjectAssignments(
|
|
17728
17747
|
host.supervisionRuntime(),
|
|
17729
17748
|
group.repoKey
|
|
@@ -18760,7 +18779,8 @@ function createLeadProjectRuntime(host) {
|
|
|
18760
18779
|
stopHerdrAgentPreservingPane: stopHerdrAgentPreservingPane2,
|
|
18761
18780
|
expectedSession: expectedSession3,
|
|
18762
18781
|
liveLead,
|
|
18763
|
-
withProjectWorkLock
|
|
18782
|
+
withProjectWorkLock,
|
|
18783
|
+
withProjectAssignmentLock
|
|
18764
18784
|
} = host;
|
|
18765
18785
|
const {
|
|
18766
18786
|
activeRole,
|
|
@@ -18830,35 +18850,44 @@ function createLeadProjectRuntime(host) {
|
|
|
18830
18850
|
);
|
|
18831
18851
|
const primaryWorkspaceId = group.primaryWorkspaceId;
|
|
18832
18852
|
const runtime = supervisionRuntime2();
|
|
18833
|
-
const
|
|
18834
|
-
|
|
18835
|
-
|
|
18836
|
-
|
|
18853
|
+
const branch = operation.branch;
|
|
18854
|
+
const topology = await runHerdr2(
|
|
18855
|
+
pi,
|
|
18856
|
+
ctx,
|
|
18857
|
+
["worktree", "list", "--workspace", primaryWorkspaceId],
|
|
18858
|
+
{ signal }
|
|
18837
18859
|
);
|
|
18860
|
+
if (topology?.source?.source_workspace_id !== primaryWorkspaceId || topology?.source?.repo_key !== manager.repoKey || !Array.isArray(topology?.worktrees))
|
|
18861
|
+
throw new Error("Herdr worktree topology is not authoritative");
|
|
18862
|
+
if (operation.action === "resume")
|
|
18863
|
+
await host.reconcileProjectAssignments(
|
|
18864
|
+
ctx,
|
|
18865
|
+
manager.repoKey,
|
|
18866
|
+
primaryWorkspaceId,
|
|
18867
|
+
typeof topology.source.source_checkout_path === "string" ? topology.source.source_checkout_path : "",
|
|
18868
|
+
true,
|
|
18869
|
+
branch
|
|
18870
|
+
);
|
|
18871
|
+
const existing = readProjectAssignment2(runtime, manager.repoKey, branch);
|
|
18838
18872
|
if (existing && existing.repoKey !== manager.repoKey)
|
|
18839
18873
|
throw new Error("Project assignment belongs to another repository");
|
|
18840
18874
|
if (operation.action === "delegate" && existing)
|
|
18841
18875
|
throw new Error(
|
|
18842
|
-
`Work already exists on ${
|
|
18876
|
+
`Work already exists on ${branch}; resume it with resume_project.`
|
|
18843
18877
|
);
|
|
18844
18878
|
if (operation.action === "resume" && !existing)
|
|
18845
18879
|
throw new Error(
|
|
18846
|
-
`No existing work was found on ${
|
|
18880
|
+
`No existing work was found on ${branch}; start it with delegate_project and a task.`
|
|
18847
18881
|
);
|
|
18848
18882
|
const presentationTask = operation.action === "resume" ? projectTaskSummary(existing.text) : void 0;
|
|
18849
18883
|
const id = operation.action === "resume" ? existing.id : operation.id;
|
|
18850
|
-
const branch = operation.branch;
|
|
18851
|
-
const topology = await runHerdr2(
|
|
18852
|
-
pi,
|
|
18853
|
-
ctx,
|
|
18854
|
-
["worktree", "list", "--workspace", primaryWorkspaceId],
|
|
18855
|
-
{ signal }
|
|
18856
|
-
);
|
|
18857
|
-
if (topology?.source?.source_workspace_id !== primaryWorkspaceId || topology?.source?.repo_key !== manager.repoKey || !Array.isArray(topology?.worktrees))
|
|
18858
|
-
throw new Error("Herdr worktree topology is not authoritative");
|
|
18859
18884
|
const branchWorktrees = topology.worktrees.filter(
|
|
18860
18885
|
(worktree) => worktree?.branch === branch
|
|
18861
18886
|
);
|
|
18887
|
+
if (operation.action === "resume" && branchWorktrees.length === 0)
|
|
18888
|
+
throw new Error(
|
|
18889
|
+
"Worktree availability could not be verified; the existing assignment was preserved. Retry when repository access is available."
|
|
18890
|
+
);
|
|
18862
18891
|
if (branchWorktrees.length > 1)
|
|
18863
18892
|
throw new Error(`Multiple Herdr worktrees match branch ${branch}`);
|
|
18864
18893
|
const live = operation.action === "resume" ? await liveAgent(ctx, existing.id) : [];
|
|
@@ -18977,7 +19006,6 @@ function createLeadProjectRuntime(host) {
|
|
|
18977
19006
|
text: prepared.text,
|
|
18978
19007
|
...prepared.resultBindings.length ? { resultBindings: prepared.resultBindings } : {}
|
|
18979
19008
|
};
|
|
18980
|
-
writeProjectAssignment2(runtime, assignment);
|
|
18981
19009
|
}
|
|
18982
19010
|
const promptPaths = [];
|
|
18983
19011
|
try {
|
|
@@ -19024,6 +19052,8 @@ function createLeadProjectRuntime(host) {
|
|
|
19024
19052
|
);
|
|
19025
19053
|
}
|
|
19026
19054
|
} else {
|
|
19055
|
+
if (operation.action === "resume")
|
|
19056
|
+
throw new Error("The existing project checkout is unavailable");
|
|
19027
19057
|
const args = [
|
|
19028
19058
|
"worktree",
|
|
19029
19059
|
"create",
|
|
@@ -19033,44 +19063,7 @@ function createLeadProjectRuntime(host) {
|
|
|
19033
19063
|
assignment.branch,
|
|
19034
19064
|
"--no-focus"
|
|
19035
19065
|
];
|
|
19036
|
-
|
|
19037
|
-
let savedCwd;
|
|
19038
|
-
if (assignment.piSessionFile) {
|
|
19039
|
-
const saved = SessionManager3.open(assignment.piSessionFile);
|
|
19040
|
-
if (saved.getSessionId() !== assignment.id)
|
|
19041
|
-
throw new Error(
|
|
19042
|
-
"Persisted project session identity does not match"
|
|
19043
|
-
);
|
|
19044
|
-
savedCwd = saved.getCwd();
|
|
19045
|
-
} else {
|
|
19046
|
-
const saved = (await SessionManager3.listAll()).filter(
|
|
19047
|
-
(session) => session.id === assignment.id
|
|
19048
|
-
);
|
|
19049
|
-
if (saved.length > 1)
|
|
19050
|
-
throw new Error(
|
|
19051
|
-
`Pi session ${assignment.id} is ambiguous; project work was preserved.`
|
|
19052
|
-
);
|
|
19053
|
-
if (saved.length === 1) {
|
|
19054
|
-
const sessionFile = saved[0].path;
|
|
19055
|
-
if (typeof sessionFile !== "string" || !sessionFile)
|
|
19056
|
-
throw new Error(
|
|
19057
|
-
"Legacy project session has no exact file path"
|
|
19058
|
-
);
|
|
19059
|
-
const session = SessionManager3.open(sessionFile);
|
|
19060
|
-
if (session.getSessionId() !== assignment.id)
|
|
19061
|
-
throw new Error(
|
|
19062
|
-
"Persisted project session identity does not match"
|
|
19063
|
-
);
|
|
19064
|
-
savedCwd = session.getCwd();
|
|
19065
|
-
assignment = await persistLeadSessionFile(
|
|
19066
|
-
assignment,
|
|
19067
|
-
realpathSync5(sessionFile)
|
|
19068
|
-
);
|
|
19069
|
-
}
|
|
19070
|
-
}
|
|
19071
|
-
args.push("--base", assignment.branch);
|
|
19072
|
-
if (savedCwd) args.push("--path", savedCwd);
|
|
19073
|
-
} else args.push("--base", operation.base ?? "HEAD");
|
|
19066
|
+
args.push("--base", operation.base ?? "HEAD");
|
|
19074
19067
|
const created = await runHerdr2(pi, ctx, args, { signal });
|
|
19075
19068
|
workspaceId = created?.workspace?.workspace_id;
|
|
19076
19069
|
paneId = created?.root_pane?.pane_id;
|
|
@@ -19085,6 +19078,40 @@ function createLeadProjectRuntime(host) {
|
|
|
19085
19078
|
(value2) => typeof value2 === "string" && value2
|
|
19086
19079
|
))
|
|
19087
19080
|
throw new Error("Herdr did not return exact worktree identities");
|
|
19081
|
+
if (operation.action === "delegate") {
|
|
19082
|
+
const [common, branchResult] = await Promise.all([
|
|
19083
|
+
pi.exec(
|
|
19084
|
+
"git",
|
|
19085
|
+
["rev-parse", "--path-format=absolute", "--git-common-dir"],
|
|
19086
|
+
{ cwd, signal, timeout: 1e4 }
|
|
19087
|
+
),
|
|
19088
|
+
pi.exec("git", ["symbolic-ref", "--quiet", "--short", "HEAD"], {
|
|
19089
|
+
cwd,
|
|
19090
|
+
signal,
|
|
19091
|
+
timeout: 1e4
|
|
19092
|
+
})
|
|
19093
|
+
]);
|
|
19094
|
+
if (common.code !== 0 || common.killed || common.stderr?.trim() || branchResult.code !== 0 || branchResult.killed || branchResult.stderr?.trim() || !isAbsolute5(String(common.stdout ?? "").trim()) || realpathSync5(String(common.stdout).trim()) !== realpathSync5(manager.repoKey) || String(branchResult.stdout ?? "").trim() !== assignment.branch)
|
|
19095
|
+
throw new Error(
|
|
19096
|
+
"Herdr checkout does not match the assigned repository and branch"
|
|
19097
|
+
);
|
|
19098
|
+
const fresh = await currentManager(ctx);
|
|
19099
|
+
if (!fresh || !sameManagerDescriptor2(fresh, manager))
|
|
19100
|
+
throw new Error(
|
|
19101
|
+
"Manager changed before project assignment was published"
|
|
19102
|
+
);
|
|
19103
|
+
await withProjectAssignmentLock(
|
|
19104
|
+
manager.repoKey,
|
|
19105
|
+
assignment.branch,
|
|
19106
|
+
() => {
|
|
19107
|
+
if (readProjectAssignment2(runtime, manager.repoKey, assignment.branch))
|
|
19108
|
+
throw new Error(
|
|
19109
|
+
`Work already exists on ${assignment.branch}; resume it with resume_project.`
|
|
19110
|
+
);
|
|
19111
|
+
writeProjectAssignment2(runtime, assignment);
|
|
19112
|
+
}
|
|
19113
|
+
);
|
|
19114
|
+
}
|
|
19088
19115
|
await assertUnoccupied(workspaceId);
|
|
19089
19116
|
let lead;
|
|
19090
19117
|
let startedLead = false;
|
|
@@ -20056,8 +20083,8 @@ function createLeadCoordinationRuntime(host) {
|
|
|
20056
20083
|
const enterCoordinationPublication = async () => {
|
|
20057
20084
|
const previous = coordinationPublication;
|
|
20058
20085
|
let release;
|
|
20059
|
-
coordinationPublication = new Promise((
|
|
20060
|
-
release =
|
|
20086
|
+
coordinationPublication = new Promise((resolve8) => {
|
|
20087
|
+
release = resolve8;
|
|
20061
20088
|
});
|
|
20062
20089
|
await previous;
|
|
20063
20090
|
return release;
|
|
@@ -21113,12 +21140,75 @@ async function resolveLeadControllerRole(state, ctx, requestedRole, deps, option
|
|
|
21113
21140
|
);
|
|
21114
21141
|
}
|
|
21115
21142
|
}
|
|
21143
|
+
function gitPathStatus(path) {
|
|
21144
|
+
try {
|
|
21145
|
+
return statSync6(path).isDirectory() ? "present" : "unknown";
|
|
21146
|
+
} catch (error2) {
|
|
21147
|
+
const code = error2.code;
|
|
21148
|
+
return code === "ENOENT" || code === "ENOTDIR" ? "absent" : "unknown";
|
|
21149
|
+
}
|
|
21150
|
+
}
|
|
21151
|
+
function parseGitWorktreeInventory(output) {
|
|
21152
|
+
if (!output.endsWith("\0\0")) return void 0;
|
|
21153
|
+
const fields = output.split("\0");
|
|
21154
|
+
if (fields.pop() !== "") return void 0;
|
|
21155
|
+
const records = [];
|
|
21156
|
+
let record = [];
|
|
21157
|
+
for (const field of fields) {
|
|
21158
|
+
if (field) record.push(field);
|
|
21159
|
+
else if (record.length) {
|
|
21160
|
+
records.push(record);
|
|
21161
|
+
record = [];
|
|
21162
|
+
}
|
|
21163
|
+
}
|
|
21164
|
+
if (record.length) records.push(record);
|
|
21165
|
+
if (!records.length) return void 0;
|
|
21166
|
+
const worktrees = [];
|
|
21167
|
+
for (const values of records) {
|
|
21168
|
+
if (!values[0]?.startsWith("worktree ")) return void 0;
|
|
21169
|
+
let path;
|
|
21170
|
+
let head;
|
|
21171
|
+
let branch;
|
|
21172
|
+
let bare = false;
|
|
21173
|
+
let detached = false;
|
|
21174
|
+
let locked = false;
|
|
21175
|
+
let prunable = false;
|
|
21176
|
+
const seen = /* @__PURE__ */ new Set();
|
|
21177
|
+
for (const value2 of values) {
|
|
21178
|
+
const key = value2.startsWith("worktree ") ? "worktree" : value2.startsWith("HEAD ") ? "HEAD" : value2.startsWith("branch refs/heads/") ? "branch" : value2 === "detached" || value2 === "bare" ? value2 : /^locked(?: |$)/.test(value2) ? "locked" : /^prunable(?: |$)/.test(value2) ? "prunable" : void 0;
|
|
21179
|
+
if (key && seen.has(key)) return void 0;
|
|
21180
|
+
if (key) seen.add(key);
|
|
21181
|
+
if (value2.startsWith("worktree ") && !path) path = value2.slice(9);
|
|
21182
|
+
else if (value2.startsWith("HEAD ") && !head) head = value2.slice(5);
|
|
21183
|
+
else if (value2.startsWith("branch refs/heads/") && !branch)
|
|
21184
|
+
branch = value2.slice("branch refs/heads/".length);
|
|
21185
|
+
else if (value2 === "detached" || value2 === "bare") {
|
|
21186
|
+
if (value2 === "bare") bare = true;
|
|
21187
|
+
else detached = true;
|
|
21188
|
+
} else if (/^locked(?: .*)?$/.test(value2)) locked = true;
|
|
21189
|
+
else if (/^prunable(?: .*)?$/.test(value2)) prunable = true;
|
|
21190
|
+
else if (!/^[^\s]+(?: .*)?$/.test(value2)) return void 0;
|
|
21191
|
+
}
|
|
21192
|
+
if (!path || !isAbsolute5(path) || (bare ? !!branch || detached : !head || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(head) || !branch && !detached) || branch !== void 0 && !branch)
|
|
21193
|
+
return void 0;
|
|
21194
|
+
worktrees.push({
|
|
21195
|
+
path,
|
|
21196
|
+
...branch ? { branch } : {},
|
|
21197
|
+
...detached ? { detached: true } : {},
|
|
21198
|
+
...locked ? { locked: true } : {},
|
|
21199
|
+
...prunable ? { prunable: true } : {}
|
|
21200
|
+
});
|
|
21201
|
+
}
|
|
21202
|
+
if (new Set(worktrees.map((worktree) => worktree.path)).size !== worktrees.length)
|
|
21203
|
+
return void 0;
|
|
21204
|
+
return worktrees;
|
|
21205
|
+
}
|
|
21116
21206
|
function createLeadRoleTransitions(state, host) {
|
|
21117
21207
|
const projectWorkLocks = /* @__PURE__ */ new Map();
|
|
21118
21208
|
const withProjectWorkLock = async (key, operation) => {
|
|
21119
21209
|
const previous = projectWorkLocks.get(key);
|
|
21120
21210
|
let release;
|
|
21121
|
-
const current = new Promise((
|
|
21211
|
+
const current = new Promise((resolve8) => release = resolve8);
|
|
21122
21212
|
projectWorkLocks.set(key, current);
|
|
21123
21213
|
await previous;
|
|
21124
21214
|
try {
|
|
@@ -21331,29 +21421,177 @@ function createLeadRoleTransitions(state, host) {
|
|
|
21331
21421
|
const assignment = scope ? projectAssignmentForScope(scope, ctx.sessionManager.getSessionId()) : void 0;
|
|
21332
21422
|
return assignment ? currentManager(ctx, scope) : host.currentChiefAuthority(ctx);
|
|
21333
21423
|
};
|
|
21334
|
-
const removeProjectAssignmentState = (assignment,
|
|
21424
|
+
const removeProjectAssignmentState = (assignment, _ctx) => {
|
|
21335
21425
|
const runtime = host.supervisionRuntime();
|
|
21336
21426
|
const current = host.readProjectAssignment(
|
|
21337
21427
|
runtime,
|
|
21338
21428
|
assignment.repoKey,
|
|
21339
21429
|
assignment.branch
|
|
21340
21430
|
);
|
|
21341
|
-
if (!current || current.id !== assignment.id)
|
|
21431
|
+
if (!current || current.repoKey !== assignment.repoKey || current.branch !== assignment.branch || current.id !== assignment.id)
|
|
21432
|
+
return false;
|
|
21433
|
+
host.removeProjectMessages(runtime, assignment.repoKey, assignment.branch);
|
|
21342
21434
|
host.removeProjectAssignment(
|
|
21343
21435
|
runtime,
|
|
21344
21436
|
assignment.repoKey,
|
|
21345
21437
|
assignment.branch
|
|
21346
21438
|
);
|
|
21439
|
+
return true;
|
|
21440
|
+
};
|
|
21441
|
+
const gitWorktreeInventory = async (ctx, repoKey, primaryWorkspaceId, sourceCheckoutPath) => {
|
|
21442
|
+
if (!isAbsolute5(sourceCheckoutPath)) return void 0;
|
|
21347
21443
|
try {
|
|
21348
|
-
host.
|
|
21349
|
-
|
|
21350
|
-
|
|
21351
|
-
|
|
21444
|
+
const scope = await host.worktreeGroupScope(
|
|
21445
|
+
ctx,
|
|
21446
|
+
primaryWorkspaceId,
|
|
21447
|
+
ctx.signal
|
|
21352
21448
|
);
|
|
21353
|
-
|
|
21354
|
-
|
|
21449
|
+
if (scope.repoKey !== repoKey || scope.primaryWorkspaceId !== primaryWorkspaceId)
|
|
21450
|
+
return void 0;
|
|
21451
|
+
const rootResult = await host.pi.exec(
|
|
21452
|
+
"git",
|
|
21453
|
+
["rev-parse", "--path-format=absolute", "--git-common-dir"],
|
|
21454
|
+
{ cwd: sourceCheckoutPath, signal: ctx.signal, timeout: 1e4 }
|
|
21455
|
+
);
|
|
21456
|
+
if (rootResult.code !== 0 || rootResult.killed || rootResult.stderr?.trim())
|
|
21457
|
+
return void 0;
|
|
21458
|
+
const commonDir = String(rootResult.stdout ?? "").trim();
|
|
21459
|
+
if (!commonDir || !isAbsolute5(commonDir)) return void 0;
|
|
21460
|
+
const result = await host.pi.exec(
|
|
21461
|
+
"git",
|
|
21462
|
+
["worktree", "list", "--porcelain", "-z"],
|
|
21463
|
+
{ cwd: sourceCheckoutPath, signal: ctx.signal, timeout: 1e4 }
|
|
21464
|
+
);
|
|
21465
|
+
if (result.code !== 0 || result.killed || result.stderr?.trim())
|
|
21466
|
+
return void 0;
|
|
21467
|
+
const inventory = parseGitWorktreeInventory(String(result.stdout ?? ""));
|
|
21468
|
+
if (!inventory || realpathSync5(commonDir) !== realpathSync5(repoKey) || gitPathStatus(sourceCheckoutPath) !== "present")
|
|
21469
|
+
return void 0;
|
|
21470
|
+
return inventory;
|
|
21471
|
+
} catch {
|
|
21472
|
+
return void 0;
|
|
21473
|
+
}
|
|
21474
|
+
};
|
|
21475
|
+
const reconcileProjectAssignments = async (ctx, repoKey, primaryWorkspaceId, sourceCheckoutPath, projectWorkLocked = false, onlyBranch, retryLockContention = false) => {
|
|
21476
|
+
const manager = await currentManager(ctx);
|
|
21477
|
+
if (!manager || manager.repoKey !== repoKey || manager.workspaceId !== primaryWorkspaceId)
|
|
21478
|
+
return;
|
|
21479
|
+
const assignments = listProjectAssignments(
|
|
21480
|
+
host.supervisionRuntime(),
|
|
21481
|
+
repoKey
|
|
21482
|
+
);
|
|
21483
|
+
const initialInventory = await gitWorktreeInventory(
|
|
21484
|
+
ctx,
|
|
21485
|
+
repoKey,
|
|
21486
|
+
primaryWorkspaceId,
|
|
21487
|
+
sourceCheckoutPath
|
|
21488
|
+
);
|
|
21489
|
+
if (!initialInventory) return;
|
|
21490
|
+
const knownCheckoutPath = (assignment) => {
|
|
21491
|
+
if (!assignment.piSessionFile) return void 0;
|
|
21492
|
+
try {
|
|
21493
|
+
const session = host.SessionManager.open(assignment.piSessionFile);
|
|
21494
|
+
if (session.getSessionId() !== assignment.id) return void 0;
|
|
21495
|
+
return resolve6(session.getCwd());
|
|
21496
|
+
} catch {
|
|
21497
|
+
return void 0;
|
|
21498
|
+
}
|
|
21499
|
+
};
|
|
21500
|
+
const checkoutMaySurvive = (assignment, inventory) => {
|
|
21501
|
+
const cwd = knownCheckoutPath(assignment);
|
|
21502
|
+
if (cwd)
|
|
21503
|
+
return inventory.some(
|
|
21504
|
+
(worktree) => resolve6(worktree.path) === cwd && gitPathStatus(worktree.path) !== "absent"
|
|
21505
|
+
);
|
|
21506
|
+
return inventory.some(
|
|
21507
|
+
(worktree) => resolve6(worktree.path) !== resolve6(sourceCheckoutPath) && gitPathStatus(worktree.path) !== "absent"
|
|
21508
|
+
);
|
|
21509
|
+
};
|
|
21510
|
+
for (const observed of assignments) {
|
|
21511
|
+
if (onlyBranch && observed.branch !== onlyBranch) continue;
|
|
21512
|
+
if (checkoutMaySurvive(observed, initialInventory)) continue;
|
|
21513
|
+
const initialMatches = initialInventory.filter(
|
|
21514
|
+
(worktree) => worktree.branch === observed.branch
|
|
21515
|
+
);
|
|
21516
|
+
if (initialMatches.length > 1 || initialMatches.some(
|
|
21517
|
+
(worktree) => worktree.locked || !worktree.prunable || gitPathStatus(worktree.path) !== "absent"
|
|
21518
|
+
))
|
|
21519
|
+
continue;
|
|
21520
|
+
const retireIfAbsent = async () => {
|
|
21521
|
+
const latestManager = await currentManager(ctx);
|
|
21522
|
+
if (!latestManager || !host.sameManagerDescriptor(latestManager, manager))
|
|
21523
|
+
return;
|
|
21524
|
+
const retireUnderAssignmentLock = async () => {
|
|
21525
|
+
const latestManager2 = await currentManager(ctx);
|
|
21526
|
+
if (!latestManager2 || !host.sameManagerDescriptor(latestManager2, manager))
|
|
21527
|
+
return;
|
|
21528
|
+
const current = host.readProjectAssignment(
|
|
21529
|
+
host.supervisionRuntime(),
|
|
21530
|
+
repoKey,
|
|
21531
|
+
observed.branch
|
|
21532
|
+
);
|
|
21533
|
+
if (!current || current.repoKey !== repoKey || current.branch !== observed.branch || current.id !== observed.id)
|
|
21534
|
+
return;
|
|
21535
|
+
const inventory = await gitWorktreeInventory(
|
|
21536
|
+
ctx,
|
|
21537
|
+
repoKey,
|
|
21538
|
+
primaryWorkspaceId,
|
|
21539
|
+
sourceCheckoutPath
|
|
21540
|
+
);
|
|
21541
|
+
if (!inventory) return;
|
|
21542
|
+
if (checkoutMaySurvive(current, inventory)) return;
|
|
21543
|
+
const matches = inventory.filter(
|
|
21544
|
+
(worktree) => worktree.branch === observed.branch
|
|
21545
|
+
);
|
|
21546
|
+
if (matches.length > 1 || matches.some(
|
|
21547
|
+
(worktree) => worktree.locked || !worktree.prunable || gitPathStatus(worktree.path) !== "absent"
|
|
21548
|
+
))
|
|
21549
|
+
return;
|
|
21550
|
+
removeProjectAssignmentState(current, ctx);
|
|
21551
|
+
};
|
|
21552
|
+
const deadline = retryLockContention ? Date.now() + 3e4 : 0;
|
|
21553
|
+
for (; ; ) {
|
|
21554
|
+
try {
|
|
21555
|
+
await withProjectAssignmentLock(
|
|
21556
|
+
repoKey,
|
|
21557
|
+
observed.branch,
|
|
21558
|
+
retireUnderAssignmentLock
|
|
21559
|
+
);
|
|
21560
|
+
return;
|
|
21561
|
+
} catch (error2) {
|
|
21562
|
+
if (!(error2 instanceof ProcessLockOccupiedError)) throw error2;
|
|
21563
|
+
if (!retryLockContention) return;
|
|
21564
|
+
if (ctx.signal.aborted) throw ctx.signal.reason;
|
|
21565
|
+
if (Date.now() >= deadline) throw error2;
|
|
21566
|
+
await host.delay(50, void 0, { signal: ctx.signal });
|
|
21567
|
+
}
|
|
21568
|
+
}
|
|
21569
|
+
};
|
|
21570
|
+
if (projectWorkLocked) await retireIfAbsent();
|
|
21571
|
+
else
|
|
21572
|
+
await withProjectWorkLock(
|
|
21573
|
+
`${repoKey}\0${observed.branch}`,
|
|
21574
|
+
retireIfAbsent
|
|
21575
|
+
);
|
|
21355
21576
|
}
|
|
21356
|
-
|
|
21577
|
+
};
|
|
21578
|
+
const reconcileCurrentManagerAssignments = async (ctx) => {
|
|
21579
|
+
const manager = await currentManager(ctx);
|
|
21580
|
+
if (!manager) return;
|
|
21581
|
+
const topology = await host.runHerdr(
|
|
21582
|
+
host.pi,
|
|
21583
|
+
ctx,
|
|
21584
|
+
["worktree", "list", "--workspace", manager.workspaceId],
|
|
21585
|
+
{ signal: ctx.signal }
|
|
21586
|
+
);
|
|
21587
|
+
if (topology?.source?.repo_key !== manager.repoKey || topology?.source?.source_workspace_id !== manager.workspaceId || typeof topology?.source?.source_checkout_path !== "string")
|
|
21588
|
+
throw new Error("Manager worktree topology is not authoritative");
|
|
21589
|
+
await reconcileProjectAssignments(
|
|
21590
|
+
ctx,
|
|
21591
|
+
manager.repoKey,
|
|
21592
|
+
manager.workspaceId,
|
|
21593
|
+
topology.source.source_checkout_path
|
|
21594
|
+
);
|
|
21357
21595
|
};
|
|
21358
21596
|
const takeover = async (ctx) => {
|
|
21359
21597
|
if (activeLeadRole(state) !== "lead" || state.chiefMode === "active" || state.roleSuspended)
|
|
@@ -21392,38 +21630,31 @@ function createLeadRoleTransitions(state, host) {
|
|
|
21392
21630
|
return true;
|
|
21393
21631
|
};
|
|
21394
21632
|
const retireRemovedProjectWork = async (removed, ctx, signal) => {
|
|
21395
|
-
|
|
21396
|
-
|
|
21397
|
-
|
|
21398
|
-
|
|
21399
|
-
|
|
21400
|
-
|
|
21401
|
-
|
|
21402
|
-
|
|
21633
|
+
let scope;
|
|
21634
|
+
try {
|
|
21635
|
+
scope = await currentWorktreeScope(ctx);
|
|
21636
|
+
} catch (error2) {
|
|
21637
|
+
if (error2 instanceof OperationError && error2.detail.details?.herdrCode === "workspace_not_found")
|
|
21638
|
+
return;
|
|
21639
|
+
throw error2;
|
|
21640
|
+
}
|
|
21403
21641
|
if (!scope || scope.repoKey !== removed.repoKey) return;
|
|
21404
|
-
await
|
|
21405
|
-
|
|
21406
|
-
|
|
21407
|
-
|
|
21408
|
-
|
|
21409
|
-
|
|
21410
|
-
|
|
21411
|
-
|
|
21412
|
-
|
|
21413
|
-
|
|
21414
|
-
|
|
21415
|
-
|
|
21416
|
-
|
|
21417
|
-
|
|
21418
|
-
|
|
21419
|
-
|
|
21420
|
-
} catch (error2) {
|
|
21421
|
-
if (!(error2 instanceof ProcessLockOccupiedError) || Date.now() >= deadline)
|
|
21422
|
-
throw error2;
|
|
21423
|
-
await host.delay(50, void 0, { signal });
|
|
21424
|
-
}
|
|
21425
|
-
}
|
|
21426
|
-
}
|
|
21642
|
+
const topology = await host.runHerdr(
|
|
21643
|
+
host.pi,
|
|
21644
|
+
ctx,
|
|
21645
|
+
["worktree", "list", "--workspace", scope.primaryWorkspaceId],
|
|
21646
|
+
{ signal }
|
|
21647
|
+
);
|
|
21648
|
+
if (topology?.source?.repo_key !== removed.repoKey || topology?.source?.source_workspace_id !== scope.primaryWorkspaceId || typeof topology?.source?.source_checkout_path !== "string")
|
|
21649
|
+
return;
|
|
21650
|
+
await reconcileProjectAssignments(
|
|
21651
|
+
ctx,
|
|
21652
|
+
removed.repoKey,
|
|
21653
|
+
scope.primaryWorkspaceId,
|
|
21654
|
+
topology.source.source_checkout_path,
|
|
21655
|
+
false,
|
|
21656
|
+
removed.branch,
|
|
21657
|
+
true
|
|
21427
21658
|
);
|
|
21428
21659
|
};
|
|
21429
21660
|
const beginShutdownRole = () => {
|
|
@@ -21692,6 +21923,11 @@ function createLeadRoleTransitions(state, host) {
|
|
|
21692
21923
|
host.advancePeerPresenceGeneration();
|
|
21693
21924
|
if (host.coordinationHealthy()) await host.schedulePeerPresence(ctx);
|
|
21694
21925
|
await host.publishLeadRole(ctx, "inactive", state.chiefModeGeneration);
|
|
21926
|
+
try {
|
|
21927
|
+
await reconcileCurrentManagerAssignments(ctx);
|
|
21928
|
+
} catch (error2) {
|
|
21929
|
+
host.appendDurableError(host.pi, ctx, "pi_herdsman_state_error", error2);
|
|
21930
|
+
}
|
|
21695
21931
|
return "Manager mode active.";
|
|
21696
21932
|
} catch (error2) {
|
|
21697
21933
|
state.controllerRole = "lead";
|
|
@@ -22033,11 +22269,13 @@ function createLeadRoleTransitions(state, host) {
|
|
|
22033
22269
|
withProjectAssignmentLock,
|
|
22034
22270
|
publishProjectMessage,
|
|
22035
22271
|
currentManager,
|
|
22272
|
+
reconcileCurrentManagerAssignments,
|
|
22036
22273
|
currentChief,
|
|
22037
22274
|
currentChiefAuthority,
|
|
22038
22275
|
currentSupervisor,
|
|
22039
22276
|
takeover,
|
|
22040
22277
|
retireRemovedProjectWork,
|
|
22278
|
+
reconcileProjectAssignments,
|
|
22041
22279
|
clearLeadContext: () => {
|
|
22042
22280
|
state.leadContext = void 0;
|
|
22043
22281
|
}
|
|
@@ -22707,9 +22945,9 @@ Use reply_agent with agent="${ask.agentLabel}" to answer this question.`,
|
|
|
22707
22945
|
const reconcileRoleTools = () => roleTransitions.reconcileRoleTools();
|
|
22708
22946
|
const isCurrentChief = (ctx) => leadRuntime.chiefMode === "active" && !!leadRuntime.chiefLease && leadRuntime.chiefLease.descriptor.piSessionId === ctx.sessionManager.getSessionId();
|
|
22709
22947
|
const chiefSystemPrompt = (options) => {
|
|
22710
|
-
const agentDir =
|
|
22948
|
+
const agentDir = resolve7(getAgentDir4());
|
|
22711
22949
|
const globalInstructions = (options?.contextFiles ?? []).filter(
|
|
22712
|
-
({ path, content }) => typeof path === "string" && typeof content === "string" &&
|
|
22950
|
+
({ path, content }) => typeof path === "string" && typeof content === "string" && resolve7(dirname7(path)) === agentDir && content.length > 0
|
|
22713
22951
|
).map(({ content }) => content).join("\n\n");
|
|
22714
22952
|
return [
|
|
22715
22953
|
CHIEF_ROLE_CHARTER,
|
|
@@ -22744,6 +22982,7 @@ ${globalInstructions}
|
|
|
22744
22982
|
paneId: () => process.env.HERDR_PANE_ID,
|
|
22745
22983
|
getSessionName: () => pi.getSessionName(),
|
|
22746
22984
|
runHerdr,
|
|
22985
|
+
SessionManager: SessionManager2,
|
|
22747
22986
|
reportLeadMetadata,
|
|
22748
22987
|
workspaceId: () => process.env.HERDR_WORKSPACE_ID,
|
|
22749
22988
|
identity: {
|