@webappwiz/arbor 0.0.15 → 0.0.16
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/arbor.d.ts +0 -3
- package/dev.d.ts +1 -3
- package/index.js +37 -48
- package/package.json +3 -2
package/arbor.d.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { type Deps } from "webappwiz/cmd";
|
|
2
|
-
import type { HttpServer } from "webappwiz/http";
|
|
3
2
|
import type { Fs } from "webappwiz/system";
|
|
4
3
|
import type { Assets } from "./dev/assets.js";
|
|
5
4
|
import { type Repository } from "./repository.js";
|
|
6
5
|
/** Everything `arbor` is run with, before the repository middleware adds to it. */
|
|
7
6
|
export interface ArborDeps extends Deps {
|
|
8
7
|
fs: Fs;
|
|
9
|
-
/** HTTP server `dev` listens on to serve its preview. */
|
|
10
|
-
http: HttpServer;
|
|
11
8
|
/** The built page `dev` serves as its preview. */
|
|
12
9
|
assets: Assets;
|
|
13
10
|
}
|
package/dev.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type AsyncResource } from "webappwiz/disposable";
|
|
2
|
-
import type { HttpServer } from "webappwiz/http";
|
|
3
2
|
import type { Logger } from "webappwiz/log";
|
|
4
3
|
import { type Fs, type PortProvider } from "webappwiz/system";
|
|
5
4
|
import type { Assets } from "./dev/assets.js";
|
|
@@ -23,12 +22,11 @@ export interface DevServer extends AsyncResource {
|
|
|
23
22
|
* repo changes. Read-only on purpose: driving arbor is what the CLI is for, and
|
|
24
23
|
* a button that took a lease would fight the agent holding it.
|
|
25
24
|
*/
|
|
26
|
-
export declare function dev({ service, fs, journal, log,
|
|
25
|
+
export declare function dev({ service, fs, journal, log, assets, }: {
|
|
27
26
|
service: WorktreeService;
|
|
28
27
|
fs: Fs;
|
|
29
28
|
journal: Journal;
|
|
30
29
|
log: Logger;
|
|
31
|
-
http: HttpServer;
|
|
32
30
|
assets: Assets;
|
|
33
31
|
}, { ports }?: DevOptions): Promise<DevServer>;
|
|
34
32
|
/**
|
package/index.js
CHANGED
|
@@ -2,13 +2,12 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
4
|
// index.ts
|
|
5
|
-
import { BunHttpServer } from "webappwiz/http/bun";
|
|
6
5
|
import { NodeFs as NodeFs7 } from "webappwiz/system";
|
|
7
6
|
|
|
8
7
|
// arbor.ts
|
|
9
8
|
import { cli } from "webappwiz/cmd";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
9
|
+
import { Duration as Duration2 } from "webappwiz/time";
|
|
10
|
+
import { z } from "zod";
|
|
12
11
|
|
|
13
12
|
// add.ts
|
|
14
13
|
import { color } from "webappwiz/log";
|
|
@@ -167,7 +166,6 @@ import {
|
|
|
167
166
|
MAX_PORT,
|
|
168
167
|
OpenPortProvider
|
|
169
168
|
} from "webappwiz/system";
|
|
170
|
-
import { Duration } from "webappwiz/time";
|
|
171
169
|
|
|
172
170
|
// snapshot.ts
|
|
173
171
|
import { NodeFs as NodeFs2 } from "webappwiz/system";
|
|
@@ -382,7 +380,6 @@ async function dev({
|
|
|
382
380
|
fs,
|
|
383
381
|
journal,
|
|
384
382
|
log: log2,
|
|
385
|
-
http,
|
|
386
383
|
assets
|
|
387
384
|
}, { ports = devPorts(DEFAULT_PORT) } = {}) {
|
|
388
385
|
const open = new Set;
|
|
@@ -425,34 +422,27 @@ async function dev({
|
|
|
425
422
|
}
|
|
426
423
|
});
|
|
427
424
|
};
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
case "/events":
|
|
445
|
-
return events();
|
|
446
|
-
default:
|
|
447
|
-
return new Response("not found", { status: 404 });
|
|
448
|
-
}
|
|
449
|
-
}, { port: await ports.get(), idleTimeout: Duration.zero() });
|
|
450
|
-
log2.info(`arbor dev on http://localhost:${listening.port}`);
|
|
425
|
+
const asset = (body, type) => new Response(body, { headers: { "content-type": type } });
|
|
426
|
+
const requested = await ports.get();
|
|
427
|
+
const server = Bun.serve({
|
|
428
|
+
port: requested,
|
|
429
|
+
idleTimeout: 0,
|
|
430
|
+
routes: {
|
|
431
|
+
"/": asset(assets.shell, "text/html; charset=utf-8"),
|
|
432
|
+
"/main.js": asset(assets.script, "text/javascript; charset=utf-8"),
|
|
433
|
+
"/styles.css": asset(assets.styles, "text/css; charset=utf-8"),
|
|
434
|
+
"/api/snapshot": async () => Response.json(await snapshot(service, journal, { fs })),
|
|
435
|
+
"/events": events
|
|
436
|
+
},
|
|
437
|
+
fetch: () => new Response("not found", { status: 404 })
|
|
438
|
+
});
|
|
439
|
+
const port = server.port ?? requested;
|
|
440
|
+
log2.info(`arbor dev on http://localhost:${port}`);
|
|
451
441
|
const disposer = new AsyncDisposer;
|
|
452
|
-
disposer.defer(() =>
|
|
442
|
+
disposer.defer(() => server.stop(true));
|
|
453
443
|
disposer.defer(async () => clearInterval(poll));
|
|
454
444
|
return {
|
|
455
|
-
port
|
|
445
|
+
port,
|
|
456
446
|
disposeAsync: disposer.disposeAsync
|
|
457
447
|
};
|
|
458
448
|
}
|
|
@@ -1306,9 +1296,9 @@ async function retry({
|
|
|
1306
1296
|
|
|
1307
1297
|
// wait.ts
|
|
1308
1298
|
import { color as color11 } from "webappwiz/log";
|
|
1309
|
-
import { Duration
|
|
1310
|
-
var DEFAULT_TIMEOUT =
|
|
1311
|
-
var POLL =
|
|
1299
|
+
import { Duration, sleep } from "webappwiz/time";
|
|
1300
|
+
var DEFAULT_TIMEOUT = Duration.mins(5);
|
|
1301
|
+
var POLL = Duration.secs(2);
|
|
1312
1302
|
var RUNNING = ["working", "merging"];
|
|
1313
1303
|
async function wait({ service, log: log2 }, task, { timeout = DEFAULT_TIMEOUT, poll = POLL } = {}) {
|
|
1314
1304
|
const deadline = Date.now() + timeout.ms;
|
|
@@ -1325,7 +1315,7 @@ async function wait({ service, log: log2 }, task, { timeout = DEFAULT_TIMEOUT, p
|
|
|
1325
1315
|
if (left <= 0) {
|
|
1326
1316
|
fail("timeout", `'${task}' is still ${worktree.status} after ${timeout.secs}s: wait again, work alongside it and accept the rebase, or ask the human`, { task, status: worktree.status });
|
|
1327
1317
|
}
|
|
1328
|
-
await sleep(
|
|
1318
|
+
await sleep(Duration.min(poll, Duration.ms(left)));
|
|
1329
1319
|
}
|
|
1330
1320
|
}
|
|
1331
1321
|
function report2(worktree) {
|
|
@@ -1343,43 +1333,43 @@ function report2(worktree) {
|
|
|
1343
1333
|
|
|
1344
1334
|
// arbor.ts
|
|
1345
1335
|
var arbor = cli("arbor").use(exits()).use(repository());
|
|
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",
|
|
1336
|
+
arbor.command("add").description("start a new task: create branch task/<task>, a worktree at ../<repo>-arbor/<task> and a state record").arg("task", z.string(), { description: "task name (lowercase-with-dashes)" }).option("base", z.string(), {
|
|
1347
1337
|
default: "",
|
|
1348
1338
|
description: "branch this task starts from and merges onto (default: trunk); `task/<other>` stacks this task on that one and lands the work in its worktree"
|
|
1349
1339
|
}).action((opts, ctx) => ctx.journal.record("add", opts.task, () => add(ctx, opts.task, { base: opts.base || undefined })));
|
|
1350
|
-
arbor.command("claim").description("resume an existing task: take ownership of its worktree and print its path, status and any half-finished rebase; refuses while another agent holds the lease, but takes a stale one silently, so `arbor show` first if the tree may not be abandoned").arg("task",
|
|
1340
|
+
arbor.command("claim").description("resume an existing task: take ownership of its worktree and print its path, status and any half-finished rebase; refuses while another agent holds the lease, but takes a stale one silently, so `arbor show` first if the tree may not be abandoned").arg("task", z.string(), { description: "task name" }).action((opts, ctx) => ctx.journal.record("claim", opts.task, () => claim(ctx, opts.task)));
|
|
1351
1341
|
arbor.command("merge").description("land this worktree's branch on its base (trunk unless created with --base): rebase onto it, run tests on the rebased code, fast-forward it in whichever worktree has it checked out, then discard the worktree, branch and record (linear history, never a merge commit, no flag to skip tests); requires committed work, refusing a dirty worktree").action(async (_opts, ctx) => ctx.journal.record("merge", await here(ctx), () => merge(ctx, ctx.ps.cwd())));
|
|
1352
|
-
arbor.command("remove").description("discard a task: worktree, branch and state file; cheap and encouraged, since redoing a task against current trunk often beats a hard rebase").arg("task",
|
|
1342
|
+
arbor.command("remove").description("discard a task: worktree, branch and state file; cheap and encouraged, since redoing a task against current trunk often beats a hard rebase").arg("task", z.string(), { description: "task name" }).option("force", z.string().transform((raw) => raw !== "false"), {
|
|
1353
1343
|
default: false,
|
|
1354
1344
|
description: "discard even when another agent holds the lease"
|
|
1355
1345
|
}).action((opts, ctx) => ctx.journal.record("remove", opts.task, () => remove(ctx, opts.task, { force: opts.force })));
|
|
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",
|
|
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",
|
|
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",
|
|
1346
|
+
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", z.string().transform((raw) => raw !== "false"), { default: false, description: "emit JSON" }).action((opts, ctx) => list(ctx, { json: opts.json }));
|
|
1347
|
+
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", z.string(), { description: "task name" }).option("json", z.string().transform((raw) => raw !== "false"), { default: false, description: "emit JSON" }).action((opts, ctx) => show(ctx, opts.task, { json: opts.json }));
|
|
1348
|
+
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", z.string(), { description: "task name" }).option("timeout-secs", z.coerce.number(), {
|
|
1359
1349
|
default: DEFAULT_TIMEOUT.secs,
|
|
1360
1350
|
description: "how long to wait before giving up"
|
|
1361
1351
|
}).action((opts, ctx) => wait(ctx, opts.task, {
|
|
1362
|
-
timeout:
|
|
1352
|
+
timeout: Duration2.secs(opts["timeout-secs"])
|
|
1363
1353
|
}));
|
|
1364
|
-
arbor.command("log").description("show what has been done here recently: one line per add, claim, merge, remove, escalate and retry, with how it ended; outlives the tasks themselves").option("count",
|
|
1354
|
+
arbor.command("log").description("show what has been done here recently: one line per add, claim, merge, remove, escalate and retry, with how it ended; outlives the tasks themselves").option("count", z.coerce.number(), {
|
|
1365
1355
|
default: DEFAULT_COUNT,
|
|
1366
1356
|
description: "how many entries to show"
|
|
1367
|
-
}).option("json",
|
|
1368
|
-
arbor.command("dev").description("serve `list`, `show` and `log` as a web page; read-only").option("port",
|
|
1357
|
+
}).option("json", z.string().transform((raw) => raw !== "false"), { default: false, description: "emit JSON" }).action((opts, ctx) => log(ctx, { count: opts.count, json: opts.json }));
|
|
1358
|
+
arbor.command("dev").description("serve `list`, `show` and `log` as a web page; read-only").option("port", z.coerce.number(), {
|
|
1369
1359
|
default: DEFAULT_PORT,
|
|
1370
1360
|
description: "port to listen on, or the next open one above it"
|
|
1371
1361
|
}).action((opts, ctx) => dev(ctx, { ports: devPorts(opts.port) }));
|
|
1372
|
-
arbor.command("path").description("print a task's worktree path, or the main tree with no task; names another agent's tree without taking its lease").arg("task",
|
|
1362
|
+
arbor.command("path").description("print a task's worktree path, or the main tree with no task; names another agent's tree without taking its lease").arg("task", z.string(), {
|
|
1373
1363
|
default: "",
|
|
1374
1364
|
description: "task name; omit for the main tree"
|
|
1375
1365
|
}).action((opts, ctx) => path(ctx, opts.task || undefined));
|
|
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",
|
|
1366
|
+
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", z.string(), { description: "why this needs a human" }).option("task", z.string(), {
|
|
1377
1367
|
default: "",
|
|
1378
1368
|
description: "task name, when run outside its worktree"
|
|
1379
1369
|
}).action(async (opts, ctx) => ctx.journal.record("escalate", opts.task || await here(ctx), () => escalate(ctx, opts.reason, ctx.ps.cwd(), {
|
|
1380
1370
|
task: opts.task || undefined
|
|
1381
1371
|
})));
|
|
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 remove and redo, and only from escalated, so a human has seen the tree first").arg("task",
|
|
1372
|
+
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 remove and redo, and only from escalated, so a human has seen the tree first").arg("task", z.string(), { description: "task name" }).action((opts, ctx) => ctx.journal.record("retry", opts.task, () => retry(ctx, opts.task)));
|
|
1383
1373
|
async function here({
|
|
1384
1374
|
service,
|
|
1385
1375
|
git,
|
|
@@ -20033,6 +20023,5 @@ var assets = { shell: shell_default, script: main_default, styles: styles_defaul
|
|
|
20033
20023
|
// index.ts
|
|
20034
20024
|
await arbor.run({
|
|
20035
20025
|
fs: new NodeFs7,
|
|
20036
|
-
http: new BunHttpServer,
|
|
20037
20026
|
assets
|
|
20038
20027
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webappwiz/arbor",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.16",
|
|
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,8 @@
|
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"webappwiz": "^0.0.
|
|
20
|
+
"webappwiz": "^0.0.16",
|
|
21
|
+
"zod": "^4.4.3"
|
|
21
22
|
},
|
|
22
23
|
"main": "./index.js",
|
|
23
24
|
"types": "./index.d.ts",
|