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/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 resolve6 } from "node:path";
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.1",
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((resolve7, reject) => {
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) : resolve7(state);
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 existing = readProjectAssignment2(
18834
- runtime,
18835
- manager.repoKey,
18836
- operation.branch
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 ${operation.branch}; resume it with resume_project.`
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 ${operation.branch}; start it with delegate_project and a task.`
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
- if (operation.action === "resume") {
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((resolve7) => {
20060
- release = resolve7;
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((resolve7) => release = resolve7);
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, ctx) => {
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) return false;
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.removeProjectMessages(
21349
- runtime,
21350
- assignment.repoKey,
21351
- assignment.branch
21444
+ const scope = await host.worktreeGroupScope(
21445
+ ctx,
21446
+ primaryWorkspaceId,
21447
+ ctx.signal
21352
21448
  );
21353
- } catch (error2) {
21354
- host.appendDurableError(host.pi, ctx, "pi_herdsman_state_error", error2);
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
- return true;
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
- const runtime = host.supervisionRuntime();
21396
- const observed = host.readProjectAssignment(
21397
- runtime,
21398
- removed.repoKey,
21399
- removed.branch
21400
- );
21401
- if (!observed) return;
21402
- const scope = await currentWorktreeScope(ctx);
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 withProjectWorkLock(
21405
- `${removed.repoKey}\0${removed.branch}`,
21406
- async () => {
21407
- const deadline = Date.now() + 3e4;
21408
- for (; ; ) {
21409
- if (signal.aborted)
21410
- throw signal.reason ?? new Error("operation aborted");
21411
- try {
21412
- await withProjectAssignmentLock(
21413
- removed.repoKey,
21414
- removed.branch,
21415
- async () => {
21416
- removeProjectAssignmentState(observed, ctx);
21417
- }
21418
- );
21419
- return;
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 = resolve6(getAgentDir4());
22948
+ const agentDir = resolve7(getAgentDir4());
22711
22949
  const globalInstructions = (options?.contextFiles ?? []).filter(
22712
- ({ path, content }) => typeof path === "string" && typeof content === "string" && resolve6(dirname7(path)) === agentDir && content.length > 0
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: {