@cmgmyr/hive 0.1.0

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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +70 -0
  3. package/claude-plugin/.claude-plugin/plugin.json +7 -0
  4. package/claude-plugin/README.md +47 -0
  5. package/claude-plugin/hooks/hooks.json +14 -0
  6. package/claude-plugin/kickoff.mjs +41 -0
  7. package/claude-plugin/skills/cleanup/SKILL.md +60 -0
  8. package/dist/abi.js +220 -0
  9. package/dist/abiProbe.js +9 -0
  10. package/dist/backgroundTasks.js +51 -0
  11. package/dist/backup.js +378 -0
  12. package/dist/brief.js +113 -0
  13. package/dist/build-info.json +1 -0
  14. package/dist/claudeDir.js +5 -0
  15. package/dist/cli.js +2901 -0
  16. package/dist/codexHome.js +297 -0
  17. package/dist/config.js +72 -0
  18. package/dist/context.js +256 -0
  19. package/dist/contextCheckpoint.js +55 -0
  20. package/dist/dashboard.js +1158 -0
  21. package/dist/dataDir.js +89 -0
  22. package/dist/db.js +421 -0
  23. package/dist/dispatcher.js +150 -0
  24. package/dist/firstPrompt.js +6 -0
  25. package/dist/harnesses.js +173 -0
  26. package/dist/help.js +353 -0
  27. package/dist/hook.js +197 -0
  28. package/dist/hooks.js +50 -0
  29. package/dist/index.js +34 -0
  30. package/dist/kickoff.js +191 -0
  31. package/dist/leadMessage.js +98 -0
  32. package/dist/mcpConfig.js +121 -0
  33. package/dist/processes.js +161 -0
  34. package/dist/profiles.js +278 -0
  35. package/dist/projectYml.js +254 -0
  36. package/dist/prompts.js +18 -0
  37. package/dist/ptys.js +146 -0
  38. package/dist/result.js +53 -0
  39. package/dist/scheduler.js +1622 -0
  40. package/dist/sessionProbe.js +131 -0
  41. package/dist/slug.js +29 -0
  42. package/dist/spawn.js +370 -0
  43. package/dist/stateProvenance.js +97 -0
  44. package/dist/statusline.js +82 -0
  45. package/dist/strictInput.js +52 -0
  46. package/dist/teardown.js +89 -0
  47. package/dist/tmux.js +1295 -0
  48. package/dist/tools/agents.js +1246 -0
  49. package/dist/tools/kv.js +89 -0
  50. package/dist/tools/leases.js +87 -0
  51. package/dist/tools/meta.js +183 -0
  52. package/dist/tools/pads.js +279 -0
  53. package/dist/tools/params.js +14 -0
  54. package/dist/tools/todos.js +382 -0
  55. package/dist/tools/wakes.js +466 -0
  56. package/dist/transcript.js +123 -0
  57. package/dist/version.js +59 -0
  58. package/package.json +62 -0
  59. package/profiles/orchestration/posture.md +27 -0
  60. package/profiles/orchestration/runbook.md +72 -0
  61. package/profiles/orchestration/worker.md +38 -0
  62. package/profiles/simple/posture.md +14 -0
@@ -0,0 +1,173 @@
1
+ import { codexInputBoxState, codexPaneChoiceCheck, codexPaneHasInputBox, inputBoxState, paneChoiceCheck, paneHasInputBox, } from "./tmux.js";
2
+ export function commandHead(command) {
3
+ return command.trim().split(/\s+/)[0] ?? "";
4
+ }
5
+ export function carriesNameFlag(tokens) {
6
+ return tokens.some((arg) => arg === "--name" || arg.startsWith("--name=") || (arg.startsWith("-n") && !arg.startsWith("--")));
7
+ }
8
+ // Stripped before comparing basenames - never a wrapper's own flags. Negative controls live in
9
+ // test/harness-wrapper-matching.test.mjs.
10
+ const ENV_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
11
+ const KNOWN_WRAPPERS = new Set(["env", "nice", "arch", "time"]);
12
+ function basenameOf(token) {
13
+ return token.split("/").pop() ?? "";
14
+ }
15
+ function resolvedCommand(command) {
16
+ const tokens = command.trim().split(/\s+/).filter(Boolean);
17
+ let i = 0;
18
+ while (i < tokens.length) {
19
+ if (ENV_ASSIGNMENT.test(tokens[i])) {
20
+ i++;
21
+ continue;
22
+ }
23
+ if (KNOWN_WRAPPERS.has(basenameOf(tokens[i])) && i + 1 < tokens.length) {
24
+ i++;
25
+ continue;
26
+ }
27
+ break;
28
+ }
29
+ return { prefix: tokens.slice(0, i + 1).join(" "), basename: basenameOf(tokens[i] ?? "") };
30
+ }
31
+ function resolvedCommandBasename(command) {
32
+ return resolvedCommand(command).basename;
33
+ }
34
+ // For a caller rebuilding an invocation over a recorded command that may carry stale flags
35
+ // (agent_resume). Reuses matches()'s own walk rather than a second parser.
36
+ export function resolvedCommandPrefix(command) {
37
+ return resolvedCommand(command).prefix;
38
+ }
39
+ const claudeHarness = {
40
+ name: "claude",
41
+ matches: (command) => resolvedCommandBasename(command) === "claude",
42
+ argsFor: ({ displayName, namedByCaller }) => displayName && !namedByCaller ? ["--name", displayName] : [],
43
+ briefDelivery: {
44
+ settingsArgs: (path) => ["--settings", path],
45
+ systemPromptArgs: (path) => ["--append-system-prompt-file", path],
46
+ },
47
+ stateSource: true,
48
+ transcriptDir: true,
49
+ contextTokens: true,
50
+ contextRecord: "claude",
51
+ mintsSessionId: true,
52
+ supportsResume: true,
53
+ supportsRename: true,
54
+ classifiesPaneScreen: true,
55
+ paneClassifier: { choiceCheck: paneChoiceCheck, inputBoxState, hasInputBox: paneHasInputBox },
56
+ hasScopes: true,
57
+ needsHome: false,
58
+ initialPromptArgs: null,
59
+ // clear, other, and an unobserved value all stop nothing (todo 765; test/fixtures/hook-payloads/
60
+ // README.md). Only these two are what Claude Code documents as the session being over for good.
61
+ terminalSessionEndReasons: ["prompt_input_exit", "logout"],
62
+ };
63
+ export const codexHarness = {
64
+ name: "codex",
65
+ matches: (command) => resolvedCommandBasename(command) === "codex",
66
+ argsFor: () => [],
67
+ briefDelivery: null,
68
+ stateSource: true,
69
+ transcriptDir: false,
70
+ contextTokens: false,
71
+ contextRecord: "codex",
72
+ // `codex resume <SESSION_ID>` is a positional; no --session-id flag exists, so codex mints its
73
+ // own. This pair disagreeing is why they are two fields.
74
+ mintsSessionId: false,
75
+ supportsResume: true,
76
+ supportsRename: false,
77
+ classifiesPaneScreen: true,
78
+ paneClassifier: {
79
+ choiceCheck: codexPaneChoiceCheck,
80
+ inputBoxState: codexInputBoxState,
81
+ hasInputBox: codexPaneHasInputBox,
82
+ },
83
+ hasScopes: false,
84
+ needsHome: true,
85
+ // Auto-submits with no Enter; verified live on v0.149.0.
86
+ initialPromptArgs: (message) => [message],
87
+ // Measured live on 0.151.0 (todo 782 S1): both `/quit` and Ctrl-C twice fire SessionEnd with
88
+ // reason "other" - never claude's `prompt_input_exit` or `logout`. Codex's vocabulary is
89
+ // disjoint from claude's, not a superset of it, so it gets its own set rather than joining
90
+ // claude's.
91
+ terminalSessionEndReasons: ["other"],
92
+ };
93
+ const unknownHarness = {
94
+ name: "unknown",
95
+ matches: () => false,
96
+ argsFor: () => [],
97
+ briefDelivery: null,
98
+ stateSource: false,
99
+ transcriptDir: false,
100
+ contextTokens: false,
101
+ contextRecord: null,
102
+ mintsSessionId: false,
103
+ supportsResume: false,
104
+ supportsRename: false,
105
+ classifiesPaneScreen: false,
106
+ paneClassifier: null,
107
+ hasScopes: false,
108
+ needsHome: false,
109
+ initialPromptArgs: null,
110
+ // An unrecognized command's vocabulary is unknown, so it stops nothing rather than guessing.
111
+ terminalSessionEndReasons: [],
112
+ };
113
+ const HARNESSES = [claudeHarness, codexHarness];
114
+ export function harnessFor(command) {
115
+ return HARNESSES.find((harness) => harness.matches(command)) ?? unknownHarness;
116
+ }
117
+ // The one source of known harness names; hive.yml's `agents:` and agent_spawn both validate here.
118
+ export function harnessNames() {
119
+ return HARNESSES.map((harness) => harness.name);
120
+ }
121
+ export function registerHarness(harness) {
122
+ if (HARNESSES.some((h) => h.name === harness.name)) {
123
+ throw new Error(`a harness named "${harness.name}" is already registered`);
124
+ }
125
+ if ((harness.paneClassifier != null) !== harness.classifiesPaneScreen) {
126
+ throw new Error(`harness "${harness.name}" sets classifiesPaneScreen to ${harness.classifiesPaneScreen} but ` +
127
+ `paneClassifier is ${harness.paneClassifier == null ? "missing" : "set"}. Every call site that reads ` +
128
+ "classifiesPaneScreen assumes paneClassifier is present whenever it is true, and never consulted " +
129
+ "when it is false; the two must agree.");
130
+ }
131
+ if (harness.supportsRename && !harness.classifiesPaneScreen) {
132
+ throw new Error(`harness "${harness.name}" sets supportsRename without classifiesPaneScreen. agent_rename PASTES ` +
133
+ "AND SUBMITS /rename into a live pane, and the guards that decide whether that is safe (a dialog " +
134
+ "on screen, unsubmitted human text in the box) can only read a screen this table says hive can " +
135
+ "classify. Set classifiesPaneScreen, or leave supportsRename off.");
136
+ }
137
+ // Defends a JS caller (a test stub, or a future third-party registration with no TypeScript
138
+ // check behind it) that omits this field: src/hook.ts calls .includes() on it unconditionally,
139
+ // and undefined.includes would throw, silently swallowed by the try/catch around that call -
140
+ // the exact silent-no-op failure mode this whole capability exists to prevent, just for the
141
+ // registering harness instead of a shipped one.
142
+ HARNESSES.push({ ...harness, terminalSessionEndReasons: harness.terminalSessionEndReasons ?? [] });
143
+ }
144
+ export function unregisterHarness(name) {
145
+ const index = HARNESSES.findIndex((harness) => harness.name === name);
146
+ if (index !== -1)
147
+ HARNESSES.splice(index, 1);
148
+ }
149
+ export function isClaudeCommand(command) {
150
+ return harnessFor(command).name === "claude";
151
+ }
152
+ // An empty command is "no fact recorded", never "unclassifiable" - reading it the other way stops
153
+ // every wake a plain session ever set for itself.
154
+ export function screenClassifiable(command) {
155
+ if (command.trim() === "")
156
+ return true;
157
+ return harnessFor(command).classifiesPaneScreen;
158
+ }
159
+ // Defaults to claude's, not null: otherwise screenClassifiable's "true" above becomes a lie the
160
+ // first time anything reads the pane it was supposed to gate.
161
+ export function paneClassifierFor(command) {
162
+ if (command.trim() === "")
163
+ return claudeHarness.paneClassifier;
164
+ return harnessFor(command).paneClassifier;
165
+ }
166
+ export function transcriptDirFor(command) {
167
+ return harnessFor(command).transcriptDir;
168
+ }
169
+ // ANY transcript to corroborate a stall against: a resolvable directory, or a path the harness
170
+ // reported through its own hook payload. One predicate so both call sites admit the same rows.
171
+ export function hasTranscriptSignal(row) {
172
+ return transcriptDirFor(row.command) || row.transcript_path !== "";
173
+ }
package/dist/help.js ADDED
@@ -0,0 +1,353 @@
1
+ export function helpOverview() {
2
+ return `HIVE — shared memory and coordination across Claude Code sessions
3
+
4
+ Many workers, one shared memory. Multiple Claude sessions (and humans) share
5
+ one project-scoped state store: pads, todos, kv, leases.
6
+
7
+ Call help(topic="<name>") for details. Topics: ${Object.keys(HELP_TOPICS).join(", ")}.
8
+
9
+ Scope resolution for project-scoped tools:
10
+ pass project_id for a one-off override
11
+ otherwise hive uses the project selected in this session
12
+ otherwise it auto-detects from the working directory, or auto-selects a
13
+ single registered project
14
+
15
+ Quick start:
16
+ 1. whoami — see your actor id and effective project
17
+ 2. pad_list / todo_list — see what other sessions have shared
18
+ 3. help(topic="workflow") — the lead/worker operating pattern
19
+
20
+ PROMPTS — invokable prompts (slash commands in MCP clients)
21
+ runbook — load this project's own standing process
22
+
23
+ AGENTS — spawn and drive worker sessions in tmux
24
+ agent_spawn, agent_list, agent_status, agent_send, agent_output,
25
+ agent_message_get, agent_rename, agent_park, agent_resume, agent_close
26
+
27
+ WAKE-UPS — scheduled nudges instead of polling
28
+ wake_set, wake_when_idle, wake_get, wake_update, wake_cancel, wake_list
29
+
30
+ PADS — shared documents (plans, findings, handoffs)
31
+ pad_write, pad_read, pad_append, pad_edit, pad_list, pad_archive, pad_delete
32
+
33
+ TODOS — shared task tracking with dependencies
34
+ todo_create, todo_list, todo_get, todo_update, todo_archive,
35
+ todo_complete, todo_comment, todo_block, todo_unblock
36
+
37
+ KV — small shared status values (with optional TTL)
38
+ kv_set, kv_get, kv_list, kv_delete
39
+
40
+ LEASES — advisory expiring claims on shared work areas
41
+ lease_acquire, lease_release
42
+
43
+ PROJECTS — scope management
44
+ project_list, project_add, project_select, project_prune
45
+
46
+ ACTORS — who is writing
47
+ actor_prune`;
48
+ }
49
+ export const HELP_TOPICS = {
50
+ workflow: `WORKFLOW — lead/worker orchestration
51
+
52
+ One session acts as the lead. It spawns and drives workers with the agent
53
+ tools; each worker is its own agent CLI session in a tmux window, locked to
54
+ this project.
55
+
56
+ Read the standing process first: run \`hive runbook\` in a shell. It prints
57
+ the project's profile runbook, or its runbook pad when the project has no
58
+ profile, with hive.yml vars resolved. Whatever it says takes precedence over
59
+ this generic pattern.
60
+
61
+ The operating pattern:
62
+ 1. Interview the human until you can write a real plan.
63
+ 2. Write the plan to a pad: pad_write(name="plan", ...). Include the goal,
64
+ code paths, constraints, parallel lanes, and verification steps. The
65
+ pad is the shared memory for the run. Keep it current.
66
+ 3. Convert the plan into todos. One todo per lane. Name the files each
67
+ lane owns and its acceptance criteria in the body.
68
+ 4. Encode ordering with blockers: todo_block(todo_id, blocker_id). Workers
69
+ pick up work with todo_list(is_blocked=false, status="open").
70
+ 5. Spawn one worker per unblocked lane: agent_spawn(name="api-worker").
71
+ For parallel file edits, give each worker its own git worktree via cwd.
72
+ A claude worker briefs itself: hive loads the brief into its system
73
+ prompt. Send the lane's objective, its pad/todo ids, and file ownership
74
+ directly. A worker whose screen hive cannot classify is refused on the
75
+ text path; its brief is written to the receipt's brief_path instead.
76
+ 6. Workers set status="in_progress", do the work, then todo_comment the
77
+ handoff: changed files, tests run, remaining risk. Then todo_complete.
78
+ Completing returns newly_unblocked todo ids.
79
+ 7. Do not poll. Running MORE THAN ONE worker, set
80
+ wake_when_idle(scope="project", body="...") ONCE and go quiet: it is a
81
+ STANDING watch over the crew you spawn, it reports each worker as it
82
+ finishes, it covers workers you spawn later, and you never re-arm it.
83
+ wake_when_idle(agents=[...], body="...") is the one-shot: it fires on
84
+ the first finish and STOPS WATCHING the rest, so at three or more
85
+ workers it will lose one. Either way, read REAL output (agent_output)
86
+ before declaring a lane done. The human can watch live with
87
+ tmux attach -t hive-main.
88
+ 8. The lead reviews real diffs and output, not just summaries, then
89
+ integrates one lane at a time.
90
+ 9. Capture handoffs in pads/todo comments BEFORE agent_close; terminal
91
+ output is not retained.
92
+
93
+ Manual workers still work too: a human can open a window and run
94
+ HIVE_AGENT_ID=worker-1 HIVE_AGENT_NAME="API worker" claude
95
+
96
+ Rules that keep this sane:
97
+ - Keep worker prompts self-contained: include pad and todo ids.
98
+ - Record every decision in the pad or a todo comment. Sessions die;
99
+ the store survives.
100
+ - Take a lease (lease_acquire) before editing a shared file area; leases
101
+ expire so a dead session never wedges anyone.`,
102
+ profiles: `PROFILES — standing instructions shared across projects
103
+
104
+ A profile is a named set of instructions this project runs under, named by
105
+ "profile:" in hive.yml. hive resolves each file from ~/.hive/profiles first,
106
+ then its own defaults, so a file the human forked is theirs and the rest
107
+ track hive.
108
+
109
+ posture.md already in your system prompt if the session started with
110
+ hive lead, with this project's vars resolved. You do not need
111
+ to read it; \`hive posture\` shows the human what you were given.
112
+ runbook.md the project's process. Read it with \`hive runbook\` (a shell
113
+ command, not a tool). It has hive.yml vars substituted.
114
+ worker.md what agent_spawn puts in a worker's system prompt.
115
+
116
+ hive runbook the process, ready to read
117
+ hive posture the posture text you were started with
118
+ hive profile list what exists and where each file comes from
119
+ hive profile fork <name> copy a default into ~/.hive so the human can edit
120
+
121
+ A project with a profile usually has NO "runbook" pad; the profile replaced
122
+ it. A project on "profile: none" keeps the pad, and \`hive runbook\` prints
123
+ that instead, so the one command is right either way.
124
+
125
+ If the human asks to change how work runs here, that is an edit to the
126
+ runbook (fork it first), not a pad write.`,
127
+ agents: `AGENTS — spawn and drive worker sessions in tmux
128
+
129
+ agent_spawn(name?, model?, command?, extra_args?, cwd?, placement?, layout?) —
130
+ start a worker (default command: claude) in session hive-main.
131
+ placement="split" (default) tiles the worker as a pane in the lead's
132
+ window so the whole crew shares one screen; placement="window" gives it
133
+ its own tmux window (an iTerm tab under control mode). layout picks how split panes are
134
+ arranged: tiled (default), main-vertical (lead takes the left half,
135
+ workers stack on the right), main-horizontal, even-horizontal,
136
+ even-vertical. hive re-applies it when a worker closes, so the
137
+ arrangement survives crew changes. Projects can set a default placement
138
+ and layout in hive.yml; an explicit argument overrides it. cwd defaults to the project root; pass
139
+ a git worktree path to isolate parallel file edits. A claude worker gets
140
+ its brief in the system prompt; other commands return instructions to
141
+ PREPEND to the first prompt.
142
+ Workers run with HIVE_PROJECT_LOCK=1.
143
+ agent_send(name|agent_id, text?, keys?, submit?, wait_ms?) — type into the
144
+ worker's terminal. Multi-line text pastes safely; keys sends tmux key
145
+ names like Escape or C-c. wait_ms returns the terminal tail after.
146
+ Text is prefixed with the sender tag.
147
+ Text over 300 chars sent to a LEAD by anyone but that lead is stored and
148
+ delivered as a one-line pointer, so the human's own pane stays readable;
149
+ the receipt says shortened and names the message id.
150
+ agent_message_get(message_id) — the full text behind a pointer line. Kept
151
+ 7 days; a lookup for a pruned id says it expired, not that it is missing.
152
+ agent_output(name|agent_id, lines?) — read the rendered terminal.
153
+ agent_status(name|agent_id, include_brief?) — liveness, current command,
154
+ short tail, and the path to the brief this worker was given
155
+ (include_brief=true returns its text; no transcript records it).
156
+ agent_list(include_closed?) — all agents with live status.
157
+ agent_rename(name|agent_id, new_name) — change the display name. actor_id
158
+ stays agent:N, so older pad writes and todo comments still point here.
159
+ A live claude worker is told to retitle its own session, which arrives as
160
+ a user turn: rename between assignments, not mid-task.
161
+ agent_park(name|agent_id) — END OF DAY. Kill the pane, mark the row PARKED
162
+ rather than plain closed, record the branch, and hand back a board line
163
+ plus the one call that brings the lane back. Use this instead of
164
+ agent_close whenever the lane is PAUSED rather than finished: "closed"
165
+ alone means both, and a next-morning lead cannot tell them apart.
166
+ agent_resume(name|agent_id) — NEXT MORNING. Reopen a closed or parked
167
+ claude or codex worker on a fresh pane from its recorded session id,
168
+ with the same actor_id and its full prior context. It does not send
169
+ the assignment; agent_send it afterwards. Read the pane before
170
+ believing any wake about a worker you just resumed.
171
+ agent_close(name|agent_id) — kill the worker's pane and mark closed. Capture
172
+ handoffs first; output is not retained. Self-close needs confirm_self.
173
+ On a PARKED row (by agent_id) it releases the park instead, which is how
174
+ you abandon a lane you have decided not to resume.
175
+
176
+ Address a worker by its name, not its id: agent_send(name="impl", ...). A
177
+ partial name works when it matches one running worker, so name="123" finds
178
+ DEVX-123. The name is the handle you chose and the one shown in its pane.
179
+
180
+ Visibility is automatic: if nothing is attached to the project session when
181
+ a worker spawns, hive pops open iTerm (control mode by default; see
182
+ hive setup --attach) or Terminal attached to it, so the human sees every
183
+ worker as a native window and can type into any of them. The human usually
184
+ starts the day with the CLI: hive lead.
185
+ Manual attach also works: tmux attach -t hive-main, or
186
+ tmux -CC attach -t hive-main for iTerm's native windows.
187
+
188
+ Worker state (agent_state on list/status) comes from Claude Code hooks that
189
+ agent_spawn wires automatically: working (prompt submitted), idle (finished
190
+ its turn), waiting (needs permission or input). Non-claude commands show
191
+ "unknown".`,
192
+ wakes: `WAKE-UPS — scheduled nudges instead of polling
193
+
194
+ wake_set(delay_seconds, body, deliver_to?, repeat_every_seconds?) —
195
+ one-shot or repeating wake-up
196
+ wake_when_idle(agents | scope, body, mode?, max_wait_seconds?,
197
+ deliver_to?) — fire when workers go idle. Pass exactly one of:
198
+ scope="project" — a STANDING watch over the crew you spawn in this project.
199
+ It reports EACH worker as it finishes or its window dies, covers
200
+ workers spawned after you set it, and keeps watching until
201
+ max_wait_seconds (default 4 hours) or wake_cancel. Workers already
202
+ idle DO count, deliberately. Use this whenever more than one worker
203
+ is running. ONE PER PROJECT: setting a second is refused and names
204
+ the one already running, so calling twice costs nothing.
205
+ agents=[...] — a ONE-SHOT over a named list. mode=any (default) fires
206
+ on the first fresh idle transition and then STOPS WATCHING the
207
+ others; mode=all fires when every watched agent is idle and returns
208
+ already_satisfied instead of scheduling if they already are.
209
+ Agents already idle when it was set do not count.
210
+ wake_get(wake_id) — read one wake-up by id, with its untruncated body
211
+ (wake_list truncates at 120 chars)
212
+ wake_update(wake_id, delay_seconds?, body?, repeat_every_seconds?) — edit
213
+ a pending wake-up you own in place, without minting a new id.
214
+ delay_seconds is relative to now; repeat_every_seconds changes only the
215
+ interval used for firings after this one. Only body can be edited on an
216
+ idle wake (from wake_when_idle); delay_seconds/repeat_every_seconds
217
+ apply to a delay wake (from wake_set) only.
218
+ wake_cancel(wake_id) — cancel a pending wake-up you own
219
+ wake_list() — pending wake-ups in this project
220
+
221
+ Delivery contract:
222
+ When a wake-up fires, its body is typed into the target session's terminal
223
+ as a fresh user turn, prefixed with [hive wake #N]. The receiving agent
224
+ gets it cold, so write bodies self-contained: agent ids, pad/todo ids,
225
+ and the next action. Plain English, no markup.
226
+ Waking your OWN lead pane is the exception: it is not cold, so carry the
227
+ action and the ids and point at where the detail lives.
228
+
229
+ Receiving wake-ups:
230
+ Spawned workers can always receive (deliver_to their name or id).
231
+ A lead session can receive only if it runs inside tmux; start it with
232
+ tmux, then claude (watch with tmux attach, or tmux -CC attach for iTerm's
233
+ native windows). Otherwise wake_set without deliver_to fails with guidance.
234
+
235
+ Idle detection is exact, not heuristic: Claude Code hooks in each spawned
236
+ worker report working/idle/waiting into the shared store the moment they
237
+ happen. Cancel wake-ups when the monitored work is done.`,
238
+ projects: `PROJECTS — scope management
239
+
240
+ Every pad, todo, kv entry, and lease belongs to one project (a directory).
241
+
242
+ project_list — all registered projects plus the current selection
243
+ project_add(path?, name?) — register a directory (defaults to cwd)
244
+ project_select(project_id) — set this session's default scope
245
+ project_prune() — delete every registered project that owns no rows
246
+ anywhere in the store, verified individually; never your own; refuses
247
+ under HIVE_PROJECT_LOCK=1 since it sweeps every project, not just yours
248
+
249
+ Resolution order: explicit project_id argument, then session selection, then
250
+ working-directory auto-detection. When the working directory matches no
251
+ registered project, it is registered as a new project automatically; hive
252
+ never falls back to an unrelated project.
253
+
254
+ Git worktrees and subdirectories resolve to the primary checkout's project,
255
+ so a worker session in a worktree shares the main repo's pads and todos.
256
+ Register a worktree path explicitly with project_add to make it a separate
257
+ project on purpose.
258
+
259
+ Cross-project access is intentional-only. An empty result means there is
260
+ nothing in this project; report that and stop. Reach into another project
261
+ (project_select or a project_id override) only when the user explicitly
262
+ names it. Set HIVE_PROJECT_LOCK=1 in a session's environment to reject
263
+ cross-project access entirely; use it for worker sessions.`,
264
+ identity: `IDENTITY — who is writing
265
+
266
+ Every write records an actor. Identity comes from environment variables read
267
+ at session start:
268
+
269
+ HIVE_AGENT_ID stable actor id, e.g. "worker-1" or "lead"
270
+ HIVE_AGENT_NAME display name, e.g. "API worker"
271
+
272
+ Without HIVE_AGENT_ID you are "user:<username>" (kind: human). Launch worker
273
+ sessions with distinct ids so handoffs and leases are attributable:
274
+
275
+ HIVE_AGENT_ID=worker-1 claude
276
+
277
+ whoami shows your actor id, kind, and effective project.
278
+
279
+ See help(topic="actors") for actor_prune.`,
280
+ actors: `ACTORS — pruning stale identities
281
+
282
+ actor_prune() — delete every actor that owns no rows anywhere in the store
283
+ and has not been active in the last minute, checked globally across
284
+ every project since actors carry no project_id; never your own;
285
+ refuses under HIVE_PROJECT_LOCK=1 since it sweeps the whole store
286
+
287
+ See help(topic="identity") for how an actor id is assigned in the first
288
+ place.`,
289
+ pads: `PADS — shared documents
290
+
291
+ pad_write(name, content, tags?) — create a pad; names are unique per project
292
+ pad_write(pad_id, name, content, expected_revision) — full overwrite; the
293
+ revision guard makes concurrent writers safe
294
+ pad_read(pad_id | name) — content plus revision and metadata
295
+ pad_append(pad_id, content, expected_revision?) — add to the end
296
+ pad_edit(pad_id, old_text, new_text, expected_revision?) — replace one
297
+ unique literal occurrence; include enough context to disambiguate
298
+ pad_list(query?, tags?, include_archived?) — summaries without content;
299
+ query matches names and content and returns a snippet
300
+ pad_archive(pad_id, archived?) — archive frees the name for a new active
301
+ pad; content stays readable by pad_id. archived=false unarchives.
302
+ pad_delete(pad_id, expected_revision?) — permanent; prefer pad_archive
303
+
304
+ Convention: read before you write. Reads return the revision; pass it back
305
+ as expected_revision so a concurrent edit fails loudly instead of clobbering.
306
+
307
+ Lifecycle: durable pads (a runbook, a daily board) get updated in place.
308
+ Temporary pads (a lane plan, a findings dump) get archived when their work
309
+ completes, like todos. Rotate a daily board by archiving yesterday's pad
310
+ and writing a fresh one under the same name; keep the live pad small,
311
+ since every read pays for its full length.`,
312
+ todos: `TODOS — shared task tracking
313
+
314
+ todo_create(title, body?, priority?, tags?, slug?, blocked_by?) — priority
315
+ is high|medium|low; slug is a short label (~3-5 words) so this todo
316
+ reads the same way everywhere it's referenced by id
317
+ todo_list(status?, is_blocked?, priority?, query?, tags?, include_archived?, limit?, offset?)
318
+ — status is open|in_progress|backlog|completed; is_blocked=false finds
319
+ dispatchable work; archived todos are excluded unless include_archived
320
+ todo_get(todo_id, include_comments?) — full body, blockers, comments;
321
+ always reaches an archived todo too, by id
322
+ todo_update(todo_id, title?, body?, priority?, status?, tags?, slug?)
323
+ todo_archive(todo_id, archived?) — retires a todo but keeps it readable
324
+ by id; archived=false reverses it. Refuses if this todo still blocks
325
+ a non-completed todo, unless this todo is itself completed
326
+ todo_complete(todo_id, completed?) — returns newly_unblocked todo ids;
327
+ completed=false reopens
328
+ todo_comment(todo_id, body) — handoffs, decisions, findings
329
+ todo_block(todo_id, blocker_id) / todo_unblock(todo_id, blocker_id)
330
+
331
+ Blockers form a dependency graph (cycles are rejected). A todo is blocked
332
+ while any of its blockers is not completed. Archived and completed are
333
+ independent axes: archiving hides a closed lane's scaffolding from
334
+ todo_list without marking anything done.`,
335
+ kv: `KV — small shared status values
336
+
337
+ kv_set(key, value, ttl_seconds?) — value is any JSON; TTL optional
338
+ kv_get(key)
339
+ kv_list(prefix?)
340
+ kv_delete(key)
341
+
342
+ Use kv for values another session needs to discover: current branch, a port,
343
+ a phase flag. Use pads for anything longer than a line or two.`,
344
+ leases: `LEASES — advisory expiring claims on shared work areas
345
+
346
+ lease_acquire(key, ttl_seconds) — non-blocking; returns the holder if
347
+ already taken; re-taking your own lease extends it
348
+ lease_release(key)
349
+
350
+ Keys are project-scoped and free-form; keep them stable and specific, like
351
+ "file:src/api/routes.ts" or "todo:42". Leases expire on their own, so a
352
+ crashed session never wedges the team.`,
353
+ };