opencode-workspace-mailbox 0.0.0-stage → 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Renzo S. (mr-wolf-rsh)
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,348 @@
1
- # Temporary Holding Version
1
+ # opencode-workspace-mailbox
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
+ A per-project handoff mailbox for [OpenCode](https://opencode.ai) sessions.
4
+
5
+ Every session gets it: a file-based inbox that other sessions write to, automatic delivery when the
6
+ session goes idle, and loop guards so an agent can never talk itself into a spin. One global config
7
+ serves a whole folder tree — **meta → account → repo** — because the tier is derived from the
8
+ directory a session opens in, not from what you remember to configure.
9
+
10
+ Built for OpenCode today; the storage layer is behind a `MailboxStore` interface, so the file
11
+ backend can be swapped for SQLite or Engram — see [Backlog](#backlog).
12
+
13
+ > Status: the code is complete (81 tests, `typecheck` and `build` green, repo at
14
+ > [mr-wolf-rsh/opencode-workspace-mailbox](https://github.com/mr-wolf-rsh/opencode-workspace-mailbox)).
15
+ > It is not yet published to npm — see [Install](#install).
16
+
17
+ ## The metaphor
18
+
19
+ The mailbox is a **chain of command made of files**. Each tier owns a `handoffs/` folder holding its
20
+ own `workspace.md` plus one file per immediate child. Messages only ever move along that ladder:
21
+ escalate up, delegate down, or talk to a sibling.
22
+
23
+ | In an organisation | In opencode-workspace-mailbox |
24
+ |---|---|
25
+ | The chief of staff | **Meta** tier — `D:\WORKSHOP` reads `handoffs/workspace.md` |
26
+ | Account leads | **Account** tier — `mr-wolf-rsh` reads `mr-wolf-rsh/handoffs/workspace.md` |
27
+ | The team doing the work | **Repo** tier — `<account>/handoffs/<project>.md` |
28
+ | Memos, never rewrites | Append-only markdown; history is the audit trail |
29
+ | "Received" stamp | `**Status:**` flips `OPEN` → `DONE`, one line, nothing else touched |
30
+ | Handing over a task | `mailbox_send({ to: "absolute-scrapers", ... })` |
31
+ | Standing instructions | `wake: true` — an idle session picks up new mail within one poll |
32
+ | Opening a desk for the recipient | `spawnOnSend: true` — creates a session in the recipient's folder |
33
+ | A message that needs a decision | Only then does the agent reply in chat |
34
+
35
+ Tiers are **discovered from the folder tree**, not configured: a directory with `handoffs/` is a
36
+ tier root, and anything under it resolves to its nearest ancestor that has one.
37
+
38
+ ## Why this exists
39
+
40
+ Multi-repo workspaces talk through asynchronous handoffs: one file per project, append-only,
41
+ addressed `from X → to Y`. The protocol works. The human relay does not.
42
+
43
+ A message sits `OPEN` until somebody opens the right session, reads it, acts on it, and flips it to
44
+ `DONE` — and "somebody remembers" is exactly the step that fails. Worse, once you have a parent
45
+ strategy session and several implementation repos, the relay has to survive closed sessions,
46
+ restarts, and a working day that never lines up.
47
+
48
+ So this plugin closes the loop **inside the session that receives the message**. No daemon, no
49
+ scheduler, no `opencode run` spawning a session per message: if a session is closed, the message
50
+ simply waits in its file. When the session is open, the message surfaces on its own.
51
+
52
+ The file format is the product, not an implementation detail: it is readable, diffable, reviewable
53
+ in a PR, greppable, and it survives every process that ever touched it.
54
+
55
+ ## What it does
56
+
57
+ Delivered as tools the agent calls and two hooks the runtime calls:
58
+
59
+ - **`mailbox_read`** — list `OPEN` messages for a project (or any routed recipient).
60
+ - **`mailbox_send`** — append a message to the recipient's mailbox, optionally spawning a session
61
+ in the recipient's directory.
62
+ - **`mailbox_ack`** — flip one message's `**Status:**` line to `DONE`, byte-preserving everywhere
63
+ else.
64
+
65
+ On `session.idle` (and, with `wake: true`, on a timer) the plugin reads the session's own mailbox,
66
+ finds `OPEN` messages it has not handled yet, and prompts the session **once** to deal with them.
67
+
68
+ ### Delivery is automatic when you opt in
69
+
70
+ `wake: true` makes an already-idle session notice new mail. Without it, the plugin only reacts to an
71
+ idle *transition* — which means a session that has been sitting idle all afternoon never learns
72
+ about the message your other session just wrote.
73
+
74
+ ### Spawning opens the recipient's desk
75
+
76
+ `spawnOnSend: true` creates a session in the recipient's project directory when you send to it, so a
77
+ project with nothing open can still be reached. Guarded so it cannot ping-pong — see
78
+ [Spawn on send](#spawn-on-send).
79
+
80
+ ## Install
81
+
82
+ 1. Add the plugin to your `opencode.json`:
83
+
84
+ ```jsonc
85
+ {
86
+ "$schema": "https://opencode.ai/config.json",
87
+ "plugin": [
88
+ ["opencode-workspace-mailbox", {
89
+ "wake": true,
90
+ "spawnOnSend": true
91
+ }]
92
+ ]
93
+ }
94
+ ```
95
+
96
+ Both flags are off by default: `wake` costs tokens while nobody is watching, and spawning creates
97
+ sessions on its own. Turn them on deliberately.
98
+
99
+ Do **not** set `mailboxDir` unless you deliberately want one pinned mailbox: it disables tier
100
+ discovery, which is the whole point of a single global config.
101
+
102
+ 2. If your config directory declares plugin dependencies (some setups keep a `package.json` next to
103
+ `opencode.json`), add the package there too:
104
+
105
+ ```powershell
106
+ cd C:\Users\<you>\.config\opencode
107
+ npm install opencode-workspace-mailbox --save
108
+ ```
109
+
110
+ 3. Restart OpenCode.
111
+
112
+ Local path variant during development (no publish needed — `prepare` builds it):
113
+
114
+ ```powershell
115
+ npm install file:D:\WORKSHOP\mr-wolf-rsh\tools\opencode-workspace-mailbox --save
116
+ ```
117
+
118
+ ```jsonc
119
+ ["D:\\WORKSHOP\\mr-wolf-rsh\\tools\\opencode-workspace-mailbox", { "wake": true }]
120
+ ```
121
+
122
+ ## Configuration
123
+
124
+ ```jsonc
125
+ ["opencode-workspace-mailbox", {
126
+ // Master switch. false = the plugin does nothing at all.
127
+ "enabled": true,
128
+
129
+ // Pin ONE mailbox directory and disable tier discovery. Leave unset normally.
130
+ "mailboxDir": null,
131
+
132
+ // Override the derived identity ("workspace" for a tier root, else the folder name).
133
+ "project": null,
134
+
135
+ // Storage backend. "file" is the MVP; the store interface is pluggable.
136
+ "storage": "file",
137
+
138
+ // Auto-prompts in a row without progress, then one toast instead. 0..20.
139
+ "maxAutoRounds": 2,
140
+
141
+ // Minimum gap between auto-prompts (and between notices).
142
+ "cooldownMs": 30000,
143
+
144
+ // Poll the mailbox and wake an idle session on a new message.
145
+ "wake": false,
146
+ "pollMs": 10000,
147
+
148
+ // Create a session in the recipient's directory when sending.
149
+ "spawnOnSend": false,
150
+ "spawnCooldownMs": 60000,
151
+ "maxSpawnsPerTurn": 1,
152
+
153
+ // Explicit recipient -> directory map for spawning. Discovery covers most trees.
154
+ "projectDirs": {},
155
+
156
+ // Where dedupe/round/spawn state is persisted (must stay inside the project directory).
157
+ "stateFile": null
158
+ }]
159
+ ```
160
+
161
+ | Option | Default | Meaning |
162
+ |---|---|---|
163
+ | `enabled` | `true` | Master switch. |
164
+ | `mailboxDir` | discovered | Pin a mailbox directory; disables discovery. |
165
+ | `project` | resolved | Explicit identity override. |
166
+ | `storage` | `file` | Backend selector (`file` implemented). |
167
+ | `maxAutoRounds` | `2` | Auto-prompts in a row without progress, `0..20`. |
168
+ | `cooldownMs` | `30000` | Gap between auto-prompts and notices. |
169
+ | `wake` | `false` | Poll and wake an idle session. |
170
+ | `pollMs` | `10000` | Wake poll interval, `100..600000`. |
171
+ | `spawnOnSend` | `false` | Create a session in the recipient's directory. |
172
+ | `spawnCooldownMs` | `60000` | Gap between spawns. |
173
+ | `maxSpawnsPerTurn` | `1` | Spawns per human turn, `0..10`. |
174
+ | `projectDirs` | `{}` | Explicit recipient → directory map. |
175
+ | `stateFile` | `<dir>/.atl/mailbox-state.json` | Persisted dedupe/round/spawn state. |
176
+
177
+ ## Tiers and identity
178
+
179
+ | Tier | Folder | Reads at start | Holds |
180
+ |---|---|---|---|
181
+ | **Meta** | `D:\WORKSHOP` | `handoffs/workspace.md` | `workspace.md` + one file per account |
182
+ | **Account** | `D:\WORKSHOP\mr-wolf-rsh` | `mr-wolf-rsh/handoffs/workspace.md` | `workspace.md` + one file per repo |
183
+ | **Repo** | `.../mr-wolf-rsh/tools/opencode-workspace-mailbox` | `mr-wolf-rsh/handoffs/opencode-workspace-mailbox.md` | its own file, at the account level |
184
+
185
+ Discovery rules, for a session opened in `D`:
186
+
187
+ 1. `project` is set in config → use it verbatim (and `mailboxDir` if set).
188
+ 2. `D` has `handoffs/` → `D` is a tier root. Parent has one too → **account**, else **meta**.
189
+ Identity is `workspace`; its mailbox is `D/handoffs`.
190
+ 3. Otherwise → walk up to the nearest ancestor with `handoffs/`; that is the **account**, `D` is a
191
+ **repo**. Identity is `D`'s folder name, kebab-cased, and the mailbox is the account's
192
+ `handoffs/` — so bucket layouts like `<account>/tools/<repo>` work without configuration.
193
+
194
+ For this workspace:
195
+
196
+ | Session opened at | Tier | Mailbox |
197
+ |---|---|---|
198
+ | `D:\WORKSHOP` | meta | `D:\WORKSHOP\handoffs\workspace.md` |
199
+ | `D:\WORKSHOP\mr-wolf-rsh` | account | `D:\WORKSHOP\mr-wolf-rsh\handoffs\workspace.md` |
200
+ | `D:\WORKSHOP\mr-wolf-rsh\tools\opencode-workspace-mailbox` | repo | `D:\WORKSHOP\mr-wolf-rsh\handoffs\opencode-workspace-mailbox.md` |
201
+ | `D:\WORKSHOP\absolute-labs-studio\absolute-scrapers` | repo | `D:\WORKSHOP\absolute-labs-studio\handoffs\absolute-scrapers.md` |
202
+
203
+ ### Routing
204
+
205
+ - **`to: workspace`** → this tier's own `workspace.md`.
206
+ - **`to: <account>`** → the root tier's `<account>.md` — escalate up, or lateral between accounts.
207
+ - **`to: <anything else>`** — a sibling or child file in this tier: lateral, or delegate down.
208
+
209
+ The inbox for a tier is created on first run; an account session also creates its file at the root.
210
+ Unknown recipients are created on first send. Reads never create files.
211
+
212
+ ## Mailbox format
213
+
214
+ Files are `<mailboxTier>/handoffs/<project>.md`. Append-only: messages are never rewritten,
215
+ reordered, or "tidied". The **only** permitted edit to history is the `**Status:**` line.
216
+
217
+ ```markdown
218
+ ### YYYY-MM-DD HH:MM — from <source> → to <recipient>
219
+ **Status:** OPEN | DONE
220
+ **Topic:** one-line subject
221
+ Body — what needs attention, the question, exact paths/commits/commands.
222
+ ```
223
+
224
+ `mailbox_ack` replaces the status line's content with `DONE`, so an annotated
225
+ `**Status:** OPEN (confirm the wording)` becomes `**Status:** DONE`. Every other byte — spacing
226
+ elsewhere, line endings, bodies — is unchanged; the tests assert that on CRLF fixtures, and it was
227
+ verified by hand against a real 113 KB mailbox file.
228
+
229
+ ## Storage
230
+
231
+ Mailboxes sit behind a `MailboxStore` interface:
232
+
233
+ ```ts
234
+ interface MailboxStore {
235
+ ensure(project: string): string
236
+ listOpen(project: string): MailboxMessage[]
237
+ read(project: string): MailboxMessage[]
238
+ append(project: string, input: AppendInput): SendResult
239
+ ack(project: string, target: { hash?: string; header?: string }): AckResult
240
+ query(project: string, filter?: MailboxFilter): MailboxMessage[]
241
+ }
242
+ ```
243
+
244
+ `createFileStore(dir)` is the reference backend. The message format and file layout are the contract:
245
+ a SQLite backend (atomic append, indexed OPEN-by-project, WAL concurrency) or an Engram-backed store
246
+ only has to implement those six operations — the tools, wake path, and spawning never change.
247
+
248
+ ## Loop guards
249
+
250
+ | Guard | Behaviour |
251
+ |---|---|
252
+ | Hash dedupe | One prompt per message, persisted across restarts. |
253
+ | Own writes | Messages this plugin appended never wake anybody. |
254
+ | Round cap | `maxAutoRounds` prompts in a row without progress, then a toast instead. |
255
+ | Progress reset | An `mailbox_ack`, or a drained inbox, restores the budget. |
256
+ | Cooldown | `cooldownMs` between prompts; one notice per distinct pending set. |
257
+ | Idempotent ack | Acking an already-`DONE` message is a no-op, not an error. |
258
+ | Spawn guards | Opt-in, one per message hash, cooldown, per-turn cap, never from a spawned session, never when a session already exists. |
259
+
260
+ ## How delivery decides
261
+
262
+ 1. **Trigger** — `session.idle`, or a `wake` poll tick for the most recently active non-busy session.
263
+ 2. **Skip** — plugin is disabled, this session was just prompted by the plugin, or the session is
264
+ mid-turn.
265
+ 3. **Filter** — keep `OPEN` messages whose hash is not in `prompted` and not in `ownWrites`.
266
+ 4. **Budget** — if the inbox is empty, reset the round budget; if the cap or cooldown is hit, stop
267
+ (surfacing a one-line toast at most once per pending set).
268
+ 5. **Dispatch** — record every hash *before* prompting, then send one prompt: load the
269
+ `workspace-mailbox` skill, read the file, handle the messages, ack them, reply only if a decision
270
+ is needed.
271
+
272
+ Hashes are recorded before dispatch on purpose: a failed prompt is retried, never silently dropped.
273
+
274
+ ## Spawn on send
275
+
276
+ With `spawnOnSend: true`, `mailbox_send` also creates a session in the recipient's directory
277
+ (`session.create` with `query.directory`), titled `mailbox: <recipient>`.
278
+
279
+ **Directory resolution**, in order:
280
+
281
+ 1. `projectDirs[recipient]`, if configured.
282
+ 2. `workspace` → this tier's own root directory.
283
+ 3. An account name → that account's directory under the workspace root.
284
+ 4. A sibling of the mailbox directory, or a bounded search for a nested bucket
285
+ (`<account>/tools/<repo>`).
286
+
287
+ If nothing resolves, the send still succeeds and the tool reports why no session was created. It
288
+ never guesses a directory.
289
+
290
+ **What spawning does not do:** it creates a server-side session; it does not attach a terminal
291
+ window. A headless session that hits a permission prompt has nobody to answer it.
292
+
293
+ ## Limitations
294
+
295
+ - **Delivery needs a plugin instance serving the recipient directory.** With no session there, the
296
+ message waits in its file. `spawnOnSend` creates a session but cannot attach a TUI to it.
297
+ - **One project per plugin instance.** Cross-project activation beyond `session.create` is out of
298
+ scope; identity is resolved per session, never shared.
299
+ - **`wake` spends tokens with nobody watching**, at the pace messages arrive. Leave it off for
300
+ idle-only delivery.
301
+ - **No migration of old layouts.** Pointing the plugin at a mailbox directory that still holds
302
+ pre-hierarchy files leaves them in place; account files appear alongside.
303
+ - **Busy detection is event-driven.** If a runtime never emits `session.status`, the wake path can
304
+ queue a prompt behind an active turn instead of waiting.
305
+ - **Message bodies are data only.** They are never executed, evaluated, or turned into commands.
306
+ - **New recipient files are not registered in the tier's `README.md`.** The plugin writes mailboxes,
307
+ not documentation.
308
+
309
+ ---
310
+
311
+ ## Backlog
312
+
313
+ Ideas for the future — not implemented yet.
314
+
315
+ - **SQLite backend** — implement `MailboxStore` over SQLite: atomic append, an index on
316
+ `(project, status)`, WAL for concurrent sessions, and `query` without re-parsing files. The
317
+ interface and the message format already allow it.
318
+ - **Engram backend** — reuse the persistent-memory store for teams that want mailboxes outside the
319
+ repo. Same six operations.
320
+ - **`to: all` broadcast** — the protocol allows a broadcast copied into every file in a tier; today
321
+ `all` is treated as a normal recipient name.
322
+ - **Migration helper** — move legacy top-level files into the tier layout with an explicit,
323
+ reviewable command instead of leaving them behind.
324
+ - **Watermark** — a `since` option so a fresh install does not replay a mailbox's entire `OPEN`
325
+ history on first contact.
326
+
327
+ ---
328
+
329
+ ## Development
330
+
331
+ ```bash
332
+ npm install
333
+ npm test # vitest: parser, ack byte-identity, send, paths, wake, spawn, tiers, store
334
+ npm run typecheck # tsc --noEmit
335
+ npm run build # tsc -> dist/
336
+ ```
337
+
338
+ ## Author
339
+
340
+ - **Renzo S.** — [mr-wolf-rsh](https://github.com/mr-wolf-rsh/)
341
+
342
+ ## Contributing
343
+
344
+ All sorts of suggestions and pull requests are welcome.
345
+
346
+ ## License
347
+
348
+ [MIT](./LICENSE)
@@ -0,0 +1,32 @@
1
+ export type MailboxConfig = {
2
+ mailboxDir?: string;
3
+ project?: string | null;
4
+ maxAutoRounds?: number;
5
+ cooldownMs?: number;
6
+ enabled?: boolean;
7
+ stateFile?: string | null;
8
+ wake?: boolean;
9
+ pollMs?: number;
10
+ spawnOnSend?: boolean;
11
+ spawnCooldownMs?: number;
12
+ maxSpawnsPerTurn?: number;
13
+ projectDirs?: Record<string, string>;
14
+ };
15
+ export type NormalizedConfig = {
16
+ mailboxDir: string;
17
+ project: string;
18
+ /** True when mailboxDir came from options (disables folder-tree discovery). */
19
+ explicitMailboxDir: boolean;
20
+ storage: "file";
21
+ maxAutoRounds: number;
22
+ cooldownMs: number;
23
+ enabled: boolean;
24
+ stateFile: string;
25
+ wake: boolean;
26
+ pollMs: number;
27
+ spawnOnSend: boolean;
28
+ spawnCooldownMs: number;
29
+ maxSpawnsPerTurn: number;
30
+ projectDirs: Record<string, string>;
31
+ };
32
+ export declare function normalizeConfig(options: Record<string, unknown> | undefined, directory: string): NormalizedConfig;
package/dist/config.js ADDED
@@ -0,0 +1,85 @@
1
+ import path from "node:path";
2
+ import { assertInside, isSafeRecipient } from "./paths.js";
3
+ function clampInt(value, def, min, max) {
4
+ if (typeof value !== "number" || !Number.isFinite(value))
5
+ return def;
6
+ return Math.min(max, Math.max(min, Math.floor(value)));
7
+ }
8
+ function nonNegativeInt(value, def) {
9
+ if (typeof value !== "number" || !Number.isFinite(value))
10
+ return def;
11
+ return Math.max(0, Math.floor(value));
12
+ }
13
+ /** Turn an arbitrary directory name into a lowercase kebab-case recipient id. */
14
+ function toKebab(value) {
15
+ return value
16
+ .toLowerCase()
17
+ .replace(/[^a-z0-9]+/g, "-")
18
+ .replace(/^-+|-+$/g, "");
19
+ }
20
+ function isStrictlyInside(base, candidate) {
21
+ const rel = path.relative(path.resolve(base), path.resolve(candidate));
22
+ return rel !== "" && !path.isAbsolute(rel) && rel !== ".." && !rel.startsWith(".." + path.sep);
23
+ }
24
+ export function normalizeConfig(options, directory) {
25
+ const opts = options ?? {};
26
+ const configuredMailboxDir = typeof opts.mailboxDir === "string" && opts.mailboxDir.length > 0 ? opts.mailboxDir : null;
27
+ const mailboxDir = path.resolve(configuredMailboxDir ?? path.join(directory, "handoffs"));
28
+ // Only the file backend is implemented today; the field exists so a SQLite
29
+ // backend can be selected later without a config-format change.
30
+ const storage = "file";
31
+ const rawProject = opts.project;
32
+ const configuredProject = typeof rawProject === "string" && rawProject.length > 0 ? rawProject : null;
33
+ // Identity is resolved per session, not per config:
34
+ // explicit `project` wins; otherwise a session whose directory *contains*
35
+ // the shared mailbox directory is the workspace parent, and every other
36
+ // session uses its own directory name. The containment rule only applies to
37
+ // an explicitly configured mailboxDir, so the per-project default
38
+ // (`<directory>/handoffs`) never turns every project into the parent.
39
+ const project = configuredProject ??
40
+ (configuredMailboxDir !== null && isStrictlyInside(directory, mailboxDir)
41
+ ? "workspace"
42
+ : toKebab(path.basename(path.resolve(directory))));
43
+ if (!isSafeRecipient(project)) {
44
+ throw new Error(configuredProject !== null
45
+ ? `Invalid project name "${configuredProject}": must be lowercase kebab-case`
46
+ : `Cannot derive a valid project name from "${directory}": set the "project" plugin option`);
47
+ }
48
+ const maxAutoRounds = clampInt(opts.maxAutoRounds, 2, 0, 20);
49
+ const cooldownMs = nonNegativeInt(opts.cooldownMs, 30000);
50
+ const enabled = typeof opts.enabled === "boolean" ? opts.enabled : true;
51
+ const wake = typeof opts.wake === "boolean" ? opts.wake : false;
52
+ const pollMs = clampInt(opts.pollMs, 10000, 100, 600000);
53
+ const spawnOnSend = typeof opts.spawnOnSend === "boolean" ? opts.spawnOnSend : false;
54
+ const spawnCooldownMs = nonNegativeInt(opts.spawnCooldownMs, 60000);
55
+ const maxSpawnsPerTurn = clampInt(opts.maxSpawnsPerTurn, 1, 0, 10);
56
+ const projectDirs = {};
57
+ if (opts.projectDirs && typeof opts.projectDirs === "object" && !Array.isArray(opts.projectDirs)) {
58
+ for (const [key, value] of Object.entries(opts.projectDirs)) {
59
+ if (typeof value === "string" && value.length > 0 && isSafeRecipient(key)) {
60
+ projectDirs[key] = path.resolve(value);
61
+ }
62
+ }
63
+ }
64
+ const rawStateFile = typeof opts.stateFile === "string" && opts.stateFile.length > 0
65
+ ? opts.stateFile
66
+ : path.join(directory, ".atl", "mailbox-state.json");
67
+ const stateFile = assertInside(directory, rawStateFile, "stateFile");
68
+ return {
69
+ mailboxDir,
70
+ project,
71
+ explicitMailboxDir: configuredMailboxDir !== null,
72
+ storage,
73
+ maxAutoRounds,
74
+ cooldownMs,
75
+ enabled,
76
+ stateFile,
77
+ wake,
78
+ pollMs,
79
+ spawnOnSend,
80
+ spawnCooldownMs,
81
+ maxSpawnsPerTurn,
82
+ projectDirs,
83
+ };
84
+ }
85
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ export type Tier = "meta" | "account" | "repo" | "custom";
2
+ export type TierContext = {
3
+ tier: Tier;
4
+ /** Mailbox identity inside its own tier: "workspace" for a tier root, else the directory name. */
5
+ project: string;
6
+ /** Directory holding this tier's files: `<tierRoot>/handoffs`. */
7
+ mailboxDir: string;
8
+ /** Session directory the context was derived from. */
9
+ directory: string;
10
+ /** The account directory (account or repo tier). */
11
+ accountRoot?: string;
12
+ accountName?: string;
13
+ /** The workspace root directory (meta tier, or the ancestor above an account). */
14
+ rootDir?: string;
15
+ /** The root tier's handoffs directory, when one exists above this tier. */
16
+ rootMailboxDir?: string;
17
+ };
18
+ /**
19
+ * Derive the tier from the folder tree:
20
+ *
21
+ * - a directory with its own `handoffs/` is a tier root: meta when nothing
22
+ * above it has one, account when the parent does;
23
+ * - anything else belongs to the nearest ancestor that has one (repo tier),
24
+ * whatever the bucket depth (`<account>/tools/<repo>` still resolves);
25
+ * - an explicit `mailboxDir` in config overrides discovery entirely.
26
+ */
27
+ export declare function discoverTier(directory: string, explicit?: {
28
+ mailboxDir?: string;
29
+ project?: string;
30
+ }): TierContext;
31
+ export type Route = {
32
+ mailboxDir: string;
33
+ project: string;
34
+ };
35
+ /**
36
+ * Where a message addressed to `to` belongs.
37
+ * - `workspace` -> this tier's own inbox (the tier root's `workspace.md`)
38
+ * - an account name -> the root tier's `<account>.md` (escalate up / lateral between accounts)
39
+ * - anything else -> a sibling/child file in this tier (lateral / delegate down)
40
+ */
41
+ export declare function routeRecipient(to: string, ctx: TierContext): Route;
@@ -0,0 +1,101 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ function kebab(value) {
4
+ return value
5
+ .toLowerCase()
6
+ .replace(/[^a-z0-9]+/g, "-")
7
+ .replace(/^-+|-+$/g, "");
8
+ }
9
+ function hasHandoffs(dir) {
10
+ return existsSync(path.join(dir, "handoffs", "workspace.md")) || existsSync(path.join(dir, "handoffs"));
11
+ }
12
+ /**
13
+ * Derive the tier from the folder tree:
14
+ *
15
+ * - a directory with its own `handoffs/` is a tier root: meta when nothing
16
+ * above it has one, account when the parent does;
17
+ * - anything else belongs to the nearest ancestor that has one (repo tier),
18
+ * whatever the bucket depth (`<account>/tools/<repo>` still resolves);
19
+ * - an explicit `mailboxDir` in config overrides discovery entirely.
20
+ */
21
+ export function discoverTier(directory, explicit) {
22
+ const dir = path.resolve(directory);
23
+ if (explicit?.mailboxDir) {
24
+ const mailboxDir = path.resolve(explicit.mailboxDir);
25
+ const inside = isStrictlyInside(dir, mailboxDir);
26
+ return {
27
+ tier: "custom",
28
+ project: explicit.project ?? (inside ? "workspace" : kebab(path.basename(dir))),
29
+ mailboxDir,
30
+ directory: dir,
31
+ rootDir: inside ? path.dirname(mailboxDir) : undefined,
32
+ rootMailboxDir: inside ? mailboxDir : undefined,
33
+ };
34
+ }
35
+ if (hasHandoffs(dir)) {
36
+ const parent = path.dirname(dir);
37
+ const above = parent !== dir && hasHandoffs(parent);
38
+ return {
39
+ tier: above ? "account" : "meta",
40
+ project: "workspace",
41
+ mailboxDir: path.join(dir, "handoffs"),
42
+ directory: dir,
43
+ accountRoot: above ? dir : undefined,
44
+ accountName: above ? kebab(path.basename(dir)) : undefined,
45
+ rootDir: above ? parent : dir,
46
+ rootMailboxDir: path.join(above ? parent : dir, "handoffs"),
47
+ };
48
+ }
49
+ let cursor = dir;
50
+ for (;;) {
51
+ const parent = path.dirname(cursor);
52
+ if (parent === cursor)
53
+ break;
54
+ if (hasHandoffs(parent)) {
55
+ const grand = path.dirname(parent);
56
+ return {
57
+ tier: "repo",
58
+ project: kebab(path.basename(dir)),
59
+ mailboxDir: path.join(parent, "handoffs"),
60
+ directory: dir,
61
+ accountRoot: parent,
62
+ accountName: kebab(path.basename(parent)),
63
+ rootDir: grand,
64
+ rootMailboxDir: hasHandoffs(grand) ? path.join(grand, "handoffs") : undefined,
65
+ };
66
+ }
67
+ cursor = parent;
68
+ }
69
+ return {
70
+ tier: "repo",
71
+ project: kebab(path.basename(dir)),
72
+ mailboxDir: path.join(dir, "handoffs"),
73
+ directory: dir,
74
+ };
75
+ }
76
+ /**
77
+ * Where a message addressed to `to` belongs.
78
+ * - `workspace` -> this tier's own inbox (the tier root's `workspace.md`)
79
+ * - an account name -> the root tier's `<account>.md` (escalate up / lateral between accounts)
80
+ * - anything else -> a sibling/child file in this tier (lateral / delegate down)
81
+ */
82
+ export function routeRecipient(to, ctx) {
83
+ if (to === "workspace")
84
+ return { mailboxDir: ctx.mailboxDir, project: "workspace" };
85
+ if (to === ctx.project)
86
+ return { mailboxDir: ctx.mailboxDir, project: ctx.project };
87
+ if (ctx.tier !== "meta" && ctx.rootMailboxDir) {
88
+ const ownAccount = ctx.accountName !== undefined && to === ctx.accountName;
89
+ const siblingAccount = existsSync(path.join(ctx.rootMailboxDir, `${to}.md`)) ||
90
+ (ctx.rootDir !== undefined && hasHandoffs(path.join(ctx.rootDir, to)));
91
+ if (ownAccount || siblingAccount) {
92
+ return { mailboxDir: ctx.rootMailboxDir, project: to };
93
+ }
94
+ }
95
+ return { mailboxDir: ctx.mailboxDir, project: to };
96
+ }
97
+ function isStrictlyInside(base, candidate) {
98
+ const rel = path.relative(path.resolve(base), path.resolve(candidate));
99
+ return rel !== "" && !path.isAbsolute(rel) && rel !== ".." && !rel.startsWith(".." + path.sep);
100
+ }
101
+ //# sourceMappingURL=discovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discovery.js","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"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"}
package/dist/hash.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export declare function normalizeForHash(text: string): string;
2
+ export declare function messageHash(parts: {
3
+ header: string;
4
+ topic: string;
5
+ body: string;
6
+ }): string;
package/dist/hash.js ADDED
@@ -0,0 +1,11 @@
1
+ import { createHash } from "node:crypto";
2
+ export function normalizeForHash(text) {
3
+ return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim();
4
+ }
5
+ export function messageHash(parts) {
6
+ const header = normalizeForHash(parts.header);
7
+ const topic = normalizeForHash(parts.topic);
8
+ const body = normalizeForHash(parts.body);
9
+ return createHash("sha256").update(`${header}\n${topic}\n${body}`).digest("hex").slice(0, 16);
10
+ }
11
+ //# sourceMappingURL=hash.js.map