pi-straw-boss 0.35.0 → 0.35.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/README.md CHANGED
@@ -86,7 +86,7 @@ The tower only routes and coordinates, so it can run a lighter model: set `tower
86
86
  | `/boss` | Take over as this tab's main agent. |
87
87
  | `/boss status`, `/boss tower` | Show the status; open and focus the tower. |
88
88
  | `/boss close` | Quit this session and close its pane. |
89
- | `/boss close all` | Quit every tower, main, and member on the net and close their bases. Every base is checked first as a stash checks it; anything running that would be lost anywhere (a turn, a queued message, a dialog, a monitor, a subagent, a busy shell) closes nothing, and every reason is listed. Workspaces outside the net are left alone. |
89
+ | `/boss close all` | Quit this base's tower, mains, and members and close it with its shells, without stashing. It is checked first as a stash checks it; anything running that would be lost (a turn, a queued message, a dialog, a monitor, a subagent, a busy shell) closes nothing, and every reason is listed. Other bases are never touched. |
90
90
  | `/boss stash [name]`, `/boss pop [name]` | Put this whole base away (its tower, mains, members, and idle shells) and close it; open it again later, every pi resumed from its own session file in its old role. A stash refuses, naming why, while anything a resume would lose is there: a running turn, a queued message, an open dialog, a running monitor, a subagent, or a busy shell. |
91
91
  | `/boss gui`, `/boss gui stop` | Open the net in the browser (any session, joins nothing); stop its server, from any session. The machine runs one server on one URL: it comes back after its session's `/reload`, and a session on a newer pi-straw-boss takes it over, so open pages reload into the new version. Type into a session from its comms view, or work its terminal with the key pad under the terminal tile, or on a desktop by clicking into it and typing; answer a waiting `robot_hand` selector, `ask_user_question` questionnaire, or pi-code hook confirmation there or in its alert (so can any extension's `select` and `confirm`, also from a paired device; other dialogs show read-only, to answer in the terminal; `secret_drop` always takes its secret in the terminal, never on a page, local or over the tailnet, because pi-intercom identities are self-reported). Documents a session shows with [pi-herdr-doc-review](https://www.npmjs.com/package/pi-herdr-doc-review) 0.4.0 or newer open beside its comms: HTML in a sandboxed frame whose form answers the agent, text read-only. |
92
92
  | `/boss gui remote`, `/boss gui remote off` | Reach the GUI from your phone over your tailnet (`tailscale serve`, never Funnel): pair by QR code, and a phone stays paired (90 days from its last use, restarts and `remote off` included) until you unpair it in the Phone tile; remote access stays on across a stop, a `/reload` and a new server until `remote off`; off removes only its own serve entry. The serve entry points at a second loopback listener of the GUI server, so a request is remote because of the listener it arrived on, whatever headers it carries. |
package/README.zh-TW.md CHANGED
@@ -85,7 +85,7 @@ pi install npm:pi-straw-boss
85
85
  | `boss_rename` | 用一個短語替這個 session 命名,說明它現在在做什麼;net、GUI,以及裝了 pi-auto-tab-name 時的分頁名稱都會跟著顯示。主智慧體換工作時會呼叫它。 |
86
86
  | `/boss` | 接手成為這個分頁的主智慧體。 |
87
87
  | `/boss status`、`/boss tower` | 顯示狀態;開啟塔台並切換過去。 |
88
- | `/boss close all` | 讓網上所有塔台、主智慧體和成員結束,並關掉它們的基地。會先用 stash 的同一套檢查看過每個基地;任何地方有會遺失的東西(正在跑的回合、排隊中的訊息、對話框、Monitor、子智慧體、前景有程式的 shell),就一個都不關,並列出所有原因。不屬於網的工作區不會被碰。 |
88
+ | `/boss close all` | 讓這個基地的塔台、主智慧體和成員結束,連同 shell 一起關掉,不存 stash。會先用 stash 的同一套檢查;有會遺失的東西(正在跑的回合、排隊中的訊息、對話框、Monitor、子智慧體、前景有程式的 shell)就什麼都不關,並列出所有原因。其他基地完全不碰。 |
89
89
  | `/boss stash [名稱]`、`/boss pop [名稱]` | 把整個基地(塔台、主智慧體、成員和閒置的 shell)收起來並關掉;之後再打開,每個 pi 都從自己的 session 檔接回原本的角色。只要有接回後會遺失的東西就拒絕並說明原因:正在跑的回合、排隊中的訊息、開著的對話框、執行中的 Monitor、子智慧體,或前景有程式在跑的 shell。 |
90
90
  | `/boss gui`、`/boss gui stop` | 在瀏覽器開啟網(任何 session 都能開,不會加入任何東西);停止它的伺服器,在哪個 session 都可以。整台機器只跑一個伺服器、一個網址:開它的 session `/reload` 之後,會在原網址重開;版本較新的 session 會接手,開著的頁面也會自動換成新版。可以在通訊畫面直接打字給某個 session,或用終端機畫面下方的按鍵面板操作它的終端機,在桌面上也可以點進終端機直接打字;在等回覆的 `robot_hand` 選單、`ask_user_question` 問卷或 pi-code 的 hook 確認,可以在那裡或它的警示裡作答(任何 extension 的 `select` 和 `confirm` 也可以,`confirm` 也可以從配對的裝置作答;其他對話框只能看,要回到終端機回答;`secret_drop` 的機密一律在終端機輸入,任何頁面(本機或 tailnet)都不接受,因為 pi-intercom 的身分是自己申報的,無法驗證)。session 用 [pi-herdr-doc-review](https://www.npmjs.com/package/pi-herdr-doc-review) 0.4.0 以上顯示的文件,會開在它的通訊畫面旁邊:HTML 放在沙箱框架裡,表單送出的答案會傳給智慧體;純文字只能讀。 |
91
91
  | `/boss gui remote`、`/boss gui remote off` | 透過你的 tailnet 用手機連 GUI(`tailscale serve`,絕不用 Funnel):掃 QR code 配對,手機配對後會一直有效(從最後一次使用起算 90 天,重啟與 `remote off` 都不影響),直到你在「手機」視窗解除配對;遠端存取在 stop、`/reload`、換新伺服器之後仍會保持開啟,直到 `remote off`;關閉時只移除它自己的 serve 設定。serve 指向 GUI 伺服器的第二個 loopback 監聽埠,請求算不算遠端取決於它進來的監聽埠,不看它帶什麼標頭。 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-straw-boss",
3
- "version": "0.35.0",
3
+ "version": "0.35.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/boss.ts CHANGED
@@ -5,12 +5,12 @@ import type { Kit } from "./state.ts";
5
5
  import type { createStashing } from "./stashing.ts";
6
6
 
7
7
  /** What Tab completes after /boss; a bare /boss (take over as the main agent) has nothing to complete. */
8
- const BOSS_ARGUMENTS = Object.entries({ status: "Show the coordinators", close: "Quit this session and close its pane", "close all": "Quit every tower, main, and member on the net and close their bases", tower: "Open and focus the workspace tower", gui: "Open the net in the browser", "gui stop": "Close the browser GUI", "gui remote": "Open the GUI to your phone over Tailscale", "gui remote off": "Close the Tailscale entry", stash: "Put this base away: its tower, mains, and members", pop: "Open a stashed base again" }).map(([value, description]) => ({ value, label: value, description }));
8
+ const BOSS_ARGUMENTS = Object.entries({ status: "Show the coordinators", close: "Quit this session and close its pane", "close all": "Quit this base's tower, mains, and members and close it", tower: "Open and focus the workspace tower", gui: "Open the net in the browser", "gui stop": "Close the browser GUI", "gui remote": "Open the GUI to your phone over Tailscale", "gui remote off": "Close the Tailscale entry", stash: "Put this base away: its tower, mains, and members", pop: "Open a stashed base again" }).map(([value, description]) => ({ value, label: value, description }));
9
9
 
10
10
  /** The /boss command. */
11
11
  export function registerBoss({ pi, cfg, s, tell }: Kit, { host, membership, reach, closeSelf, stashing }: { host: ReturnType<typeof createHost>; membership: Membership; reach: Reach; closeSelf(keepPane: boolean): string | undefined; stashing: ReturnType<typeof createStashing> }) {
12
12
  pi.registerCommand("boss", {
13
- description: "Take over as this tab's main agent (every session already joins as a member). /boss status shows the coordinators; /boss tower focuses the workspace tower (a main opens it when there is none); /boss close quits this session and closes its pane (/boss close all: every tower, main, and member on the net); /boss stash [name] puts this base away (tower, mains, members) and /boss pop [name] opens it again; /boss gui opens the net in the browser (/boss gui stop closes it; /boss gui remote opens it to your phone over Tailscale, /boss gui remote off closes that).",
13
+ description: "Take over as this tab's main agent (every session already joins as a member). /boss status shows the coordinators; /boss tower focuses the workspace tower (a main opens it when there is none); /boss close quits this session and closes its pane (/boss close all: this whole base, its tower, mains, and members); /boss stash [name] puts this base away (tower, mains, members) and /boss pop [name] opens it again; /boss gui opens the net in the browser (/boss gui stop closes it; /boss gui remote opens it to your phone over Tailscale, /boss gui remote off closes that).",
14
14
  // pi passes the whole argument text and replaces it with the chosen value, so each value is a full argument line.
15
15
  getArgumentCompletions: (prefix) => {
16
16
  const stashed = stashing.names().map((name) => ({ value: `pop ${name}`, label: `pop ${name}`, description: "Open this stashed base again" }));
package/src/closer.ts CHANGED
@@ -11,7 +11,7 @@ import { closeAll, stashBase } from "./stash.ts";
11
11
  // POST /<token>/api/close/tab {workspace, tab} every agent in the tab quits, then the tab closes
12
12
  // POST /<token>/api/close/base {workspace} every agent in the workspace quits, then the workspace closes
13
13
  // POST /<token>/api/close/stash {workspace, name} the base is checked and recorded first (stash.ts), then closed as above
14
- // POST /<token>/api/close/all every base of the net is checked, then each closes as above (stash.ts closeAll)
14
+ // POST /<token>/api/close/all {by} the base of session `by` is checked, then closes as above (stash.ts closeAll)
15
15
 
16
16
  const EXIT_MS = 30_000, JOINED = new Set(["main", "member", "tower"]);
17
17
  /** `check` and `stateDir` are a stash's: without them this closer stashes nothing. */
package/src/stash.ts CHANGED
@@ -129,24 +129,25 @@ async function askSessions(d: Pick<StashDeps, "sleep" | "check">, agents: Sessio
129
129
  }
130
130
 
131
131
  /**
132
- * Close all: every base of the net (a workspace with a live tower or a joined main or member; sub workspaces and workspaces outside the net are
133
- * left alone) is checked as a stash checks it, minus what only a pop needs. One refusal anywhere closes nothing. Then each base closes as its
134
- * close button does, each session quitting only while it can still be stashed, and the base of the server's own session last (it ends with it).
132
+ * Close all: the invoker's own base (its tower, mains, members, and shells; never another base, a sub workspace, or a workspace outside the net)
133
+ * is checked as a stash checks it, minus what only a pop needs; any refusal closes nothing. Then it closes as its close button does, each session
134
+ * quitting only while it can still be stashed. The base is looked at again just before it closes, and the server's own session, when it sits
135
+ * there, is asked again too (its batch does not ask it).
135
136
  */
136
137
  export async function closeAll(d: Pick<StashDeps, "run" | "sleep" | "sessions" | "check">, me: string, closeBase: (workspace: string) => Promise<Reply>, refresh?: () => Promise<unknown>, by?: string): Promise<Reply> {
137
138
  const live = await d.sessions(), alive = new Set(live.map((s) => s.id)), ps = await panes(d.run), lives = (p: Pane) => live.some((s) => s.herdrLocation?.paneId === p.pane_id);
139
+ // Only the invoker's own base: the other bases of the net are not its to close.
140
+ const baseOf = (id?: string) => (id ? live.find((s) => s.id === id)?.herdrLocation?.workspace?.id : undefined), mine = baseOf(me), theirs = baseOf(by);
141
+ if (!theirs) return no(400, "bad_request", "Close all closes the base of the session that asks; that session is not on the net.");
138
142
  // A role token outlives its pi up to its TTL: only a live tower, main, or member puts a workspace on the net.
139
- const bases = (await workspaces(d.run)).filter((w) => !isSub(w) && ((w.tokens?.tower && alive.has(w.tokens.tower)) || ps.some((p) => p.workspace_id === w.workspace_id && (p.tokens?.sb_role === "main" || p.tokens?.sb_role === "member") && lives(p))));
140
- if (!bases.length) return no(404, "not_found", "No base is on the net.");
143
+ const bases = (await workspaces(d.run)).filter((w) => w.workspace_id === theirs && !isSub(w) && ((w.tokens?.tower && alive.has(w.tokens.tower)) || ps.some((p) => p.workspace_id === w.workspace_id && (p.tokens?.sb_role === "main" || p.tokens?.sb_role === "member") && lives(p))));
144
+ if (!bases.length) return no(404, "not_found", "This session's workspace is not a base on the net.");
141
145
  const surveyAll = async (sessions: Session[]) => Promise.all(bases.map(async (w) => ({ w, sv: await survey(d.run, w.workspace_id, sessions, false) })));
142
146
  const first = await surveyAll(live), reasons = first.flatMap(({ w, sv }) => (sv ? sv.reasons.map((r) => `${w.label}: ${r}`) : []));
143
147
  reasons.push(...(await askSessions(d, first.flatMap(({ sv }) => sv?.agents ?? []), refresh)));
144
148
  if (!reasons.length) reasons.push(...(await surveyAll(await d.sessions())).flatMap(({ w, sv }) => (sv?.reasons ?? []).map((r) => `${w.label}: ${r}`)));
145
149
  if (reasons.length) return no(409, "refused", `Nothing was closed: ${reasons.join("; ")}.`);
146
- // The invoker's base closes after the others, so it hears how the run ended; the server's own base last of all, since it ends with it.
147
- const baseOf = (id?: string) => (id ? live.find((s) => s.id === id)?.herdrLocation?.workspace?.id : undefined), mine = baseOf(me), theirs = baseOf(by);
148
- const rank = (w: { workspace_id: string }) => (w.workspace_id === mine ? 2 : w.workspace_id === theirs ? 1 : 0);
149
- const order = bases.toSorted((a, b) => rank(a) - rank(b)), done: string[] = [];
150
+ const order = bases, done: string[] = [];
150
151
  const stop = (why: string) => no(409, "close_failed", `${why} Closed before it: ${done.join(", ") || "none"}; the rest stay open.`);
151
152
  for (const w of order) {
152
153
  // The run takes minutes: each base is looked at again just before it closes (a shell job, a subagent left running), and the server's own
package/src/stashing.ts CHANGED
@@ -31,12 +31,12 @@ export function createStashing({ cfg, s, guiDir, tell, me }: Kit, { host }: { ho
31
31
  const why = typeof reply.body.reason === "string" ? reply.body.reason : reply.status ? `the server answered ${reply.status}` : "the GUI server did not answer";
32
32
  tell(reply.status === 404 && reply.body.error === "not_found" ? `The GUI server cannot stash yet (an older pi-straw-boss in session ${shared.owner}); /reload it first` : why, "error");
33
33
  },
34
- /** Quits every tower, main, and member on the net and closes their bases, or nothing when anything there would be lost. */
34
+ /** Quits this base's tower, mains, and members and closes it, or nothing when anything there would be lost. */
35
35
  async closeAll() {
36
36
  const shared = await host.share();
37
37
  const record = readRecord(guiDir);
38
38
  if (shared.stale || !record) throw new Error(`The straw net GUI runs in session ${shared.owner} on a version that cannot close all; /reload that session first`);
39
- tell("Closing every base on the net…");
39
+ tell("Closing this base…");
40
40
  const reply = await postServer(record, "close/all", { by: me() }, STASH_MS * 4);
41
41
  if (reply.status === 200) return tell(`Closed ${(reply.body.closed as string[]).join(", ")}`);
42
42
  // A session still here to read this was not closed: the server went with its own base, timed out, or is down.