@webappwiz/arbor 0.0.1

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 Jared Johnson
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 ADDED
@@ -0,0 +1,297 @@
1
+ # @webappwiz/arbor
2
+
3
+ Runs several AI coding agents on one repository at once, each in its own git
4
+ worktree, landing on `main` without pull requests.
5
+
6
+ ```bash
7
+ bunx @webappwiz/arbor <command>
8
+ ```
9
+
10
+ Each agent drives its own landing. It works in a worktree, then calls
11
+ `arbor merge` to get that work onto trunk. If merging fails, the failure comes
12
+ back to that same agent, in the same conversation, which fixes it and calls
13
+ `merge` again. There is no daemon, no queue, no orchestrator.
14
+
15
+ Two rules make that safe:
16
+
17
+ 1. **arbor never spawns an agent.** Agents exist because a human opened a
18
+ thread. arbor is a set of deterministic verbs over git and disk: no LLM
19
+ calls, no knowledge of who is running.
20
+ 2. **Agents never run raw git for these operations.** Every state transition
21
+ goes through an arbor command. Exit codes and stderr are the interface the
22
+ agent reasons about.
23
+
24
+ Conflicts between agents are expected, not a process failure. Discarding a task
25
+ and redoing it against current trunk is cheap and often better than a hard
26
+ rebase, and that is what `rm` is for.
27
+
28
+ ## Commands
29
+
30
+ ### `arbor add <task>`
31
+
32
+ Creates the task: branch `task/<task>`, a worktree at
33
+ `../<repo>-arbor/<task>`, and a state record.
34
+
35
+ A fresh worktree shares no untracked files with the repo (no `node_modules`,
36
+ no `.env`) which is what `postCheckout` is for.
37
+
38
+ If the hook fails the worktree stays; fix it and re-run the hook by hand.
39
+
40
+ Refuses a repo with submodules (`usage`). A worktree gets its own empty
41
+ submodule directories, so every task would have to bootstrap them before
42
+ anything builds, and arbor would rather say so than hand back a tree that does
43
+ not work.
44
+
45
+ ### `arbor claim <task>`
46
+
47
+ Takes ownership of an existing worktree. **This is the resume entry point**: a
48
+ fresh agent thread picking up dead work starts here.
49
+
50
+ Prints the worktree path, status, uncommitted changes, and, loudly, any
51
+ half-finished rebase or merge the tree is standing in. Refuses if another agent
52
+ holds the lease. A worktree with no record is rebuilt rather than rejected.
53
+
54
+ ### `arbor merge`
55
+
56
+ Lands the current worktree's branch on trunk. The core command.
57
+
58
+ **Never a merge commit.** It rebases onto trunk, runs the `preMerge` gate
59
+ there, and fast-forwards trunk with `git merge --ff-only`. History stays
60
+ linear.
61
+
62
+ 1. Refuses if the worktree is dirty, out of retry budget, or leased elsewhere.
63
+ 2. Takes the merge lock, **blocking**, polling every 2s. Blocking is
64
+ deliberate: telling an agent "busy, try later" invites it to go edit more
65
+ code in a branch that is supposed to be frozen.
66
+ 3. `git rebase <trunk>`, then the `preMerge` gate, **in that order**. A branch that
67
+ passed before rebasing says nothing about whether it works against current
68
+ trunk; this is the only defense against semantic conflicts, where both sides
69
+ merge cleanly and the combination is broken.
70
+ 4. Re-checks the lease, then `git checkout <trunk> && git merge --ff-only`.
71
+ 5. Discards the task (worktree, branch and record) exactly as `rm`
72
+ would. The work is on trunk, so the tree has nothing left to hold, and
73
+ `arbor ls` stays a list of live work rather than a graveyard of landed
74
+ tasks. The agent's own directory goes with it, so the success message
75
+ prints the main tree to `cd` back to.
76
+
77
+ On conflict the rebase is **left in progress**: the agent needs the markers.
78
+ Resolve, `git add`, `git rebase --continue`, `arbor merge` again. When the gate
79
+ fails the branch is reset to where it was and trunk is never touched.
80
+
81
+ There is deliberately no flag to skip the gate: a repo that wants none
82
+ configures none.
83
+
84
+ ### `arbor rm <task>`
85
+
86
+ Discards a task: `git worktree remove` plus the branch and the record.
87
+
88
+ For abandoning work that will never land. A successful `merge` already
89
+ discards its own tree. Use it freely. Warns about commits that never landed,
90
+ but never blocks: throwing work away is the cheap escape hatch, not a last
91
+ resort.
92
+
93
+ Removal leaves a tombstone in `.git/arbor/removed/` so a second `rm` can say
94
+ `already_removed` rather than `not_found`. The ledger keeps the 50 most recent
95
+ and drops the oldest as new ones arrive, so a long-forgotten task reports
96
+ `not_found` again.
97
+
98
+ ### `arbor ls [--json]`
99
+
100
+ Every task: name, status, lease (`held`/`stale`/`none`), commits ahead of
101
+ trunk, age. A corrupt record shows as `unknown` instead of taking
102
+ down the listing; a record whose worktree vanished shows as `orphaned`.
103
+
104
+ ### `arbor show <task> [--json]`
105
+
106
+ One task in full: the row `ls` would print for it, plus the `ARBOR.md`
107
+ its agent keeps at the worktree root and the reason behind an `escalated`
108
+ status.
109
+
110
+ ```
111
+ alpha working
112
+ branch: task/alpha
113
+ worktree: /src/repo-arbor/alpha
114
+ lease: held
115
+ ahead: 3 +82 -14
116
+ age: 2h
117
+
118
+ ARBOR.md
119
+ # alpha
120
+ ...
121
+ ```
122
+
123
+ `ls` says a task exists; this says what it is doing. Like `path`, it takes no
124
+ lease, so reading another agent's tree cannot knock it off its own work the way
125
+ `claim` would. A task with no `ARBOR.md` is called out rather than passed over
126
+ in silence: it is the one thing that makes the work resumable.
127
+
128
+ A `ARBOR.md` that is there gets checked against the shape the agent skill
129
+ prescribes (`# <task>`, `## Goal`, `## Next` with something unchecked in it, a
130
+ `## Blocked` question once escalated), and anything off is printed under it.
131
+ Warnings only, never a refusal: the agent that wrote the file is the one that
132
+ runs `show` on it, and a rough plan still beats none.
133
+
134
+ ### `arbor log [--count 20] [--json]`
135
+
136
+ The last N things done here (`add`, `claim`, `merge`, `rm`, `escalate`,
137
+ `retry`),
138
+ oldest first, each with the task and how it ended (`ok`, or the refusal reason).
139
+
140
+ ```
141
+ WHEN ACTION TASK RESULT
142
+ 2h add alpha ok
143
+ 1h merge alpha tests_failed
144
+ 1h merge alpha ok
145
+ ```
146
+
147
+ `ls` is what still exists; this is what happened. Entries outlive their tasks:
148
+ a successful `merge` and a `rm` both take the record with them, so this is
149
+ the only thing that remembers a task landed at all. The last 200 are kept
150
+ (`logCapacity`) in `.git/arbor/log.jsonl`.
151
+
152
+ ### `arbor dev [--port 4269]`
153
+
154
+ `ls`, `show` and `log` in a browser, on `http://localhost:4269`, reloading
155
+ themselves as tasks change. Read-only, and takes no lease.
156
+
157
+ ### `arbor path [task]`
158
+
159
+ Prints one path and nothing else, so it composes:
160
+
161
+ ```bash
162
+ cd "$(arbor path)" # back to the main tree, from any worktree
163
+ zed -a "$(arbor path alpha)" # read a task's work beside your own
164
+ git -C "$(arbor path alpha)" diff main...task/alpha
165
+ ```
166
+
167
+ **This is how a human looks at an agent's work.** Moving between trees is `cd`
168
+ and nothing else. Worktrees are directories, not checkouts, so your main tree
169
+ stays on trunk while agents work and there is no branch to switch, nothing to
170
+ stash, nothing to switch back. Reading a task this way takes no lease, so it
171
+ cannot knock the agent driving it off its own tree the way `claim` would.
172
+
173
+ With no task it prints the main tree, which is the one path a process standing
174
+ in a worktree cannot otherwise name: git's `--show-toplevel` hands back the
175
+ worktree it is already in.
176
+
177
+ Refuses a task that does not exist, or one whose directory is gone, rather than
178
+ printing a path you cannot `cd` into.
179
+
180
+ ### `arbor escalate <reason> [--task <name>]`
181
+
182
+ The explicit "this needs a human" exit. Records the reason, drops the lease, and
183
+ leaves the worktree **exactly** as it is so the human sees what the agent saw.
184
+
185
+ This exists so an agent has a way out that is not "resolve the conflict badly to
186
+ finish the task". Agents are reliable at mechanical conflicts (both sides added
187
+ imports, a signature changed on one side and its callers on the other) and
188
+ unreliable when both sides restructured the same logic, because then there is no
189
+ correct merge, only a decision.
190
+
191
+ ### `arbor retry <task>`
192
+
193
+ Grants an escalated task another `mergeRetryCount` merge attempts and puts it
194
+ back to `working`. The way out of `budget_exhausted` that is not `rm` and redo,
195
+ for the case where the task was one fix away rather than genuinely lost.
196
+
197
+ Only from `escalated`, and that is the whole design. The budget exists to make
198
+ an agent stop and hand the task over; an agent that could grant itself more
199
+ attempts would be back to grinding against a moving trunk forever. So the price
200
+ of a fresh budget is that a human has looked at the tree first.
201
+
202
+ It takes no lease: whoever picks the task up runs `arbor claim` as usual.
203
+
204
+ ## Exit codes
205
+
206
+ The agent's control flow runs on these.
207
+
208
+ | Code | Reason | Meaning and what to do |
209
+ | ---- | ------------------- | ----------------------------------------------------------------- |
210
+ | 0 | none | Success. |
211
+ | 1 | `usage` | Bad task name, bad flags, a repo arbor does not support, or an unexpected git failure. |
212
+ | 2 | `conflict` | Rebase conflicted. **Rebase is still in progress.** Resolve, `git add`, `git rebase --continue`, merge again. |
213
+ | 3 | `tests_failed` | The gate (`postRewrite`, `preMerge`) failed after the rebase. Branch rolled back, trunk untouched. Fix and merge again. |
214
+ | 4 | `lease_lost` | Another agent took the tree mid-merge. **Stop. Do not retry.** |
215
+ | 5 | `budget_exhausted` | Out of merge attempts. `arbor escalate`, and a human can grant another budget with `arbor retry`; or `arbor rm` and redo against current trunk. |
216
+ | 6 | `lease_held` | Another agent is driving this tree. |
217
+ | 7 | `dirty` | Uncommitted changes. Commit before merging. |
218
+ | 8 | `not_found` | No such task, or not run from a task worktree. |
219
+ | 9 | `hook_failed` | `postCheckout` failed. The worktree still exists; fix and re-run the hook. |
220
+ | 10 | `exists` | Task already exists. `arbor claim` it, or `arbor rm` first. |
221
+ | 11 | `orphaned` | Record with no worktree. `arbor rm` it. |
222
+ | 12 | `merge_failed` | Trunk could not be fast-forwarded (usually a dirty main worktree). |
223
+ | 13 | `already_removed` | This task was removed earlier; nothing left to remove. |
224
+
225
+ Every failure prints a one-line JSON object on **stdout** (`{"reason": ...}`,
226
+ plus fields like `paths` for conflicts) and the human explanation on **stderr**.
227
+
228
+ ## Configuration
229
+
230
+ `arbor.config.ts` at the repo root, all keys optional. `defineConfig` is an
231
+ identity function that exists for the types:
232
+
233
+ ```ts
234
+ import { defineConfig } from "@webappwiz/arbor/config";
235
+
236
+ export default defineConfig({
237
+ trunk: "main",
238
+ worktreeRoot: "../myrepo-arbor",
239
+ postCheckout: "bun install && cp ../../myrepo/.env .env",
240
+ postRewrite: "bun install", // after each rebase, before preMerge
241
+ preMerge: "bun test", // the last gate before the branch lands
242
+ leaseStalenessMs: 90_000,
243
+ mergeRetryCount: 2,
244
+ removedCapacity: 50, // removed names kept, so rm can say "already removed"
245
+ logCapacity: 200, // entries `arbor log` keeps before the oldest fall off
246
+ });
247
+ ```
248
+
249
+ The hooks are named for the git events they sit around: `postCheckout` runs
250
+ once, when `add` checks the worktree out; `postRewrite` runs after every rebase
251
+ `merge` does; `preMerge` runs after that, and is the last thing between the
252
+ branch and the base. They all run through `sh -c` in the worktree with
253
+ `ARBOR_TASK` and `ARBOR_WORKTREE` in the environment.
254
+
255
+ Every hook is unset by default: arbor has no opinion about what a repo runs, or
256
+ whether it has tests at all. Configure `preMerge` and a nonzero exit rolls the
257
+ branch back and spends an attempt, leaving the base untouched; `postRewrite`
258
+ fails the same gate the same way. Configure neither and a task lands on a clean
259
+ rebase alone.
260
+
261
+ arbor does not allocate ports. Several worktrees running at once will collide on
262
+ whatever they bind, and the thing that binds (docker-compose, a dev server, a
263
+ test harness) is the only thing able to retry and release. `ARBOR_TASK` is in
264
+ the environment to derive a stable port from if a task needs one.
265
+
266
+ ### Leases and locks
267
+
268
+ State lives in `.git/arbor/`, shared by every worktree, never tracked by git.
269
+ Records are written to a temp file and `rename()`d into place, so a concurrent
270
+ reader never sees half a file. The merge lock is `mkdir` on
271
+ `.git/arbor/merge.lock`: atomic everywhere, no dependencies, and it either
272
+ succeeds or fails with no check-then-write window. A holder that dies is
273
+ detected (dead pid, or a timestamp past `leaseStalenessMs`) and its lock is
274
+ stolen, loudly.
275
+
276
+ A lease is **held** when its heartbeat is fresh *and*, for a holder on this
277
+ host, its pid still exists. The pid check matters because every arbor command is
278
+ its own short-lived process: without it, a tree would stay locked for the whole
279
+ staleness window after a command that merely finished, and `add` would block
280
+ the `merge` that follows it.
281
+
282
+ ### `git rerere`
283
+
284
+ Adding `git config rerere.enabled true` to `postCheckout` is worth it. The cache
285
+ lives in `.git/rr-cache`, which every worktree shares, verified against two
286
+ real worktrees: a conflict resolved in one is replayed automatically in the
287
+ other. Git still leaves the file staged as `UU`, so the agent must confirm with
288
+ `git add` and `git rebase --continue`. Not enabled by default; opt in per repo.
289
+
290
+ ## Retry budget
291
+
292
+ `mergeAttempts` counts conflicts, failed test runs, and failed fast-forwards. It
293
+ exists because of a real livelock: an agent rebases onto trunk, another agent
294
+ lands during its test run, and it is stale again before it finishes. Under load
295
+ an unlucky agent can chase a moving trunk indefinitely. When the budget is gone,
296
+ escalate or `arbor rm`: redoing the task against current trunk usually beats
297
+ retrofitting a rebase.
package/add.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { type Logger } from "webappwiz/log";
2
+ import type { Fs } from "webappwiz/system";
3
+ import type { Config } from "./config.js";
4
+ import type { Shell } from "./shell.js";
5
+ import type { WorktreeService } from "./worktree-service.js";
6
+ export interface AddOptions {
7
+ /** Branch the task starts from and merges onto. Defaults to the trunk. */
8
+ base?: string;
9
+ }
10
+ export declare function add({ service, shell, config, log, fs, }: {
11
+ service: WorktreeService;
12
+ shell: Shell;
13
+ config: Config;
14
+ log: Logger;
15
+ fs: Fs;
16
+ }, task: string, { base }?: AddOptions): Promise<void>;
package/age.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ /** How long ago an ISO timestamp was, at the coarsest unit that still reads. */
2
+ export declare function age(since: string): string;
package/arbor.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { type Deps } from "webappwiz/cmd";
2
+ import type { HttpServer } from "webappwiz/http";
3
+ import type { Fs } from "webappwiz/system";
4
+ import type { Assets } from "./dev/assets.js";
5
+ import { type Repository } from "./repository.js";
6
+ /** Everything `arbor` is run with, before the repository middleware adds to it. */
7
+ export interface ArborDeps extends Deps {
8
+ fs: Fs;
9
+ /** Only `dev` listens, but the runtime is picked once, where arbor starts. */
10
+ http: HttpServer;
11
+ /** Likewise: only `dev` serves the page, and it is built before it ships. */
12
+ assets: Assets;
13
+ }
14
+ export declare const arbor: import("webappwiz/cmd").Cli<ArborDeps, ArborDeps & Repository>;
package/claim.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { type Logger } from "webappwiz/log";
2
+ import type { WorktreeService } from "./worktree-service.js";
3
+ /** The resume entry point: a fresh agent thread picking up existing work. */
4
+ export declare function claim({ service, log }: {
5
+ service: WorktreeService;
6
+ log: Logger;
7
+ }, task: string): Promise<void>;
package/config.d.ts ADDED
@@ -0,0 +1,43 @@
1
+ export interface Config {
2
+ /**
3
+ * The integration branch name. `merge` rebases a task onto this branch and
4
+ * then fast-forwards it to the result; new worktrees start from it.
5
+ */
6
+ trunk: string;
7
+ /** Directory holding one worktree per task, a sibling of the repo. */
8
+ worktreeRoot: string;
9
+ /** Command run by `add` in the new worktree, via `sh -c`. */
10
+ postCheckout: string | null;
11
+ /**
12
+ * Command run by `merge` after the rebase, before `preMerge`, via `sh -c`.
13
+ * A rebase can bring in a dependency the worktree has never installed, and
14
+ * whatever `preMerge` runs needs it.
15
+ */
16
+ postRewrite: string | null;
17
+ /**
18
+ * Command run by `merge` after the rebase, via `sh -c`. The last gate before
19
+ * the branch lands: a nonzero exit rolls the branch back and leaves the base
20
+ * untouched. Tests are the obvious thing to put here, but arbor has no
21
+ * opinion; a repo with nothing to run leaves it null and merges on a green
22
+ * rebase alone.
23
+ */
24
+ preMerge: string | null;
25
+ /** How long since its last heartbeat before a task's lease is up for grabs. */
26
+ leaseStalenessMs: number;
27
+ /** Failed `merge` attempts a task gets before it must escalate or be removed. */
28
+ mergeRetryCount: number;
29
+ /**
30
+ * How many removed task names to keep, so `rm` can say "already removed"
31
+ * rather than "never existed". A flat cap with no age policy: losing the
32
+ * oldest costs a nicer message and nothing else.
33
+ */
34
+ removedCapacity: number;
35
+ /** How many entries `arbor log` keeps before the oldest fall off. */
36
+ logCapacity: number;
37
+ }
38
+ /**
39
+ * Identity, for the types. `export default defineConfig({ ... })` in
40
+ * `arbor.config.ts` gets the key names and their types checked, and completion
41
+ * while writing it; a bare object literal gets neither.
42
+ */
43
+ export declare function defineConfig(config: Partial<Config>): Partial<Config>;
package/config.js ADDED
@@ -0,0 +1,7 @@
1
+ // config.ts
2
+ function defineConfig(config) {
3
+ return config;
4
+ }
5
+ export {
6
+ defineConfig
7
+ };
@@ -0,0 +1,17 @@
1
+ /** The three files the browser asks for, as the server hands them over. */
2
+ export interface Assets {
3
+ /** The page, which asks for the other two. */
4
+ shell: string;
5
+ /** The React app, bundled for the browser. */
6
+ script: string;
7
+ /** The stylesheet, with Tailwind already compiled. */
8
+ styles: string;
9
+ }
10
+ /**
11
+ * The page this package ships, built by `build.ts` and imported here as text so
12
+ * it travels inside the bundle a release publishes. Serving it costs a string
13
+ * lookup, and nothing is read off disk: a published package has no `dev/`
14
+ * directory to read from, and the CLI's own dependencies stay clear of React
15
+ * and Tailwind because neither is needed once the page is built.
16
+ */
17
+ export declare const assets: Assets;
package/dev.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ import type { HttpServer } from "webappwiz/http";
2
+ import type { Logger } from "webappwiz/log";
3
+ import type { Fs } from "webappwiz/system";
4
+ import type { Assets } from "./dev/assets.js";
5
+ import type { Journal } from "./journal.js";
6
+ import type { WorktreeService } from "./worktree-service.js";
7
+ export declare const DEFAULT_PORT = 4269;
8
+ /** A running server, and the one thing a caller ever wants to do with it. */
9
+ export interface DevServer {
10
+ port: number;
11
+ stop(): Promise<void>;
12
+ }
13
+ /**
14
+ * Serves what `ls`, `show` and `log` print, as one page that refetches when the
15
+ * repo changes. Read-only on purpose: driving arbor is what the CLI is for, and
16
+ * a button that took a lease would fight the agent holding it.
17
+ *
18
+ * The page itself is a React app under `dev/`, built before publishing and
19
+ * carried in the bundle, so nothing here builds markup and nothing reads it
20
+ * off disk.
21
+ */
22
+ export declare function dev({ service, fs, journal, log, http, assets, }: {
23
+ service: WorktreeService;
24
+ fs: Fs;
25
+ journal: Journal;
26
+ log: Logger;
27
+ http: HttpServer;
28
+ assets: Assets;
29
+ }, { port }?: {
30
+ port?: number | undefined;
31
+ }): Promise<DevServer>;
package/escalate.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { type Logger } from "webappwiz/log";
2
+ import type { Lock } from "webappwiz/system";
3
+ import type { Git } from "./git.js";
4
+ import type { WorktreeService } from "./worktree-service.js";
5
+ /**
6
+ * The way out that is not "resolve the conflict badly to finish the task".
7
+ * When both sides restructured the same logic there is no correct merge, only
8
+ * a decision, and that belongs to a human.
9
+ */
10
+ export declare function escalate({ service, git, lock, log, }: {
11
+ service: WorktreeService;
12
+ git: Git;
13
+ lock: Lock;
14
+ log: Logger;
15
+ }, reason: string, cwd: string, task?: string): Promise<void>;
package/exit.d.ts ADDED
@@ -0,0 +1,40 @@
1
+ import type { Deps, Middleware } from "webappwiz/cmd";
2
+ /** Exit codes are the API: an agent branches on these, not on prose. */
3
+ export declare const EXIT: {
4
+ readonly usage: 1;
5
+ readonly conflict: 2;
6
+ readonly tests_failed: 3;
7
+ readonly lease_lost: 4;
8
+ readonly budget_exhausted: 5;
9
+ readonly lease_held: 6;
10
+ readonly dirty: 7;
11
+ readonly not_found: 8;
12
+ readonly hook_failed: 9;
13
+ readonly exists: 10;
14
+ readonly orphaned: 11;
15
+ readonly merge_failed: 12;
16
+ readonly already_removed: 13;
17
+ };
18
+ export type Reason = keyof typeof EXIT;
19
+ /**
20
+ * A refusal, carrying the reason to branch on, a message to show, and any
21
+ * data worth reporting with it. `exits` turns one into output and a status
22
+ * code.
23
+ */
24
+ export declare class Exit extends Error {
25
+ readonly reason: Reason;
26
+ readonly data: Record<string, unknown>;
27
+ constructor(reason: Reason, message: string, data?: Record<string, unknown>);
28
+ }
29
+ /**
30
+ * How a command refuses. Every call site means "and go no further", which is
31
+ * what the `never` return buys: the code below a `fail` is unreachable, and
32
+ * TypeScript narrows accordingly.
33
+ */
34
+ export declare function fail(reason: Reason, message: string, data?: Record<string, unknown>): never;
35
+ /**
36
+ * Decides what a refusal looks like (a machine-readable reason on stdout, a
37
+ * human explanation on stderr) and is the only place that ends the process.
38
+ * An agent branches on the first and reads the second.
39
+ */
40
+ export declare function exits<C extends Deps>(): Middleware<C>;
package/git.d.ts ADDED
@@ -0,0 +1,54 @@
1
+ import { type Fs, type Ps } from "webappwiz/system";
2
+ export interface GitResult {
3
+ code: number;
4
+ stdout: string;
5
+ stderr: string;
6
+ }
7
+ /**
8
+ * Every git call arbor makes, scoped to one repository. Agents are not allowed
9
+ * to run these themselves, since going through arbor is what makes concurrent
10
+ * work safe, so this is the only place git is spoken.
11
+ */
12
+ /** What a `Git` works through; the real ones by default. */
13
+ export interface GitOptions {
14
+ ps?: Ps;
15
+ fs?: Fs;
16
+ }
17
+ export declare class Git {
18
+ /** The main worktree, where trunk lives. */
19
+ readonly root: string;
20
+ private readonly ps;
21
+ private readonly fs;
22
+ constructor(
23
+ /** The main worktree, where trunk lives. */
24
+ root: string, opts?: GitOptions);
25
+ run(cwd: string, ...args: string[]): Promise<GitResult>;
26
+ out(cwd: string, ...args: string[]): Promise<string>;
27
+ branchExists(branch: string): Promise<boolean>;
28
+ currentBranch(cwd: string): Promise<string>;
29
+ head(cwd: string): Promise<string>;
30
+ shortHead(cwd: string): Promise<string>;
31
+ porcelain(cwd: string): Promise<string[]>;
32
+ conflictedPaths(cwd: string): Promise<string[]>;
33
+ commitsAhead(trunk: string, branch: string): Promise<number | null>;
34
+ /** Lines added and removed on `branch` since it left `trunk`. */
35
+ diffStat(trunk: string, branch: string): Promise<{
36
+ added: number;
37
+ removed: number;
38
+ } | null>;
39
+ rebase(cwd: string, onto: string): Promise<GitResult>;
40
+ resetHard(cwd: string, commit: string): Promise<GitResult>;
41
+ checkout(branch: string): Promise<GitResult>;
42
+ mergeFfOnly(branch: string): Promise<GitResult>;
43
+ addWorktree(branch: string, path: string, from: string): Promise<GitResult>;
44
+ removeWorktree(path: string): Promise<GitResult>;
45
+ /** Drops metadata for worktrees whose directories are gone. */
46
+ pruneWorktrees(): Promise<GitResult>;
47
+ deleteBranch(branch: string): Promise<GitResult>;
48
+ /** The one `.git` every worktree of this repo shares. */
49
+ commonDir(): Promise<string>;
50
+ /** The git directory belonging to a worktree, or null if it has none. */
51
+ worktreeGitDir(worktree: string): Promise<string | null>;
52
+ /** Half-finished merges and rebases a resuming agent is standing in. */
53
+ interruptedOps(worktree: string): Promise<string[]>;
54
+ }
package/index.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ export {};