pi-herdsman 0.20.0 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
15
15
  // package.json
16
16
  var package_default = {
17
17
  name: "pi-herdsman",
18
- version: "0.20.0",
18
+ version: "0.20.1",
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",
@@ -6128,10 +6128,7 @@ var StatusWidget = class {
6128
6128
  const s = this.snapshot;
6129
6129
  const suffix = s.unavailable ? "unavailable" : `${formatStatusCounts(s.agents)}${s.stale ? " \xB7 stale" : ""}`;
6130
6130
  const availableWidth = Math.max(0, width);
6131
- const breadcrumb = renderBreadcrumb(
6132
- s.breadcrumb ?? ["herd"],
6133
- availableWidth
6134
- );
6131
+ const breadcrumb = renderBreadcrumb(s.breadcrumb ?? ["?"], availableWidth);
6135
6132
  const elapsed = formatElapsed(s.herdRunStartedAt, Date.now());
6136
6133
  const run = !s.identityOnly && elapsed ? ` \xB7 ${elapsed}` : "";
6137
6134
  const suffixText = s.identityOnly || !suffix ? "" : ` ${suffix}`;
@@ -7378,8 +7375,10 @@ function claimManagerLease(identity, runtime = supervisionRuntime()) {
7378
7375
  };
7379
7376
  }
7380
7377
  var PROJECT_ASSIGNMENT_MAX_BYTES = 1024 * 1024;
7378
+ var PROJECT_ASSIGNMENT_FILE_MAX_BYTES = PROJECT_ASSIGNMENT_MAX_BYTES * 2;
7381
7379
  function projectAssignmentBytes(assignment) {
7382
- return Buffer.byteLength(`${JSON.stringify(assignment)}
7380
+ const { piSessionFile: _, ...payload } = assignment;
7381
+ return Buffer.byteLength(`${JSON.stringify(payload)}
7383
7382
  `, "utf8");
7384
7383
  }
7385
7384
  function projectAssignmentPath(runtime, repoKey, branch) {
@@ -7395,17 +7394,17 @@ function validProjectAssignment(value2) {
7395
7394
  if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) return false;
7396
7395
  const r = value2;
7397
7396
  const required = ["version", "id", "repoKey", "branch", "text"];
7398
- const optional = ["resultBindings"];
7397
+ const optional = ["piSessionFile", "resultBindings"];
7399
7398
  return required.every((key) => Object.hasOwn(r, key)) && Object.keys(r).every(
7400
7399
  (key) => required.includes(key) || optional.includes(key)
7401
- ) && Object.keys(r).length >= required.length && r.version === 2 && UUID3.test(String(r.id)) && validNativeIdentity(r.repoKey) && validNativeIdentity(r.branch) && typeof r.text === "string" && r.text.length > 0 && (r.resultBindings === void 0 || Array.isArray(r.resultBindings) && r.resultBindings.every(isResultBinding));
7400
+ ) && Object.keys(r).length >= required.length && r.version === 2 && UUID3.test(String(r.id)) && validNativeIdentity(r.repoKey) && validNativeIdentity(r.branch) && typeof r.text === "string" && r.text.length > 0 && (r.piSessionFile === void 0 || typeof r.piSessionFile === "string" && r.piSessionFile.length > 0) && (r.resultBindings === void 0 || Array.isArray(r.resultBindings) && r.resultBindings.every(isResultBinding));
7402
7401
  }
7403
7402
  function writeProjectAssignment(runtime, assignment) {
7404
7403
  if (!validProjectAssignment(assignment))
7405
7404
  throw new Error("Invalid project assignment");
7406
7405
  const content = `${JSON.stringify(assignment)}
7407
7406
  `;
7408
- if (projectAssignmentBytes(assignment) > PROJECT_ASSIGNMENT_MAX_BYTES)
7407
+ if (projectAssignmentBytes(assignment) > PROJECT_ASSIGNMENT_MAX_BYTES || Buffer.byteLength(content, "utf8") > PROJECT_ASSIGNMENT_FILE_MAX_BYTES)
7409
7408
  throw new Error("Project assignment is too large");
7410
7409
  const path = projectAssignmentPath(
7411
7410
  runtime,
@@ -7444,7 +7443,7 @@ function readProjectAssignment(runtime, repoKey, branch) {
7444
7443
  function readProjectAssignmentFile(path, repoKey, branch) {
7445
7444
  let value2;
7446
7445
  try {
7447
- if (statSync4(path).size > PROJECT_ASSIGNMENT_MAX_BYTES)
7446
+ if (statSync4(path).size > PROJECT_ASSIGNMENT_FILE_MAX_BYTES)
7448
7447
  throw new Error("file is too large");
7449
7448
  value2 = JSON.parse(readFileSync8(path, "utf8"));
7450
7449
  } catch (error2) {
@@ -7454,6 +7453,8 @@ function readProjectAssignmentFile(path, repoKey, branch) {
7454
7453
  }
7455
7454
  if (!validProjectAssignment(value2))
7456
7455
  throw projectAssignmentReadError(path, "invalid assignment schema");
7456
+ if (projectAssignmentBytes(value2) > PROJECT_ASSIGNMENT_MAX_BYTES)
7457
+ throw projectAssignmentReadError(path, "assignment payload is too large");
7457
7458
  if (value2.repoKey !== repoKey || basename5(dirname6(path)) !== createHash6("sha256").update(repoKey).digest("hex") || branch !== void 0 && value2.branch !== branch || basename5(path) !== `${createHash6("sha256").update(value2.branch).digest("hex")}.json`)
7458
7459
  throw projectAssignmentReadError(path, "assignment identity mismatch");
7459
7460
  return value2;
@@ -7977,7 +7978,7 @@ function buildAgentStatusSnapshot(view, ctx, host) {
7977
7978
  stale: false,
7978
7979
  unavailable: false,
7979
7980
  ...herdStartedAt !== void 0 ? { herdRunStartedAt: herdStartedAt } : {},
7980
- breadcrumb: host.scope.kind === "lead" ? ["herd"] : statusBreadcrumb(
7981
+ breadcrumb: host.scope.kind === "lead" ? ["lead"] : statusBreadcrumb(
7981
7982
  view,
7982
7983
  host.environmentIdentity(ctx),
7983
7984
  host.identityFromEnvironment(),
@@ -8000,8 +8001,7 @@ function createAgentStatusRuntime() {
8000
8001
  let lastValidStatus = {
8001
8002
  agents: [],
8002
8003
  stale: false,
8003
- unavailable: true,
8004
- breadcrumb: ["herd"]
8004
+ unavailable: true
8005
8005
  };
8006
8006
  const widgetSnapshot = (snapshot) => {
8007
8007
  const pendingStarts = options?.pendingStartEntries() ?? [];
@@ -8045,11 +8045,12 @@ function createAgentStatusRuntime() {
8045
8045
  }
8046
8046
  statusInFlight = true;
8047
8047
  try {
8048
- lastValidStatus = await configured.loadSnapshot(ctx);
8048
+ const snapshot = await configured.loadSnapshot(ctx);
8049
8049
  if (generation !== statusGeneration || ctx !== statusContext) return;
8050
- reconcilePendingStarts(lastValidStatus);
8050
+ lastValidStatus = snapshot;
8051
+ reconcilePendingStarts(snapshot);
8051
8052
  if (generation === statusGeneration && ctx === statusContext && statusWidgetGeneration === generation)
8052
- statusWidget?.setSnapshot(widgetSnapshot(lastValidStatus));
8053
+ statusWidget?.setSnapshot(widgetSnapshot(snapshot));
8053
8054
  } catch {
8054
8055
  if (generation === statusGeneration && ctx === statusContext)
8055
8056
  statusWidget?.setSnapshot(
@@ -8102,8 +8103,7 @@ function createAgentStatusRuntime() {
8102
8103
  statusContext = ctx;
8103
8104
  ctx.ui.setWidget("pi-herdsman", (tui, theme) => {
8104
8105
  const widget = createStatusWidget(() => tui.requestRender(), theme);
8105
- const initial = options?.initialWidgetSnapshot?.();
8106
- if (initial) widget.setSnapshot(initial);
8106
+ widget.setSnapshot(widgetSnapshot(lastValidStatus));
8107
8107
  if (generation === statusGeneration && ctx === statusContext) {
8108
8108
  statusWidget = widget;
8109
8109
  statusWidgetGeneration = generation;
@@ -8129,6 +8129,11 @@ function createAgentStatusRuntime() {
8129
8129
  statusWidgetGeneration = 0;
8130
8130
  statusContext = void 0;
8131
8131
  requestActive = false;
8132
+ lastValidStatus = {
8133
+ agents: [],
8134
+ stale: false,
8135
+ unavailable: true
8136
+ };
8132
8137
  beforeActivate?.();
8133
8138
  statusContext = ctx;
8134
8139
  return ++statusGeneration;
@@ -8506,7 +8511,7 @@ function statusBreadcrumb(snapshot, candidate, identity, sameIdentity) {
8506
8511
  ({ state }) => state.piSessionId === ownerSessionId
8507
8512
  ))
8508
8513
  return ["?", ...definitions.reverse()];
8509
- return snapshot.leadSessionIds.includes(ownerSessionId) ? ["herd", ...definitions.reverse()] : ["?", ...definitions.reverse()];
8514
+ return snapshot.leadSessionIds.includes(ownerSessionId) ? ["lead", ...definitions.reverse()] : ["?", ...definitions.reverse()];
8510
8515
  }
8511
8516
  }
8512
8517
  function sessionAgentIdentity(entries, sessionId) {
@@ -15301,7 +15306,7 @@ import { getSelectListTheme } from "@earendil-works/pi-coding-agent";
15301
15306
  import { visibleWidth as visibleWidth2 } from "@earendil-works/pi-tui";
15302
15307
  import { contentText as contentText4, getSupportedThinkingLevels } from "@earendil-works/pi-ai";
15303
15308
  import { randomUUID as randomUUID10 } from "node:crypto";
15304
- import { statSync as statSync6, unlinkSync as unlinkSync8 } from "node:fs";
15309
+ import { realpathSync as realpathSync5, statSync as statSync6, unlinkSync as unlinkSync8 } from "node:fs";
15305
15310
  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}$/;
15306
15311
  function createLeadRuntimeState() {
15307
15312
  return {
@@ -15973,7 +15978,7 @@ function createLeadCommandRuntime(host) {
15973
15978
  },
15974
15979
  async showSessionStats(ctx) {
15975
15980
  const managerActive = host.roleActive("manager");
15976
- const stats = await host.collectOwnedSessionUsage(ctx, managerActive);
15981
+ const stats = host.collectOwnedSessionUsage(ctx);
15977
15982
  ctx.ui.notify(
15978
15983
  host.formatSessionUsage(
15979
15984
  stats.current,
@@ -16485,6 +16490,9 @@ function registerLeadRuntime(pi, options) {
16485
16490
  currentManager: (...args) => roleTransitions.currentManager(...args),
16486
16491
  sameManagerDescriptor: projectHost.sameManagerDescriptor,
16487
16492
  withProjectWorkLock: (...args) => roleTransitions.withProjectWorkLock(...args),
16493
+ withProjectAssignmentLock: (...args) => roleTransitions.withProjectAssignmentLock(...args),
16494
+ supervisedSessionFile: (agent, sessionId) => options.supervisionHost.supervisedSessionFile(agent, sessionId),
16495
+ sameObservedSessionPath,
16488
16496
  activeRole: () => activeLeadRole(options.leadRuntime),
16489
16497
  managerLease: () => options.leadRuntime.managerLease,
16490
16498
  currentChiefAuthority: (...args) => roleTransitions.currentChiefAuthority(...args),
@@ -16517,8 +16525,8 @@ function registerLeadRuntime(pi, options) {
16517
16525
  });
16518
16526
  const commandRuntime = createLeadCommandRuntime({
16519
16527
  ...options.commandHost,
16520
- collectOwnedSessionUsage: async (ctx, managerScopeOrLeadIds) => {
16521
- const managerActive = typeof managerScopeOrLeadIds === "boolean" ? managerScopeOrLeadIds : activeLeadRole(options.leadRuntime) === "manager" && !options.leadRuntime.roleSuspended;
16528
+ collectOwnedSessionUsage: (ctx) => {
16529
+ const managerActive = activeLeadRole(options.leadRuntime) === "manager" && !options.leadRuntime.roleSuspended;
16522
16530
  if (!managerActive)
16523
16531
  return options.commandHost.collectOwnedSessionUsage(ctx);
16524
16532
  const lease = options.leadRuntime.managerLease;
@@ -16529,7 +16537,7 @@ function registerLeadRuntime(pi, options) {
16529
16537
  );
16530
16538
  return options.commandHost.collectOwnedSessionUsage(
16531
16539
  ctx,
16532
- assignments.map((assignment) => assignment.id)
16540
+ assignments.map(({ id, piSessionFile }) => ({ id, piSessionFile }))
16533
16541
  );
16534
16542
  },
16535
16543
  isLead: () => true,
@@ -17681,6 +17689,8 @@ function createLeadProjectRuntime(host) {
17681
17689
  HERDSMAN_BUILD: HERDSMAN_BUILD2,
17682
17690
  HERDSMAN_EXTENSION_PATH: HERDSMAN_EXTENSION_PATH2,
17683
17691
  SessionManager: SessionManager3,
17692
+ supervisedSessionFile: supervisedSessionFile2,
17693
+ sameObservedSessionPath: sameObservedSessionPath2,
17684
17694
  currentManager,
17685
17695
  sameManagerDescriptor: sameManagerDescriptor2,
17686
17696
  worktreeGroupScope: worktreeGroupScope2,
@@ -17721,6 +17731,33 @@ function createLeadProjectRuntime(host) {
17721
17731
  prepareCoordinationInput,
17722
17732
  stopProjectLead: projectStop
17723
17733
  } = host;
17734
+ const persistLeadSessionFile = async (assignment, piSessionFile) => host.withProjectAssignmentLock(
17735
+ assignment.repoKey,
17736
+ assignment.branch,
17737
+ () => {
17738
+ const current = host.readProjectAssignment(
17739
+ host.supervisionRuntime(),
17740
+ assignment.repoKey,
17741
+ assignment.branch
17742
+ );
17743
+ if (!current || current.id !== assignment.id)
17744
+ throw new Error("Project assignment changed during Lead activation");
17745
+ if (current.piSessionFile) {
17746
+ if (!sameObservedSessionPath2(current.piSessionFile, piSessionFile))
17747
+ throw new Error(
17748
+ "Lead session path conflicts with persisted project identity"
17749
+ );
17750
+ return current;
17751
+ }
17752
+ const bound = { ...current, piSessionFile };
17753
+ host.writeProjectAssignment(host.supervisionRuntime(), bound);
17754
+ return bound;
17755
+ }
17756
+ );
17757
+ const bindLeadSessionFile = async (assignment, lead) => {
17758
+ const piSessionFile = supervisedSessionFile2(lead, assignment.id);
17759
+ return piSessionFile ? persistLeadSessionFile(assignment, piSessionFile) : assignment;
17760
+ };
17724
17761
  const activateProjectLead = async (params, ctx, signal) => {
17725
17762
  const repoKey = leadRuntime.managerLease?.descriptor.repoKey;
17726
17763
  if (!repoKey) throw new Error("Manager lease is no longer active");
@@ -17806,6 +17843,7 @@ function createLeadProjectRuntime(host) {
17806
17843
  operationName,
17807
17844
  `Lead ${id}`
17808
17845
  );
17846
+ await bindLeadSessionFile(existing, live[0]);
17809
17847
  writeChiefMessage2({
17810
17848
  version: 2,
17811
17849
  build: HERDSMAN_BUILD2,
@@ -17952,15 +17990,42 @@ function createLeadProjectRuntime(host) {
17952
17990
  "--no-focus"
17953
17991
  ];
17954
17992
  if (operation.action === "resume") {
17955
- const saved = (await SessionManager3.listAll()).filter(
17956
- (session) => session.id === assignment.id
17957
- );
17958
- if (saved.length > 1)
17959
- throw new Error(
17960
- `Pi session ${assignment.id} is ambiguous; project work was preserved.`
17993
+ let savedCwd;
17994
+ if (assignment.piSessionFile) {
17995
+ const saved = SessionManager3.open(assignment.piSessionFile);
17996
+ if (saved.getSessionId() !== assignment.id)
17997
+ throw new Error(
17998
+ "Persisted project session identity does not match"
17999
+ );
18000
+ savedCwd = saved.getCwd();
18001
+ } else {
18002
+ const saved = (await SessionManager3.listAll()).filter(
18003
+ (session) => session.id === assignment.id
17961
18004
  );
18005
+ if (saved.length > 1)
18006
+ throw new Error(
18007
+ `Pi session ${assignment.id} is ambiguous; project work was preserved.`
18008
+ );
18009
+ if (saved.length === 1) {
18010
+ const sessionFile = saved[0].path;
18011
+ if (typeof sessionFile !== "string" || !sessionFile)
18012
+ throw new Error(
18013
+ "Legacy project session has no exact file path"
18014
+ );
18015
+ const session = SessionManager3.open(sessionFile);
18016
+ if (session.getSessionId() !== assignment.id)
18017
+ throw new Error(
18018
+ "Persisted project session identity does not match"
18019
+ );
18020
+ savedCwd = session.getCwd();
18021
+ assignment = await persistLeadSessionFile(
18022
+ assignment,
18023
+ realpathSync5(sessionFile)
18024
+ );
18025
+ }
18026
+ }
17962
18027
  args.push("--base", assignment.branch);
17963
- if (saved[0]?.cwd) args.push("--path", saved[0].cwd);
18028
+ if (savedCwd) args.push("--path", savedCwd);
17964
18029
  } else args.push("--base", operation.base ?? "HEAD");
17965
18030
  const created = await runHerdr2(pi, ctx, args, { signal });
17966
18031
  workspaceId = created?.workspace?.workspace_id;
@@ -18177,6 +18242,7 @@ function createLeadProjectRuntime(host) {
18177
18242
  operationName,
18178
18243
  `Lead ${assignment.id}`
18179
18244
  );
18245
+ assignment = await bindLeadSessionFile(assignment, lead);
18180
18246
  writeChiefMessage2({
18181
18247
  version: 2,
18182
18248
  build: HERDSMAN_BUILD2,
@@ -18233,6 +18299,7 @@ function createLeadProjectRuntime(host) {
18233
18299
  );
18234
18300
  }
18235
18301
  await assertManagerCurrent();
18302
+ await bindLeadSessionFile(assignment, rediscovered);
18236
18303
  return {
18237
18304
  content: [
18238
18305
  {
@@ -21012,46 +21079,31 @@ function retiredManagedSession(manager) {
21012
21079
  const entries = manager.getEntries();
21013
21080
  return !!sessionAgentIdentity(entries, sessionId) && sessionContextRetired(entries, sessionId);
21014
21081
  }
21015
- async function collectOwnedSessionUsage(ctx, managerScopeOrLeadIds) {
21016
- const managedLeadIds = Array.isArray(managerScopeOrLeadIds) ? managerScopeOrLeadIds : void 0;
21017
- if (!managedLeadIds?.length)
21082
+ function collectOwnedSessionUsage(ctx, managedLeads) {
21083
+ if (managedLeads === void 0)
21018
21084
  return collectSessionUsage(
21019
21085
  ctx,
21020
21086
  ownedAssignmentChildren,
21021
21087
  openOwnedAssignmentSession
21022
21088
  );
21023
- const wanted = new Set(managedLeadIds);
21024
- const signal = ctx.signal;
21025
- let sessions;
21026
- try {
21027
- sessions = await SessionManager2.listAll(void 0, signal);
21028
- } catch (error2) {
21029
- if (signal?.aborted) throw error2;
21030
- return collectSessionUsage(
21031
- ctx,
21032
- ownedAssignmentChildren,
21033
- openOwnedAssignmentSession,
21034
- [],
21035
- false
21036
- );
21037
- }
21038
- const matches = /* @__PURE__ */ new Map();
21039
- for (const session of sessions) {
21040
- if (!wanted.has(session.id)) continue;
21041
- const paths = matches.get(session.id) ?? [];
21042
- paths.push(session.path);
21043
- matches.set(session.id, paths);
21089
+ const pathsById = /* @__PURE__ */ new Map();
21090
+ for (const { id, piSessionFile } of managedLeads) {
21091
+ if (!pathsById.has(id)) pathsById.set(id, /* @__PURE__ */ new Set());
21092
+ if (!piSessionFile) continue;
21093
+ try {
21094
+ pathsById.get(id).add(canonicalSessionPath(piSessionFile));
21095
+ } catch {
21096
+ }
21044
21097
  }
21045
21098
  const roots = [];
21046
21099
  let complete = true;
21047
- for (const id of wanted) {
21048
- const paths = matches.get(id) ?? [];
21049
- if (paths.length !== 1) {
21100
+ for (const [id, paths] of pathsById) {
21101
+ if (paths.size !== 1) {
21050
21102
  complete = false;
21051
21103
  continue;
21052
21104
  }
21053
21105
  try {
21054
- const manager = SessionManager2.open(paths[0]);
21106
+ const manager = SessionManager2.open(paths.values().next().value);
21055
21107
  if (manager.getSessionId() !== id) {
21056
21108
  complete = false;
21057
21109
  continue;
@@ -21771,7 +21823,7 @@ ${globalInstructions}
21771
21823
  definitionRoster: () => startupDefinitionRoster,
21772
21824
  isStaffTool: (name) => STAFF_TOOLS.includes(name)
21773
21825
  };
21774
- const initialStatusBreadcrumb = controllerScope.kind === "lead" ? ["herd"] : [
21826
+ const initialStatusBreadcrumb = controllerScope.kind === "lead" ? ["lead"] : [
21775
21827
  "?",
21776
21828
  process.env.PI_HERDSMAN_AGENT_DEFINITION && process.env.PI_HERDSMAN_LABEL ? displayIdentity(
21777
21829
  process.env.PI_HERDSMAN_AGENT_DEFINITION,