pi-straw-boss 0.9.0 → 0.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-straw-boss",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Straw Boss coordination for pi in Herdr: sessions join a tab's net by choice, the first joiner is its main agent, a workspace tower opens on demand with a live monitoring dashboard, and boss_broadcast reaches the rest",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/herdr.ts CHANGED
@@ -164,14 +164,24 @@ async function open({ run, sleep, live, moveFirst, model }: TowerEnv, workspace:
164
164
 
165
165
  // A leftover tower tab is reused: a pane already running pi only needs its token awaited, a shell prompt gets pi started.
166
166
  // The tab is told apart by the `sb_role=tower` pane token that only Straw Boss writes; a tab merely labelled "tower"
167
- // (a pi-herdr-agents subagent can be named so) is someone else's.
167
+ // (a pi-herdr-agents subagent can be named so) is someone else's. A tab whose token was cleared (its tower quit) is still ours when it has
168
+ // exactly the tower label (the glyph), one pane, and no agent in it.
168
169
  let paneId: string;
169
170
  let running = false;
170
- const marked = (await panes(run, ws)).find((p) => p.tokens?.sb_role === "tower");
171
- if (marked) {
172
- paneId = marked.pane_id;
173
- running = marked.agent === "pi";
174
- await moveFirst(marked.tab_id);
171
+ const here = await panes(run, ws);
172
+ const marked = here.find((p) => p.tokens?.sb_role === "tower");
173
+ const bare = marked ? [] : (await tabs(run, ws)).filter((t) => t.label === TOWER_LABEL).flatMap((t) => {
174
+ const inside = here.filter((p) => p.tab_id === t.tab_id);
175
+ return inside.length === 1 && !inside[0]!.agent && !inside[0]!.tokens?.sb_role ? [inside[0]!] : [];
176
+ });
177
+ const reused = marked ?? bare[0];
178
+ if (reused) {
179
+ paneId = reused.pane_id;
180
+ running = reused.agent === "pi";
181
+ if (!marked) await report(run, "pane", paneId, { sb_role: "tower" });
182
+ await moveFirst(reused.tab_id);
183
+ // Other shell-only leftovers are closed; failing to close one leaves a harmless tab, so it does not stop the tower.
184
+ for (const extra of bare.slice(1)) await run(["tab", "close", extra.tab_id]).catch(() => {});
175
185
  } else {
176
186
  const dir = workspace.worktree?.checkout_path ?? cwd;
177
187
  const created = await createTab(run, ws, dir, ["STRAW_BOSS_ROLE=tower"], TOWER_LABEL);
@@ -182,7 +192,11 @@ async function open({ run, sleep, live, moveFirst, model }: TowerEnv, workspace:
182
192
  }
183
193
  if (!running) {
184
194
  // Herdr agent names are unique across workspaces and lowercase only, so the name carries the lowercased workspace id.
185
- await startPi(run, sleep, `tower-${ws.toLowerCase()}`, paneId, ["--name", `tower · ${workspace.label}`, ...(model ? ["--model", model] : [])]);
195
+ // An adopted shell that turns out busy (the user runs something there) gives the token back, so the next open does not count it as the tower.
196
+ await startPi(run, sleep, `tower-${ws.toLowerCase()}`, paneId, ["--name", `tower · ${workspace.label}`, ...(model ? ["--model", model] : [])]).catch(async (error: unknown) => {
197
+ if (reused && !marked) await report(run, "pane", paneId, { sb_role: null }).catch(() => {});
198
+ throw error;
199
+ });
186
200
  }
187
201
  const id = await waitFor(sleep, held, TOWER_WAIT_MS, 1000);
188
202
  if (!id) throw new Error(`The tower of ${workspace.label} did not publish its token within ${TOWER_WAIT_MS / 1000} s (pane ${paneId})`);
package/src/index.ts CHANGED
@@ -441,7 +441,7 @@ export function createStrawBoss({ run = herdrRun, api = herdrApi(), call = herdr
441
441
  if (["subagent", "subagent_resume"].includes(event.toolName) && !isTower && !barred && role()) for (const ms of KID_MARK_MS) setTimeout(() => void publish(), ms).unref();
442
442
  });
443
443
 
444
- pi.on("session_shutdown", async () => {
444
+ pi.on("session_shutdown", async (event) => {
445
445
  if (gui) await switchRemote(gui, false, guiDir, ts).catch(() => {});
446
446
  await gui?.stop();
447
447
  gui = undefined;
@@ -459,6 +459,12 @@ export function createStrawBoss({ run = herdrRun, api = herdrApi(), call = herdr
459
459
  await report(run, "pane", at.paneId, { sb_role: null, sb_opened_by: null, sb_order: null, sb_mark: null, sb_indent: null, sb_tab: null }).catch(() => {});
460
460
  await clearOrder(run, [...kids]);
461
461
  if (held) await report(run, "workspace", at.workspaceId, { tower: null }).catch(() => {});
462
+ // A tower that quits leaves its tab as a plain shell: close the tab, but only when this pane is all it holds. A reload, /new, resume or fork keeps the tab.
463
+ if (isTower && event.reason === "quit") {
464
+ const all = await panes(run, at.workspaceId).catch(() => []);
465
+ const tab = all.find((p) => p.pane_id === at.paneId)?.tab_id;
466
+ if (tab && all.every((p) => p.tab_id !== tab || p.pane_id === at.paneId)) await run(["tab", "close", tab]).catch(() => {});
467
+ }
462
468
  });
463
469
 
464
470
  // /boss gui is a read-only viewer: any session may open it, and it joins nothing and writes no token. A session outside the net