@webappwiz/arbor 0.0.9 → 0.0.10
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 +34 -11
- package/config.js +5 -6
- package/index.js +20 -25
- package/load-config.d.ts +1 -1
- package/package.json +2 -2
- package/index-84xjmns8.js +0 -20
package/config.d.ts
CHANGED
|
@@ -1,26 +1,49 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export declare const CONFIG_FACTORY: import("webappwiz/config").ConfigFactory<{
|
|
1
|
+
export interface Config {
|
|
2
|
+
/**
|
|
3
|
+
* The integration branch name. `merge` rebases a task onto this branch and
|
|
4
|
+
* then fast-forwards it to the result; new worktrees start from it.
|
|
5
|
+
*/
|
|
7
6
|
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`. */
|
|
9
10
|
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
|
+
*/
|
|
10
16
|
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
|
+
*/
|
|
11
24
|
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
|
+
*/
|
|
12
30
|
postMerge: string | null;
|
|
31
|
+
/** How long since its last heartbeat before a task's lease is up for grabs. */
|
|
13
32
|
leaseStalenessMs: number;
|
|
33
|
+
/** Failed `merge` attempts a task gets before it must escalate or be removed. */
|
|
14
34
|
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
|
+
*/
|
|
15
40
|
removedCapacity: number;
|
|
41
|
+
/** How many entries `arbor log` keeps before the oldest fall off. */
|
|
16
42
|
logCapacity: number;
|
|
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"]>;
|
|
43
|
+
}
|
|
21
44
|
/**
|
|
22
45
|
* Identity, for the types. `export default defineConfig({ ... })` in
|
|
23
46
|
* `arbor.config.ts` gets the key names and their types checked, and completion
|
|
24
47
|
* while writing it; a bare object literal gets neither.
|
|
25
48
|
*/
|
|
26
|
-
export declare function defineConfig(config: Partial<
|
|
49
|
+
export declare function defineConfig(config: Partial<Config>): Partial<Config>;
|
package/config.js
CHANGED
package/index.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
-
import {
|
|
4
|
-
CONFIG_FACTORY
|
|
5
|
-
} from "./index-84xjmns8.js";
|
|
6
3
|
|
|
7
4
|
// index.ts
|
|
8
5
|
import { BunHttpServer } from "webappwiz/http/bun";
|
|
@@ -72,7 +69,7 @@ async function add({
|
|
|
72
69
|
config,
|
|
73
70
|
log,
|
|
74
71
|
fs
|
|
75
|
-
}, task, { base = config.
|
|
72
|
+
}, task, { base = config.trunk } = {}) {
|
|
76
73
|
if (!NAME.test(task)) {
|
|
77
74
|
fail("usage", `invalid task name '${task}': use lowercase letters, digits and dashes`, { task });
|
|
78
75
|
}
|
|
@@ -88,7 +85,7 @@ async function add({
|
|
|
88
85
|
}
|
|
89
86
|
const added = await service.add(task, { base });
|
|
90
87
|
if (added.code !== 0) {
|
|
91
|
-
const missingTrunk = base === config.
|
|
88
|
+
const missingTrunk = base === config.trunk && !await service.git.branchExists(base);
|
|
92
89
|
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 });
|
|
93
90
|
}
|
|
94
91
|
const worktree = await (await service.find(task)).take({ base });
|
|
@@ -102,14 +99,13 @@ ${PLAN_FILE}
|
|
|
102
99
|
`.trimStart());
|
|
103
100
|
}
|
|
104
101
|
await fs.write(`${worktree.path}/${PLAN_FILE}`, PLAN(task));
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const { exitCode } = await shell.stream(postCheckout, {
|
|
102
|
+
if (config.postCheckout) {
|
|
103
|
+
const { exitCode } = await shell.stream(config.postCheckout, {
|
|
108
104
|
cwd: worktree.path,
|
|
109
105
|
env: {
|
|
110
106
|
ARBOR_TASK: task,
|
|
111
107
|
ARBOR_WORKTREE: worktree.path,
|
|
112
|
-
ARBOR_TRUNK: config.
|
|
108
|
+
ARBOR_TRUNK: config.trunk
|
|
113
109
|
}
|
|
114
110
|
});
|
|
115
111
|
if (exitCode !== 0) {
|
|
@@ -582,8 +578,8 @@ async function merge({
|
|
|
582
578
|
if (dirty.length > 0) {
|
|
583
579
|
fail("dirty", `'${task}' has uncommitted changes: commit them before merging`, { task, paths: dirty });
|
|
584
580
|
}
|
|
585
|
-
if (worktree.mergeAttempts >= config.
|
|
586
|
-
fail("budget_exhausted", `'${task}' has used its ${config.
|
|
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 });
|
|
587
583
|
}
|
|
588
584
|
if (worktree.leaseHeldByOther) {
|
|
589
585
|
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 });
|
|
@@ -608,7 +604,7 @@ async function merge({
|
|
|
608
604
|
].join(`
|
|
609
605
|
`), { task, paths });
|
|
610
606
|
}
|
|
611
|
-
const gate = [config.
|
|
607
|
+
const gate = [config.postRewrite, config.preMerge].filter(Boolean).join(" && ");
|
|
612
608
|
const gated = gate ? await shell.run(gate, {
|
|
613
609
|
cwd: worktree.path,
|
|
614
610
|
env: {
|
|
@@ -649,9 +645,8 @@ ${gated.stderr}`)
|
|
|
649
645
|
fail("usage", `landed '${task}' on ${base} (${head}) but could not discard its worktree: ${discarded.stderr || discarded.stdout}
|
|
650
646
|
Run \`arbor rm ${task}\` to clean up.`, { task });
|
|
651
647
|
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
const ran = await shell.run(postMerge, {
|
|
648
|
+
if (config.postMerge) {
|
|
649
|
+
const ran = await shell.run(config.postMerge, {
|
|
655
650
|
cwd: git.root,
|
|
656
651
|
env: { ARBOR_TASK: task }
|
|
657
652
|
});
|
|
@@ -875,7 +870,7 @@ import { NodeFs as NodeFs5 } from "webappwiz/system";
|
|
|
875
870
|
async function loadConfig(root, opts = {}) {
|
|
876
871
|
const found = await file(opts.fs ?? new NodeFs5, root);
|
|
877
872
|
const trunk = found.trunk ?? await (opts.git ?? new Git(root)).defaultBranch() ?? "main";
|
|
878
|
-
return
|
|
873
|
+
return { ...defaults(root, trunk), ...found };
|
|
879
874
|
}
|
|
880
875
|
function defaults(root, trunk) {
|
|
881
876
|
return {
|
|
@@ -956,7 +951,7 @@ class Worktree {
|
|
|
956
951
|
if (!lease) {
|
|
957
952
|
return false;
|
|
958
953
|
}
|
|
959
|
-
if (Date.now() - Date.parse(lease.heartbeatAt) >= config.
|
|
954
|
+
if (Date.now() - Date.parse(lease.heartbeatAt) >= config.leaseStalenessMs) {
|
|
960
955
|
return false;
|
|
961
956
|
}
|
|
962
957
|
return lease.hostname === ps.hostname ? ps.alive(lease.pid) : true;
|
|
@@ -1077,10 +1072,10 @@ class WorktreeService {
|
|
|
1077
1072
|
await this.fs.mkdir(this.removedDir);
|
|
1078
1073
|
}
|
|
1079
1074
|
get trunk() {
|
|
1080
|
-
return this.config.
|
|
1075
|
+
return this.config.trunk;
|
|
1081
1076
|
}
|
|
1082
1077
|
pathFor(task) {
|
|
1083
|
-
return resolve2(this.config.
|
|
1078
|
+
return resolve2(this.config.worktreeRoot, task);
|
|
1084
1079
|
}
|
|
1085
1080
|
branchFor(task) {
|
|
1086
1081
|
return `${BRANCH_PREFIX}${task}`;
|
|
@@ -1122,8 +1117,8 @@ class WorktreeService {
|
|
|
1122
1117
|
}
|
|
1123
1118
|
return found;
|
|
1124
1119
|
}
|
|
1125
|
-
async add(task, { base = this.config.
|
|
1126
|
-
await this.fs.mkdir(this.config.
|
|
1120
|
+
async add(task, { base = this.config.trunk } = {}) {
|
|
1121
|
+
await this.fs.mkdir(this.config.worktreeRoot);
|
|
1127
1122
|
return this.git.addWorktree(this.branchFor(task), this.pathFor(task), base);
|
|
1128
1123
|
}
|
|
1129
1124
|
async discard(worktree) {
|
|
@@ -1160,7 +1155,7 @@ class WorktreeService {
|
|
|
1160
1155
|
return raw?.trim() ?? null;
|
|
1161
1156
|
}
|
|
1162
1157
|
async rememberRemoved(task, at = new Date().toISOString()) {
|
|
1163
|
-
const removedCapacity = this.config
|
|
1158
|
+
const { removedCapacity } = this.config;
|
|
1164
1159
|
await this.fs.write(this.removedPath(task), `${at}
|
|
1165
1160
|
`);
|
|
1166
1161
|
const names = await this.fs.readdir(this.removedDir).catch(() => []);
|
|
@@ -1209,10 +1204,10 @@ function repository(at) {
|
|
|
1209
1204
|
fs,
|
|
1210
1205
|
ps,
|
|
1211
1206
|
log: log2,
|
|
1212
|
-
stalenessMs: config.
|
|
1207
|
+
stalenessMs: config.leaseStalenessMs
|
|
1213
1208
|
}),
|
|
1214
1209
|
shell: new Shell({ ps }),
|
|
1215
|
-
journal: new Journal(`${arborDir}/log.jsonl`, config.
|
|
1210
|
+
journal: new Journal(`${arborDir}/log.jsonl`, config.logCapacity, {
|
|
1216
1211
|
fs
|
|
1217
1212
|
})
|
|
1218
1213
|
});
|
|
@@ -1236,7 +1231,7 @@ async function retry({
|
|
|
1236
1231
|
const worktree = await found.save({ status: "working", mergeAttempts: 0 });
|
|
1237
1232
|
log2.info([
|
|
1238
1233
|
`${color9.green("retry")} ${worktree.task}`,
|
|
1239
|
-
` attempts: 0 of ${config.
|
|
1234
|
+
` attempts: 0 of ${config.mergeRetryCount}`,
|
|
1240
1235
|
` status: working`,
|
|
1241
1236
|
"",
|
|
1242
1237
|
`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 {
|
|
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.10",
|
|
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.10"
|
|
21
21
|
},
|
|
22
22
|
"main": "./index.js",
|
|
23
23
|
"types": "./index.d.ts",
|
package/index-84xjmns8.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
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 };
|