@webappwiz/arbor 0.0.20 → 0.0.22

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/list.d.ts CHANGED
@@ -1,10 +1,18 @@
1
1
  import { type Logger } from "webappwiz/log";
2
+ import type { Fs } from "webappwiz/system";
2
3
  import type { WorktreeService } from "./worktree-service.js";
3
4
  export interface ListOptions {
4
5
  /** Print the rows as JSON instead of a table. */
5
6
  json?: boolean;
7
+ /**
8
+ * Add each task's changed and planned files, which is what an agent checks
9
+ * its own plan against before starting. Off by default: it costs git calls
10
+ * and a file read per task.
11
+ */
12
+ files?: boolean;
6
13
  }
7
- export declare function list({ service, log }: {
14
+ export declare function list({ service, log, fs }: {
8
15
  service: WorktreeService;
9
16
  log: Logger;
10
- }, { json }?: ListOptions): Promise<void>;
17
+ fs: Fs;
18
+ }, { json, files }?: ListOptions): Promise<void>;
package/merge.d.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import { type Logger } from "webappwiz/log";
2
- import type { Lock } from "webappwiz/system";
2
+ import type { Fs, Lock } from "webappwiz/system";
3
3
  import type { Config } from "./config.js";
4
4
  import type { Git } from "./git.js";
5
+ import type { Replies } from "./replies.js";
5
6
  import type { Shell } from "./shell.js";
7
+ import { type Todos } from "./todo.js";
6
8
  import type { WorktreeService } from "./worktree-service.js";
7
9
  /**
8
10
  * Lands the current worktree's branch on its base branch, trunk unless the
@@ -11,11 +13,14 @@ import type { WorktreeService } from "./worktree-service.js";
11
13
  * `git merge --ff-only`, in whichever worktree has the base checked out.
12
14
  * History stays linear.
13
15
  */
14
- export declare function merge({ service, git, lock, shell, config, log, }: {
16
+ export declare function merge({ service, git, lock, shell, config, log, todos, replies, fs, }: {
15
17
  service: WorktreeService;
16
18
  git: Git;
17
19
  lock: Lock;
18
20
  shell: Shell;
19
21
  config: Config;
20
22
  log: Logger;
23
+ todos: Todos;
24
+ replies: Replies;
25
+ fs: Fs;
21
26
  }, cwd: string): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webappwiz/arbor",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
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",
@@ -10,6 +10,9 @@
10
10
  "directory": "packages/arbor"
11
11
  },
12
12
  "type": "module",
13
+ "imports": {
14
+ "#dev/*": "./dev/*"
15
+ },
13
16
  "engines": {
14
17
  "bun": ">=1.3"
15
18
  },
@@ -17,7 +20,7 @@
17
20
  "access": "public"
18
21
  },
19
22
  "dependencies": {
20
- "webappwiz": "^0.0.20",
23
+ "webappwiz": "^0.0.22",
21
24
  "zod": "^4.4.3"
22
25
  },
23
26
  "main": "./index.js",
package/plan.d.ts CHANGED
@@ -1,3 +1,102 @@
1
+ /** The plan each task keeps at its worktree root, excluded from git. */
2
+ export declare const PLAN_FILE = "ARBOR.md";
3
+ /**
4
+ * The paths an `ARBOR.md` says its task will touch: each top-level bullet
5
+ * under `## Files`, taken as written. Prose around the list is left out, and a
6
+ * plan with no such section plans nothing.
7
+ */
8
+ export declare function plannedFiles(text: string): string[];
9
+ /**
10
+ * One `- [ ] Q9.` item under `## Blocked`: something only a person can do.
11
+ * The item's line is its subject; lines indented under it are its body, and
12
+ * `- (a) ...` or `- [a] ...` lines among them are the answers it offers.
13
+ */
14
+ export interface Question {
15
+ /** As the plan numbers it, `Q9`: the name a reply goes by. */
16
+ number: string;
17
+ /** Checked off, which the agent does once it has acted on the reply. */
18
+ done: boolean;
19
+ /** The item's own line, without its checkbox, number or reply. */
20
+ text: string;
21
+ /**
22
+ * The markdown indented under it, dedented, choices left out: detail,
23
+ * code blocks, `![shot](/abs/path.png)` images. Empty when there is none.
24
+ */
25
+ body: string;
26
+ /** Whatever follows ` → `, or null when nobody has answered yet. */
27
+ reply: string | null;
28
+ /**
29
+ * What a person added once the reply was read, oldest first: each an
30
+ * indented `→ ` line under the question.
31
+ */
32
+ followUps: string[];
33
+ /**
34
+ * The answers it offers, in order. Empty for a question answered in words
35
+ * alone. A reply can always add words to its picks, or answer in words
36
+ * instead.
37
+ */
38
+ choices: Choice[];
39
+ /**
40
+ * `one` for `- (a)` choices, at most one of which a reply picks; `any` for
41
+ * `- [a]` choices, where it picks all that apply. Null without choices.
42
+ */
43
+ pick: "one" | "any" | null;
44
+ /** The keys of the choices the reply picked, in the order offered. */
45
+ chosen: string[];
46
+ /** The absolute paths of the images its text and body show. */
47
+ images: string[];
48
+ }
49
+ /** One answer a question offers: `- (b) Migrate on next login`. */
50
+ export interface Choice {
51
+ /** The letter it goes by: `b`. */
52
+ key: string;
53
+ text: string;
54
+ }
55
+ /**
56
+ * Every numbered item under `## Blocked`, open or checked off, each with the
57
+ * indented lines under it. The reply is read off the item's own line: an
58
+ * answer is one line, written by `arbor reply` or by hand after the arrow.
59
+ */
60
+ export declare function questions(text: string): Question[];
61
+ /**
62
+ * How a reply reads in the plan: each pick spelled out, so the agent needs
63
+ * nothing but the line, then any words after it. `a (Email), c (Push): and
64
+ * log it`.
65
+ */
66
+ export declare function replyLine(asked: Question, { choices, text }: {
67
+ choices?: string[];
68
+ text: string;
69
+ }): string;
70
+ /**
71
+ * The plan with `reply` written onto that question's line, in place of any
72
+ * earlier one, or with no reply there at all when it is null. The checkbox is left alone: checking it off is the agent's
73
+ * word that it has acted on the answer. Null when the plan has no such
74
+ * question.
75
+ */
76
+ export declare function withReply(text: string, number: string,
77
+ /** Null takes the reply off, leaving the question as it was asked. */
78
+ reply: string | null): string | null;
79
+ /**
80
+ * The plan with `reply` added under a question already answered, as an
81
+ * indented `→ ` line after everything else under it, and the question
82
+ * unchecked: whatever the agent did about the answer, it has more to read.
83
+ * Null when the plan has no such question.
84
+ */
85
+ export declare function withFollowUp(text: string, number: string, reply: string): string | null;
86
+ /**
87
+ * The plan with a new open question at the end of `## Blocked`, the section
88
+ * made if missing, numbered after the highest there. Returns the plan and
89
+ * the number it went by.
90
+ */
91
+ export declare function withQuestion(text: string, subject: string, body?: string): {
92
+ plan: string;
93
+ number: string;
94
+ };
95
+ /**
96
+ * A question's number however a person typed it: `Q9`, `q9`, `9` and `Q9.`
97
+ * all mean `Q9`. Null for anything that is not a number at all.
98
+ */
99
+ export declare function questionNumber(raw: string): string | null;
1
100
  export interface PlanOptions {
2
101
  /** The task name the title is expected to match. */
3
102
  task: string;
package/remove.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type Logger } from "webappwiz/log";
2
+ import type { Todos } from "./todo.js";
2
3
  import type { WorktreeService } from "./worktree-service.js";
3
4
  export interface RemoveOptions {
4
5
  /** Discard the tree even when another agent holds its lease. */
@@ -11,7 +12,8 @@ export interface RemoveOptions {
11
12
  * Throwing a task away and redoing it against current trunk is usually cheaper
12
13
  * than a hard rebase, so this is meant to be used freely.
13
14
  */
14
- export declare function remove({ service, log }: {
15
+ export declare function remove({ service, log, todos, }: {
15
16
  service: WorktreeService;
16
17
  log: Logger;
18
+ todos: Todos;
17
19
  }, task: string, { force }?: RemoveOptions): Promise<void>;
package/replies.d.ts ADDED
@@ -0,0 +1,81 @@
1
+ import { type IdProvider } from "webappwiz/id";
2
+ import type { Fs, Lock } from "webappwiz/system";
3
+ import { Attachments } from "./attachments.js";
4
+ import { type Question } from "./plan.js";
5
+ /** How long a person opening a reply holds it before it lapses unsaved. */
6
+ export declare const EDIT_MS: number;
7
+ /**
8
+ * A reply a person gave that its agent has not read yet. It waits here, out
9
+ * of the plan, so the agent sees nothing half-written: the agent claims it to
10
+ * read it, which writes it into the plan and ends any chance to edit it.
11
+ */
12
+ export interface ReplyState {
13
+ task: string;
14
+ /** `Q2`. */
15
+ question: string;
16
+ /** The keys of the choices picked. */
17
+ choices: string[];
18
+ /** Words, alongside the picks or instead of them. */
19
+ text: string;
20
+ /** Absolute paths of the files attached, in `replies/<task>/Q2/`. */
21
+ files: string[];
22
+ repliedAt: string;
23
+ /**
24
+ * While a person has it open to edit, when that hold lapses; null when
25
+ * nobody does. An agent reads nothing held, so it never sees a reply
26
+ * mid-change.
27
+ */
28
+ editingUntil: string | null;
29
+ }
30
+ /** One reply waiting to be read, with what can be done to it. */
31
+ export declare class PendingReply {
32
+ private readonly replies;
33
+ readonly state: ReplyState;
34
+ constructor(replies: Replies, state: ReplyState);
35
+ /** Held by a person right now. */
36
+ get editing(): boolean;
37
+ get attachments(): Attachments;
38
+ /** Holds it for a person to edit, for `EDIT_MS` from now. */
39
+ hold(): Promise<PendingReply>;
40
+ release(): Promise<PendingReply>;
41
+ /** Drops the record. Its files go too unless `keepFiles`, as a claim wants. */
42
+ remove({ keepFiles }?: {
43
+ keepFiles?: boolean | undefined;
44
+ }): Promise<void>;
45
+ }
46
+ /**
47
+ * Every reply waiting to be read, one JSON file each at
48
+ * `.git/arbor/replies/<task>/<question>.json`, its files beside it. Removing a
49
+ * task takes its folder with it.
50
+ */
51
+ export declare class Replies {
52
+ readonly dir: string;
53
+ /** Held across a read and the write it decides, so a claim and an edit never cross. */
54
+ private readonly lock;
55
+ private readonly fs;
56
+ private readonly ids;
57
+ constructor(dir: string,
58
+ /** Held across a read and the write it decides, so a claim and an edit never cross. */
59
+ lock: Lock, fs: Fs, { ids }?: {
60
+ ids?: IdProvider;
61
+ });
62
+ /** Runs `work` holding the lock every write to a reply goes through. */
63
+ locked<T>(work: () => Promise<T>): Promise<T>;
64
+ find(task: string, question: string): Promise<PendingReply | null>;
65
+ /** A task's waiting replies, by question number. */
66
+ forTask(task: string): Promise<PendingReply[]>;
67
+ attachments(task: string, question: string): Attachments;
68
+ /** Whether `path` is a file some reply holds, and not a way out of here. */
69
+ owns(path: string): boolean;
70
+ /** @internal Writes one reply. Rename makes the swap atomic for readers. */
71
+ save(state: ReplyState): Promise<PendingReply>;
72
+ /** @internal */
73
+ delete(task: string, question: string): Promise<void>;
74
+ private path;
75
+ private read;
76
+ }
77
+ /**
78
+ * A reply as its line in the plan reads once claimed: the picks spelled out,
79
+ * the words, then each file's path, so the line alone is the whole answer.
80
+ */
81
+ export declare function replyText(asked: Question, reply: ReplyState): string;
package/reply.d.ts ADDED
@@ -0,0 +1,102 @@
1
+ import { type Logger } from "webappwiz/log";
2
+ import type { Fs } from "webappwiz/system";
3
+ import type { Attachment } from "./attachments.js";
4
+ import { type Question } from "./plan.js";
5
+ import { type Replies, type ReplyState } from "./replies.js";
6
+ import type { Todos } from "./todo.js";
7
+ import type { WorktreeService } from "./worktree-service.js";
8
+ export type { Attachment } from "./attachments.js";
9
+ /** What answering, and reading an answer, work with. */
10
+ export interface ReplyDeps {
11
+ service: WorktreeService;
12
+ fs: Fs;
13
+ replies: Replies;
14
+ }
15
+ export interface ReplyInput {
16
+ /** Words, alongside the picks or instead of them. */
17
+ text: string;
18
+ /**
19
+ * The keys of the choices picked (`b`), for a question that offers them:
20
+ * at most one for a `- (a)` question, any number for a `- [a]` one.
21
+ */
22
+ choices?: string[];
23
+ /** Files of any kind to add. */
24
+ files?: Attachment[];
25
+ /**
26
+ * Which files an earlier reply attached to keep, by path. Absent keeps
27
+ * none: a new reply replaces the old one whole.
28
+ */
29
+ keep?: string[];
30
+ }
31
+ export interface Replied {
32
+ task: string;
33
+ /** The question as the plan asks it. */
34
+ question: Question;
35
+ /** The reply, waiting for its agent to claim it. */
36
+ reply: ReplyState;
37
+ }
38
+ /**
39
+ * Answers one of a task's questions, or follows up one whose answer its agent
40
+ * has read. The reply waits under `.git/arbor/replies/<task>/` with its files,
41
+ * not in the plan, and replaces any earlier one there, until the task's agent
42
+ * claims it with `arbor replies` or `arbor wait --answered`. Until then it can
43
+ * be changed or withdrawn; after, it is the agent's, and anything more is
44
+ * another follow-up.
45
+ *
46
+ * Refuses a tree whose agent is in a live session. That agent is not reading
47
+ * its inbox, it is waiting in its chat, which is where the answer belongs.
48
+ */
49
+ export declare function replyTo(deps: ReplyDeps, task: string, question: string, { text, choices, files, keep }: ReplyInput): Promise<Replied>;
50
+ /**
51
+ * Takes back a reply its agent has not claimed yet, files and all: the
52
+ * question waits on a person again.
53
+ */
54
+ export declare function withdrawReply(deps: ReplyDeps, task: string, question: string): Promise<Replied>;
55
+ /**
56
+ * Holds a reply for a person editing it, so its agent cannot claim it
57
+ * half-changed. The hold lapses on its own (`EDIT_MS`) if the person walks
58
+ * away; saving or `releaseReply` ends it sooner. Refuses one its agent has
59
+ * already claimed: that is the moment it stops being editable.
60
+ */
61
+ export declare function holdReply(deps: ReplyDeps, task: string, question: string): Promise<Replied>;
62
+ /** Lets go of a reply a person was editing, leaving it as it was. */
63
+ export declare function releaseReply({ replies }: {
64
+ replies: Replies;
65
+ }, task: string, question: string): Promise<void>;
66
+ export interface Claimed {
67
+ task: string;
68
+ /**
69
+ * The questions whose replies or follow-ups were claimed, as the plan now
70
+ * reads them.
71
+ */
72
+ claimed: Question[];
73
+ /** Questions with a reply a person is still editing: not yet readable. */
74
+ editing: string[];
75
+ /** Open questions with no reply at all. */
76
+ unanswered: string[];
77
+ }
78
+ /**
79
+ * What the task's agent does to read its replies: every one waiting and not
80
+ * being edited is written into the plan after ` → `, where the agent reads
81
+ * it, and from then on belongs to the agent. The files stay where they are,
82
+ * named by path in the line, until the task goes.
83
+ */
84
+ export declare function claimReplies(deps: ReplyDeps, task: string): Promise<Claimed>;
85
+ /** `arbor replies`: claims the task's replies and prints them. */
86
+ export declare function readReplies(deps: ReplyDeps & {
87
+ log: Logger;
88
+ }, task: string): Promise<Claimed>;
89
+ /** What claiming found: the replies now in the plan, and what still waits. */
90
+ export declare function claimedListing({ task, claimed, editing, unanswered, }: Claimed): string;
91
+ /** What a person says to approve a review: the task may land. */
92
+ export declare const APPROVED = "Approved: merge it.";
93
+ /** What a person says to a question they would rather not answer here. */
94
+ export declare const SKIP = "Skip this: go ahead without it.";
95
+ /**
96
+ * Answers a question by setting it aside: it becomes a todo, its images
97
+ * carried along since the tree they sit in goes when the task does, and the
98
+ * reply tells the agent to leave it out of this task.
99
+ */
100
+ export declare function defer(deps: ReplyDeps & {
101
+ todos: Todos;
102
+ }, task: string, question: string): Promise<Replied>;
package/repository.d.ts CHANGED
@@ -3,7 +3,9 @@ import { type Fs, type Lock } from "webappwiz/system";
3
3
  import type { Config } from "./config.js";
4
4
  import { Git } from "./git.js";
5
5
  import { Journal } from "./journal.js";
6
+ import { Replies } from "./replies.js";
6
7
  import { Shell } from "./shell.js";
8
+ import { Todos } from "./todo.js";
7
9
  import { WorktreeService } from "./worktree-service.js";
8
10
  /** What a command gets to work with, once there is a repository to work in. */
9
11
  export interface Repository {
@@ -13,6 +15,8 @@ export interface Repository {
13
15
  lock: Lock;
14
16
  shell: Shell;
15
17
  journal: Journal;
18
+ todos: Todos;
19
+ replies: Replies;
16
20
  }
17
21
  /**
18
22
  * Runs the action against the repository `cwd` sits in, and refuses to run it
package/show.d.ts CHANGED
@@ -14,6 +14,11 @@ export interface Details {
14
14
  removed: number | null;
15
15
  age: string | null;
16
16
  escalation: string | null;
17
+ /**
18
+ * While escalated to be approved, the question that asks for it, `Q4`;
19
+ * null otherwise.
20
+ */
21
+ review: string | null;
17
22
  plan: string | null;
18
23
  /** How the `ARBOR.md` departs from the shape the skill prescribes. */
19
24
  planProblems: string[];
package/snapshot.d.ts CHANGED
@@ -1,24 +1,35 @@
1
- import { type Fs } from "webappwiz/system";
2
- import type { Entry, Journal } from "./journal.js";
1
+ import type { Fs } from "webappwiz/system";
2
+ import { type Inbox } from "./inbox.js";
3
+ import type { Replies } from "./replies.js";
3
4
  import { type Details } from "./show.js";
5
+ import type { TodoState, Todos } from "./todo.js";
4
6
  import type { WorktreeService } from "./worktree-service.js";
5
- /** Everything one page shows: `list` and `show` for each task, plus `log`. */
7
+ /**
8
+ * Everything one page shows: `list` and `show` for each task, every question
9
+ * each one asked however it stands, and `todo list`.
10
+ */
6
11
  export interface Snapshot {
12
+ /** The repository's directory name, so a page among many says whose it is. */
13
+ repo: string;
14
+ /** Past this age a todo is offered for removal rather than recommended. */
15
+ todoStalenessMs: number;
16
+ inbox: Inbox;
17
+ todos: TodoState[];
7
18
  tasks: Details[];
8
- entries: Entry[];
9
19
  }
10
20
  /**
11
- * Reads the whole repo the way `list`, `show` and `log` do. Takes no lease, so
12
- * serving a page cannot knock an agent off the tree it is driving.
21
+ * Reads the whole repo the way the CLI's read commands do. Takes no lease, so
22
+ * the page can sit open on a live tree without disturbing the agent in it.
13
23
  */
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>;
24
+ export declare function snapshot({ service, todos, replies, fs, }: {
25
+ service: WorktreeService;
26
+ todos: Todos;
27
+ replies: Replies;
28
+ fs: Fs;
29
+ }): Promise<Snapshot>;
19
30
  /**
20
31
  * What the page is actually showing, minus the fields that move on their own.
21
32
  * `age` ticks every minute, and hashing it would push to every open page for
22
33
  * nothing.
23
34
  */
24
- export declare function fingerprint({ tasks, entries }: Snapshot): string;
35
+ export declare function fingerprint({ inbox, todos, tasks }: Snapshot): string;
package/todo.d.ts ADDED
@@ -0,0 +1,138 @@
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
+ text: string;
9
+ /** The task it came up in, or null when a person added it from the main tree. */
10
+ from: string | null;
11
+ createdAt: string;
12
+ /** The task working on it now, or null while it waits to be picked up. */
13
+ takenBy: string | null;
14
+ /** Absolute paths of the files attached, in `todos/<id>/`. */
15
+ files: string[];
16
+ }
17
+ /** A todo's words and files changed: new ones added, some of the old kept. */
18
+ export interface TodoChange {
19
+ /** New words; the old ones stay when this is absent. */
20
+ text?: string;
21
+ /** Files to add. */
22
+ files?: Attachment[];
23
+ /** The paths of the files it has now to keep; absent keeps them all. */
24
+ keep?: string[];
25
+ }
26
+ /**
27
+ * Work deferred for later: what an agent notes when something outside its
28
+ * task comes up, so it can move on instead of growing the task. Kept under the
29
+ * shared `.git`, so every worktree sees a new one at once, with nothing to
30
+ * commit and nothing to merge.
31
+ */
32
+ export declare class Todo {
33
+ private readonly todos;
34
+ readonly state: TodoState;
35
+ constructor(todos: Todos, state: TodoState);
36
+ get id(): number;
37
+ get text(): string;
38
+ get from(): string | null;
39
+ get takenBy(): string | null;
40
+ get createdAt(): Date;
41
+ /** How long it has waited, in milliseconds. */
42
+ get waited(): number;
43
+ /** Marks it as the work of `task`, refusing one another task already has. */
44
+ take(task: string): Promise<Todo>;
45
+ /** Puts it back on the list, as when the task that took it is removed. */
46
+ release(): Promise<Todo>;
47
+ get files(): string[];
48
+ get attachments(): Attachments;
49
+ /**
50
+ * Says what is left to do in other words, or with other files, keeping its
51
+ * id and history.
52
+ */
53
+ update({ text, files, keep }: TodoChange): Promise<Todo>;
54
+ remove(): Promise<void>;
55
+ }
56
+ /** What `Todos` is stored through; the real filesystem by default. */
57
+ export interface TodosOptions {
58
+ fs?: Fs;
59
+ /** Names stored files apart; a test counts. */
60
+ ids?: IdProvider;
61
+ }
62
+ /** Every todo in the repo, one JSON file each under `.git/arbor/todos`. */
63
+ export declare class Todos {
64
+ readonly dir: string;
65
+ /** Held while numbering a new todo, so two agents never share an id. */
66
+ private readonly lock;
67
+ private readonly fs;
68
+ private readonly ids;
69
+ constructor(dir: string,
70
+ /** Held while numbering a new todo, so two agents never share an id. */
71
+ lock: Lock, opts?: TodosOptions);
72
+ /** Where a todo's files live, beside its record. */
73
+ attachments(id: number): Attachments;
74
+ /** Whether `path` is a file some todo holds, and not a way out of here. */
75
+ owns(path: string): boolean;
76
+ add(text: string, from: string | null, files?: Attachment[]): Promise<Todo>;
77
+ /** Oldest first. A file that will not parse is skipped rather than fatal. */
78
+ all(): Promise<Todo[]>;
79
+ find(id: number): Promise<Todo>;
80
+ /** The todos `task` has taken; what a merge or remove settles. */
81
+ takenBy(task: string): Promise<Todo[]>;
82
+ /** @internal Writes one todo. Rename makes the swap atomic for readers. */
83
+ save(state: TodoState): Promise<Todo>;
84
+ /** @internal */
85
+ delete(id: number): Promise<void>;
86
+ private path;
87
+ private lastId;
88
+ private read;
89
+ }
90
+ /** What a finished task should be followed by, and what should just go. */
91
+ export interface Recommendation {
92
+ /** The todo to pick up next, or null when there is nothing fresh left. */
93
+ next: Todo | null;
94
+ /** Todos left waiting so long they probably no longer apply. */
95
+ stale: Todo[];
96
+ }
97
+ /**
98
+ * Which todo to take up after `task`: its own first, since whoever just
99
+ * finished it knows that context best, then the longest waiting. Todos past
100
+ * `staleness` are never recommended, only offered for removal.
101
+ */
102
+ export declare function recommend(todos: Todos, task: string | null, staleness: number): Promise<Recommendation>;
103
+ /** The lines `merge` ends with, or none when there is nothing to say. */
104
+ export declare function recommendation({ next, stale }: Recommendation): string[];
105
+ export interface TodoListOptions {
106
+ /** Print the todos as JSON instead of a table. */
107
+ json?: boolean;
108
+ }
109
+ export interface TodoFileOptions {
110
+ /** Files to attach, relative to the current directory or absolute. */
111
+ files?: string[];
112
+ }
113
+ export declare function todoAdd(deps: {
114
+ todos: Todos;
115
+ log: Logger;
116
+ fs: Fs;
117
+ ps: Ps;
118
+ }, text: string, from: string | null, { files }?: TodoFileOptions): Promise<Todo>;
119
+ export declare function todoList({ todos, log }: {
120
+ todos: Todos;
121
+ log: Logger;
122
+ }, { json }?: TodoListOptions): Promise<void>;
123
+ export interface TodoUpdateOptions extends TodoFileOptions {
124
+ /** New words; the old ones stay when this is absent or empty. */
125
+ text?: string;
126
+ /** Attached files to drop, by path or by the name they were stored under. */
127
+ removeFiles?: string[];
128
+ }
129
+ export declare function todoUpdate(deps: {
130
+ todos: Todos;
131
+ log: Logger;
132
+ fs: Fs;
133
+ ps: Ps;
134
+ }, id: number, { text, files, removeFiles }?: TodoUpdateOptions): Promise<Todo>;
135
+ export declare function todoRemove({ todos, log }: {
136
+ todos: Todos;
137
+ log: Logger;
138
+ }, id: number): Promise<void>;
package/wait.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { type Logger } from "webappwiz/log";
2
+ import type { Fs } from "webappwiz/system";
2
3
  import { Duration } from "webappwiz/time";
4
+ import type { Replies } from "./replies.js";
3
5
  import type { WorktreeService } from "./worktree-service.js";
4
6
  /** How long `wait` gives a task before handing the wait back to its caller. */
5
7
  export declare const DEFAULT_TIMEOUT: Duration;
@@ -8,6 +10,11 @@ export interface WaitOptions {
8
10
  timeout?: Duration;
9
11
  /** How long between reads of the record; the default suits a human's patience. */
10
12
  poll?: Duration;
13
+ /**
14
+ * Wait for the task's open questions to be answered instead of for the task
15
+ * to stop moving: what an agent that escalated waits on.
16
+ */
17
+ answered?: boolean;
11
18
  }
12
19
  /**
13
20
  * Blocks until a task stops moving: merged or removed (both leave the name
@@ -15,8 +22,15 @@ export interface WaitOptions {
15
22
  * own work overlaps this one's and would rather rebase onto the result than
16
23
  * against it, which is why the timeout is short enough to come back and think
17
24
  * again rather than block a session for an afternoon.
25
+ *
26
+ * With `answered` it blocks instead until every open question under the
27
+ * task's `## Blocked` has a reply nobody is still editing, then claims them,
28
+ * which writes them into the plan, and prints them: the agent that escalated
29
+ * waits for its human this way, rather than polling its own plan.
18
30
  */
19
- export declare function wait({ service, log }: {
31
+ export declare function wait({ service, log, fs, replies, }: {
20
32
  service: WorktreeService;
21
33
  log: Logger;
22
- }, task: string, { timeout, poll }?: WaitOptions): Promise<void>;
34
+ fs: Fs;
35
+ replies: Replies;
36
+ }, task: string, { timeout, poll, answered, }?: WaitOptions): Promise<void>;
@@ -21,6 +21,7 @@ export declare class WorktreeService {
21
21
  readonly config: Config;
22
22
  private readonly tasksDir;
23
23
  private readonly removedDir;
24
+ private readonly repliesDir;
24
25
  private readonly fs;
25
26
  readonly ps: Ps;
26
27
  constructor(git: Git, config: Config, arborDir: string, opts?: WorktreeServiceOptions);
@@ -31,6 +32,12 @@ export declare class WorktreeService {
31
32
  branchFor(task: string): string;
32
33
  taskFor(branch: string): string | null;
33
34
  recordPath(task: string): string;
35
+ /**
36
+ * Where a task's replies wait to be read, and the files attached to them.
37
+ * Under `.git/arbor` rather than in the worktree, so they are never
38
+ * committed and every tree can open them, and gone with the task.
39
+ */
40
+ repliesPath(task: string): string;
34
41
  /** Always answers; the returned worktree's status says what was found. */
35
42
  find(task: string): Promise<Worktree>;
36
43
  list(): Promise<Worktree[]>;