ac-teams 0.0.7 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,7 +28,7 @@ Your Pi session is the root; its child agents are members. Ask Pi to delegate a
28
28
 
29
29
  The task summary sits above the input. The agent selector sits below the input, above your existing footer, and shows descriptive names, models, and context usage. ac-teams publishes a compact team status without replacing the footer or editor supplied by other extensions. `/teams config` toggles these displays independently.
30
30
 
31
- From an empty editor, press Down to select the team row, then Enter to open a member. A red **Needs input** label marks a member with an open question. Ctrl+Shift+Left returns to Main without stopping the member or dismissing its question. `/teams stop` stops every descendant.
31
+ From an empty editor, press Down to select the team row, then Enter to open a member. A red **Needs input** label marks a member with an open question. Ctrl+Shift+Left returns to Main without stopping the member or dismissing its question. `/teams stop` stops every member; main sessions keep running. `/teams join` and `/teams leave` add this session to another team or take it out.
32
32
 
33
33
  Defaults allow up to **10 live members** and **two levels of delegation**. Use the fewest useful agents.
34
34
 
package/docs/access.md CHANGED
@@ -84,6 +84,10 @@ docker build -t ac-teams-member:1.5.0-<hash> --build-arg PI_VERSION=1.5.0 -f <ac
84
84
 
85
85
  The image is `node:22-bookworm-slim`, pinned by digest, with bash, curl, Git, ripgrep, CA certificates, and the same Pi version as Main. Rebuild after upgrading Pi.
86
86
 
87
+ ## Promotion drops enforcement
88
+
89
+ Promoting an enforced member to a main session relaunches it outside the container with your user's access. The panel asks for confirmation first. Members always run with `ACSTACK_AGENT_ROLE=member`; main sessions run without it and without inherited `AC_TEAMS_*` variables.
90
+
87
91
  ## Storage
88
92
 
89
93
  - Coordinator state and settings: `~/.pi/ac-teams/teams/<team>/`, `~/.pi/ac-teams/config.json`, and `~/.pi/ac-teams/display.json`
@@ -0,0 +1,343 @@
1
+ # Session roles - promotion, joining, and several main sessions
2
+
3
+ A team can have several main sessions. The user can promote a member into a main session or join an existing session to a team. Identity, transcript discovery, membership, coordinator ownership, and permission authority stay separate, and ACSTACK\_AGENT\_ROLE tells hooks which processes are main.
4
+
5
+ <!-- Generated from the entry YAML and its element files; regenerate with design-doc --markdown -->
6
+
7
+ ```mermaid
8
+ flowchart TD
9
+ element_user["User<br/>actor"]
10
+ element_main-session["Main session<br/>component · cli"]
11
+ element_member["Member<br/>component"]
12
+ element_coordinator["Coordinator<br/>component"]
13
+ element_team-store["Team store<br/>store"]
14
+ element_transcripts["Pi transcripts<br/>store"]
15
+ element_herdr["Herdr<br/>external-system"]
16
+ element_read-guard["ACStack read guard<br/>external-system"]
17
+ element_user -->|"Promotes, joins, and leaves through /teams in"| element_main-session
18
+ element_user -->|"Switches between main sessions in"| element_herdr
19
+ element_main-session -->|"Hosts as owner, or connects to as a client"| element_coordinator
20
+ element_main-session -->|"Writes its conversation and team entry to"| element_transcripts
21
+ element_read-guard -->|"Finds no member role in"| element_main-session
22
+ element_coordinator -->|"Relaunches promoted members as"| element_main-session
23
+ element_coordinator -->|"Launches, stops, and resumes"| element_member
24
+ element_member -->|"Calls team tools through"| element_coordinator
25
+ element_member -->|"Writes its conversation to"| element_transcripts
26
+ element_read-guard -->|"Reads ACSTACK_AGENT_ROLE=member in"| element_member
27
+ element_coordinator -->|"Holds owner.lock and records roles in"| element_team-store
28
+ element_coordinator -->|"Moves promoted transcripts within"| element_transcripts
29
+ element_coordinator -->|"Opens main-session tabs through"| element_herdr
30
+ ```
31
+
32
+ ## Session roles: promotion, joining, and several main sessions
33
+
34
+ Status: draft for review. Implements nothing yet. Resolves the design questions in [issue #19](https://github.com/ac8318740/ac-teams/issues/19).
35
+
36
+ A team can have several **main sessions**. A main session is a Pi session the user created, joined to a team, or promoted. Every other team process is a **member**. The user can promote a member to a main session from the TUI, or join an existing session to a team with `/teams join`. ac-teams marks every member process with `ACSTACK_AGENT_ROLE=member`, so ACStack's large-read guard applies only to main sessions.
37
+
38
+ ### Review questions
39
+
40
+ 1. Does promotion by relaunch feel seamless enough, given that a promoted session moves to its own Herdr tab?
41
+ 2. Does it look right that every main session can control every member and other main sessions? That matches what an unsandboxed session can already do through its shell.
42
+ 3. Is the takeover after the owner exits (automatic, with a notice) the right default?
43
+ 4. Are the changes and tests at the end complete enough to start implementation?
44
+
45
+ ### Trade-offs to judge
46
+
47
+ - **Relaunch, not live handover.** Promotion stops the member process and reopens its transcript in a new process. The member loses its in-flight turn, but its role, sandbox, token, and environment change cleanly.
48
+ - **No coordinator daemon.** The coordinator stays inside whichever main session holds `owner.lock`. When the owner exits, members stop and the next owner resumes them automatically. The cost is a pause and at most one redone turn per busy member.
49
+ - **Wide main-session authority.** Any main session can stop work started by another. ac-teams does not try to guard against this, because an unsandboxed session can already stop processes through the shell or Herdr.
50
+
51
+ ## Definitions
52
+
53
+ These five properties are independent. A change to one never changes another.
54
+
55
+ | Property | Defined by | Changes when |
56
+ |---|---|---|
57
+ | Identity | Pi session id in the transcript header | Never |
58
+ | Discovery | Where the transcript file lives | Promotion moves it to the top-level session folder |
59
+ | Membership | A member record in the team store | Join adds one; leave clears its role |
60
+ | Coordinator ownership | Holder of `owner.lock` | The owner exits and another main session takes it |
61
+ | Authority | The record's `role`: `main` or `member` | Only promotion, join, or leave, all user actions |
62
+
63
+ ### What the coordinator does
64
+
65
+ The coordinator is ac-teams' in-process server. Today it runs inside Main. In this design, it runs inside whichever main session holds `owner.lock`. In practice it:
66
+
67
+ - **Runs member processes.** Starts each member's Pi, either headless or in tmux, optionally inside a Docker sandbox. It also stops and resumes members, and stops them all when its host session exits
68
+ - **Answers team tools.** Every `team_agent`, `team_task`, and `team_message` call from any session arrives over `coordinator.sock` and is applied here, so concurrent edits are serialized
69
+ - **Keeps team state.** Writes members, tasks, and messages to the team store
70
+ - **Delivers notifications.** Tells each session when new team messages arrive
71
+ - **Enforces rules.** Checks authority, depth and member limits, and sandbox grants, and brokers provider credentials for sandboxed members
72
+
73
+ This design keeps all of that. It adds role checks, connections from main sessions it did not launch, promotion, joining, leaving, and ownership takeover.
74
+
75
+ The team id stays the session id of the session that created the team, even after that session leaves or hands over ownership.
76
+
77
+ ### Main-agent signal
78
+
79
+ - The coordinator sets `ACSTACK_AGENT_ROLE=member` on every member process, next to `AC_TEAMS_MEMBER`. That includes the terminal-launch script, which today only passes `AC_TEAMS_*` and `HERDR_ENV`
80
+ - A hook treats the session as main when the variable is absent or `main`, and as not main for any other value
81
+ - Anything a member starts inherits the variable and counts as not main
82
+ - Promotion and every main-session launch must remove `ACSTACK_AGENT_ROLE`, every `AC_TEAMS_*` variable, and `HERDR_ENV=0`. Tests must cover this
83
+ - ACStack reads the variable on every tool call. It detects Claude Code and Codex subagents from `agent_id` in their hook payloads instead. That detection belongs to ACStack, not ac-teams
84
+ - Role grants no permission by itself
85
+
86
+ ## User flows
87
+
88
+ ### Promote a member
89
+
90
+ 1. The user opens a teammate from the agent selector or `/teams` and chooses **Promote to main**
91
+ 2. If the member is sandboxed, a confirmation states that the session will run with the user's full access. Declining cancels
92
+ 3. The coordinator stops the member, waiting for its process to exit as `stop` does today
93
+ 4. The coordinator moves the transcript from `ac-teams/<team>/<member>/` to the project's top-level session folder. It removes the member's config at `~/.pi/ac-teams/agents/<id>` and sets the member's role to `main` without a parent. The member's children stay its descendants
94
+ 5. The coordinator writes the session's team entry: a Pi custom entry holding the team id and member id, with no secrets
95
+ 6. The coordinator relaunches `pi --session <moved file>` with a clean environment:
96
+ - With Herdr, in a new tab in the workspace for the session's worktree, creating the workspace if needed. The tab gets focus and the session's name
97
+ - Without Herdr, in a tmux session on a per-main-session socket that the owner's `kill-server` cannot reach. The user's screen stays attached, and ac-teams prints the reattach command
98
+ 7. The new process reads its team entry, connects to the coordinator with a main-session token from the team store, and appears in `/resume`
99
+
100
+ ### Join an existing session to a team
101
+
102
+ 1. The user runs `/teams join` in an ordinary Pi session
103
+ 2. ac-teams lists this project's teams. The user picks one
104
+ 3. ac-teams refuses if the session already belongs to a team with members, or if its session id is already in the chosen team
105
+ 4. The coordinator adds a member record with role `main` and writes the team entry
106
+ 5. The session receives a short summary of open tasks, owners, and recent messages, with its message cursor at the latest message. It claims or creates work with the normal team tools
107
+
108
+ ### Several main sessions
109
+
110
+ - Each main session holds one connection to the coordinator. The owner hosts it in-process; the others connect over `coordinator.sock`
111
+ - Every main session sits at depth 0. Any main session can launch, stop, resume, and clean up any member, and stop or resume another main session
112
+ - Stopping a main session asks it to shut down through its connection. Resuming one relaunches its transcript, as in step 6 of promotion
113
+ - Members keep today's rule: they control only their descendants and never a main session
114
+ - Tasks and messages stay shared. Ownership and revision checks still serialize concurrent edits, because one coordinator applies them all
115
+
116
+ ### Leave a team
117
+
118
+ `/teams leave` returns the session's in-progress tasks to todo and clears their ownership. The member record keeps its history but loses its role, so its descendants stay under the other main sessions. The team entry records the departure, so a later `/resume` does not rejoin. Leaving is refused while the session is the only main session and members are still live.
119
+
120
+ ## Recovery
121
+
122
+ - **The owner exits while another main session runs.** Before stopping members, the owner records each one's state (busy, idle, or needs input) in the team store. Today, `Coordinator.open` discards that state. A connected main session then takes `owner.lock`, opens the coordinator, and runs the resume pass below. Every main session gets a notice naming the new owner
123
+ - **No main session is running when the owner exits.** Members stay stopped with their recorded state. The next main session that opens the team runs the same resume pass
124
+ - **A promoted session is resumed with no coordinator running.** The extension reads its team entry at startup and offers to take ownership. Accepting opens the coordinator and runs the resume pass. Declining runs the session without team tools
125
+
126
+ ### Resume pass
127
+
128
+ | Recorded state | Action |
129
+ |---|---|
130
+ | Busy | Resume and send: "Your process was restarted during a coordinator handover. Check your last steps and continue your current task." |
131
+ | Idle | Resume and leave idle |
132
+ | Needs input | Resume and show **Needs input** again |
133
+
134
+ Pi saves each completed message, so a busy member loses only the reply it was generating and redoes that turn. A command killed mid-run may have half finished. The message tells the member to check before it continues, which reduces that risk but does not remove it, the same as stopping a member today.
135
+ - **A main session crashes.** Its record stays. The coordinator marks it interrupted, and any main session can resume it
136
+ - **Promotion fails partway.** Each step is recorded in the team store before the next starts. On failure, the coordinator rolls back to a stopped member with its original transcript path, and the member can be retried or resumed
137
+
138
+ ## Security boundaries
139
+
140
+ - Only the user can promote, join, or leave. These actions have no agent-tool form
141
+ - Promoting a sandboxed member is an explicit escalation, approved in the confirmation step. Without approval, nothing changes
142
+ - Main-session tokens live in the private team store, which sandboxed members cannot read. A member therefore cannot impersonate a main session over the socket
143
+ - Descendant non-escalation stays as it is: a member cannot grant its descendants more access than it has
144
+ - Provider credentials for sandboxed members come from the current owner's Pi login. After a takeover, the new owner brokers them under the same OS user
145
+
146
+ ## Changes needed before implementation
147
+
148
+ - **Team store schema 4.** Add `role` to members and migrate `root` to a main record. Replace `actor === "root"` checks with role checks. Store each main session's token
149
+ - **Coordinator.**
150
+ - Accept connections from main sessions that were not launched by the coordinator
151
+ - Add promote, join, leave, and the stop and resume of main sessions
152
+ - Add ownership takeover, and recompute descendant depths after a promotion
153
+ - **Launch environment.** Set `ACSTACK_AGENT_ROLE=member` for every member, including the terminal script's variable filter. Strip it on main-session launches
154
+ - **Extension.** Read the team entry at `session_start`. Add `/teams join`, `/teams leave`, and the **Promote to main** action. Open Herdr tabs, or use the tmux fallback
155
+ - **Docs.** Update `docs/usage.md`, `docs/operations.md`, and `docs/access.md`
156
+
157
+ ## Tests needed
158
+
159
+ - A launched member has `ACSTACK_AGENT_ROLE=member`, in both RPC and terminal modes
160
+ - A promoted session's environment has no `ACSTACK_AGENT_ROLE` and no `AC_TEAMS_*` variables
161
+ - Promotion keeps the session id, moves the file, updates the stored path, and writes the team entry. Pi's session listing then includes the session
162
+ - A sandboxed promotion is refused without confirmation and runs unsandboxed after it
163
+ - Joining refuses a duplicate session id, and refuses a session that leads another team with members
164
+ - A main session can stop and resume members and other main sessions. A member cannot act on a main session
165
+ - When the owner exits, another main session takes the lock. Busy members resume with the continue message, idle members stay idle, and members that needed input show **Needs input** again
166
+ - When no main session is running, the next one to open the team runs the same resume pass
167
+ - Resuming a promoted transcript with no coordinator offers takeover, and declining leaves team tools off
168
+ - Leaving returns in-progress tasks to todo and keeps descendants manageable
169
+ - A promotion interrupted after each step rolls back to a resumable member
170
+ - Schema 3 teams migrate to schema 4 with root as a main record
171
+
172
+
173
+ ## User
174
+
175
+ *Starts main sessions and is the only party who can promote a member, join a session to a team, or approve a sandboxed member's full access.*
176
+
177
+ - Identity: `user`
178
+ - Kind: actor
179
+ - Channels: none
180
+ - Source: [user.yaml](user.yaml)
181
+
182
+
183
+
184
+ ## Main session
185
+
186
+ *A Pi session the user started, joined to a team, or promoted. Every main session controls every member and other main sessions, shares the team's tasks and messages, and belongs to one team at a time.*
187
+
188
+ - Identity: `main-session`
189
+ - Kind: component
190
+ - Channels: cli
191
+ - Source: [main-session.yaml](main-session.yaml)
192
+
193
+ ## Main-agent signal
194
+
195
+ - Requirement: ACStack's large-read guard applies only to main agents. A main agent is a session the user created or promoted, never one an agent created. The signal must reach in-process Pi hooks and shell hooks, and must change when a member is promoted.
196
+ - Design response: The coordinator sets ACSTACK\_AGENT\_ROLE=member on every member process, next to AC\_TEAMS\_MEMBER. Absent or "main" means main; any other value means not main. Children of a member inherit it. Promotion always relaunches the transcript in a new process without ACSTACK\_AGENT\_ROLE or any AC\_TEAMS\_\* variable, so a live process never needs to change its role. Main status grants no permissions.
197
+ - Applies to: coordinator, member, main-session, read-guard
198
+
199
+ ## Separate identity, discovery, membership, and authority
200
+
201
+ - Requirement: Issue \#19 forbids tying session identity or permission authority to picker visibility, storage location, or a single main session.
202
+ - Design response: Identity is the Pi session id. Discovery is where the transcript file lives. Membership and role are records in the team store. Ownership is whoever holds owner.lock. Authority comes from the member's role in the team store, which only explicit user actions change. Moving a file or taking the lock changes nothing else.
203
+ - Applies to: main-session, member, coordinator, team-store, transcripts
204
+
205
+ ## Duplicate prevention
206
+
207
+ - Requirement: A session must not appear twice in a team, run as two processes, or belong to two teams.
208
+ - Design response: The team store keys members by member id and refuses a second record with the same Pi session id. The coordinator accepts one live connection per member and keeps the existing pid and identity check before any launch or relaunch. A session in a team with members cannot join another team.
209
+ - Applies to: coordinator, team-store, main-session
210
+
211
+ ## Recovery without the original owner
212
+
213
+ - Requirement: The team must carry on when the owner exits, and a promoted session resumed later must find its team again.
214
+ - Design response: When the owner exits, another connected main session takes owner.lock and resumes members by their recorded state: busy members get a message to check their last steps and continue, idle members stay idle, and open questions reappear. A main session resumed from /resume reads its team entry, then connects to the live coordinator or offers to take ownership.
215
+ - Applies to: main-session, coordinator, team-store, member
216
+
217
+ ## Member
218
+
219
+ *An agent-launched Pi process with ACSTACK\_AGENT\_ROLE=member and AC\_TEAMS\_\* variables. It controls only its descendants, and its transcript stays under ac-teams/&\#60;team&\#62;/&\#60;member&\#62;/ until promotion.*
220
+
221
+ - Identity: `member`
222
+ - Kind: component
223
+ - Channels: none
224
+ - Source: [member.yaml](member.yaml)
225
+
226
+ ## Main-agent signal
227
+
228
+ - Requirement: ACStack's large-read guard applies only to main agents. A main agent is a session the user created or promoted, never one an agent created. The signal must reach in-process Pi hooks and shell hooks, and must change when a member is promoted.
229
+ - Design response: The coordinator sets ACSTACK\_AGENT\_ROLE=member on every member process, next to AC\_TEAMS\_MEMBER. Absent or "main" means main; any other value means not main. Children of a member inherit it. Promotion always relaunches the transcript in a new process without ACSTACK\_AGENT\_ROLE or any AC\_TEAMS\_\* variable, so a live process never needs to change its role. Main status grants no permissions.
230
+ - Applies to: coordinator, member, main-session, read-guard
231
+
232
+ ## Separate identity, discovery, membership, and authority
233
+
234
+ - Requirement: Issue \#19 forbids tying session identity or permission authority to picker visibility, storage location, or a single main session.
235
+ - Design response: Identity is the Pi session id. Discovery is where the transcript file lives. Membership and role are records in the team store. Ownership is whoever holds owner.lock. Authority comes from the member's role in the team store, which only explicit user actions change. Moving a file or taking the lock changes nothing else.
236
+ - Applies to: main-session, member, coordinator, team-store, transcripts
237
+
238
+ ## Recovery without the original owner
239
+
240
+ - Requirement: The team must carry on when the owner exits, and a promoted session resumed later must find its team again.
241
+ - Design response: When the owner exits, another connected main session takes owner.lock and resumes members by their recorded state: busy members get a message to check their last steps and continue, idle members stay idle, and open questions reappear. A main session resumed from /resume reads its team entry, then connects to the live coordinator or offers to take ownership.
242
+ - Applies to: main-session, coordinator, team-store, member
243
+
244
+ ## Coordinator
245
+
246
+ *The team's single referee, running inside whichever main session holds owner.lock. It starts and stops member processes, answers every team\_agent, team\_task, and team\_message call, delivers message notifications, checks who may do what, and runs promotion, joining, and takeover.*
247
+
248
+ - Identity: `coordinator`
249
+ - Kind: component
250
+ - Channels: none
251
+ - Source: [coordinator.yaml](coordinator.yaml)
252
+
253
+ ## Main-agent signal
254
+
255
+ - Requirement: ACStack's large-read guard applies only to main agents. A main agent is a session the user created or promoted, never one an agent created. The signal must reach in-process Pi hooks and shell hooks, and must change when a member is promoted.
256
+ - Design response: The coordinator sets ACSTACK\_AGENT\_ROLE=member on every member process, next to AC\_TEAMS\_MEMBER. Absent or "main" means main; any other value means not main. Children of a member inherit it. Promotion always relaunches the transcript in a new process without ACSTACK\_AGENT\_ROLE or any AC\_TEAMS\_\* variable, so a live process never needs to change its role. Main status grants no permissions.
257
+ - Applies to: coordinator, member, main-session, read-guard
258
+
259
+ ## Separate identity, discovery, membership, and authority
260
+
261
+ - Requirement: Issue \#19 forbids tying session identity or permission authority to picker visibility, storage location, or a single main session.
262
+ - Design response: Identity is the Pi session id. Discovery is where the transcript file lives. Membership and role are records in the team store. Ownership is whoever holds owner.lock. Authority comes from the member's role in the team store, which only explicit user actions change. Moving a file or taking the lock changes nothing else.
263
+ - Applies to: main-session, member, coordinator, team-store, transcripts
264
+
265
+ ## Duplicate prevention
266
+
267
+ - Requirement: A session must not appear twice in a team, run as two processes, or belong to two teams.
268
+ - Design response: The team store keys members by member id and refuses a second record with the same Pi session id. The coordinator accepts one live connection per member and keeps the existing pid and identity check before any launch or relaunch. A session in a team with members cannot join another team.
269
+ - Applies to: coordinator, team-store, main-session
270
+
271
+ ## Recovery without the original owner
272
+
273
+ - Requirement: The team must carry on when the owner exits, and a promoted session resumed later must find its team again.
274
+ - Design response: When the owner exits, another connected main session takes owner.lock and resumes members by their recorded state: busy members get a message to check their last steps and continue, idle members stay idle, and open questions reappear. A main session resumed from /resume reads its team entry, then connects to the live coordinator or offers to take ownership.
275
+ - Applies to: main-session, coordinator, team-store, member
276
+
277
+ ## Team store
278
+
279
+ *Private team state under \~/.pi/ac-teams/teams/&\#60;team&\#62;/: members with their role, tasks, messages, owner.lock, and each main session's connection token. Sandboxed members cannot read it.*
280
+
281
+ - Identity: `team-store`
282
+ - Kind: store
283
+ - Channels: none
284
+ - Source: [team-store.yaml](team-store.yaml)
285
+
286
+ ## Separate identity, discovery, membership, and authority
287
+
288
+ - Requirement: Issue \#19 forbids tying session identity or permission authority to picker visibility, storage location, or a single main session.
289
+ - Design response: Identity is the Pi session id. Discovery is where the transcript file lives. Membership and role are records in the team store. Ownership is whoever holds owner.lock. Authority comes from the member's role in the team store, which only explicit user actions change. Moving a file or taking the lock changes nothing else.
290
+ - Applies to: main-session, member, coordinator, team-store, transcripts
291
+
292
+ ## Duplicate prevention
293
+
294
+ - Requirement: A session must not appear twice in a team, run as two processes, or belong to two teams.
295
+ - Design response: The team store keys members by member id and refuses a second record with the same Pi session id. The coordinator accepts one live connection per member and keeps the existing pid and identity check before any launch or relaunch. A session in a team with members cannot join another team.
296
+ - Applies to: coordinator, team-store, main-session
297
+
298
+ ## Recovery without the original owner
299
+
300
+ - Requirement: The team must carry on when the owner exits, and a promoted session resumed later must find its team again.
301
+ - Design response: When the owner exits, another connected main session takes owner.lock and resumes members by their recorded state: busy members get a message to check their last steps and continue, idle members stay idle, and open questions reappear. A main session resumed from /resume reads its team entry, then connects to the live coordinator or offers to take ownership.
302
+ - Applies to: main-session, coordinator, team-store, member
303
+
304
+ ## Pi transcripts
305
+
306
+ *Standard Pi session files. Main sessions sit at the top level of the project's session folder, where /resume finds them. Members sit under ac-teams/&\#60;team&\#62;/&\#60;member&\#62;/, where only /teams finds them.*
307
+
308
+ - Identity: `transcripts`
309
+ - Kind: store
310
+ - Channels: none
311
+ - Source: [transcripts.yaml](transcripts.yaml)
312
+
313
+ ## Separate identity, discovery, membership, and authority
314
+
315
+ - Requirement: Issue \#19 forbids tying session identity or permission authority to picker visibility, storage location, or a single main session.
316
+ - Design response: Identity is the Pi session id. Discovery is where the transcript file lives. Membership and role are records in the team store. Ownership is whoever holds owner.lock. Authority comes from the member's role in the team store, which only explicit user actions change. Moving a file or taking the lock changes nothing else.
317
+ - Applies to: main-session, member, coordinator, team-store, transcripts
318
+
319
+ ## Herdr
320
+
321
+ *Optional terminal workspace manager. When present, each main session gets its own tab in the workspace for its worktree.*
322
+
323
+ - Identity: `herdr`
324
+ - Kind: external-system
325
+ - Channels: none
326
+ - Source: [herdr.yaml](herdr.yaml)
327
+
328
+
329
+
330
+ ## ACStack read guard
331
+
332
+ *ACStack's large-read hook. It blocks a large untargeted read once in main agents only, and reads ACSTACK\_AGENT\_ROLE on every call to decide.*
333
+
334
+ - Identity: `read-guard`
335
+ - Kind: external-system
336
+ - Channels: none
337
+ - Source: [read-guard.yaml](read-guard.yaml)
338
+
339
+ ## Main-agent signal
340
+
341
+ - Requirement: ACStack's large-read guard applies only to main agents. A main agent is a session the user created or promoted, never one an agent created. The signal must reach in-process Pi hooks and shell hooks, and must change when a member is promoted.
342
+ - Design response: The coordinator sets ACSTACK\_AGENT\_ROLE=member on every member process, next to AC\_TEAMS\_MEMBER. Absent or "main" means main; any other value means not main. Children of a member inherit it. Promotion always relaunches the transcript in a new process without ACSTACK\_AGENT\_ROLE or any AC\_TEAMS\_\* variable, so a live process never needs to change its role. Main status grants no permissions.
343
+ - Applies to: coordinator, member, main-session, read-guard
@@ -0,0 +1,44 @@
1
+ id: coordinator
2
+ name: Coordinator
3
+ summary: >-
4
+ The team's single referee, running inside whichever main session holds
5
+ owner.lock. It starts and stops member processes, answers every team_agent,
6
+ team_task, and team_message call, delivers message notifications, checks
7
+ who may do what, and runs promotion, joining, and takeover.
8
+ kind: component
9
+ relationships:
10
+ - id: main-uses-coordinator
11
+ from: main-session
12
+ to: coordinator
13
+ direction: one-way
14
+ label: Hosts as owner, or connects to as a client
15
+ - id: member-uses-coordinator
16
+ from: member
17
+ to: coordinator
18
+ direction: one-way
19
+ label: Calls team tools through
20
+ - id: coordinator-runs-member
21
+ from: coordinator
22
+ to: member
23
+ direction: one-way
24
+ label: Launches, stops, and resumes
25
+ - id: coordinator-launches-main
26
+ from: coordinator
27
+ to: main-session
28
+ direction: one-way
29
+ label: Relaunches promoted members as
30
+ - id: coordinator-writes-store
31
+ from: coordinator
32
+ to: team-store
33
+ direction: one-way
34
+ label: Holds owner.lock and records roles in
35
+ - id: coordinator-moves-transcript
36
+ from: coordinator
37
+ to: transcripts
38
+ direction: one-way
39
+ label: Moves promoted transcripts within
40
+ - id: coordinator-opens-tabs
41
+ from: coordinator
42
+ to: herdr
43
+ direction: one-way
44
+ label: Opens main-session tabs through
@@ -0,0 +1,70 @@
1
+ id: session-roles
2
+ name: Session roles - promotion, joining, and several main sessions
3
+ summary: >-
4
+ A team can have several main sessions. The user can promote a member into a
5
+ main session or join an existing session to a team. Identity, transcript
6
+ discovery, membership, coordinator ownership, and permission authority stay
7
+ separate, and ACSTACK_AGENT_ROLE tells hooks which processes are main.
8
+ detail: journey.md
9
+ elements:
10
+ - user.yaml
11
+ - main-session.yaml
12
+ - member.yaml
13
+ - coordinator.yaml
14
+ - team-store.yaml
15
+ - transcripts.yaml
16
+ - herdr.yaml
17
+ - read-guard.yaml
18
+ concerns:
19
+ - id: main-signal
20
+ name: Main-agent signal
21
+ elements: [coordinator, member, main-session, read-guard]
22
+ requirement: >-
23
+ ACStack's large-read guard applies only to main agents. A main agent is
24
+ a session the user created or promoted, never one an agent created. The
25
+ signal must reach in-process Pi hooks and shell hooks, and must change
26
+ when a member is promoted.
27
+ response: >-
28
+ The coordinator sets ACSTACK_AGENT_ROLE=member on every member process,
29
+ next to AC_TEAMS_MEMBER. Absent or "main" means main; any other value
30
+ means not main. Children of a member inherit it. Promotion always
31
+ relaunches the transcript in a new process without ACSTACK_AGENT_ROLE
32
+ or any AC_TEAMS_* variable, so a live process never needs to change its
33
+ role. Main status grants no permissions.
34
+ - id: separate-axes
35
+ name: Separate identity, discovery, membership, and authority
36
+ elements: [main-session, member, coordinator, team-store, transcripts]
37
+ requirement: >-
38
+ Issue #19 forbids tying session identity or permission authority to
39
+ picker visibility, storage location, or a single main session.
40
+ response: >-
41
+ Identity is the Pi session id. Discovery is where the transcript file
42
+ lives. Membership and role are records in the team store. Ownership is
43
+ whoever holds owner.lock. Authority comes from the member's role in the
44
+ team store, which only explicit user actions change. Moving a file or
45
+ taking the lock changes nothing else.
46
+ - id: duplicate-prevention
47
+ name: Duplicate prevention
48
+ elements: [coordinator, team-store, main-session]
49
+ requirement: >-
50
+ A session must not appear twice in a team, run as two processes, or
51
+ belong to two teams.
52
+ response: >-
53
+ The team store keys members by member id and refuses a second record
54
+ with the same Pi session id. The coordinator accepts one live
55
+ connection per member and keeps the existing pid and identity check
56
+ before any launch or relaunch. A session in a team with members cannot
57
+ join another team.
58
+ - id: recovery
59
+ name: Recovery without the original owner
60
+ elements: [main-session, coordinator, team-store, member]
61
+ requirement: >-
62
+ The team must carry on when the owner exits, and a promoted session
63
+ resumed later must find its team again.
64
+ response: >-
65
+ When the owner exits, another connected main session takes owner.lock
66
+ and resumes members by their recorded state: busy members get a message
67
+ to check their last steps and continue, idle members stay idle, and open
68
+ questions reappear. A main session resumed
69
+ from /resume reads its team entry, then connects to the live
70
+ coordinator or offers to take ownership.
@@ -0,0 +1,17 @@
1
+ id: herdr
2
+ name: Herdr
3
+ summary: >-
4
+ Optional terminal workspace manager. When present, each main session gets
5
+ its own tab in the workspace for its worktree.
6
+ kind: external-system
7
+ relationships:
8
+ - id: user-uses-herdr
9
+ from: user
10
+ to: herdr
11
+ direction: one-way
12
+ label: Switches between main sessions in
13
+ - id: coordinator-opens-tabs
14
+ from: coordinator
15
+ to: herdr
16
+ direction: one-way
17
+ label: Opens main-session tabs through
@@ -0,0 +1,139 @@
1
+ ## Session roles: promotion, joining, and several main sessions
2
+
3
+ Status: draft for review. Implements nothing yet. Resolves the design questions in [issue #19](https://github.com/ac8318740/ac-teams/issues/19).
4
+
5
+ A team can have several **main sessions**. A main session is a Pi session the user created, joined to a team, or promoted. Every other team process is a **member**. The user can promote a member to a main session from the TUI, or join an existing session to a team with `/teams join`. ac-teams marks every member process with `ACSTACK_AGENT_ROLE=member`, so ACStack's large-read guard applies only to main sessions.
6
+
7
+ ### Review questions
8
+
9
+ 1. Does promotion by relaunch feel seamless enough, given that a promoted session moves to its own Herdr tab?
10
+ 2. Does it look right that every main session can control every member and other main sessions? That matches what an unsandboxed session can already do through its shell.
11
+ 3. Is the takeover after the owner exits (automatic, with a notice) the right default?
12
+ 4. Are the changes and tests at the end complete enough to start implementation?
13
+
14
+ ### Trade-offs to judge
15
+
16
+ - **Relaunch, not live handover.** Promotion stops the member process and reopens its transcript in a new process. The member loses its in-flight turn, but its role, sandbox, token, and environment change cleanly.
17
+ - **No coordinator daemon.** The coordinator stays inside whichever main session holds `owner.lock`. When the owner exits, members stop and the next owner resumes them automatically. The cost is a pause and at most one redone turn per busy member.
18
+ - **Wide main-session authority.** Any main session can stop work started by another. ac-teams does not try to guard against this, because an unsandboxed session can already stop processes through the shell or Herdr.
19
+
20
+ ## Definitions
21
+
22
+ These five properties are independent. A change to one never changes another.
23
+
24
+ | Property | Defined by | Changes when |
25
+ |---|---|---|
26
+ | Identity | Pi session id in the transcript header | Never |
27
+ | Discovery | Where the transcript file lives | Promotion moves it to the top-level session folder |
28
+ | Membership | A member record in the team store | Join adds one; leave clears its role |
29
+ | Coordinator ownership | Holder of `owner.lock` | The owner exits and another main session takes it |
30
+ | Authority | The record's `role`: `main` or `member` | Only promotion, join, or leave, all user actions |
31
+
32
+ ### What the coordinator does
33
+
34
+ The coordinator is ac-teams' in-process server. Today it runs inside Main. In this design, it runs inside whichever main session holds `owner.lock`. In practice it:
35
+
36
+ - **Runs member processes.** Starts each member's Pi, either headless or in tmux, optionally inside a Docker sandbox. It also stops and resumes members, and stops them all when its host session exits
37
+ - **Answers team tools.** Every `team_agent`, `team_task`, and `team_message` call from any session arrives over `coordinator.sock` and is applied here, so concurrent edits are serialized
38
+ - **Keeps team state.** Writes members, tasks, and messages to the team store
39
+ - **Delivers notifications.** Tells each session when new team messages arrive
40
+ - **Enforces rules.** Checks authority, depth and member limits, and sandbox grants, and brokers provider credentials for sandboxed members
41
+
42
+ This design keeps all of that. It adds role checks, connections from main sessions it did not launch, promotion, joining, leaving, and ownership takeover.
43
+
44
+ The team id stays the session id of the session that created the team, even after that session leaves or hands over ownership.
45
+
46
+ ### Main-agent signal
47
+
48
+ - The coordinator sets `ACSTACK_AGENT_ROLE=member` on every member process, next to `AC_TEAMS_MEMBER`. That includes the terminal-launch script, which today only passes `AC_TEAMS_*` and `HERDR_ENV`
49
+ - A hook treats the session as main when the variable is absent or `main`, and as not main for any other value
50
+ - Anything a member starts inherits the variable and counts as not main
51
+ - Promotion and every main-session launch must remove `ACSTACK_AGENT_ROLE`, every `AC_TEAMS_*` variable, and `HERDR_ENV=0`. Tests must cover this
52
+ - ACStack reads the variable on every tool call. It detects Claude Code and Codex subagents from `agent_id` in their hook payloads instead. That detection belongs to ACStack, not ac-teams
53
+ - Role grants no permission by itself
54
+
55
+ ## User flows
56
+
57
+ ### Promote a member
58
+
59
+ 1. The user opens a teammate from the agent selector or `/teams` and chooses **Promote to main**
60
+ 2. If the member is sandboxed, a confirmation states that the session will run with the user's full access. Declining cancels
61
+ 3. The coordinator stops the member, waiting for its process to exit as `stop` does today
62
+ 4. The coordinator moves the transcript from `ac-teams/<team>/<member>/` to the project's top-level session folder. It removes the member's config at `~/.pi/ac-teams/agents/<id>` and sets the member's role to `main` without a parent. The member's children stay its descendants
63
+ 5. The coordinator writes the session's team entry: a Pi custom entry holding the team id and member id, with no secrets
64
+ 6. The coordinator relaunches `pi --session <moved file>` with a clean environment:
65
+ - With Herdr, in a new tab in the workspace for the session's worktree, creating the workspace if needed. The tab gets focus and the session's name
66
+ - Without Herdr, in a tmux session on a per-main-session socket that the owner's `kill-server` cannot reach. The user's screen stays attached, and ac-teams prints the reattach command
67
+ 7. The new process reads its team entry, connects to the coordinator with a main-session token from the team store, and appears in `/resume`
68
+
69
+ ### Join an existing session to a team
70
+
71
+ 1. The user runs `/teams join` in an ordinary Pi session
72
+ 2. ac-teams lists this project's teams. The user picks one
73
+ 3. ac-teams refuses if the session already belongs to a team with members, or if its session id is already in the chosen team
74
+ 4. The coordinator adds a member record with role `main` and writes the team entry
75
+ 5. The session receives a short summary of open tasks, owners, and recent messages, with its message cursor at the latest message. It claims or creates work with the normal team tools
76
+
77
+ ### Several main sessions
78
+
79
+ - Each main session holds one connection to the coordinator. The owner hosts it in-process; the others connect over `coordinator.sock`
80
+ - Every main session sits at depth 0. Any main session can launch, stop, resume, and clean up any member, and stop or resume another main session
81
+ - Stopping a main session asks it to shut down through its connection. Resuming one relaunches its transcript, as in step 6 of promotion
82
+ - Members keep today's rule: they control only their descendants and never a main session
83
+ - Tasks and messages stay shared. Ownership and revision checks still serialize concurrent edits, because one coordinator applies them all
84
+
85
+ ### Leave a team
86
+
87
+ `/teams leave` returns the session's in-progress tasks to todo and clears their ownership. The member record keeps its history but loses its role, so its descendants stay under the other main sessions. The team entry records the departure, so a later `/resume` does not rejoin. Leaving is refused while the session is the only main session and members are still live.
88
+
89
+ ## Recovery
90
+
91
+ - **The owner exits while another main session runs.** Before stopping members, the owner records each one's state (busy, idle, or needs input) in the team store. Today, `Coordinator.open` discards that state. A connected main session then takes `owner.lock`, opens the coordinator, and runs the resume pass below. Every main session gets a notice naming the new owner
92
+ - **No main session is running when the owner exits.** Members stay stopped with their recorded state. The next main session that opens the team runs the same resume pass
93
+ - **A promoted session is resumed with no coordinator running.** The extension reads its team entry at startup and offers to take ownership. Accepting opens the coordinator and runs the resume pass. Declining runs the session without team tools
94
+
95
+ ### Resume pass
96
+
97
+ | Recorded state | Action |
98
+ |---|---|
99
+ | Busy | Resume and send: "Your process was restarted during a coordinator handover. Check your last steps and continue your current task." |
100
+ | Idle | Resume and leave idle |
101
+ | Needs input | Resume and show **Needs input** again |
102
+
103
+ Pi saves each completed message, so a busy member loses only the reply it was generating and redoes that turn. A command killed mid-run may have half finished. The message tells the member to check before it continues, which reduces that risk but does not remove it, the same as stopping a member today.
104
+ - **A main session crashes.** Its record stays. The coordinator marks it interrupted, and any main session can resume it
105
+ - **Promotion fails partway.** Each step is recorded in the team store before the next starts. On failure, the coordinator rolls back to a stopped member with its original transcript path, and the member can be retried or resumed
106
+
107
+ ## Security boundaries
108
+
109
+ - Only the user can promote, join, or leave. These actions have no agent-tool form
110
+ - Promoting a sandboxed member is an explicit escalation, approved in the confirmation step. Without approval, nothing changes
111
+ - Main-session tokens live in the private team store, which sandboxed members cannot read. A member therefore cannot impersonate a main session over the socket
112
+ - Descendant non-escalation stays as it is: a member cannot grant its descendants more access than it has
113
+ - Provider credentials for sandboxed members come from the current owner's Pi login. After a takeover, the new owner brokers them under the same OS user
114
+
115
+ ## Changes needed before implementation
116
+
117
+ - **Team store schema 4.** Add `role` to members and migrate `root` to a main record. Replace `actor === "root"` checks with role checks. Store each main session's token
118
+ - **Coordinator.**
119
+ - Accept connections from main sessions that were not launched by the coordinator
120
+ - Add promote, join, leave, and the stop and resume of main sessions
121
+ - Add ownership takeover, and recompute descendant depths after a promotion
122
+ - **Launch environment.** Set `ACSTACK_AGENT_ROLE=member` for every member, including the terminal script's variable filter. Strip it on main-session launches
123
+ - **Extension.** Read the team entry at `session_start`. Add `/teams join`, `/teams leave`, and the **Promote to main** action. Open Herdr tabs, or use the tmux fallback
124
+ - **Docs.** Update `docs/usage.md`, `docs/operations.md`, and `docs/access.md`
125
+
126
+ ## Tests needed
127
+
128
+ - A launched member has `ACSTACK_AGENT_ROLE=member`, in both RPC and terminal modes
129
+ - A promoted session's environment has no `ACSTACK_AGENT_ROLE` and no `AC_TEAMS_*` variables
130
+ - Promotion keeps the session id, moves the file, updates the stored path, and writes the team entry. Pi's session listing then includes the session
131
+ - A sandboxed promotion is refused without confirmation and runs unsandboxed after it
132
+ - Joining refuses a duplicate session id, and refuses a session that leads another team with members
133
+ - A main session can stop and resume members and other main sessions. A member cannot act on a main session
134
+ - When the owner exits, another main session takes the lock. Busy members resume with the continue message, idle members stay idle, and members that needed input show **Needs input** again
135
+ - When no main session is running, the next one to open the team runs the same resume pass
136
+ - Resuming a promoted transcript with no coordinator offers takeover, and declining leaves team tools off
137
+ - Leaving returns in-progress tasks to todo and keeps descendants manageable
138
+ - A promotion interrupted after each step rolls back to a resumable member
139
+ - Schema 3 teams migrate to schema 4 with root as a main record