pi-gang 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lachlan Jacobs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/NOTICE ADDED
@@ -0,0 +1,15 @@
1
+ gang
2
+ Copyright (c) 2026 Lachlan Jacobs
3
+
4
+ This product vendors and modifies source from pi-intercom, used under the MIT License:
5
+
6
+ pi-intercom
7
+ Copyright (c) 2026 Nico Bailon
8
+ https://github.com/nicobailon/pi-intercom
9
+
10
+ The vendored source lives under src/intercom/. It has been modified: peer-dependency
11
+ imports retargeted from @mariozechner/* to @earendil-works/*, a broker message tap and
12
+ an embedded mission-control HTTP/SSE server added.
13
+
14
+ gang targets the Pi coding agent (@earendil-works/pi-coding-agent, originally authored by
15
+ Mario Zechner as @mariozechner/pi-coding-agent).
package/README.md ADDED
@@ -0,0 +1,159 @@
1
+ # gang
2
+
3
+ <p align="center">
4
+ <img src="assets/gang-logo.png" alt="gang mission-control crew logo" width="260">
5
+ </p>
6
+
7
+ A minimal, **observable** subagent primitive for [Pi](https://github.com/earendil-works). Fire up
8
+ subagents as **visible tmux panes** you watch live, let them talk over a **cross-agent message
9
+ bus**, and observe everything three ways: live panes, a durable log, and a mission-control view —
10
+ in your terminal (a Pi overlay) **and** in the browser.
11
+
12
+ Built on a vendored, fully-owned copy of [pi-intercom](https://github.com/nicobailon/pi-intercom)
13
+ (MIT). No external `pi-intercom` runtime dependency; npm deps install with the package.
14
+ Targets `@earendil-works` Pi **0.80.x** (supported range: `>=0.80.2 <0.81.0`).
15
+
16
+ ```
17
+ ┌─ tmux panes ──────────────┐ raw per-agent view — watch each member's pi session
18
+ │ superintendent │ worker │
19
+ │ │ reviewer │
20
+ └───────────┴───────────────┘
21
+ │ intercom (unix socket: ~/.pi/agent/intercom/broker.sock)
22
+
23
+ ┌─ owned broker (vendored) ────────────────────┐
24
+ │ • routes messages │
25
+ │ • TAP → ~/.pi/agent/intercom/intercom.jsonl │ durable, greppable, replayable
26
+ │ • HTTP + SSE :7717 ─────────────────────────┼─► mission control: /gang watch (in Pi)
27
+ └───────────────────────────────────────────────┘ 127.0.0.1:7717 (browser)
28
+ ```
29
+
30
+ ## Requirements
31
+
32
+ - **Pi** (`@earendil-works/pi-coding-agent`) 0.80.x, ≥ 0.80.2 — `pi --version`
33
+ - **tmux** — members run as visible panes (`brew install tmux`)
34
+ - **Node** ≥ 22
35
+
36
+ ## Install
37
+
38
+ ### Option A — from npm (recommended)
39
+
40
+ ```sh
41
+ pi install pi-gang
42
+ pi list # verify: should list gang
43
+ ```
44
+
45
+ ### Option B — from GitHub
46
+
47
+ Pi packages run local extension code with your user permissions, so review the source before
48
+ installing. Pi installs the package's runtime dependencies for you on a git install:
49
+
50
+ ```sh
51
+ pi install https://github.com/Jeecabs/gang.git
52
+ pi list # verify: should list gang
53
+ ```
54
+
55
+ SSH also works if your GitHub account is configured for it:
56
+ `pi install ssh://git@github.com/Jeecabs/gang.git`
57
+
58
+ ### Option C — local clone (to hack on it)
59
+
60
+ ```sh
61
+ git clone git@github.com:Jeecabs/gang.git
62
+ cd gang
63
+ npm install # REQUIRED — the broker + GUI run from node_modules
64
+ pi install . # persistent: registers it in your Pi settings
65
+ # …or load it for a single session without touching settings:
66
+ pi -e ./src/intercom/index.ts -e ./src/gang/index.ts
67
+ ```
68
+
69
+ > ⚠️ A **local** install (`pi install .`) references the folder **in place** and does **not** run
70
+ > `npm install` for you — so run it yourself, and don't move or delete the folder (the broker
71
+ > launches from its `node_modules`). A git/npm install handles deps automatically.
72
+
73
+ Unsure about Pi extensions in general? Pi documents itself — just ask it:
74
+ `pi -p "how do I write and install a Pi extension?"`
75
+
76
+ ## Use
77
+
78
+ In any Pi session, ask for a member — or call the tool yourself. The session adopts the **`superintendent`**
79
+ identity the first time you spawn (not before), so sessions where you never use gang keep their
80
+ natural title in `pi -r`:
81
+
82
+ ```
83
+ /gang spawn list the repo's largest files
84
+ /gang spawn @reviewer -t high review the diff carefully
85
+ ```
86
+
87
+ `gang spawn` returns immediately and opens a tmux pane. The member does the task in its own Pi
88
+ session and sends the result **back to superintendent as an intercom message** (it does not return inline).
89
+ Keep working; handle the result when it arrives.
90
+
91
+ Watch the gang three ways:
92
+
93
+ ```sh
94
+ # 1. in Pi — live mission-control overlay (members + status + feed)
95
+ /gang watch # or press alt+g
96
+
97
+ # 2. in the browser — the same feed, richer
98
+ /gang url # prints computed loopback URL from GANG_GUI_PORT
99
+ open http://127.0.0.1:7717
100
+
101
+ # 3. durable, greppable log of every cross-agent message
102
+ tail -f ~/.pi/agent/intercom/intercom.jsonl
103
+ ```
104
+
105
+ ## Commands & tools
106
+
107
+ | Surface | What |
108
+ |---|---|
109
+ | `gang` tool | `{action:"spawn", task, role?, thinking?}` · `{action:"list"}` · `{action:"clean", force?, all?}` · `{action:"stop", role? finished?}` · `{action:"name", name}` — model-callable |
110
+ | `/gang` | show the roster |
111
+ | `/gang spawn [@name] [-t <level>] <task>` | spawn a member by hand; name optional (auto `m1`, `m2`, …). Levels: `off`, `minimal`, `low`, `medium`, `high`, `xhigh` |
112
+ | `/gang clean` · `/gang clean --force` · `/gang clean all` | `clean` reaps dead/missing panes and prunes stale roster entries; `clean --force` also kills members that already reported back; `clean all` stops this superintendent's members (detached mode kills the `gang` session; in-tmux mode kills direct member panes only) |
113
+ | `/gang stop <member>` · `/gang stop --all-finished` | stop one member immediately, or bulk-stop any member already reapable (`reported_done`, `pane_dead`, `pane_missing`) |
114
+ | `/gang url` | show the browser mission-control URL (`http://127.0.0.1:<GANG_GUI_PORT>`) |
115
+ | `/gang name [name]` | show or set this agent/session's own name |
116
+ | `/gang watch` · `alt+g` | open mission control (in-Pi overlay) |
117
+ | `intercom` tool | message any session: `list` / `send` / `ask` / `reply` |
118
+
119
+ Env knobs: `GANG_GUI_PORT` (default `7717`), `GANG_TMUX_BIN` (default Homebrew tmux), `GANG_FEED_HOURS` (mission-control feed recency window, default `6`).
120
+
121
+ Finished members linger on purpose — their panes stay (`remain-on-exit on`) so you can read the final state. Reap them when you're done with `/gang clean` (or `gang({ action: "clean" })`). If a member has already reported back but its pane is still hanging around, use `/gang clean --force` or `/gang stop --all-finished`. `/gang stop <member>` is the first-class escape hatch when you know a specific pane should die. `/gang clean all` tears down the detached `gang` session outside tmux; inside tmux it kills this superintendent's direct member panes only.
122
+
123
+ Agent naming: an unnamed orchestrator session claims `superintendent of <current-folder>` (e.g. `superintendent of private-evals`) **on its first spawn**, not at startup — so plain Pi sessions stay unnamed in the `pi -r` resume list. Use `/gang name <name>` before spawning to pick a custom target; spawned members get that exact supervisor name. Member task prompts also tell agents to name themselves with `gang({ action: "name", name: "<clear role/name>" })` before spinning up their own teammate.
124
+
125
+ ## How it's wired
126
+
127
+ `package.json` → `pi.extensions` points Pi at two TypeScript entry files it loads on startup:
128
+
129
+ - `src/intercom/` — vendored pi-intercom: the broker, client, and the `intercom` /
130
+ `contact_supervisor` tools. **Owned** — imports retargeted to `@earendil-works/*`, plus a broker
131
+ message **tap** (durable log) and an embedded **HTTP/SSE** mission-control server.
132
+ - `src/gang/` — the `gang` tool (spawn members in tmux, list the roster), the `/gang watch`
133
+ overlay, and superintendent auto-naming.
134
+
135
+ `gang spawn <task>` (optionally `@name`) launches `pi --name <name> … @<taskfile>` in a tmux pane with five
136
+ `PI_SUBAGENT_*` env vars. Pass `--thinking <level>` to add Pi's `--thinking` flag for that member.
137
+ The vendored intercom extension reads the env vars at child startup to register
138
+ the member on the bus and unlock its `contact_supervisor` tool — that's the whole contract:
139
+
140
+ | Env var | Set to | Effect |
141
+ |---|---|---|
142
+ | `PI_SUBAGENT_INTERCOM_SESSION_NAME` | `<role>` | member's intercom identity |
143
+ | `PI_SUBAGENT_ORCHESTRATOR_TARGET` | current supervisor name (`superintendent of <folder>` or custom `/gang name`) | who it reports to |
144
+ | `PI_SUBAGENT_RUN_ID` / `_CHILD_AGENT` / `_CHILD_INDEX` | run metadata | unlocks `contact_supervisor` |
145
+
146
+ (The member's addressable **name** comes from Pi's `--name` flag; the supervisor self-names
147
+ lazily to the computed or custom `orchestratorName`.)
148
+
149
+ ## Develop
150
+
151
+ ```sh
152
+ npm test # vendored intercom suite + gang/tmux/tap/GUI/overlay tests
153
+ npm run typecheck # strict TypeScript check
154
+ ```
155
+
156
+ ## Credits
157
+
158
+ Vendors [pi-intercom](https://github.com/nicobailon/pi-intercom) by Nico Bailon (MIT). See `NOTICE`.
159
+ Built on Pi by the [earendil-works](https://github.com/earendil-works) team.
Binary file
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "pi-gang",
3
+ "version": "0.1.0",
4
+ "description": "Minimal, observable subagent primitive for Pi — visible tmux members, cross-agent intercom, and a mission-control GUI.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "keywords": [
8
+ "pi-package"
9
+ ],
10
+ "files": [
11
+ "assets",
12
+ "src/**/*.ts",
13
+ "src/**/*.html",
14
+ "src/**/SKILL.md",
15
+ "!src/**/*.test.ts",
16
+ "!src/**/test/**",
17
+ "NOTICE"
18
+ ],
19
+ "scripts": {
20
+ "test": "tsx --test \"src/**/*.test.ts\"",
21
+ "typecheck": "tsc --noEmit",
22
+ "check": "npm run typecheck && npm test",
23
+ "prepublishOnly": "npm run check"
24
+ },
25
+ "pi": {
26
+ "extensions": [
27
+ "./src/intercom/index.ts",
28
+ "./src/gang/index.ts"
29
+ ],
30
+ "skills": [
31
+ "./src/intercom/skills"
32
+ ]
33
+ },
34
+ "engines": {
35
+ "node": ">=22.19.0"
36
+ },
37
+ "peerDependencies": {
38
+ "@earendil-works/pi-coding-agent": ">=0.80.2 <0.81.0",
39
+ "@earendil-works/pi-tui": ">=0.80.2 <0.81.0",
40
+ "typebox": ">=1.1.38 <2"
41
+ },
42
+ "devDependencies": {
43
+ "@earendil-works/pi-coding-agent": "^0.80.6",
44
+ "@earendil-works/pi-tui": "^0.80.6",
45
+ "@types/node": "^22.20.1",
46
+ "typebox": "^1.3.6",
47
+ "typescript": "^5.9.3"
48
+ },
49
+ "dependencies": {
50
+ "tsx": "^4.23.1"
51
+ },
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "git+https://github.com/Jeecabs/gang.git"
55
+ },
56
+ "author": "Lachlan Jacobs",
57
+ "bugs": {
58
+ "url": "https://github.com/Jeecabs/gang/issues"
59
+ },
60
+ "homepage": "https://github.com/Jeecabs/gang#readme",
61
+ "publishConfig": {
62
+ "access": "public"
63
+ }
64
+ }
@@ -0,0 +1,11 @@
1
+ export const SUPERINTENDENT_NAMED_EVENT = "gang:superintendent-named";
2
+ export const GANG_MEMBER_REPORT_EVENT = "gang:member-report";
3
+
4
+ export interface GangMemberReportEvent {
5
+ fromId: string;
6
+ fromName?: string;
7
+ text: string;
8
+ timestamp: number;
9
+ expectsReply: boolean;
10
+ replyTo?: string;
11
+ }
@@ -0,0 +1,88 @@
1
+ import http from "http";
2
+ import { EventEmitter } from "events";
3
+ import { GUI_PORT } from "../intercom/gui/server.js";
4
+
5
+ /** A parsed event from the broker's SSE feed (same stream the browser dashboard consumes). */
6
+ export interface FeedEvent {
7
+ type: "snapshot" | "session_joined" | "session_left" | "presence_update" | "message";
8
+ [key: string]: unknown;
9
+ }
10
+
11
+ /**
12
+ * Consumes the broker's `/events` SSE stream in-process so the in-pi overlay renders the exact
13
+ * same feed as the browser GUI. Reconnects if the broker restarts. Emits:
14
+ * "event" (FeedEvent) — each parsed feed event
15
+ * "status" ("up" | "down") — connection state for the overlay's live indicator
16
+ */
17
+ export class FeedClient extends EventEmitter {
18
+ private req?: http.ClientRequest;
19
+ private reconnectTimer?: NodeJS.Timeout;
20
+ private buf = "";
21
+ private stopped = false;
22
+
23
+ constructor(private readonly port: number = GUI_PORT) {
24
+ super();
25
+ }
26
+
27
+ start(): this {
28
+ this.connect();
29
+ return this;
30
+ }
31
+
32
+ private connect(): void {
33
+ if (this.stopped) return;
34
+ this.req = http.get({ host: "127.0.0.1", port: this.port, path: "/events" }, (res) => {
35
+ if (res.statusCode !== 200) {
36
+ res.destroy();
37
+ this.scheduleReconnect();
38
+ return;
39
+ }
40
+ this.clearReconnectTimer();
41
+ this.emit("status", "up");
42
+ res.setEncoding("utf8");
43
+ res.on("data", (chunk: string) => this.onData(chunk));
44
+ res.on("close", () => this.scheduleReconnect());
45
+ });
46
+ this.req.on("error", () => this.scheduleReconnect());
47
+ }
48
+
49
+ /** Parse SSE frames (separated by blank lines); ignore comment/keepalive lines. */
50
+ private onData(chunk: string): void {
51
+ this.buf += chunk;
52
+ let idx: number;
53
+ while ((idx = this.buf.indexOf("\n\n")) !== -1) {
54
+ const frame = this.buf.slice(0, idx);
55
+ this.buf = this.buf.slice(idx + 2);
56
+ const dataLine = frame.split("\n").find((l) => l.startsWith("data: "));
57
+ if (!dataLine) continue;
58
+ try {
59
+ this.emit("event", JSON.parse(dataLine.slice(6)) as FeedEvent);
60
+ } catch {
61
+ // ignore malformed frame
62
+ }
63
+ }
64
+ }
65
+
66
+ private scheduleReconnect(): void {
67
+ if (this.stopped || this.reconnectTimer) return;
68
+ this.emit("status", "down");
69
+ this.buf = "";
70
+ this.reconnectTimer = setTimeout(() => {
71
+ this.reconnectTimer = undefined;
72
+ this.connect();
73
+ }, 1000);
74
+ }
75
+
76
+ private clearReconnectTimer(): void {
77
+ if (!this.reconnectTimer) return;
78
+ clearTimeout(this.reconnectTimer);
79
+ this.reconnectTimer = undefined;
80
+ }
81
+
82
+ stop(): void {
83
+ this.stopped = true;
84
+ this.clearReconnectTimer();
85
+ this.req?.destroy();
86
+ this.removeAllListeners();
87
+ }
88
+ }