@webappwiz/arbor 0.0.20 → 0.0.22

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/README.md CHANGED
@@ -27,11 +27,15 @@ rebase, and that is what `remove` is for.
27
27
 
28
28
  ## Commands
29
29
 
30
- ### `arbor add <task>`
30
+ ### `arbor add <task> [--base <branch>] [--todo <id>]`
31
31
 
32
32
  Creates the task: branch `task/<task>`, a worktree at
33
33
  `../<repo>-arbor/<task>`, and a state record.
34
34
 
35
+ `--todo <id>` takes up a todo (see `arbor todo`): its text becomes the plan's
36
+ `## Goal`, followed by the path of each file attached to it, and no other task
37
+ can take it while this one lives.
38
+
35
39
  `--base <branch>` starts the task from that branch and lands it back there
36
40
  instead of trunk. It takes another task's branch too: `--base task/<other>`
37
41
  stacks this task on that one, and the work lands in that task's worktree
@@ -56,6 +60,11 @@ Prints the worktree path, status, uncommitted changes, and, loudly, any
56
60
  half-finished rebase or merge the tree is standing in. Refuses if another agent
57
61
  holds the lease. A worktree with no record is rebuilt rather than rejected.
58
62
 
63
+ Claiming an `escalated` task puts it back to `working`: someone is on it
64
+ again, and whatever it was waiting on is theirs to act on. Its merge budget
65
+ stays as it was. Only `retry` refills that, and only from `escalated`, so a
66
+ human granting a fresh budget does it before the agent claims.
67
+
59
68
  ### `arbor merge`
60
69
 
61
70
  Lands the current worktree's branch on its base, trunk unless the task was
@@ -65,7 +74,11 @@ created with `--base`. The core command.
65
74
  there, and fast-forwards the base with `git merge --ff-only`. History stays
66
75
  linear.
67
76
 
68
- 1. Refuses if the worktree is dirty, out of retry budget, or leased elsewhere.
77
+ 1. Refuses if the worktree is dirty, out of retry budget, or leased elsewhere,
78
+ or while a person's reply waits unclaimed (`unread`: run `arbor replies`),
79
+ or while a question under `## Blocked` is unchecked (`blocked`): whether
80
+ nobody has answered it yet, or the agent has yet to act on an answer or a
81
+ follow-up and check it off.
69
82
  2. Takes the merge lock, **blocking**, polling every 2s. Blocking is
70
83
  deliberate: telling an agent "busy, try later" invites it to go edit more
71
84
  code in a branch that is supposed to be frozen.
@@ -91,6 +104,23 @@ fails the branch is reset to where it was and trunk is never touched.
91
104
  There is deliberately no flag to skip the gate: a repo that wants none
92
105
  configures none.
93
106
 
107
+ A merge onto trunk ends by recommending what to do next: one todo, the task's
108
+ own follow-ups first and then the longest waiting, plus any todo older than
109
+ `todoStalenessMs` (30 days) offered for removal instead. The todos the task
110
+ took up with `--todo` leave the list, since that work is now done.
111
+
112
+ ```
113
+ merged alpha onto main (1a2b3c4)
114
+ worktree removed, cd /src/repo
115
+
116
+ next todo 7: retry the upload when the token expires
117
+ from alpha, waiting 2h
118
+ start it: arbor add <task> --todo 7
119
+
120
+ stale todos, remove unless they still apply:
121
+ 2: try the old parser again (41d) arbor todo remove 2
122
+ ```
123
+
94
124
  ### `arbor remove <task>`
95
125
 
96
126
  Discards a task: `git worktree remove` plus the branch and the record.
@@ -100,17 +130,31 @@ discards its own tree. Use it freely. Warns about commits that never landed,
100
130
  but never blocks: throwing work away is the cheap escape hatch, not a last
101
131
  resort.
102
132
 
133
+ The todos it took up are open again, since the work they asked for was not
134
+ done.
135
+
103
136
  Removal leaves a tombstone in `.git/arbor/removed/` so a second `remove` can say
104
137
  `already_removed` rather than `not_found`. The ledger keeps the 50 most recent
105
138
  and drops the oldest as new ones arrive, so a long-forgotten task reports
106
139
  `not_found` again.
107
140
 
108
- ### `arbor list [--json]`
141
+ ### `arbor list [--json] [--files]`
109
142
 
110
143
  Every task: name, status, lease (`held`/`stale`/`none`), commits ahead of
111
144
  trunk, age. A corrupt record shows as `unknown` instead of taking
112
145
  down the listing; a record whose worktree vanished shows as `orphaned`.
113
146
 
147
+ `--files` adds, under each task, every path it has changed (committed or
148
+ not) and every path its `ARBOR.md` plans under `## Files` that it has not
149
+ touched yet. This is the overlap check an agent runs before starting: its own
150
+ list of files against everyone else's.
151
+
152
+ ```
153
+ alpha
154
+ changed src/auth.ts
155
+ planned src/session.ts
156
+ ```
157
+
114
158
  ### `arbor show <task> [--json]`
115
159
 
116
160
  One task in full: the row `list` would print for it, plus the `ARBOR.md`
@@ -142,7 +186,7 @@ and anything off is printed under it.
142
186
  Warnings only, never a refusal: the agent that wrote the file is the one that
143
187
  runs `show` on it, and a rough plan still beats none.
144
188
 
145
- ### `arbor wait <task> [--timeout-secs 300]`
189
+ ### `arbor wait <task> [--timeout-secs 300] [--answered]`
146
190
 
147
191
  Blocks until a task stops moving, then prints where it stopped.
148
192
 
@@ -159,11 +203,109 @@ driving. Wait again, work alongside it, or ask the human.
159
203
  Like `show` and `path`, it takes no lease, so watching a task cannot knock its
160
204
  agent off it.
161
205
 
206
+ `--answered` waits for something else: until every open question under the
207
+ task's `## Blocked` has a reply (or none is open), then claims everything new
208
+ the way `arbor replies` does and prints it. This is how an agent that
209
+ escalated waits for its human, with the same timeout and the same `timeout`
210
+ refusal, which names the questions still unanswered. A reply someone has open
211
+ to edit does not count yet. A task that is gone ends the wait too, since
212
+ nothing is left to answer.
213
+
214
+ ```
215
+ alpha has new
216
+ Q2 🎨 Does the header wrap to two lines?
217
+ → yes
218
+ ```
219
+
220
+ ### `arbor inbox [--replied] [--json]`
221
+
222
+ Every question waiting on a person, across all tasks: each unchecked
223
+ `- [ ] Q9.` item under a task's `## Blocked` with no reply yet, grouped by
224
+ task. A question leaves the inbox once it is answered. `--replied` brings back
225
+ the ones answered but not yet checked off by their agent, with the reply under
226
+ each, follow-ups too: one still waiting for its agent says so, and can be
227
+ changed on the page until the agent claims it. Takes no lease.
228
+
229
+ ```
230
+ alpha
231
+ Q3 🧹 Keep or drop the old flag?
232
+ It has been off since March.
233
+
234
+ beta (in a live session: answer it there)
235
+ Q1 🗄️ When should the migration run?
236
+ (a) Now
237
+ (b) After the backfill
238
+
239
+ 1 replied, not yet acted on: arbor inbox --replied
240
+ ```
241
+
242
+ A question's line is its subject. Lines indented under it are its body,
243
+ markdown with code blocks and images (`![shot](/abs/path.png)`, which the page
244
+ shows inline). Choices come last in the body: `- (a) ...` lines take one or
245
+ none, `- [a] ...` lines take any that apply.
246
+
247
+ ```markdown
248
+ - [ ] Q3. 🔐 How should existing sessions move to the new tokens?
249
+ Sessions are keyed by the old cookie.
250
+ ![login screen](/abs/path/login.png)
251
+ - (a) Sign everyone out once
252
+ - (b) Migrate each session on its next request
253
+ - [ ] Q4. 🔔 Where should failures notify?
254
+ - [a] Email
255
+ - [b] Slack
256
+ - [c] Push
257
+ ```
258
+
259
+ ### `arbor replies [task]`
260
+
261
+ How an agent reads what its human answered, and the only way it does. A
262
+ person answers from the page (`arbor dev`), never the CLI, so agents have no
263
+ way to answer each other: every answer an agent reads through arbor came from
264
+ a person. For anything no question asked, the person tells the agent in its
265
+ chat.
266
+
267
+ Claiming takes every reply waiting for the task, writes each into `ARBOR.md`,
268
+ and prints them with the questions still unanswered. The first answer to a
269
+ question goes on its line after ` → `, its picks spelled out so the line alone
270
+ says what was picked: ` → b (Migrate each session on its next request)`, or
271
+ ` → a (Email), c (Push): and log it` with words after the picks. Once the
272
+ agent has read an answer, the person can follow it up; a follow-up goes on a
273
+ line of its own under the question, and unchecks it, whatever the agent did
274
+ about the answer before:
275
+
276
+ ```markdown
277
+ - [ ] Q3. 🔐 How should existing sessions move to the new tokens? → b (Migrate each session on its next request)
278
+ Sessions are keyed by the old cookie.
279
+ → Sign out the admins, though.
280
+ ```
281
+
282
+ The box stays unchecked: checking it off is the agent's word that it has
283
+ acted on everything under it. Once claimed, a reply can no longer change or be
284
+ taken back, so the agent never acts on words that change under it. A reply the
285
+ person has open to edit on the page is left for the next call. Run from a
286
+ task worktree, the task is that one.
287
+
288
+ ```
289
+ alpha replied
290
+ Q3 🧹 Keep or drop the old flag?
291
+ → drop
292
+ unanswered: Q4
293
+ ```
294
+
295
+ Until claimed, a reply waits in `.git/arbor/replies/<task>/Q3.json`, its files
296
+ beside it in `Q3/`, and the claimed line names each file by absolute path so
297
+ the agent can open it from its own tree. They go when the task is merged or
298
+ removed.
299
+
300
+ A reply is refused (`lease_held`) while the task's agent is in a live
301
+ session: it is waiting in its chat, not reading its plan, so answer it there.
302
+
162
303
  ### `arbor log [--count 20] [--json]`
163
304
 
164
305
  The last N things done here (`add`, `claim`, `merge`, `remove`, `escalate`,
165
- `retry`),
166
- oldest first, each with the task and how it ended (`ok`, or the refusal reason).
306
+ `retry`, `replies`, `todo add`, `todo update`, `todo remove`, and from the
307
+ page `reply`, `withdraw`, `defer`, `skip` and `approve`), oldest first, each with the task and how it ended (`ok`, or
308
+ the refusal reason).
167
309
 
168
310
  ```
169
311
  WHEN ACTION TASK RESULT
@@ -174,13 +316,71 @@ WHEN ACTION TASK RESULT
174
316
 
175
317
  `list` is what still exists; this is what happened. Entries outlive their tasks:
176
318
  a successful `merge` and a `remove` both take the record with them, so this is
177
- the only thing that remembers a task landed at all. The last 200 are kept
319
+ the only thing that remembers a task landed at all. The last 1000 are kept
178
320
  (`logCapacity`) in `.git/arbor/log.jsonl`.
179
321
 
180
- ### `arbor dev [--port 4269]`
322
+ ### `arbor dev [--port 4269] [--allow-hosts <names>]`
323
+
324
+ The inbox, what you sent, the todos, and the tasks in a browser, on
325
+ `http://localhost:4269`, reloading themselves as anything changes. Built for a
326
+ phone first. The inbox holds only what waits on you: each unanswered question,
327
+ grouped by task, a task only when it has one. A task whose agent is in a live
328
+ session is left out, since that agent is answered in its chat. With a keyboard,
329
+ J opens the next question, and a hint under the list says so when the page
330
+ sees a mouse or trackpad.
331
+
332
+ Tapping a question opens it with its body, images full size on a tap, its
333
+ choices, a View button for its task, and a reply box that takes pasted or
334
+ picked files. Beside the box, **Defer** makes the question a todo, its images
335
+ carried along, and answers it "Deferred to todo 7: leave it out of this
336
+ task."; **Skip** answers "Skip this: go ahead without it.". A task escalated
337
+ with `arbor escalate --review` asks `✅ Ready to merge?` like any other
338
+ question, with **Approve** ("Approved: merge it.") beside a box to request
339
+ changes.
340
+
341
+ Once answered, a question moves to Sent, one line each with where it stands
342
+ in a word: Waiting (for its agent, still yours to change or withdraw),
343
+ Editing, Read (by its agent), or Done (checked off). Opening one still
344
+ waiting holds it, so its agent cannot claim it half-changed, and closing it
345
+ lets go; the hold also lapses on its own after five minutes. One its agent
346
+ has read shows what was said so far and a box to follow it up. Each stays
347
+ until its task lands or goes.
348
+
349
+ A line across the top always says something, so nothing under it moves: the
350
+ question you last replied to while its agent has yet to read it, with
351
+ Withdraw, or else how many questions need you and how many replies wait for
352
+ their agents.
353
+
354
+ Tasks lists every task with its progress through its plan, flagging only an
355
+ escalated or broken status; tapping one opens its details and whole plan.
356
+
357
+ Todos open to reword, attach files to, or remove. Typing `@` in a reply or a
358
+ todo offers the files and directories in the task's tree (the main tree's for
359
+ a todo), tracked or new but not ignored, and writes the one picked as
360
+ `@path/from/root`; picking a directory keeps the list open on what is inside.
361
+ On a phone the tabs sit along the bottom, in reach of a thumb; on anything
362
+ wider they run down a sidebar. If the server stops answering, the header says
363
+ it is offline, since what the page shows may be stale.
364
+
365
+ Answering is done here and nowhere else, so an agent with a shell cannot
366
+ answer another: see `arbor replies`. Todos are the CLI's too, through the same
367
+ functions (`arbor todo add`, `update` and `remove`). Merging, removing tasks
368
+ and claiming stay in the CLI, so a page that should not have been reachable
369
+ can at worst leave a reply and change todos. It takes no lease.
370
+
371
+ It listens on 127.0.0.1 only and refuses a request whose `Host` is not this
372
+ machine, and any write from another origin. To use it from another device,
373
+ put a tunnel in front of it (Cloudflare Tunnel, Tailscale, ngrok) and name the
374
+ tunnel's hostname in `--allow-hosts`:
181
375
 
182
- `list`, `show` and `log` in a browser, on `http://localhost:4269`, reloading
183
- themselves as tasks change. Read-only, and takes no lease.
376
+ ```bash
377
+ arbor dev --allow-hosts myrepo-arbor.example.dev
378
+ ```
379
+
380
+ arbor has no login of its own. Whoever can reach an allowed host can read the
381
+ repo's plans and reply, so the tunnel has to be the one asking who you are
382
+ (Cloudflare Access, a Tailscale tailnet). Never expose it through a tunnel
383
+ with no login in front.
184
384
 
185
385
  ### `arbor path [task]`
186
386
 
@@ -205,17 +405,47 @@ worktree it is already in.
205
405
  Refuses a task that does not exist, or one whose directory is gone, rather than
206
406
  printing a path you cannot `cd` into.
207
407
 
208
- ### `arbor escalate <reason> [--task <name>]`
408
+ ### `arbor escalate <reason> [--task <name>] [--review]`
209
409
 
210
410
  The explicit "this needs a human" exit. Records the reason, drops the lease, and
211
411
  leaves the worktree **exactly** as it is so the human sees what the agent saw.
212
412
 
413
+ `--review` asks for approval to merge rather than for answers: it adds
414
+ `- [ ] Q5. ✅ Ready to merge?` under `## Blocked`, the reason indented under
415
+ it as what to look at, and the page shows the task as one card with Approve
416
+ and Request changes. Refused (`blocked`) while another question is unchecked,
417
+ so a review is the last thing standing between the task and trunk. The agent
418
+ waits with `arbor wait --answered`, then merges on "Approved: merge it." or
419
+ acts on the changes asked for.
420
+
213
421
  This exists so an agent has a way out that is not "resolve the conflict badly to
214
422
  finish the task". Agents are reliable at mechanical conflicts (both sides added
215
423
  imports, a signature changed on one side and its callers on the other) and
216
424
  unreliable when both sides restructured the same logic, because then there is no
217
425
  correct merge, only a decision.
218
426
 
427
+ ### `arbor todo add <text> [--file <path>]`, `arbor todo list [--json]`, `arbor todo update <id> [text] [--file <path>] [--remove-file <name>]`, `arbor todo remove <id>`
428
+
429
+ Work deferred for later. When something outside the task comes up (a bug next
430
+ door, a follow-up the reviewer asked for, a question that turns out to be its
431
+ own project), the agent notes it with `arbor todo add` and carries on instead
432
+ of growing the task. Run from a worktree, `add` records the task it came up
433
+ in, which is what lets `merge` recommend a task's own follow-ups first.
434
+
435
+ Todos live in `.git/arbor/todos/`, one file each, so every worktree sees a new
436
+ one at once, with nothing to commit and no two agents rewriting the same file.
437
+ Numbers are never reused. They are local to the clone: not in git history,
438
+ not on a fresh checkout.
439
+
440
+ `arbor add <task> --todo <id>` is how one gets picked up. `update` rewords
441
+ one and keeps its number; `remove` drops one done some other way or no longer
442
+ wanted.
443
+
444
+ `--file a.png,notes.md` attaches files of any kind, stored beside the todo in
445
+ `.git/arbor/todos/<id>/` the way a reply's are. `update --remove-file` drops
446
+ one, named by path or by its stored file name (`todo list` shows them). They
447
+ go when the todo does.
448
+
219
449
  ### `arbor retry <task>`
220
450
 
221
451
  Grants an escalated task another `mergeRetryCount` merge attempts and puts it
@@ -241,15 +471,17 @@ The agent's control flow runs on these.
241
471
  | 3 | `tests_failed` | The gate (`postRewrite`, `preMerge`) failed after the rebase. Branch rolled back, trunk untouched. Fix and merge again. |
242
472
  | 4 | `lease_lost` | Another agent took the tree mid-merge. **Stop. Do not retry.** |
243
473
  | 5 | `budget_exhausted` | Out of merge attempts. `arbor escalate`, and a human can grant another budget with `arbor retry`; or `arbor remove` and redo against current trunk. |
244
- | 6 | `lease_held` | Another agent is driving this tree. |
474
+ | 6 | `lease_held` | Another agent is driving this tree. For a reply from the page: answer that agent in its chat. |
245
475
  | 7 | `dirty` | Uncommitted changes. Commit before merging. |
246
- | 8 | `not_found` | No such task, or not run from a task worktree. |
476
+ | 8 | `not_found` | No such task, or not run from a task worktree; for a reply from the page, no such open question. |
247
477
  | 9 | `hook_failed` | `postCheckout` failed (worktree still exists; fix and re-run the hook), or `postMerge` failed (the branch already landed; nothing rolled back). |
248
478
  | 10 | `exists` | Task already exists. `arbor claim` it, or `arbor remove` first. |
249
479
  | 11 | `orphaned` | Record with no worktree. `arbor remove` it. |
250
480
  | 12 | `merge_failed` | The base could not be fast-forwarded (usually uncommitted changes in the worktree holding it). |
251
481
  | 13 | `already_removed` | This task was removed earlier; nothing left to remove. |
252
- | 14 | `timeout` | `arbor wait` gave up: the task is still working or merging. |
482
+ | 14 | `timeout` | `arbor wait` gave up: the task is still working or merging, or with `--answered`, a question is still unanswered. |
483
+ | 15 | `unread` | `arbor merge` refused: a person's reply waits unclaimed. `arbor replies`, act on it, check it off, merge again. |
484
+ | 16 | `blocked` | `arbor merge` refused: a question under `## Blocked` is unchecked, unanswered or not yet acted on. Also `escalate --review` while one is. |
253
485
 
254
486
  Every failure prints a one-line JSON object on **stdout** (`{"reason": ...}`,
255
487
  plus fields like `paths` for conflicts) and the human explanation on **stderr**.
@@ -272,7 +504,8 @@ export default defineConfig({
272
504
  leaseStalenessMs: 90_000,
273
505
  mergeRetryCount: 2,
274
506
  removedCapacity: 50, // removed names kept, so remove can say "already removed"
275
- logCapacity: 200, // entries `arbor log` keeps before the oldest fall off
507
+ logCapacity: 1000, // entries `arbor log` keeps before the oldest fall off
508
+ todoStalenessMs: 2_592_000_000, // 30 days: past this, merge offers a todo for removal
276
509
  });
277
510
  ```
278
511
 
package/add.d.ts CHANGED
@@ -2,15 +2,22 @@ import { type Logger } from "webappwiz/log";
2
2
  import type { Fs } from "webappwiz/system";
3
3
  import type { Config } from "./config.js";
4
4
  import type { Shell } from "./shell.js";
5
+ import type { Todos } from "./todo.js";
5
6
  import type { WorktreeService } from "./worktree-service.js";
6
7
  export interface AddOptions {
7
8
  /** Branch the task starts from and merges onto. Defaults to the trunk. */
8
9
  base?: string;
10
+ /**
11
+ * The todo this task takes up: its text seeds the plan's Goal, and nobody
12
+ * else can take it while the task lives.
13
+ */
14
+ todo?: number;
9
15
  }
10
- export declare function add({ service, shell, config, log, fs, }: {
16
+ export declare function add({ service, shell, config, log, fs, todos, }: {
11
17
  service: WorktreeService;
12
18
  shell: Shell;
13
19
  config: Config;
14
20
  log: Logger;
15
21
  fs: Fs;
16
- }, task: string, { base }?: AddOptions): Promise<void>;
22
+ todos: Todos;
23
+ }, task: string, { base, todo: id }?: AddOptions): Promise<void>;
@@ -0,0 +1,36 @@
1
+ import { type IdProvider } from "webappwiz/id";
2
+ import type { Fs, Ps } from "webappwiz/system";
3
+ /** A file handed over, as bytes so a pasted image needs no temp file. */
4
+ export interface Attachment {
5
+ /** What it was called, kept in the stored name so the files stay apart. */
6
+ name: string;
7
+ bytes: Uint8Array;
8
+ }
9
+ /**
10
+ * The folder of files that belong to one record, a reply's or a todo's, kept
11
+ * beside it: `replies/<task>/Q2.json` has `replies/<task>/Q2/`, and
12
+ * `todos/7.json` has `todos/7/`. Under `.git/arbor`, so nothing here is ever
13
+ * committed and every tree can open what it holds.
14
+ */
15
+ export declare class Attachments {
16
+ readonly dir: string;
17
+ private readonly fs;
18
+ private readonly ids;
19
+ constructor(dir: string, fs: Fs, ids?: IdProvider);
20
+ /** Copies each file in, returning the absolute path each was stored at. */
21
+ store(files: Attachment[]): Promise<string[]>;
22
+ /** Whether `path` is one of this folder's files, and not a way out of it. */
23
+ owns(path: string): boolean;
24
+ /** Deletes the files given that are this folder's; others are left alone. */
25
+ remove(paths: string[]): Promise<void>;
26
+ /** Deletes the folder and everything in it. */
27
+ clear(): Promise<void>;
28
+ }
29
+ /**
30
+ * Reads files named on the command line, relative to the current directory or
31
+ * absolute, refusing the whole command over one that cannot be read.
32
+ */
33
+ export declare function readFiles({ fs, ps }: {
34
+ fs: Fs;
35
+ ps: Ps;
36
+ }, paths: string[]): Promise<Attachment[]>;
package/claim.d.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  import { type Logger } from "webappwiz/log";
2
2
  import type { WorktreeService } from "./worktree-service.js";
3
- /** The resume entry point: a fresh agent thread picking up existing work. */
3
+ /**
4
+ * The resume entry point: a fresh agent thread picking up existing work. An
5
+ * escalated task goes back to `working`, since someone is on it again; its
6
+ * merge budget stays as it was, which only `retry` refills.
7
+ */
4
8
  export declare function claim({ service, log }: {
5
9
  service: WorktreeService;
6
10
  log: Logger;
package/config.d.ts CHANGED
@@ -40,6 +40,12 @@ export interface Config {
40
40
  removedCapacity: number;
41
41
  /** How many entries `arbor log` keeps before the oldest fall off. */
42
42
  logCapacity: number;
43
+ /**
44
+ * How long a todo can wait before `merge` stops recommending it and offers
45
+ * it for removal instead: work deferred that long has usually been done
46
+ * some other way, or stopped mattering.
47
+ */
48
+ todoStalenessMs: number;
43
49
  }
44
50
  /**
45
51
  * Identity, for the types. `export default defineConfig({ ... })` in
package/dev.d.ts CHANGED
@@ -3,6 +3,8 @@ import type { Logger } from "webappwiz/log";
3
3
  import { type Fs, type PortProvider } from "webappwiz/system";
4
4
  import type { Assets } from "./dev/assets.js";
5
5
  import type { Journal } from "./journal.js";
6
+ import type { Replies } from "./replies.js";
7
+ import type { Todos } from "./todo.js";
6
8
  import type { WorktreeService } from "./worktree-service.js";
7
9
  /** Preferred, not required: `dev` moves up from here when it is taken. */
8
10
  export declare const DEFAULT_PORT = 4269;
@@ -12,23 +14,36 @@ export declare const PORT_SPAN = 20;
12
14
  export interface DevOptions {
13
15
  /** Where to listen; the port `--port` asked for, and the span above it. */
14
16
  ports?: PortProvider;
17
+ /**
18
+ * Host names besides this machine's own that the page may be reached by,
19
+ * like the hostname of a tunnel. Anything else is refused, which is what
20
+ * stops another site from reading the repo through a browser pointed at
21
+ * localhost. Who may use those names is the tunnel's business, not arbor's.
22
+ */
23
+ hosts?: string[];
15
24
  }
16
25
  /** A running server, and the one thing a caller ever wants to do with it. */
17
26
  export interface DevServer extends AsyncResource {
18
27
  port: number;
19
28
  }
20
29
  /**
21
- * Serves what `list`, `show` and `log` print, as one page that refetches when the
22
- * repo changes. Read-only on purpose: driving arbor is what the CLI is for, and
23
- * a button that took a lease would fight the agent holding it.
30
+ * Serves what `list`, `show`, `inbox` and `todo list` print, as one page that
31
+ * refetches when the repo changes. It is the only place a person answers a
32
+ * question: follows one up, defers or skips it, or approves a merge. The CLI
33
+ * has no command for any of them, so no agent is tempted to answer another.
34
+ * Anything that moves a task (merge, remove, claim) stays in the CLI, so a
35
+ * page that should not have been reachable can at worst leave a reply or
36
+ * change a todo.
24
37
  */
25
- export declare function dev({ service, fs, journal, log, assets, }: {
38
+ export declare function dev({ service, fs, journal, todos, replies, log, assets, }: {
26
39
  service: WorktreeService;
27
40
  fs: Fs;
28
41
  journal: Journal;
42
+ todos: Todos;
43
+ replies: Replies;
29
44
  log: Logger;
30
45
  assets: Assets;
31
- }, { ports }?: DevOptions): Promise<DevServer>;
46
+ }, { ports, hosts }?: DevOptions): Promise<DevServer>;
32
47
  /**
33
48
  * Where `dev` will listen, given the port asked for. A flag is outside input,
34
49
  * so a port that cannot exist is a refusal with an exit code rather than the
package/escalate.d.ts CHANGED
@@ -1,19 +1,28 @@
1
1
  import { type Logger } from "webappwiz/log";
2
- import type { Lock } from "webappwiz/system";
2
+ import type { Fs, Lock } from "webappwiz/system";
3
3
  import type { Git } from "./git.js";
4
4
  import type { WorktreeService } from "./worktree-service.js";
5
5
  export interface EscalateOptions {
6
6
  /** Task to escalate. Defaults to the one `cwd` is a worktree for. */
7
7
  task?: string;
8
+ /**
9
+ * Ask a person to approve merging, rather than to answer questions: adds
10
+ * the question that asks it to the plan, with the reason as its detail.
11
+ * Refused while another question is unchecked.
12
+ */
13
+ review?: boolean;
8
14
  }
15
+ /** The question a review asks. */
16
+ export declare const REVIEW_SUBJECT = "\u2705 Ready to merge?";
9
17
  /**
10
18
  * The way out that is not "resolve the conflict badly to finish the task".
11
19
  * When both sides restructured the same logic there is no correct merge, only
12
20
  * a decision, and that belongs to a human.
13
21
  */
14
- export declare function escalate({ service, git, lock, log, }: {
22
+ export declare function escalate({ service, git, lock, log, fs, }: {
15
23
  service: WorktreeService;
16
24
  git: Git;
17
25
  lock: Lock;
18
26
  log: Logger;
19
- }, reason: string, cwd: string, { task }?: EscalateOptions): Promise<void>;
27
+ fs: Fs;
28
+ }, reason: string, cwd: string, { task, review }?: EscalateOptions): Promise<void>;
package/exit.d.ts CHANGED
@@ -15,6 +15,8 @@ export declare const EXIT: {
15
15
  readonly merge_failed: 12;
16
16
  readonly already_removed: 13;
17
17
  readonly timeout: 14;
18
+ readonly unread: 15;
19
+ readonly blocked: 16;
18
20
  };
19
21
  export type Reason = keyof typeof EXIT;
20
22
  /**
package/git.d.ts CHANGED
@@ -41,6 +41,18 @@ export declare class Git {
41
41
  added: number;
42
42
  removed: number;
43
43
  } | null>;
44
+ /**
45
+ * Every path `branch` touches since it left `trunk`, plus what is still
46
+ * uncommitted in `worktree` when there is one: an agent mid-task has usually
47
+ * committed nothing yet, and its edits are the overlap that matters most.
48
+ */
49
+ changedFiles(trunk: string, branch: string, worktree: string | null): Promise<string[] | null>;
50
+ /**
51
+ * Every path a person might point an agent at in the tree at `cwd`: the
52
+ * files git tracks, the new ones it does not ignore, and each directory
53
+ * above them, which ends in `/`. Sorted, relative to the tree's root.
54
+ */
55
+ paths(cwd: string): Promise<string[]>;
44
56
  rebase(cwd: string, onto: string): Promise<GitResult>;
45
57
  resetHard(cwd: string, commit: string): Promise<GitResult>;
46
58
  checkout(branch: string): Promise<GitResult>;
package/inbox.d.ts ADDED
@@ -0,0 +1,80 @@
1
+ import { type Logger } from "webappwiz/log";
2
+ import type { Fs } from "webappwiz/system";
3
+ import { type Question } from "./plan.js";
4
+ import { type Replies, type ReplyState } from "./replies.js";
5
+ import type { WorktreeStatus } from "./worktree.js";
6
+ import type { WorktreeService } from "./worktree-service.js";
7
+ /** A question, with the task that asked it and where it stands. */
8
+ export interface OpenQuestion extends Question {
9
+ task: string;
10
+ status: WorktreeStatus;
11
+ /**
12
+ * `held` means the asking agent is in a live session: answer it there,
13
+ * since `arbor reply` refuses a tree someone is driving.
14
+ */
15
+ lease: "held" | "stale" | "none";
16
+ /**
17
+ * A reply or follow-up given and waiting for the agent to claim it, or
18
+ * null.
19
+ */
20
+ pending: ReplyState | null;
21
+ /**
22
+ * Where it stands. `open` waits on a person. `waiting` has a reply its
23
+ * agent has yet to read, which can still be edited. `editing` is held by
24
+ * someone changing it. `read` has been claimed by its agent, which has yet
25
+ * to check it off. `done` is checked off. A follow-up takes a `read` or
26
+ * `done` question back to `waiting`.
27
+ */
28
+ state: QuestionState;
29
+ }
30
+ export type QuestionState = "open" | "waiting" | "editing" | "read" | "done";
31
+ export interface Inbox {
32
+ /**
33
+ * Questions, by task name and then in the order each plan lists them.
34
+ * Only the unanswered ones unless the others were asked for too.
35
+ */
36
+ questions: OpenQuestion[];
37
+ /** How many unchecked questions have a reply their agent has yet to act on. */
38
+ replied: number;
39
+ }
40
+ export interface InboxOptions {
41
+ /**
42
+ * Keep the questions replied to but not yet checked off, to change or add
43
+ * to an answer. Without it, a question leaves the inbox once it is answered.
44
+ */
45
+ replied?: boolean;
46
+ /** Keep the questions checked off too, to follow one up. */
47
+ done?: boolean;
48
+ }
49
+ /**
50
+ * Every question still waiting on a person, across all tasks: the unchecked
51
+ * items under each worktree's `## Blocked` with no reply yet, and with
52
+ * `replied` also those answered but not yet checked off, whether their agent
53
+ * has read the reply or not.
54
+ *
55
+ * Returns data rather than printing it, so the CLI and the dev server show the
56
+ * same inbox.
57
+ */
58
+ export declare function openQuestions({ service, fs, replies, }: {
59
+ service: WorktreeService;
60
+ fs: Fs;
61
+ replies: Replies;
62
+ }, { replied, done }?: InboxOptions): Promise<Inbox>;
63
+ export interface InboxPrintOptions extends InboxOptions {
64
+ /** Print the inbox as JSON instead of a listing. */
65
+ json?: boolean;
66
+ }
67
+ /** `arbor inbox`: the open questions, grouped by task. */
68
+ export declare function inbox(deps: {
69
+ service: WorktreeService;
70
+ fs: Fs;
71
+ replies: Replies;
72
+ log: Logger;
73
+ }, { json, replied }?: InboxPrintOptions): Promise<void>;
74
+ /**
75
+ * One question as the inbox prints it: `Q9 subject`, its body and choices
76
+ * under it, then the reply the agent has yet to act on, if there is one.
77
+ */
78
+ export declare function formatQuestion(question: Question & {
79
+ pending?: ReplyState | null;
80
+ }): string[];