@webappwiz/arbor 0.0.8 → 0.0.9
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/config.d.ts +11 -34
- package/config.js +6 -5
- package/index-84xjmns8.js +20 -0
- package/index.js +25 -20
- package/load-config.d.ts +1 -1
- package/package.json +2 -2
package/config.d.ts
CHANGED
|
@@ -1,49 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { type InferConfig } from "webappwiz/config";
|
|
2
|
+
/**
|
|
3
|
+
* The settings arbor runs a repo with. `loadConfig` builds one; everything a
|
|
4
|
+
* command needs comes off it with `get`.
|
|
5
|
+
*/
|
|
6
|
+
export declare const CONFIG_FACTORY: import("webappwiz/config").ConfigFactory<{
|
|
6
7
|
trunk: string;
|
|
7
|
-
/** Directory holding one worktree per task, a sibling of the repo. */
|
|
8
8
|
worktreeRoot: string;
|
|
9
|
-
/** Command run by `add` in the new worktree, via `sh -c`. */
|
|
10
9
|
postCheckout: string | null;
|
|
11
|
-
/**
|
|
12
|
-
* Command run by `merge` after the rebase, before `preMerge`, via `sh -c`.
|
|
13
|
-
* A rebase can bring in a dependency the worktree has never installed, and
|
|
14
|
-
* whatever `preMerge` runs needs it.
|
|
15
|
-
*/
|
|
16
10
|
postRewrite: string | null;
|
|
17
|
-
/**
|
|
18
|
-
* Command run by `merge` after the rebase, via `sh -c`. The last gate before
|
|
19
|
-
* the branch lands: a nonzero exit rolls the branch back and leaves the base
|
|
20
|
-
* untouched. Tests are the obvious thing to put here, but arbor has no
|
|
21
|
-
* opinion; a repo with nothing to run leaves it null and merges on a green
|
|
22
|
-
* rebase alone.
|
|
23
|
-
*/
|
|
24
11
|
preMerge: string | null;
|
|
25
|
-
/**
|
|
26
|
-
* Command run by `merge` in the main tree after the base fast-forwards, via
|
|
27
|
-
* `sh -c`. The branch has already landed and the worktree is gone: a nonzero
|
|
28
|
-
* exit reports the failure but rolls nothing back.
|
|
29
|
-
*/
|
|
30
12
|
postMerge: string | null;
|
|
31
|
-
/** How long since its last heartbeat before a task's lease is up for grabs. */
|
|
32
13
|
leaseStalenessMs: number;
|
|
33
|
-
/** Failed `merge` attempts a task gets before it must escalate or be removed. */
|
|
34
14
|
mergeRetryCount: number;
|
|
35
|
-
/**
|
|
36
|
-
* How many removed task names to keep, so `rm` can say "already removed"
|
|
37
|
-
* rather than "never existed". A flat cap with no age policy: losing the
|
|
38
|
-
* oldest costs a nicer message and nothing else.
|
|
39
|
-
*/
|
|
40
15
|
removedCapacity: number;
|
|
41
|
-
/** How many entries `arbor log` keeps before the oldest fall off. */
|
|
42
16
|
logCapacity: number;
|
|
43
|
-
}
|
|
17
|
+
}>;
|
|
18
|
+
export type Config = InferConfig<typeof CONFIG_FACTORY>;
|
|
19
|
+
/** The plain shape behind a `Config`: what a config file exports a part of. */
|
|
20
|
+
export type ConfigRecord = ReturnType<Config["toRecord"]>;
|
|
44
21
|
/**
|
|
45
22
|
* Identity, for the types. `export default defineConfig({ ... })` in
|
|
46
23
|
* `arbor.config.ts` gets the key names and their types checked, and completion
|
|
47
24
|
* while writing it; a bare object literal gets neither.
|
|
48
25
|
*/
|
|
49
|
-
export declare function defineConfig(config: Partial<
|
|
26
|
+
export declare function defineConfig(config: Partial<ConfigRecord>): Partial<ConfigRecord>;
|
package/config.js
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// config.ts
|
|
2
|
+
import { Config as WizConfig } from "webappwiz/config";
|
|
3
|
+
import { t } from "webappwiz/t";
|
|
4
|
+
var CONFIG_FACTORY = WizConfig.factory({
|
|
5
|
+
trunk: t.string(),
|
|
6
|
+
worktreeRoot: t.string(),
|
|
7
|
+
postCheckout: t.nullable(t.string()),
|
|
8
|
+
postRewrite: t.nullable(t.string()),
|
|
9
|
+
preMerge: t.nullable(t.string()),
|
|
10
|
+
postMerge: t.nullable(t.string()),
|
|
11
|
+
leaseStalenessMs: t.number(),
|
|
12
|
+
mergeRetryCount: t.number(),
|
|
13
|
+
removedCapacity: t.number(),
|
|
14
|
+
logCapacity: t.number()
|
|
15
|
+
});
|
|
16
|
+
function defineConfig(config) {
|
|
17
|
+
return config;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { CONFIG_FACTORY, defineConfig };
|
package/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
+
import {
|
|
4
|
+
CONFIG_FACTORY
|
|
5
|
+
} from "./index-84xjmns8.js";
|
|
3
6
|
|
|
4
7
|
// index.ts
|
|
5
8
|
import { BunHttpServer } from "webappwiz/http/bun";
|
|
@@ -69,7 +72,7 @@ async function add({
|
|
|
69
72
|
config,
|
|
70
73
|
log,
|
|
71
74
|
fs
|
|
72
|
-
}, task, { base = config.trunk } = {}) {
|
|
75
|
+
}, task, { base = config.get("trunk") } = {}) {
|
|
73
76
|
if (!NAME.test(task)) {
|
|
74
77
|
fail("usage", `invalid task name '${task}': use lowercase letters, digits and dashes`, { task });
|
|
75
78
|
}
|
|
@@ -85,7 +88,7 @@ async function add({
|
|
|
85
88
|
}
|
|
86
89
|
const added = await service.add(task, { base });
|
|
87
90
|
if (added.code !== 0) {
|
|
88
|
-
const missingTrunk = base === config.trunk && !await service.git.branchExists(base);
|
|
91
|
+
const missingTrunk = base === config.get("trunk") && !await service.git.branchExists(base);
|
|
89
92
|
fail("usage", missingTrunk ? `trunk '${base}' is not a branch in this repo: set \`trunk\` in arbor.config.ts` : `git worktree add failed: ${added.stderr}`, { task });
|
|
90
93
|
}
|
|
91
94
|
const worktree = await (await service.find(task)).take({ base });
|
|
@@ -99,13 +102,14 @@ ${PLAN_FILE}
|
|
|
99
102
|
`.trimStart());
|
|
100
103
|
}
|
|
101
104
|
await fs.write(`${worktree.path}/${PLAN_FILE}`, PLAN(task));
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
const postCheckout = config.get("postCheckout");
|
|
106
|
+
if (postCheckout) {
|
|
107
|
+
const { exitCode } = await shell.stream(postCheckout, {
|
|
104
108
|
cwd: worktree.path,
|
|
105
109
|
env: {
|
|
106
110
|
ARBOR_TASK: task,
|
|
107
111
|
ARBOR_WORKTREE: worktree.path,
|
|
108
|
-
ARBOR_TRUNK: config.trunk
|
|
112
|
+
ARBOR_TRUNK: config.get("trunk")
|
|
109
113
|
}
|
|
110
114
|
});
|
|
111
115
|
if (exitCode !== 0) {
|
|
@@ -578,8 +582,8 @@ async function merge({
|
|
|
578
582
|
if (dirty.length > 0) {
|
|
579
583
|
fail("dirty", `'${task}' has uncommitted changes: commit them before merging`, { task, paths: dirty });
|
|
580
584
|
}
|
|
581
|
-
if (worktree.mergeAttempts >= config.mergeRetryCount) {
|
|
582
|
-
fail("budget_exhausted", `'${task}' has used its ${config.mergeRetryCount} merge attempts: run \`arbor escalate <reason>\`, and a human can grant another ${config.mergeRetryCount} with \`arbor retry ${task}\`; or \`arbor rm ${task}\` and start over against current ${base}`, { task, mergeAttempts: worktree.mergeAttempts });
|
|
585
|
+
if (worktree.mergeAttempts >= config.get("mergeRetryCount")) {
|
|
586
|
+
fail("budget_exhausted", `'${task}' has used its ${config.get("mergeRetryCount")} merge attempts: run \`arbor escalate <reason>\`, and a human can grant another ${config.get("mergeRetryCount")} with \`arbor retry ${task}\`; or \`arbor rm ${task}\` and start over against current ${base}`, { task, mergeAttempts: worktree.mergeAttempts });
|
|
583
587
|
}
|
|
584
588
|
if (worktree.leaseHeldByOther) {
|
|
585
589
|
fail("lease_held", `'${task}' is held by pid ${worktree.lease?.pid} on ${worktree.lease?.hostname}: another agent is driving this tree`, { task, lease: worktree.lease });
|
|
@@ -604,7 +608,7 @@ async function merge({
|
|
|
604
608
|
].join(`
|
|
605
609
|
`), { task, paths });
|
|
606
610
|
}
|
|
607
|
-
const gate = [config.postRewrite, config.preMerge].filter(Boolean).join(" && ");
|
|
611
|
+
const gate = [config.get("postRewrite"), config.get("preMerge")].filter(Boolean).join(" && ");
|
|
608
612
|
const gated = gate ? await shell.run(gate, {
|
|
609
613
|
cwd: worktree.path,
|
|
610
614
|
env: {
|
|
@@ -645,8 +649,9 @@ ${gated.stderr}`)
|
|
|
645
649
|
fail("usage", `landed '${task}' on ${base} (${head}) but could not discard its worktree: ${discarded.stderr || discarded.stdout}
|
|
646
650
|
Run \`arbor rm ${task}\` to clean up.`, { task });
|
|
647
651
|
}
|
|
648
|
-
|
|
649
|
-
|
|
652
|
+
const postMerge = config.get("postMerge");
|
|
653
|
+
if (postMerge) {
|
|
654
|
+
const ran = await shell.run(postMerge, {
|
|
650
655
|
cwd: git.root,
|
|
651
656
|
env: { ARBOR_TASK: task }
|
|
652
657
|
});
|
|
@@ -870,7 +875,7 @@ import { NodeFs as NodeFs5 } from "webappwiz/system";
|
|
|
870
875
|
async function loadConfig(root, opts = {}) {
|
|
871
876
|
const found = await file(opts.fs ?? new NodeFs5, root);
|
|
872
877
|
const trunk = found.trunk ?? await (opts.git ?? new Git(root)).defaultBranch() ?? "main";
|
|
873
|
-
return { ...defaults(root, trunk), ...found };
|
|
878
|
+
return CONFIG_FACTORY.create({ ...defaults(root, trunk), ...found });
|
|
874
879
|
}
|
|
875
880
|
function defaults(root, trunk) {
|
|
876
881
|
return {
|
|
@@ -951,7 +956,7 @@ class Worktree {
|
|
|
951
956
|
if (!lease) {
|
|
952
957
|
return false;
|
|
953
958
|
}
|
|
954
|
-
if (Date.now() - Date.parse(lease.heartbeatAt) >= config.leaseStalenessMs) {
|
|
959
|
+
if (Date.now() - Date.parse(lease.heartbeatAt) >= config.get("leaseStalenessMs")) {
|
|
955
960
|
return false;
|
|
956
961
|
}
|
|
957
962
|
return lease.hostname === ps.hostname ? ps.alive(lease.pid) : true;
|
|
@@ -1072,10 +1077,10 @@ class WorktreeService {
|
|
|
1072
1077
|
await this.fs.mkdir(this.removedDir);
|
|
1073
1078
|
}
|
|
1074
1079
|
get trunk() {
|
|
1075
|
-
return this.config.trunk;
|
|
1080
|
+
return this.config.get("trunk");
|
|
1076
1081
|
}
|
|
1077
1082
|
pathFor(task) {
|
|
1078
|
-
return resolve2(this.config.worktreeRoot, task);
|
|
1083
|
+
return resolve2(this.config.get("worktreeRoot"), task);
|
|
1079
1084
|
}
|
|
1080
1085
|
branchFor(task) {
|
|
1081
1086
|
return `${BRANCH_PREFIX}${task}`;
|
|
@@ -1117,8 +1122,8 @@ class WorktreeService {
|
|
|
1117
1122
|
}
|
|
1118
1123
|
return found;
|
|
1119
1124
|
}
|
|
1120
|
-
async add(task, { base = this.config.trunk } = {}) {
|
|
1121
|
-
await this.fs.mkdir(this.config.worktreeRoot);
|
|
1125
|
+
async add(task, { base = this.config.get("trunk") } = {}) {
|
|
1126
|
+
await this.fs.mkdir(this.config.get("worktreeRoot"));
|
|
1122
1127
|
return this.git.addWorktree(this.branchFor(task), this.pathFor(task), base);
|
|
1123
1128
|
}
|
|
1124
1129
|
async discard(worktree) {
|
|
@@ -1155,7 +1160,7 @@ class WorktreeService {
|
|
|
1155
1160
|
return raw?.trim() ?? null;
|
|
1156
1161
|
}
|
|
1157
1162
|
async rememberRemoved(task, at = new Date().toISOString()) {
|
|
1158
|
-
const
|
|
1163
|
+
const removedCapacity = this.config.get("removedCapacity");
|
|
1159
1164
|
await this.fs.write(this.removedPath(task), `${at}
|
|
1160
1165
|
`);
|
|
1161
1166
|
const names = await this.fs.readdir(this.removedDir).catch(() => []);
|
|
@@ -1204,10 +1209,10 @@ function repository(at) {
|
|
|
1204
1209
|
fs,
|
|
1205
1210
|
ps,
|
|
1206
1211
|
log: log2,
|
|
1207
|
-
stalenessMs: config.leaseStalenessMs
|
|
1212
|
+
stalenessMs: config.get("leaseStalenessMs")
|
|
1208
1213
|
}),
|
|
1209
1214
|
shell: new Shell({ ps }),
|
|
1210
|
-
journal: new Journal(`${arborDir}/log.jsonl`, config.logCapacity, {
|
|
1215
|
+
journal: new Journal(`${arborDir}/log.jsonl`, config.get("logCapacity"), {
|
|
1211
1216
|
fs
|
|
1212
1217
|
})
|
|
1213
1218
|
});
|
|
@@ -1231,7 +1236,7 @@ async function retry({
|
|
|
1231
1236
|
const worktree = await found.save({ status: "working", mergeAttempts: 0 });
|
|
1232
1237
|
log2.info([
|
|
1233
1238
|
`${color9.green("retry")} ${worktree.task}`,
|
|
1234
|
-
` attempts: 0 of ${config.mergeRetryCount}`,
|
|
1239
|
+
` attempts: 0 of ${config.get("mergeRetryCount")}`,
|
|
1235
1240
|
` status: working`,
|
|
1236
1241
|
"",
|
|
1237
1242
|
`Run \`arbor claim ${worktree.task}\` to pick it up.`
|
package/load-config.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Fs } from "webappwiz/system";
|
|
2
|
-
import type
|
|
2
|
+
import { type Config } from "./config.js";
|
|
3
3
|
import { Git } from "./git.js";
|
|
4
4
|
export interface LoadConfigOptions {
|
|
5
5
|
/** What `arbor.config.ts` is looked for through; the real one by default. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webappwiz/arbor",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
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.9"
|
|
21
21
|
},
|
|
22
22
|
"main": "./index.js",
|
|
23
23
|
"types": "./index.d.ts",
|