@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 CHANGED
@@ -1,26 +1,49 @@
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<{
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<ConfigRecord>): Partial<ConfigRecord>;
49
+ export declare function defineConfig(config: Partial<Config>): Partial<Config>;
package/config.js CHANGED
@@ -1,8 +1,7 @@
1
- import {
2
- CONFIG_FACTORY,
3
- defineConfig
4
- } from "./index-84xjmns8.js";
1
+ // config.ts
2
+ function defineConfig(config) {
3
+ return config;
4
+ }
5
5
  export {
6
- defineConfig,
7
- CONFIG_FACTORY
6
+ defineConfig
8
7
  };
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.get("trunk") } = {}) {
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.get("trunk") && !await service.git.branchExists(base);
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
- const postCheckout = config.get("postCheckout");
106
- if (postCheckout) {
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.get("trunk")
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.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 });
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.get("postRewrite"), config.get("preMerge")].filter(Boolean).join(" && ");
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
- const postMerge = config.get("postMerge");
653
- if (postMerge) {
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 CONFIG_FACTORY.create({ ...defaults(root, trunk), ...found });
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.get("leaseStalenessMs")) {
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.get("trunk");
1075
+ return this.config.trunk;
1081
1076
  }
1082
1077
  pathFor(task) {
1083
- return resolve2(this.config.get("worktreeRoot"), task);
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.get("trunk") } = {}) {
1126
- await this.fs.mkdir(this.config.get("worktreeRoot"));
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.get("removedCapacity");
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.get("leaseStalenessMs")
1207
+ stalenessMs: config.leaseStalenessMs
1213
1208
  }),
1214
1209
  shell: new Shell({ ps }),
1215
- journal: new Journal(`${arborDir}/log.jsonl`, config.get("logCapacity"), {
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.get("mergeRetryCount")}`,
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 { 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.9",
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.9"
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 };