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.
Files changed (67) hide show
  1. package/ADAPTATION.md +73 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +19 -0
  4. package/README.md +151 -2
  5. package/config.example.env +81 -0
  6. package/docs/architecture.md +160 -0
  7. package/docs/design-decisions.md +230 -0
  8. package/docs/formal-verification.md +106 -0
  9. package/docs/operator-guide.md +137 -0
  10. package/formal/Intake.cfg +18 -0
  11. package/formal/Intake.tla +206 -0
  12. package/formal/README.md +31 -0
  13. package/formal/Ticket.cfg +23 -0
  14. package/formal/Ticket.tla +244 -0
  15. package/formal/Workers.cfg +20 -0
  16. package/formal/Workers.tla +152 -0
  17. package/launchd/com.example.superagent-health.plist +31 -0
  18. package/package.json +72 -4
  19. package/scripts/superagent +219 -0
  20. package/src/core/assignments.ts +140 -0
  21. package/src/core/engine.ts +270 -0
  22. package/src/core/errors.ts +61 -0
  23. package/src/core/evals.ts +340 -0
  24. package/src/core/ids.ts +5 -0
  25. package/src/core/index.ts +10 -0
  26. package/src/core/intake.ts +63 -0
  27. package/src/core/memory.ts +50 -0
  28. package/src/core/model.ts +353 -0
  29. package/src/core/part.ts +80 -0
  30. package/src/core/ports.ts +134 -0
  31. package/src/core/reasons.ts +38 -0
  32. package/src/core/replies.ts +95 -0
  33. package/src/core/stage.ts +26 -0
  34. package/src/core/tickets.ts +423 -0
  35. package/src/pi/assessor.ts +97 -0
  36. package/src/pi/authors.ts +10 -0
  37. package/src/pi/executor.ts +267 -0
  38. package/src/pi/extension.ts +703 -0
  39. package/src/pi/gh-api.ts +147 -0
  40. package/src/pi/herdr-workers.ts +299 -0
  41. package/src/pi/herdr.ts +91 -0
  42. package/src/pi/judge.ts +60 -0
  43. package/src/pi/log.ts +35 -0
  44. package/src/pi/outbox.ts +82 -0
  45. package/src/pi/pulls.ts +154 -0
  46. package/src/pi/receipts.ts +43 -0
  47. package/src/pi/report-tool.ts +35 -0
  48. package/src/pi/router.ts +125 -0
  49. package/src/pi/sdk-contract.ts +51 -0
  50. package/src/pi/server.ts +171 -0
  51. package/src/pi/sink-session-inbox.ts +28 -0
  52. package/src/pi/source-github.ts +209 -0
  53. package/src/pi/store.ts +132 -0
  54. package/src/pi/worker-role.ts +79 -0
  55. package/src/pi/worker-view.ts +80 -0
  56. package/src/pi/workspace.ts +155 -0
  57. package/vendor/dsh-superagent/LICENSE +21 -0
  58. package/vendor/dsh-superagent/UPSTREAM_SHA +1 -0
  59. package/vendor/dsh-superagent/docs/000-philosophy.md +33 -0
  60. package/vendor/dsh-superagent/docs/001-domain.md +112 -0
  61. package/vendor/dsh-superagent/docs/002-authorization.md +155 -0
  62. package/vendor/dsh-superagent/docs/003-execution.md +203 -0
  63. package/vendor/dsh-superagent/docs/004-prompts.md +96 -0
  64. package/vendor/dsh-superagent/docs/005-panel.md +58 -0
  65. package/vendor/dsh-superagent/docs/006-deployment.md +274 -0
  66. package/vendor/dsh-superagent/docs/007-architecture.md +617 -0
  67. 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
- # Temporary Holding Version
1
+ # pi-superagent
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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 |