Expand description
Durable, system-wide record of which teams are up and where they live.
teamctl’s other notions of identity are all keyed on name, never on
path: the session UUID, the tmux session name, the mailbox row key,
the applied.json key. So nothing could durably answer “what teams are
running across this machine, and in which directories?” — the question
behind system-wide teamctl ps, orphan reaping, and the same-name guard.
This registry is that durable record. It lives at
~/.config/teamctl/teams.json (the same config dir the context store
uses) and is keyed on the compound (project_id, root) — one entry per
project per absolute team root, so two installs of the same template in
different folders are distinct rows rather than a collision.
up upserts this team’s entries (see crates/teamctl/src/cmd/up.rs),
down clears them (down.rs). Writes are atomic (temp + rename) so a
reader never observes a torn file. Atomicity is per-write, not a
cross-process lock: two ups of different teams racing the
load-modify-save could still drop one update — acceptable for a
self-healing side store (the next up re-records it), and called out
here rather than over-claimed.
The store records the path alongside the name; it deliberately does NOT make the session UUID or tmux name itself path-aware (a larger identity change, out of scope).
Structs§
- Registry
- The whole store.
#[serde(default)]per field keeps old files forward-compatible as the shape grows, matchingContextStore. - Roster
Entry - One recorded
(project, agent)running under a root, with its tmux session name reconstructed from the prefix the team was brought up with. The unitdown/reloadorphan-reaping operates on. - Team
Entry - One running team: a single
(project_id, root)pair plus the metadataps/ reaping / the same-name guard need. Field names serialize verbatim (snake_case), matching every other persisted teamctl store.
Functions§
- clear
- Clear entries for
root.project = Nonedrops every entry at that root (a whole-teamdown);project = Some(id)drops only that project’s entry (a project-scopeddownon a multi-project root, so sibling projects stay registered). A no-op write is skipped. - config_
dir ~/.config/teamctl— derived from$HOME($USERPROFILEfallback for Windows).Nonewhen neither is set, so callers warn-and-skip rather than guess a path. Mirrors the CLIcontextstore’s resolver andsession::claude_home; deliberately NOTdirs::config_dir(), which on macOS resolves to~/Library/Application Supportand would break the literal~/.config/teamctlpath the rest of the CLI uses.- is_
orphan truewhen this entry’s recorded root no longer holds ateam-compose.yaml— a stale row whose team config was moved or deleted.upalways records the.teamdirectory asroot, so the<root>/team-compose.yamlcheck is the live one for registry entries; the<root>/.team/team-compose.yamlfallback is kept only for parity with the sharedsessions::root_has_composerule (which also accepts a root that points at the bare project dir).path_existsis injected so the classifier is pure and unit-testable; production passes|p| p.exists().- load
- Load the registry from
dir/teams.json. A missing file is an empty registry; a corrupt one degrades to empty rather than erroring (the nextupoverwrites it) — the same toleranceContextStore::loadandsnapshot::readapply. Only a genuine read error (e.g. permissions) is propagated.diris the config dir (seeconfig_dir); it is taken as a parameter so tests can point at a tempdir without touching$HOME. - now_
rfc3339 - RFC3339 (seconds,
Z) — the serialized-timestamp idiom shared withsnapshot::applied_at/attachments. Co-located here because this module owns thestarted_atfield’s format contract. - orphans_
for_ root - Load the registry at
dirand return the orphan roster forroot— recorded agents no longer indesired. The down/reload reap entry point: composesload+Registry::roster_for_root+reap_targetsso the whole on-disk → orphans chain is one tested call.diris taken as a parameter so tests point at a tempdir without touching$HOME. A missing store loads as empty (no orphans); only a genuine read error propagates. - reap_
targets - The reap set: recorded roster agents that are no longer desired.
desiredis the current compose’s agent ids (<project>:<agent>).scopedlimits the reap to one project (a--projectdown/reload);per_agent(a--agentselector active) disables reaping entirely — a partial teardown leaves the rest of the team registered and running, so nothing there is an orphan. Pure; mirrorsdown::registry_clear_scope’s scope contract so reaping is never broader than the invocation. - same_
name_ other_ root - If another team is registered with the same
project_idbut a DIFFERENT root that still holds a compose (not an orphan), return that root — the same-name-different-folder collisionupmust refuse. It’s the cluster’s root cause: identity is name-keyed, not path-keyed, so two such teams alias each other’s sessions, tmux names, and mailbox keys. The team’s own root is never a conflict (a re-up of the same(project_id, root)is fine).path_existsis injected so the liveness check is pure and unit-testable; production passes|p| p.exists(). - upsert
- Upsert one team entry. Convenience over
upsert_many. - upsert_
many - Upsert a batch in a single load-modify-save (one atomic write for the
whole
up). Each entry replaces any existing row with the same(project_id, root)key, preserving that row’s originalstarted_at; new keys are appended. Entries stay sorted by(root, project_id)for stable on-disk output.