@webappwiz/arbor 0.0.12 → 0.0.13
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 +6 -6
- package/arbor.d.ts +2 -2
- package/dev/assets.d.ts +1 -7
- package/dev.d.ts +9 -13
- package/escalate.d.ts +5 -1
- package/index.js +77 -67
- package/{ls.d.ts → list.d.ts} +3 -3
- package/package.json +2 -2
- package/shell.d.ts +2 -6
- package/show.d.ts +11 -6
- package/snapshot.d.ts +2 -2
package/README.md
CHANGED
|
@@ -80,7 +80,7 @@ linear.
|
|
|
80
80
|
back would leave its index and files on the old commit.
|
|
81
81
|
5. Discards the task (worktree, branch and record) exactly as `rm`
|
|
82
82
|
would. The work is on trunk, so the tree has nothing left to hold, and
|
|
83
|
-
`arbor
|
|
83
|
+
`arbor list` stays a list of live work rather than a graveyard of landed
|
|
84
84
|
tasks. The agent's own directory goes with it, so the success message
|
|
85
85
|
prints the main tree to `cd` back to.
|
|
86
86
|
|
|
@@ -105,7 +105,7 @@ Removal leaves a tombstone in `.git/arbor/removed/` so a second `rm` can say
|
|
|
105
105
|
and drops the oldest as new ones arrive, so a long-forgotten task reports
|
|
106
106
|
`not_found` again.
|
|
107
107
|
|
|
108
|
-
### `arbor
|
|
108
|
+
### `arbor list [--json]`
|
|
109
109
|
|
|
110
110
|
Every task: name, status, lease (`held`/`stale`/`none`), commits ahead of
|
|
111
111
|
trunk, age. A corrupt record shows as `unknown` instead of taking
|
|
@@ -113,7 +113,7 @@ down the listing; a record whose worktree vanished shows as `orphaned`.
|
|
|
113
113
|
|
|
114
114
|
### `arbor show <task> [--json]`
|
|
115
115
|
|
|
116
|
-
One task in full: the row `
|
|
116
|
+
One task in full: the row `list` would print for it, plus the `ARBOR.md`
|
|
117
117
|
its agent keeps at the worktree root and the reason behind an `escalated`
|
|
118
118
|
status.
|
|
119
119
|
|
|
@@ -130,7 +130,7 @@ ARBOR.md
|
|
|
130
130
|
...
|
|
131
131
|
```
|
|
132
132
|
|
|
133
|
-
`
|
|
133
|
+
`list` says a task exists; this says what it is doing. Like `path`, it takes no
|
|
134
134
|
lease, so reading another agent's tree cannot knock it off its own work the way
|
|
135
135
|
`claim` would. A task with no `ARBOR.md` is called out rather than passed over
|
|
136
136
|
in silence: it is the one thing that makes the work resumable.
|
|
@@ -171,14 +171,14 @@ WHEN ACTION TASK RESULT
|
|
|
171
171
|
1h merge alpha ok
|
|
172
172
|
```
|
|
173
173
|
|
|
174
|
-
`
|
|
174
|
+
`list` is what still exists; this is what happened. Entries outlive their tasks:
|
|
175
175
|
a successful `merge` and a `rm` both take the record with them, so this is
|
|
176
176
|
the only thing that remembers a task landed at all. The last 200 are kept
|
|
177
177
|
(`logCapacity`) in `.git/arbor/log.jsonl`.
|
|
178
178
|
|
|
179
179
|
### `arbor dev [--port 4269]`
|
|
180
180
|
|
|
181
|
-
`
|
|
181
|
+
`list`, `show` and `log` in a browser, on `http://localhost:4269`, reloading
|
|
182
182
|
themselves as tasks change. Read-only, and takes no lease.
|
|
183
183
|
|
|
184
184
|
### `arbor path [task]`
|
package/arbor.d.ts
CHANGED
|
@@ -6,9 +6,9 @@ import { type Repository } from "./repository.js";
|
|
|
6
6
|
/** Everything `arbor` is run with, before the repository middleware adds to it. */
|
|
7
7
|
export interface ArborDeps extends Deps {
|
|
8
8
|
fs: Fs;
|
|
9
|
-
/**
|
|
9
|
+
/** HTTP server `dev` listens on to serve its preview. */
|
|
10
10
|
http: HttpServer;
|
|
11
|
-
/**
|
|
11
|
+
/** The built page `dev` serves as its preview. */
|
|
12
12
|
assets: Assets;
|
|
13
13
|
}
|
|
14
14
|
export declare const arbor: import("webappwiz/cmd").Cli<ArborDeps, ArborDeps & Repository>;
|
package/dev/assets.d.ts
CHANGED
|
@@ -7,11 +7,5 @@ export interface Assets {
|
|
|
7
7
|
/** The stylesheet, with Tailwind already compiled. */
|
|
8
8
|
styles: string;
|
|
9
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
|
-
*/
|
|
10
|
+
/** The page this package ships. Serve each field's contents as-is. */
|
|
17
11
|
export declare const assets: Assets;
|
package/dev.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type AsyncResource } from "webappwiz/disposable";
|
|
1
2
|
import type { HttpServer } from "webappwiz/http";
|
|
2
3
|
import type { Logger } from "webappwiz/log";
|
|
3
4
|
import { type Fs, type PortProvider } from "webappwiz/system";
|
|
@@ -8,30 +9,19 @@ import type { WorktreeService } from "./worktree-service.js";
|
|
|
8
9
|
export declare const DEFAULT_PORT = 4269;
|
|
9
10
|
/** How far above the port asked for `dev` will look before giving up. */
|
|
10
11
|
export declare const PORT_SPAN = 20;
|
|
11
|
-
/**
|
|
12
|
-
* Where `dev` will listen, given the port asked for. A flag is outside input,
|
|
13
|
-
* so a port that cannot exist is a refusal with an exit code rather than the
|
|
14
|
-
* assertion `OpenPortProvider` would raise for a bug in here.
|
|
15
|
-
*/
|
|
16
|
-
export declare function devPorts(from: number): PortProvider;
|
|
17
12
|
/** What `dev` lets a caller choose. */
|
|
18
13
|
export interface DevOptions {
|
|
19
14
|
/** Where to listen; the port `--port` asked for, and the span above it. */
|
|
20
15
|
ports?: PortProvider;
|
|
21
16
|
}
|
|
22
17
|
/** A running server, and the one thing a caller ever wants to do with it. */
|
|
23
|
-
export interface DevServer {
|
|
18
|
+
export interface DevServer extends AsyncResource {
|
|
24
19
|
port: number;
|
|
25
|
-
stop(): Promise<void>;
|
|
26
20
|
}
|
|
27
21
|
/**
|
|
28
|
-
* Serves what `
|
|
22
|
+
* Serves what `list`, `show` and `log` print, as one page that refetches when the
|
|
29
23
|
* repo changes. Read-only on purpose: driving arbor is what the CLI is for, and
|
|
30
24
|
* a button that took a lease would fight the agent holding it.
|
|
31
|
-
*
|
|
32
|
-
* The page itself is a React app under `dev/`, built before publishing and
|
|
33
|
-
* carried in the bundle, so nothing here builds markup and nothing reads it
|
|
34
|
-
* off disk.
|
|
35
25
|
*/
|
|
36
26
|
export declare function dev({ service, fs, journal, log, http, assets, }: {
|
|
37
27
|
service: WorktreeService;
|
|
@@ -41,3 +31,9 @@ export declare function dev({ service, fs, journal, log, http, assets, }: {
|
|
|
41
31
|
http: HttpServer;
|
|
42
32
|
assets: Assets;
|
|
43
33
|
}, { ports }?: DevOptions): Promise<DevServer>;
|
|
34
|
+
/**
|
|
35
|
+
* Where `dev` will listen, given the port asked for. A flag is outside input,
|
|
36
|
+
* so a port that cannot exist is a refusal with an exit code rather than the
|
|
37
|
+
* assertion `OpenPortProvider` would raise for a bug in here.
|
|
38
|
+
*/
|
|
39
|
+
export declare function devPorts(from: number): PortProvider;
|
package/escalate.d.ts
CHANGED
|
@@ -2,6 +2,10 @@ import { type Logger } from "webappwiz/log";
|
|
|
2
2
|
import type { Lock } from "webappwiz/system";
|
|
3
3
|
import type { Git } from "./git.js";
|
|
4
4
|
import type { WorktreeService } from "./worktree-service.js";
|
|
5
|
+
export interface EscalateOptions {
|
|
6
|
+
/** Task to escalate. Defaults to the one `cwd` is a worktree for. */
|
|
7
|
+
task?: string;
|
|
8
|
+
}
|
|
5
9
|
/**
|
|
6
10
|
* The way out that is not "resolve the conflict badly to finish the task".
|
|
7
11
|
* When both sides restructured the same logic there is no correct merge, only
|
|
@@ -12,4 +16,4 @@ export declare function escalate({ service, git, lock, log, }: {
|
|
|
12
16
|
git: Git;
|
|
13
17
|
lock: Lock;
|
|
14
18
|
log: Logger;
|
|
15
|
-
}, reason: string, cwd: string, task?:
|
|
19
|
+
}, reason: string, cwd: string, { task }?: EscalateOptions): Promise<void>;
|
package/index.js
CHANGED
|
@@ -62,7 +62,6 @@ function exits() {
|
|
|
62
62
|
// add.ts
|
|
63
63
|
var NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
64
64
|
var PLAN_FILE = "ARBOR.md";
|
|
65
|
-
var PLAN = (task) => new MarkdownWriter().heading(1, task).heading(2, "Goal").heading(2, "Files").heading(2, "Next").checklist("fill in Goal and list the steps here as `- [ ]` items").toString();
|
|
66
65
|
async function add({
|
|
67
66
|
service,
|
|
68
67
|
shell,
|
|
@@ -100,8 +99,7 @@ ${PLAN_FILE}
|
|
|
100
99
|
}
|
|
101
100
|
await fs.write(`${worktree.path}/${PLAN_FILE}`, PLAN(task));
|
|
102
101
|
if (config.postCheckout) {
|
|
103
|
-
const { exitCode } = await shell.stream(config.postCheckout, {
|
|
104
|
-
cwd: worktree.path,
|
|
102
|
+
const { exitCode } = await shell.stream(config.postCheckout, worktree.path, {
|
|
105
103
|
env: {
|
|
106
104
|
ARBOR_TASK: task,
|
|
107
105
|
ARBOR_WORKTREE: worktree.path,
|
|
@@ -117,6 +115,9 @@ ${PLAN_FILE}
|
|
|
117
115
|
branch: ${worktree.branch}
|
|
118
116
|
base: ${base}`);
|
|
119
117
|
}
|
|
118
|
+
function PLAN(task) {
|
|
119
|
+
return new MarkdownWriter().heading(1, task).heading(2, "Goal").heading(2, "Files").heading(2, "Next").checklist("fill in Goal and list the steps here as `- [ ]` items").toString();
|
|
120
|
+
}
|
|
120
121
|
|
|
121
122
|
// claim.ts
|
|
122
123
|
import { color as color2 } from "webappwiz/log";
|
|
@@ -161,6 +162,7 @@ ${changes.map((change) => ` ${change}`).join(`
|
|
|
161
162
|
}
|
|
162
163
|
|
|
163
164
|
// dev.ts
|
|
165
|
+
import { AsyncDisposer } from "webappwiz/disposable";
|
|
164
166
|
import {
|
|
165
167
|
MAX_PORT,
|
|
166
168
|
OpenPortProvider
|
|
@@ -271,41 +273,47 @@ function checkPlan(text, { task, escalated = false }) {
|
|
|
271
273
|
|
|
272
274
|
// show.ts
|
|
273
275
|
var FILE = "ARBOR.md";
|
|
274
|
-
async function show({ service, fs, log: log2 }, task,
|
|
276
|
+
async function show({ service, fs, log: log2 }, task, opts = {}) {
|
|
275
277
|
const worktree = await service.find(task);
|
|
276
278
|
if (worktree.gone) {
|
|
277
|
-
fail("not_found", `no task '${task}': run \`arbor
|
|
279
|
+
fail("not_found", `no task '${task}': run \`arbor list\` to see what there is`, { task });
|
|
278
280
|
}
|
|
279
|
-
const details = await
|
|
280
|
-
if (json) {
|
|
281
|
+
const details = await new TaskDetails({ fs }).get(worktree);
|
|
282
|
+
if (opts.json ?? false) {
|
|
281
283
|
log2.info(JSON.stringify(details, null, "\t"));
|
|
282
284
|
return;
|
|
283
285
|
}
|
|
284
286
|
log2.info(report(details));
|
|
285
287
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
lease: worktree.leaseStatus,
|
|
298
|
-
ahead: worktree.hasBranch ? await worktree.commitsAhead() : null,
|
|
299
|
-
added: stat?.added ?? null,
|
|
300
|
-
removed: stat?.removed ?? null,
|
|
301
|
-
age: state ? age(state.createdAt) : null,
|
|
302
|
-
escalation: state?.escalations?.at(-1)?.reason ?? null,
|
|
303
|
-
plan,
|
|
304
|
-
planProblems: plan === null ? [] : checkPlan(plan, {
|
|
288
|
+
|
|
289
|
+
class TaskDetails {
|
|
290
|
+
fs;
|
|
291
|
+
constructor(opts = {}) {
|
|
292
|
+
this.fs = opts.fs ?? new NodeFs;
|
|
293
|
+
}
|
|
294
|
+
async get(worktree) {
|
|
295
|
+
const { state } = worktree;
|
|
296
|
+
const stat = worktree.hasBranch ? await worktree.diffStat() : null;
|
|
297
|
+
const plan = worktree.exists ? await this.fs.read(`${worktree.path}/${FILE}`).catch(() => null) : null;
|
|
298
|
+
return {
|
|
305
299
|
task: worktree.task,
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
300
|
+
status: worktree.status,
|
|
301
|
+
branch: worktree.branch,
|
|
302
|
+
base: worktree.base,
|
|
303
|
+
worktree: worktree.path,
|
|
304
|
+
lease: worktree.leaseStatus,
|
|
305
|
+
ahead: worktree.hasBranch ? await worktree.commitsAhead() : null,
|
|
306
|
+
added: stat?.added ?? null,
|
|
307
|
+
removed: stat?.removed ?? null,
|
|
308
|
+
age: state ? age(state.createdAt) : null,
|
|
309
|
+
escalation: state?.escalations?.at(-1)?.reason ?? null,
|
|
310
|
+
plan,
|
|
311
|
+
planProblems: plan === null ? [] : checkPlan(plan, {
|
|
312
|
+
task: worktree.task,
|
|
313
|
+
escalated: worktree.status === "escalated"
|
|
314
|
+
})
|
|
315
|
+
};
|
|
316
|
+
}
|
|
309
317
|
}
|
|
310
318
|
function report(details) {
|
|
311
319
|
const lines = [
|
|
@@ -340,10 +348,10 @@ function diff(details) {
|
|
|
340
348
|
|
|
341
349
|
// snapshot.ts
|
|
342
350
|
async function snapshot(service, journal, opts = {}) {
|
|
343
|
-
const
|
|
351
|
+
const details = new TaskDetails({ fs: opts.fs ?? new NodeFs2 });
|
|
344
352
|
const tasks = [];
|
|
345
353
|
for (const worktree of await service.list()) {
|
|
346
|
-
tasks.push(await
|
|
354
|
+
tasks.push(await details.get(worktree));
|
|
347
355
|
}
|
|
348
356
|
return { tasks, entries: await journal.tail(DEFAULT_COUNT) };
|
|
349
357
|
}
|
|
@@ -368,14 +376,6 @@ function fingerprint({ tasks, entries }) {
|
|
|
368
376
|
// dev.ts
|
|
369
377
|
var DEFAULT_PORT = 4269;
|
|
370
378
|
var PORT_SPAN = 20;
|
|
371
|
-
function devPorts(from) {
|
|
372
|
-
if (!Number.isInteger(from) || from < 0 || from > MAX_PORT) {
|
|
373
|
-
fail("usage", `invalid port '${from}': use a whole number 0 to ${MAX_PORT}`, {
|
|
374
|
-
port: from
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
return OpenPortProvider.span({ from, span: PORT_SPAN });
|
|
378
|
-
}
|
|
379
379
|
var POLL_MS = 2000;
|
|
380
380
|
async function dev({
|
|
381
381
|
service,
|
|
@@ -448,14 +448,22 @@ async function dev({
|
|
|
448
448
|
}
|
|
449
449
|
}, { port: await ports.get(), idleTimeout: Duration.zero() });
|
|
450
450
|
log2.info(`arbor dev on http://localhost:${listening.port}`);
|
|
451
|
+
const disposer = new AsyncDisposer;
|
|
452
|
+
disposer.defer(() => listening.stop());
|
|
453
|
+
disposer.defer(async () => clearInterval(poll));
|
|
451
454
|
return {
|
|
452
455
|
port: listening.port,
|
|
453
|
-
|
|
454
|
-
clearInterval(poll);
|
|
455
|
-
await listening.stop();
|
|
456
|
-
}
|
|
456
|
+
disposeAsync: disposer.disposeAsync
|
|
457
457
|
};
|
|
458
458
|
}
|
|
459
|
+
function devPorts(from) {
|
|
460
|
+
if (!Number.isInteger(from) || from < 0 || from > MAX_PORT) {
|
|
461
|
+
fail("usage", `invalid port '${from}': use a whole number 0 to ${MAX_PORT}`, {
|
|
462
|
+
port: from
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
return OpenPortProvider.span({ from, span: PORT_SPAN });
|
|
466
|
+
}
|
|
459
467
|
|
|
460
468
|
// escalate.ts
|
|
461
469
|
import { color as color6 } from "webappwiz/log";
|
|
@@ -464,7 +472,7 @@ async function escalate({
|
|
|
464
472
|
git,
|
|
465
473
|
lock,
|
|
466
474
|
log: log2
|
|
467
|
-
}, reason, cwd, task) {
|
|
475
|
+
}, reason, cwd, { task } = {}) {
|
|
468
476
|
const branch = await git.currentBranch(cwd).catch(() => "");
|
|
469
477
|
const name = task || service.taskFor(branch);
|
|
470
478
|
if (!name) {
|
|
@@ -496,9 +504,9 @@ async function escalate({
|
|
|
496
504
|
`));
|
|
497
505
|
}
|
|
498
506
|
|
|
499
|
-
//
|
|
507
|
+
// list.ts
|
|
500
508
|
import { color as color7 } from "webappwiz/log";
|
|
501
|
-
async function
|
|
509
|
+
async function list({ service, log: log2 }, { json = false } = {}) {
|
|
502
510
|
const rows = [];
|
|
503
511
|
for (const worktree of await service.list()) {
|
|
504
512
|
rows.push(await row2(worktree));
|
|
@@ -605,8 +613,7 @@ async function merge({
|
|
|
605
613
|
`), { task, paths });
|
|
606
614
|
}
|
|
607
615
|
const gate = [config.postRewrite, config.preMerge].filter(Boolean).join(" && ");
|
|
608
|
-
const gated = gate ? await shell.run(gate, {
|
|
609
|
-
cwd: worktree.path,
|
|
616
|
+
const gated = gate ? await shell.run(gate, worktree.path, {
|
|
610
617
|
env: {
|
|
611
618
|
ARBOR_TASK: task,
|
|
612
619
|
ARBOR_WORKTREE: worktree.path
|
|
@@ -657,8 +664,7 @@ ${gated.stderr}`)
|
|
|
657
664
|
Run \`arbor rm ${task}\` to clean up.`, { task });
|
|
658
665
|
}
|
|
659
666
|
if (config.postMerge) {
|
|
660
|
-
const ran = await shell.run(config.postMerge, {
|
|
661
|
-
cwd: landing,
|
|
667
|
+
const ran = await shell.run(config.postMerge, landing, {
|
|
662
668
|
env: { ARBOR_TASK: task }
|
|
663
669
|
});
|
|
664
670
|
if (ran.exitCode !== 0) {
|
|
@@ -695,7 +701,7 @@ async function path({ service, log: log2 }, task) {
|
|
|
695
701
|
}
|
|
696
702
|
const worktree = await service.find(task);
|
|
697
703
|
if (worktree.gone) {
|
|
698
|
-
fail("not_found", `no task '${task}': run \`arbor
|
|
704
|
+
fail("not_found", `no task '${task}': run \`arbor list\` to see what there is`, { task });
|
|
699
705
|
}
|
|
700
706
|
if (!worktree.exists) {
|
|
701
707
|
fail("orphaned", `'${task}' has no worktree at ${worktree.path}`, {
|
|
@@ -932,11 +938,11 @@ class Shell {
|
|
|
932
938
|
constructor(opts = {}) {
|
|
933
939
|
this.ps = opts.ps ?? new NodePs2;
|
|
934
940
|
}
|
|
935
|
-
run(command, opts) {
|
|
936
|
-
return this.ps.spawnCapture(["sh", "-c", command], opts);
|
|
941
|
+
run(command, cwd, opts = {}) {
|
|
942
|
+
return this.ps.spawnCapture(["sh", "-c", command], { ...opts, cwd });
|
|
937
943
|
}
|
|
938
|
-
stream(command, opts) {
|
|
939
|
-
return this.ps.spawn(["sh", "-c", command], opts);
|
|
944
|
+
stream(command, cwd, opts = {}) {
|
|
945
|
+
return this.ps.spawn(["sh", "-c", command], { ...opts, cwd });
|
|
940
946
|
}
|
|
941
947
|
}
|
|
942
948
|
|
|
@@ -1309,7 +1315,7 @@ async function wait({ service, log: log2 }, task, { timeout = DEFAULT_TIMEOUT, p
|
|
|
1309
1315
|
for (;; ) {
|
|
1310
1316
|
const worktree = await service.find(task);
|
|
1311
1317
|
if (worktree.status === "absent") {
|
|
1312
|
-
fail("not_found", `no task '${task}': run \`arbor
|
|
1318
|
+
fail("not_found", `no task '${task}': run \`arbor list\` to see what there is`, { task });
|
|
1313
1319
|
}
|
|
1314
1320
|
if (!RUNNING.includes(worktree.status)) {
|
|
1315
1321
|
log2.info(report2(worktree));
|
|
@@ -1336,11 +1342,6 @@ function report2(worktree) {
|
|
|
1336
1342
|
}
|
|
1337
1343
|
|
|
1338
1344
|
// arbor.ts
|
|
1339
|
-
var here = async ({
|
|
1340
|
-
service,
|
|
1341
|
-
git,
|
|
1342
|
-
ps
|
|
1343
|
-
}) => service.taskFor(await git.currentBranch(ps.cwd()).catch(() => ""));
|
|
1344
1345
|
var arbor = cli("arbor").use(exits()).use(repository());
|
|
1345
1346
|
arbor.command("add").description("start a new task: create branch task/<task>, a worktree at ../<repo>-arbor/<task> and a state record").arg("task", t.string(), { description: "task name (lowercase-with-dashes)" }).option("base", t.string(), {
|
|
1346
1347
|
default: "",
|
|
@@ -1352,8 +1353,8 @@ arbor.command("rm").description("discard a task: worktree, branch and state file
|
|
|
1352
1353
|
default: false,
|
|
1353
1354
|
description: "discard even when another agent holds the lease"
|
|
1354
1355
|
}).action((opts, ctx) => ctx.journal.record("rm", opts.task, () => rm(ctx, opts.task, { force: opts.force })));
|
|
1355
|
-
arbor.command("
|
|
1356
|
-
arbor.command("show").description("read one task without touching it: everything `
|
|
1356
|
+
arbor.command("list").description("list every task: name, status, lease (held: an agent is on it now; stale: gone quiet, normal for a task mid-edit; none), commits ahead of trunk, age").option("json", t.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => list(ctx, { json: opts.json }));
|
|
1357
|
+
arbor.command("show").description("read one task without touching it: everything `list` shows for it, plus the ARBOR.md its agent left at the worktree root; takes no lease, so it cannot knock that agent off its own tree").arg("task", t.string(), { description: "task name" }).option("json", t.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => show(ctx, opts.task, { json: opts.json }));
|
|
1357
1358
|
arbor.command("wait").description("block until a task stops moving: escalated, or gone (merged or removed), or broken; takes no lease, and gives up with `timeout` rather than waiting forever").arg("task", t.string(), { description: "task name" }).option("timeout-secs", t.number(), {
|
|
1358
1359
|
default: DEFAULT_TIMEOUT.secs,
|
|
1359
1360
|
description: "how long to wait before giving up"
|
|
@@ -1364,7 +1365,7 @@ arbor.command("log").description("show what has been done here recently: one lin
|
|
|
1364
1365
|
default: DEFAULT_COUNT,
|
|
1365
1366
|
description: "how many entries to show"
|
|
1366
1367
|
}).option("json", t.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => log(ctx, { count: opts.count, json: opts.json }));
|
|
1367
|
-
arbor.command("dev").description("serve `
|
|
1368
|
+
arbor.command("dev").description("serve `list`, `show` and `log` as a web page; read-only").option("port", t.number(), {
|
|
1368
1369
|
default: DEFAULT_PORT,
|
|
1369
1370
|
description: "port to listen on, or the next open one above it"
|
|
1370
1371
|
}).action((opts, ctx) => dev(ctx, { ports: devPorts(opts.port) }));
|
|
@@ -1375,8 +1376,17 @@ arbor.command("path").description("print a task's worktree path, or the main tre
|
|
|
1375
1376
|
arbor.command("escalate").description("hand this task to a human and stop: records the reason, drops the lease and leaves the worktree exactly as it is; use instead of resolving a genuine conflict badly just to finish").arg("reason", t.string(), { description: "why this needs a human" }).option("task", t.string(), {
|
|
1376
1377
|
default: "",
|
|
1377
1378
|
description: "task name, when run outside its worktree"
|
|
1378
|
-
}).action(async (opts, ctx) => ctx.journal.record("escalate", opts.task || await here(ctx), () => escalate(ctx, opts.reason, ctx.ps.cwd(),
|
|
1379
|
+
}).action(async (opts, ctx) => ctx.journal.record("escalate", opts.task || await here(ctx), () => escalate(ctx, opts.reason, ctx.ps.cwd(), {
|
|
1380
|
+
task: opts.task || undefined
|
|
1381
|
+
})));
|
|
1379
1382
|
arbor.command("retry").description("give an escalated task another mergeRetryCount merge attempts and put it back to working; the way out of `budget_exhausted` that is not rm and redo, and only from escalated, so a human has seen the tree first").arg("task", t.string(), { description: "task name" }).action((opts, ctx) => ctx.journal.record("retry", opts.task, () => retry(ctx, opts.task)));
|
|
1383
|
+
async function here({
|
|
1384
|
+
service,
|
|
1385
|
+
git,
|
|
1386
|
+
ps
|
|
1387
|
+
}) {
|
|
1388
|
+
return service.taskFor(await git.currentBranch(ps.cwd()).catch(() => ""));
|
|
1389
|
+
}
|
|
1380
1390
|
|
|
1381
1391
|
// dev/build/main.txt
|
|
1382
1392
|
var main_default = `var __create = Object.create;
|
|
@@ -18914,7 +18924,7 @@ class Feed {
|
|
|
18914
18924
|
stream.onerror = () => this.set({ offline: true });
|
|
18915
18925
|
this.load();
|
|
18916
18926
|
}
|
|
18917
|
-
|
|
18927
|
+
dispose() {
|
|
18918
18928
|
this.stream?.close();
|
|
18919
18929
|
this.stream = null;
|
|
18920
18930
|
}
|
|
@@ -19131,7 +19141,7 @@ function App() {
|
|
|
19131
19141
|
const [feed] = import_react6.useState(() => new Feed);
|
|
19132
19142
|
useDisposerEffect((disposer) => {
|
|
19133
19143
|
feed.start();
|
|
19134
|
-
disposer.
|
|
19144
|
+
disposer.use(feed);
|
|
19135
19145
|
}, [feed]);
|
|
19136
19146
|
const { snapshot, offline } = useReactive(feed, (feed2) => ({ snapshot: feed2.snapshot, offline: feed2.offline }), ["changed"]);
|
|
19137
19147
|
const [tab, setTab] = import_react6.useState("tasks");
|
package/{ls.d.ts → list.d.ts}
RENAMED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { type Logger } from "webappwiz/log";
|
|
2
2
|
import type { WorktreeService } from "./worktree-service.js";
|
|
3
|
-
export interface
|
|
3
|
+
export interface ListOptions {
|
|
4
4
|
/** Print the rows as JSON instead of a table. */
|
|
5
5
|
json?: boolean;
|
|
6
6
|
}
|
|
7
|
-
export declare function
|
|
7
|
+
export declare function list({ service, log }: {
|
|
8
8
|
service: WorktreeService;
|
|
9
9
|
log: Logger;
|
|
10
|
-
}, { json }?:
|
|
10
|
+
}, { json }?: ListOptions): Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webappwiz/arbor",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.13",
|
|
4
4
|
"description": "Runs several AI coding agents on one repository at once, each in its own git worktree",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Jared Johnson",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"webappwiz": "^0.0.
|
|
20
|
+
"webappwiz": "^0.0.13"
|
|
21
21
|
},
|
|
22
22
|
"main": "./index.js",
|
|
23
23
|
"types": "./index.d.ts",
|
package/shell.d.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
import { type Ps, type SpawnCaptureResult, type SpawnOptions, type SpawnResult } from "webappwiz/system";
|
|
2
|
-
/** Spawn options for a shell command. `cwd` is the worktree it runs against. */
|
|
3
|
-
export type ShellRunOptions = SpawnOptions & {
|
|
4
|
-
cwd: string;
|
|
5
|
-
};
|
|
6
2
|
/** What a `Shell` spawns through; the real process by default. */
|
|
7
3
|
export interface ShellOptions {
|
|
8
4
|
ps?: Ps;
|
|
@@ -15,7 +11,7 @@ export declare class Shell {
|
|
|
15
11
|
private readonly ps;
|
|
16
12
|
constructor(opts?: ShellOptions);
|
|
17
13
|
/** Captures output, for commands whose failure has to be reported back. */
|
|
18
|
-
run(command: string,
|
|
14
|
+
run(command: string, cwd: string, opts?: SpawnOptions): Promise<SpawnCaptureResult>;
|
|
19
15
|
/** Inherits stdio, for commands the agent should watch as they run. */
|
|
20
|
-
stream(command: string,
|
|
16
|
+
stream(command: string, cwd: string, opts?: SpawnOptions): Promise<SpawnResult>;
|
|
21
17
|
}
|
package/show.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export interface ShowOptions {
|
|
|
23
23
|
json?: boolean;
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
|
-
* One task in full: what `
|
|
26
|
+
* One task in full: what `list` shows for it, plus the `ARBOR.md` its agent
|
|
27
27
|
* left at the worktree root. Reading a tree this way takes no lease, so it
|
|
28
28
|
* cannot knock the agent driving it off its own work.
|
|
29
29
|
*/
|
|
@@ -31,13 +31,18 @@ export declare function show({ service, fs, log }: {
|
|
|
31
31
|
service: WorktreeService;
|
|
32
32
|
fs: Fs;
|
|
33
33
|
log: Logger;
|
|
34
|
-
}, task: string,
|
|
34
|
+
}, task: string, opts?: ShowOptions): Promise<void>;
|
|
35
|
+
/** Configures how {@link TaskDetails} gathers a task's info. */
|
|
36
|
+
export interface TaskDetailsOptions {
|
|
37
|
+
/** What the worktree is read through; the real filesystem by default. */
|
|
38
|
+
fs?: Fs;
|
|
39
|
+
}
|
|
35
40
|
/**
|
|
36
41
|
* Everything there is to say about one task. Split out so `dev` can render the
|
|
37
42
|
* same fields this prints, rather than assembling its own and drifting.
|
|
38
43
|
*/
|
|
39
|
-
export
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
export declare class TaskDetails {
|
|
45
|
+
private readonly fs;
|
|
46
|
+
constructor(opts?: TaskDetailsOptions);
|
|
47
|
+
get(worktree: Worktree): Promise<Details>;
|
|
42
48
|
}
|
|
43
|
-
export declare function taskDetails(worktree: Worktree, opts?: TaskDetailsOptions): Promise<Details>;
|
package/snapshot.d.ts
CHANGED
|
@@ -2,13 +2,13 @@ import { type Fs } from "webappwiz/system";
|
|
|
2
2
|
import type { Entry, Journal } from "./journal.js";
|
|
3
3
|
import { type Details } from "./show.js";
|
|
4
4
|
import type { WorktreeService } from "./worktree-service.js";
|
|
5
|
-
/** Everything one page shows: `
|
|
5
|
+
/** Everything one page shows: `list` and `show` for each task, plus `log`. */
|
|
6
6
|
export interface Snapshot {
|
|
7
7
|
tasks: Details[];
|
|
8
8
|
entries: Entry[];
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
|
-
* Reads the whole repo the way `
|
|
11
|
+
* Reads the whole repo the way `list`, `show` and `log` do. Takes no lease, so
|
|
12
12
|
* serving a page cannot knock an agent off the tree it is driving.
|
|
13
13
|
*/
|
|
14
14
|
export interface SnapshotOptions {
|