pi-superagent 0.1.0 → 0.1.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.
@@ -1,131 +1,111 @@
1
1
  # Architecture
2
2
 
3
- This is the map: what runs, in what order, and which file owns each rule. The reasons behind the rules
4
- are in [design-decisions.md](design-decisions.md); the proofs about them are in
5
- [formal-verification.md](formal-verification.md).
6
-
7
- ## The path of one request
8
-
9
- ```
10
- GitHub: issues, comments, reviews the only way in
11
- │ polled every 30 s through the gh CLI gh-api.ts
12
- ▼
13
- GitHub source seed rule · deferred read positions · own-writing filter
14
- │ Trigger: content-derived id, thread #N source-github.ts
15
- ▼
16
- engine scope: write access only src/core, router.ts
17
- │ policy gate: a person, not a bot
18
- │ Evaluation: the thread's waiting triggers
19
- ▼
20
- judge one candidate at a time, answered through ticket_decide
21
- │ accept assessor.ts, judge.ts
22
- ▼
23
- engine ticket + pending assignment; acknowledgement queued
24
- ▼
25
- executor a pi worker in its own herdr tab and workspace
26
- │ ticket_report: ready · blocked · failed · cancelled, and any pull requests
27
- ▼ executor.ts, herdr-workers.ts
28
- engine pull requests bound first; the report lands on the ticket;
29
- │ the receipt is queued
30
- ▼
31
- outbox the comment, posted on the issue once outbox.ts
32
- ⋮
33
- pull request merged or closed: the ticket ends, and the issue hears it
34
- read each poll, or every 2 min elsewhere pulls.ts
35
- ```
36
-
37
- A crash anywhere on this path loses nothing and doubles nothing:
38
-
39
- - **GitHub to source.** Read positions are staged during a poll and committed at the next one, after the
40
- engine holds every trigger they produced. A crash replays the last poll instead of skipping it.
41
- - **Source to engine.** Trigger ids derive from content: a comment from its author, time and body; an
42
- issue from its number, title and last update; a review from GitHub's own id. The engine accepts an id
43
- once, so a replay is a no-op.
44
- - **Inside the engine.** Each command is one SQLite transaction: its writes land together or not at all.
45
- - **Engine to executor.** The engine's calls to start, deliver to and stop a worker run only after the
46
- command commits, so a worker never acts on a write that could still roll back.
47
- - **Executor and worker.** A worker writes its events and reports to files; the router reads them from a
48
- cursor it persists. A restarted router resumes reading where the last one stopped, and adopts the
49
- running worker instead of starting a second.
50
- - **Outbox to GitHub.** A message is marked sent only once GitHub accepts it. A retry first looks for the
51
- comment on the issue, in case the last attempt landed before the process died.
3
+ A GitHub issue is where people talk about the work. A ticket is how pi-superagent remembers whose
4
+ turn it is. The **router** keeps the two in step. It watches the repository, asks a **judge** to
5
+ interpret new requests, and gives each accepted task to a **worker** with its own terminal and
6
+ checkout.
7
+
8
+ ## Follow a comment through the system
9
+
10
+ Imagine adding “Please keep the keyboard shortcut” to an issue while a worker is editing the menu.
11
+ On its next poll, the router reads your comment through the GitHub CLI. The GitHub source turns it
12
+ into a **trigger**, a message with an ID and a thread. The engine checks who sent it, finds the
13
+ ticket that already owns the thread and queues the comment for that worker.
14
+
15
+ A new thread takes one extra step. The judge reads the request and calls `ticket_decide`. An
16
+ acceptance opens a ticket and queues an acknowledgement on GitHub. The worker later calls
17
+ `ticket_report` with its outcome. If the report links a pull request, the router records that link
18
+ before the engine processes the report. That’s how an issue can stay active through code review
19
+ even after the worker has finished its first edit.
20
+
21
+ ## If the router restarts halfway through
22
+
23
+ Suppose the router reads your comment and loses power a moment later. Whether the worker received
24
+ it depends on where the router stopped. Three records let it resume the conversation.
25
+
26
+ - **The GitHub read position** records how far the router has read. A poll saves its new position
27
+ at the start of the next poll, after the engine has received the triggers. On replay, stable IDs
28
+ let the engine recognize a comment, issue or review it has already seen.
29
+ - **The ticket and worker records** say who has the assignment. Each engine command commits its
30
+ changes in one SQLite transaction. Worker starts, deliveries and stops follow that commit.
31
+ The worker’s tab and files can outlive the router, which uses a saved position to resume reading
32
+ the worker’s events and reports.
33
+ - **The outbox** records what the router owes the GitHub thread. If a reply was pending when the
34
+ router stopped, a retry looks for the comment on GitHub before posting it again.
52
35
 
53
36
  ## The parts
54
37
 
55
- **The engine: `src/core/` (vendored, unmodified).** The state machine. `intake.ts` hands a trigger to the
56
- ticket that owns its thread and queues an Evaluation only when no ticket does; `evals.ts` turns an
57
- accepted Evaluation into a ticket; `assignments.ts` records reports and keeps one running assignment per
58
- ticket; `tickets.ts` stops, retries and wakes tickets; `replies.ts` queues acknowledgements and replies,
59
- each under a key that is queued once. The engine is synchronous and does no I/O of its own: everything
60
- that touches a process, the network or a model arrives through a port in `ports.ts`.
61
-
62
- **The router: `src/pi/extension.ts`.** The composition root. It builds the store, the GitHub source, the
63
- outbox, the judge, the executor and the panel, then runs the loop. Every 30 seconds, and whenever its own
64
- pi session goes idle, the loop polls GitHub, hands new triggers to the engine, stops tickets whose thread
65
- has closed, lets the engine start due evaluations, launches queued work, flags workers on a run that
66
- have gone quiet for 15 minutes, and flushes the outbox. Three failed polls in a row put an error in `health.json`.
67
-
68
- **GitHub in: `source-github.ts` and `gh-api.ts`.** The source reads issues, issue and pull-request
69
- comments, and submitted reviews. Its first look at a repository records what already exists and fires
70
- none of it. Its read positions commit one poll late, as described above. It ignores its own writing,
71
- recognized by the account it posts from or by the hidden marker every reply carries, and it marks
72
- authors matching the bot patterns as not human. Every trigger carries its author's association with the
73
- repository, and the router's scope (`scopeOf` in `router.ts`) files one from anyone without write access
74
- as filtered: it reaches neither the judge nor a worker. `gh-api.ts` talks to GitHub through the `gh` CLI, so the
75
- credentials stay in the operator's keyring; every call is asynchronous, issue and pull-request lists are
76
- paginated, and comments are read forward from the cursor.
77
-
78
- **Pull requests: `pulls.ts`.** A report may name the pull requests its work opened. The executor hands
79
- them to the router before the engine reads the report, and the router binds each one to the ticket with
80
- the engine's `bindThread`, so a *ready* report waits for it instead of completing the ticket. Each poll,
81
- the follower asks GitHub about every followed pull request that is no longer open in the watched
82
- repository and reports the merge or close through `threadClosed`, which ends the ticket. A pull request
83
- in another repository is read every two minutes, and its comments and reviews become triggers on its own
84
- thread, `owner/repo#N`, with the read position committed once the engine holds them.
85
-
86
- **The judge: `assessor.ts` and `judge.ts`.** The engine's policy gate, defined in `router.ts`, admits a
87
- trigger only from a person who is not on the bot list. That check is code, not a model call. An admitted
88
- trigger becomes a candidate: the thread, its messages, the tickets already on it, and a link to the
89
- issue. The assessor sends candidates to the judge one at a time. The judge is a headless pi session with
90
- no extensions, skills or context files and a single tool, `ticket_decide`. If the judge accepts a thread
91
- that already has an open ticket, the router overrules it: that thread's messages belong to its worker.
92
-
93
- **Workers: `executor.ts`, `herdr-workers.ts` and `worker-role.ts`.** Each running assignment gets one
94
- worker, a pi session that herdr starts in a new tab, in a workspace of its own. Router and worker talk
95
- through files in `workers/<session>/`: inputs land in `inbox/`, and the worker appends events and reports
96
- to `events.jsonl` and `reports.jsonl`. The files and the tab outlive the router, which is why a restart
97
- adopts workers rather than replacing them. Inside the worker, `worker-role.ts` registers `ticket_report`
98
- and the guard from `workspace.ts`: no delegation tools, no blocked commands, no writes outside the
99
- workspace. It also files any run that ends without a report as a failure: only the worker process knows
100
- the order of its own events, so only it can tell for certain. A crash frees the worker's slot at once, and
101
- the router answers the ticket's first crash with the engine's retry: a fresh worker, told what happened. Only workers on a run count against the cap: a worker between runs is parked, keeping its
102
- session for follow-ups while its slot goes to the queue. A finished worker's tab lingers for ten minutes
103
- of follow-up and closes once its pi sits idle, checked again every ten minutes while it is busy. At startup the router also closes worker tabs that no record owns or whose agent herdr no longer
104
- knows.
105
-
106
- **GitHub out: `outbox.ts` and `receipts.ts`.** Every way a run ends (a report, a question, a crash, a run
107
- with no report, a launch that fails) becomes a message on the thread that says what happened and what the
108
- person can do next; `receipts.ts` holds the wording. Acknowledgements, receipts and notices are queued
109
- under ids, and an id is never queued twice: a launch that fails on every poll posts once. A message counts as sent only once GitHub accepts it; a failed post stays pending for
110
- the next pass. Every comment ends with an invisible marker, `<!-- superagent -->`, which lets the source
111
- recognize the agent's writing even when the agent and the operator share one GitHub account. Sent entries
112
- are pruned after 30 days.
113
-
114
- **The panel: `server.ts`.** A page and a small JSON API on 127.0.0.1, port 8788 by default, computed
115
- from the engine's store on every request, so the page can show no count the API would deny. It refuses
116
- any Host header other than 127.0.0.1 or localhost, and any API call without `x-superagent-request: 1`,
117
- so a web page elsewhere cannot read or steer it. If the port is taken, the panel binds a free one and logs
118
- which; it never takes the router down.
119
-
120
- **The start script: `scripts/superagent`.** Starts the router as a pi agent in a herdr tab, stops it, and
121
- reports its health as an exit code: 0 healthy, 1 down, 2 a part is failing, 3 waiting for setup. The
122
- launchd job in `launchd/` runs it every five minutes, restarts a router that is down, and leaves alone a
123
- router that was stopped on purpose.
38
+ ### Ticket engine
39
+
40
+ The vendored state machine lives in `src/core/`. `intake.ts` routes triggers to an existing ticket
41
+ or queues an evaluation. `evals.ts` turns an accepted evaluation into a ticket.
42
+ `assignments.ts` records reports and tracks the running assignment. `tickets.ts` handles stops,
43
+ retries and wake-ups. `replies.ts` gives each acknowledgement or reply a stable queue key.
44
+ The synchronous engine reaches storage, processes and models through the interfaces in `ports.ts`.
45
+
46
+ ### Router and intake
47
+
48
+ `src/pi/extension.ts` creates the store, GitHub source, judge, executor, outbox and panel. By default
49
+ the router polls every 30 seconds and also wakes when its pi session goes idle. A pass delivers new
50
+ triggers, reconciles PRs and closed threads, evaluates requests, starts pending work, checks for quiet
51
+ workers and flushes replies. Three consecutive GitHub poll failures set an error in `health.json`.
52
+
53
+ `source-github.ts` tracks issues, comments and submitted reviews. It recognizes the router’s own
54
+ comments by the bot login or a hidden marker. `scopeOf` in `router.ts` checks each author’s GitHub
55
+ association or the configured login list before the trigger can reach a judge or worker.
56
+ `gh-api.ts` runs the CLI asynchronously, paginates issue and PR lists, and reads comments forward
57
+ from the saved position.
58
+
59
+ ### Judge
60
+
61
+ `policyOf` in `router.ts` checks human authorship and bot patterns. For a request that needs a
62
+ decision, `assessor.ts` assembles the thread, messages, existing tickets and issue link.
63
+ `judge.ts` runs one candidate at a time in a headless pi session with a single tool, `ticket_decide`.
64
+ The session excludes extensions, skills and context files. The router overrides an acceptance if
65
+ an open ticket already owns the thread.
66
+
67
+ ### Workers
68
+
69
+ `executor.ts` schedules an assignment and `herdr-workers.ts` starts pi in a new tab. By default the
70
+ worker gets a clone of the watched repository on its own ticket branch. Under `workers/<session>/`,
71
+ the router writes prompts to `inbox/` and the worker appends to `events.jsonl` and `reports.jsonl`.
72
+ A restarted router can adopt the tab and resume reading those files.
73
+
74
+ Inside the session, `worker-role.ts` registers `ticket_report` and the tool guards in `workspace.ts`.
75
+ The worker observes the order of its own events, so it can report a run that ended without a
76
+ report. A crash frees the slot immediately. On the first crash the router starts a replacement in
77
+ the same workspace and includes the previous failure in its brief.
78
+
79
+ A parked worker keeps its context and releases its slot. A finished worker’s tab stays open for
80
+ ten minutes. If pi is still busy, the router checks again ten minutes later. At startup it also
81
+ checks for tabs with no owning record or no corresponding herdr agent.
82
+
83
+ ### Pull requests and replies
84
+
85
+ `pulls.ts` binds each reported PR to its ticket with `bindThread` before the engine processes
86
+ the report. That order matters. A ready report on a ticket with an open PR leaves it awaiting review.
87
+ When the PR leaves the watched repository’s open list, the follower checks whether it merged or
88
+ closed and calls `threadClosed`. For another repository it checks every two minutes and routes
89
+ comments and reviews through the qualified thread ID, `owner/repo#N`.
90
+
91
+ `receipts.ts` formats reports, questions, crashes and launch failures for the GitHub thread.
92
+ `outbox.ts` queues messages by ID. Failed posts remain pending, and a recurring launch failure
93
+ produces one notice. The source recognizes the hidden `<!-- superagent -->` marker on outgoing
94
+ comments. Sent entries expire after 30 days.
95
+
96
+ ### Panel and process management
97
+
98
+ `server.ts` reads the engine store for each page or API request. It listens on `127.0.0.1:8788`
99
+ by default and logs the chosen port if it has to bind a free one. It checks the Host header against
100
+ loopback names and requires `x-superagent-request: 1` on API calls.
101
+
102
+ `scripts/superagent` configures the repository, starts or stops the router and reports health.
103
+ The macOS job in `launchd/` checks every five minutes. It restarts a down router and respects the
104
+ operator’s stop marker.
124
105
 
125
106
  ## The state directory
126
107
 
127
- Everything the system knows lives in one directory, `~/.pi/superagent` unless `PI_SUPERAGENT_STATE` says
128
- otherwise.
108
+ The router stores its local state under `~/.pi/superagent`. Set `PI_SUPERAGENT_STATE` to use a different directory.
129
109
 
130
110
  | Path | What it holds |
131
111
  | --- | --- |
@@ -137,8 +117,7 @@ otherwise.
137
117
  | `work/<ticket>/` | one workspace per ticket |
138
118
  | `worker-bin/` | shims that refuse blocked commands, first on the PATH of each workspace's setup script |
139
119
 
140
- To start over, stop the router and delete the directory. The next first look treats every open issue as
141
- history.
120
+ To start over, stop the router and delete the directory. On the next start, the first poll records existing open issues as history.
142
121
 
143
122
  ## Where each guarantee lives
144
123
 
@@ -149,12 +128,12 @@ history.
149
128
  | A person's stop is final | guards in `core/tickets.ts` and `core/intake.ts` | `Ticket.tla` I1 |
150
129
  | One running assignment per ticket | the executor's launch guard | `Ticket.tla` I3 |
151
130
  | Each acknowledgement queued and sent once | outbox keys in `core/replies.ts` | `Ticket.tla` I5, I6 |
152
- | Issues older than the router fire nothing | the seed rule | `Intake.tla` C1 |
131
+ | The initial poll treats existing open issues as history | the seed rule | `Intake.tla` C1 |
153
132
  | One request per open interval of an issue | the seen set | `Intake.tla` C2 |
154
133
  | No comment lost to a crash | deferred read positions | `Intake.tla` C3, C6 |
155
134
  | No trigger accepted twice | content-derived ids | `Intake.tla` C4 |
156
135
  | A bot never passes for a person | bot patterns and the policy gate | `Intake.tla` C5 |
157
- | A stranger reaches neither the judge nor a worker | the source scope, `scopeOf` | tests only |
136
+ | Only authors admitted by the source scope reach the judge or a worker | the source scope, `scopeOf` | tests only |
158
137
  | A ticket with a pull request ends with it, as merged or closed | binding before the report, `threadClosed` | tests only |
159
138
  | A live agent's tab is never swept | the liveness check in `closeStaleTabs` | `Workers.tla` W2, W4 |
160
139
  | Dead workers' tabs do not pile up | the sweep and the linger close | `Workers.tla` W5 |