command hq: data model and the shared commanders directory

first commit of command-hq/command-hq.md's design. two tables plus a
many-to-many for tags, no server surface yet — this is data layer only.

command_hq_sessions has no name column: /chat never persists a title to
postgres, renameClaudeSession appends a summary entry straight into the
transcript's own jsonl, and that stays the ONLY place a commander's name can
live. no status column either — liveness is derived from chat_session_events
the same way the background-job tray already derives it, because a stored
status would go stale the moment the harness sidecar restarts without a
final task:notification and nothing would ever correct it.

category is a plain FK (one per commander, confirmed in spec); tags are the
many-to-many case, schema only, no ui planned yet.

also: getCommandersDir(email) -> DATA_PATH/<email>/commanders, one shared
directory for every commander a user has. the first design was per-commander
directories and was dropped same-night: creating a fresh directory timed
against the harness's own spawn is a race, and a shared pre-existing
directory removes it instead of handling it. deliberately DATA_PATH-rooted
even for the owner, unlike ordinary /chat which runs in the owner's real
home — that's what keeps commanders out of both the owner's own project
transcripts and the /chat list.

bunx tsgo clean. no db:push yet — next commit is the router, and the table
needs to exist before anything can call it; will note the push requirement
clearly when that lands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 06:03:13 +00:00
co-authored by Claude Opus 5
parent 0f93262a8c
commit 56fe3b22df
8 changed files with 597 additions and 0 deletions
+15
View File
@@ -197,6 +197,21 @@ export const getEmailDbPath = (ownerEmail: string, accountEmail: string) =>
export const getEmailAttachmentCacheDir = (ownerEmail: string) =>
join(getEmailAccountsDir(ownerEmail), 'attachment_cache');
/**
* Every commander a user has shares this ONE directory as its cwd — deliberately not one directory per
* commander. The first design was per-commander, and was dropped the same night it was proposed: a fresh
* directory would have to exist before or exactly when a commander's first turn fires, and racing that
* creation against the harness's own spawn is precisely the kind of timing hazard Claude Code's
* directory-per-project convention was never built to be driven by. One shared, pre-existing directory
* removes the race instead of handling it.
*
* `DATA_PATH`-rooted even for the OWNER, which is a deliberate departure from ordinary `/chat` — an owner's
* normal chat runs in `getOwnerHomeDir` (their real login home). Commanders use the managed directory
* instead, on both sides of the account split, which is what keeps them out of the owner's own project
* transcripts as surely as `command_hq_sessions` keeps them out of the `/chat` list. See `docs/command-hq.md`.
*/
export const getCommandersDir = (email: string) => join(DATA_PATH, email, 'commanders');
/**
* Which mail account's store to use, found on DISK rather than by asking the database.
*