agentus 0.0.0-stage → 0.2.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/LICENSE +202 -0
- package/NOTICE +11 -0
- package/README.md +476 -2
- package/README.zh-CN.md +396 -0
- package/bin/agentus.mjs +147 -0
- package/docs/android-notify-contract.md +658 -0
- package/docs/dev/01-dev-loop.md +186 -0
- package/docs/dev/02-verify.md +237 -0
- package/docs/dev/03-release.md +218 -0
- package/docs/dev/04-recover.md +199 -0
- package/docs/dev/05-agent-driven.md +114 -0
- package/docs/dev/README.md +65 -0
- package/docs/installation.md +366 -0
- package/docs/installation.zh-CN.md +350 -0
- package/docs/refs/aionui-acp-rules.md +46 -0
- package/docs/refs/hermes-studio-ui-ideas.md +191 -0
- package/docs/transcript-folding.md +139 -0
- package/package.json +76 -5
- package/packages/server/mcp/plan-server.mjs +217 -0
- package/packages/server/mock/agent.mjs +693 -0
- package/packages/server/package.json +22 -0
- package/packages/server/src/acp/backends.ts +196 -0
- package/packages/server/src/acp/errors.ts +46 -0
- package/packages/server/src/acp/registry.ts +579 -0
- package/packages/server/src/acp/session-manager.ts +1868 -0
- package/packages/server/src/auth.ts +688 -0
- package/packages/server/src/dashscope.ts +235 -0
- package/packages/server/src/fs.ts +379 -0
- package/packages/server/src/hotwords.ts +132 -0
- package/packages/server/src/index.ts +1486 -0
- package/packages/server/src/notify/center.ts +994 -0
- package/packages/server/src/notify/types.ts +222 -0
- package/packages/server/src/plan/plan.ts +126 -0
- package/packages/server/src/pty-helper.py +135 -0
- package/packages/server/src/settings.ts +469 -0
- package/packages/server/src/store/export.ts +209 -0
- package/packages/server/src/store/store.ts +1238 -0
- package/packages/server/src/term.ts +201 -0
- package/packages/server/src/title.ts +109 -0
- package/packages/server/src/voice.ts +251 -0
- package/packages/shared/package.json +14 -0
- package/packages/shared/src/index.ts +271 -0
- package/packages/web/dist/assets/index-D667PX2c.css +32 -0
- package/packages/web/dist/assets/index-d9NzwPFu.js +283 -0
- package/packages/web/dist/coding-agents/hermes.png +0 -0
- package/packages/web/dist/coding-agents/qoder.svg +5 -0
- package/packages/web/dist/icons/agentus-icon-inverse.svg +301 -0
- package/packages/web/dist/icons/agentus-icon.svg +301 -0
- package/packages/web/dist/icons/agentus-mark-inverse.svg +301 -0
- package/packages/web/dist/icons/agentus-mark.svg +301 -0
- package/packages/web/dist/icons/apple-touch-icon-180.png +0 -0
- package/packages/web/dist/icons/favicon.ico +0 -0
- package/packages/web/dist/icons/icon-192.png +0 -0
- package/packages/web/dist/icons/icon-512.png +0 -0
- package/packages/web/dist/icons/icon-maskable-512.png +0 -0
- package/packages/web/dist/index.html +30 -0
- package/packages/web/dist/manifest.webmanifest +17 -0
- package/packages/web/dist/sw.js +61 -0
- package/scripts/make-cert.sh +101 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Running a development instance
|
|
2
|
+
|
|
3
|
+
This doc answers: how do I start a dev instance of Agentus, and how is its code and data
|
|
4
|
+
kept away from the live one?
|
|
5
|
+
|
|
6
|
+
## The two-worktree model
|
|
7
|
+
|
|
8
|
+
Agentus is developed in **two git worktrees of the same repository**, running as two
|
|
9
|
+
instances:
|
|
10
|
+
|
|
11
|
+
| | live | dev |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| path | `worktrees/agentus` | `worktrees/agentus-dev` |
|
|
14
|
+
| HTTP port | `8787` | `8901` |
|
|
15
|
+
| TLS port | `8443` | disabled (`AGENTUS_TLS_PORT=0`) |
|
|
16
|
+
| data dir | `<tree>/packages/server/.data` | `/tmp/agentus-qa-account` |
|
|
17
|
+
| started by | `~/.hermes/cache/agentus/launch.py` | `bash scripts/dev.sh start` |
|
|
18
|
+
| watcher | no (one process, no reload) | yes (`tsx watch`) |
|
|
19
|
+
|
|
20
|
+
The live tree serves the operator's real sessions. The dev tree is where you edit; its
|
|
21
|
+
`tsx watch` reloads on save, which is convenient precisely because its sessions are
|
|
22
|
+
disposable. **Never edit the live tree while the operator is using it** — a reload there
|
|
23
|
+
kills his agent children (see [03-release.md](./03-release.md) for the release path and
|
|
24
|
+
[04-recover.md](./04-recover.md) for what to do if it happened).
|
|
25
|
+
|
|
26
|
+
First move of any round: check where you are.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
git -C worktrees/agentus branch --show-current && git -C worktrees/agentus status --short
|
|
30
|
+
git -C worktrees/agentus-dev branch --show-current && git -C worktrees/agentus-dev status --short
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Edit whatever the **dev** tree has checked out. Use absolute paths when writing files —
|
|
34
|
+
the two trees have identical filenames, so nothing reminds you if you picked the wrong one.
|
|
35
|
+
|
|
36
|
+
## The dev launcher: `scripts/dev.sh`
|
|
37
|
+
|
|
38
|
+
Run from the dev worktree root:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
bash scripts/dev.sh start # start :8901, wait for /healthz, re-point the mock backend row
|
|
42
|
+
bash scripts/dev.sh status # is :8901 up? prints the pid, the tree, and /healthz
|
|
43
|
+
bash scripts/dev.sh logs # follow the dev log (tail -f /tmp/agentus-dev.log)
|
|
44
|
+
bash scripts/dev.sh stop # stop the process group on :8901
|
|
45
|
+
bash scripts/dev.sh promote [<slug>] # release to live — see 03-release.md
|
|
46
|
+
bash scripts/dev.sh promote-log # follow /tmp/agentus-qa-account/promote.log
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`start` prints where everything went:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
[dev] pid=<n> port=8901 data=/tmp/agentus-qa-account log=/tmp/agentus-dev.log
|
|
53
|
+
[dev] http://127.0.0.1:8901 login scratch / scratch-pass-1
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Defaults are overridable by `DEV_PORT`, `DEV_DATA`, `DEV_USER`, `DEV_PASS`, `DEV_LOG`.
|
|
57
|
+
|
|
58
|
+
Three things `start` does that matter:
|
|
59
|
+
|
|
60
|
+
- **It detaches** the server from the calling shell's process group (`detached: true`), so
|
|
61
|
+
it does not die with the terminal that launched it.
|
|
62
|
+
- **It strips inherited identity.** `dev.sh` reads the caller's environment and deletes
|
|
63
|
+
every `AGENTUS_*` / `AGENTSL*` / `DEV_*` key it did not set itself, printing
|
|
64
|
+
`[dev] dropped inherited <KEY>`. This exists because a dev watcher that inherits a live
|
|
65
|
+
identity pair will, on a reload, read the live port/data and reap the live agent
|
|
66
|
+
children. Keep that behaviour when you touch the script.
|
|
67
|
+
- **It re-points the `mock` backend row at this tree.** A backend row stores an absolute
|
|
68
|
+
path to the mock agent, seeded by whichever tree first created the store, so a dev
|
|
69
|
+
instance reusing a store can silently spawn the *live* tree's mock. `start` PATCHes the
|
|
70
|
+
row back to `$HERE/packages/server/mock/agent.mjs` each boot and prints
|
|
71
|
+
`[dev] re-pointing backend 'mock' at this tree (was …)` when it does.
|
|
72
|
+
|
|
73
|
+
## Data directories and how they resolve
|
|
74
|
+
|
|
75
|
+
The server resolves its data directory in three ordered steps, and the **boot log prints
|
|
76
|
+
which one won and why** — a directory change is the thing that would otherwise read as
|
|
77
|
+
"my sessions are gone":
|
|
78
|
+
|
|
79
|
+
| order | rule |
|
|
80
|
+
|---|---|
|
|
81
|
+
| 1 | `AGENTUS_DATA` when it is set |
|
|
82
|
+
| 2 | `~/.agentus` once it holds something, or when there is no repo-side data yet |
|
|
83
|
+
| 3 | `<repo>/packages/server/.data` — an existing checkout that predates the install layout |
|
|
84
|
+
|
|
85
|
+
For the worktrees, the outcome is fixed by the launchers rather than by the rules:
|
|
86
|
+
|
|
87
|
+
- **live** — `~/.hermes/cache/agentus/launch.py` pins `AGENTUS_DATA=<live>/packages/server/.data`
|
|
88
|
+
and `AGENTUS_PORT=8787`, and pops the inherited QA/DEV keys. The launcher owns its
|
|
89
|
+
identity; it never trusts `os.environ`.
|
|
90
|
+
- **dev** — `scripts/dev.sh` exports `AGENTUS_DATA=/tmp/agentus-qa-account` and
|
|
91
|
+
`AGENTUS_PORT=8901`.
|
|
92
|
+
|
|
93
|
+
`agentus --where` (or `AGENTUS_PRINT_PATHS=1`) prints the resolved paths and exits without
|
|
94
|
+
starting anything — the single source of truth for "where did my state go".
|
|
95
|
+
|
|
96
|
+
### Keeping a dev instance's data separate
|
|
97
|
+
|
|
98
|
+
The dev instance already points at `/tmp/agentus-qa-account`, so it shares nothing with
|
|
99
|
+
live. Two independent knobs if you need more:
|
|
100
|
+
|
|
101
|
+
- **The store** — override `AGENTUS_DATA` (as `dev.sh` does). A throwaway instance is one
|
|
102
|
+
line:
|
|
103
|
+
```bash
|
|
104
|
+
AGENTUS_PORT=8907 AGENTUS_TLS_PORT=0 AGENTUS_DATA=/tmp/<name> npx tsx packages/server/src/index.ts
|
|
105
|
+
```
|
|
106
|
+
Note the server reads `AGENTUS_PORT`, not bare `PORT` — and this box exports an ambient
|
|
107
|
+
`PORT=8648` (Hermes Studio), so a script that reads the ambient value starts a retry loop
|
|
108
|
+
against the wrong port.
|
|
109
|
+
- **The agent subprocess home** — see below.
|
|
110
|
+
|
|
111
|
+
## The mock agent backend
|
|
112
|
+
|
|
113
|
+
The dev instance runs against a MOCK ACP agent:
|
|
114
|
+
`packages/server/mock/agent.mjs`. It implements enough of the ACP agent surface
|
|
115
|
+
(`initialize` / `newSession` / `prompt` / `cancel` / `setSessionMode` + the permission
|
|
116
|
+
flow) to exercise the whole pipeline with fake streaming and no real model or token spend.
|
|
117
|
+
|
|
118
|
+
The mock switches behaviour on the prompt text (per turn) and on env vars (global
|
|
119
|
+
defaults). The ones QA relies on:
|
|
120
|
+
|
|
121
|
+
| trigger / env | effect |
|
|
122
|
+
|---|---|
|
|
123
|
+
| `[think]` / `[think:N]` | stream N thought chunks before the answer |
|
|
124
|
+
| `[tools:N]` | N tool calls, each with a reasoning burst (the transcript-flood case) |
|
|
125
|
+
| `[tool]`, `[tool-diff]`, `[tool-hermes]` | a tool call + `requestPermission` (the approval surface) |
|
|
126
|
+
| `[table]` | a deliberately-unsorted table, a local file link and a relative image |
|
|
127
|
+
| `[slow]` | ~900 ms between chunks, so a socket can be dropped mid-turn |
|
|
128
|
+
| `[sink]` | exit the process mid-turn (the crash path) |
|
|
129
|
+
| `[plan]`, `[blocks]`, `[mcp]` | plan / structured-block / handshake-shape probes |
|
|
130
|
+
| `MOCK_FORGET=1` | answer like a real agent to a forgotten session (empty `load`) |
|
|
131
|
+
|
|
132
|
+
**For an ordinary prompt the mock emits no tool calls.** So a visual rule about tool cards
|
|
133
|
+
(a "short card is not stretched to the column" CSS rule, say) cannot be measured from a
|
|
134
|
+
real dev turn — either drive a `[tools:N]` turn or inject a fixture row with the real class
|
|
135
|
+
names and assert the injection count before and after. See [02-verify.md](./02-verify.md).
|
|
136
|
+
|
|
137
|
+
## Front-end hot reload
|
|
138
|
+
|
|
139
|
+
The web bundle is built by Vite (`packages/web`, `vite.config.ts`). The server serves the
|
|
140
|
+
**built** bundle at `packages/web/dist`, not the sources, so:
|
|
141
|
+
|
|
142
|
+
- for standalone front-end work: `npm run dev -w @agentus/web` (Vite dev server, `host: true`
|
|
143
|
+
so it is reachable over the LAN);
|
|
144
|
+
- for anything the dev server or a sweep must serve: **`npm run build`** first. A sweep run
|
|
145
|
+
after editing web sources but before a build measures the previous bundle and reports
|
|
146
|
+
the old behaviour as if the fix did nothing.
|
|
147
|
+
|
|
148
|
+
## Isolating a slot's agent subprocess (HERMES_HOME)
|
|
149
|
+
|
|
150
|
+
A spawned agent CLI inherits the server's environment, so the server **always sets
|
|
151
|
+
`HERMES_HOME` explicitly**: the backend row's own home if it names one, otherwise the
|
|
152
|
+
default — the operator's real `~/.hermes`. That default is intentional for the live
|
|
153
|
+
instance (a slot drives the real agent). For a dev/test slot that should not touch the
|
|
154
|
+
operator's data, name a directory in that row's HERMES_HOME (no permission gate exists
|
|
155
|
+
any more).
|
|
156
|
+
|
|
157
|
+
One trap when a row points at a fresh home: an empty home has no `config.yaml`, no
|
|
158
|
+
`auth.json` and no `.env`, so `session/new` fails and the cockpit session goes `error`
|
|
159
|
+
with zero messages — which looks exactly like a broken adapter. Copy **all three** files
|
|
160
|
+
from a working home:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
mkdir -p /tmp/<home> && cp ~/.hermes/{config.yaml,auth.json,.env} /tmp/<home>/
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Also note a fresh home whose `hindsight/config.json` keeps the default profile name
|
|
167
|
+
`"hermes"` would attach to the production memory daemon; the spawn builder warns when it
|
|
168
|
+
would. Details: [`05-agent-driven.md`](./05-agent-driven.md) and
|
|
169
|
+
[`docs/refs/`](../refs/).
|
|
170
|
+
|
|
171
|
+
## Reading the dev logs
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
bash scripts/dev.sh logs # tail -f /tmp/agentus-dev.log
|
|
175
|
+
bash scripts/dev.sh status # :8901 up? pid, tree, /healthz
|
|
176
|
+
curl -s --noproxy '*' http://127.0.0.1:8901/healthz # {"ok":true,...} + backend list
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The dev log is a history of many launches (the `tsx watch` restarts write to it too), so
|
|
180
|
+
it is not by itself evidence about the *current* process — for "did it restart?", read the
|
|
181
|
+
served pid and the group leader's argv, not the log (see [03-release.md](./03-release.md)
|
|
182
|
+
and [04-recover.md](./04-recover.md)).
|
|
183
|
+
|
|
184
|
+
Cross-links: [02-verify.md](./02-verify.md) for the suites and sweeps ·
|
|
185
|
+
[03-release.md](./03-release.md) for `promote` · [04-recover.md](./04-recover.md) for
|
|
186
|
+
ports, stores and orphaned processes.
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# Verifying a change
|
|
2
|
+
|
|
3
|
+
This doc answers: how do I prove a change to Agentus actually works, and which check is the
|
|
4
|
+
right size for which change?
|
|
5
|
+
|
|
6
|
+
## Pick the cheapest evidence that can fail
|
|
7
|
+
|
|
8
|
+
Not every change needs a browser. Reach for the smallest check that can actually go red:
|
|
9
|
+
|
|
10
|
+
| change | evidence |
|
|
11
|
+
|---|---|
|
|
12
|
+
| a pure formatter / decision function | a `*.mts` unit sweep (runs under `npx tsx`, needs no build, no browser) |
|
|
13
|
+
| server logic / a REST contract | a `node scripts/*.mjs` suite against a scratch server |
|
|
14
|
+
| rendered UI behaviour or geometry | a CDP page sweep (`scripts/qa/*.mjs`) |
|
|
15
|
+
| a release | the promote log + the served bundle (see [03-release.md](./03-release.md)) |
|
|
16
|
+
| a doc / config edit | it is done when it is written — no verification run |
|
|
17
|
+
|
|
18
|
+
A change with no runtime effect is finished when it is written; an unasked-for sweep just
|
|
19
|
+
spends the round.
|
|
20
|
+
|
|
21
|
+
## Unit suites and smoke suites
|
|
22
|
+
|
|
23
|
+
### `npm test`
|
|
24
|
+
|
|
25
|
+
`package.json` maps `test` to `npm run test -ws --if-present`, i.e. "run each workspace's
|
|
26
|
+
own `test` script if it has one". As of this writing **no workspace defines a `test`
|
|
27
|
+
script**, so `npm test` runs nothing. The runnable suites are the scripted ones below —
|
|
28
|
+
run them directly. `TODO(verify)`: this placeholder is expected to gain real suites.
|
|
29
|
+
|
|
30
|
+
### Server / API smoke suites (each boots its own scratch server on a random port `8900+`)
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm run typecheck # 0 errors before any commit
|
|
34
|
+
npm run build # web bundle + shared types
|
|
35
|
+
node scripts/auth-smoke.mjs # the lock, both credentials (~48 assertions)
|
|
36
|
+
node scripts/backend-registry-smoke.mjs # backend registry, spawn env, health (~42)
|
|
37
|
+
node scripts/workspace-smoke.mjs # workspace, file API, shell, attachments (~85)
|
|
38
|
+
node scripts/voice-smoke.mjs # settings/theme guards, voice router (~47)
|
|
39
|
+
npm run tls-smoke # the TLS listener, real sockets + real certs
|
|
40
|
+
npm run package-smoke # pack + install into a throwaway HOME + boot
|
|
41
|
+
node scripts/smoke.mjs mock "hello" # end-to-end against the mock agent
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`backend-registry-smoke` is shaped as three layers so it passes on a machine without the
|
|
45
|
+
operator's CLI: always-on (registry CRUD, spawn env, session health via the in-repo mock),
|
|
46
|
+
local-only checks guarded by "does this command resolve / does that tree exist" and
|
|
47
|
+
announced as `skip`, and a summary line reporting the skip count. A suite that needs a
|
|
48
|
+
local binary must **skip**, never fail.
|
|
49
|
+
|
|
50
|
+
### `*.mts` unit sweeps (importable TS, no build, no browser)
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm run transcript-fold # tsx scripts/qa/transcript-fold.mts
|
|
54
|
+
npm run store-fold # tsx scripts/qa/store-fold.mts
|
|
55
|
+
npm run live-stream # tsx scripts/qa/live-stream.mts real server frames -> real store
|
|
56
|
+
npm run resume-tail
|
|
57
|
+
npm run rail-recent
|
|
58
|
+
npm run turn-pulse # the silence->amber decision, with injected `now`
|
|
59
|
+
npm run sentence-units
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Plus non-aliased ones run with `npx tsx scripts/qa/<name>.mts`: `accent-text.mts` (the
|
|
63
|
+
accent-as-text derivation, 61 checks), `presence-rule.mts` (`watchingNow`), `open-target.mts`,
|
|
64
|
+
`plan-v2-sweep.mts`, `asr-idle-wedge.mts`. These are the right home for a **time- or
|
|
65
|
+
boundary-driven decision**: extract it into a pure function with an injected `now` and
|
|
66
|
+
assert either side of the boundary (59s vs 60s, `2 < 10 < 100`), because a browser cannot
|
|
67
|
+
be made to wait for a 60-second window reliably.
|
|
68
|
+
|
|
69
|
+
## The CDP browser sweeps
|
|
70
|
+
|
|
71
|
+
`scripts/qa/*.mjs` drive a **real browser** over the Chrome DevTools Protocol and read
|
|
72
|
+
computed styles and geometry from the rendered page — never assert on internals. They run
|
|
73
|
+
by hand and are not in CI.
|
|
74
|
+
|
|
75
|
+
### Start the debug browser
|
|
76
|
+
|
|
77
|
+
Launch Edge or Chrome with remote debugging on `127.0.0.1:9222`:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
open -a "Microsoft Edge" --args --remote-debugging-port=9222
|
|
81
|
+
# or: /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Run a sweep
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
cd worktrees/agentus-dev
|
|
88
|
+
PORT=8901 node scripts/qa/layout-sweep.mjs
|
|
89
|
+
PORT=8901 W=390 H=844 node scripts/qa/popover-sweep.mjs
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Each sweep opens its **own tab** (`PUT http://127.0.0.1:9222/json/new?<BASE>`), builds its
|
|
93
|
+
own page state, and defaults to the dev port (`8901`, overridable with `PORT` / `BASE`).
|
|
94
|
+
Sweeps that post REST calls want the QA data dir: `AGENTUS_DATA=/tmp/agentus-qa-account`.
|
|
95
|
+
|
|
96
|
+
**Rebuild before you sweep a web change.** The dev server serves `packages/web/dist`, so
|
|
97
|
+
after editing web sources run `npm run build` (or `npm run build -w @agentus/web`) first, or
|
|
98
|
+
the sweep measures the previous bundle.
|
|
99
|
+
|
|
100
|
+
**Page-driving sweeps on `:8901` must log in themselves.** The operator's Edge profile
|
|
101
|
+
holds the cookie for the live port, not the dev one, so an unattended tab renders the login
|
|
102
|
+
wall and every step reads as a 401-induced "product failure". Standard opening:
|
|
103
|
+
`POST /api/auth/login` as `scratch` / `scratch-pass-1` (the `DEV_USER`/`DEV_PASS` defaults),
|
|
104
|
+
then reload. Assert the page is authenticated before measuring.
|
|
105
|
+
|
|
106
|
+
**Run one browser sweep at a time.** Two sweeps share this box's CPU and the one CDP
|
|
107
|
+
browser; the starved one fails in a cluster of unrelated places. Re-run a red suite alone
|
|
108
|
+
before believing it. And do not run a sweep in the same breath as a branch switch or any
|
|
109
|
+
server-source edit — the dev watcher restarts, the page's WebSocket drops mid-run, and one
|
|
110
|
+
assertion fails at random.
|
|
111
|
+
|
|
112
|
+
### The exit-code discipline (or node hangs)
|
|
113
|
+
|
|
114
|
+
End every sweep with:
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
ws.close();
|
|
118
|
+
process.exit(fail ? 1 : 0);
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
A sweep that only sets `process.exitCode` prints its summary and then **hangs forever on
|
|
122
|
+
the open CDP WebSocket** — and a shell chaining two sweeps with `;` never reaches the
|
|
123
|
+
second one. Measured: three sweeps sat "still running" 8–25 minutes after their last check,
|
|
124
|
+
each holding a tab, which then made the next sweep time out. When a suite "hangs after
|
|
125
|
+
passing", look for the missing exit before hunting a product bug.
|
|
126
|
+
|
|
127
|
+
### Traps worth knowing before you write one
|
|
128
|
+
|
|
129
|
+
- **`Runtime.evaluate` needs `awaitPromise: true`**, or a `fetch(...)` expression comes back
|
|
130
|
+
as a serialised pending Promise (`{}`) — truthy, so a step "succeeds" while nothing was
|
|
131
|
+
created.
|
|
132
|
+
- **`Runtime.evaluate` must return something serialisable.** `returnByValue: true` on an
|
|
133
|
+
expression that evaluates to a DOM node fails the whole call (`Object reference chain is
|
|
134
|
+
too long`). Wrap it: `Boolean(document.querySelector('.sidebar'))`.
|
|
135
|
+
- **Enter needs `text:'\r'`.** `Input.dispatchKeyEvent` with only `key:'Enter'` / `code`
|
|
136
|
+
delivers the event but the app's `keydown` handler never sees it. Add `text:'\r'`.
|
|
137
|
+
- **A trusted click lands where the viewport says.** If the target is scrolled out of the
|
|
138
|
+
viewport the event is silently swallowed — `scrollIntoView({block:'center'})`, re-measure,
|
|
139
|
+
assert `top >= 0 && bottom <= innerHeight`, then click.
|
|
140
|
+
- **The oracle comes from the page, not from node.** The cockpit is authenticated, so a
|
|
141
|
+
node-side `fetch('<BASE>/api/sessions')` returns 401 and every "UI value == server value"
|
|
142
|
+
comparison silently becomes "== undefined". Fetch from inside the page:
|
|
143
|
+
`ev("fetch('/api/sessions').then(r=>r.json())")`.
|
|
144
|
+
- **The app consumes `?session=` and strips it** (`state.ts #openTarget` → `replaceState`),
|
|
145
|
+
so a later `location.search` read is always null — keep the id in a variable. And
|
|
146
|
+
`Page.reload` wipes injected page helpers; re-inject after every reload.
|
|
147
|
+
- **Never wrap `window.WebSocket`.** Installed via `Page.addScriptToEvaluateOnNewDocument`
|
|
148
|
+
it replaces the constructor the app uses and the cockpit's own socket never connects.
|
|
149
|
+
Patch `WebSocket.prototype.send` after the app is up instead.
|
|
150
|
+
- **Token-injection cannot reach the WS handshake.** Injecting the machine token as an HTTP
|
|
151
|
+
header (via `Fetch.continueRequest` with `headers` as an **array** of `{name,value}`)
|
|
152
|
+
authenticates REST but not `/ws`, and the app's WS URL carries no token. That is enough
|
|
153
|
+
to prove REST-fed surfaces on the live instance but **not** WS-fed ones like the session
|
|
154
|
+
rail's live rows — say which you proved and which you did not.
|
|
155
|
+
|
|
156
|
+
### Why a visual rule needs an injected fixture row
|
|
157
|
+
|
|
158
|
+
The dev instance's mock emits **no tool calls for an ordinary prompt** (see
|
|
159
|
+
[01-dev-loop.md](./01-dev-loop.md)). A sweep whose subject is a `.tool-card` will therefore
|
|
160
|
+
never produce one and spends its time failing. When the assertion is about the CSS, inject a
|
|
161
|
+
probe with the **real class names** into the real container, measure it, remove it, and
|
|
162
|
+
assert the count is unchanged (`0 → 0`) so a leaked probe cannot pass:
|
|
163
|
+
|
|
164
|
+
```js
|
|
165
|
+
const mk = (html) => { const d = document.createElement('div'); d.innerHTML = html;
|
|
166
|
+
const el = d.firstElementChild; stream.appendChild(el); return el; };
|
|
167
|
+
const row = mk('<div class="msg"><div class="tool-card completed">…</div></div>');
|
|
168
|
+
const box = row.querySelector('.tool-card').getBoundingClientRect();
|
|
169
|
+
row.remove();
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Say in the check's label which rows are real and which are probes. An operator who judges a
|
|
173
|
+
layout by eye still needs a screenshot; a probe that only the script saw is not verified.
|
|
174
|
+
|
|
175
|
+
### Writing a new sweep from an existing one
|
|
176
|
+
|
|
177
|
+
Copy the closest existing sweep and keep its scaffolding (identical in every file):
|
|
178
|
+
|
|
179
|
+
1. the CDP client (`send` / `ev` / `until` helpers over a `WebSocket`),
|
|
180
|
+
2. the `check(ok, what, detail)` counter and the pass/fail summary,
|
|
181
|
+
3. `PUT /json/new?<BASE>` to open the tab, `sleep(~2600)` to let the app boot,
|
|
182
|
+
4. login-as-scratch if the surface needs a live socket,
|
|
183
|
+
5. build the page state you need (create a **fresh** mock session rather than adopting one),
|
|
184
|
+
and reset any preference you depend on (both `localStorage` and the server setting),
|
|
185
|
+
6. `ws.close(); process.exit(fail ? 1 : 0)`.
|
|
186
|
+
|
|
187
|
+
Prefer a new mock `[trigger]` prompt over editing a shared fixture constant — the mock is
|
|
188
|
+
spawned per session, so a new session picks up the edit with no server restart, and the
|
|
189
|
+
assertion then drives a real agent bubble instead of HTML the sweep injects.
|
|
190
|
+
|
|
191
|
+
## Table of existing sweeps
|
|
192
|
+
|
|
193
|
+
The QA directory is large; these are the ones that carry each surface, with what each covers.
|
|
194
|
+
|
|
195
|
+
| sweep (`scripts/qa/`) | covers |
|
|
196
|
+
|---|---|
|
|
197
|
+
| `layout-sweep.mjs` | rail indent ladder + folder icon + directory colour, accent-neutral split, three-pane widths with real pointer drags, chat column edge alignment, content-width rows (short tool card not stretched), phone 390px — 58 checks |
|
|
198
|
+
| `rail-archive-sweep.mjs` | the 已归档 section: built like 工作空间 (same row shape, folder icons, grouped by directory), a fold flag that survives a reload, and the two sections keeping separate fold state for the same path — 15 checks |
|
|
199
|
+
| `terminal-sweep.mjs` | the workspace panel's terminal is a real pty, not a text box: xterm mounted, a prompt only a tty prints, the shell's own echo of what was typed, an SGR colour surviving to the screen, and a resize that reaches the kernel (`stty size` + SIGWINCH) — 15 checks |
|
|
200
|
+
| `queue-sweep.mjs` | the message queue: enqueue while a turn runs, the row is not in the transcript, the three buttons' order, delete/edit, automatic ordered drain on `turn-end` — 28 checks |
|
|
201
|
+
| `table-sweep.mjs` | table headers sort three ways (numeric, not string), filter, full-screen reader with content rotation on a phone, a file chip in the reply, a relative image — real agent bubble via `[table]` |
|
|
202
|
+
| `rail-contrast-sweep.mjs` | sidebar text contrast computed per element against its actual background, in both palettes with the operator's blue and the built-in amber; the accent-vs-neutral chroma gap |
|
|
203
|
+
| `render-files-sweep.mjs` | local file links / images in a reply (all four forms), the workspace panel previewing and saving by type, the raw endpoint behind auth |
|
|
204
|
+
| `popover-sweep.mjs` | every popover's geometry at both viewports (a popover can pass every DOM text assertion while being off-screen) |
|
|
205
|
+
| `rail-sweep.mjs`, `rail-live-sweep.mjs`, `rail-recent-sweep.mjs` | the session rail: one title per row, row geometry, the ⋯ menu, inline rename landing in DOM *and* API, the phone bottom sheet, rail ordering |
|
|
206
|
+
| `message-actions-sweep.mjs` | per-message copy (text compared character-for-character), fork-on-tail |
|
|
207
|
+
| `permission-sweep.mjs` | a permission request reaches the operator's view (card + dialog), names the file, shows the diff, the answer reaches the agent, ≥32px tap targets |
|
|
208
|
+
| `voice-stop-sweep.mjs`, `call-sweep.mjs` | the speech layer and the full-screen call: deliberate stop vs real failure, the hero's real animation, phases, barge-in, controls on screen at 390×844 |
|
|
209
|
+
| `session-title-sweep.mjs`, `session-restart-sweep.mjs` | naming over API + browser; swapping an agent child under a live slot without it leaving the live list |
|
|
210
|
+
| `transcript-sweep.mjs`, `window-ui-sweep.mjs`, `turn-line-sweep.mjs`, `turn-pulse-sweep.mjs` | transcript rendering/paging/gestures, the usage strip, the turn timer and the silence→amber state |
|
|
211
|
+
| `config-sweep.mjs`, `dismiss-sweep.mjs`, `model-switch-sweep.mjs`, `plan-*-sweep.mjs` | the config/plan/model surfaces |
|
|
212
|
+
| `live-endpoints-check.mjs` | the file endpoints over the machine token; same source runs against live **and** dev |
|
|
213
|
+
| `think-cadence.mjs`, `render-lag-sweep.mjs`, `replay-window.mjs` | streaming cadence, render lag, replay windows |
|
|
214
|
+
|
|
215
|
+
Unit (`*.mts`) sweeps: `accent-text`, `transcript-fold`, `store-fold`, `live-stream`,
|
|
216
|
+
`resume-tail`, `rail-recent`, `turn-pulse`, `sentence-units`, `presence-rule`, `open-target`,
|
|
217
|
+
`plan-v2-sweep`, `asr-idle-wedge`. There are also `notify-smoke.mts` (the notify contract,
|
|
218
|
+
65 → 72 checks) and `notify-cockpit-e2e.mts` (a real agent turn end-to-end) for the phone
|
|
219
|
+
channel; see [`docs/android-notify-contract.md`](../android-notify-contract.md).
|
|
220
|
+
|
|
221
|
+
## Assertion discipline
|
|
222
|
+
|
|
223
|
+
Every sweep assertion must be able to prove **this run** caused the observed state:
|
|
224
|
+
|
|
225
|
+
- use a water mark (`createdAt >= runStart`) rather than "the newest row in that directory";
|
|
226
|
+
- establish `cwd` for any seeded fixture (`POST /api/sessions` refuses a `cwd` that is not a
|
|
227
|
+
directory) and assert the fixture exists before asserting behaviour;
|
|
228
|
+
- set what you depend on first — a leftover server preference from an earlier run turns
|
|
229
|
+
unrelated checks red;
|
|
230
|
+
- judge the effect on the far side ("the button cleared our state" is not "the answer
|
|
231
|
+
reached the agent" — read the agent's own tool output);
|
|
232
|
+
- when a fix is claimed, re-run the suite whose surface you touched: a change to the harness
|
|
233
|
+
invalidates every green it produced.
|
|
234
|
+
|
|
235
|
+
Cross-links: [01-dev-loop.md](./01-dev-loop.md) (ports, mock triggers) ·
|
|
236
|
+
[03-release.md](./03-release.md) (proving a release) ·
|
|
237
|
+
[04-recover.md](./04-recover.md) (stray tabs, leaked processes).
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Releasing to the live instance
|
|
2
|
+
|
|
3
|
+
This doc answers: how do I move a verified change from the dev tree onto the live instance
|
|
4
|
+
the operator uses, and how do I undo it?
|
|
5
|
+
|
|
6
|
+
## The rule that governs every release
|
|
7
|
+
|
|
8
|
+
`promote` **restarts the live server when server-side code changed**, and a restart SIGTERMs
|
|
9
|
+
every live `hermes acp` child — every session the operator has open, including his current
|
|
10
|
+
conversation. It is a release, not a hot reload. So:
|
|
11
|
+
|
|
12
|
+
- releases happen only after the sweeps are green and, unless the change is strictly
|
|
13
|
+
web-only, **only when the operator says so** in that round;
|
|
14
|
+
- a strictly web-only change may be published without asking first — but "web-only" is
|
|
15
|
+
decided by what `promote` prints, not by which files you remember editing (below).
|
|
16
|
+
|
|
17
|
+
## Branches
|
|
18
|
+
|
|
19
|
+
Work happens on a branch cut in the dev tree:
|
|
20
|
+
|
|
21
|
+
| branch | use |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `feat/<slug>` | a feature |
|
|
24
|
+
| `fix/<slug>` | a fix (e.g. `fix/rail-readable-tokens`) |
|
|
25
|
+
| `run/<slug>` | created **by `promote` on the live tree** — never check it out yourself |
|
|
26
|
+
| `main` | the integration line; **not** what live runs from |
|
|
27
|
+
|
|
28
|
+
`TODO(verify)`: the project also refers to a `fix/<base>/<desc>` convention; the branches
|
|
29
|
+
observed in the tree are `feat/<slug>` and `fix/<slug>`.
|
|
30
|
+
|
|
31
|
+
Keep the dev tree on its own branch. `promote` publishes the dev tree's **current HEAD**,
|
|
32
|
+
whatever branch that is — leaving it parked on a stale branch publishes a rollback with no
|
|
33
|
+
warning. `promote` guards against the common case (HEAD strictly behind `origin/main` → it
|
|
34
|
+
refuses; override with `DEV_PROMOTE_FORCE=1` for a deliberate rollback), but it will not
|
|
35
|
+
know that you meant to be on another branch.
|
|
36
|
+
|
|
37
|
+
## Committing
|
|
38
|
+
|
|
39
|
+
Commit and push the dev branch **before** promoting: `promote` refuses an uncommitted tree
|
|
40
|
+
(`the dev tree has uncommitted changes — commit them first`), and the pushed branch is what
|
|
41
|
+
makes the round recoverable if the release goes wrong.
|
|
42
|
+
|
|
43
|
+
**Write the commit message to a file and use `-F`.** A multi-line message with inline
|
|
44
|
+
backticks (`` `--text-dim` ``) or `$`/`!` triggers shell command substitution inside
|
|
45
|
+
`-m "…"`, and the substitutions silently vanish from the message:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
$EDITOR /tmp/msg.txt # backticks, $ and ! are literal here
|
|
49
|
+
git commit -F /tmp/msg.txt
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Promoting
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
bash scripts/dev.sh promote [<slug>]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
If `<slug>` is omitted, the branch name is used with `/` → `-`. What it does:
|
|
59
|
+
|
|
60
|
+
1. **Refuses** an uncommitted dev tree, and refuses HEAD-behind-`origin/main` (unless
|
|
61
|
+
`DEV_PROMOTE_FORCE=1`).
|
|
62
|
+
2. Runs **detached** — because the restart it may cause kills the agent that ran it — and
|
|
63
|
+
writes its log to `/tmp/agentus-qa-account/promote.log`. Follow it with:
|
|
64
|
+
```bash
|
|
65
|
+
bash scripts/dev.sh promote-log # follows the log (it hangs until the run ends)
|
|
66
|
+
```
|
|
67
|
+
or read `/tmp/agentus-qa-account/promote.log` directly.
|
|
68
|
+
3. Checks the **live** tree out onto `run/<slug>` at the dev tip and rebuilds the web bundle.
|
|
69
|
+
4. Decides whether the live process must restart, from the diff between the live tree's old
|
|
70
|
+
tip and the promoted HEAD:
|
|
71
|
+
|
|
72
|
+
| trigger | files |
|
|
73
|
+
|---|---|
|
|
74
|
+
| server graph | `packages/server/src/**`, `packages/shared/src/**` |
|
|
75
|
+
| start shape | `scripts/start.sh`, `package.json` |
|
|
76
|
+
| stale launcher | the live process is still running under `tsx watch` |
|
|
77
|
+
|
|
78
|
+
5. If a restart is needed, calls `scripts/relaunch.sh --port 8787 --tree <live>
|
|
79
|
+
--launcher <live launcher>`, which TERMs the process group, waits for the port to free,
|
|
80
|
+
starts the launcher, polls `/healthz`, and **asserts the pid changed** (a failure exits
|
|
81
|
+
non-zero, so "built fine, still serving old code" cannot pass).
|
|
82
|
+
6. Resumes every live session it captured **before** the restart
|
|
83
|
+
(`POST /api/sessions/<id>/resume`, expected HTTP 200 each).
|
|
84
|
+
|
|
85
|
+
### Web-only release (no restart)
|
|
86
|
+
|
|
87
|
+
When nothing in the trigger list moved, `promote` prints `no server-side change` and the
|
|
88
|
+
running server keeps its process. The new bundle is served on reload, so **no session is
|
|
89
|
+
interrupted at all** — the operator only hard-refreshes his tab. The three success criteria
|
|
90
|
+
are all required:
|
|
91
|
+
|
|
92
|
+
- `no server-side change` printed,
|
|
93
|
+
- the **asset hash moved** (`web asset: <before> -> <after>`; `promote` warns if web sources
|
|
94
|
+
changed but the hash did not),
|
|
95
|
+
- the **pid did NOT move** (`lsof -ti:8787` unchanged — a moved pid means it took the
|
|
96
|
+
restart branch and the release was not the kind it claimed to be).
|
|
97
|
+
|
|
98
|
+
`package.json` is a restart trigger, so **any** edit to it — even one new npm alias for a QA
|
|
99
|
+
sweep — turns a web-only round into a restarting one. When a web change rides on a dev
|
|
100
|
+
branch that also carries unreleased server work, cut the release from the live tip instead:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
cd worktrees/agentus-dev
|
|
104
|
+
git checkout -B feat/<slug>-live <live-tip> # live tip = git -C ../agentus rev-parse HEAD
|
|
105
|
+
git cherry-pick <web-commit>
|
|
106
|
+
git diff --name-only <live-tip> HEAD # MUST list only packages/web/** (+ scripts/qa/**)
|
|
107
|
+
env -u AGENTUS_DATA -u AGENTUS_BASE -u AGENTUS_PORT -u DEV_* bash scripts/dev.sh promote <slug>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Also drop `package.json` back to the live tip's version before promoting (`git checkout
|
|
111
|
+
<live-tip> -- package.json && git commit --amend --no-edit`) so the alias stays out of the
|
|
112
|
+
promoted tree, and run the sweep directly (`npx tsx scripts/qa/x.mts`) where the alias is
|
|
113
|
+
absent.
|
|
114
|
+
|
|
115
|
+
### Never fire the release from the dev environment
|
|
116
|
+
|
|
117
|
+
The shell that runs `promote` usually carries the QA instance's variables
|
|
118
|
+
(`AGENTUS_DATA=/tmp/agentus-qa-account`, `AGENTUS_BASE=…:8901`, `DEV_*`), and
|
|
119
|
+
`promote → relaunch.sh → launch.py` inherits them. The launcher now pins its own identity,
|
|
120
|
+
but wrap it anyway to keep the environment (and the log) honest:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
env -u AGENTUS_DATA -u AGENTUS_BASE -u AGENTUS_PORT \
|
|
124
|
+
-u DEV_LOG_PATH -u DEV_PROMOTE_DETACHED -u DEV_PROMOTE_SELF -u DEV_PROMOTE_SLUG \
|
|
125
|
+
bash scripts/dev.sh promote <slug>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Verifying what actually got deployed (with no password)
|
|
129
|
+
|
|
130
|
+
The promote log is one leg; prove the *served* instance independently. Everything below
|
|
131
|
+
needs no browser and no operator password.
|
|
132
|
+
|
|
133
|
+
**1. The machine token.** Each instance mints one at `<DATA_DIR>/auth.token` (0600). The
|
|
134
|
+
server accepts it as `Authorization: Bearer <token>` **or** `?token=<token>`. Read it inside
|
|
135
|
+
a script and never print it — and do not write the literal `Bearer <value>` shape into a
|
|
136
|
+
file, because the write-time secret masker rewrites it to `***` and breaks the syntax.
|
|
137
|
+
Assemble the header at runtime.
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
TOK=$(cat /tmp/agentus-qa-account/auth.token) # dev; live: worktrees/agentus/packages/server/.data/auth.token
|
|
141
|
+
curl -s --noproxy '*' -H "Authorization: Bearer $TOK" http://127.0.0.1:8901/api/sessions | head -c 300
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**2. The served asset hash.** `GET /api/version` is public and returns the served bundle
|
|
145
|
+
hash. `promote` echoes it; you can also read it directly:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
curl -s --noproxy '*' http://127.0.0.1:8787/api/version
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**3. Grep the served bundle for the new markers.** Fetch the real vite-named file
|
|
152
|
+
(`assets/index-<hash>.js`) and look for a string only the new code has:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
HASH=$(curl -s --noproxy '*' http://127.0.0.1:8787/api/version | ...)
|
|
156
|
+
curl -s --noproxy '*' "http://127.0.0.1:8787/assets/$HASH.js" | grep -c '<new-marker>'
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Two traps: the file is `index-<hash>.js`, so a check that fetches `/assets/<hash>.js`
|
|
160
|
+
misses and hits the SPA fallback (a body opening with `<!doctype` means the wrong URL, not a
|
|
161
|
+
broken build); and `echo "$B" | grep -q` under `set -o pipefail` reports a **hit as a miss**
|
|
162
|
+
(grep exits on first match, `echo` takes SIGPIPE) — use `grep -q "$lit" <<<"$B"` or
|
|
163
|
+
`grep -c` against a saved file.
|
|
164
|
+
|
|
165
|
+
**4. Rebuild locally and compare the emitted filename.** Stronger than the hash alone and
|
|
166
|
+
needs no credentials: rebuild the same source in the live tree and confirm the produced
|
|
167
|
+
asset filename is **identical** to the one the live instance is serving.
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
cd worktrees/agentus && npm run build
|
|
171
|
+
ls packages/web/dist/assets/ # index-<hash>.js must match what /api/version reports
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Same source in the two trees legitimately differs by ~5 KB (the dev build bakes absolute
|
|
175
|
+
source paths into its debug info) and therefore by hash — compare **strings/paths**, not
|
|
176
|
+
sizes, and expect the only differences to be tree-name fragments.
|
|
177
|
+
|
|
178
|
+
After a **restarting** release, the operator's open tab keeps the old bundle until a hard
|
|
179
|
+
refresh — say so in the hand-off. A web-only release needs only the hard refresh.
|
|
180
|
+
|
|
181
|
+
## Merging to main
|
|
182
|
+
|
|
183
|
+
Merging is a separate step from releasing, and is the standing convention for this repo once
|
|
184
|
+
a release is done. `main` is not what live runs from (`promote` puts live on `run/<slug>`),
|
|
185
|
+
so fast-forward the ref without checking `main` out anywhere:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
cd worktrees/agentus-dev
|
|
189
|
+
git fetch origin
|
|
190
|
+
git push origin <dev-branch>:main # fast-forward only
|
|
191
|
+
git branch -f main origin/<dev-branch>
|
|
192
|
+
git branch --set-upstream-to=origin/main main
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Judge the outcome by identity, not by "the push succeeded": `git ls-remote origin
|
|
196
|
+
refs/heads/main` must equal the commit live is actually serving.
|
|
197
|
+
|
|
198
|
+
## Rollback
|
|
199
|
+
|
|
200
|
+
The release path has no separate rollback mechanism — you revert the change and promote
|
|
201
|
+
again:
|
|
202
|
+
|
|
203
|
+
1. On the dev tree, revert or fix the offending commit.
|
|
204
|
+
2. Promote. If HEAD is now behind `origin/main` and you mean to roll back deliberately,
|
|
205
|
+
`promote` refuses; override with `DEV_PROMOTE_FORCE=1 bash scripts/dev.sh promote <slug>`.
|
|
206
|
+
If the release restarted live, the new (reverted) server starts and `promote` resumes the
|
|
207
|
+
sessions it captured before the restart.
|
|
208
|
+
|
|
209
|
+
The promote log is the record of what happened and where to look:
|
|
210
|
+
|
|
211
|
+
- **log file:** `/tmp/agentus-qa-account/promote.log`
|
|
212
|
+
- **follow:** `bash scripts/dev.sh promote-log`
|
|
213
|
+
|
|
214
|
+
Relay-side incident recovery (a live instance that came up on the wrong store, a slot that
|
|
215
|
+
did not resume) is in [04-recover.md](./04-recover.md).
|
|
216
|
+
|
|
217
|
+
Cross-links: [01-dev-loop.md](./01-dev-loop.md) · [02-verify.md](./02-verify.md) ·
|
|
218
|
+
[04-recover.md](./04-recover.md).
|