@prjct.app/pi-team 0.5.4 → 0.5.6

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/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## [0.5.6](https://github.com/prjct-app/pi-team/compare/v0.5.5...v0.5.6) (2026-09-10)
2
+
3
+ ## [0.5.5](https://github.com/prjct-app/pi-team/compare/v0.5.4...v0.5.5) (2026-09-10)
4
+
5
+ ### Performance Improvements
6
+
7
+ * carry the canonical payload serialization instead of recomputing it ([#24](https://github.com/prjct-app/pi-team/issues/24)) ([340c495](https://github.com/prjct-app/pi-team/commit/340c4954358e715c6aed5e37e9f0a78f33100877))
8
+
1
9
  ## [0.5.4](https://github.com/prjct-app/pi-team/compare/v0.5.3...v0.5.4) (2026-09-10)
2
10
 
3
11
  ### Performance Improvements
package/CONTRIBUTING.md CHANGED
@@ -4,6 +4,7 @@
4
4
  - Deliver changes through a pull request using `.github/pull_request_template.md`.
5
5
  - Use English for code, documentation, tests, issues, and pull requests.
6
6
  - Use strict TypeScript and only APIs documented by Pi 0.85.1.
7
+ - Use immutable values: `npm run check` fails on any `let` under `src/`. See [AGENTS.md](AGENTS.md).
7
8
  - Do not import host internals, monkey-patch prototypes, or access real credentials, sessions, or user configuration in tests.
8
9
  - Keep runtime dependencies in `dependencies`; list Pi-provided packages in `peerDependencies` with a `*` range.
9
10
  - Run `npm run check`, `npm test`, and `npm pack --dry-run` before review.
@@ -11,7 +12,7 @@
11
12
 
12
13
  ## Package documentation
13
14
 
14
- Follow [docs/package.md](docs/package.md) and its versioned official references. Keep README examples consistent with registered commands, distinguish tested behavior from unverified compatibility, and verify `npm run check:package` before release.
15
+ Follow [docs/package.md](docs/package.md) and its versioned official references. Keep README examples consistent with registered commands, distinguish tested behavior from unverified compatibility, and verify `npm run check:package` before release. Design detail belongs in [docs/architecture.md](docs/architecture.md), not the README.
15
16
 
16
17
  ## Releases
17
18
 
package/README.md CHANGED
@@ -2,37 +2,29 @@
2
2
 
3
3
  [![pi-team — extension for PI Agent](https://raw.githubusercontent.com/prjct-app/pi-clipboard/main/docs/covers/pi-team.png)](https://pi.dev)
4
4
 
5
- Coordinate independent PI Agent sessions with local team messaging, queued tasks, and shared results.
5
+ Coordinate independent PI Agent sessions with local team messaging, queued tasks,
6
+ and shared results.
6
7
 
7
- `@prjct.app/pi-team` · Team commands, messaging tools, and local mailbox storage; one extension.
8
+ You open the terminals; `pi-team` lets those sessions send each other work, wake a
9
+ free teammate, and return a result the requester can verify. Everything stays on
10
+ this machine, in local files.
8
11
 
9
- - Local messaging between independent Pi sessions: queued **requests**, display-only **notes**, and correlated **results**.
10
- - Concurrent mailbox storage: many agents write at the same time without lock failures.
11
- - Automatic delivery when a teammate is idle; pending work survives restarts.
12
- - Automatic results verified against the original request, plus periodic review turns that chase unresolved work.
13
- - One `/team wake [message]` command requests an actionable status check-in from every teammate.
14
- - Automatic task-boundary compaction before the next team turn, keeping independent sessions focused and reusable.
15
- - Minimal live session widget, on-demand requester → assignee status, folded transcript previews, and one `/team` command surface.
16
-
17
- ## Demo
18
-
19
- [![Watch the pi-team promotional demo](media/pi-team-demo/poster.png)](https://github.com/prjct-app/pi-team/raw/refs/heads/main/media/pi-team-demo/pi-team-demo.mp4)
20
-
21
- [Watch or download the 40-second demo](https://github.com/prjct-app/pi-team/raw/refs/heads/main/media/pi-team-demo/pi-team-demo.mp4). It shows the request flow, persistent local coordination, correlated results, and the two-command session setup. The soundtrack is an original instrumental composition with no voice-over or external samples.
12
+ [![Watch the pi-team demo](media/pi-team-demo/poster.png)](https://github.com/prjct-app/pi-team/raw/refs/heads/main/media/pi-team-demo/pi-team-demo.mp4)
22
13
 
23
14
  ## Install
24
15
 
25
- Requires Pi installed separately and Node.js **22.19 or later**. Compatibility is tested with **Pi 0.85.1**; newer versions are not yet verified. This is an independent community package.
26
-
27
- Install with Pi's package manager:
16
+ Requires Pi installed separately and Node.js **22.19 or later**. Tested against
17
+ Pi **0.85.1**; newer versions are not yet verified. Independent community package.
28
18
 
29
19
  ```sh
30
20
  pi install npm:@prjct.app/pi-team
31
21
  ```
32
22
 
33
- For project-only installation, add `-l`: `pi install -l npm:@prjct.app/pi-team`. Restart Pi after installation. Do not install the same extension from both GitHub and npm: Pi treats those as different package identities.
23
+ Add `-l` for project-only installation, and restart Pi afterwards. Manage it with
24
+ the usual `pi list` / `pi update` / `pi remove` and `pi config`. Do not install the
25
+ same extension from both GitHub and npm: Pi treats those as different packages.
34
26
 
35
- ## Usage
27
+ ## Quickstart
36
28
 
37
29
  Open two interactive Pi terminals. In the first:
38
30
 
@@ -47,22 +39,28 @@ In the second:
47
39
  /team join demo reviewer
48
40
  ```
49
41
 
50
- Back in the first terminal:
42
+ Back in the first:
51
43
 
52
44
  ```text
53
45
  /team note reviewer Please review the current README.
54
- /team wake Prioritize anything blocking the release.
46
+ /team send reviewer Add a limits table to the README.
55
47
  ```
56
48
 
57
- A note appears in the transcript without starting model work. Use `/team send reviewer <task>` to queue work for one teammate, or `/team wake [message]` to queue the standard actionable check-in for everyone else in the team. Installation alone never joins a team. See the command reference below before enabling automatic reception.
49
+ The note appears in the reviewer's transcript without starting any model work.
50
+ The request wakes the reviewer once it is idle, and its result comes back to the
51
+ coordinator. Installing alone never joins a team.
58
52
 
59
- Supported on Linux/macOS with local disk storage. Native Windows, shared network filesystems, and cross-machine messaging are not supported. Tests cover simulated Pi/model boundaries and real local processes; live model coordination still requires manual acceptance.
53
+ Supported on Linux and macOS with local disk storage. Network filesystems,
54
+ cross-machine messaging, and native Windows are not supported.
60
55
 
61
56
  ## Concepts
62
57
 
63
- A **team** is a named local mailbox on this machine. A session joins a team under
64
- an **alias**, which is its address; aliases are shared team addresses, not private
65
- identities. Messages come in three kinds:
58
+ A **team** is a named local mailbox. A session joins under an **alias**: an
59
+ address, not a privileged role or an automatic persona, and shared rather than
60
+ private anyone using this OS account can rejoin an offline alias and see its
61
+ history. Each session keeps its own model, cwd, instructions, permissions, and
62
+ conversation. Two agents editing the same files can still overwrite each other:
63
+ this package does not manage file ownership.
66
64
 
67
65
  | Kind | Meaning |
68
66
  | --- | --- |
@@ -70,66 +68,14 @@ identities. Messages come in three kinds:
70
68
  | `note` | Display-only FYI. Appears in the transcript; never starts a model turn. |
71
69
  | `result` | The automatic reply to a request: outcome, final text, and observed files. Delivered to the emitter for verification. |
72
70
 
73
- Every message moves through visible states: `pending` (queued), `processing`
74
- (claimed by a live session), `completed` / `interrupted` (settled), and `seen`
75
- (notes already shown). The lifecycle of a request is: queued claimed when the
76
- recipient is idle → worked on → result persisted and made available to the
77
- emitter → the recipient compacts before accepting another team turn → the
78
- emitter verifies the result against the original request and, if anything is
79
- missing, replies in the same thread with what remains to finish → the emitter
80
- compacts that result-review turn before accepting another team turn.
81
-
82
- ### Status widget
83
-
84
- While joined, a minimal live widget above the editor shows only the session
85
- header:
86
-
87
- ```text
88
- shop · pm · connected
89
- ```
90
-
91
- The state is `connected`, `working`, `compacting`, `paused`, or `select a model`.
92
- A pending count is appended when work addressed to you is still queued. Run
93
- `/team status` to show the complete unresolved requester → assignee flow,
94
- including each task subject, assignee presence, and whether the request is queued
95
- or active. Subjects are visible team-wide for coordination, while request bodies
96
- remain limited to their sender and recipient.
97
-
98
- ## Three terminals
99
-
100
- In the planning terminal:
101
-
102
- ```text
103
- /team create shop
104
- /team join shop pm
105
- ```
106
-
107
- In a backend worktree or repository:
108
-
109
- ```text
110
- /team join shop backend
111
- ```
112
-
113
- In a frontend worktree or repository:
114
-
115
- ```text
116
- /team join shop frontend
117
- ```
118
-
119
- Then tell PM: "Coordinate the login feature with backend and frontend. Agree on the
120
- API contract before implementation. Ask me before any push or deployment."
71
+ While joined, a minimal widget above the editor shows `team · alias · state`,
72
+ where state is `connected`, `working`, `compacting`, `paused`, or `select a
73
+ model`, plus a pending count when work is queued for you.
121
74
 
122
75
  Membership is restored automatically when the same Pi session is resumed or
123
- reloaded, so a restarted terminal rejoins its team without any command.
124
- `/new` and `/fork` start unaffiliated sessions on purpose.
125
-
126
- `pm` is an address, not a privileged role or an automatic persona. Give each agent
127
- its responsibilities in its own session. Each retains its own model, cwd,
128
- instructions, permissions and conversation. Team names connect separate worktrees;
129
- they are not inferred from directory names. Two agents using the same files can
130
- still overwrite each other's edits: this package does not manage file ownership.
76
+ reloaded. `/new` and `/fork` start unaffiliated sessions on purpose.
131
77
 
132
- ### Commands
78
+ ## Commands
133
79
 
134
80
  | Command | Meaning |
135
81
  | --- | --- |
@@ -137,233 +83,102 @@ still overwrite each other's edits: this package does not manage file ownership.
137
83
  | `/team join shop backend` | Register this session and enable automatic reception |
138
84
  | `/team list` | List teams; refresh team-name completion |
139
85
  | `/team members` | Show aliases, cwd, and idle/busy/paused/offline status |
140
- | `/team status` | Show every unresolved requester → assignee relationship and task subject |
141
- | `/team wake [message]` | Queue an actionable check-in request for every teammate except this session; the message is optional |
86
+ | `/team status` | Show every unresolved requester → assignee relationship |
87
+ | `/team wake [message]` | Queue an actionable check-in for every other teammate |
142
88
  | `/team send backend Implement login` | Queue a request that can start work |
143
89
  | `/team note frontend API contract changed` | Display an FYI; never starts a model turn |
144
- | `/team inbox` | Show the most recent 20 sent/received records and their states |
90
+ | `/team inbox` | Show the most recent 20 records and their states |
145
91
  | `/team pause` | Pause new work, without cancelling current work |
146
- | `/team resume` | Resume reception and reset the five-turn automatic budget |
147
- | `/team leave` | Leave; if processing a peer request, report its result first |
148
-
149
- Team names and aliases accept 1–48 lowercase letters, digits or hyphens, beginning
150
- with a letter. Unknown teams are rejected, never implicitly created. Duplicate
151
- live aliases are rejected. Tab completion supports subcommands, discovered team
152
- names and current teammate aliases. `/team list` refreshes teams created elsewhere.
153
-
154
- Sending to an offline **known** alias queues work until someone explicitly rejoins
155
- that alias. Sending to an unknown alias fails. An alias is a shared team address,
156
- not a private address for a particular human; anyone using this OS account can
157
- rejoin an offline alias and see its history. Use a new alias for a different role.
158
-
159
- `/team wake` sends a normal request to every known teammate except the sender,
160
- including offline aliases. Its standard prompt asks each agent to report current
161
- work, remaining work, blockers, and the next concrete step; request missing input
162
- through `team_send`; and finish authorized pending work instead of waiting. An
163
- optional message is appended as sender-provided context and does not replace the
164
- standard safety and authorization boundaries. Each recipient returns its own
165
- correlated result. If any teammate cannot be queued, the command reports the
166
- successful count and each failed alias instead of claiming complete delivery.
167
-
168
- ### Agent tools
169
-
170
- - `team_members`: discover the current team, without leaking lease tokens.
171
- - `team_send`: send `{ to, kind: "request" | "note", subject, body }`.
172
- - `team_status`: read-only view of outstanding work: the team-wide unresolved
173
- requester → assignee flow, requests you emitted (with recipient presence and
174
- age), work queued for you, results awaiting your review, your currently
175
- claimed task, and teammate presence.
92
+ | `/team resume` | Resume reception and reset the automatic turn budget |
93
+ | `/team leave` | Leave; if processing a request, report its result first |
94
+
95
+ Names and aliases are 1–48 lowercase letters, digits, or hyphens, starting with a
96
+ letter. Unknown teams are rejected, never implicitly created; duplicate live
97
+ aliases are rejected. Sending to an offline **known** alias queues until someone
98
+ rejoins it. Tab completion covers subcommands, discovered teams, and teammates.
99
+
100
+ ## Agent tools
101
+
102
+ - `team_members` discover teammates and their status.
103
+ - `team_send` send `{ to, kind: "request" | "note", subject, body }`.
104
+ - `team_status` — outstanding work: what you emitted and is unresolved, what is
105
+ queued for you, results awaiting your review, and third-party team activity.
176
106
 
177
107
  Tools cannot create teams, join, resume reception, change permissions, or launch
178
- terminals. They require membership established by you. Requests return **queued**,
179
- not "task completed". Tools and manual commands use the same mailbox validation.
180
-
181
- ## Delivery and results
182
-
183
- Requests and correlated results start a new turn only when the recipient is idle,
184
- has a selected model, no pending user messages or open extension prompt, an empty editor,
185
- and no task-boundary compaction in progress.
186
- A second readiness check handles a user starting work during a filesystem read.
187
- No running tool is interrupted. Notes are transcript-only; view them with
188
- `/team inbox`. They are not injected into the model's context.
189
-
190
- Incoming/outgoing messages show a folded preview. Use Pi's tool-output expansion
191
- shortcut (Ctrl+O by default) to read the full content. `/team inbox` refreshes the
192
- persisted state; an old transcript preview is an event snapshot, not a live receipt.
193
-
194
- For each processed request, the extension automatically sends **one** correlated
195
- result after `agent_settled`, not merely after an individual model/tool turn:
196
-
197
- - Last assistant **text**, capped at 3,000 characters. No thinking blocks.
198
- - Up to 50 absolute file paths observed in successful `edit`/`write` results.
199
- - An execution outcome: completed, failed, or interrupted.
200
-
201
- The assistant is instructed to include actual test results and blockers in its final
202
- text. The extension does **not** infer test success from a shell command or model
203
- claim. It does not automatically enumerate files changed through bash, custom tools,
204
- or other processes. The observed file list is not a Git diff or a complete change
205
- inventory. A completed run is not proof of task success; review the reported outcome
206
- and changes in the recipient worktree.
207
-
208
- Results may wake the requester so it can continue coordinating, but processing a
209
- result never produces another automatic reply. Delivered results quote the
210
- original request, and agents are instructed to verify the deliverable against it
211
- and reply in-thread with exactly what is missing when a result is incomplete or
212
- failed. Notes/acknowledgements never wake a model.
213
-
214
- ### Task-boundary compaction
215
-
216
- After a request or correlated result-review turn settles and its mailbox outcome
217
- is safely persisted, the extension calls Pi's documented `ctx.compact()` API.
218
- That session claims no other peer message while compaction is running. The focused
219
- instructions preserve user-authored goals and constraints, team identity,
220
- unresolved requester → assignee relationships, concrete outcomes, blockers,
221
- files, tests, and next actions while asking Pi to discard verbose tool output,
222
- duplicated task payloads, completed traces, and private reasoning.
223
-
224
- Compaction changes model context, not extension registration or mailbox state:
225
- `/team` commands and team tools remain available, and each terminal continues as
226
- an independent Pi session rather than a spawned subagent. Pi still applies its
227
- configured `keepRecentTokens`, so this is compaction rather than a hard context
228
- reset. It uses a summarization model call now to reduce repeated context on later
229
- tasks. If compaction fails, the TUI warns and reception continues; Pi's normal
230
- context-threshold compaction remains available. User takeover skips this automatic
231
- step because the resulting turn is no longer an isolated team task.
232
-
233
- While you have emitted requests that stay unresolved past five minutes, an
234
- automatic review turn asks your agent every minute to chase the responsible
235
- teammate in-thread or report the blockage to you. Reviews quiet down after three
236
- turns without mailbox progress and re-arm on any change; they share the
237
- five-turn automatic budget, never start new work, and never retry interrupted
238
- work on their own. The one-minute cadence and five-minute threshold are fixed
239
- defaults; they are not user-configurable yet.
240
-
241
- User takeover during a peer task pauses reception and sends an interrupted
242
- notice instead of forwarding the unrelated final answer. Files observed after the
243
- takeover are not included. `/team leave` does not cancel the current run; quitting
244
- Pi or reloading while working produces an interrupted record, not a success report.
245
-
246
- ## Limits and safety
247
-
248
- - At most five automatic peer turns per session before reception pauses. Use
249
- `/team resume` to continue. No daily token or monetary budget is enforced.
250
- - At most eight non-result messages in an automatically linked conversation.
251
- - Duplicate sender/recipient/subject/body messages within one minute are refused.
252
- - Maximum 16 KB per outgoing body, 20 KB per serialized outgoing message, and
253
- 50 inbox slots per recipient. Pending/claimed deliveries occupy a slot until
254
- settled, and each outgoing request reserves another slot for its automatic reply.
255
- A send is rejected if the recipient is full, or if a request's sender cannot
256
- reserve its reply. Notes need no reply reservation. Automatic replies are capped
257
- at 32 KB; their observed file list is shortened with a notice when necessary.
258
- Older teams may already be overcommitted; allow them to drain before sending
259
- more work. Results for previously accepted requests are never discarded.
260
- - Maximum 100 aliases and 500 message records per team, reserving result capacity
261
- for outstanding requests. History is not silently deleted. Create a fresh team
262
- when full. Inbox display is limited to the latest 20; persisted records remain.
263
- - Peer messages explicitly identify their origin and are not user consent. Rules
264
- instruct agents not to relay denied work, alter configuration, or evade plan mode.
265
- - Text is delivered as a custom message, never executed as a slash command or used
266
- for automatic `@file` expansion. The session's normal tool policy still applies.
267
- - This is **not a sandbox or an authorization system**. Agents and processes under
268
- the same OS user already have filesystem access. Prompt-level rules are not a
269
- hard guarantee against a model that ignores instructions. Do not place untrusted
270
- agents in a team or rely on team boundaries to protect secrets from that OS user.
271
- - Messages stay in local mailbox files but, when processed, their text is sent to
272
- the recipient's configured model provider like normal prompt content. Results
273
- are also shared with the requester. Do not send credentials or unrelated secrets.
274
-
275
- ## Persistence and recovery
276
-
277
- Storage: `~/.pi/agent/teams/<team>/state.json` (respects `PI_CODING_AGENT_DIR`).
278
- Each team is one small JSON record stored with optimistic concurrency: readers
279
- never wait on a lock, and writers compare-and-swap a monotonically increasing
280
- revision, retrying against a fresh read on conflict. Many agents can therefore
281
- write at the same time instead of queueing for a team-wide lock. Every
282
- publication is written to a private temporary file, synced, hard-linked into a
283
- bounded `revisions/` history, and atomically renamed into place; the history
284
- doubles as recovery evidence for interrupted writes. Files are 0600 and team
285
- folders 0700; envelopes carry a content hash. Unsafe/symlinked roots or mailbox
286
- files and invalid schemas fail closed; corrupt files are preserved for manual
287
- recovery, not erased. Mailboxes written before envelope records migrate
288
- transparently on their first write.
289
-
290
- Presence lives outside the shared record: each member renews its own
291
- `presence/<alias>.json` every two seconds, so heartbeats add no write
292
- contention. Presence expires after 30 seconds, or sooner when the recorded
293
- process has exited. A lock abandoned by a crashed writer is reclaimed after ten
294
- seconds. Transient storage errors are reported but never pause reception; the
295
- next tick retries.
296
-
297
- Directory watchers provide prompt delivery; periodic polling recovers missed
298
- notifications. Watchers and timers only run for joined interactive sessions and are
299
- closed on shutdown. Teammates observing a disconnected peer holding a claim
300
- sweep it so its requester receives an interrupted result instead of waiting
301
- forever.
302
-
303
- Ownership tokens fence out replaced sessions. Pending messages survive disconnection.
304
- Claimed work is marked interrupted on disconnect/rejoin; it is **not automatically
305
- replayed**, since edits may already have happened. This favors avoiding duplicate
306
- side effects over guaranteed execution: a crash after claiming but before starting
307
- can also leave an interrupted task. There is no exactly-once guarantee for filesystem
308
- changes or model actions. If storage cannot record a result, reception pauses and
309
- reports an error; review before retrying.
310
-
311
- Membership, pause state, and pending task-boundary compaction are recorded in Pi
312
- session entries. Resuming the same session can rejoin; `/new` and `/fork` do not
313
- inherit membership. Explicit leave clears restoration. Before attempting to claim
314
- work, the extension records that restoration must pause, without pausing the live
315
- session. Successful result persistence clears this recovery-only pause; failures
316
- and interruptions retain it. If shutdown interrupts a post-task compaction, the
317
- same session retries compaction before claiming queued peer work.
318
-
319
- Thus even an abrupt process death during a task restores paused and requires
320
- `/team resume` before pending work starts. A crash just before a claim can
321
- conservatively require resume too. History and pending work remain in the team
322
- until explicitly managed outside this prototype.
323
-
324
- Local disks only: shared network filesystems, containers with separate home
325
- directories, cross-machine transport, and native Windows are not supported here.
326
-
327
-
328
- ## Manage the package
329
-
330
- For an npm installation:
108
+ terminals; they require membership you established. A request returns **queued**,
109
+ never "task completed".
331
110
 
332
- ```sh
333
- pi list
334
- pi update npm:@prjct.app/pi-team
335
- pi remove npm:@prjct.app/pi-team
336
- ```
111
+ ## How delivery works
337
112
 
338
- Use `pi config` to enable or disable individual resources. Use `pi config -l` for project settings and add `-l` to removal when you installed locally.
113
+ A request or result starts a turn only when the recipient is idle, has a selected
114
+ model, no pending user message or open prompt, an empty editor, and no
115
+ task-boundary compaction running. No running tool is interrupted.
339
116
 
340
- To pin version 0.1.3, use `pi install npm:@prjct.app/pi-team@0.1.3`. Pi skips pinned npm versions during package updates. For a Git installation, update or remove using the same `git:github.com/prjct-app/pi-team` source instead of the npm source.
117
+ For each processed request the extension sends **one** result after the agent
118
+ settles: the last assistant text capped at 3,000 characters (no thinking), up to
119
+ 50 absolute paths observed in successful `edit`/`write` calls, and an outcome of
120
+ completed, failed, or interrupted.
341
121
 
342
- When switching from GitHub to npm, remove the Git installation first, then install the npm package and restart Pi.
122
+ That file list is **not** a Git diff. Files changed through bash, custom tools, or
123
+ other processes are not enumerated, and the extension never infers test success
124
+ from a shell command or a model claim. **A completed run is not proof of success**:
125
+ review the reported outcome and the recipient worktree.
343
126
 
344
- ## Troubleshooting
127
+ After each task the session compacts before accepting another, so independent
128
+ sessions stay focused. While a request you emitted stays unresolved past five
129
+ minutes, a review turn asks your agent to chase the teammate or tell you what is
130
+ blocked. See [Architecture](docs/architecture.md) for both mechanisms.
345
131
 
346
- | Symptom or notice | Cause and action |
347
- | --- | --- |
348
- | A request stays queued | Run `/team status` to identify its requester, assignee, subject, and assignee presence. The recipient may be busy, paused, offline, missing a selected model, or typing in its editor. After five minutes, automatic review turns chase the teammate or surface the blockage to you. |
349
- | `Team auto-turn limit reached` | Five automatic peer turns ran without user input. Review the transcript, then `/team resume`. |
350
- | Automatic context compaction failed | The mailbox result was already persisted. Reception continues, and Pi can retry through its normal threshold compaction or `/compact`. |
351
- | `Membership expired or replaced` | Another live session took your alias, or your membership was fenced out. Rejoin with `/team join <team> <alias>`; choose a new alias if the old one is in use. |
352
- | `Recipient inbox full` / `Sender inbox full` | Fifty unsettled deliveries per member, with one slot reserved per outstanding request. Let the teammate drain its queue; notes are exempt from reply reservations. |
353
- | `Team history full (500 records)` | The team is at capacity; history is never silently deleted. Create a fresh team and rejoin. |
354
- | Repeated storage warnings | Transient read/write conflicts are retried automatically and never pause reception. If the same warning persists, check that the teams directory is a local disk and report the issue. |
355
- | A teammate went offline mid-task | Its claimed work is interrupted and the emitter receives that result; it is not replayed automatically. Review the worktree, then resend explicitly if still needed. |
132
+ ## Safety
356
133
 
357
- ## Package and API documentation
134
+ - **This is not a sandbox or an authorization system.** Agents and processes under
135
+ the same OS user already have filesystem access. Prompt-level rules are not a
136
+ hard guarantee against a model that ignores them. Do not place untrusted agents
137
+ in a team or rely on team boundaries to protect secrets from that OS user.
138
+ - Peer messages are identified as untrusted data and **are not user consent**.
139
+ Agents are instructed not to relay denied work, change configuration, or evade
140
+ plan mode. Peer text is never executed as a slash command or expanded as a file
141
+ mention.
142
+ - Messages stay in local files, but when processed their text goes to the
143
+ recipient's configured **model provider** like any prompt, and results go to the
144
+ requester. Do not send credentials or unrelated secrets.
145
+ - User takeover during a peer task pauses reception and reports an interrupted
146
+ result instead of forwarding your unrelated work.
147
+
148
+ ## Limits
149
+
150
+ | Limit | Value |
151
+ | --- | --- |
152
+ | Automatic peer turns before reception pauses | 5, then `/team resume` |
153
+ | Messages in one automatically linked conversation | 8 non-result |
154
+ | Unsettled deliveries per member | 50 slots, one reserved per outstanding request |
155
+ | Records per team | 500, including reserved result capacity |
156
+ | Members per team | 100 |
157
+ | Outgoing body / serialized message | 16 KB / 20 KB |
158
+ | Automatic result | 32 KB, file list shortened with a notice |
159
+ | Presence lease | 30 s, renewed every 2 s |
160
+ | Review threshold / cadence | 5 min unresolved, checked every 1 min |
161
+ | Duplicate suppression | identical message within 1 min is refused |
162
+
163
+ No daily token or monetary budget is enforced. History is never silently deleted:
164
+ create a fresh team when one is full. Review cadence and the turn budget are fixed
165
+ defaults, not user-configurable yet.
358
166
 
359
- Uses public commands, tools, lifecycle events, custom messages, persisted session entries, and `ExtensionContext.compact()`. Storage is self-contained (no runtime dependencies); Pi libraries remain peer dependencies.
167
+ ## Troubleshooting
360
168
 
361
- See [Package structure and compatibility](docs/package.md) for the manifest, dependency policy, shipped resources, and official references. This package follows the [official Pi package guide](https://github.com/earendil-works/pi/blob/v0.85.1/packages/coding-agent/docs/packages.md) and [extension API guide](https://github.com/earendil-works/pi/blob/v0.85.1/packages/coding-agent/docs/extensions.md) for the tested version.
169
+ | Symptom | Cause and action |
170
+ | --- | --- |
171
+ | A request stays queued | Run `/team status`. The recipient may be busy, paused, offline, missing a model, or typing. After five minutes, review turns chase it or surface the blockage. |
172
+ | `Team auto-turn limit reached` | Five automatic turns ran without user input. Review the transcript, then `/team resume`. |
173
+ | Automatic compaction failed | The result was already persisted. Reception continues; Pi can still compact normally or via `/compact`. |
174
+ | `Membership expired or replaced` | Another live session took your alias. Rejoin, choosing a new alias if the old one is in use. |
175
+ | `Recipient inbox full` / `Sender inbox full` | Fifty unsettled deliveries per member, one slot reserved per outstanding request. Let the teammate drain; notes need no reservation. |
176
+ | `Team history full (500 records)` | At capacity; history is never deleted. Create a fresh team and rejoin. |
177
+ | Repeated storage warnings | Conflicts retry automatically and never pause reception. If one persists, check that the teams directory is on a local disk and report it. |
178
+ | A teammate went offline mid-task | Its claimed work is interrupted and the emitter receives that result; it is not replayed. Review the worktree, then resend explicitly. |
362
179
 
363
180
  ## Development
364
181
 
365
- From a repository checkout:
366
-
367
182
  ```sh
368
183
  npm ci --ignore-scripts
369
184
  npm run check
@@ -371,8 +186,16 @@ npm test
371
186
  npm run check:package
372
187
  ```
373
188
 
374
- Pi loads the TypeScript entry point directly; no build step is required. To try this checkout for one run, use `pi -e .`. Tests use isolated temporary state and do not call model APIs. See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution rules and [CHANGELOG.md](CHANGELOG.md) for release notes.
189
+ Pi loads the TypeScript entry point directly; there is no build step. Use `pi -e .`
190
+ to try a checkout. Tests use isolated temporary state and never call model APIs.
191
+
192
+ `npm run check` also enforces that `src/` contains no `let`: session state is a
193
+ single immutable record updated functionally, and reads go through its accessor at
194
+ the point of use rather than being captured across an `await`.
195
+
196
+ ## More
375
197
 
376
- ## License
198
+ - [Architecture](docs/architecture.md) — storage, concurrency, recovery, context budget
199
+ - [Package structure](docs/package.md) · [Releases](docs/releases.md) · [Contributing](CONTRIBUTING.md) · [Changelog](CHANGELOG.md)
377
200
 
378
201
  [MIT](LICENSE).
@@ -0,0 +1,160 @@
1
+ # Architecture
2
+
3
+ How pi-team stores state, coordinates concurrent sessions, and recovers from
4
+ failure. For everyday use see the [README](../README.md).
5
+
6
+ ## Storage model
7
+
8
+ Each team is one JSON record at `~/.pi/agent/teams/<team>/state.json`, honouring
9
+ `PI_CODING_AGENT_DIR`. The record holds the member roster and the full message
10
+ history. Team folders are `0700` and files `0600`; an unsafe, symlinked, or
11
+ foreign-owned directory or record fails closed rather than being repaired.
12
+
13
+ Records are wrapped in an envelope carrying a schema marker, a monotonically
14
+ increasing revision, and a SHA-256 of the payload. A corrupt record is preserved
15
+ for manual recovery, never erased. Mailboxes written before envelopes existed
16
+ are read as revision 0 and rewritten as envelope records on their first write.
17
+
18
+ ## Concurrency
19
+
20
+ Readers never take a lock. Every publication writes a private temporary file,
21
+ hard-links it into a bounded `revisions/` history, and atomically renames it into
22
+ place, so a concurrent read sees either the whole previous record or the whole
23
+ next one. The history doubles as recovery evidence for an interrupted write.
24
+
25
+ Writers compare-and-swap on the revision under a short-lived sibling lock.
26
+ A conflict fails fast and the caller retries against a fresh read, so many agents
27
+ write concurrently instead of queueing behind a team-wide lock. A lock abandoned
28
+ by a crashed writer is reclaimed after ten seconds.
29
+
30
+ Because every publication renames a **new inode** into place, readers can safely
31
+ cache a parsed record keyed on `(inode, size, mtime)`: a write by any process
32
+ changes the inode. Cached reads serve the polling loop; mutations always read
33
+ uncached, so they never operate on the shared cached object.
34
+
35
+ ## Presence
36
+
37
+ Presence lives outside the shared record. Each member rewrites only its own
38
+ `presence/<alias>.json` every two seconds, so heartbeats add no write contention
39
+ and never touch `state.json`.
40
+
41
+ These files are deliberately non-durable: the atomic rename is kept, both fsyncs
42
+ are not. Presence expires after 30 seconds and is rewritten every 2, so a write
43
+ lost to a crash only makes a member look offline sooner — never alive longer.
44
+ A member is also considered gone as soon as its recorded process has exited.
45
+
46
+ ## Request lifecycle
47
+
48
+ A request is queued, claimed when the recipient is idle, worked on, and settled
49
+ with a result delivered back to the emitter. The recipient compacts before
50
+ accepting another team turn; the emitter verifies the result against its original
51
+ request and replies in-thread only if something is missing.
52
+
53
+ States are `pending`, `processing`, `completed`, `interrupted`, and `seen`
54
+ (notes already displayed).
55
+
56
+ ## Sweeping orphaned claims
57
+
58
+ A session that dies holding a claim would otherwise leave its requester waiting
59
+ forever, so peers interrupt the claim on its behalf and the requester receives an
60
+ `interrupted` result.
61
+
62
+ Members are never removed from the record — leaving only marks them offline — so
63
+ "someone is offline" is permanently true once anyone has ever left and cannot be
64
+ used to decide when to sweep. A snapshot instead reports `sweepable`, true only
65
+ when a member the record still counts as connected is actually dead **and** still
66
+ holds a claim, which is the only case where sweeping changes anything. Everything
67
+ else degrades correctly without it: rejoining re-admits a stale alias on its own,
68
+ and displayed status comes from presence rather than the record.
69
+
70
+ ## Context budget
71
+
72
+ What an extension puts in the model context is paid on every later turn and
73
+ stays in the session branch, so injected values are bounded and any elision is
74
+ stated rather than silent.
75
+
76
+ | Carrier | In LLM context | Used for |
77
+ | --- | --- | --- |
78
+ | `before_agent_start` system prompt | yes, every turn | peer rules and team identity, carried exactly once |
79
+ | `sendMessage` content | yes, and it persists | peer messages and review turns |
80
+ | Tool results | yes, and they persist | `team_members`, `team_send`, `team_status` |
81
+ | `appendEntry` | **no**, TUI only | notes, `/team inbox`, sent-message previews |
82
+
83
+ Consequences worth knowing:
84
+
85
+ - Peer rules are **not** repeated inside each message; they arrive once per turn
86
+ through the system prompt.
87
+ - A delivered result quotes the original request excerpted to 500 characters,
88
+ with the id kept. Verification of a very long request works from an extract.
89
+ - `team_status` lists are capped and report an `omitted` count. Its
90
+ `otherTeamWork` list carries only third-party work; anything addressed to or
91
+ emitted by this session is already in the other lists.
92
+ - Each `team_status` call is a point-in-time snapshot. Earlier results in the
93
+ same conversation are stale but cannot be retracted, which is why each one is
94
+ kept small.
95
+
96
+ ## Task-boundary compaction
97
+
98
+ Once a result is safely persisted, the extension calls Pi's `ctx.compact()`. That
99
+ session claims no other peer message while compaction runs. The instructions
100
+ preserve user-authored goals and constraints, team identity, unresolved
101
+ requester → assignee relationships, outcomes, blockers, files, tests, and next
102
+ actions, and discard verbose tool output, duplicated payloads, completed traces,
103
+ and private reasoning.
104
+
105
+ Compaction changes model context, not extension registration or mailbox state:
106
+ `/team` commands and tools stay available, and each terminal remains an
107
+ independent Pi session rather than a spawned subagent. Pi still applies its
108
+ configured `keepRecentTokens`. If compaction fails, the TUI warns and reception
109
+ continues. User takeover skips it, because the turn is no longer an isolated team
110
+ task.
111
+
112
+ ## Recovery and guarantees
113
+
114
+ Ownership tokens fence out replaced sessions. Pending messages survive
115
+ disconnection. Claimed work is marked interrupted on disconnect or rejoin and is
116
+ **not automatically replayed**, since edits may already have happened.
117
+
118
+ This favours avoiding duplicate side effects over guaranteed execution. **There is
119
+ no exactly-once guarantee** for filesystem changes or model actions: a crash after
120
+ claiming but before starting also leaves an interrupted task. If storage cannot
121
+ record a result, reception pauses and reports an error.
122
+
123
+ Membership, pause state, and pending compaction are recorded in Pi session
124
+ entries. Resuming the same session rejoins; `/new` and `/fork` do not inherit
125
+ membership. Before claiming work the extension records that restoration must
126
+ pause, without pausing the live session, so an abrupt process death during a task
127
+ restores paused and requires `/team resume`. A crash just before a claim can
128
+ conservatively require resume too.
129
+
130
+ Directory watchers provide prompt delivery; polling every two seconds recovers
131
+ missed notifications. Both run only for joined interactive sessions and close on
132
+ shutdown. Transient storage errors are reported but never pause reception.
133
+
134
+ ## Design decisions and non-goals
135
+
136
+ | Concern | Choice |
137
+ | --- | --- |
138
+ | Session creation | The user opens every terminal; nothing is spawned |
139
+ | Discovery | Explicit named teams and aliases, never inferred from directories |
140
+ | Transport | Local filesystem records; no broker, socket server, or daemon |
141
+ | Presence | Per-member files outside the record; heartbeats never lock |
142
+ | Concurrent writes | Compare-and-swap with retry; no team-wide lock |
143
+ | Follow-up | Periodic review turns delegated to the agent, not programmatic retries |
144
+ | Active recipient | Wait until fully idle; no steering between tools |
145
+ | Offline recipient | Persist to a known alias until it rejoins |
146
+ | Approval | Never supplied by peers; local policies always win |
147
+ | Coordination | Direct messages and bulk check-ins; no task board or worktree manager |
148
+ | Scope | Local disks only: no network filesystems, cross-machine transport, or native Windows |
149
+
150
+ Not provided: file ownership between agents, a sandbox, an authorization system,
151
+ or protection of secrets from other processes under the same OS user.
152
+
153
+ ## Pi interfaces used
154
+
155
+ `registerCommand`, `registerTool`, `sendMessage`, `appendEntry`, custom entry and
156
+ message renderers, `setWidget`, `getEditorText`, `isIdle`, `hasPendingMessages`,
157
+ `compact`, session lifecycle events, UI prompt events, `tool_result`,
158
+ `message_end`, and `agent_settled`. All public and documented; no host internals
159
+ are imported, no prototypes patched, and peer text is never shell-evaluated or
160
+ expanded as file mentions.
package/docs/package.md CHANGED
@@ -2,57 +2,40 @@
2
2
 
3
3
  ## Identity
4
4
 
5
- - npm name: `@prjct.app/pi-team`.
6
- - Initial version: `0.1.0`.
7
- - Source repository: [prjct-app/pi-team](https://github.com/prjct-app/pi-team).
8
- - Tested host: Pi `0.85.1`; Node.js `22.19+`.
9
-
10
- The npm name and repository name may differ. Repository URLs remain unchanged. Existing runtime command names, event names, persisted entry types, and settings keys are unchanged by the package rename.
11
-
12
- ## Resource manifest
13
-
14
- ```json
15
- {
16
- "name": "@prjct.app/pi-team",
17
- "keywords": [
18
- "pi-package"
19
- ],
20
- "pi": {
21
- "extensions": [
22
- "./index.ts"
23
- ]
24
- }
25
- }
26
- ```
27
-
28
- The `pi-package` keyword makes the package discoverable. Manifest paths are relative to the package root. The extension entry point is shipped as TypeScript because Pi loads it directly. There is no CLI binary or JavaScript build artifact to install separately.
5
+ - npm name: `@prjct.app/pi-team`
6
+ - Repository: [prjct-app/pi-team](https://github.com/prjct-app/pi-team)
7
+ - Tested host: Pi `0.85.1`; Node.js `22.19+`
29
8
 
30
- ## Dependencies
31
-
32
- Pi-provided libraries imported by this package are declared in `peerDependencies` with `*`, as required by Pi's package guide. They are not bundled. Exact Pi 0.85.1 development dependencies establish the tested baseline; the peer wildcard is not a claim that every Pi release is supported.
33
-
34
- Third-party runtime dependencies belong in `dependencies`. Companion extensions are installed separately only when communication uses Pi's event bus; this package does not import code from a separately installed companion. A package that directly imports another Pi package's resources must instead bundle it following the official guide.
9
+ The npm name and repository name may differ. The package rename changed no
10
+ runtime command names, event names, persisted entry types, or settings keys.
35
11
 
36
- ## Public interfaces
12
+ ## Dependencies
37
13
 
38
- Uses public commands, tools, lifecycle events, custom messages, persisted session entries, and programmatic compaction through `ExtensionContext.compact()`. Storage is self-contained with optimistic-concurrency records; this package has no runtime dependencies. Pi libraries remain peer dependencies.
14
+ Pi-provided libraries are declared in `peerDependencies` with `*`, as the Pi
15
+ package guide requires, and are not bundled. The exact Pi 0.85.1 devDependencies
16
+ establish the tested baseline; the peer wildcard is not a claim that every Pi
17
+ release is supported. This package has **no runtime dependencies**: storage is
18
+ self-contained.
39
19
 
40
- ## Published contents
20
+ Third-party runtime dependencies would belong in `dependencies`. A package that
21
+ directly imports another Pi package's resources must bundle it instead.
41
22
 
42
- The `files` allowlist includes runtime resources, user documentation, and license files. Development tests, dependency folders, repository settings, and Git history are excluded. npm also includes `package.json` automatically. The npm lockfile remains in the repository for repeatable development installs.
23
+ ## Publishing
43
24
 
44
- Run `npm run check:package` to inspect the exact prospective tarball before release. Check that each manifest entry and each referenced local document exists in the packed file list. Only claim npm availability after verifying a successful registry publication.
25
+ The `files` allowlist ships runtime resources, user documentation, and licence
26
+ files; tests, dependencies, and repository settings are excluded. Run
27
+ `npm run check:package` to inspect the exact prospective tarball, and confirm every
28
+ manifest entry and referenced local document is present. Only claim npm
29
+ availability after verifying a successful registry publication.
45
30
 
46
31
  ## Official references
47
32
 
48
- These links are pinned to the tested Pi version rather than the moving main branch:
49
-
50
- - [Packages: manifest, sources, dependencies, filtering, and deduplication](https://github.com/earendil-works/pi/blob/v0.85.1/packages/coding-agent/docs/packages.md).
51
- - [Extensions: lifecycle, commands, tools, messages, and UI APIs](https://github.com/earendil-works/pi/blob/v0.85.1/packages/coding-agent/docs/extensions.md).
52
- - [TUI: components, rendering, terminal widths, and image support](https://github.com/earendil-works/pi/blob/v0.85.1/packages/coding-agent/docs/tui.md).
53
-
54
- The installed `@earendil-works/pi-coding-agent@0.85.1` package ships the same guides under `docs/`. The [current official guide](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/packages.md) may describe changes beyond this tested baseline.
33
+ Pinned to the tested version rather than the moving main branch:
55
34
 
56
- ## Discovery metadata
35
+ - [Packages](https://github.com/earendil-works/pi/blob/v0.85.1/packages/coding-agent/docs/packages.md) — manifest, sources, dependencies, deduplication
36
+ - [Extensions](https://github.com/earendil-works/pi/blob/v0.85.1/packages/coding-agent/docs/extensions.md) — lifecycle, commands, tools, messages, UI
37
+ - [TUI](https://github.com/earendil-works/pi/blob/v0.85.1/packages/coding-agent/docs/tui.md) — components, rendering, widths
57
38
 
58
- The `pi-package` keyword identifies this package for the official Pi gallery. Focused keywords describe its actual features. The `pi.image` field points to its public cover, following the [official gallery metadata format](https://github.com/earendil-works/pi/blob/v0.85.1/packages/coding-agent/docs/packages.md#gallery-metadata).
39
+ The installed `@earendil-works/pi-coding-agent@0.85.1` ships the same guides under
40
+ `docs/`. The [current guide](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/packages.md)
41
+ may describe changes beyond this tested baseline.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prjct.app/pi-team",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "description": "Coordinate independent PI Agent sessions with local team messaging, queued tasks, and shared results.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -63,10 +63,10 @@
63
63
  "index.ts",
64
64
  "src",
65
65
  "docs",
66
+ "!docs/cover.png",
66
67
  "README.md",
67
68
  "CONTRIBUTING.md",
68
69
  "LICENSE",
69
- "CHANGELOG.md",
70
- "docs/package.md"
70
+ "CHANGELOG.md"
71
71
  ]
72
72
  }
package/src/mailbox.ts CHANGED
@@ -150,15 +150,22 @@ export class Mailbox {
150
150
  for (const attempt of ATTEMPTS) {
151
151
  const record = await this.readState(team);
152
152
  const state = record.payload;
153
- const before = JSON.stringify(state);
153
+ // The parser already produced this for the content hash. A pre-envelope
154
+ // record carries none, so that path serializes as before. A mismatch
155
+ // could only ever cause one redundant publication, never a lost write:
156
+ // `before` comes from the pre-action object and `after` from the
157
+ // post-action one, so they cannot coincide by accident.
158
+ const before = record.payloadJson ?? JSON.stringify(state);
154
159
  const presence = await this.readPresence(team);
155
160
  const swept = state.members.filter(member => member.status !== 'offline' && !this.alive(member, presence));
156
161
  for (const member of swept) this.disconnect(state, member);
157
162
  const result = action(state);
158
- if (before === JSON.stringify(state)) return result;
163
+ const after = JSON.stringify(state);
164
+ if (before === after) return result;
159
165
  if (!Value.Check(StateSchema, state)) throw new Error('Invalid mailbox format; refusing to write');
160
166
  try {
161
- await publish(this.recordPath(team), record.revision, state, this.normalize(team), { maxBytes: MAX_BYTES });
167
+ await publish(this.recordPath(team), record.revision, state, this.normalize(team),
168
+ { maxBytes: MAX_BYTES, payloadJson: after });
162
169
  await Promise.all(swept.map(member => unlink(this.presencePath(team, member.alias)).catch(() => {})));
163
170
  return result;
164
171
  } catch (error) {
@@ -223,7 +230,14 @@ export class Mailbox {
223
230
  await this.writePresence(member, status);
224
231
  }
225
232
 
226
- /** Lock-free consistent view of the record with presence-based statuses. */
233
+ /**
234
+ * Lock-free consistent view of the record with presence-based statuses.
235
+ *
236
+ * The cached read returns a shared record, and the message objects below are
237
+ * the record's own, not copies. Never mutate them: it would corrupt both the
238
+ * process-wide cache and the `payloadJson` taken alongside it. `mutate` is
239
+ * safe because it reads uncached.
240
+ */
227
241
  async snapshot(member: Membership): Promise<Snapshot> {
228
242
  const record = await this.readState(member.team, true);
229
243
  this.owner(record.payload, member);
package/src/store.ts CHANGED
@@ -14,7 +14,13 @@ import { dirname, join } from 'node:path';
14
14
  * hard-links its envelope into a bounded revisions/ history, which doubles as
15
15
  * recovery evidence for interrupted writes.
16
16
  */
17
- export type Record<T> = { revision: number; payload: T };
17
+ /**
18
+ * `payloadJson` is the canonical `JSON.stringify(payload)`, carried when the
19
+ * parser already had to compute it for the content hash. It is consistent by
20
+ * construction, never derived from the raw file text, and always optional: a
21
+ * pre-envelope record has none and callers fall back to serializing.
22
+ */
23
+ export type Record<T> = { revision: number; payload: T; payloadJson?: string };
18
24
  /** Parse raw file bytes into a record, throwing on corruption. Never deletes. */
19
25
  export type Normalize<T> = (raw: string) => Record<T>;
20
26
  /**
@@ -46,10 +52,11 @@ export function envelope<T>(raw: string): Record<T> {
46
52
  if (typeof parsed.revision !== 'number' || !Number.isSafeInteger(parsed.revision) || parsed.revision < 1) {
47
53
  throw Object.assign(new Error('Invalid record revision.'), { code: 'CORRUPT_RECORD' });
48
54
  }
49
- if (parsed.contentHash !== sha256(JSON.stringify(parsed.payload))) {
55
+ const payloadJson = JSON.stringify(parsed.payload);
56
+ if (parsed.contentHash !== sha256(payloadJson)) {
50
57
  throw Object.assign(new Error('Record hash mismatch; preserved for manual recovery.'), { code: 'CORRUPT_RECORD' });
51
58
  }
52
- return { revision: parsed.revision, payload: parsed.payload as T };
59
+ return { revision: parsed.revision, payload: parsed.payload as T, payloadJson };
53
60
  }
54
61
 
55
62
  function assertSafeFile(path: string, info: { isFile(): boolean; size: number; mode: number; uid: number }, maxBytes: number): void {
@@ -162,7 +169,7 @@ async function pruneRevisions(dir: string, latest: number): Promise<void> {
162
169
  */
163
170
  export async function publish<T>(
164
171
  path: string, expectedRevision: number, payload: T, normalize: Normalize<T>,
165
- options: { maxBytes: number; durability?: Durability },
172
+ options: { maxBytes: number; durability?: Durability; payloadJson?: string },
166
173
  ): Promise<Record<T>> {
167
174
  const durability = options.durability ?? 'full';
168
175
  await mkdir(dirname(path), { recursive: true, mode: 0o700 });
@@ -175,12 +182,15 @@ export async function publish<T>(
175
182
  throw Object.assign(new Error(`Record changed before the write; current revision is ${revision}.`), { code: 'STALE_REVISION' });
176
183
  }
177
184
  const next = revision + 1;
178
- const payloadJson = JSON.stringify(payload);
185
+ // Supplied by callers that already serialized this exact object; it must
186
+ // equal JSON.stringify(payload). The record stays self-consistent either
187
+ // way, because the hash is taken over the string that gets embedded.
188
+ const payloadJson = options.payloadJson ?? JSON.stringify(payload);
179
189
  const text = `{"schemaVersion":1,"revision":${next},"contentHash":"${sha256(payloadJson)}","payload":${payloadJson}}`;
180
190
  if (Buffer.byteLength(text) > options.maxBytes) throw new Error('Record size limit exceeded.');
181
191
  const historyPath = join(dirname(path), 'revisions', `${next}.json`);
182
192
  const previous = await readRecord(historyPath, (raw: string) => envelope<T>(raw), options.maxBytes);
183
- if (previous && (previous.revision !== next || sha256(JSON.stringify(previous.payload)) !== sha256(payloadJson))) {
193
+ if (previous && (previous.revision !== next || (previous.payloadJson ?? JSON.stringify(previous.payload)) !== payloadJson)) {
184
194
  throw new Error('An interrupted publication owns this revision; explicit recovery is required.');
185
195
  }
186
196
  if (!previous) await writeAtomic(historyPath, text, durability);
@@ -195,7 +205,7 @@ export async function publish<T>(
195
205
  cache.delete(path);
196
206
  counters.publishes++;
197
207
  await pruneRevisions(dirname(path), next).catch(() => {});
198
- return { revision: next, payload };
208
+ return { revision: next, payload, payloadJson };
199
209
  } finally {
200
210
  await lock.close();
201
211
  await unlink(lockPath).catch(() => {});
package/docs/cover.png DELETED
Binary file
package/docs/reference.md DELETED
@@ -1,42 +0,0 @@
1
- # Reference and deliberate differences
2
-
3
- Verified against the live official Markdown documentation during implementation:
4
-
5
- - https://code.claude.com/docs/en/cross-session-messaging.md
6
- - https://code.claude.com/docs/en/agent-teams.md
7
-
8
- The closest reference is **cross-session messaging**: independently started sessions,
9
- discovery with `ListAgents`, communication with `SendMessage`, compact expandable
10
- previews, and per-session permission boundaries. Current documentation says local
11
- Claude Code sessions use per-session sockets/named pipes. The team feature adds a
12
- lead that spawns teammates, optional shared task coordination, and JSON mailboxes.
13
- These are distinct features; historical `TeamCreate` examples are not current.
14
-
15
- Our user-selected scope differs deliberately:
16
-
17
- | Concern | pi-team |
18
- | --- | --- |
19
- | Session creation | User opens all terminals |
20
- | Discovery | Explicit named team and aliases |
21
- | Transport | Local filesystem records with optimistic concurrency, no broker or socket server |
22
- | Presence | Per-member files outside the shared record; heartbeats never lock |
23
- | Concurrent writes | Compare-and-swap revisions with retry; no team-wide lock |
24
- | Follow-up | Periodic review turns delegated to the agent, not programmatic retries |
25
- | Active recipient | Wait until fully idle; no between-tool steering |
26
- | Offline recipient | Persist to a known alias until rejoin |
27
- | Results | Automatic last-text reply to requests, quoted against the original request |
28
- | Approval | Never supplied by peers; preserve local policies |
29
- | Coordination | Direct messages, `/team wake [message]` bulk check-ins, and task-boundary context compaction; no task board or worktree manager |
30
- | Limits | Bounded conversations, inboxes and automatic turns |
31
- | UI | Existing Pi loader plus a minimal session widget, on-demand requester → assignee flow through `/team status`, and expandable messages |
32
-
33
- Pi APIs used: `registerCommand`, `registerTool`, `sendMessage`, custom entry/message
34
- renderers, `setWidget`, `getEditorText`, `isIdle`, `hasPendingMessages`, `compact`,
35
- session lifecycle, UI prompt events, `tool_result`, `message_end`, and `agent_settled`.
36
- No monkey-patching of Pi internals, shell evaluation of peer messages, forwarding
37
- of thinking, or modifications to existing local extensions are required.
38
-
39
- Integration tests exercise the extension through a simulated Pi API, backed by real
40
- filesystem operations. The three-process scenario hosts PM/backend/frontend in
41
- separate Node processes, checks queued delivery and correlated responses, kills the
42
- backend, and checks recovery. This is not a live three-model behavioral evaluation.