ac-teams 0.0.0-stage → 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alex Coote
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/README.md CHANGED
@@ -1,3 +1,208 @@
1
- # Temporary Holding Version
1
+ # ac-teams
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Persistent Pi agents, shared tasks, and searchable messages, without a workflow engine or a background service. A member stays alive after an assignment so the same session can take more work.
4
+
5
+ Local v0.0.1 implementation, tested on Linux with Pi 1.4.0. Not published to npm.
6
+
7
+ ## Features at a glance
8
+
9
+ Each column shows ac-teams alone or with one companion tool. A check mark (✓) means that setup provides the feature; a dash (-) means it does not.
10
+
11
+ | Feature | Standalone ac-teams | + ACStack | + Herdr |
12
+ | --- | --- | --- | --- |
13
+ | Persistent Pi members and native session resume | ✓ | ✓ | ✓ |
14
+ | Shared tasks, dependencies, and searchable messages | ✓ | ✓ | ✓ |
15
+ | Optional isolated Git worktrees | ✓ | ✓ | ✓ |
16
+ | Planning, review, and shipping skills | - | ✓ | - |
17
+ | Terminal tabs, panes, and visible root sessions | - | - | ✓ |
18
+
19
+ ACStack adds workflow guidance; Herdr adds terminal management. These are companion capabilities, not automatic integrations. ac-teams members run headlessly; Herdr does not automatically open a pane for each member.
20
+
21
+ ## Try it locally
22
+
23
+ Requirements: Linux, Node 22.19 or newer, and an authenticated `pi` executable on `PATH`.
24
+
25
+ ```sh
26
+ npm ci
27
+ pi -e /absolute/path/to/ac-teams/src/index.ts
28
+ ```
29
+
30
+ Load the extension once, either explicitly or through Pi package settings. Children load ac-teams explicitly and otherwise keep normal Pi extensions, skills, authentication, and cwd instructions. They receive their brief, not the parent's conversation.
31
+
32
+ Use Pi's `/trust` command to approve each member's cwd before launching there. RPC children cannot answer Pi's initial trust prompt. ac-teams rejects readiness if project resources were skipped; it never auto-approves a project. A one-run approval in the root is not a saved approval for children or new worktrees.
33
+
34
+ Other delegation tools can coexist. ac-teams warns when `Agent`, `SubagentWorkflow`, or `subagent` is active, but does not disable them. **Its limits cover only members launched through `team_agent`.** Use Pi's resource settings to disable other launchers if you want one team authority. Shell access and same-user extensions are not sandboxed.
35
+
36
+ ## Three tools, no prescribed workflow
37
+
38
+ | Tool | Actions | Main rule |
39
+ | --- | --- | --- |
40
+ | `team_agent` | `launch`, `list`, `status`, `stop`, `resume`, `limits`, `cleanup` | A parent controls its descendants, not peers or ancestors |
41
+ | `team_task` | `create`, `list`, `read`, `edit`, `claim`, `start`, `release`, `complete`, `reopen` | Checkout reserves work; dependencies gate starting, not ownership |
42
+ | `team_message` | `send`, `history`, `search` | Recipients select who is notified, not who can read |
43
+
44
+ `/teams` shows member status and native session/worktree locations. `/teams stop` stops every descendant. The root opens coordination state lazily on the first team request.
45
+
46
+ ### Give members bounded assignments
47
+
48
+ Example `team_agent` arguments:
49
+
50
+ ```json
51
+ {"action":"launch","brief":"Review src/parser.ts. Read only; do not delegate or edit. Send findings to root through team_message."}
52
+ ```
53
+
54
+ `cwd` defaults to the requesting member's directory. `model` (`provider/model`) and `thinking` default to that member's current selection. An explicit override wins. Native Pi selects the model; provider/account rejections remain visible in member status and notify the parent. There is no automatic model retry.
55
+
56
+ Launch returns a stable member ID, PID, cwd, and native session ID/path. Finishing a turn makes the member idle only after Pi reports `agent_settled`. It does not stop the process.
57
+
58
+ Children can delegate through the same coordinator, subject to depth and capacity. Delegation still needs user authorization. Keep briefs bounded, agree on file ownership, and use separate worktrees for concurrent writers.
59
+
60
+ ### Share work without losing ownership
61
+
62
+ ```json
63
+ {"action":"create","title":"Review parser errors","description":"Check malformed input and report concrete failures; no edits."}
64
+ ```
65
+
66
+ Each task has three independent parts:
67
+
68
+ - **Progress:** `status` is `todo`, `in_progress`, or `done`
69
+ - **Ownership:** `owner` is one member ID, or absent
70
+ - **Dependencies:** `dependencies` lists prerequisite task IDs. Returned `blockedBy` lists those not yet done; it is calculated, never stored as a status.
71
+
72
+ Use the returned task ID with these actions:
73
+
74
+ | Action | Effect |
75
+ | --- | --- |
76
+ | `claim` | Reserve an unowned to-do task, even if blocked. Progress stays `todo`. |
77
+ | `start` | The owner moves `todo` to `in_progress`, only when every dependency is `done`. |
78
+ | `complete` | The owner moves `in_progress` to `done`, with `acceptance: true`. Ownership remains. |
79
+ | `release` | Clear ownership. In-progress work returns to `todo`; done work stays `done`. |
80
+ | `reopen` | Explicitly move `done` to `todo`, retaining any owner. Refused if a dependent is in progress or done. |
81
+
82
+ Only the owner can start, complete, or release its work. An ancestor may explicitly release a stopped or interrupted descendant's ownership. The owner or root can reopen an owned task; any member can reopen an unowned one. Done tasks cannot be claimed until reopened.
83
+
84
+ Include `dependencies` on creation or `edit`. Any member can edit unowned to-do work; only the owner can edit reserved to-do work. In-progress and done tasks cannot be edited. Unknown dependencies and cycles are rejected. An optional `revision` rejects stale mutations.
85
+
86
+ `acceptance: true` asserts that the agent checked the acceptance criteria; it is not an independent test runner. Stop/crash retains ownership and progress. There is no automatic reassignment.
87
+
88
+ ### Send messages instead of polling
89
+
90
+ ```json
91
+ {"action":"send","recipients":["root"],"text":"Parser review complete. No edits; two malformed-input cases need tests."}
92
+ ```
93
+
94
+ Omit `recipients` to notify all other live members. Explicit sets must contain unique live member IDs; an invalid set rejects the whole send. All team members can read the full history, including direct messages.
95
+
96
+ ```json
97
+ {"action":"history","after":0,"limit":20}
98
+ ```
99
+
100
+ Use the last returned `seq` as the next `after` cursor. `search` adds a case-sensitive literal `query`; results remain cursor-paged. Reads never consume messages. Task and message result pages are limited to 100 items.
101
+
102
+ New messages queue a compact Pi follow-up: an idle member wakes, while a busy member finishes its current work first. Notifications batch for 50 ms and reference history rather than copying every message into context. Notification acknowledgement means queued, not read or acted on. Messages survive crashes; exactly-once delivery or model execution does not. Avoid acknowledgement-only replies.
103
+
104
+ ## Limits are ceilings, not staffing targets
105
+
106
+ Default: at most **10 live descendants**, counting starting, busy, idle, and stopping members. Root does not consume a slot. Default depth is **2**: root, child, grandchild. Use the fewest useful agents.
107
+
108
+ Root policy precedence:
109
+
110
+ 1. Built-in defaults
111
+ 2. `~/.acstack/ac-teams/config.json`
112
+ 3. `.pi/ac-teams.json`, only with `--teams-project-config`
113
+ 4. Explicit `--teams-limit` and `--teams-depth` flags
114
+
115
+ Config files accept only `limit` and `depth`, for example `{"limit":4,"depth":2}`. Limits must be 1–100; depth must be 0–10. A value above 10 requires an explicit root startup flag or interactive confirmation of a root `limits` tool request. Configuration alone cannot raise it. Lowering limits does not kill existing members; it blocks further launches until capacity fits.
116
+
117
+ ```sh
118
+ pi -e /absolute/path/to/ac-teams/src/index.ts --teams-limit 4 --teams-depth 1
119
+ ```
120
+
121
+ ## Stop, resume, and keep work safe
122
+
123
+ `stop` stops the member's descendants deepest-first, clears queued work, aborts the turn, and closes the owned RPC pipe. Hung processes escalate to SIGTERM and then SIGKILL, with a warning. Escalation requires a matching captured process-group identity; uncertain ownership is refused. This is snapshot-based cleanup, not OS containment: detached or uncaptured descendants and identity-check races remain outside the guarantee.
124
+
125
+ `resume` needs a stopped descendant ID and a new brief. It starts a new process against the same native Pi session file; it does not reconstruct a transcript, reconnect to old stdio, or replay the previous assignment automatically. Resume is refused if the old process is live or its identity is uncertain.
126
+
127
+ Root session switching, forking, and tree traversal are blocked while members are live. A member cannot switch away from its team binding. Stop members explicitly before replacing the root session.
128
+
129
+ **Exit confirmation requires Pi's `session_before_exit` hook.** The prerequisite is implemented in the separate local Pi checkout described in [the implementation history](docs/implementation-plan.md#exit-confirmation-supported-hook-versus-package-interception), not in installed Pi 1.4.0. With that hook, native exit asks before stopping live members; cancellation leaves them running. Without it, shutdown performs cleanup but cannot ask first. Signals bypass confirmation.
130
+
131
+ If the root crashes, closing its owned pipes and losing the coordinator connection makes ordinary RPC children shut down. This passed SIGTERM and SIGKILL probes with native child and grandchild processes. It is best effort, not a guarantee against a hung event loop, machine failure, or detached shell processes. Recovery refuses duplicate work when a previous member is still live or uncertain.
132
+
133
+ ### Optional worktrees
134
+
135
+ Set `worktree: true` on launch. `base` defaults to the repository's HEAD; `target` records the branch/ref against which later cleanup must prove integration. Worktrees live under a sibling `.ac-teams-worktrees` directory. No fetch, merge, stash, or checkout of the original directory occurs.
136
+
137
+ Stopping never deletes a worktree. `cleanup` is explicit and requires a stopped member, a clean tree including ignored/untracked files, and HEAD ancestry proven against the recorded target's current commit. Missing targets, dirty work, and uncertain/squashed integration retain the tree. Cleanup never forces removal and retains the branch. Cleanups and resumes exclude each other.
138
+
139
+ Worktree creation copies committed content, not uncommitted project configuration. Approve the new cwd through native Pi trust before retrying a launch that could not load its project resources. A failed launch retains its created worktree and member record.
140
+
141
+ ## Local records and security boundaries
142
+
143
+ Each root session uses `~/.acstack/ac-teams/teams/<session-id>/`:
144
+
145
+ - `state.json` contains membership, policy, tasks, messages, claims, and notification cursors
146
+ - `sessions/` contains native Pi-managed member transcripts
147
+ - `owner.lock` and `coordinator.sock` coordinate the running root
148
+
149
+ State schema 2 separates ownership from progress. On reopening an older team, the coordinator atomically migrates schema 1: `pending` becomes `todo`, `claimed` becomes `in_progress`, and `completed` becomes `done`. IDs, owners, dependencies, and revisions are preserved. Invalid snapshots are rejected without replacement. Older ac-teams code cannot reopen the migrated schema; stop old sessions before updating.
150
+
151
+ Only the root writes state. Mutations validate a copy, flush a temporary snapshot, atomically rename it, and flush the directory before returning success. An exclusive process-identity lock prevents concurrent owners. Private team directories use 0700; state files and the authenticated Unix socket use 0600. Uncertain locks fail closed.
152
+
153
+ This is a small-team store: each mutation rewrites the snapshot, and search scans local history. There is no database, compaction, retention policy, automatic deletion, or deduplication of repeated mutation requests. Inspect state before retrying an ambiguous failed request. Records and transcripts can contain sensitive project data. Permissions protect against other users, not agents running as the same OS user.
154
+
155
+ `npm audit` currently reports Pi 1.4.0's pinned `undici` dependency (including a high-severity advisory). ac-teams adds no HTTP client and cannot repair an existing host installation. The suggested npm fix downgrades Pi incompatibly; it has not been applied. Review the host dependency before deployment.
156
+
157
+ ## Development and verification
158
+
159
+ ```sh
160
+ npm ci
161
+ npm run check
162
+ npm test
163
+ npm pack --dry-run
164
+ ```
165
+
166
+ The deterministic suite covers task reservations, progress, dependency readiness, release/reopen rules, durable migration, real subprocesses, sockets, notification batching, shutdown escalation, worktree safety, and lifecycle races. It makes no model calls.
167
+
168
+ Additional local checks on 2026-10-24:
169
+
170
+ - Native Pi busy follow-up, idle wake with the same PID, and resume with the same session ID/path and a new PID
171
+ - Normal project-extension loading and a 10-call model smoke using the existing default model; Pi reported an estimated $0.0861
172
+ - Actual ac-teams exit confirmation, cancel/retry, and child cleanup in a real terminal using patched Pi and a model-free RPC child fixture
173
+ - Native child/grandchild cleanup after coordinator SIGTERM and SIGKILL, without model calls
174
+ - Independent review and executable checks of stop/start, cleanup/resume, and shutdown/socket races
175
+
176
+ Not verified on macOS, Windows, or network filesystems. The package is Linux-only because safe process identity uses `/proc`. No dashboard, workflow DSL, daemon, or automatic merging is included.
177
+
178
+ ## Publish to npm
179
+
180
+ The `publish-npm.yml` GitHub workflow follows acstack's staged-release approach: it runs typecheck, tests, and package checks, then submits the package for approval. It does not publish the release directly.
181
+
182
+ Set the repository secret once using an interactive prompt, not a token pasted into a command or chat:
183
+
184
+ ```sh
185
+ gh secret set NPM_TOKEN --repo ac8318740/ac-teams
186
+ ```
187
+
188
+ Reuse acstack's token only if its permissions allow creating and staging `ac-teams`. GitHub cannot reveal the existing secret value. Otherwise, create a granular npm token with stage-publish access. npm requires 2FA on the publishing account.
189
+
190
+ After the workflow reaches the repository's default branch, run it from GitHub Actions or dispatch it:
191
+
192
+ ```sh
193
+ gh workflow run publish-npm.yml --repo ac8318740/ac-teams
194
+ ```
195
+
196
+ Pushing a `v<version>` tag also triggers it; the tag must match `package.json`. An already published version is a no-op. If the version is already staged, approve the existing submission rather than staging it again.
197
+
198
+ Review and approve `ac-teams@0.0.1` in npm's **Staged Packages** tab using 2FA. The first staging of a new name creates a public `0.0.0-stage` placeholder; the requested version and its contents become public only after approval. See [npm's staged publishing documentation](https://docs.npmjs.com/staged-publishing/).
199
+
200
+ After approval, install the published version through Pi:
201
+
202
+ ```sh
203
+ pi install npm:ac-teams@0.0.1
204
+ ```
205
+
206
+ ## License
207
+
208
+ [MIT](LICENSE). No third-party extension implementation was copied. Pi and TypeBox remain peer dependencies; exit-guard prior art and license findings are recorded in the implementation history.
@@ -0,0 +1,197 @@
1
+ # ac-teams implementation history
2
+
3
+ **Status:** the local v0.0.1 implementation is built and verified on Linux as of 2026-10-24. [README.md](../README.md) describes the current contract, commands, verification, and limitations. No release, global installation, or change to installed Pi has been made.
4
+
5
+ The user approved implementation with these refinements:
6
+
7
+ - Warn about competing delegation tools instead of refusing coexistence. Keep normal child resources. Limits cover only ac-teams launches, not other launchers or same-user shell access.
8
+ - Require explicit root opt-in before reading project policy. Configuration alone cannot authorize a limit above 10.
9
+ - Stop, rather than detach, live members when exit is confirmed. Confirmation uses the separately implemented native Pi hook, not input interception.
10
+ - Separate task progress (`todo`, `in_progress`, `done`), ownership, and dependency readiness. Checkout reserves even blocked work; `start` requires completed dependencies. Release clears ownership without reopening done work; reopening is explicit and protects active/done dependents. This supersedes the initial claim-as-progress design. Schema 1 snapshots migrate atomically to schema 2, preserving IDs, owners, dependencies, and revisions.
11
+
12
+ The smallest complete implementation uses one atomically replaced `state.json` for messages and tasks, rather than a separate message journal. Mutations are not request-deduplicated; ambiguous retries require inspection. Notification acknowledgement records queuing, not reading, and does not guarantee at-least-once model execution across crashes. Worktree cleanup is explicit, never automatic.
13
+
14
+ Deterministic subprocess/socket/storage/Git tests, native Pi busy/idle/resume checks, an actual terminal confirmation test, model-free native SIGTERM/SIGKILL probes, package checks, and independent review passed. The paid smoke used 10 calls and Pi reported an estimated $0.0861. A host dependency advisory remains documented in the README.
15
+
16
+ ## Original proposal
17
+
18
+ The sections below preserve the design investigation and its proposed acceptance checks. Where they differ from the implementation, the current README and the refinements above take precedence. Their original future-tense recommendations are not additional pending work.
19
+
20
+ ## Architecture: the root owns transport, not everyone's work
21
+
22
+ ```text
23
+ root Pi extension
24
+ |
25
+ +-- coordinator: durable state, local socket, event delivery
26
+ | |
27
+ | +-- owns RPC pipes --> child Pi session
28
+ | +-- owns RPC pipes --> grandchild Pi session
29
+ |
30
+ +-- native root Pi session/transcript
31
+
32
+ child extension -- authenticated local socket --> coordinator
33
+ requests launch, tasks, messages, or stop
34
+ ```
35
+
36
+ A child can delegate, but the coordinator launches every process. The recorded parent tree controls depth and stop permissions. Owning all RPC pipes at the root avoids a persistent service and makes root process death close every child's input pipe. Finishing an assignment changes an agent to idle; it does not close that pipe or exit the session.
37
+
38
+ Use Node platform APIs, Pi's peer APIs, and its schema dependency. No database, process supervisor service, or orchestration dependency is needed for this size. Use a small JSONL RPC transport rather than Pi's exported `RpcClient`: v1 needs explicit pipe ownership, EOF shutdown, process identity, and bounded shutdown escalation. The installed client's `stop()` sends SIGTERM and then SIGKILL after one second; its public interface does not provide the required graceful EOF control.
39
+
40
+ ### Proposed source boundaries
41
+
42
+ | File | Responsibility |
43
+ | --- | --- |
44
+ | `src/index.ts` | Register tools, guidance, flags, and Pi lifecycle handlers |
45
+ | `src/coordinator.ts` | Team membership, authorization, single-writer mutations, socket requests, notifications |
46
+ | `src/rpc-agent.ts` | Spawn Pi, correlate RPC responses, track settlement, stop processes |
47
+ | `src/store.ts` | Private local storage, durable writes, message search and cursors |
48
+ | `src/worktrees.ts` | Create optional worktrees and verify safe removal |
49
+
50
+ Keep task rules in the coordinator initially; do not create a generic scheduler. Add the Pi package manifest, TypeScript configuration, and tests only after approval.
51
+
52
+ ## The three tools compose without prescribing a workflow
53
+
54
+ Proposed names and actions:
55
+
56
+ | Tool | Actions | Rules |
57
+ | --- | --- | --- |
58
+ | `team_agent` | Launch, list/status, stop, explicit resume, root limit update | Brief, cwd, optional worktree, model, and thinking level; return member and native session identifiers |
59
+ | `team_message` | Send, history, search | Notify all teammates or a nonempty explicit recipient set; all members can read all messages |
60
+ | `team_task` | Create, list/read, edit to-do work, claim, start, release, complete, reopen | Separate progress, ownership, and dependencies; blocked checkout is allowed, starting is gated |
61
+
62
+ Children receive their launch brief and normal project instructions, not the parent's conversation. Startup guidance explains how to pull shared messages and tasks. It asks agents to use the fewest useful teammates, consider thinking level, state edit ownership, and notify their parent before an in-scope split. Scope expansion or changed shared commitments requires approval. There are no fixed manager roles or file locks.
63
+
64
+ Stop authorization follows ancestry: a parent can stop its descendants, not a peer or ancestor. The user can override. Task ownership is separate: agents complete or release their own claims; a parent or the user can explicitly release an interrupted descendant's claim. Stop never silently releases or reruns work.
65
+
66
+ ### Launch controls and precedence
67
+
68
+ - Defaults: depth 2, at most 10 live descendants including idle members. Root is depth 0 and does not consume a descendant slot.
69
+ - Root settings resolve in order: built-in defaults, user ac-teams configuration, project ac-teams configuration, explicit root launch flags. Descendants receive the resolved team policy from the coordinator, not authority to rewrite it.
70
+ - A ceiling above 10 requires explicit user authorization: a root startup flag, or a root tool request confirmed through the host UI. Configuration alone cannot silently raise it. In noninteractive mode, require the explicit startup override. Descendants cannot raise either ceiling.
71
+ - Lowering a live ceiling does not stop existing agents. Reject new launches until the proposed membership fits. Lowering depth prevents further over-depth launches; it does not kill existing members.
72
+ - Reserve a slot atomically before spawn; count starting and stopping processes until confirmed exited. Roll back failed startup reservations. Validate the requesting member and ancestry server-side, not from a caller-supplied depth.
73
+ - Read the spawning member's current model and thinking level for each launch. Pass provider/model and thinking explicitly to Pi; an explicit launch override wins. Validate support and fail clearly rather than silently routing to another model.
74
+
75
+ Configuration is convenience, not a security boundary. Members share OS-user access; shell access can bypass coordination policies.
76
+
77
+ ### Normal Pi resources, with explicit readiness checks
78
+
79
+ Launch `pi --mode rpc` in the requested cwd, using the installed executable, a dedicated native session directory, and an explicit absolute `-e` path to ac-teams. Preserve normal package, extension, skill, model, authentication, and project-instruction loading. Do not use `--no-extensions` as the production shortcut.
80
+
81
+ Before sending an assignment, require a child readiness handshake proving that ac-teams registered, joined the expected Team, and sees the coordinator's policy. The handshake also reports competing delegation tools/resources. Reject startup on conflicts, missing resources, trust restrictions, or timeout; never continue with an ungoverned child. Normalize package paths to avoid loading ac-teams twice.
82
+
83
+ Known conflicts must be checked before spawning where discoverable from settings/resources, and again at child readiness before model work. A post-load readiness check cannot undo another extension's startup side effects. Supporting arbitrary unknown launchers or claiming full coexistence is outside v1; do not modify existing installations during this project.
84
+
85
+ ## Durable state and atomic ownership
86
+
87
+ Store coordination data at `~/.acstack/ac-teams/teams/<team-id>/` regardless of cwd. Use private directories (0700) and private files/socket (0600). The product uses this directory name without depending on ACStack. Never send tasks or messages to GitHub, ACStack maps, or another tracker.
88
+
89
+ - `state.json`: member identities and ancestry, policy, task state, claims, session paths, and notification progress.
90
+ - `messages.jsonl`: append-only shared message history with monotonic sequence numbers and stable message IDs.
91
+ - `sessions/`: native Pi-managed session files; store references instead of copying transcripts.
92
+ - Coordinator socket and exclusive owner lock: runtime coordination only, with per-member credentials and validated request schemas.
93
+
94
+ Only the root coordinator writes. Serialize mutations in one request queue. Validate and commit each reservation in the same critical section: the task is to-do and unowned. Checkout does not start work and can reserve a blocked task. Starting requires ownership and completed dependencies. Atomic snapshot replacement uses a same-directory temporary file, flush, rename, and directory flush. Append and flush a message before notifying recipients. A successful mutation response follows durable persistence; use request IDs to make retries idempotent.
95
+
96
+ Validate dependency IDs and reject cycles. Freeze dependencies while a task is in progress or done; a reserved to-do task remains editable by its owner. Return unfinished prerequisites as derived `blockedBy`, not a stored progress value. Release clears ownership, returns in-progress work to to-do, and leaves done work done. Explicit reopening cannot invalidate in-progress or done dependents. A process crash marks the member interrupted but retains task progress and ownership visibly.
97
+
98
+ An exclusive team lock prevents simultaneous roots writing the same state. Check process identity, not PID alone, before deciding a lock is stale. If identity or liveness is uncertain, refuse takeover and report recovery steps rather than killing processes or breaking the lock automatically. Test these filesystem assumptions on supported local Linux filesystems; do not promise network-filesystem locking guarantees.
99
+
100
+ Retain all durable records until explicit user-authorized deletion. Refuse deletion while members are live. Do not log tokens or credentials. Document that coordination messages, launch briefs, and native transcripts can contain sensitive project data.
101
+
102
+ ## Delivery wakes agents without model polling
103
+
104
+ Append each message once. Broadcast resolves to the other current members; an explicit recipient set selects notification targets, not access permissions. History/search remains team-wide, including messages between other members. Paginate by sequence/cursor and return bounded text matches without loading the entire history into the model.
105
+
106
+ The coordinator pushes sequence references to connected member extensions. Use Pi's custom message delivery to wake an idle agent and queue a follow-up while busy. Batch pending references per member to avoid a model turn for each message. Reuse the same notification ID after reconnect and keep recipient progress for deduplication. Treat delivery as at-least-once; do not promise exactly-once model execution across crashes.
107
+
108
+ An assignment is complete only after `agent_settled`, not the RPC prompt acknowledgement or an intermediate `agent_end`. Idle members remain available. Default notifications do not interrupt running tools; reserve steering for an explicit urgent instruction. Transport failure leaves pending notifications visible. No automatic model retry or task reassignment follows a crash.
109
+
110
+ ## Stop, exit, and resume guarantees
111
+
112
+ | Situation | Proposed behavior | Guarantee boundary |
113
+ | --- | --- | --- |
114
+ | Parent requests stop | Stop descendants in that subtree, clear queued work, abort active work, close stdin, wait, then SIGTERM/SIGKILL on a timeout | Test graceful and forced paths; report forced exits |
115
+ | Assignment settles | Record idle; keep process and RPC connection open | Supported by Pi RPC lifecycle |
116
+ | Root native exit | Confirm while descendants are live, then stop them and retain records | Requires the local Pi pre-exit patch; installed Pi 1.4.0 supports cleanup only |
117
+ | Intermediate member exits unexpectedly | Coordinator observes exit and stops its descendants; preserve interrupted claims | Requires root still functioning |
118
+ | Root crashes or receives SIGKILL | Its owned pipes close; RPC children receive EOF and begin shutdown | Best effort; child/event-loop hangs or inherited pipe handles can defeat timely cleanup |
119
+ | Machine failure | Processes stop; reconcile records on next open | No cleanup hook or guaranteed final write |
120
+ | Native resume | Use `pi --session <path\|id>`; rejoin records after identity checks | Resume is a new process, not reconnection to old stdio |
121
+
122
+ Track only ac-teams-owned processes. Never enumerate and stop unrelated Pi sessions. Descendant process-group cleanup must be tested; arbitrary shell-spawned detached processes are not covered by a universal kill guarantee.
123
+
124
+ Expose session ID and path for every member. On explicit resume, verify that the old member is not still live, reserve capacity, and start the native session with the same membership binding. Keep its interrupted claims visible for explicit continuation or release. Never reconstruct a transcript or automatically replay its last assignment. A manually resumed member without its coordinator can inspect its transcript, but cannot launch team work until team recovery is explicit.
125
+
126
+ Because this coordinator lives in the root, automatic attachment to an orphan's existing RPC pipes is not supported. If post-crash processes remain live or uncertain, block replacement instead of running duplicate work. Switching/forking the interactive root session must not silently abandon the old Team: reject cancellable switches with live descendants unless they are explicitly stopped; a cloned/forked session must not inherit authority to the original Team.
127
+
128
+ ## Exit confirmation: supported hook versus package interception
129
+
130
+ The earlier recommendation to omit confirmation in v1 was too restrictive. Confirmation can be built; the distinction is whether it uses a supported lifecycle hook or intercepts selected exit routes.
131
+
132
+ Verified installed behavior: `session_shutdown` has no cancellation result, and normal interactive shutdown stops the TUI before disposing extensions. `/quit` is handled directly by the interactive mode before extension command dispatch. `app.clear` and `app.exit` shortcuts are reserved, but `ctx.ui.onTerminalInput()` can consume raw input before built-in handling. Raw input interception is not a global shutdown gate: it must preserve editor, abort, paste, modal, and configured keybinding behavior, and cannot intercept another extension calling `ctx.shutdown()`.
133
+
134
+ The Pi prerequisite is implemented locally in `/home/acoote/projects/pi`, branch `feat/cancellable-exit-hook`. It is not committed, published, or installed. The implementation uses no private-method replacement or copied third-party code.
135
+
136
+ Verification passed: 73 focused tests, `npm run check`, and real-terminal checks for `/quit`, Ctrl+D, double Ctrl+C, extension-requested exit, cancellation/retry, and SIGTERM during confirmation. No model prompts were sent.
137
+
138
+ Hook behavior and the proposed ac-teams integration contract:
139
+
140
+ - Pi emits `session_before_exit` before interactive terminal teardown. Returning `{ cancel: true }` keeps Pi open; a handler error also cancels exit and is reported.
141
+ - `/quit`, exit keybindings, and extension-requested graceful exit use the hook. Concurrent exit requests share one pending check; cancellation leaves Pi usable. Reload and session replacement do not emit it.
142
+ - In ac-teams, prompt only for a root with live descendants, including idle members: cancel exit, or stop descendants and exit while retaining records. Any detach/keep-running choice needs a separate lifecycle decision; it is not implied by confirmation.
143
+ - Do not prompt on SIGTERM, SIGHUP, terminal loss, noninteractive exit, or crashes. Preserve best-effort cleanup; SIGKILL and machine failure cannot run a hook.
144
+ - Test exit routes, cancellation, repeated requests, busy agents, no-live-member exits, cleanup, and unavailable UI. Until Pi supports the hook, a package-only exit command or input guard must explicitly document its bypasses.
145
+
146
+ Prior art checked on 2026-10-24:
147
+
148
+ | Project | Actual mechanism | Reuse assessment |
149
+ | --- | --- | --- |
150
+ | [sidstack/pi-codex-agents](https://github.com/sidstack/pi-codex-agents/blob/ae789448a8b1724075343d3341e99d04c628a1a6/src/core/host.ts#L145-L198) | Exit menu for its managed sessions; replaces the owned interactive mode's private `shutdown` and signal methods | Closest working design reference, but not a drop-in supported hook. Do not import its daemon/UI architecture or private-method patch for this tools-only package. MIT |
151
+ | [atayarani/pi-close-guard](https://github.com/atayarani/pi-close-guard/blob/54f9be0872b9881e57b0d1ddd34ba7608769f6c8/extensions/close-guard.ts) | Uses cancellable `session_before_switch` for `/new` and optionally `/resume`; explicitly excludes `/quit` | Useful confirm/cancel pattern, not an exit solution. MIT |
152
+ | [j1nn0/pi-extensions, pi-exit](https://github.com/j1nn0/pi-extensions/tree/78b556daf6dcd02757aec77077f278c39944c71d/packages/pi-exit) | Dedicated exit command/tool and keyboard shortcut | Example of requesting exit, not vetoing built-in shutdown. MIT |
153
+ | [getsimpledirect/vinci-code-cli](https://github.com/getsimpledirect/vinci-code-cli/blob/33b4ae7b6788c07a9c7e5109b083ae1e313aff4e/packages/coding-agent/src/core/extensions/runner.ts) | Defines `session_before_exit` in a fork for one-shot modes, without cancellation | A matching name does not supply the interactive cancellable behavior. MIT |
154
+
155
+ No third-party implementation was copied or installed. If code is adapted, retain the relevant copyright and MIT permission notice and identify the project, source commit, and adapted files in third-party notices. Acknowledgment alone does not replace the license notice. Upstream API evidence was checked at [earendil-works/pi commit d0aa218](https://github.com/earendil-works/pi/blob/d0aa218cb16c333c008f6aa3dcd670ac8b68383c/packages/coding-agent/src/core/extensions/types.ts). These are static source findings, not runtime verification of third-party exit behavior.
156
+
157
+ ## Worktrees preserve work by default
158
+
159
+ A launch can use its supplied cwd or request a worktree. For a worktree, resolve the repository and base commit before spawning. Default to that repository's current HEAD; accept an explicit base ref. Do not fetch, checkout, reset, merge, or stash another working tree. Use an ac-teams-specific branch name containing team/member identifiers and pass Git arguments without shell interpolation.
160
+
161
+ Creation records repository, resolved base, branch, path, and an optional user-authorized integration target. Branch selection must reject collisions and invalid refs.
162
+
163
+ Automatic worktree removal is allowed only after the member has stopped, the tree is clean including untracked files, and its HEAD is an ancestor of the recorded integration target's current commit. Recheck immediately before removal. Missing target, uncertain status, or squash/rebase integration without ancestry proof means retain the tree and explain why. Never use force removal. V1 does not delete branches automatically and never treats stopping an agent as permission to merge.
164
+
165
+ ## Implementation order and acceptance checks
166
+
167
+ Each step adds focused tests before it is considered complete. Before writing tests, follow the test-conventions skill and choose ordinary unit/integration tests for this standalone package, not ACStack application conventions.
168
+
169
+ 1. **Package and child lifecycle.** Register the extension, root coordinator, readiness protocol, and RPC transport. Check normal cwd resources, explicit package loading, no parent-conversation inheritance, model/thinking inheritance and overrides, idle reuse across two assignments, and native session IDs/paths. Fail closed on both installed competing launchers. Bound output buffers and shutdown waits; always drain stderr.
170
+ 2. **Durable tasks and limits.** Race simultaneous claims and launches; exactly one claimant wins, dependencies and cycles are checked, team/depth caps include idle agents, and startup failures release only their reservations. Test owner permissions, lower-limit behavior, request retries, disk failure, truncated message-tail recovery, duplicate roots, and crash-preserved claims.
171
+ 3. **Messages and notifications.** Test broadcast and explicit multiple recipients, team-wide search, pagination, idle wake-up, busy follow-up, settlement, notification batching/deduplication, disconnection, and no periodic model polling. Define behavior for invalid/departed recipients explicitly and reject invalid recipient sets before append.
172
+ 4. **Failure and resume.** Exercise orderly stop, queued-work cancellation, intermediate failure, root SIGTERM/SIGKILL, EOF, a child ignoring termination, and explicit native resume. Check no unrelated process is touched, no duplicate member starts, and no interrupted claim silently changes owner. Document observed best-effort limits.
173
+ 5. **Worktrees and usage guidance.** Test clean and dirty repositories, branch collisions, integrated/unmerged heads, untracked files, absent targets, and safe cleanup. Add concise examples of child delegation and parent notification. An optional short Herdr skill may reference `herdr --skill`; it must not become a runtime dependency or a second messaging implementation.
174
+
175
+ Use fake RPC peers for deterministic protocol tests and real Pi processes for lifecycle/resource checks. A small paid-model smoke test is needed for two real assignments, busy/idle messaging, and tool-guidance behavior; do not confuse the no-model probe below with that verification. No frontend or dashboard verification is required for this tools-only scope.
176
+
177
+ ## API evidence and investigation limits
178
+
179
+ The evidence below describes the installed package; the separate local Pi patch is documented above. Package root: `/home/acoote/.npm-global/lib/node_modules/@earendil-works/pi-coding-agent/`.
180
+
181
+ | Evidence | Supports |
182
+ | --- | --- |
183
+ | `docs/extensions.md`, `dist/core/extensions/types.d.ts:1229-1300` | Tool registration, extension flags, custom/user messages, model/thinking access |
184
+ | `docs/sdk.md`, `dist/core/resource-loader.d.ts:70-124` | SDK session/resource APIs; normal cwd loading and explicit additional resources |
185
+ | `dist/core/resource-loader.js:409-422` | Resource merging and extension loading; an after-load override is not proof of safe conflict prevention |
186
+ | `docs/rpc.md`, `docs/rpc-commands.md` | Persistent JSONL RPC; prompt/steer/follow-up, clear queue, abort, state, and settlement |
187
+ | `docs/rpc-extension-ui.md` | RPC UI requests require host responses; headless approval needs explicit policy |
188
+ | `dist/modes/rpc/rpc-mode.js:579-650` | Shutdown on stdin EOF and native runtime disposal |
189
+ | `dist/modes/rpc/rpc-client.js:89` | Exported client's limited stop sequence |
190
+ | `dist/modes/interactive/interactive-mode.js:3444-3488`, `dist/core/extensions/types.d.ts:637` | Shutdown ordering, non-cancellable shutdown event, emergency-exit limits |
191
+ | `docs/cli.md`, installed `pi --help`, `docs/sessions.md` | Native `--session <path\|id>` resume and session persistence |
192
+ | `docs/packages.md`, `docs/configuration.md`, `docs/settings.md`, `docs/security.md` | Package resources, settings/trust, and extension security limits |
193
+ | Installed `@tintinweb/pi-subagents` 0.22.0 and `pi-subagents` 0.81.0 | Independent delegation and lifecycle behavior; both MIT, neither required or copied |
194
+
195
+ A temporary, isolated real-Pi probe disabled all optional extensions/skills/templates/themes and sent no model prompt. It verified successful `get_state`, `get_commands`, `clear_queue`, and `abort` responses; continued availability between commands; native session ID/path reporting; and exit code 0 after stdin EOF. The first probe timed out because its Python text-buffered selector reader missed buffered lines; switching the probe to byte reads resolved that harness error.
196
+
197
+ At the end of the initial investigation, extension coexistence/readiness, paid model turns, busy message delivery, concurrent claims, parent-death cleanup, native resume/rejoin, and worktree safety were still unverified. Those checks were subsequently exercised during implementation; see the current README for evidence and limits. Publishing, marketplace integration, and ACStack changes remain outside this local delivery.
package/package.json CHANGED
@@ -1,6 +1,26 @@
1
1
  {
2
2
  "name": "ac-teams",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.1",
4
+ "description": "Small, persistent teams for Pi",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": { "type": "git", "url": "git+https://github.com/ac8318740/ac-teams.git" },
8
+ "files": ["src", "README.md", "LICENSE", "docs/implementation-plan.md"],
9
+ "os": ["linux"],
10
+ "pi": { "extensions": ["./src/index.ts"] },
11
+ "scripts": {
12
+ "check": "tsc --noEmit",
13
+ "test": "node --experimental-strip-types --test test/*.test.ts"
14
+ },
15
+ "engines": { "node": ">=22.19.0" },
16
+ "peerDependencies": {
17
+ "@earendil-works/pi-coding-agent": "^1.4.0",
18
+ "typebox": "^1.0.0"
19
+ },
20
+ "devDependencies": {
21
+ "@earendil-works/pi-coding-agent": "1.4.0",
22
+ "@types/node": "^22.0.0",
23
+ "typebox": "^1.0.0",
24
+ "typescript": "^5.9.0"
25
+ }
26
+ }
package/src/config.ts ADDED
@@ -0,0 +1,22 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { isRecord } from "./connection.ts";
5
+
6
+ export function policy(cwd: string, trusted: boolean, flags: { limit?: string; depth?: string }) {
7
+ let config: Record<string, unknown> = { limit: 10, depth: 2 };
8
+ const files = [join(homedir(), ".acstack", "ac-teams", "config.json")];
9
+ if (trusted) files.push(join(cwd, ".pi", "ac-teams.json"));
10
+ for (const file of files) {
11
+ let value: unknown;
12
+ try { value = JSON.parse(readFileSync(file, "utf8")); }
13
+ catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") continue; throw new Error(`Cannot read team policy ${file}: ${String(error)}`); }
14
+ if (!isRecord(value) || Object.keys(value).some(key => !["limit", "depth"].includes(key))) throw new Error(`Invalid team policy: ${file}`);
15
+ config = { ...config, ...value };
16
+ }
17
+ const limit = flags.limit === undefined ? config.limit : Number(flags.limit);
18
+ const depth = flags.depth === undefined ? config.depth : Number(flags.depth);
19
+ if (!Number.isSafeInteger(limit) || Number(limit) < 1 || Number(limit) > 100 || !Number.isSafeInteger(depth) || Number(depth) < 0 || Number(depth) > 10) throw new Error("Team limit must be 1..100 and depth 0..10");
20
+ if (Number(limit) > 10 && flags.limit === undefined) throw new Error("A ceiling above 10 requires the explicit --teams-limit startup flag");
21
+ return { limit: Number(limit), depth: Number(depth) };
22
+ }
@@ -0,0 +1,82 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { connect, type Socket } from "node:net";
3
+ import { randomUUID, timingSafeEqual } from "node:crypto";
4
+ import type { Operation } from "./types.ts";
5
+
6
+ export interface Request { id: string; member: string; token: string; operation: Operation; args: unknown }
7
+ export function authentic(token: string, expected: string): boolean {
8
+ const a = Buffer.from(token), b = Buffer.from(expected);
9
+ return a.length === b.length && timingSafeEqual(a, b);
10
+ }
11
+ // A 100-item page can approach 10 MiB after JSON escaping; requests remain limited to 128 KiB.
12
+ const responseLimit = 16 * 1024 * 1024;
13
+ export function send(socket: Socket, value: Record<string, unknown>): void {
14
+ const line = JSON.stringify(value) + "\n";
15
+ if (socket.destroyed || socket.writableLength + Buffer.byteLength(line) > responseLimit) { socket.destroy(); return; }
16
+ socket.write(line);
17
+ }
18
+ export function readLines(socket: Socket, receive: (value: unknown) => void, limit = 128 * 1024): void {
19
+ let buffer = "";
20
+ socket.setEncoding("utf8");
21
+ socket.on("data", (chunk: string) => {
22
+ buffer += chunk;
23
+ let end: number;
24
+ while ((end = buffer.indexOf("\n")) >= 0) {
25
+ const line = buffer.slice(0, end);
26
+ buffer = buffer.slice(end + 1);
27
+ if (Buffer.byteLength(line) > limit) { socket.destroy(); return; }
28
+ try { receive(JSON.parse(line)); }
29
+ catch { socket.destroy(); return; }
30
+ }
31
+ if (Buffer.byteLength(buffer) > limit) socket.destroy();
32
+ });
33
+ }
34
+ export function isRecord(value: unknown): value is Record<string, unknown> {
35
+ return typeof value === "object" && value !== null && !Array.isArray(value);
36
+ }
37
+ export class Connection extends EventEmitter {
38
+ private socket?: Socket;
39
+ private pending = new Map<string, { resolve: (value: unknown) => void; reject: (error: Error) => void; timer: NodeJS.Timeout }>();
40
+ private closed = false;
41
+ private path: string;
42
+ private member: string;
43
+ private token: string;
44
+ constructor(path: string, member: string, token: string) { super(); this.path = path; this.member = member; this.token = token; }
45
+ async open(): Promise<void> {
46
+ const socket = connect(this.path);
47
+ this.socket = socket;
48
+ readLines(socket, value => {
49
+ if (!isRecord(value)) throw new Error("Invalid coordinator response");
50
+ if (value.type === "notice" && Number.isSafeInteger(value.through)) { this.emit("notice", value.through); return; }
51
+ if (typeof value.id !== "string") return;
52
+ const pending = this.pending.get(value.id);
53
+ if (!pending) return;
54
+ clearTimeout(pending.timer);
55
+ this.pending.delete(value.id);
56
+ if (typeof value.error === "string") pending.reject(new Error(value.error));
57
+ else pending.resolve(value.data);
58
+ }, responseLimit);
59
+ socket.on("error", () => {});
60
+ socket.once("close", () => {
61
+ for (const pending of this.pending.values()) { clearTimeout(pending.timer); pending.reject(new Error("Coordinator disconnected")); }
62
+ this.pending.clear();
63
+ if (!this.closed) this.emit("disconnect");
64
+ });
65
+ await new Promise<void>((resolve, reject) => {
66
+ const timer = setTimeout(() => { socket.destroy(); reject(new Error("Coordinator connection timed out")); }, 10000);
67
+ socket.once("connect", () => { clearTimeout(timer); resolve(); });
68
+ socket.once("error", error => { clearTimeout(timer); reject(error); });
69
+ });
70
+ }
71
+ request(operation: Operation, args: unknown): Promise<unknown> {
72
+ const socket = this.socket;
73
+ if (!socket || socket.destroyed) return Promise.reject(new Error("Coordinator is unavailable"));
74
+ const id = randomUUID();
75
+ return new Promise((resolve, reject) => {
76
+ const timer = setTimeout(() => { this.pending.delete(id); reject(new Error("Coordinator request timed out; inspect state before retrying mutations")); }, 120000);
77
+ this.pending.set(id, { resolve, reject, timer });
78
+ send(socket, { id, member: this.member, token: this.token, operation, args });
79
+ });
80
+ }
81
+ close(): void { this.closed = true; this.socket?.destroy(); }
82
+ }