@webappwiz/arbor 0.0.30 → 0.0.31

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.
Files changed (118) hide show
  1. package/add.d.ts +23 -0
  2. package/age.d.ts +2 -0
  3. package/arbor.d.ts +11 -0
  4. package/attachments.d.ts +36 -0
  5. package/claim.d.ts +11 -0
  6. package/config.d.ts +55 -0
  7. package/config.js +7 -0
  8. package/dev/assets.d.ts +11 -0
  9. package/dev.d.ts +48 -0
  10. package/escalate.d.ts +28 -0
  11. package/exit.d.ts +42 -0
  12. package/git.d.ts +77 -0
  13. package/index.d.ts +2 -0
  14. package/index.js +4712 -0
  15. package/journal.d.ts +29 -0
  16. package/list.d.ts +18 -0
  17. package/load-config.d.ts +15 -0
  18. package/log.d.ts +14 -0
  19. package/merge.d.ts +24 -0
  20. package/package.json +15 -32
  21. package/path.d.ts +13 -0
  22. package/plan.d.ts +83 -0
  23. package/remove.d.ts +19 -0
  24. package/repository.d.ts +25 -0
  25. package/retry.d.ts +17 -0
  26. package/shell.d.ts +17 -0
  27. package/show.d.ts +53 -0
  28. package/snapshot.d.ts +35 -0
  29. package/table.d.ts +5 -0
  30. package/todo.d.ts +264 -0
  31. package/wait.d.ts +22 -0
  32. package/worktree-service.d.ts +51 -0
  33. package/worktree.d.ts +95 -0
  34. package/AGENTS.md +0 -7
  35. package/add.test.ts +0 -125
  36. package/add.ts +0 -192
  37. package/age.ts +0 -11
  38. package/arbor.test.ts +0 -145
  39. package/arbor.ts +0 -429
  40. package/assets.d.ts +0 -9
  41. package/attachments.ts +0 -87
  42. package/build.ts +0 -98
  43. package/claim.test.ts +0 -89
  44. package/claim.ts +0 -73
  45. package/components.json +0 -21
  46. package/config.ts +0 -58
  47. package/dev/api.ts +0 -120
  48. package/dev/app.test.tsx +0 -645
  49. package/dev/app.tsx +0 -80
  50. package/dev/assets.ts +0 -16
  51. package/dev/build/main.txt +0 -68
  52. package/dev/build/shell.txt +0 -21
  53. package/dev/build/styles.txt +0 -2263
  54. package/dev/components/ui/badge.tsx +0 -51
  55. package/dev/components/ui/button.tsx +0 -57
  56. package/dev/components/ui/dialog.tsx +0 -154
  57. package/dev/components/ui/empty.tsx +0 -100
  58. package/dev/components/ui/input-group.tsx +0 -155
  59. package/dev/components/ui/input.tsx +0 -19
  60. package/dev/components/ui/progress.tsx +0 -80
  61. package/dev/components/ui/textarea.tsx +0 -17
  62. package/dev/components/ui/toast.tsx +0 -227
  63. package/dev/feed.ts +0 -69
  64. package/dev/files.tsx +0 -191
  65. package/dev/index.html +0 -21
  66. package/dev/lib/utils.ts +0 -7
  67. package/dev/main.tsx +0 -22
  68. package/dev/markdown.test.tsx +0 -133
  69. package/dev/markdown.tsx +0 -124
  70. package/dev/mentions.tsx +0 -319
  71. package/dev/shadcn.css +0 -649
  72. package/dev/styles.css +0 -107
  73. package/dev/tags.tsx +0 -44
  74. package/dev/tasks.tsx +0 -151
  75. package/dev/todos.tsx +0 -759
  76. package/dev.test.ts +0 -439
  77. package/dev.ts +0 -386
  78. package/e2e.test.ts +0 -256
  79. package/escalate.test.ts +0 -76
  80. package/escalate.ts +0 -112
  81. package/exit.test.ts +0 -71
  82. package/exit.ts +0 -74
  83. package/git.ts +0 -294
  84. package/index.ts +0 -9
  85. package/journal.ts +0 -89
  86. package/list.test.ts +0 -82
  87. package/list.ts +0 -137
  88. package/load-config.test.ts +0 -121
  89. package/load-config.ts +0 -62
  90. package/log.test.ts +0 -65
  91. package/log.ts +0 -40
  92. package/merge.test.ts +0 -332
  93. package/merge.ts +0 -283
  94. package/path.test.ts +0 -35
  95. package/path.ts +0 -38
  96. package/plan.test.ts +0 -318
  97. package/plan.ts +0 -302
  98. package/progress.test.ts +0 -54
  99. package/progress.ts +0 -31
  100. package/remove.test.ts +0 -68
  101. package/remove.ts +0 -88
  102. package/repository.ts +0 -83
  103. package/retry.test.ts +0 -43
  104. package/retry.ts +0 -48
  105. package/shell.ts +0 -44
  106. package/show.test.ts +0 -91
  107. package/show.ts +0 -151
  108. package/snapshot.test.ts +0 -33
  109. package/snapshot.ts +0 -84
  110. package/table.ts +0 -20
  111. package/testing.ts +0 -229
  112. package/todo.test.ts +0 -490
  113. package/todo.ts +0 -834
  114. package/wait.test.ts +0 -61
  115. package/wait.ts +0 -77
  116. package/worktree-service.test.ts +0 -158
  117. package/worktree-service.ts +0 -193
  118. package/worktree.ts +0 -239
package/todo.d.ts ADDED
@@ -0,0 +1,264 @@
1
+ import { type IdProvider } from "webappwiz/id";
2
+ import { type Logger } from "webappwiz/log";
3
+ import { type Fs, type Lock, type Ps } from "webappwiz/system";
4
+ import { type Attachment, Attachments } from "./attachments.js";
5
+ /** What a todo is on disk: one file apiece, so two agents never rewrite one. */
6
+ export interface TodoState {
7
+ id: number;
8
+ /** What is left to do, in a line. */
9
+ subject: string;
10
+ /** Whatever more there is to say about it, or empty when the line is enough. */
11
+ text: string;
12
+ /** Where it stands in the list, 1 at the top: the one to pick up first. */
13
+ position: number;
14
+ /** The task it came up in, or null when a person added it from the main tree. */
15
+ from: string | null;
16
+ createdAt: string;
17
+ /** The task working on it now, or null while it waits to be picked up. */
18
+ takenBy: string | null;
19
+ /** Absolute paths of the files attached, in `todos/<id>/`. */
20
+ files: string[];
21
+ /** What it belongs to, sorted: the areas or goals it adds up to with others. */
22
+ tags: string[];
23
+ }
24
+ /** A tag in use, with how many todos have it. */
25
+ export interface TagState {
26
+ tag: string;
27
+ todos: number;
28
+ }
29
+ /** `tags` deduped and sorted, refusing any not written the one way. */
30
+ export declare function tagList(tags: string[]): string[];
31
+ /** A todo's words, place or files changed: new ones added, some of the old kept. */
32
+ export interface TodoChange {
33
+ /** A new line; the old one stays when this is absent. */
34
+ subject?: string;
35
+ /** New detail, empty for none; the old stays when this is absent. */
36
+ text?: string;
37
+ /** Where to move it in the list; it stays put when this is absent. */
38
+ position?: number;
39
+ /** Files to add. */
40
+ files?: Attachment[];
41
+ /** The paths of the files it has now to keep; absent keeps them all. */
42
+ keep?: string[];
43
+ /** Its tags from now on; the old stay when this is absent. */
44
+ tags?: string[];
45
+ }
46
+ /** What a new todo has besides its subject, all of it optional. */
47
+ export interface TodoNew {
48
+ /** Whatever more there is to say about it. */
49
+ text?: string;
50
+ files?: Attachment[];
51
+ /** Where it goes in the list, pushing those from there down; the bottom by default. */
52
+ position?: number;
53
+ tags?: string[];
54
+ }
55
+ /**
56
+ * Work deferred for later: what an agent notes when something outside its
57
+ * task comes up, so it can move on instead of growing the task. Kept under the
58
+ * shared `.git`, so every worktree sees a new one at once, with nothing to
59
+ * commit and nothing to merge.
60
+ */
61
+ export declare class Todo {
62
+ private readonly todos;
63
+ readonly state: TodoState;
64
+ constructor(todos: Todos, state: TodoState);
65
+ get id(): number;
66
+ get subject(): string;
67
+ get text(): string;
68
+ get position(): number;
69
+ get from(): string | null;
70
+ get takenBy(): string | null;
71
+ get createdAt(): Date;
72
+ /** How long it has waited, in milliseconds. */
73
+ get waited(): number;
74
+ /** Marks it as the work of `task`, refusing one another task already has. */
75
+ take(task: string): Promise<Todo>;
76
+ /** Puts it back on the list, as when the task that took it is removed. */
77
+ release(): Promise<Todo>;
78
+ get files(): string[];
79
+ get tags(): string[];
80
+ get attachments(): Attachments;
81
+ /**
82
+ * Says what is left to do in other words, with other files, or higher or
83
+ * lower in the list, keeping its id and history.
84
+ */
85
+ update({ subject, text, position, files, keep, tags, }: TodoChange): Promise<Todo>;
86
+ remove(): Promise<void>;
87
+ }
88
+ /** What `Todos` is stored through; the real filesystem by default. */
89
+ export interface TodosOptions {
90
+ fs?: Fs;
91
+ /** Names stored files apart; a test counts. */
92
+ ids?: IdProvider;
93
+ }
94
+ /** Every todo in the repo, one JSON file each under `.git/arbor/todos`. */
95
+ export declare class Todos {
96
+ readonly dir: string;
97
+ /**
98
+ * Held while numbering a new todo, so two agents never share an id, and
99
+ * while writing one, so a move renumbering the rest never loses to a
100
+ * write of a position it just changed.
101
+ */
102
+ private readonly lock;
103
+ private readonly fs;
104
+ private readonly ids;
105
+ constructor(dir: string,
106
+ /**
107
+ * Held while numbering a new todo, so two agents never share an id, and
108
+ * while writing one, so a move renumbering the rest never loses to a
109
+ * write of a position it just changed.
110
+ */
111
+ lock: Lock, opts?: TodosOptions);
112
+ /** Where a todo's files live, beside its record. */
113
+ attachments(id: number): Attachments;
114
+ /** Whether `path` is a file some todo holds, and not a way out of here. */
115
+ owns(path: string): boolean;
116
+ add(subject: string, from: string | null, { text, files, position, tags }?: TodoNew): Promise<Todo>;
117
+ /**
118
+ * Top of the list first. A file that will not parse is skipped rather than
119
+ * fatal, and positions are counted again from 1 as they are read, so a
120
+ * gap or a tie left by a crash, or a todo saved before they had any (it
121
+ * goes below those that do, oldest first), never shows.
122
+ */
123
+ all(): Promise<Todo[]>;
124
+ find(id: number): Promise<Todo>;
125
+ /** Every tag a todo on the list has, by name, with how many have it. */
126
+ tags(): Promise<TagState[]>;
127
+ /** The todos `task` has taken; what a merge or remove settles. */
128
+ takenBy(task: string): Promise<Todo[]>;
129
+ /**
130
+ * Puts a todo at `position`, 1 at the top, past the bottom meaning the
131
+ * bottom, and numbers the rest around it.
132
+ */
133
+ move(id: number, position: number): Promise<Todo>;
134
+ /**
135
+ * @internal Rewrites one todo as it stands on disk now, under the lock, so
136
+ * a change to its words or who has it never undoes a move made meanwhile.
137
+ */
138
+ revise(id: number, change: (state: TodoState) => TodoState): Promise<Todo>;
139
+ /** @internal Writes one todo. Rename makes the swap atomic for readers. */
140
+ save(state: TodoState): Promise<Todo>;
141
+ /** @internal Removes one, moving those below it up to close the gap. */
142
+ delete(id: number): Promise<void>;
143
+ private locked;
144
+ /** One todo as it stands on disk; call it under the lock. */
145
+ private state;
146
+ /**
147
+ * Slots `state` in at `position` among the rest and saves whichever moved.
148
+ * Call it under the lock.
149
+ */
150
+ private place;
151
+ /** Every todo in list order, with the positions it has on disk. */
152
+ private stored;
153
+ /** Saves each todo in `order` whose position on disk is not its place there. */
154
+ private renumber;
155
+ private path;
156
+ private lastId;
157
+ private read;
158
+ }
159
+ /** What a finished task should be followed by, and what should just go. */
160
+ export interface Recommendation {
161
+ /** The todo to pick up next, or null when there is nothing fresh left. */
162
+ next: Todo | null;
163
+ /** Todos left waiting so long they probably no longer apply. */
164
+ stale: Todo[];
165
+ }
166
+ /**
167
+ * Which todo to take up after `task`: one that came up in it first, since
168
+ * whoever just finished it knows that context best, then one sharing a tag
169
+ * in `settled`, those of the todos it finished, the same area of work, then the open one highest
170
+ * on the list, which is how whoever keeps the list says what matters most.
171
+ * Todos past `staleness` are never recommended, only offered for removal.
172
+ */
173
+ export declare function recommend(todos: Todos, task: string | null, staleness: number, settled?: string[]): Promise<Recommendation>;
174
+ /** The lines `merge` ends with, or none when there is nothing to say. */
175
+ export declare function recommendation({ next, stale }: Recommendation): string[];
176
+ export interface TodoListOptions {
177
+ /** Print the todos as JSON instead of a table. */
178
+ json?: boolean;
179
+ /** Only those no task has taken: the ones free to pick up. */
180
+ open?: boolean;
181
+ /** Only those with this tag. */
182
+ tag?: string;
183
+ }
184
+ export interface TodoFileOptions {
185
+ /** Files to attach, relative to the current directory or absolute. */
186
+ files?: string[];
187
+ }
188
+ export interface TodoAddOptions extends TodoFileOptions {
189
+ /** Whatever more there is to say than the subject. */
190
+ text?: string;
191
+ /** Where it goes in the list, 1 at the top; the bottom by default. */
192
+ position?: number;
193
+ tags?: string[];
194
+ }
195
+ export declare function todoAdd(deps: {
196
+ todos: Todos;
197
+ log: Logger;
198
+ fs: Fs;
199
+ ps: Ps;
200
+ }, subject: string, from: string | null, { text, files, position, tags }?: TodoAddOptions): Promise<Todo>;
201
+ export declare function todoList({ todos, log }: {
202
+ todos: Todos;
203
+ log: Logger;
204
+ }, { json, open, tag }?: TodoListOptions): Promise<void>;
205
+ export interface TodoShowOptions {
206
+ /** Print the todo as JSON instead of prose. */
207
+ json?: boolean;
208
+ }
209
+ /** One todo in full: what `todo list` shows of it, its files, and its detail. */
210
+ export declare function todoShow({ todos, log }: {
211
+ todos: Todos;
212
+ log: Logger;
213
+ }, id: number, { json }?: TodoShowOptions): Promise<void>;
214
+ export interface TodoUpdateOptions extends TodoFileOptions {
215
+ /** A new line; the old one stays when this is absent or empty. */
216
+ subject?: string;
217
+ /** New detail; the old stays when this is absent or empty. */
218
+ text?: string;
219
+ /** Where to move it in the list, 1 at the top; it stays put when absent. */
220
+ position?: number;
221
+ /** Attached files to drop, by path or by the name they were stored under. */
222
+ removeFiles?: string[];
223
+ /** Tags to add. */
224
+ tags?: string[];
225
+ /** Tags to drop. */
226
+ removeTags?: string[];
227
+ }
228
+ export declare function todoUpdate(deps: {
229
+ todos: Todos;
230
+ log: Logger;
231
+ fs: Fs;
232
+ ps: Ps;
233
+ }, id: number, { subject, text, position, files, removeFiles, tags, removeTags, }?: TodoUpdateOptions): Promise<Todo>;
234
+ /**
235
+ * Takes up todos for `task` after it started, as when one turns out to be
236
+ * part of the work. Every id is checked before any is taken, so one that is
237
+ * gone or another task's refuses them all.
238
+ */
239
+ export declare function todoTake({ todos, log }: {
240
+ todos: Todos;
241
+ log: Logger;
242
+ }, ids: number[], task: string | null): Promise<Todo[]>;
243
+ /**
244
+ * Puts todos back on the list, as when a task is landing without finishing
245
+ * them: say what is left with `arbor todo update` first. From a worktree it
246
+ * gives back only its own task's todos.
247
+ */
248
+ export declare function todoRelease({ todos, log }: {
249
+ todos: Todos;
250
+ log: Logger;
251
+ }, ids: number[], task: string | null): Promise<Todo[]>;
252
+ export declare function todoRemove({ todos, log }: {
253
+ todos: Todos;
254
+ log: Logger;
255
+ }, id: number): Promise<void>;
256
+ export interface TodoTagsOptions {
257
+ /** Print the tags as JSON instead of a table. */
258
+ json?: boolean;
259
+ }
260
+ /** Every tag in use, with how many todos have it: the names to reuse first. */
261
+ export declare function todoTags({ todos, log }: {
262
+ todos: Todos;
263
+ log: Logger;
264
+ }, { json }?: TodoTagsOptions): Promise<void>;
package/wait.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ import { type Logger } from "webappwiz/log";
2
+ import { Duration } from "webappwiz/time";
3
+ import type { WorktreeService } from "./worktree-service.js";
4
+ /** How long `wait` gives a task before handing the wait back to its caller. */
5
+ export declare const DEFAULT_TIMEOUT: Duration;
6
+ export interface WaitOptions {
7
+ /** How long to wait before giving up. */
8
+ timeout?: Duration;
9
+ /** How long between reads of the record; the default suits a human's patience. */
10
+ poll?: Duration;
11
+ }
12
+ /**
13
+ * Blocks until a task stops moving: merged or removed (both leave the name
14
+ * `removed`), escalated to a human, or broken. Waiting is for the agent whose
15
+ * own work overlaps this one's and would rather rebase onto the result than
16
+ * against it, which is why the timeout is short enough to come back and think
17
+ * again rather than block a session for an afternoon.
18
+ */
19
+ export declare function wait({ service, log }: {
20
+ service: WorktreeService;
21
+ log: Logger;
22
+ }, task: string, { timeout, poll }?: WaitOptions): Promise<void>;
@@ -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 `remove` 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,95 @@
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
+ * For a task handed over to be approved rather than to answer questions,
16
+ * the question in its plan that asks for the approval, `4`.
17
+ */
18
+ review?: string;
19
+ }
20
+ export interface TaskState {
21
+ task: string;
22
+ branch: string;
23
+ worktree: string;
24
+ /** The branch this task lands on. Absent in old records: trunk. */
25
+ base?: string;
26
+ status: RecordStatus;
27
+ lease: LeaseState | null;
28
+ mergeAttempts: number;
29
+ createdAt: string;
30
+ updatedAt: string;
31
+ escalations?: Escalation[];
32
+ }
33
+ /**
34
+ * Everything a name can turn out to be. The record's own status when the
35
+ * task is intact, and otherwise the way in which it is not.
36
+ */
37
+ export type WorktreeStatus = RecordStatus | "absent" | "removed" | "orphaned" | "stray" | "unrecorded" | "unknown";
38
+ /** What the service found on disk. */
39
+ export interface WorktreeSnapshot {
40
+ task: string;
41
+ branch: string;
42
+ path: string;
43
+ state: TaskState | null;
44
+ exists: boolean;
45
+ hasBranch: boolean;
46
+ removedAt: string | null;
47
+ corrupt: boolean;
48
+ }
49
+ /**
50
+ * One task, whether or not it is still there. Commands ask the service for
51
+ * one of these and read its status rather than assembling the same handful of
52
+ * existence checks themselves.
53
+ */
54
+ export declare class Worktree {
55
+ private readonly service;
56
+ private readonly snapshot;
57
+ constructor(service: WorktreeService, snapshot: WorktreeSnapshot);
58
+ get task(): string;
59
+ get branch(): string;
60
+ /** Where this task's work lands: its recorded base branch, else trunk. */
61
+ get base(): string;
62
+ get path(): string;
63
+ get state(): TaskState | null;
64
+ get lease(): LeaseState | null;
65
+ get leaseHeld(): boolean;
66
+ get leaseStatus(): "held" | "stale" | "none";
67
+ get leaseOurs(): boolean;
68
+ get leaseHeldByOther(): boolean;
69
+ get mergeAttempts(): number;
70
+ get exists(): boolean;
71
+ get hasBranch(): boolean;
72
+ get removedAt(): string | null;
73
+ get status(): WorktreeStatus;
74
+ get gone(): boolean;
75
+ /**
76
+ * Merges changes into the record and writes it. Works on a name that has no
77
+ * record yet, which is how `add` writes the first one and how `claim`
78
+ * rebuilds one from a worktree found on disk.
79
+ */
80
+ save(changes?: Partial<TaskState>): Promise<Worktree>;
81
+ take(changes?: Partial<TaskState>): Promise<Worktree>;
82
+ /** Re-reads from disk. `merge` needs this: the record is the truth. */
83
+ reload(): Promise<Worktree>;
84
+ /** Removes the directory, the branch, and the record; remembers the name. */
85
+ discard(): Promise<GitResult>;
86
+ commitsAhead(): Promise<number | null>;
87
+ diffStat(): Promise<{
88
+ added: number;
89
+ removed: number;
90
+ } | null>;
91
+ /** Committed and uncommitted paths this task touches; null without a branch. */
92
+ changedFiles(): Promise<string[] | null>;
93
+ uncommitted(): Promise<string[]>;
94
+ interruptedOps(): Promise<string[]>;
95
+ }
package/AGENTS.md DELETED
@@ -1,7 +0,0 @@
1
- If you change this package's API, check that the agent skill at
2
- `packages/cli/templates/arbor.skill.md` is still in sync. That is the source;
3
- the copies under `.agents/skills/` and `.claude/skills/` are installed from it.
4
-
5
- `arbor` runs whichever checkout you are standing in, so working on this package
6
- from a worktree merges with the code you are editing. Pin the main tree for the
7
- merge itself: `WEBAPPWIZ_ROOT="$(arbor path)" arbor merge`.
package/add.test.ts DELETED
@@ -1,125 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
- import { join } from "node:path";
3
- import { add } from "./add";
4
- import { Testing } from "./testing";
5
-
6
- describe("add", () => {
7
- let deps: Testing;
8
-
9
- beforeEach(async () => {
10
- deps = await Testing.open();
11
- });
12
-
13
- afterEach(() => deps.disposeAsync());
14
-
15
- it("makes a worktree, a branch and a record", async () => {
16
- await add(deps, "alpha");
17
-
18
- const state = (await deps.service.find("alpha")).state;
19
- expect(state).toMatchObject({
20
- task: "alpha",
21
- branch: "task/alpha",
22
- status: "working",
23
- mergeAttempts: 0,
24
- });
25
- expect(await deps.fs.exists(join(state?.worktree ?? "", "README.md"))).toBe(
26
- true,
27
- );
28
- expect(
29
- await deps.gitCli(
30
- state?.worktree ?? "",
31
- "rev-parse",
32
- "--abbrev-ref",
33
- "HEAD",
34
- ),
35
- ).toBe("task/alpha");
36
- expect(deps.out()).toContain("added alpha");
37
- });
38
-
39
- it("leaves a stub ARBOR.md for the agent to fill in", async () => {
40
- await add(deps, "alpha");
41
-
42
- const state = (await deps.service.find("alpha")).state;
43
- const plan = await deps.fs.read(join(state?.worktree ?? "", "ARBOR.md"));
44
- expect(plan).toStartWith("# alpha\n");
45
- expect(plan).toContain("## Goal");
46
- expect(plan).toContain("## Files");
47
- expect(plan).toContain("- [ ]");
48
- });
49
-
50
- it("excludes the plan from git, once, so no task can commit it", async () => {
51
- await add(deps, "alpha");
52
- await add(deps, "beta");
53
-
54
- const exclude = await deps.fs.read(
55
- join(deps.root, ".git", "info", "exclude"),
56
- );
57
- expect(exclude.split("\n").filter((line) => line === "ARBOR.md")).toEqual([
58
- "ARBOR.md",
59
- ]);
60
- const state = (await deps.service.find("alpha")).state;
61
- expect(
62
- await deps.gitCli(state?.worktree ?? "", "status", "--porcelain"),
63
- ).toBe("");
64
- });
65
-
66
- it("refuses a name that is already taken and points at claim", async () => {
67
- await add(deps, "alpha");
68
-
69
- await expect(add(deps, "alpha")).toBail("exists", {
70
- message: "arbor claim alpha",
71
- });
72
- });
73
-
74
- it("rejects names that are not legal branch or directory names", async () => {
75
- const names = ["Alpha", "a b", "feature/x", "-alpha", ""];
76
-
77
- await Promise.all(
78
- names.map((name) => expect(add(deps, name)).toBail("usage")),
79
- );
80
- });
81
-
82
- it("refuses a repo with submodules, before making anything", async () => {
83
- await deps.fs.write(join(deps.root, ".gitmodules"), "");
84
-
85
- await expect(add(deps, "alpha")).toBail("usage", {
86
- message: ".gitmodules",
87
- });
88
- expect((await deps.service.find("alpha")).gone).toBe(true);
89
- });
90
-
91
- it("reports a failed postCheckout hook but keeps the worktree", async () => {
92
- deps.config.postCheckout = "exit 3";
93
-
94
- await expect(add(deps, "alpha")).toBail("hook_failed");
95
- const state = (await deps.service.find("alpha")).state;
96
- expect(await deps.fs.exists(state?.worktree ?? "")).toBe(true);
97
- });
98
-
99
- it("tells the postCheckout hook which branch the trunk is", async () => {
100
- deps.config.trunk = "master";
101
- deps.config.postCheckout = "printenv ARBOR_TRUNK > trunk.txt";
102
-
103
- await add(deps, "alpha", { base: "main" });
104
-
105
- const state = (await deps.service.find("alpha")).state;
106
- expect(await deps.fs.read(join(state?.worktree ?? "", "trunk.txt"))).toBe(
107
- "master\n",
108
- );
109
- });
110
-
111
- it("names the trunk and the config when the trunk is not a branch", async () => {
112
- deps.config.trunk = "master";
113
-
114
- await expect(add(deps, "alpha")).toBail("usage", {
115
- message: ["trunk 'master'", "arbor.config.ts"],
116
- });
117
- });
118
-
119
- it("refuses with a reason, a message and the data behind it", async () => {
120
- await expect(add(deps, "Alpha")).toBail("usage", {
121
- message: "invalid task name 'Alpha'",
122
- data: { task: "Alpha" },
123
- });
124
- });
125
- });