@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 CHANGED
@@ -1,49 +1,26 @@
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
- */
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<Config>): Partial<Config>;
26
+ export declare function defineConfig(config: Partial<ConfigRecord>): Partial<ConfigRecord>;
package/config.js CHANGED
@@ -1,7 +1,8 @@
1
- // config.ts
2
- function defineConfig(config) {
3
- return config;
4
- }
5
- export {
1
+ import {
2
+ CONFIG_FACTORY,
6
3
  defineConfig
4
+ } from "./index-84xjmns8.js";
5
+ export {
6
+ defineConfig,
7
+ CONFIG_FACTORY
7
8
  };
@@ -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
- if (config.postCheckout) {
103
- const { exitCode } = await shell.stream(config.postCheckout, {
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
- if (config.postMerge) {
649
- const ran = await shell.run(config.postMerge, {
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 { removedCapacity } = this.config;
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 { Config } from "./config.js";
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.8",
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.8"
20
+ "webappwiz": "^0.0.9"
21
21
  },
22
22
  "main": "./index.js",
23
23
  "types": "./index.d.ts",