pi-superagent 0.0.0-stage → 0.1.0
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/ADAPTATION.md +73 -0
- package/LICENSE +21 -0
- package/NOTICE +19 -0
- package/README.md +151 -2
- package/config.example.env +81 -0
- package/docs/architecture.md +160 -0
- package/docs/design-decisions.md +230 -0
- package/docs/formal-verification.md +106 -0
- package/docs/operator-guide.md +137 -0
- package/formal/Intake.cfg +18 -0
- package/formal/Intake.tla +206 -0
- package/formal/README.md +31 -0
- package/formal/Ticket.cfg +23 -0
- package/formal/Ticket.tla +244 -0
- package/formal/Workers.cfg +20 -0
- package/formal/Workers.tla +152 -0
- package/launchd/com.example.superagent-health.plist +31 -0
- package/package.json +72 -4
- package/scripts/superagent +219 -0
- package/src/core/assignments.ts +140 -0
- package/src/core/engine.ts +270 -0
- package/src/core/errors.ts +61 -0
- package/src/core/evals.ts +340 -0
- package/src/core/ids.ts +5 -0
- package/src/core/index.ts +10 -0
- package/src/core/intake.ts +63 -0
- package/src/core/memory.ts +50 -0
- package/src/core/model.ts +353 -0
- package/src/core/part.ts +80 -0
- package/src/core/ports.ts +134 -0
- package/src/core/reasons.ts +38 -0
- package/src/core/replies.ts +95 -0
- package/src/core/stage.ts +26 -0
- package/src/core/tickets.ts +423 -0
- package/src/pi/assessor.ts +97 -0
- package/src/pi/authors.ts +10 -0
- package/src/pi/executor.ts +267 -0
- package/src/pi/extension.ts +703 -0
- package/src/pi/gh-api.ts +147 -0
- package/src/pi/herdr-workers.ts +299 -0
- package/src/pi/herdr.ts +91 -0
- package/src/pi/judge.ts +60 -0
- package/src/pi/log.ts +35 -0
- package/src/pi/outbox.ts +82 -0
- package/src/pi/pulls.ts +154 -0
- package/src/pi/receipts.ts +43 -0
- package/src/pi/report-tool.ts +35 -0
- package/src/pi/router.ts +125 -0
- package/src/pi/sdk-contract.ts +51 -0
- package/src/pi/server.ts +171 -0
- package/src/pi/sink-session-inbox.ts +28 -0
- package/src/pi/source-github.ts +209 -0
- package/src/pi/store.ts +132 -0
- package/src/pi/worker-role.ts +79 -0
- package/src/pi/worker-view.ts +80 -0
- package/src/pi/workspace.ts +155 -0
- package/vendor/dsh-superagent/LICENSE +21 -0
- package/vendor/dsh-superagent/UPSTREAM_SHA +1 -0
- package/vendor/dsh-superagent/docs/000-philosophy.md +33 -0
- package/vendor/dsh-superagent/docs/001-domain.md +112 -0
- package/vendor/dsh-superagent/docs/002-authorization.md +155 -0
- package/vendor/dsh-superagent/docs/003-execution.md +203 -0
- package/vendor/dsh-superagent/docs/004-prompts.md +96 -0
- package/vendor/dsh-superagent/docs/005-panel.md +58 -0
- package/vendor/dsh-superagent/docs/006-deployment.md +274 -0
- package/vendor/dsh-superagent/docs/007-architecture.md +617 -0
- package/vendor/dsh-superagent/docs/008-cookbook.md +425 -0
package/ADAPTATION.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Adaptation ledger
|
|
2
|
+
|
|
3
|
+
The engine in `src/core/` is DeepSeek's, copied from upstream without a single change. This file is the
|
|
4
|
+
record that keeps that true: the commit it was copied from, how to prove the copy is exact, and where
|
|
5
|
+
everything that is *not* upstream plugs in.
|
|
6
|
+
|
|
7
|
+
## The baseline
|
|
8
|
+
|
|
9
|
+
Upstream commit `8c32a9dc5d72c033816c6a8110b37f40cf94fba4`, also recorded in
|
|
10
|
+
`vendor/dsh-superagent/UPSTREAM_SHA`. At that commit these are byte-identical:
|
|
11
|
+
|
|
12
|
+
| Here | Upstream |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| `src/core/` | `src/core/` |
|
|
15
|
+
| `test/core-engine.spec.ts` | `tests/core-engine.spec.ts` |
|
|
16
|
+
| `vendor/dsh-superagent/docs/` | `docs/` |
|
|
17
|
+
| `vendor/dsh-superagent/LICENSE` | `LICENSE` |
|
|
18
|
+
|
|
19
|
+
To prove it, compare against a fresh clone. Every `diff` should print nothing:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
git clone https://github.com/deepseek-ai/dsh-superagent-prototype /tmp/upstream
|
|
23
|
+
git -C /tmp/upstream checkout "$(cat vendor/dsh-superagent/UPSTREAM_SHA)"
|
|
24
|
+
diff -r /tmp/upstream/src/core src/core
|
|
25
|
+
diff /tmp/upstream/tests/core-engine.spec.ts test/core-engine.spec.ts
|
|
26
|
+
diff -r /tmp/upstream/docs vendor/dsh-superagent/docs
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Deviations
|
|
30
|
+
|
|
31
|
+
None. If one is ever unavoidable, list it here with the file, the upstream commit it was measured
|
|
32
|
+
against, what changed and why, so that the next upstream fix can still be merged by hand.
|
|
33
|
+
|
|
34
|
+
## Where this repository plugs in
|
|
35
|
+
|
|
36
|
+
The engine is synchronous and does no I/O of its own. Everything that touches a process, the network or
|
|
37
|
+
a model reaches it through an interface in `src/core/ports.ts`, and every binding of those interfaces
|
|
38
|
+
lives in `src/pi/`:
|
|
39
|
+
|
|
40
|
+
| Port | Bound in | How |
|
|
41
|
+
| --- | --- | --- |
|
|
42
|
+
| `Repositories` | `store.ts` | synchronous `node:sqlite` in WAL mode with `synchronous=FULL`, one transaction per engine command |
|
|
43
|
+
| `Policy` | `router.ts` (`policyOf`) | admits a trigger only from a person who is not on the bot list |
|
|
44
|
+
| `SourceScope` | `extension.ts` | every thread is listened to |
|
|
45
|
+
| `Assessor` | `assessor.ts`, `judge.ts` | candidates go one at a time to a headless pi judge, which answers with `ticket_decide` |
|
|
46
|
+
| `Executor` | `executor.ts`, `herdr-workers.ts` | one pi session per assignment, in its own herdr tab, reached through files that outlive the router |
|
|
47
|
+
| `Outbox` | `outbox.ts` | GitHub comments, queued once per id and marked sent only once GitHub accepts them |
|
|
48
|
+
|
|
49
|
+
Sources are not a port upstream; the engine simply receives Triggers. `source-github.ts` produces them
|
|
50
|
+
from issues, comments and reviews, with read positions kept in the same SQLite file as the engine's state.
|
|
51
|
+
|
|
52
|
+
Upstream ships its own executor, Feishu and GitHub sources, panel and SQLite store. None of them are
|
|
53
|
+
vendored: this repository replaces them with pi sessions in herdr, the GitHub source above, the panel in
|
|
54
|
+
`server.ts`, and the store in `store.ts`.
|
|
55
|
+
|
|
56
|
+
Two of upstream's rules carry through every binding:
|
|
57
|
+
|
|
58
|
+
- **A report is a tool call, not prose.** A worker's `ticket_report` call sets the state of its
|
|
59
|
+
assignment; a question in a *blocked* report becomes the ticket's request for a person.
|
|
60
|
+
- **Only the engine writes a ticket.** A worker that could edit its own ticket could mark its own work
|
|
61
|
+
done.
|
|
62
|
+
|
|
63
|
+
## Tests
|
|
64
|
+
|
|
65
|
+
`test/core-engine.spec.ts` is upstream's engine suite, unchanged. `test/sqlite-engine.spec.ts` runs the
|
|
66
|
+
same 113 cases against the SQLite store, with one line changed, because durability can only be tested
|
|
67
|
+
against a store that survives the process.
|
|
68
|
+
|
|
69
|
+
## Taking an upstream change
|
|
70
|
+
|
|
71
|
+
Copy upstream's `src/core/`, `tests/core-engine.spec.ts` and `docs/` over their counterparts here, update
|
|
72
|
+
`UPSTREAM_SHA`, and run `npm run check` and `scripts/check-formal`. If a ticket transition changed,
|
|
73
|
+
change `formal/Ticket.tla` to match before trusting the result.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 the pi-superagent authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/NOTICE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# NOTICE: vendored code
|
|
2
|
+
|
|
3
|
+
`src/core/` and `test/core-engine.spec.ts` are copied, unchanged, from:
|
|
4
|
+
|
|
5
|
+
- project: **dsh-superagent-prototype**, a DeepSeek research prototype
|
|
6
|
+
- repository: https://github.com/deepseek-ai/dsh-superagent-prototype
|
|
7
|
+
- commit: `8c32a9dc5d72c033816c6a8110b37f40cf94fba4` (2026-10-31, "feat: add the dsh-superagent research prototype")
|
|
8
|
+
- license: MIT, full text in `vendor/dsh-superagent/LICENSE`
|
|
9
|
+
|
|
10
|
+
`vendor/dsh-superagent/docs/` keeps upstream's nine design documents. They specify the state machine in
|
|
11
|
+
`src/core`, and they are the place to look when one of its rules seems arbitrary: start with
|
|
12
|
+
`001-domain.md` (the concepts, and the ones rejected), `002-authorization.md` (the gate and the admission
|
|
13
|
+
table) or `003-execution.md` (reports and attention).
|
|
14
|
+
|
|
15
|
+
Upstream calls its code a research prototype. This repository vendors the domain, not the deployment:
|
|
16
|
+
upstream's executor, its Feishu and GitHub sources and its panel are replaced here by pi sessions in
|
|
17
|
+
herdr, a GitHub source of this repository's own, and a local panel.
|
|
18
|
+
|
|
19
|
+
[ADAPTATION.md](ADAPTATION.md) shows how to prove the copy is exact, and where each binding plugs in.
|
package/README.md
CHANGED
|
@@ -1,3 +1,152 @@
|
|
|
1
|
-
#
|
|
1
|
+
# pi-superagent
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Open a GitHub issue. An agent does the work and answers you there.**
|
|
4
|
+
|
|
5
|
+
pi-superagent watches one GitHub repository. When someone opens an issue, comments on one, or reviews a
|
|
6
|
+
pull request, a judge reads it and decides whether a person has asked for something concrete. If one has,
|
|
7
|
+
a ticket opens and a worker starts: a full [pi](https://github.com/earendil-works/pi) session in its own
|
|
8
|
+
tab of herdr, a terminal multiplexer for coding agents, where you can watch it work, type into it, or take
|
|
9
|
+
over. The worker finishes by calling a report tool, and the result arrives on the issue as a reply. Work
|
|
10
|
+
that changes a repository arrives as a pull request, and the ticket stays open until it is merged.
|
|
11
|
+
|
|
12
|
+
There are no labels to learn and no board to tend. The issue is the request, a comment is a follow-up,
|
|
13
|
+
and closing the issue is the stop.
|
|
14
|
+
|
|
15
|
+
The ticket engine underneath is DeepSeek's
|
|
16
|
+
[dsh-superagent-prototype](https://github.com/deepseek-ai/dsh-superagent-prototype), vendored
|
|
17
|
+
byte-for-byte. This repository binds it to pi, herdr and GitHub.
|
|
18
|
+
|
|
19
|
+
## How a request travels
|
|
20
|
+
|
|
21
|
+
1. **Trigger.** A new issue is a request; so is a comment on an issue or a pull request, and so is a
|
|
22
|
+
submitted review. Only people with write access to the repository are heard: on a public repository
|
|
23
|
+
anyone can open an issue, and a request starts a worker with a shell on your machine. On its first
|
|
24
|
+
look at a repository the router records what is already there and fires none of it.
|
|
25
|
+
2. **Judgement.** The judge, a headless pi session, reads the request with its link and answers through
|
|
26
|
+
a tool: *accept*, *ignore* (chatter, a note to a colleague, a thread that already has a ticket) or
|
|
27
|
+
*stop*.
|
|
28
|
+
3. **Work.** An accept opens a ticket and posts the judge's acknowledgement on the issue. A worker starts
|
|
29
|
+
in a workspace of its own, briefed with the ticket's mandate. Every later comment a person writes on
|
|
30
|
+
the thread goes straight to that worker.
|
|
31
|
+
4. **Report.** The worker ends every run by calling `ticket_report`, and the router posts the report on
|
|
32
|
+
the issue. A *blocked* report asks you a question there, and your reply goes straight back to the
|
|
33
|
+
worker. A run that ends without a report, a worker that crashes, and a launch that fails are posted
|
|
34
|
+
too, each with what you can do next. A crashed worker is replaced once, without anyone asking.
|
|
35
|
+
5. **Pull request.** Work that changes a repository, in any repository, goes out as a pull request the
|
|
36
|
+
worker opens and names in its report. The ticket then waits for review: comments and reviews on the
|
|
37
|
+
pull request go to the same worker. The merge ends the ticket, and the issue hears it is done; closing
|
|
38
|
+
the pull request unmerged ends it too. Work that changes no repository, such as an answer, an
|
|
39
|
+
investigation or a deployment, ends with its report.
|
|
40
|
+
|
|
41
|
+
Closing the issue stops its ticket and says so on the thread. Reopening the issue starts the request again.
|
|
42
|
+
|
|
43
|
+
## Why it is built this way
|
|
44
|
+
|
|
45
|
+
pi-superagent grew out of a setup where agents polled markdown cards, prompts were typed into terminal
|
|
46
|
+
panes, and a classifier guessed from prose whether a person had answered. Every failure there was the
|
|
47
|
+
same defect in a different hat: **state recorded as text, transitions guessed by readers.** Three rules
|
|
48
|
+
answer it:
|
|
49
|
+
|
|
50
|
+
- **A worker reports through a tool call, never in prose.** "Done" in a chat message changes nothing.
|
|
51
|
+
- **Only the engine writes a ticket.** A worker that could edit its own ticket could mark its own work done.
|
|
52
|
+
- **A restart loses nothing and repeats nothing.** A read position commits only once the engine holds
|
|
53
|
+
what it produced, every trigger carries an id the engine accepts once, and every outgoing message
|
|
54
|
+
carries an id the outbox posts once.
|
|
55
|
+
|
|
56
|
+
The ticket lifecycle, the GitHub intake and the worker sweep are specified in TLA+ and checked
|
|
57
|
+
exhaustively, 1.5 million distinct states in all, by one command.
|
|
58
|
+
|
|
59
|
+
## Install
|
|
60
|
+
|
|
61
|
+
Use macOS or Linux with Git, Node 22.19 or later, [pi](https://github.com/earendil-works/pi),
|
|
62
|
+
[herdr](https://herdr.dev), and the [GitHub CLI](https://cli.github.com).
|
|
63
|
+
Sign into a model provider in pi and confirm your model appears in `pi --list-models`.
|
|
64
|
+
Use a GitHub account with write access to the repository you want to watch.
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
gh auth login
|
|
68
|
+
gh auth setup-git
|
|
69
|
+
pi install npm:pi-superagent
|
|
70
|
+
npm install -g pi-superagent
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Pi loads the extension in router and worker sessions. The global npm install puts the
|
|
74
|
+
`superagent` command on your PATH. Use the same package version for both.
|
|
75
|
+
|
|
76
|
+
## Run your first ticket
|
|
77
|
+
|
|
78
|
+
Substitute the GitHub repository you want the worker to change. `init` checks the local tools
|
|
79
|
+
and your GitHub access, then saves the repository in `~/.pi/superagent/config.env`.
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
superagent init your-org/your-repo
|
|
83
|
+
herdr # keep it open, then use another terminal
|
|
84
|
+
superagent start
|
|
85
|
+
superagent status
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Wait for the first GitHub poll in the **engine log** tab, then open a new issue asking for a small
|
|
89
|
+
change. The first poll records existing issues as the starting point. Watch for the acknowledgement
|
|
90
|
+
on your issue. The worker clones the watched repository into its own directory, switches to a ticket
|
|
91
|
+
branch, and posts the pull request on the issue. Request a change on the PR to send it back to the
|
|
92
|
+
worker. Merge when you are satisfied.
|
|
93
|
+
|
|
94
|
+
To supply your own workspace setup, set `PI_SUPERAGENT_PREPARE`. A replacement worker keeps the same
|
|
95
|
+
ticket directory. To narrow who can start work or give the router its own GitHub identity, see
|
|
96
|
+
[`config.example.env`](config.example.env). Stop the router with `superagent stop`.
|
|
97
|
+
|
|
98
|
+
## Watching it
|
|
99
|
+
|
|
100
|
+
Everything happens in the herdr workspace `superagent`:
|
|
101
|
+
|
|
102
|
+
| Tab | What it shows |
|
|
103
|
+
| --- | --- |
|
|
104
|
+
| `engine log` | the router's log as it happens: every poll, judgement, launch and receipt |
|
|
105
|
+
| `engine` | the router itself, a pi session that stays quiet while its loop runs in the background |
|
|
106
|
+
| one per ticket | that ticket's worker, live, labelled `#141 · <issue title>` |
|
|
107
|
+
|
|
108
|
+
The panel at <http://127.0.0.1:8788/> answers three questions at a glance: does anything need me, what
|
|
109
|
+
is running, and what did the judge decide.
|
|
110
|
+
|
|
111
|
+
## Documentation
|
|
112
|
+
|
|
113
|
+
| Read | When you want |
|
|
114
|
+
| --- | --- |
|
|
115
|
+
| [Operator guide](docs/operator-guide.md) | to ask for work, watch it, and fix what goes wrong |
|
|
116
|
+
| [Architecture](docs/architecture.md) | the map: what runs, in what order, and which file owns each rule |
|
|
117
|
+
| [Design decisions](docs/design-decisions.md) | the reasons, each with the failure that taught it |
|
|
118
|
+
| [Formal verification](docs/formal-verification.md) | what the TLA+ specifications prove, and what they do not |
|
|
119
|
+
| [Adaptation ledger](ADAPTATION.md) | how the vendored engine is bound, and how to prove it unmodified |
|
|
120
|
+
|
|
121
|
+
## Repository layout
|
|
122
|
+
|
|
123
|
+
| Path | What it holds |
|
|
124
|
+
| --- | --- |
|
|
125
|
+
| `src/core/` | the vendored engine: tickets, evaluations, assignments, the outbox queue |
|
|
126
|
+
| `src/pi/extension.ts` | the router: wires the parts together and runs the loop |
|
|
127
|
+
| `src/pi/source-github.ts`, `gh-api.ts` | GitHub in: issues, comments and reviews become triggers |
|
|
128
|
+
| `src/pi/pulls.ts` | pull requests: bound to their ticket, followed until merged or closed |
|
|
129
|
+
| `src/pi/assessor.ts`, `judge.ts` | the judge: one candidate at a time, answered through `ticket_decide` |
|
|
130
|
+
| `src/pi/executor.ts`, `herdr-workers.ts` | workers: pi sessions in herdr tabs, and records that outlive the router |
|
|
131
|
+
| `src/pi/worker-role.ts`, `report-tool.ts`, `workspace.ts` | inside a worker: the report tool, the guard, the workspace |
|
|
132
|
+
| `src/pi/outbox.ts`, `receipts.ts` | GitHub out: what each ending says, each message posted once |
|
|
133
|
+
| `src/pi/store.ts` | the engine's SQLite store |
|
|
134
|
+
| `src/pi/server.ts` | the panel and its local API |
|
|
135
|
+
| `scripts/superagent` | start, stop, restart and status, with exit codes a health check can read |
|
|
136
|
+
| `formal/` | the TLA+ specifications |
|
|
137
|
+
|
|
138
|
+
## Development
|
|
139
|
+
|
|
140
|
+
```sh
|
|
141
|
+
npm install
|
|
142
|
+
npm run check # typecheck and the full suite: no model calls, no network
|
|
143
|
+
npm run check:packed # pack, install globally into a temporary prefix, run both halves from there
|
|
144
|
+
scripts/check-formal # model-check all three specifications (about five minutes; needs a JDK)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`npm publish` runs the first two first and stops on a failure.
|
|
148
|
+
|
|
149
|
+
The suite clears every `PI_SUPERAGENT_*` variable before it runs (`test/setup.ts`), so no test can reach a
|
|
150
|
+
live router.
|
|
151
|
+
|
|
152
|
+
MIT licensed. The vendored engine is MIT licensed too; see [NOTICE](NOTICE).
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# pi-superagent settings.
|
|
2
|
+
#
|
|
3
|
+
# Copy this file to config.env in the state directory (~/.pi/superagent unless PI_SUPERAGENT_STATE says
|
|
4
|
+
# otherwise), edit it, and run `superagent restart`. The start script reads it on every start, so
|
|
5
|
+
# a restart by hand and a restart by the health check run the same router. A variable already set in the
|
|
6
|
+
# environment wins over this file.
|
|
7
|
+
#
|
|
8
|
+
# One KEY=value per line, taken literally: no quotes, no $VARIABLES, no ~, and no comment after a value.
|
|
9
|
+
# Each commented-out line shows a setting's default.
|
|
10
|
+
|
|
11
|
+
# ---- GitHub, the only way in ----
|
|
12
|
+
|
|
13
|
+
# The repository to watch. Unset, the router runs with intake switched off.
|
|
14
|
+
PI_SUPERAGENT_GITHUB_REPO=your-org/your-repo
|
|
15
|
+
|
|
16
|
+
# The account the agent replies from, if it has one of its own: its comments are never requests. Leave it
|
|
17
|
+
# unset when you and the agent share an account. The agent's replies then carry a hidden marker instead,
|
|
18
|
+
# and naming your own account here would discard every comment you write.
|
|
19
|
+
#PI_SUPERAGENT_GITHUB_SELF=your-agent-bot
|
|
20
|
+
|
|
21
|
+
# The agent's own GitHub account: a file holding a token for it (classic, repo scope), read on every call.
|
|
22
|
+
# The router then reads the watched repository and posts every reply as that account. Give the account
|
|
23
|
+
# access to the watched repository, set PI_SUPERAGENT_GITHUB_SELF to its login, and keep the file readable
|
|
24
|
+
# only by you (chmod 600). Workers still act as the account gh is signed in as, and the router reads their
|
|
25
|
+
# pull requests as that account too, since it opened them. Unset, the router acts as gh's account.
|
|
26
|
+
#PI_SUPERAGENT_GITHUB_TOKEN_FILE=/absolute/path/to/token
|
|
27
|
+
|
|
28
|
+
# Author patterns that are bots, never people. A * matches anything.
|
|
29
|
+
#PI_SUPERAGENT_BOTS=pi-*,fleet-*
|
|
30
|
+
|
|
31
|
+
# Who the router hears. Unset, only people with write access to the repository: GitHub's OWNER, MEMBER
|
|
32
|
+
# (any member of the owning organization) and COLLABORATOR. Everyone else's issues, comments and reviews
|
|
33
|
+
# are dropped before the judge sees them, because a request starts a worker with a shell on this machine.
|
|
34
|
+
# Set, this comma-separated list of logins replaces that rule, so include your own. A * matches anything.
|
|
35
|
+
#PI_SUPERAGENT_ALLOWED=your-login,a-teammate
|
|
36
|
+
|
|
37
|
+
# ---- the router ----
|
|
38
|
+
|
|
39
|
+
# Milliseconds between polls of GitHub when nothing else wakes the router.
|
|
40
|
+
#PI_SUPERAGENT_TICK_MS=30000
|
|
41
|
+
|
|
42
|
+
# The panel's port, on 127.0.0.1.
|
|
43
|
+
#PI_SUPERAGENT_PANEL_PORT=8788
|
|
44
|
+
|
|
45
|
+
# The judge's thinking level, in pi's names for them.
|
|
46
|
+
#PI_SUPERAGENT_JUDGE_THINKING=low
|
|
47
|
+
|
|
48
|
+
# Seconds `superagent stop` waits for the router to exit before closing its tab.
|
|
49
|
+
#PI_SUPERAGENT_GRACE=30
|
|
50
|
+
|
|
51
|
+
# ---- herdr ----
|
|
52
|
+
|
|
53
|
+
# The herdr binary, if it is not on PATH.
|
|
54
|
+
#PI_SUPERAGENT_HERDR=herdr
|
|
55
|
+
|
|
56
|
+
# The workspace that holds the router and its workers.
|
|
57
|
+
#PI_SUPERAGENT_HERDR_WORKSPACE=superagent
|
|
58
|
+
|
|
59
|
+
# The router's agent name in herdr.
|
|
60
|
+
#PI_SUPERAGENT_SESSION=superagent
|
|
61
|
+
|
|
62
|
+
# ---- workers ----
|
|
63
|
+
|
|
64
|
+
# Workers on a run at once. Workers parked between runs do not count; further tickets wait their turn.
|
|
65
|
+
#PI_SUPERAGENT_MAX_WORKERS=4
|
|
66
|
+
|
|
67
|
+
# An absolute path for the per-ticket workspaces. The default is the work directory in the state directory.
|
|
68
|
+
#PI_SUPERAGENT_WORKTREES=/absolute/path/to/workspaces
|
|
69
|
+
|
|
70
|
+
# By default a new worker clones the watched repository and checks out superagent/<ticket-id>.
|
|
71
|
+
# Set this command to replace the default checkout. It runs with PI_SUPERAGENT_TICKET and
|
|
72
|
+
# PI_SUPERAGENT_DIR set. It runs again whenever the ticket gets a fresh worker, so make it safe to repeat.
|
|
73
|
+
# If it fails, the ticket fails with the command's own output.
|
|
74
|
+
#PI_SUPERAGENT_PREPARE=[ -d .git ] || git clone --depth 1 https://github.com/your-org/your-repo .
|
|
75
|
+
|
|
76
|
+
# Command names a worker may not run, comma-separated. Names only: no arguments, no paths.
|
|
77
|
+
#PI_SUPERAGENT_BLOCKED=gh,ssh
|
|
78
|
+
|
|
79
|
+
# Set to 1 to let workers start subagents, workflows and other sessions. Delegated work escapes the
|
|
80
|
+
# ticket's mandate, workspace and report, so it is off by default.
|
|
81
|
+
#PI_SUPERAGENT_ALLOW_DELEGATION=1
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Architecture
|
|
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.
|
|
52
|
+
|
|
53
|
+
## The parts
|
|
54
|
+
|
|
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.
|
|
124
|
+
|
|
125
|
+
## The state directory
|
|
126
|
+
|
|
127
|
+
Everything the system knows lives in one directory, `~/.pi/superagent` unless `PI_SUPERAGENT_STATE` says
|
|
128
|
+
otherwise.
|
|
129
|
+
|
|
130
|
+
| Path | What it holds |
|
|
131
|
+
| --- | --- |
|
|
132
|
+
| `tickets.db` | the engine's SQLite store, including read positions and the outbox |
|
|
133
|
+
| `config.env` | deployment settings, read by the start script |
|
|
134
|
+
| `engine.log` | the router's log, rotated at 5 MB |
|
|
135
|
+
| `health.json`, `setup.json`, `ready` | what `superagent status` reads |
|
|
136
|
+
| `workers/<session>/` | one directory per worker: its record, inbox, events and reports |
|
|
137
|
+
| `work/<ticket>/` | one workspace per ticket |
|
|
138
|
+
| `worker-bin/` | shims that refuse blocked commands, first on the PATH of each workspace's setup script |
|
|
139
|
+
|
|
140
|
+
To start over, stop the router and delete the directory. The next first look treats every open issue as
|
|
141
|
+
history.
|
|
142
|
+
|
|
143
|
+
## Where each guarantee lives
|
|
144
|
+
|
|
145
|
+
| Guarantee | Enforced by | Proved in |
|
|
146
|
+
| --- | --- | --- |
|
|
147
|
+
| One ticket per thread | intake routing in the engine; the router overrules a second accept | `Ticket.tla` I4 |
|
|
148
|
+
| A ticket completes only on a *ready* report | `core/assignments.ts` | `Ticket.tla` I2 |
|
|
149
|
+
| A person's stop is final | guards in `core/tickets.ts` and `core/intake.ts` | `Ticket.tla` I1 |
|
|
150
|
+
| One running assignment per ticket | the executor's launch guard | `Ticket.tla` I3 |
|
|
151
|
+
| 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 |
|
|
153
|
+
| One request per open interval of an issue | the seen set | `Intake.tla` C2 |
|
|
154
|
+
| No comment lost to a crash | deferred read positions | `Intake.tla` C3, C6 |
|
|
155
|
+
| No trigger accepted twice | content-derived ids | `Intake.tla` C4 |
|
|
156
|
+
| 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 |
|
|
158
|
+
| A ticket with a pull request ends with it, as merged or closed | binding before the report, `threadClosed` | tests only |
|
|
159
|
+
| A live agent's tab is never swept | the liveness check in `closeStaleTabs` | `Workers.tla` W2, W4 |
|
|
160
|
+
| Dead workers' tabs do not pile up | the sweep and the linger close | `Workers.tla` W5 |
|