@webappwiz/arbor 0.0.1

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/journal.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ import { type Fs } from "webappwiz/system";
2
+ /** One thing that was done to a task, and how it went. */
3
+ export interface Entry {
4
+ at: string;
5
+ action: string;
6
+ /** The task it was about, or null when the command names none. */
7
+ task: string | null;
8
+ /** The refusal reason, or null when the command succeeded. */
9
+ reason: string | null;
10
+ }
11
+ /**
12
+ * What has been done in this repo, oldest first. Entries outlive their tasks:
13
+ * `merge` and `rm` take the record with them, so this is the only thing that
14
+ * remembers a task existed at all.
15
+ */
16
+ /** What a `Journal` is stored through; the real filesystem by default. */
17
+ export interface JournalOptions {
18
+ fs?: Fs;
19
+ }
20
+ export declare class Journal {
21
+ private readonly path;
22
+ private readonly capacity;
23
+ private readonly fs;
24
+ constructor(path: string, capacity: number, opts?: JournalOptions);
25
+ record<T>(action: string, task: string | null, run: () => Promise<T>): Promise<T>;
26
+ tail(count: number): Promise<Entry[]>;
27
+ private append;
28
+ private entries;
29
+ }
@@ -0,0 +1,7 @@
1
+ import { type Fs } from "webappwiz/system";
2
+ import type { Config } from "./config.js";
3
+ export interface LoadConfigOptions {
4
+ /** What `arbor.config.ts` is looked for through; the real one by default. */
5
+ fs?: Fs;
6
+ }
7
+ export declare function loadConfig(root: string, opts?: LoadConfigOptions): Promise<Config>;
package/log.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { type Logger } from "webappwiz/log";
2
+ import type { Journal } from "./journal.js";
3
+ /** Enough to cover a session's worth of work without scrolling. */
4
+ export declare const DEFAULT_COUNT = 20;
5
+ export interface LogOptions {
6
+ /** How many of the most recent entries to show. */
7
+ count?: number;
8
+ /** Print the entries as JSON instead of a table. */
9
+ json?: boolean;
10
+ }
11
+ export declare function log({ journal, log }: {
12
+ journal: Journal;
13
+ log: Logger;
14
+ }, { count, json }?: LogOptions): Promise<void>;
package/ls.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { type Logger } from "webappwiz/log";
2
+ import type { WorktreeService } from "./worktree-service.js";
3
+ export interface LsOptions {
4
+ /** Print the rows as JSON instead of a table. */
5
+ json?: boolean;
6
+ }
7
+ export declare function ls({ service, log }: {
8
+ service: WorktreeService;
9
+ log: Logger;
10
+ }, { json }?: LsOptions): Promise<void>;
package/merge.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { type Logger } from "webappwiz/log";
2
+ import type { Lock } from "webappwiz/system";
3
+ import type { Config } from "./config.js";
4
+ import type { Git } from "./git.js";
5
+ import type { Shell } from "./shell.js";
6
+ import type { WorktreeService } from "./worktree-service.js";
7
+ /**
8
+ * Lands the current worktree's branch on its base branch, trunk unless the
9
+ * task was created with `--base`. Never a merge commit: it rebases onto the
10
+ * base, runs the gate there, and fast-forwards the base with
11
+ * `git merge --ff-only`. History stays linear.
12
+ */
13
+ export declare function merge({ service, git, lock, shell, config, log, }: {
14
+ service: WorktreeService;
15
+ git: Git;
16
+ lock: Lock;
17
+ shell: Shell;
18
+ config: Config;
19
+ log: Logger;
20
+ }, cwd: string): Promise<void>;
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@webappwiz/arbor",
3
+ "version": "0.0.1",
4
+ "description": "Runs several AI coding agents on one repository at once, each in its own git worktree",
5
+ "license": "MIT",
6
+ "author": "Jared Johnson",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/jaredjj3/webappwiz.git",
10
+ "directory": "packages/arbor"
11
+ },
12
+ "type": "module",
13
+ "engines": {
14
+ "bun": ">=1.3"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "dependencies": {
20
+ "webappwiz": "^0.0.1"
21
+ },
22
+ "main": "./index.js",
23
+ "types": "./index.d.ts",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./index.d.ts",
27
+ "default": "./index.js"
28
+ },
29
+ "./config": {
30
+ "types": "./config.d.ts",
31
+ "default": "./config.js"
32
+ }
33
+ },
34
+ "bin": {
35
+ "arbor": "./index.js"
36
+ }
37
+ }
package/path.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { Logger } from "webappwiz/log";
2
+ import type { WorktreeService } from "./worktree-service.js";
3
+ /**
4
+ * Where a task lives, or (with no task) the main tree. Nothing but the path
5
+ * on stdout, so it composes: `cd "$(arbor path)"`, `zed -a "$(arbor path x)"`.
6
+ * Moving between trees is `cd` and nothing else, and the main tree is the one
7
+ * path a process standing in a worktree cannot otherwise name: git's
8
+ * `--show-toplevel` hands back the worktree it is already in.
9
+ */
10
+ export declare function path({ service, log }: {
11
+ service: WorktreeService;
12
+ log: Logger;
13
+ }, task?: string): Promise<void>;
package/plan.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ export interface PlanOptions {
2
+ /** The task name the title is expected to match. */
3
+ task: string;
4
+ /** Whether the task has escalated, which makes `## Blocked` required. */
5
+ escalated?: boolean;
6
+ }
7
+ /**
8
+ * Every way a task's `ARBOR.md` departs from the shape the agent skill
9
+ * prescribes, phrased for the agent that wrote it. Advisory only: a resumable
10
+ * plan is the point, and a malformed one still beats none, so nothing here
11
+ * blocks a merge.
12
+ */
13
+ export declare function checkPlan(text: string, { task, escalated }: PlanOptions): string[];
@@ -0,0 +1,23 @@
1
+ import type { Deps, Middleware } from "webappwiz/cmd";
2
+ import { type Fs, type Lock } from "webappwiz/system";
3
+ import type { Config } from "./config.js";
4
+ import { Git } from "./git.js";
5
+ import { Journal } from "./journal.js";
6
+ import { Shell } from "./shell.js";
7
+ import { WorktreeService } from "./worktree-service.js";
8
+ /** What a command gets to work with, once there is a repository to work in. */
9
+ export interface Repository {
10
+ config: Config;
11
+ git: Git;
12
+ service: WorktreeService;
13
+ lock: Lock;
14
+ shell: Shell;
15
+ journal: Journal;
16
+ }
17
+ /**
18
+ * Runs the action against the repository `cwd` sits in, and refuses to run it
19
+ * at all when that is not a repository.
20
+ */
21
+ export declare function repository<C extends Deps & {
22
+ fs: Fs;
23
+ }>(at?: string): Middleware<C, C & Repository>;
package/retry.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { type Logger } from "webappwiz/log";
2
+ import type { Config } from "./config.js";
3
+ import type { WorktreeService } from "./worktree-service.js";
4
+ /**
5
+ * The way back from `budget_exhausted`. A task that has spent its merge
6
+ * attempts gets another `mergeRetryCount` of them, instead of `rm` and redo.
7
+ *
8
+ * Only from `escalated`, and deliberately: the budget's whole job is to make an
9
+ * agent stop and hand the task over. An agent that could grant itself more
10
+ * would be back to grinding against a moving trunk forever, so the price of a
11
+ * fresh budget is that a human has looked at the tree first.
12
+ */
13
+ export declare function retry({ service, config, log, }: {
14
+ service: WorktreeService;
15
+ config: Config;
16
+ log: Logger;
17
+ }, task: string): Promise<void>;
package/rm.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { type Logger } from "webappwiz/log";
2
+ import type { WorktreeService } from "./worktree-service.js";
3
+ export interface RmOptions {
4
+ /** Discard the tree even when another agent holds its lease. */
5
+ force?: boolean;
6
+ }
7
+ /**
8
+ * Discards a whole task: `git worktree remove` plus the branch and the
9
+ * record.
10
+ *
11
+ * Throwing a task away and redoing it against current trunk is usually cheaper
12
+ * than a hard rebase, so this is meant to be used freely.
13
+ */
14
+ export declare function rm({ service, log }: {
15
+ service: WorktreeService;
16
+ log: Logger;
17
+ }, task: string, { force }?: RmOptions): Promise<void>;
package/shell.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { type Ps, type SpawnCaptureResult, type SpawnOptions, type SpawnResult } from "webappwiz/system";
2
+ /** Spawn options for a shell command. `cwd` is the worktree it runs against. */
3
+ export type ShellRunOptions = SpawnOptions & {
4
+ cwd: string;
5
+ };
6
+ /** What a `Shell` spawns through; the real process by default. */
7
+ export interface ShellOptions {
8
+ ps?: Ps;
9
+ }
10
+ /**
11
+ * Runs the commands a repo configures (the lifecycle hooks) as shell strings
12
+ * rather than argv, since that is how a repo writes them.
13
+ */
14
+ export declare class Shell {
15
+ private readonly ps;
16
+ constructor(opts?: ShellOptions);
17
+ /** Captures output, for commands whose failure has to be reported back. */
18
+ run(command: string, opts: ShellRunOptions): Promise<SpawnCaptureResult>;
19
+ /** Inherits stdio, for commands the agent should watch as they run. */
20
+ stream(command: string, opts: ShellRunOptions): Promise<SpawnResult>;
21
+ }
package/show.d.ts ADDED
@@ -0,0 +1,43 @@
1
+ import { type Logger } from "webappwiz/log";
2
+ import { type Fs } from "webappwiz/system";
3
+ import type { Worktree } from "./worktree.js";
4
+ import type { WorktreeService } from "./worktree-service.js";
5
+ export interface Details {
6
+ task: string;
7
+ status: string;
8
+ branch: string;
9
+ base: string;
10
+ worktree: string;
11
+ lease: "held" | "stale" | "none";
12
+ ahead: number | null;
13
+ added: number | null;
14
+ removed: number | null;
15
+ age: string | null;
16
+ escalation: string | null;
17
+ plan: string | null;
18
+ /** How the `ARBOR.md` departs from the shape the skill prescribes. */
19
+ planProblems: string[];
20
+ }
21
+ export interface ShowOptions {
22
+ /** Print the details as JSON instead of prose. */
23
+ json?: boolean;
24
+ }
25
+ /**
26
+ * One task in full: what `ls` shows for it, plus the `ARBOR.md` its agent
27
+ * left at the worktree root. Reading a tree this way takes no lease, so it
28
+ * cannot knock the agent driving it off its own work.
29
+ */
30
+ export declare function show({ service, fs, log }: {
31
+ service: WorktreeService;
32
+ fs: Fs;
33
+ log: Logger;
34
+ }, task: string, { json }?: ShowOptions): Promise<void>;
35
+ /**
36
+ * Everything there is to say about one task. Split out so `dev` can render the
37
+ * same fields this prints, rather than assembling its own and drifting.
38
+ */
39
+ export interface TaskDetailsOptions {
40
+ /** What the worktree is read through; the real filesystem by default. */
41
+ fs?: Fs;
42
+ }
43
+ export declare function taskDetails(worktree: Worktree, opts?: TaskDetailsOptions): Promise<Details>;
package/snapshot.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ import { type Fs } from "webappwiz/system";
2
+ import type { Entry, Journal } from "./journal.js";
3
+ import { type Details } from "./show.js";
4
+ import type { WorktreeService } from "./worktree-service.js";
5
+ /** Everything one page shows: `ls` and `show` for each task, plus `log`. */
6
+ export interface Snapshot {
7
+ tasks: Details[];
8
+ entries: Entry[];
9
+ }
10
+ /**
11
+ * Reads the whole repo the way `ls`, `show` and `log` do. Takes no lease, so
12
+ * serving a page cannot knock an agent off the tree it is driving.
13
+ */
14
+ export interface SnapshotOptions {
15
+ /** What the worktrees are read through; the real filesystem by default. */
16
+ fs?: Fs;
17
+ }
18
+ export declare function snapshot(service: WorktreeService, journal: Journal, opts?: SnapshotOptions): Promise<Snapshot>;
19
+ /**
20
+ * What the page is actually showing, minus the fields that move on their own.
21
+ * `age` ticks every minute, and hashing it would push to every open page for
22
+ * nothing.
23
+ */
24
+ export declare function fingerprint({ tasks, entries }: Snapshot): string;
package/table.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Columns padded to their widest cell. Padding goes by visible width, not
3
+ * string length: cells carry color codes.
4
+ */
5
+ export declare function table(header: string[], rows: string[][]): string;
@@ -0,0 +1,51 @@
1
+ import { type Fs, type Ps } from "webappwiz/system";
2
+ import type { Config } from "./config.js";
3
+ import type { Git, GitResult } from "./git.js";
4
+ import { type TaskState, Worktree } from "./worktree.js";
5
+ export interface AddOptions {
6
+ /** Branch the new worktree starts from. Defaults to the trunk. */
7
+ base?: string;
8
+ }
9
+ /**
10
+ * Where tasks live and everything persistent about them: the worktree
11
+ * directories, the records under `.git/arbor/tasks`, and the names of tasks
12
+ * already removed. One name, one lookup, whatever state it turns out to be in.
13
+ */
14
+ /** What a `WorktreeService` works through; the real ones by default. */
15
+ export interface WorktreeServiceOptions {
16
+ fs?: Fs;
17
+ ps?: Ps;
18
+ }
19
+ export declare class WorktreeService {
20
+ readonly git: Git;
21
+ readonly config: Config;
22
+ private readonly tasksDir;
23
+ private readonly removedDir;
24
+ private readonly fs;
25
+ readonly ps: Ps;
26
+ constructor(git: Git, config: Config, arborDir: string, opts?: WorktreeServiceOptions);
27
+ init(): Promise<void>;
28
+ get trunk(): string;
29
+ /** Where a task's worktree lives: a sibling of the repo, never inside it. */
30
+ pathFor(task: string): string;
31
+ branchFor(task: string): string;
32
+ taskFor(branch: string): string | null;
33
+ recordPath(task: string): string;
34
+ /** Always answers; the returned worktree's status says what was found. */
35
+ find(task: string): Promise<Worktree>;
36
+ list(): Promise<Worktree[]>;
37
+ /** Adds the branch and the working directory. The record comes after. */
38
+ add(task: string, { base }?: AddOptions): Promise<GitResult>;
39
+ discard(worktree: Worktree): Promise<GitResult>;
40
+ /** Writes a task's record. A concurrent reader sees the old one or the new. */
41
+ saveRecord(state: TaskState): Promise<void>;
42
+ private readRecord;
43
+ private removedPath;
44
+ private removedAt;
45
+ /**
46
+ * Remembers a removed name so a second `rm` can say "already removed"
47
+ * instead of "never existed", then drops the oldest so the list of
48
+ * remembered names cannot grow without bound.
49
+ */
50
+ private rememberRemoved;
51
+ }
package/worktree.d.ts ADDED
@@ -0,0 +1,88 @@
1
+ import type { GitResult } from "./git.js";
2
+ import type { WorktreeService } from "./worktree-service.js";
3
+ /** The status a task's own record carries. */
4
+ export type RecordStatus = "working" | "merging" | "escalated";
5
+ /** What a task's record stores about who is driving it. */
6
+ export interface LeaseState {
7
+ pid: number;
8
+ hostname: string;
9
+ heartbeatAt: string;
10
+ }
11
+ export interface Escalation {
12
+ reason: string;
13
+ at: string;
14
+ }
15
+ export interface TaskState {
16
+ task: string;
17
+ branch: string;
18
+ worktree: string;
19
+ /** The branch this task lands on. Absent in old records: trunk. */
20
+ base?: string;
21
+ status: RecordStatus;
22
+ lease: LeaseState | null;
23
+ mergeAttempts: number;
24
+ createdAt: string;
25
+ updatedAt: string;
26
+ escalations?: Escalation[];
27
+ }
28
+ /**
29
+ * Everything a name can turn out to be. The record's own status when the
30
+ * task is intact, and otherwise the way in which it is not.
31
+ */
32
+ export type WorktreeStatus = RecordStatus | "absent" | "removed" | "orphaned" | "stray" | "unrecorded" | "unknown";
33
+ /** What the service found on disk. */
34
+ export interface WorktreeSnapshot {
35
+ task: string;
36
+ branch: string;
37
+ path: string;
38
+ state: TaskState | null;
39
+ exists: boolean;
40
+ hasBranch: boolean;
41
+ removedAt: string | null;
42
+ corrupt: boolean;
43
+ }
44
+ /**
45
+ * One task, whether or not it is still there. Commands ask the service for
46
+ * one of these and read its status rather than assembling the same handful of
47
+ * existence checks themselves.
48
+ */
49
+ export declare class Worktree {
50
+ private readonly service;
51
+ private readonly snapshot;
52
+ constructor(service: WorktreeService, snapshot: WorktreeSnapshot);
53
+ get task(): string;
54
+ get branch(): string;
55
+ /** Where this task's work lands: its recorded base branch, else trunk. */
56
+ get base(): string;
57
+ get path(): string;
58
+ get state(): TaskState | null;
59
+ get lease(): LeaseState | null;
60
+ get leaseHeld(): boolean;
61
+ get leaseStatus(): "held" | "stale" | "none";
62
+ get leaseOurs(): boolean;
63
+ get leaseHeldByOther(): boolean;
64
+ get mergeAttempts(): number;
65
+ get exists(): boolean;
66
+ get hasBranch(): boolean;
67
+ get removedAt(): string | null;
68
+ get status(): WorktreeStatus;
69
+ get gone(): boolean;
70
+ /**
71
+ * Merges changes into the record and writes it. Works on a name that has no
72
+ * record yet, which is how `add` writes the first one and how `claim`
73
+ * rebuilds one from a worktree found on disk.
74
+ */
75
+ save(changes?: Partial<TaskState>): Promise<Worktree>;
76
+ take(changes?: Partial<TaskState>): Promise<Worktree>;
77
+ /** Re-reads from disk. `merge` needs this: the record is the truth. */
78
+ reload(): Promise<Worktree>;
79
+ /** Removes the directory, the branch, and the record; remembers the name. */
80
+ discard(): Promise<GitResult>;
81
+ commitsAhead(): Promise<number | null>;
82
+ diffStat(): Promise<{
83
+ added: number;
84
+ removed: number;
85
+ } | null>;
86
+ uncommitted(): Promise<string[]>;
87
+ interruptedOps(): Promise<string[]>;
88
+ }