@webappwiz/arbor 0.0.19 → 0.0.21
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 +250 -16
- package/add.d.ts +9 -2
- package/attachments.d.ts +36 -0
- package/claim.d.ts +5 -1
- package/config.d.ts +6 -0
- package/dev.d.ts +20 -5
- package/escalate.d.ts +12 -3
- package/exit.d.ts +2 -0
- package/git.d.ts +12 -0
- package/inbox.d.ts +80 -0
- package/index.js +3913 -18589
- package/list.d.ts +10 -2
- package/merge.d.ts +7 -2
- package/package.json +5 -2
- package/plan.d.ts +99 -0
- package/remove.d.ts +3 -1
- package/replies.d.ts +81 -0
- package/reply.d.ts +102 -0
- package/repository.d.ts +4 -0
- package/show.d.ts +5 -0
- package/snapshot.d.ts +23 -12
- package/todo.d.ts +138 -0
- package/wait.d.ts +16 -2
- package/worktree-service.d.ts +7 -0
- package/worktree.d.ts +7 -0
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`
|
|
@@ -137,11 +181,12 @@ in silence: it is the one thing that makes the work resumable.
|
|
|
137
181
|
|
|
138
182
|
A `ARBOR.md` that is there gets checked against the shape the agent skill
|
|
139
183
|
prescribes (`# <task>`, `## Goal`, `## Next` with something unchecked in it, a
|
|
140
|
-
`## Blocked`
|
|
184
|
+
`## Blocked` with `- [ ] Q1.` items once escalated, and none left open after),
|
|
185
|
+
and anything off is printed under it.
|
|
141
186
|
Warnings only, never a refusal: the agent that wrote the file is the one that
|
|
142
187
|
runs `show` on it, and a rough plan still beats none.
|
|
143
188
|
|
|
144
|
-
### `arbor wait <task> [--timeout-secs 300]`
|
|
189
|
+
### `arbor wait <task> [--timeout-secs 300] [--answered]`
|
|
145
190
|
|
|
146
191
|
Blocks until a task stops moving, then prints where it stopped.
|
|
147
192
|
|
|
@@ -158,11 +203,109 @@ driving. Wait again, work alongside it, or ask the human.
|
|
|
158
203
|
Like `show` and `path`, it takes no lease, so watching a task cannot knock its
|
|
159
204
|
agent off it.
|
|
160
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 (``, 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
|
+

|
|
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
|
+
|
|
161
303
|
### `arbor log [--count 20] [--json]`
|
|
162
304
|
|
|
163
305
|
The last N things done here (`add`, `claim`, `merge`, `remove`, `escalate`,
|
|
164
|
-
`retry`
|
|
165
|
-
oldest first, each with the task and how it ended (`ok`, or
|
|
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).
|
|
166
309
|
|
|
167
310
|
```
|
|
168
311
|
WHEN ACTION TASK RESULT
|
|
@@ -173,13 +316,71 @@ WHEN ACTION TASK RESULT
|
|
|
173
316
|
|
|
174
317
|
`list` is what still exists; this is what happened. Entries outlive their tasks:
|
|
175
318
|
a successful `merge` and a `remove` both take the record with them, so this is
|
|
176
|
-
the only thing that remembers a task landed at all. The last
|
|
319
|
+
the only thing that remembers a task landed at all. The last 1000 are kept
|
|
177
320
|
(`logCapacity`) in `.git/arbor/log.jsonl`.
|
|
178
321
|
|
|
179
|
-
### `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`:
|
|
180
375
|
|
|
181
|
-
|
|
182
|
-
|
|
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.
|
|
183
384
|
|
|
184
385
|
### `arbor path [task]`
|
|
185
386
|
|
|
@@ -204,17 +405,47 @@ worktree it is already in.
|
|
|
204
405
|
Refuses a task that does not exist, or one whose directory is gone, rather than
|
|
205
406
|
printing a path you cannot `cd` into.
|
|
206
407
|
|
|
207
|
-
### `arbor escalate <reason> [--task <name>]`
|
|
408
|
+
### `arbor escalate <reason> [--task <name>] [--review]`
|
|
208
409
|
|
|
209
410
|
The explicit "this needs a human" exit. Records the reason, drops the lease, and
|
|
210
411
|
leaves the worktree **exactly** as it is so the human sees what the agent saw.
|
|
211
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
|
+
|
|
212
421
|
This exists so an agent has a way out that is not "resolve the conflict badly to
|
|
213
422
|
finish the task". Agents are reliable at mechanical conflicts (both sides added
|
|
214
423
|
imports, a signature changed on one side and its callers on the other) and
|
|
215
424
|
unreliable when both sides restructured the same logic, because then there is no
|
|
216
425
|
correct merge, only a decision.
|
|
217
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
|
+
|
|
218
449
|
### `arbor retry <task>`
|
|
219
450
|
|
|
220
451
|
Grants an escalated task another `mergeRetryCount` merge attempts and puts it
|
|
@@ -240,15 +471,17 @@ The agent's control flow runs on these.
|
|
|
240
471
|
| 3 | `tests_failed` | The gate (`postRewrite`, `preMerge`) failed after the rebase. Branch rolled back, trunk untouched. Fix and merge again. |
|
|
241
472
|
| 4 | `lease_lost` | Another agent took the tree mid-merge. **Stop. Do not retry.** |
|
|
242
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. |
|
|
243
|
-
| 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. |
|
|
244
475
|
| 7 | `dirty` | Uncommitted changes. Commit before merging. |
|
|
245
|
-
| 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. |
|
|
246
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). |
|
|
247
478
|
| 10 | `exists` | Task already exists. `arbor claim` it, or `arbor remove` first. |
|
|
248
479
|
| 11 | `orphaned` | Record with no worktree. `arbor remove` it. |
|
|
249
480
|
| 12 | `merge_failed` | The base could not be fast-forwarded (usually uncommitted changes in the worktree holding it). |
|
|
250
481
|
| 13 | `already_removed` | This task was removed earlier; nothing left to remove. |
|
|
251
|
-
| 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. |
|
|
252
485
|
|
|
253
486
|
Every failure prints a one-line JSON object on **stdout** (`{"reason": ...}`,
|
|
254
487
|
plus fields like `paths` for conflicts) and the human explanation on **stderr**.
|
|
@@ -271,7 +504,8 @@ export default defineConfig({
|
|
|
271
504
|
leaseStalenessMs: 90_000,
|
|
272
505
|
mergeRetryCount: 2,
|
|
273
506
|
removedCapacity: 50, // removed names kept, so remove can say "already removed"
|
|
274
|
-
logCapacity:
|
|
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
|
|
275
509
|
});
|
|
276
510
|
```
|
|
277
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
|
-
|
|
22
|
+
todos: Todos;
|
|
23
|
+
}, task: string, { base, todo: id }?: AddOptions): Promise<void>;
|
package/attachments.d.ts
ADDED
|
@@ -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
|
-
/**
|
|
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 `
|
|
22
|
-
* repo changes.
|
|
23
|
-
*
|
|
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
|
-
|
|
27
|
+
fs: Fs;
|
|
28
|
+
}, reason: string, cwd: string, { task, review }?: EscalateOptions): Promise<void>;
|
package/exit.d.ts
CHANGED
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[];
|