mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-09 03:39:51 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c02d7e7bb4 | ||
|
|
95bad721d2 | ||
|
|
1d4e5cf4ee | ||
|
|
2b6ea03d21 | ||
|
|
32d0b5de56 | ||
|
|
fdbfc30b1b | ||
|
|
dc472f9045 | ||
|
|
7a9952ff8c | ||
|
|
954404a21e | ||
|
|
a0370e972d | ||
|
|
730e850b34 | ||
|
|
622c8bdce8 | ||
|
|
1482aef986 | ||
|
|
3fc4bc7fa3 | ||
|
|
a64b71967c | ||
|
|
52dcff1f6c | ||
|
|
0f581591fb | ||
|
|
f2f3531881 | ||
|
|
9fbe3a15de | ||
|
|
1644e742ac | ||
|
|
333b25dc72 | ||
|
|
7d29771fff | ||
|
|
4801a224ff | ||
|
|
8008b163ec | ||
|
|
7b8d7c9b19 | ||
|
|
74120509a1 | ||
|
|
89cb7670b4 | ||
|
|
1783334f6d | ||
|
|
be32d5932e | ||
|
|
bb6381cf7c | ||
|
|
ab39ee9a38 | ||
|
|
d815a08145 | ||
|
|
3cb94ef1de | ||
|
|
fb981ac311 | ||
|
|
8f01d319af | ||
|
|
fe5a18bccf | ||
|
|
d022e43cf4 | ||
|
|
a57924e0e6 | ||
|
|
13c5f08750 | ||
|
|
31ec09a160 | ||
|
|
f3a99af1a2 | ||
|
|
2657bcedd7 | ||
|
|
5134ee22ba | ||
|
|
c7c9751adc | ||
|
|
8e4976d0d2 | ||
|
|
c732348be5 | ||
|
|
17fbc00da8 | ||
|
|
d81425892e | ||
|
|
5e2cd5f7a2 | ||
|
|
95c296c073 | ||
|
|
93de12ea90 | ||
|
|
2566e1c1d0 | ||
|
|
6e179bbc00 | ||
|
|
400d3313d5 | ||
|
|
1c19846093 | ||
|
|
734b4c2864 | ||
|
|
76dfe455f6 | ||
|
|
1cb3db2c73 | ||
|
|
1ae42ee8bf | ||
|
|
5270545432 | ||
|
|
a7a34ca1ed | ||
|
|
ec43d48af9 | ||
|
|
7a026128a0 | ||
|
|
ecaca01131 | ||
|
|
ff2b1a6d40 | ||
|
|
7284cda909 | ||
|
|
4ad5078224 | ||
|
|
d330fe81e8 | ||
|
|
a5faf034e4 | ||
|
|
f664c26b75 | ||
|
|
15115fdc04 | ||
|
|
7d4334b1de | ||
|
|
c265707236 | ||
|
|
06fb4f6f2c | ||
|
|
9512ab0502 | ||
|
|
3151a5d9eb | ||
|
|
1d3cabbdda | ||
|
|
b53849c044 | ||
|
|
714932c491 | ||
|
|
b4b93f4d69 | ||
|
|
06cb6b21c6 | ||
|
|
8cc36e9ee4 | ||
|
|
0aa7960215 | ||
|
|
2ea5748c31 | ||
|
|
cce70504ff | ||
|
|
cd5273363f | ||
|
|
abb154eff3 | ||
|
|
d8ad5d4292 | ||
|
|
971170daf0 | ||
|
|
1eb71fc0b8 | ||
|
|
a3a76a8f40 | ||
|
|
49108fea01 | ||
|
|
f3780c0656 | ||
|
|
615baadfac | ||
|
|
668e156609 | ||
|
|
483c23f3e2 | ||
|
|
1fb50d15d4 | ||
|
|
3981ad187e | ||
|
|
4e67cf660c | ||
|
|
919a428ee9 | ||
|
|
233aeb258a | ||
|
|
246f9651be | ||
|
|
ec2e13bd52 | ||
|
|
f1b91ff642 | ||
|
|
336a37bc73 | ||
|
|
4932f5d68e | ||
|
|
59cee0ecb2 | ||
|
|
b549426d73 | ||
|
|
bbe6250b97 | ||
|
|
ed08ff2376 | ||
|
|
210c32277d | ||
|
|
84557e5e1d | ||
|
|
6f86a5cd97 | ||
|
|
400477ba4a | ||
|
|
7aa1aac65c | ||
|
|
5720ca79a6 | ||
|
|
7f8872f2c7 | ||
|
|
c916c4bed6 | ||
|
|
a69275805d | ||
|
|
87f81553d9 | ||
|
|
5f53d31ec3 | ||
|
|
609e525436 | ||
|
|
9827de6a88 | ||
|
|
24db2b09e5 | ||
|
|
1aa8bcd0ad | ||
|
|
f1fe12a5d4 | ||
|
|
78cfd18cc5 | ||
|
|
6a2a1796d6 | ||
|
|
ab1fbeef34 | ||
|
|
6834a7fd00 | ||
|
|
63bc58e75d | ||
|
|
cf29fdaaa2 | ||
|
|
ab01fdae90 | ||
|
|
bddc46d914 | ||
|
|
70f8adcbcf | ||
|
|
0e7f3a3cb0 | ||
|
|
8d4ab3f205 | ||
|
|
c2d546a464 | ||
|
|
52d679e5d0 | ||
|
|
cb70bf49f9 | ||
|
|
f9051590cb | ||
|
|
a9637fd1f3 | ||
|
|
54413ecda9 | ||
|
|
6f338f5341 | ||
|
|
e4d3f8e1dc | ||
|
|
9d5932b898 | ||
|
|
e8d6a4e92b | ||
|
|
d2243da13b | ||
|
|
fce486c16d | ||
|
|
cca3baada1 | ||
|
|
2210ea3e74 | ||
|
|
2a70fa03ea | ||
|
|
0a4697bcd4 | ||
|
|
222429d59a | ||
|
|
6fb14e39e1 | ||
|
|
daa66e0ff9 | ||
|
|
96a46eeb1f | ||
|
|
be2a239441 | ||
|
|
ff531aceff | ||
|
|
4f48164bd8 | ||
|
|
9f3db4f7f5 | ||
|
|
299b4a06e5 | ||
|
|
af4e220e1d | ||
|
|
b218f3ccf1 | ||
|
|
effb163160 | ||
|
|
44635b4130 | ||
|
|
1a12230dce | ||
|
|
7bb892e1ba | ||
|
|
6097f4e633 | ||
|
|
72ddbe2101 | ||
|
|
a34e99cd5b | ||
|
|
5bdda4f8be | ||
|
|
66796c21dd | ||
|
|
fdbe1e07d6 | ||
|
|
b678e67130 | ||
|
|
babf6ecc9d | ||
|
|
3f98dead6f | ||
|
|
74e77f0e0c | ||
|
|
7645e54b37 | ||
|
|
1c4cd15e31 | ||
|
|
bc68cccc31 | ||
|
|
92e95494c4 | ||
|
|
0016661e96 | ||
|
|
06b1fc0dea | ||
|
|
de0a3b9223 | ||
|
|
ee39768e8e | ||
|
|
a3780a66c9 | ||
|
|
42711ae95d | ||
|
|
cd071b03b3 | ||
|
|
ca4f02ead9 | ||
|
|
ab4473b46c | ||
|
|
de8e0b2d93 | ||
|
|
33e01bd472 | ||
|
|
d15ca1fe05 | ||
|
|
3b1205f3be | ||
|
|
e40214f0f8 | ||
|
|
90b177242f | ||
|
|
a536df3130 | ||
|
|
cff08c7e93 | ||
|
|
fe5b23e088 | ||
|
|
934f3c9d63 | ||
|
|
7a0ae0fa81 | ||
|
|
05ca19c11f | ||
|
|
3e32b0a80c | ||
|
|
1c94719b11 | ||
|
|
bf28cc2f7e | ||
|
|
55bdcbb73c | ||
|
|
1a3b313f0d | ||
|
|
dd9820eb2d | ||
|
|
48c0267615 | ||
|
|
629d061325 | ||
|
|
9f86df49e2 | ||
|
|
4ac0d20e35 | ||
|
|
194a1ebb4c | ||
|
|
fd4fb309b5 | ||
|
|
d8d8ee91c8 | ||
|
|
ab822eef53 | ||
|
|
a5132dad87 | ||
|
|
c7b74fa08f | ||
|
|
a8401f6181 | ||
|
|
e5de3abd12 | ||
|
|
5c87145e54 | ||
|
|
bdf4cf6844 | ||
|
|
40d07241ab | ||
|
|
de2dbd4f3a | ||
|
|
6af2754824 | ||
|
|
2f8f26952b | ||
|
|
2673e30ec6 | ||
|
|
eb4acf2c1d | ||
|
|
53e9409306 | ||
|
|
63ee206e93 | ||
|
|
fcfc7dc93c | ||
|
|
ab01331f50 | ||
|
|
6b7043a04d | ||
|
|
315745a167 | ||
|
|
578bd5c57d | ||
|
|
cdce3a6864 | ||
|
|
c7b6df192f | ||
|
|
4e277d7744 | ||
|
|
0a9fb436cd | ||
|
|
e70694f660 | ||
|
|
66785c8ee7 | ||
|
|
fad10a9311 | ||
|
|
3ad1df6339 | ||
|
|
17812d5d3b | ||
|
|
3071234eca | ||
|
|
de62cc2593 | ||
|
|
d7af73706a | ||
|
|
5a78aedc02 | ||
|
|
56b90d48c3 | ||
|
|
2e4067f41d | ||
|
|
e9b1251591 | ||
|
|
878a18296d | ||
|
|
437eaafbb5 | ||
|
|
d50f7b9aa5 | ||
|
|
388ea09e5b | ||
|
|
2def07e878 | ||
|
|
462550807a | ||
|
|
6a7e38fcf3 | ||
|
|
f1e4192975 | ||
|
|
aefe4b4a5d | ||
|
|
501f77766b | ||
|
|
e49bae30eb | ||
|
|
910d9786b5 | ||
|
|
5f3844fa39 | ||
|
|
9e6faddc32 | ||
|
|
6f90d644b4 | ||
|
|
6c7cc0d3f6 | ||
|
|
f657e53c3a | ||
|
|
f09dc5f6c5 | ||
|
|
b2d121c4d4 | ||
|
|
6696649095 | ||
|
|
331e9321e9 | ||
|
|
0843d4bc30 | ||
|
|
186fc4b221 | ||
|
|
c56934f427 | ||
|
|
69df0f1c99 | ||
|
|
2b6224c2be | ||
|
|
cee9a8481a | ||
|
|
69c1bffe7b | ||
|
|
5109053de5 | ||
|
|
6c52cbfef4 | ||
|
|
6aa68d89b2 | ||
|
|
004e36f2fa | ||
|
|
d8ef8dd18a | ||
|
|
89e25027f8 | ||
|
|
4a333f294b | ||
|
|
9efaa249f1 | ||
|
|
fbddd1155e | ||
|
|
a5d5fc3eae | ||
|
|
bad82b3a7b | ||
|
|
efa6f41575 | ||
|
|
bc9c9025b8 | ||
|
|
daccae566f | ||
|
|
9010f4563a | ||
|
|
44b7a3d872 | ||
|
|
8411d0bb0d | ||
|
|
3cafd3927d | ||
|
|
3b7ffa7d3b | ||
|
|
3d8d7e83c8 | ||
|
|
09f8cc8682 | ||
|
|
638c541151 | ||
|
|
c377fa935c | ||
|
|
0cbeca0c6d | ||
|
|
18bbd7f08f | ||
|
|
14a91700ca | ||
|
|
55385068f1 | ||
|
|
69479d9a39 | ||
|
|
85d0844303 | ||
|
|
a4071441c3 | ||
|
|
31b9d63307 | ||
|
|
beb3cc453e | ||
|
|
a41d1091be | ||
|
|
ca5f36d5b7 | ||
|
|
74cf893c00 | ||
|
|
37e2d5fda7 | ||
|
|
0d7e6eca65 | ||
|
|
233ec8128c | ||
|
|
43a0ace5d0 | ||
|
|
848b0327be | ||
|
|
2500ee028e | ||
|
|
105ed7e34d | ||
|
|
c44507fd11 | ||
|
|
30cf604d4c | ||
|
|
e4619cf13a | ||
|
|
a8adfe257a | ||
|
|
8ca8e43753 | ||
|
|
8b074614dc | ||
|
|
d10d277ebf | ||
|
|
9632c2b9bf | ||
|
|
b8758ed0ee | ||
|
|
fd22057318 | ||
|
|
138618637c | ||
|
|
4eda5c5ef4 | ||
|
|
d8881d867e | ||
|
|
697f8e60f8 | ||
|
|
3023de8112 | ||
|
|
bc2c899b45 | ||
|
|
e74ebbc91b | ||
|
|
e4a03ae48b | ||
|
|
b9e64c004a | ||
|
|
da581a9471 | ||
|
|
c10b8f8f81 | ||
|
|
81262deb52 | ||
|
|
fccb65a211 | ||
|
|
ae4ebb53a7 | ||
|
|
93a36c5f57 | ||
|
|
0aefd99a0e | ||
|
|
db8c54208a | ||
|
|
d4088d042e | ||
|
|
48f3c9dd25 | ||
|
|
b60725bccd | ||
|
|
36f6cc199b | ||
|
|
357d490ef3 | ||
|
|
e71ac524dc | ||
|
|
0dc01f188e | ||
|
|
9e9b3e020b | ||
|
|
68eb886041 | ||
|
|
6e0d9f8943 | ||
|
|
099edb33e1 | ||
|
|
39ab0f5508 | ||
|
|
9199ed7954 | ||
|
|
b9330f635b | ||
|
|
4fcd9764eb | ||
|
|
9abb516e6a | ||
|
|
428529d883 | ||
|
|
3f7e7e1ce5 | ||
|
|
1663286d33 | ||
|
|
496aef5a28 | ||
|
|
7a29f192d0 | ||
|
|
94ed046c72 | ||
|
|
d6521cb89d | ||
|
|
8c1479faf7 | ||
|
|
ef2b03272a | ||
|
|
2f4722cd03 | ||
|
|
8c10e9632e | ||
|
|
971da63cc9 | ||
|
|
778aa94ae9 | ||
|
|
fa4cd5034c | ||
|
|
1cefe8e07b | ||
|
|
e653db411e | ||
|
|
d6981151ef | ||
|
|
672d724dff | ||
|
|
1ba4540451 | ||
|
|
72b1dc7069 | ||
|
|
a6c1487ffa | ||
|
|
d16fadcbb0 | ||
|
|
4ab7ee68a3 | ||
|
|
464b810fec | ||
|
|
fa867b5ae6 | ||
|
|
9c16026744 | ||
|
|
8eef407062 | ||
|
|
70998e8ea8 | ||
|
|
aa4f1659ee | ||
|
|
b17c8857c4 | ||
|
|
0f564a2905 | ||
|
|
ec47323f5e | ||
|
|
959394a4ca | ||
|
|
76006c2ece | ||
|
|
fe0a000d51 | ||
|
|
b56ce63b13 | ||
|
|
0e4bfaa418 | ||
|
|
4b0544a127 | ||
|
|
7ac72c37d0 | ||
|
|
e6f854a3e6 | ||
|
|
dd0385e310 | ||
|
|
4771f9adfa | ||
|
|
83df1da3d4 | ||
|
|
ebf106100b |
No files matched your search
@@ -1,75 +0,0 @@
|
||||
---
|
||||
name: developing-openrig
|
||||
description: "Use when changing OpenRig's own source in a clone of the openrig repository: finding which package or file owns a behaviour, choosing which tests to run, testing a change without disturbing the OpenRig daemon your own session runs on, judging whether a diff touches a high-risk area, or preparing a pull request. Not for operating rigs (openrig-skills) or designing rig topologies."
|
||||
---
|
||||
|
||||
# Developing OpenRig
|
||||
|
||||
This skill is a map to the other maps. Each one is incomplete and may be stale: where a map and the code disagree, the
|
||||
code is right, and fixing the map is a welcome contribution.
|
||||
|
||||
## First: which OpenRig are you in?
|
||||
|
||||
If you are an agent running inside OpenRig, two different OpenRigs are in play:
|
||||
|
||||
- **The installed one runs your session.** `rig --version` and `rig daemon status` describe it.
|
||||
- **The checkout is what you're changing.** `git rev-parse HEAD` describes it.
|
||||
|
||||
Restarting the installed daemon does not run your checkout, and stopping it stops the agents running on it, including
|
||||
you. Test your change with the repo's own harnesses, which start a private daemon and tmux server (see "Test it" below),
|
||||
not by restarting the daemon you live in.
|
||||
|
||||
## Find your way
|
||||
|
||||
| You want to know | Read | How much to trust it |
|
||||
|---|---|---|
|
||||
| What OpenRig can already do, before you decide something is missing | `rig context get onboarding-width/public-what-you-can-do.md`; in this checkout, `packages/daemon/context-packs-src/onboarding-width/public-what-you-can-do.md` (the installed copy can differ) | The capability map; check it before proposing a new command or calling a gap a defect |
|
||||
| Whether a `rig` command already exists | `rig --help`: the full command list, before you decide a command is missing | The running binary; read the whole list, not only the command you expected |
|
||||
| Whether a problem is already known | The "Known problems to compare against" section of [`docs/reference/help.md`](../../../docs/reference/help.md) | Lists known issues by symptom; check an issue's current status before relying on it |
|
||||
| Whether a feature is already planned | [`ROADMAP.md`](../../../ROADMAP.md) | Check it before proposing a feature |
|
||||
| How the packages fit, the request path, where to add a command, route, migration, adapter, skill, context pack or scenario | [`ARCHITECTURE.md`](../../../ARCHITECTURE.md) | Checked against the commit it names; counts come with the command to refresh them |
|
||||
| Whether your change touches a high-risk area, what depends on it, what broke there before | [`docs/as-built/arteries.md`](../../../docs/as-built/arteries.md) | Incomplete by design: absence from it doesn't make a change safe |
|
||||
| What to run before you push, and what each layer can and can't prove | [`docs/as-built/test-layers.md`](../../../docs/as-built/test-layers.md) | Checked against the commit it names |
|
||||
| The exact behaviour of a `rig` command | `rig <command> --help` on the running binary, then `packages/cli/src/commands/` | The binary and the code win over any document |
|
||||
| A subsystem in depth | [`docs/as-built/`](../../../docs/as-built/README.md) | Each module names the commit it was last checked against in `last-verified-against-source`; compare that marker with `git log` before relying on it |
|
||||
| Rig spec, agent spec and other user-facing reference | [`docs/reference/`](../../../docs/reference/) | Ships to every user; if you find drift, fix it there |
|
||||
| Which shipped skill covers a task | [`skills/_canonical/core/openrig-skills/SKILL.md`](../../../skills/_canonical/core/openrig-skills/SKILL.md) | The index of the skills that ship with OpenRig |
|
||||
| How to open a good pull request, and what review looks like | [`CONTRIBUTING.md`](../../../CONTRIBUTING.md) | Current |
|
||||
|
||||
If none of these answers your question, search the code before inventing an answer. A missing map is worth an issue.
|
||||
|
||||
## Before you change an artery
|
||||
|
||||
If your diff touches anything in `arteries.md` (message delivery, launch and resume, the queue, rig identity, skill
|
||||
projection, process observation, migrations, restore):
|
||||
|
||||
1. Read the artery row: what depends on it, and the past regressions listed there.
|
||||
2. Describe the downstream effect in your pull request, not just the diff.
|
||||
3. Add or extend a stub-agent scenario, or say precisely why the stub can't exercise it and what you ran instead.
|
||||
|
||||
## Test it
|
||||
|
||||
The short version is `npm run build`, `npm run lint`, `npm test`. `test-layers.md` has the full ladder, including the
|
||||
stub-agent scenarios, which run the real CLI, daemon, tmux and SQLite with scripted agents and no model cost.
|
||||
|
||||
- The scenario runner starts its own daemon and tmux server and drops your session's `TMUX` and daemon variables, so
|
||||
it won't touch the daemon your session runs on.
|
||||
- A stub proves OpenRig's own plumbing, not how Claude Code or Codex behave. Say which you exercised.
|
||||
|
||||
## How we judge changes
|
||||
|
||||
OpenRig is a coordination layer for a trusted environment: it should help agents and people keep work flowing.
|
||||
|
||||
- A change that removes friction, or makes state more truthful, is usually welcome.
|
||||
- OpenRig runs on agents working in context with tools. Before adding code for a behaviour, ask whether an agent with
|
||||
the right instructions and today's commands could do it. A skill or a page of guidance is often the better change.
|
||||
- Where to look: the capability map above, and the `agent-operated-workflows` skill, which ships in the
|
||||
`openrig-core` plugin (`packages/daemon/assets/plugins/openrig-core/skills/agent-operated-workflows/SKILL.md`).
|
||||
- A change that adds a refusal, a prompt or a required step needs the concrete case in CONTRIBUTING.md: who is
|
||||
harmed, how, and what it costs everyone else.
|
||||
- One concern per pull request. Keep the diff reviewable in one sitting.
|
||||
|
||||
## When the map is wrong
|
||||
|
||||
Fix it in the same pull request, or open an issue. Update a document's `last-verified-against-source` marker only when
|
||||
you have actually checked it against that commit.
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"name": "openrig-app",
|
||||
"description": "The OpenRig plugin: work with your OpenRig team from this app.",
|
||||
"owner": {
|
||||
"name": "Mike Schwarz",
|
||||
"url": "https://github.com/mvschwarz"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "openrig",
|
||||
"source": "./packages/daemon/assets/plugins/openrig",
|
||||
"description": "Work with your OpenRig team from your coding agent: hand it work, follow that work, answer its questions and read what it returns."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
---
|
||||
name: developing-openrig
|
||||
description: "Use when changing OpenRig's own source in a clone of the openrig repository: finding which package or file owns a behaviour, choosing which tests to run, testing a change without disturbing the OpenRig daemon your own session runs on, judging whether a diff touches a high-risk area, or preparing a pull request. Not for operating rigs (openrig-skills) or designing rig topologies."
|
||||
---
|
||||
|
||||
# Developing OpenRig
|
||||
|
||||
This skill is a map to the other maps. Each one is incomplete and may be stale: where a map and the code disagree, the
|
||||
code is right, and fixing the map is a welcome contribution.
|
||||
|
||||
## First: which OpenRig are you in?
|
||||
|
||||
If you are an agent running inside OpenRig, two different OpenRigs are in play:
|
||||
|
||||
- **The installed one runs your session.** `rig --version` and `rig daemon status` describe it.
|
||||
- **The checkout is what you're changing.** `git rev-parse HEAD` describes it.
|
||||
|
||||
Restarting the installed daemon does not run your checkout, and stopping it stops the agents running on it, including
|
||||
you. Test your change with the repo's own harnesses, which start a private daemon and tmux server (see "Test it" below),
|
||||
not by restarting the daemon you live in.
|
||||
|
||||
## Find your way
|
||||
|
||||
| You want to know | Read | How much to trust it |
|
||||
|---|---|---|
|
||||
| What OpenRig can already do, before you decide something is missing | `rig context get onboarding-width/public-what-you-can-do.md`; in this checkout, `packages/daemon/context-packs-src/onboarding-width/public-what-you-can-do.md` (the installed copy can differ) | The capability map; check it before proposing a new command or calling a gap a defect |
|
||||
| Whether a `rig` command already exists | `rig --help`: the full command list, before you decide a command is missing | The running binary; read the whole list, not only the command you expected |
|
||||
| Whether a problem is already known | The "Known problems to compare against" section of [`docs/reference/help.md`](../../../docs/reference/help.md) | Lists known issues by symptom; check an issue's current status before relying on it |
|
||||
| Whether a feature is already planned | [`ROADMAP.md`](../../../ROADMAP.md) | Check it before proposing a feature |
|
||||
| How the packages fit, the request path, where to add a command, route, migration, adapter, skill, context pack or scenario | [`ARCHITECTURE.md`](../../../ARCHITECTURE.md) | Checked against the commit it names; counts come with the command to refresh them |
|
||||
| Whether your change touches a high-risk area, what depends on it, what broke there before | [`docs/as-built/arteries.md`](../../../docs/as-built/arteries.md) | Incomplete by design: absence from it doesn't make a change safe |
|
||||
| What to run before you push, and what each layer can and can't prove | [`docs/as-built/test-layers.md`](../../../docs/as-built/test-layers.md) | Checked against the commit it names |
|
||||
| The exact behaviour of a `rig` command | `rig <command> --help` on the running binary, then `packages/cli/src/commands/` | The binary and the code win over any document |
|
||||
| A subsystem in depth | [`docs/as-built/`](../../../docs/as-built/README.md) | Each module names the commit it was last checked against in `last-verified-against-source`; compare that marker with `git log` before relying on it |
|
||||
| Rig spec, agent spec and other user-facing reference | [`docs/reference/`](../../../docs/reference/) | Ships to every user; if you find drift, fix it there |
|
||||
| Which shipped skill covers a task | [`skills/_canonical/core/openrig-skills/SKILL.md`](../../../skills/_canonical/core/openrig-skills/SKILL.md) | The index of the skills that ship with OpenRig |
|
||||
| How to open a good pull request, and what review looks like | [`CONTRIBUTING.md`](../../../CONTRIBUTING.md) | Current |
|
||||
|
||||
If none of these answers your question, search the code before inventing an answer. A missing map is worth an issue.
|
||||
|
||||
## Before you change an artery
|
||||
|
||||
If your diff touches anything in `arteries.md` (message delivery, launch and resume, the queue, rig identity, skill
|
||||
projection, process observation, migrations, restore):
|
||||
|
||||
1. Read the artery row: what depends on it, and the past regressions listed there.
|
||||
2. Describe the downstream effect in your pull request, not just the diff.
|
||||
3. Add or extend a stub-agent scenario, or say precisely why the stub can't exercise it and what you ran instead.
|
||||
|
||||
## Test it
|
||||
|
||||
The short version is `npm run build`, `npm run lint`, `npm test`. `test-layers.md` has the full ladder, including the
|
||||
stub-agent scenarios, which run the real CLI, daemon, tmux and SQLite with scripted agents and no model cost.
|
||||
|
||||
- The scenario runner starts its own daemon and tmux server and drops your session's `TMUX` and daemon variables, so
|
||||
it won't touch the daemon your session runs on.
|
||||
- A stub proves OpenRig's own plumbing, not how Claude Code or Codex behave. Say which you exercised.
|
||||
|
||||
## How we judge changes
|
||||
|
||||
OpenRig is a coordination layer for a trusted environment: it should help agents and people keep work flowing.
|
||||
|
||||
- A change that removes friction, or makes state more truthful, is usually welcome.
|
||||
- OpenRig runs on agents working in context with tools. Before adding code for a behaviour, ask whether an agent with
|
||||
the right instructions and today's commands could do it. A skill or a page of guidance is often the better change.
|
||||
- Where to look: the capability map above, and the `agent-operated-workflows` skill, which ships in the
|
||||
`openrig-core` plugin (`packages/daemon/assets/plugins/openrig-core/skills/agent-operated-workflows/SKILL.md`).
|
||||
- A change that adds a refusal, a prompt or a required step needs the concrete case in CONTRIBUTING.md: who is
|
||||
harmed, how, and what it costs everyone else.
|
||||
- One concern per pull request. Keep the diff reviewable in one sitting.
|
||||
|
||||
## When the map is wrong
|
||||
|
||||
Fix it in the same pull request, or open an issue. Update a document's `last-verified-against-source` marker only when
|
||||
you have actually checked it against that commit.
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"name": "openrig-app",
|
||||
"description": "The OpenRig plugin: work with your OpenRig team from this app.",
|
||||
"owner": {
|
||||
"name": "Mike Schwarz",
|
||||
"url": "https://github.com/mvschwarz"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "openrig",
|
||||
"source": "./packages/daemon/assets/plugins/openrig",
|
||||
"description": "Work with your OpenRig team from your coding agent: hand it work, follow that work, answer its questions and read what it returns."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
github: mvschwarz
|
||||
@@ -1,60 +0,0 @@
|
||||
name: Bug report
|
||||
description: Something OpenRig does is wrong, or a command fails
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks. Include a small reproduction and the relevant versions.
|
||||
This report will be public. Remove API keys, tokens, private prompts, personal details and private paths from commands, logs and configuration.
|
||||
For a suspected vulnerability, follow our [security policy](https://github.com/mvschwarz/openrig/security/policy) instead of posting details here.
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: OpenRig version
|
||||
description: Output of `rig --version`
|
||||
placeholder: 0.5.14 (2edc1b7a)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: platform
|
||||
attributes:
|
||||
label: OS, Node and tmux versions
|
||||
placeholder: macOS 15.6, Node 22.22.1, tmux 3.5a
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: harness
|
||||
attributes:
|
||||
label: Harnesses involved
|
||||
multiple: true
|
||||
options:
|
||||
- None / not sure
|
||||
- Claude Code
|
||||
- Codex
|
||||
- Pi / Oh My Pi
|
||||
- Other (say which below)
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: repro
|
||||
attributes:
|
||||
label: What you ran and what happened
|
||||
description: The command(s), relevant redacted output, and what you expected instead. Paste text where you can; screenshots are useful for visual issues.
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: state
|
||||
attributes:
|
||||
label: Instance state that might matter
|
||||
description: Fresh install or upgrade? Include only relevant redacted lines from status output or your rig spec; do not upload full transcripts, credentials or your instance directory.
|
||||
- type: checkboxes
|
||||
id: checks
|
||||
attributes:
|
||||
label: Checks
|
||||
options:
|
||||
- label: I searched existing issues and Discussions for this.
|
||||
required: true
|
||||
- label: I am on the latest published version, or I said which version above.
|
||||
required: true
|
||||
@@ -1,14 +0,0 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Ask a question
|
||||
url: https://github.com/mvschwarz/openrig/discussions/categories/q-a
|
||||
about: Setup, usage and "is this expected" questions go to Discussions Q&A.
|
||||
- name: Share an idea or a setup
|
||||
url: https://github.com/mvschwarz/openrig/discussions
|
||||
about: Ideas and Show and tell live in Discussions.
|
||||
- name: Documentation
|
||||
url: https://openrig.dev/docs
|
||||
about: Getting started, CLI reference, configuration and troubleshooting.
|
||||
- name: Security report
|
||||
url: https://github.com/mvschwarz/openrig/security/policy
|
||||
about: Read how to arrange a private report. Do not include vulnerability details in public issues.
|
||||
@@ -1,32 +0,0 @@
|
||||
name: Feature request
|
||||
description: Something you want OpenRig to do that it does not
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
For open-ended ideas or "how should this work" questions, [Discussions › Ideas](https://github.com/mvschwarz/openrig/discussions/categories/ideas) is the better place. Use this form when you can name the outcome you want.
|
||||
This request will be public; use examples without credentials, private prompts or personal details.
|
||||
- type: textarea
|
||||
id: outcome
|
||||
attributes:
|
||||
label: What are you trying to do, and what stops you today?
|
||||
description: Describe the situation, not the solution. One real example beats a general case.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: proposal
|
||||
attributes:
|
||||
label: What would you like OpenRig to do?
|
||||
description: A command, a setting, a behaviour. Rough is fine.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: workaround
|
||||
attributes:
|
||||
label: How do you handle it now?
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: OpenRig version
|
||||
placeholder: 0.5.14
|
||||
@@ -1,21 +0,0 @@
|
||||
<!-- Thanks for the PR. Three short answers are all a reviewer needs. -->
|
||||
|
||||
## What a user gets
|
||||
|
||||
<!-- One or two sentences: the behaviour before, the behaviour after. Link the issue if there is one. -->
|
||||
|
||||
## How you verified it
|
||||
|
||||
<!-- What you actually ran and saw, and which revision/local changes you tested. The documented checks are `npm run build`, `npm test`, and `npm run lint`; say what you could not run. Redact credentials and private information from logs or screenshots. -->
|
||||
|
||||
## Anything you were unsure about
|
||||
|
||||
<!-- Design choices, edge cases you did not cover, places a reviewer should look hardest. Empty is a fine answer. -->
|
||||
|
||||
## If this is security-related
|
||||
|
||||
<!-- Exploitable? Report it privately first (SECURITY.md). Otherwise: how does this go wrong for someone using OpenRig as designed (your own machine or a trusted private network), with evidence? Who causes it, and how do they reach the install? What does the change cost everyone else? See CONTRIBUTING.md. -->
|
||||
|
||||
- [ ] One concern per PR; no version bump; no `CHANGELOG.md` edit
|
||||
- [ ] Tests added or updated where the change is testable
|
||||
- [ ] I listed the checks I ran, their results, and any checks I could not run
|
||||
@@ -1,20 +0,0 @@
|
||||
# Getting help with OpenRig
|
||||
|
||||
- **Docs:** https://openrig.dev/docs — getting started, CLI reference, configuration,
|
||||
troubleshooting. An index linking to agent-friendly documentation is at
|
||||
https://openrig.dev/llms.txt.
|
||||
- **Agents helping a user:** read [docs/reference/help.md](../docs/reference/help.md), or run `rig context get help`.
|
||||
The same guide is at https://www.openrig.dev/help/agents.
|
||||
- **Email:** hello@openrig.dev, which needs no GitHub account.
|
||||
- **Questions:** [Discussions › Q&A](https://github.com/mvschwarz/openrig/discussions/categories/q-a).
|
||||
- **Bugs:** [open an issue](https://github.com/mvschwarz/openrig/issues/new/choose) with the bug
|
||||
template. Include `rig --version`, your OS, Node and tmux versions, the harnesses involved, and
|
||||
relevant redacted command output.
|
||||
Reports are public: remove credentials, private prompts and personal information.
|
||||
- **Ideas and setups:** [Ideas](https://github.com/mvschwarz/openrig/discussions/categories/ideas)
|
||||
and [Show and tell](https://github.com/mvschwarz/openrig/discussions/categories/show-and-tell).
|
||||
- **Videos:** https://www.youtube.com/@openrig
|
||||
- **Security:** see [SECURITY.md](../SECURITY.md); please report privately.
|
||||
|
||||
We aim to acknowledge issues and pull requests within one day. A substantive PR review decision
|
||||
within seven days is a target, not a guaranteed deadline. Details in [CONTRIBUTING.md](../CONTRIBUTING.md).
|
||||
@@ -1,33 +0,0 @@
|
||||
name: Portability report
|
||||
|
||||
# Lists lines a pull request adds that contain values tied to one machine, network or account
|
||||
# (credentials, home paths, temporary paths, network addresses, email addresses). Findings never
|
||||
# fail the job; the author reads the report and decides what to change. An operational error (for
|
||||
# example a git failure) does fail it. The repository is public, so this summary lists file and
|
||||
# line only; authors see the matched text when they run the report locally or on push.
|
||||
# See scripts/portability-report.mjs.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
portability-report:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Report added lines with machine-specific values
|
||||
run: >
|
||||
node scripts/portability-report.mjs
|
||||
--from ${{ github.event.pull_request.base.sha }}
|
||||
--to ${{ github.event.pull_request.head.sha }}
|
||||
--locations-only
|
||||
--out "$GITHUB_STEP_SUMMARY"
|
||||
@@ -1,177 +0,0 @@
|
||||
name: Tests
|
||||
|
||||
# Builds, type-checks and runs every test suite on a clean GitHub runner, and records the file
|
||||
# list each published package would contain. Each suite is its own job so one failure does not
|
||||
# hide the others. No provider credentials or operator configuration are supplied.
|
||||
# Package suites and the Linux seat-environment check deny external networking;
|
||||
# scenario runtime has no external route.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: tests-${{ github.event_name }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-and-package:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- name: Record the published file list of each package
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p pack-lists
|
||||
for p in cli daemon tui; do
|
||||
(cd packages/$p && npm pack --dry-run --json --ignore-scripts) \
|
||||
| node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{for(const f of JSON.parse(s)[0].files)console.log(f.path)})' \
|
||||
| sort > pack-lists/$p.txt
|
||||
echo "$p: $(wc -l < pack-lists/$p.txt) files"
|
||||
done
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: pack-lists
|
||||
path: pack-lists/
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run lint
|
||||
|
||||
repo-checks:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run test:repo
|
||||
|
||||
linux-seat-environment:
|
||||
if: ${{ github.event_name != 'push' }}
|
||||
# Cover /proc seat environments and Linux canonical-input limits that macOS cannot exercise.
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
- name: Install the real terminal and staging tools
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes tmux rsync
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- name: Check seat environments without ambient credentials or external network
|
||||
run: |
|
||||
set -euo pipefail
|
||||
suite_root=$(mktemp -d "$RUNNER_TEMP/opr-seat-env.XXXXXX")
|
||||
mkdir -p "$suite_root/home"
|
||||
# Only loopback exists in this namespace. Drop back to the runner user
|
||||
# before the CLI, private daemon and private tmux server are started.
|
||||
sudo unshare --net -- /bin/bash -s -- "$(id -un)" "$suite_root" "$PATH" <<'SUITE'
|
||||
set -euo pipefail
|
||||
ip link set lo up
|
||||
exec runuser -u "$1" -- env -i HOME="$2/home" PATH="$3" TMPDIR="$2" TMUX_TMPDIR="$2" \
|
||||
CI=true TERM=xterm-256color \
|
||||
npm run test -w packages/daemon -- test/seat-env-parent-session.integration.test.ts test/pi-shell-launch-native.test.ts --maxWorkers=1 --reporter=verbose
|
||||
SUITE
|
||||
|
||||
package-tests:
|
||||
if: ${{ github.event_name != 'push' }}
|
||||
# macOS, the platform OpenRig runs on: several daemon and cli tests read macOS ps columns,
|
||||
# file-inode behaviour and release tags.
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
package: [daemon, cli, tui, ui]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
- run: brew install tmux
|
||||
- run: npm ci
|
||||
- name: Build the packages the tests import
|
||||
run: npm run build
|
||||
- name: Run the suite without ambient credentials or external network
|
||||
run: |
|
||||
suite_root=$(mktemp -d "$RUNNER_TEMP/opr-test.XXXXXX")
|
||||
suite_root=$(cd "$suite_root" && pwd -P)
|
||||
mkdir -p "$suite_root/home" "$suite_root/bin"
|
||||
# Same system bytes, nonprivileged mode; no system file is changed.
|
||||
/usr/bin/install -m 0755 /bin/ps "$suite_root/bin/ps"
|
||||
# Only this job's temporary sockets (including tmux) are reachable.
|
||||
env -i HOME="$suite_root/home" PATH="$PATH" TMPDIR="$suite_root" TMUX_TMPDIR="$suite_root" \
|
||||
CI=true TERM=xterm-256color \
|
||||
sandbox-exec -D IPC_ROOT="$suite_root" -p '(version 1)(allow default)(deny network-outbound)(allow network-outbound (remote ip "localhost:*"))(allow network-outbound (subpath (param "IPC_ROOT")))' \
|
||||
/bin/bash -s "${{ matrix.package }}" <<'SUITE'
|
||||
set -euo pipefail
|
||||
node scripts/ci-macos-process-probe.mjs
|
||||
# The production readers use bare ps. Select real system bytes only after
|
||||
# self/owned-child observation and network/IPC confinement are proved.
|
||||
export PATH="$TMPDIR/bin:$PATH"
|
||||
test "$(command -v ps)" = "$TMPDIR/bin/ps"
|
||||
exec npm run test -w "packages/$1"
|
||||
SUITE
|
||||
|
||||
installed-scenario:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 35
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- name: Installed package, daemon restart, and seeded durability failure
|
||||
run: bash scripts/run-pr-scenarios.sh
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: installed-scenario-evidence
|
||||
path: dist/pr-scenarios/
|
||||
if-no-files-found: error
|
||||
+2
-16
@@ -1,5 +1,4 @@
|
||||
node_modules/
|
||||
node_modules
|
||||
dist/
|
||||
.DS_Store
|
||||
*.log
|
||||
@@ -22,14 +21,8 @@ notes/*
|
||||
*.sqlite-wal
|
||||
*.sqlite.pre-reboot-backup
|
||||
.openrig/
|
||||
.claude/*
|
||||
!.claude/skills/
|
||||
.claude/skills/*
|
||||
!.claude/skills/developing-openrig/
|
||||
.agents/*
|
||||
!.agents/skills/
|
||||
.agents/skills/*
|
||||
!.agents/skills/developing-openrig/
|
||||
.claude
|
||||
.agents
|
||||
.openrig
|
||||
.worktrees/
|
||||
|
||||
@@ -38,10 +31,3 @@ demo/proof/
|
||||
|
||||
# Build artifacts
|
||||
*.tgz
|
||||
|
||||
# OPR.0.5.3.7 R2 — the shipped context-pack projection is DERIVED AT PACKAGE TIME
|
||||
# (scripts/generate-context-packs.mjs), never committed or edited in place.
|
||||
packages/daemon/context-packs/
|
||||
|
||||
# Internal build receipts; never commit
|
||||
/.evidence/
|
||||
-334
@@ -1,334 +0,0 @@
|
||||
# OpenRig architecture
|
||||
|
||||
> **This map is not the territory.** It is a zoomed-out guide for finding your way around the
|
||||
> code. It is incomplete by design and it may be stale. Where it disagrees with the code, the code
|
||||
> is right; please fix the page. Every path, symbol, count and command below was checked against
|
||||
> commit `c750304f` (0.6.6 development). Each count shows the command that produced it, so you
|
||||
> can re-run it.
|
||||
|
||||
## What OpenRig is
|
||||
|
||||
OpenRig runs a team of coding agents on your machine as one system. A local daemon keeps the team's
|
||||
state in SQLite and runs each agent as an ordinary Claude Code or Codex session (or a plain
|
||||
terminal, a Pi or Oh My Pi (OMP) runner, or a scripted test stub) inside tmux. People and agents drive it through the
|
||||
`rig` CLI, a terminal UI (TUI) and an MCP server, all of which talk to the daemon over local HTTP.
|
||||
The agents themselves are unmodified. What OpenRig adds is the coordination around them: launching
|
||||
and restoring agents, delivering messages, keeping durable work in a queue, and giving each agent
|
||||
the right skills and context.
|
||||
|
||||
## Packages
|
||||
|
||||
| Directory | Package | What it is | How it ships |
|
||||
|---|---|---|---|
|
||||
| `packages/daemon` | `@openrig/daemon` (private) | The daemon: HTTP routes, domain services, SQLite migrations, runtime adapters, built-in specs, skills, plugins, static context-pack sources | Copied into `@openrig/cli` |
|
||||
| `packages/cli` | `@openrig/cli` | The `rig` command, the MCP server, daemon start and stop | The only published package |
|
||||
| `packages/tui` | `@openrig/tui` (private) | The terminal UI | Copied into `@openrig/cli`; opened by `rig tui`, by bare `rig` in a terminal, or by the `openrig-tui` bin |
|
||||
| `packages/ui` | `@openrig/ui` (private) | The React web UI, in **maintenance mode**: it still ships and runs, but gets no new feature work. The CLI and TUI are the supported surfaces | Built files copied into `@openrig/cli` and served by the daemon |
|
||||
| `packages/test-system` | none (not an npm workspace) | The stub-agent scenario library, the CI scenario harness, live-model eval cases | Not shipped |
|
||||
|
||||
**What bundles what.** `packages/cli/package.json` publishes `LICENSE`, `dist`, `daemon`, `ui`,
|
||||
`tui` and `scripts`. `scripts/build-package.sh` (the CLI's `prepublishOnly`) builds the daemon, UI,
|
||||
TUI and CLI, then assembles them inside `packages/cli/`: the daemon's `dist`, `assets`, `specs` and
|
||||
`policies`, the generated context packs and a copy of `docs/reference/` go under `daemon/`; the UI
|
||||
build goes to `ui/dist`; the TUI build goes to `tui/dist`. So `docs/reference/` ships to every
|
||||
user; `docs/as-built/` and this file do not.
|
||||
|
||||
**Sharing code between packages.** The CLI and TUI import daemon code only through the subpaths in
|
||||
the `exports` field of `packages/daemon/package.json` (21 at this commit), for example
|
||||
`@openrig/daemon/attention`, which points at `packages/daemon/src/attention-surface.ts`. The CLI
|
||||
uses 19 of them and the TUI uses 2. In development they resolve through the npm workspace. At
|
||||
package time `scripts/rewrite-daemon-imports.mjs` rewrites them to the shipped `daemon/dist` copy
|
||||
and fails the build on a specifier with no `exports` entry. To share new daemon code, add a
|
||||
`*-surface.ts` file and an `exports` entry.
|
||||
|
||||
## The request path
|
||||
|
||||
```
|
||||
rig (CLI) TUI MCP server (rig mcp serve, stdio)
|
||||
\ | /
|
||||
+---- HTTP to the daemon (port 7433 by default) --------------+
|
||||
|
|
||||
packages/daemon/src/server.ts createApp(): context middleware, /healthz, app.route("/api/...")
|
||||
|
|
||||
packages/daemon/src/routes/ one Hono router per area; reads services with c.get(...)
|
||||
|
|
||||
packages/daemon/src/domain/ services, repositories, orchestrators, event bus
|
||||
/ | \
|
||||
SQLite (db/) tmux (adapters/tmux.ts) runtime adapters (adapters/*-adapter.ts)
|
||||
claude-code, codex, pi, omp, terminal, stub
|
||||
```
|
||||
|
||||
**Inside the daemon** (`packages/daemon/src/`):
|
||||
|
||||
- **Startup.** `index.ts` is the process entry: it resolves where to listen, starts the server and
|
||||
runs the periodic queue sweeps. It calls `createDaemon()` in `startup.ts`, which opens the
|
||||
database (`db/connection.ts`: WAL mode, foreign keys on), runs `migrate(db, ALL_MIGRATIONS)`,
|
||||
constructs every service and adapter, and passes them to `createAppWithWebSocket(deps)` in
|
||||
`server.ts`.
|
||||
- **`server.ts`.** In `createApp(deps)`, an early `app.use("*", ...)` middleware (after the
|
||||
request-phase observer and the JSON body-parse tracker) puts each
|
||||
service on the request context (`c.set("<name>" as never, deps.<name>)`), serves `/healthz`, and
|
||||
mounts each router with `app.route("/api/<area>", ...)`. Unknown `/api/*` paths get a JSON 404;
|
||||
other GET requests serve the web UI's built files.
|
||||
- **`routes/`.** Thin handlers. For example, `routes/ps.ts` reads `psProjectionService` from the
|
||||
context and returns its entries as JSON. `routes/terminal-ws.ts` registers the terminal
|
||||
WebSocket, and `routes/require-sender-identity.ts` is a shared helper rather than a router.
|
||||
- **`domain/`.** The behaviour: repositories over SQLite (`rig-repository.ts`,
|
||||
`queue-repository.ts`, `session-registry.ts`), orchestration (`startup-orchestrator.ts`,
|
||||
`node-launcher.ts`, `rigspec-instantiator.ts`), message delivery (`session-transport.ts`,
|
||||
`seat-delivery-guard.ts`) and events (`event-bus.ts`). Larger areas have their own
|
||||
subdirectories, such as `gateway/`, `provider/`, `policies/`, `scope/`, `context-packs/` and
|
||||
`mission-control/`.
|
||||
- **`adapters/`.** The outside world. `tmux.ts` wraps tmux. The six `RuntimeAdapter`
|
||||
implementations (OMP's extends Pi's) launch each runtime and project skills and files into it. `cmux.ts` and
|
||||
`compose-services-adapter.ts` cover other integrations.
|
||||
- **`db/`.** `connection.ts`, `migrate.ts`, `all-migrations.ts` and `migrations/`.
|
||||
- **`middleware/auth-bearer-token.ts`.** The bearer-token check for operator write routes, plus
|
||||
the loopback and Tailscale bind detection used at startup.
|
||||
- **Live updates** go out as server-sent events: `routes/events.ts` uses Hono's `streamSSE`, and
|
||||
the TUI subscribes to `/api/activity/events`.
|
||||
- **State** lives in `$OPENRIG_HOME` (default `~/.openrig`). The database is `openrig.sqlite`
|
||||
there (`daemon-db-path.ts`).
|
||||
|
||||
**The clients:**
|
||||
|
||||
- **CLI.** `createProgram()` in `packages/cli/src/index.ts` registers every top-level command;
|
||||
the commands live in `packages/cli/src/commands/`. Most are thin clients: they call a daemon route
|
||||
through `DaemonClient` (`packages/cli/src/client.ts`) and render the result. A few lifecycle
|
||||
helpers open the database file directly. Bare `rig` in a terminal opens the TUI
|
||||
(`packages/cli/src/front-door.ts`).
|
||||
- **MCP.** `createMcpServer(client)` in `packages/cli/src/mcp-server.ts` registers the tools over
|
||||
the same `DaemonClient`. `rig mcp serve` (`packages/cli/src/commands/mcp.ts`) runs it on stdio.
|
||||
- **TUI.** `packages/tui/src/daemon-client.ts` is its only HTTP module. It reads existing daemon
|
||||
projections, for example `/api/ps`, `/api/rigs/summary` and `/api/queue/list`, and the
|
||||
`/api/activity/events` stream when the daemon offers it. The daemon URL comes from `--url`,
|
||||
`OPENRIG_URL`, or `http://127.0.0.1:7433`. Agents can drive a running TUI through its control
|
||||
socket (`packages/tui/src/socket-server.ts`).
|
||||
- **Web UI.** Served by the daemon from the bundled `ui/dist`.
|
||||
|
||||
**In a development checkout,** `rig daemon start` prefers `packages/daemon/dist` over the bundled
|
||||
copy (`resolveDaemonPath` in `packages/cli/src/daemon-lifecycle.ts`), and the TUI launcher prefers
|
||||
`packages/tui/dist` (`packages/cli/src/front-door.ts`). Rebuild the package you changed. A daemon
|
||||
that is already running keeps running the code it started with.
|
||||
|
||||
## Key counts
|
||||
|
||||
At commit `c750304f`. Run these from the repository root to refresh them.
|
||||
|
||||
| What | Count | Command |
|
||||
|---|---|---|
|
||||
| Database migrations | 94 files (latest: `095_rig_non_interruptive.ts`; there is no 093) | `git ls-files packages/daemon/src/db/migrations \| wc -l` |
|
||||
| Files in `routes/` | 67 | `git ls-files packages/daemon/src/routes \| wc -l` |
|
||||
| ... of which create a Hono router | 65 | `git grep -l 'new Hono' -- packages/daemon/src/routes \| wc -l` |
|
||||
| `app.route(...)` mounts in `server.ts` | 69 | `grep -c 'app.route(' packages/daemon/src/server.ts` |
|
||||
| Top-level `rig` commands | 87 | `grep -c 'program.addCommand(' packages/cli/src/index.ts` |
|
||||
| Runtime adapters | 6 (`claude-code`, `codex`, `pi`, `omp`, `terminal`, `stub`); the command counts 5 because `OmpRuntimeAdapter` extends `PiRuntimeAdapter` | `git grep -l 'implements RuntimeAdapter' packages/daemon/src \| wc -l` |
|
||||
| MCP tools | 18 | `grep -c 'server.tool(' packages/cli/src/mcp-server.ts` |
|
||||
| Daemon `exports` subpaths | 21 | `node -p 'Object.keys(require("./packages/daemon/package.json").exports).length'` |
|
||||
| Library scenarios | 15 | `grep -l '^scenario:' packages/test-system/scenarios/*.yaml \| wc -l` |
|
||||
|
||||
## Where to add things
|
||||
|
||||
Each recipe names the files to touch. Read a neighbouring example before you start; the code is
|
||||
the authority.
|
||||
|
||||
### A CLI command
|
||||
|
||||
1. Create `packages/cli/src/commands/<name>.ts` exporting a function that returns a commander
|
||||
`Command` and takes optional injected dependencies. `packages/cli/src/commands/unarchive.ts`
|
||||
is a short example.
|
||||
2. In the action, get the daemon with `getDaemonStatus(...)` and `daemonStatusGuard(status)`
|
||||
(`packages/cli/src/daemon-lifecycle.ts`), then call it with
|
||||
`new DaemonClient(getDaemonUrl(status))` (`packages/cli/src/client.ts`). Offer `--json` for
|
||||
agents, and set `process.exitCode` on failure.
|
||||
3. Register it in `createProgram()` in `packages/cli/src/index.ts` with
|
||||
`program.addCommand(...)`. If it takes injected dependencies, add them to `ProgramDeps` in the
|
||||
same file.
|
||||
4. Add `packages/cli/test/<name>.test.ts`. `packages/cli/test/archive.test.ts` shows how tests
|
||||
inject mocked lifecycle dependencies.
|
||||
5. If the command needs new daemon behaviour, add a route as well.
|
||||
|
||||
### A daemon route
|
||||
|
||||
1. Create `packages/daemon/src/routes/<area>.ts` with a Hono router. Both styles exist: a
|
||||
constant (`export const psRoutes = new Hono()` in `routes/ps.ts`) and a factory
|
||||
(`whoamiRoutes()` in `routes/whoami.ts`).
|
||||
2. Read services from the request context with `c.get("<name>" as never)`. Keep the behaviour in
|
||||
`packages/daemon/src/domain/`.
|
||||
3. Mount it in `createApp()` in `packages/daemon/src/server.ts` with
|
||||
`app.route("/api/<area>", ...)`, above the `/api/*` 404 catch-all.
|
||||
4. A new service needs three more edits: a field on `AppDeps` and a `c.set(...)` line in
|
||||
`server.ts`, and its construction in `createDaemon()` in `packages/daemon/src/startup.ts`.
|
||||
5. Test it the way `packages/daemon/test/whoami-routes.test.ts` does: build a small Hono app,
|
||||
`c.set` the service, mount the router and call `app.request(...)`. `createFullTestDb()` in
|
||||
`packages/daemon/test/helpers/test-app.ts` gives you an in-memory database migrated with the
|
||||
test fixture list (`migrationsForFullTestDb`).
|
||||
|
||||
### A database migration
|
||||
|
||||
1. Create `packages/daemon/src/db/migrations/<NNN>_<name>.ts` exporting a `Migration`
|
||||
(`{ name: "<NNN>_<name>.sql", sql: "..." }`; the type is in `packages/daemon/src/db/migrate.ts`).
|
||||
`089_classification_identity_provenance.ts` is a one-line example.
|
||||
2. Take the next number after the highest on `main` (095 at this commit). Parallel pull requests
|
||||
often pick the same number, so check again and renumber if `main` moved before you merge.
|
||||
3. Import it in `packages/daemon/src/db/all-migrations.ts` and append it to `ALL_MIGRATIONS`.
|
||||
Startup runs exactly that list (`startup-migrations-mirror.test.ts` pins this).
|
||||
4. Add it to `migrationsForFullTestDb` in `packages/daemon/test/helpers/test-app.ts`, or list it in
|
||||
`migrationsForFullTestDbExclusions` with a reason. `migration-fixture-parity.test.ts` fails
|
||||
otherwise.
|
||||
5. `migrate()` sorts migrations by name and records each applied name in `schema_migrations`, so
|
||||
an install that already applied a migration never runs it again. Editing a migration that has
|
||||
shipped changes nothing for those installs; add a new one instead. A long-lived install applies
|
||||
every new migration on its next start, against real data.
|
||||
|
||||
### A runtime adapter
|
||||
|
||||
1. Implement `RuntimeAdapter` from `packages/daemon/src/domain/runtime-adapter.ts`: `runtime`,
|
||||
`listInstalled`, `project`, `deliverStartup`, `launchHarness` and `checkReady`. The smallest
|
||||
implementation is `packages/daemon/src/adapters/terminal-adapter.ts`; the scripted test double
|
||||
is `stub-runtime-adapter.ts`.
|
||||
2. Register it in `packages/daemon/src/startup.ts`. The adapter maps are keyed by runtime name:
|
||||
`adapters:` (for the pod instantiator) and `runtimeAdapters:`. The context monitor has its own
|
||||
map for the runtimes it samples.
|
||||
3. Accept the runtime name in rig spec validation: `SUPPORTED_RUNTIMES` in
|
||||
`packages/daemon/src/domain/rigspec-preflight.ts`. Other runtime lists, such as `RuntimeHint` in
|
||||
`domain/discovery-types.ts` and `LEGACY_KNOWN_RUNTIMES` in `domain/rigspec-schema.ts`, may need
|
||||
it too; search for an existing runtime name such as `"pi"` to find them.
|
||||
4. Launch, projection and readiness are arteries. Read
|
||||
[docs/as-built/arteries.md](docs/as-built/arteries.md) first.
|
||||
|
||||
### A shipped skill
|
||||
|
||||
Skills ship from three copies in this repository (the "edges"), listed in
|
||||
`scripts/skill-edge-layout.generated.json`:
|
||||
|
||||
| Edge | Path | Layout |
|
||||
|---|---|---|
|
||||
| `spec` | `packages/daemon/specs/agents/shared/skills/` | by category (`core`, `pm`, `pods`, `process`) |
|
||||
| `plugin` | `packages/daemon/assets/plugins/openrig-core/skills/` | flat |
|
||||
| `canonical` | `skills/_canonical/` | public mirror of the `spec` copy |
|
||||
|
||||
Each skill in the layout file lists which edges carry it. At this commit, 33 skills are in `spec`
|
||||
and `canonical`, 17 only in `plugin`, and 2 in all three.
|
||||
|
||||
- Each skill has one editable home: its `spec` copy when it has a `spec` edge, otherwise its
|
||||
`plugin` copy. Its other edges are generated from that home.
|
||||
- `npm run mirror-skills:check` (part of `npm test`) hashes every file in each edge and compares the
|
||||
hashes with `scripts/skill-edge-digests.generated.json`. It also fails on a `SKILL.md` the layout
|
||||
does not list, on a generated copy that differs from its home, and on a home holding text the
|
||||
public copies would drop.
|
||||
- The full apply, `npm run mirror-skills`, regenerates every generated copy from its home and
|
||||
refreshes the digests. It reads nothing outside the repository and never writes to a home.
|
||||
- **To fix a skill:** edit its home, run `npm run mirror-skills`, then run
|
||||
`npm run mirror-skills:check`.
|
||||
- **To add a skill:** open an issue first. Its layout entry is generated from files that are not
|
||||
in this repository.
|
||||
- Every shipped skill also becomes a context pack at package time (next recipe).
|
||||
|
||||
### A context pack
|
||||
|
||||
- **Static packs** are hand-written: a directory under `packages/daemon/context-packs-src/<name>/`
|
||||
with a `manifest.yaml` and its files. Keep `version: "0"` in the manifest; the generator stamps
|
||||
the package version. A pack file may be a symlink to a file under `docs/reference/`, so a
|
||||
reference document has one source (for example `help/help.md` points to
|
||||
`docs/reference/help.md`). Any other symlink fails the build. There are 5 static packs at this
|
||||
commit.
|
||||
- **Generated packs** go to `packages/daemon/context-packs/`, which is gitignored and never edited
|
||||
by hand. `scripts/generate-context-packs.mjs` builds it at package time from every shipped skill
|
||||
and every static pack.
|
||||
- `npm run generate-context-packs:check` (part of `npm test`) validates every pack through the
|
||||
daemon's own manifest parser, so build the daemon first. It also runs the leak scan over the
|
||||
static sources, including the `docs/reference/` files they link to.
|
||||
- The daemon serves the shipped packs through `rig context get`; `startup.ts` registers the bundled
|
||||
directory as its built-in root.
|
||||
|
||||
### A scenario
|
||||
|
||||
Scenarios run the real CLI, daemon, tmux and SQLite with scripted stub agents (`runtime: stub`)
|
||||
and no model cost.
|
||||
|
||||
1. Add `packages/test-system/scenarios/<defect-class>.yaml`, named for the class of bug it
|
||||
catches. Topology fixtures sit beside it (`*-stub.yaml`).
|
||||
2. The format is checked by `validateScenario` in
|
||||
`packages/daemon/test/helpers/scenario-schema.ts`. Step verbs: `up`, `down`, `send`, `restart`,
|
||||
`restore`, `emit`, `mutate`, `policy`, `seed_regression`, `daemon`, plus `expect`. `expect` may
|
||||
only read shipped surfaces: `ps`, `queue`, `stream`, `scope`, `pane`, `transcript`,
|
||||
`tui_socket`, `policy_provenance`.
|
||||
3. Today the runner binds `up`, `down`, `send`, `restart` and `daemon`. `restore`, `emit`,
|
||||
`mutate` and `policy` fail with `UnboundActionError`, and `seed_regression` needs a fault
|
||||
controller (`packages/daemon/test/helpers/scenario-real-deps.ts`).
|
||||
4. Write a header comment naming the defect class and the seed: what a seeded regression plants,
|
||||
and which `expect` must catch it. A scenario counts only as a pair: it passes on healthy code
|
||||
and fails on the seeded run (`packages/test-system/README.md`).
|
||||
5. **Run it on your machine** after `npm run build`:
|
||||
`node --import tsx packages/daemon/scripts/run-scenarios.mjs <file.yaml>`. It uses a private
|
||||
daemon and a private tmux server, and builds its environment from `HOME`, `PATH` and `TERM`
|
||||
only, so your session's `TMUX` and daemon variables are dropped and it is safe to run from
|
||||
inside tmux, including an agent's pane. It supplies no fault controller, so a
|
||||
`seed_regression` step fails there; the seeded pair runs in the container path.
|
||||
6. **CI.** The `installed-scenario` job runs `scripts/run-pr-scenarios.sh` in disposable,
|
||||
network-less containers built from the packed package. It runs four cases: `fixture` and `library`
|
||||
(`queue-baton-survives-restart.yaml`), each with a healthy run and a seeded lost-baton run, plus
|
||||
`transcript` and `capture`, healthy runs only. Adding a seeded case means changing three files:
|
||||
the `--case` list in `scripts/run-pr-scenarios.sh`, `CASES` in
|
||||
`packages/test-system/ci/result.mjs`, and the fault controller in
|
||||
`packages/test-system/ci/run.mjs`; a healthy-only case goes in `PASSING_CASES` in the first two
|
||||
and needs no fault controller. [docs/as-built/test-layers.md](docs/as-built/test-layers.md)
|
||||
covers both run modes, running a case on your own Docker host, and what a stub can and cannot
|
||||
prove.
|
||||
|
||||
## Guards you will meet
|
||||
|
||||
`npm test` runs `test:repo` (builds the daemon, runs `node --test scripts/*.test.mjs`, the docs
|
||||
guard, `mirror-skills --check` and `generate-context-packs --check`) and then the daemon, CLI and
|
||||
TUI suites. `npm run test:ui` runs the web UI suite separately. `npm run lint` typechecks all four
|
||||
packages.
|
||||
|
||||
- **Docs guard** (`scripts/check-docs-guard.mjs`). Under `docs/`, git may track only
|
||||
`docs/as-built/`, `docs/reference/`, `docs/releases/` and exactly `docs/DESIGN.md`;
|
||||
`.gitignore` ignores the rest of `docs/`. Remember that `docs/reference/` ships inside the npm
|
||||
package, and the daemon copies its Markdown files to `$OPENRIG_HOME/reference/` when it
|
||||
starts. Files in `docs/as-built/` carry a `last-verified-against-source` commit in their
|
||||
frontmatter.
|
||||
- **Internal-leak guard** (`scripts/internal-leak-scanner.mjs`). Its term lists come from an
|
||||
optional local file that `OPENRIG_LEAK_GUARD_RULES` names (`scripts/leak-guard-rules.mjs`);
|
||||
without one the term checks are skipped and every other check runs, and the release-time
|
||||
substance gate requires it. It runs in `generate-context-packs.mjs` (static pack sources, on every
|
||||
`npm test`), in the full skill mirror apply (each public skill), in `scripts/build-package.sh`
|
||||
(the daemon `specs/` tree before it is copied into the package), and in the release-time
|
||||
substance gate (`scripts/check-substance-gate.mjs`, `npm run gate:substance`) over the packed
|
||||
CLI. No workflow runs it over a pull request's whole diff.
|
||||
- **Portability report** (`.github/workflows/portability-report.yml`, running
|
||||
`scripts/portability-report.mjs`). On each pull request it lists added lines containing
|
||||
machine-specific values: credentials, home and temporary paths, network addresses, email
|
||||
addresses. It never fails the job; you read it and decide.
|
||||
- **Generated files.** Do not hand-edit `scripts/*.generated.json` or anything under
|
||||
`packages/daemon/context-packs/`. The skill-edge digests are refreshed by
|
||||
`npm run mirror-skills` (see "A shipped skill").
|
||||
The internal-token rules support optional `allowed_context_lines` for reviewed generic
|
||||
examples: the complete line must match exactly, including indentation and case. Unlike
|
||||
`allowed_context_substrings`, this does not exempt a line with added or substituted text;
|
||||
internal-path checks and the substance gate's human-review dispositions still apply.
|
||||
- **Hosted CI** (`.github/workflows/tests.yml`): `build-and-package`, `typecheck`, `repo-checks`,
|
||||
`package-tests` (macOS; daemon, CLI, TUI and UI suites with no credentials and no external
|
||||
network) and `installed-scenario`.
|
||||
|
||||
## Pointers
|
||||
|
||||
- [CONTRIBUTING.md](CONTRIBUTING.md): setup, what a pull request needs, what to expect from review.
|
||||
- [docs/reference/developing.md](docs/reference/developing.md): which checks block and which are
|
||||
advisory, and what `.github/workflows/tests.yml` runs.
|
||||
- [docs/reference/worktree-builds.md](docs/reference/worktree-builds.md): building in a git
|
||||
worktree, with its own `npm ci` rather than a symlinked `node_modules`.
|
||||
- [docs/reference/](docs/reference/): user and operator reference; it ships with the package.
|
||||
- [docs/as-built/](docs/as-built/README.md): module-by-module descriptions of the system. **These
|
||||
are being re-verified for 0.6.6.** Each page's `last-verified-against-source` names the commit it was
|
||||
checked against; until a page is re-verified, treat its counts as historical. Use them for
|
||||
orientation, then check the code.
|
||||
- [docs/as-built/test-layers.md](docs/as-built/test-layers.md): what each test layer covers and
|
||||
what it cannot catch.
|
||||
- [docs/as-built/arteries.md](docs/as-built/arteries.md): the areas where a small change has a
|
||||
large effect, what depends on them, and what broke there before.
|
||||
- [packages/test-system/ci/README.md](packages/test-system/ci/README.md): the CI scenario job.
|
||||
- [docs/releases/](docs/releases/) and [CHANGELOG.md](CHANGELOG.md): release history, written by
|
||||
the maintainers at release time.
|
||||
-8154
File diff suppressed because it is too large.
Load diff
@@ -1,75 +0,0 @@
|
||||
# Code of Conduct
|
||||
|
||||
## Our pledge
|
||||
|
||||
We as members, contributors, and maintainers pledge to make participation in OpenRig a
|
||||
harassment-free experience for everyone, regardless of age, body size, visible or invisible
|
||||
disability, ethnicity, sex characteristics, gender identity and expression, level of experience,
|
||||
education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive,
|
||||
and healthy community.
|
||||
|
||||
## Our standards
|
||||
|
||||
Examples of behaviour that contributes to a positive environment:
|
||||
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes, and learning from
|
||||
the experience
|
||||
- Focusing on what is best not just for us as individuals, but for the overall community
|
||||
|
||||
Examples of unacceptable behaviour:
|
||||
|
||||
- The use of sexualized language or imagery, and sexual attention or advances of any kind
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email address, without their
|
||||
explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Enforcement responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying and enforcing our standards of acceptable
|
||||
behaviour and will take appropriate and fair corrective action in response to any behaviour that
|
||||
they deem inappropriate, threatening, offensive, or harmful. Maintainers have the right and
|
||||
responsibility to remove, edit, or reject comments, commits, code, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all project spaces (the repository, Discussions, issues and
|
||||
pull requests) and when an individual is officially representing the project in public spaces.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behaviour may be reported to the
|
||||
maintainers at **[email protected]**. All complaints will be reviewed and investigated
|
||||
promptly and fairly.
|
||||
All maintainers are obligated to respect the privacy and security of the reporter of any incident.
|
||||
|
||||
## Enforcement guidelines
|
||||
|
||||
Maintainers will follow these community impact guidelines in determining the consequences for any
|
||||
action they deem in violation of this Code of Conduct:
|
||||
|
||||
1. **Correction.** A private, written warning, with clarity around the nature of the violation and
|
||||
an explanation of why the behaviour was inappropriate. A public apology may be requested.
|
||||
2. **Warning.** A warning with consequences for continued behaviour, including no interaction with
|
||||
the people involved for a specified period.
|
||||
3. **Temporary ban.** A temporary ban from any sort of interaction or public communication with
|
||||
the community for a specified period.
|
||||
4. **Permanent ban.** A permanent ban from any sort of public interaction within the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org),
|
||||
version 2.1 by Coraline Ada Ehmke, available at
|
||||
https://www.contributor-covenant.org/version/2/1/code_of_conduct.html and licensed under
|
||||
[Creative Commons Attribution 4.0 International](https://creativecommons.org/licenses/by/4.0/).
|
||||
This adaptation names OpenRig and its maintainers and summarizes the enforcement guidelines. Community impact guidelines
|
||||
were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
|
||||
-135
@@ -1,135 +0,0 @@
|
||||
# Contributing to OpenRig
|
||||
|
||||
Thanks for being here. OpenRig is built in the open and by the thing it is: a rig of coding
|
||||
agents and a small group of people. External pull requests and issues have started arriving
|
||||
faster than we planned for, which is the best problem to have. This page says how to get a
|
||||
change in with the least friction on both sides.
|
||||
|
||||
## Before you start
|
||||
|
||||
- **Bugs:** open an issue with the bug template. Include your OpenRig version (`rig --version`),
|
||||
OS, Node and tmux versions, which harnesses are involved (or none), and the relevant command and output.
|
||||
Reports are public: remove credentials, private prompts, personal details and private paths
|
||||
before posting. Share a small reproduction rather than a full transcript or instance dump.
|
||||
- **Features and behaviour changes:** open an issue or a Discussion in *Ideas* first. Search issues
|
||||
and Discussions, and check ROADMAP.md, before proposing a feature. A short "what I am trying to do
|
||||
and what stops me" saves both of us a rewrite. Small, obvious fixes do not need an issue.
|
||||
- **Questions:** use [Discussions › Q&A](https://github.com/mvschwarz/openrig/discussions/categories/q-a),
|
||||
not an issue.
|
||||
|
||||
## Before a security or integration PR
|
||||
|
||||
OpenRig connects your coding agents to each other: shared context, messaging, coordination and long-running seats.
|
||||
It's designed for your own machine or a trusted private network, for you and people you trust, not for the open
|
||||
internet. It doesn't try to act for you in the outside world; your agents already have tools for that.
|
||||
|
||||
**Security-related PRs.** If you've found something exploitable, report it privately first (see
|
||||
[SECURITY.md](SECURITY.md)) rather than in a public PR. For hardening changes, tell us in the PR description:
|
||||
|
||||
- the scenario: how this goes wrong for someone using OpenRig as it's designed to be used, and the evidence you have
|
||||
- who or what causes it, and how they reach the install
|
||||
- what the change costs everyone else: a refusal, an extra step, a new setting
|
||||
|
||||
We look at the finding and the fix separately. We may agree with a finding and fix it differently, or decide the fix
|
||||
costs more than it protects. If the answers are missing we'll ask once, and close the PR with thanks if they don't
|
||||
come.
|
||||
|
||||
**Integrations with other tools or projects.** Open an issue or an Ideas Discussion and wait for a maintainer's yes on
|
||||
scope before you build it; an issue on its own isn't a yes. Often the best home for an integration is your own
|
||||
repository, and we're happy to link to it.
|
||||
|
||||
## Setting up
|
||||
|
||||
Node `^22 || ^24`, a working `tmux` and `rsync` are required. macOS includes `rsync`; many Linux
|
||||
installs don't (`sudo apt-get install rsync` on Debian or Ubuntu). `npm test` and `npm run mirror-skills`
|
||||
use it. Use the npm that comes with your Node, which is npm 10 or newer (`npm --version` shows which you
|
||||
have). A separately packaged npm 9, such as Debian or Ubuntu's `npm` package, fails a fresh `npm ci` with
|
||||
`ENOENT` on `chmod` of `node_modules/@openrig/tui/dist/main.js`. Then:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/mvschwarz/openrig.git
|
||||
cd openrig
|
||||
npm ci # exactly what package-lock.json records
|
||||
npm run build # all workspaces
|
||||
npm test # repo checks + daemon, cli, tui test suites
|
||||
npm run lint # typecheck every package
|
||||
```
|
||||
|
||||
`npm ci` installs the locked versions and never rewrites `package-lock.json`, which is what CI
|
||||
runs too. Use `npm install <package>` only when you're adding or changing a dependency, and commit
|
||||
the lockfile change with it.
|
||||
|
||||
`npm test` builds the daemon and runs repository checks before the package suites. Read the
|
||||
specific failure. Each shipped skill has one editable home in this repository (see
|
||||
["A shipped skill" in ARCHITECTURE.md](ARCHITECTURE.md#a-shipped-skill)). Edit its home, run
|
||||
`npm run mirror-skills` to regenerate its other copies and refresh the digests, then run
|
||||
`npm run mirror-skills:check` and commit the result. The apply reads nothing outside the repository.
|
||||
The repository checks include a docs guard: a
|
||||
tracked file under `docs/` must be in `docs/as-built/`, `docs/reference/` or `docs/releases/`
|
||||
(`docs/DESIGN.md` is the one exception). They check that shipped first-run text names only teams, skills and
|
||||
commands that exist, and they validate the context-pack sources;
|
||||
`npm run generate-context-packs` rebuilds the generated packs locally, which aren't committed.
|
||||
The UI unit-test suite is separate from `npm test`: run `npm run test:ui`. CI runs it on every pull
|
||||
request as `package-tests (ui)`, so a change has to pass it.
|
||||
[docs/as-built/test-layers.md](docs/as-built/test-layers.md) lists every test layer, what CI runs,
|
||||
and what each layer can and cannot prove.
|
||||
|
||||
For hands-on development, read the [check requirements](docs/reference/developing.md),
|
||||
[worktree setup](docs/reference/worktree-builds.md), and
|
||||
[machine changes](README.md#what-openrig-changes-on-your-machine). Use an isolated environment
|
||||
for changes that start daemons or agents; `OPENRIG_HOME` alone does not isolate provider settings.
|
||||
Permission configuration is an [explicit choice](docs/reference/getting-started.md#have-your-agent-configure-permissions).
|
||||
A checked-out tree is not the installed daemon; restarting an installed daemon does not adopt
|
||||
your working copy.
|
||||
|
||||
## Making the change
|
||||
|
||||
- One concern per pull request. Keep unrelated refactors separate; explain any refactor needed
|
||||
for the fix.
|
||||
- Keep the diff small enough to review in one sitting. If it is not, say why in the description.
|
||||
- Add or update a test where the change is testable. Use focused deterministic tests where
|
||||
possible. For terminal or provider behaviour, state what was exercised with the actual runtime
|
||||
and what was simulated; a stub alone does not prove the native interaction works.
|
||||
- Do not edit `CHANGELOG.md`. Maintainers write release notes at the tag.
|
||||
- Do not bump versions.
|
||||
- Match the surrounding style. `npm run lint` typechecks; it does not format code.
|
||||
- Write commit messages in the form the log already uses: `fix(cli): …`, `feat(daemon): …`,
|
||||
`docs(reference): …`, `test: …`.
|
||||
|
||||
## The pull request
|
||||
|
||||
Fill in the template. The three things a reviewer needs are: what a user gets, how you verified
|
||||
it, and anything you were unsure about. State the revision and relevant local changes you tested,
|
||||
what you actually ran, and any checks you could not run. Redact private information from evidence.
|
||||
|
||||
Contributions follow the repository's [Apache-2.0 license](LICENSE). Preserve attribution and any
|
||||
applicable license notices when adapting third-party material.
|
||||
|
||||
## What to expect from us
|
||||
|
||||
- We aim to acknowledge issues and pull requests within **one day**. An acknowledgement is not
|
||||
a completed review or a merge decision.
|
||||
- Our target for a first substantive review decision on an external PR is **seven days**. If it
|
||||
takes longer, we explain what is pending on the PR. This is a target, not a guaranteed deadline.
|
||||
- Labels you will see: `needs-repro` (we could not reproduce it yet; waiting on versions or steps),
|
||||
`fixed-on-main` (merged, not yet on npm), `good first issue`, `help wanted`, `discussion`
|
||||
(direction question; continues in Discussions).
|
||||
|
||||
Reviews here are done by people and by the project's own agents. An agent may ask the first
|
||||
clarifying question or run the reproduction; a maintainer makes the merge decision.
|
||||
|
||||
## Where things live
|
||||
|
||||
- **Start here:** [ARCHITECTURE.md](ARCHITECTURE.md) maps the packages, the request path, and where to
|
||||
add a command, route, migration, adapter, skill, context pack or scenario.
|
||||
[docs/as-built/arteries.md](docs/as-built/arteries.md) lists the areas where a small change has a
|
||||
large effect; if your change touches one, read it first.
|
||||
- **Using a coding agent?** Claude Code and Codex load the repository's `developing-openrig` skill
|
||||
when they work in this checkout (`.claude/skills/` and `.agents/skills/`). It points at the maps
|
||||
above and at what to run before you push.
|
||||
- Repository reference: `docs/reference/`; user documentation: [openrig.dev/docs](https://openrig.dev/docs).
|
||||
- Skills: `packages/daemon/specs/agents/shared/skills/`, plugin skills under
|
||||
`packages/daemon/assets/plugins/`, and `skills/_canonical/`; static context-pack sources:
|
||||
`packages/daemon/context-packs-src/`.
|
||||
Generated packs live in `packages/daemon/context-packs/` and are not hand-edited or committed.
|
||||
- Releases: [GitHub Releases](https://github.com/mvschwarz/openrig/releases) and npm `@openrig/cli`.
|
||||
@@ -4,10 +4,6 @@ This document captures the current OpenRig visual system so the product can be
|
||||
recreated, extended, or translated into other media without reverse-engineering
|
||||
the UI from code.
|
||||
|
||||
It describes the web UI in `packages/ui`, which is in maintenance mode (see
|
||||
`docs/reference/developing.md`). The terminal UI has its own theme layer in
|
||||
`packages/tui/src/theme.ts`.
|
||||
|
||||
## Essence
|
||||
|
||||
OpenRig is a local control plane for multi-agent work. The interface should feel
|
||||
@@ -86,12 +82,6 @@ terminal emulator.
|
||||
The canonical tokens live in `packages/ui/src/globals.css` and are surfaced to
|
||||
Tailwind in `packages/ui/tailwind.config.ts`.
|
||||
|
||||
The values below are the light set (`:root`). A `.dark` block in the same file
|
||||
redeclares the same names for Vellum Dark. The theme selector offers Vellum
|
||||
Light, Vellum Dark and System (`packages/ui/src/lib/theme.ts`). With no stored
|
||||
choice the UI follows the operating system's color scheme (`system`); the choice
|
||||
is stored under the `openrig.theme` local-storage key.
|
||||
|
||||
Paper:
|
||||
|
||||
- `--background: 47 20% 97%` - base paper, `#faf9f5`
|
||||
@@ -240,8 +230,6 @@ Runtime marks:
|
||||
|
||||
- Claude: pixel-style Claude mark, label `Claude`
|
||||
- Codex: Codex CLI terminal-circle mark, label `Codex`
|
||||
- Pi: blue rounded-square π mark, label `Pi`
|
||||
- Oh My Pi: violet rounded-square π mark, label `Oh My Pi` (short label `OMP`)
|
||||
- Terminal: compact terminal glyph, label `TTY` or `Terminal`
|
||||
- Unknown: neutral fallback
|
||||
|
||||
@@ -1,318 +1,42 @@
|
||||
# OpenRig
|
||||
|
||||
[](https://www.npmjs.com/package/@openrig/cli) [](https://www.npmjs.com/package/@openrig/cli) [](LICENSE) [](https://github.com/mvschwarz/openrig/stargazers)
|
||||
Open source multi-agent harness for coding teams.
|
||||
|
||||
A harness wraps a model. A rig wraps your harnesses. Define your agent team in YAML, boot it with one command. Claude Code and Codex in the same rig, managed as one system.
|
||||
|
||||
OpenRig is open-source software for building and running your own network of agents. It turns AI coding agents from a pile of terminal sessions into a persistent, organized team. Talk to a lead agent about the outcome you want; it can coordinate specialists across teams and bring you results and decisions that need your attention. Start with a repository and one useful change, then keep the team's work and context at the same addresses.
|
||||

|
||||
|
||||
It's the open-source system behind my AI civilization experiments.
|
||||
|
||||
**Guide:** [Getting started](docs/reference/quickstart.md) · **Stuck?** [Help](docs/reference/help.md) · **Questions:** [Q&A](https://github.com/mvschwarz/openrig/discussions/92) · **Updates and demos:** [@_feralmachine on X](https://x.com/_feralmachine)
|
||||
|
||||
## See it running
|
||||
|
||||

|
||||
|
||||
**Start here:** [the guided first-use path](docs/reference/quickstart.md): install, talk to the kernel operator, then choose a team for your first useful change.
|
||||
|
||||
Not setting this up today? Get the next walkthrough and occasional OpenRig updates → https://openrig.dev/follow
|
||||
|
||||
## Install and first run
|
||||
|
||||
**Let your coding agent set it up.** First check that `node --version` shows 22 or 24: the command below needs
|
||||
Node.js and the `npx` that comes with it. Then, in your project folder, add OpenRig's `rigs` skill to the agent you
|
||||
use:
|
||||
|
||||
```sh
|
||||
# Claude Code
|
||||
npx skills add https://github.com/mvschwarz/openrig/tree/v0.7.5/skills/rigs --skill rigs --agent claude-code
|
||||
# Codex
|
||||
npx skills add https://github.com/mvschwarz/openrig/tree/v0.7.5/skills/rigs --skill rigs --agent codex
|
||||
```
|
||||
|
||||
Then type `/rigs` in Claude Code, or `$rigs` in Codex, and say what you want built. The skill has your agent tell
|
||||
you what it will run and change before it does, install OpenRig, open the OpenRig view and pass your goal to
|
||||
OpenRig's operator.
|
||||
|
||||
You need Node.js 22 or 24 (22 on Apple silicon), tmux, macOS or Linux (WSL2 on Windows), and Claude Code or Codex
|
||||
signed in. One provider is enough to start.
|
||||
|
||||
Already have OpenRig? Use the tag that matches `rig --version`, or upgrade first.
|
||||
|
||||
Prefer to type the commands yourself? [Install it yourself ↓](#install-it-yourself)
|
||||
|
||||
Requires Node.js 22 or 24 and tmux, on macOS or Linux, and one working Claude Code or Codex account: one is enough, since the kernel operator adapts the shipped `starter` team to whichever you have. On Linux, the distribution's own Node.js can be older (Ubuntu 24.04's is 18); install a supported version with [nvm](https://github.com/nvm-sh/nvm) (`nvm install 22`) or NodeSource. On a Mac with Apple silicon, use Node.js 22 ([compatibility history](docs/releases/v0.5.15.md#known-compatibility-limitation)). On a Mac where you aren't an administrator, or where Homebrew belongs to another account (its `brew install` is refused), install Node.js in your own home folder with [nvm](https://github.com/nvm-sh/nvm): run `touch ~/.zshrc` (a new Mac account has none, and nvm's installer only adds itself to a profile file that already exists), then nvm's install command, for nvm v0.40.8 `curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.8/install.sh | bash` ([nvm's page](https://github.com/nvm-sh/nvm#install--update-script) has the newest), open a new terminal, and run `nvm install 22`. nvm itself needs no administrator password, and `npm install -g` then installs into your nvm folder. If nvm's installer says you need Apple's command line tools, run `xcode-select --install`, then its install command again. On Windows, use WSL2, the Windows route OpenRig supports; native Windows isn't supported. OpenRig's automated tests don't run on WSL2 yet; see [one user's working setup](docs/reference/getting-started.md#wsl2-a-reported-working-setup).
|
||||
|
||||
> **What OpenRig changes on your machine**
|
||||
> - **Install:** `@openrig/cli` goes in globally with npm.
|
||||
> - **Setup:** `rig setup` adds an OpenRig block to `~/.tmux.conf`. It can install tmux (with Homebrew on macOS), and
|
||||
> Herdr unless you pass `--no-herdr`. Plain `rig setup`, which is what the one-command install runs, checks both
|
||||
> Claude Code and Codex and may install a missing one; `rig setup --providers claude` (or `codex`) limits it to
|
||||
> yours.
|
||||
> - **Starting OpenRig, for the coding agent you use** (paths are the usual defaults):
|
||||
> - Codex: hooks and trust in `~/.codex/config.toml`.
|
||||
> - Claude Code: workspace trust in `~/.claude.json`, and activity hooks in the project's
|
||||
> `.claude/settings.local.json`, plus a status line unless you already have your own.
|
||||
> - Discovery skills go in `~/.claude/skills` and `~/.agents/skills`.
|
||||
> - **Starting a team in your project:** adds managed guidance to `AGENTS.md` or `CLAUDE.md`, plus the team's skills
|
||||
> and plugins.
|
||||
> - **Permissions:** by default, OpenRig's own kernel gets broader permissions than a project team gets by default.
|
||||
> On Codex that's full access with no approval prompts (`-s danger-full-access -a never`). On Claude Code, its file
|
||||
> tools, web fetches and operational commands, including reads under your home folder, run without asking. A
|
||||
> permission policy, per-agent choice or Codex profile changes that. What your team can do depends on the team you
|
||||
> choose and your settings; `workshop`, for example, ships with broad access.
|
||||
> - **Backups:** OpenRig doesn't guarantee it can preserve or roll back these files, so back them up first.
|
||||
>
|
||||
> [Everything OpenRig writes, and why →](https://github.com/mvschwarz/openrig#what-openrig-changes-on-your-machine)
|
||||
|
||||
### Install it yourself
|
||||
|
||||
This runs OpenRig's install script from the `v0.7.5` release. It checks Node.js and npm, installs the latest published `@openrig/cli` with `npm install -g`, runs the Node.js and SQLite check, then runs `rig setup --dry-run` and `rig setup`. The first line prints that plan and changes nothing:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.5/scripts/install.sh | sh -s -- --dry-run
|
||||
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.5/scripts/install.sh | sh
|
||||
```
|
||||
|
||||
If the last step isn't ready, the installer reports `INSTALLED, SETUP NOT COMPLETE` and points to the details from `rig setup` above. Follow that output and any reported Fix, then rerun `rig setup`; the CLI does not need reinstalling. If the only remaining failures are provider sign-ins, sign in to each selected provider as below and continue.
|
||||
|
||||
**Or step by step:**
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Install
|
||||
npm install -g @openrig/cli
|
||||
rig setup --dry-run
|
||||
|
||||
# Prepare the machine (attempts tmux, cmux, Claude Code, Codex, tmux defaults — reports what worked)
|
||||
rig setup
|
||||
|
||||
# Boot the conveyor starter rig (4 seats, mixed runtimes)
|
||||
rig up conveyor
|
||||
|
||||
# Open the UI
|
||||
rig ui open
|
||||
```
|
||||
|
||||
To install with Bun instead, run `bun add -g @openrig/cli`. OpenRig still runs on Node.js, so install Node.js 22 as well. Bun may block this package's postinstall script, in which case the Node.js and SQLite check described under [what OpenRig changes on your machine](#what-openrig-changes-on-your-machine) does not run at install time.
|
||||
After the conveyor rig boots, open the UI and click **intake.lead** in the topology graph. Use **Open CMUX** to jump into a terminal for that node. If cmux is not available, use the tmux attach command shown in the node detail panel instead.
|
||||
|
||||
npm 11 and later can skip that postinstall script and print `npm warn install-scripts` naming `@openrig/cli`. That's expected: the CLI still works, and only the Node.js and SQLite check was skipped. To run it yourself, use `node "$(npm root -g)/@openrig/cli/scripts/check-abi.mjs"`; `rig doctor` checks the Node.js version only.
|
||||
## Launch Walkthrough
|
||||
|
||||
Choose the working account you already have: **Claude Code, Codex, or both**. Reuse an explicit choice; no second subscription is required. `rig setup --dry-run` previews the broader setup; applying `rig setup` checks both harnesses and installs a missing one, plus Herdr unless declined with `--no-herdr`. Pass the choice with `--providers claude` or `--providers codex` so the other reads not selected instead of failed. The one-command install above doesn't know the choice, so it installs and checks both; for one provider, go step by step and run `rig setup --providers <choice>`. On macOS, the installing agent offers Ghostty once. Setup leaves an existing cmux installation alone. It is optional for the [selected-provider path](docs/reference/getting-started.md#choose-your-providers).
|
||||
The launch path above has been walked end-to-end on fresh macOS VMs. The only thing your agent can't do would be the oauth logins for claude and openai and dealing with permission prompts.
|
||||
|
||||
A team seat with no permission policy, per-seat choice or (for Codex) named profile launches with OpenRig's [team default](docs/reference/rig-spec.md#team-launch-defaults): Claude runs ordinary `rig` commands, project reads and common tests without prompting, while lifecycle commands such as `rig up` and `rig down` still ask; Codex also gets the OpenRig workspace and its pod's state directory as writable directories. Before launching, your agent recommends keeping that default, and offers to remember selected OpenRig commands in your native settings only if you want that, at personal project scope unless you explicitly choose user-wide sessions. It is not global YOLO or permission to invent work. On Yes, the agent [adds and verifies native rules](docs/reference/getting-started.md#have-your-agent-configure-permissions); No or no answer keeps the team default and leaves settings unchanged. An existing explicit choice is reused. Say “Undo the OpenRig command allowances added by this setup” to remove only its additions.
|
||||
### 1. Open the conveyor rig
|
||||
|
||||
If permission prompts are slowing you down, consider the
|
||||
[workshop bundle](https://openrig.dev/rigs/workshop). It ships with broad access
|
||||
and [non-interruptive mode](docs/reference/non-interruptive-mode.md): its agents
|
||||
bypass permission prompts and supported harness warning dialogs. The listing
|
||||
and before-install view explain that access before you choose it.
|
||||
When the UI opens, the explorer is already visible on the left. Click the `conveyor` rig to load its live topology.
|
||||
|
||||
Check `tmux -V` and only your selected CLI/login: `claude --version` plus
|
||||
`claude auth status`, or `codex --version` plus `codex login status`. If needed,
|
||||
sign in once with `claude auth login` or `codex login`; do not install or log in
|
||||
to an unused provider.
|
||||
### 2. Click `CMUX` on `intake.lead`
|
||||
|
||||
### Your agent opens OpenRig for you
|
||||
Once the topology loads, go to the `intake.lead` node and click its `CMUX` button. That opens the intake terminal directly.
|
||||
|
||||
After installing, your agent opens the OpenRig TUI and the operator for you. To get back to them later, ask your
|
||||
agent for OpenRig or your agents in any words. After checking the selected login, it starts the daemon if it's stopped
|
||||
and opens the view:
|
||||
|
||||
```sh
|
||||
rig daemon start
|
||||
rig terminal open saved:kernel --window
|
||||
```
|
||||
|
||||
`--window` opens a new terminal tab or window itself, or a space in your current
|
||||
Herdr session. Below 120 measured columns,
|
||||
the **operator** fills the first page; from 120, the dashboard and operator share
|
||||
it equally. The advisor has a separate tab or tmux window.
|
||||
An installing agent can run it from its shell on the daemon's
|
||||
desktop; the person copies nothing. It uses Herdr when installed, otherwise
|
||||
the same layout in plain tmux, and preserves existing conversations. `rig tui`
|
||||
is the team dashboard. Installation is finished when you are talking to the
|
||||
operator; if you choose to talk later, that handoff remains pending.
|
||||
|
||||
If setup stops, read the named step: missing tools and selected-provider logins
|
||||
need their specific setup action. A harness permission rule, sandbox or automatic
|
||||
permission decision can also block the installing agent's command. The agent
|
||||
should name the command and reported reason, then help you resolve that specific
|
||||
step within your chosen permissions. A permission refusal is not an OpenRig
|
||||
installation result.
|
||||
|
||||
The desktop may need an Automation approval; SSH/headless sessions may have no
|
||||
display. Herdr can show first-run panels, and an 80×24 window can cramp the view.
|
||||
Follow [the OpenRig view checks and next steps](docs/reference/getting-started.md#what-can-interrupt-installation-and-the-openrig-view).
|
||||
Herdr is visible only inside a terminal you can see. Changing the shared TUI to
|
||||
`:terminals` does not open that terminal. The agent checks the visible window when
|
||||
it has desktop access and says what it could not verify otherwise. Over headless
|
||||
SSH, it asks you to open a new terminal window or tab and gives the exact connection
|
||||
and attachment command for the current view. If you choose a manual attachment
|
||||
after a failure,
|
||||
ask the installing agent for the exact command using your current operator
|
||||
session; run that in a new terminal window.
|
||||
|
||||
The kernel's operator helps you pick a first team. It asks what you want to build,
|
||||
recommends one of three, and fits it to the providers you have.
|
||||
|
||||
| Team | Talk to | Agents and purpose |
|
||||
| --- | --- | --- |
|
||||
| `starter` | `dev-build@starter` | A Claude Code builder and Codex reviewer for one bounded change |
|
||||
| `workshop` | `orch-lead@workshop` | A lead, builder, QA and reviewer for ongoing work in one repository; a rig bundle the operator installs from its pinned listing |
|
||||
| `factory` | `orch-lead@factory` | Seven: a lead, advisor, build, QA, design and two independent reviewers for sustained product work |
|
||||
|
||||
As shipped, `starter` uses both Claude Code and Codex. With only one of them, ask
|
||||
the kernel operator to adapt it: it writes a copy of the team for your providers
|
||||
under the same name. [The naming guide](docs/reference/topology-naming.md) explains
|
||||
how their pods and roles grow and how to name your own team. `first-project` is
|
||||
starter's old name and still starts it,
|
||||
unless you already have a rig named `first-project` or `starter`: then it refuses
|
||||
and names the `rig up <name> --existing` command that brings that rig back.
|
||||
Show the selected runtime, configured model and command before launch; confirm the
|
||||
account supports the model instead of silently falling back. The kernel starts
|
||||
automatically and selects from available authenticated providers independently of
|
||||
these teams. A missing unused provider is not a setup requirement.
|
||||
|
||||
To launch the starter yourself instead (the manual path, without the operator):
|
||||
|
||||
```bash
|
||||
cd /path/to/your/repository
|
||||
rig specs preview starter --kind rig
|
||||
rig up starter --cwd . --plan
|
||||
rig up starter --cwd .
|
||||
```
|
||||
|
||||
To see your agents, use `rig terminal open saved:kernel --window`. If that window cannot open, `rig tui --shared` is the named dashboard-only fallback; it does not show the operator conversation. The installing agent should resolve or explain the window failure, not finish by suggesting a command for you to type. Closing a viewing terminal does not mean you should relaunch the team.
|
||||
|
||||
Check project-seat readiness with `rig ps --nodes --rig starter` and resolve any authentication, trust or permission prompt before assigning work. If a seat stopped at such a prompt before its startup context arrived, `rig ps` shows the `rig seat continue <seat>` command that delivers it once the prompt is answered. Then give the builder one bounded outcome from your repository:
|
||||
|
||||
```bash
|
||||
rig send dev-build@starter 'Implement <one useful change>. Track the task in the queue and return its ID. Keep it local, verify the behavior, ask dev-review in this rig to check the exact candidate, and record the result and how I can try it.'
|
||||
rig queue list --destination dev-build@starter --limit 1000
|
||||
```
|
||||
|
||||
Sending a message does not itself create a queue item; the builder records the task. Read the final artifact and the review of its exact candidate, then return to the same builder for the next change. [The guided first-use path](docs/reference/getting-started.md) covers readiness, a useful task, a reviewed result, Herdr/cmux terminals and recovery.
|
||||
|
||||
Not setting this up today? Get the next walkthrough and occasional OpenRig updates → https://openrig.dev/follow
|
||||
|
||||
## Community
|
||||
|
||||
- **Questions:** [Discussions › Q&A](https://github.com/mvschwarz/openrig/discussions/categories/q-a)
|
||||
- **Bugs and feature requests:** [open an issue](https://github.com/mvschwarz/openrig/issues/new/choose)
|
||||
- **Contributing:** [CONTRIBUTING.md](CONTRIBUTING.md) · [Code of Conduct](CODE_OF_CONDUCT.md) · [Security policy](SECURITY.md) · [Getting help](.github/SUPPORT.md)
|
||||
- **Working on OpenRig with a coding agent?** Start with the `developing-openrig` skill in `.agents/skills/` (Claude Code: `.claude/skills/`) and [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
- **Videos:** [youtube.com/@openrig](https://www.youtube.com/@openrig)
|
||||
- **Releases:** [GitHub Releases](https://github.com/mvschwarz/openrig/releases) and npm `@openrig/cli`
|
||||
|
||||
We aim to acknowledge issues and pull requests within one day; see [CONTRIBUTING.md](CONTRIBUTING.md#what-to-expect-from-us) for review targets.
|
||||
|
||||
## What OpenRig changes on your machine
|
||||
|
||||
OpenRig writes instance state, provider integration and workspace files as part
|
||||
of setup and operation. These include **trust settings and executable hooks**.
|
||||
The summary below follows this source revision; check `rig --version` when
|
||||
using a published package, since repository guidance can be ahead of npm.
|
||||
|
||||
| When | What changes and why |
|
||||
| --- | --- |
|
||||
| **npm installation** | Installs the CLI (`rig` and `openrig-tui`), bundled components and dependencies under your npm prefix (with Bun, under Bun's global directory). OpenRig's postinstall checks the Node.js version and that the SQLite module loads; Bun may block this script. It does not run daemon or provider setup. |
|
||||
| **`rig setup`** | Attempts missing tools and writes an OpenRig block in `~/.tmux.conf` for mouse support and scrollback. Installs herdr by default on macOS and Linux unless declined with `--no-herdr`; an unavailable herdr install is a warning. On macOS the installing agent offers Ghostty, and `--ghostty` attempts it after acceptance. Existing cmux settings are left unchanged. `--full` adds workstation tools. `--dry-run` shows setup's plan without applying it. |
|
||||
| **Daemon startup** | Creates/updates instance state under `OPENRIG_HOME` (normally `~/.openrig`), including its database and managed plugin resources. Seeds the `openrig-skills` discovery skill, the `refocusing` skill and the person-facing `rigs` skill in `~/.claude/skills` and `~/.agents/skills`, subject to existing version ownership. When a Codex seat is already recorded and `runtime.codex.hooks_enabled` is enabled (the default), ensures its hook configuration and trust records as described below. With no recorded Codex seat, enabled hooks wait for its first launch; disabled hooks still remove a stale OpenRig-managed hook block, if present. |
|
||||
| **Rig/seat launch and attachment** | Creates tmux sessions, supplies seat identity and daemon connection environment, and projects selected guidance, skills, plugins and runtime resources into the workspace. Managed startup pre-trusts the workspace. Claude context collection can also be provisioned for attached sessions and refreshed during monitoring. In a Git repository, newly created files under `.codex/plugins/shared:openrig-core/` (or the unqualified `.codex/plugins/openrig-core/`) are added to the repository's Git `info/exclude` inside an `# BEGIN OpenRig generated files` block; new `AGENTS.md`, `CLAUDE.md` and `CLAUDE.local.md` files stay visible with a warning. |
|
||||
| **Bundle install** (`rig bundle install`, or `rig up` with a `.rigbundle` or GitHub link) | Writes the bundle's files into the install target: `--target`, or the current directory for `rig up` and for a GitHub link. `rig bundle install` with a local archive needs `--target`. Routes its declared skills, plugins and context packs into your libraries. A GitHub link's archive is kept under `OPENRIG_HOME/bundle-imports/`, and installs are recorded in `OPENRIG_HOME/bundle-audit.jsonl`. A bundle that carries a project creates it under `workspace.projects_root` and records it, with the rig's association, in the workspace catalog. |
|
||||
| **Explicit permission configuration** | The built-in bootstrap does **not** add `rig` command allow rules to your settings files; a team seat with no permission policy, per-seat choice or (for Codex) named profile gets the per-launch [team default](docs/reference/rig-spec.md#team-launch-defaults) instead. Agent-guided setup recommends keeping it and requires your actual answer before the agent [adds remembered rules at your chosen scope](docs/reference/getting-started.md#have-your-agent-configure-permissions). No/no answer keeps the team default and preserves settings; existing choices and stricter rules remain relevant. Broader access is separate. |
|
||||
|
||||
The provider files are separate from instance state. Here `~` means the daemon
|
||||
user's home; changing `OPENRIG_HOME` alone does not isolate provider configuration.
|
||||
|
||||
- **Claude Code:** startup writes workspace trust and onboarding completion.
|
||||
With an explicit permission mode, it uses the launch-selected `HOME/.claude.json`,
|
||||
or `<CLAUDE_CONFIG_DIR>/.claude.json` when that variable is set. Classic startup
|
||||
writes `HOME/.claude.json`, and also `<CLAUDE_CONFIG_DIR>/.claude.json` when the
|
||||
daemon has that variable set. In the workspace, `.claude/settings.local.json` receives
|
||||
the context collector's `statusLine` command and selected activity hooks;
|
||||
a user's own status-line command is preserved, so it can prevent collector
|
||||
installation. Selected settings resources can still replace overlapping keys.
|
||||
The collector script lives under `.openrig/`, and the activity-hook relay under
|
||||
the instance's `state/claude-activity-hooks/`. Selected settings/MCP resources can also
|
||||
change that settings file and `.mcp.json`. The shared settings resource enables
|
||||
Exa/Context7 MCP entries and leaves existing `permissions.defaultMode` and permission
|
||||
rules unchanged. Managed launch flags select the session mode. Selected MCP resources
|
||||
configure those external services. Built-in bootstrap
|
||||
no longer writes a command allowlist to `~/.claude/settings.json` or removes
|
||||
older allowances. `CLAUDE_CONFIG_DIR` does not relocate the project-local writes.
|
||||
To keep your own status line alongside the collector, see
|
||||
[Custom Claude status line](docs/reference/claude-status-line.md).
|
||||
- **Codex:** writes the daemon's `CODEX_HOME/config.toml` (normally
|
||||
`~/.codex/config.toml`). With hooks enabled, daemon startup ensures the OpenRig
|
||||
activity relay commands and their trust hashes when a Codex seat is already
|
||||
recorded; otherwise the first Codex launch does so. With hooks disabled, startup
|
||||
can remove a stale OpenRig-managed hook block even without a Codex seat. Seat startup adds
|
||||
`trust_level = "trusted"` for the workspace; selected config resources can
|
||||
add MCP settings. Managed launches pass `-c check_for_update_on_startup=false`,
|
||||
which writes no config; a recognized update notice that still appears can be
|
||||
skipped, recording the skipped version in Codex's cache; this is not an update install.
|
||||
|
||||
Activity relays send event type/subtype, seat/runtime identity, timestamps and
|
||||
native session identity to the configured OpenRig daemon's `/api/activity/hooks`
|
||||
endpoint, using its activity token. That payload excludes prompt text and tool
|
||||
arguments. Claude's collector writes context/token usage, session/transcript-path
|
||||
metadata and available rate-limit data to the instance's `state/context-usage`
|
||||
and `state/provider-usage`. Provider and selected MCP connections have their own
|
||||
data flows. Daemon plugin initialization also checks the OpenRig plugin release
|
||||
endpoint on GitHub.
|
||||
|
||||
Managed launches supply `HOME`, `CODEX_HOME` and `OPENRIG_*` identity/connection
|
||||
variables. Claude uses `--permission-mode acceptEdits` and defaults to the classic
|
||||
renderer for terminal scrollback. Codex uses `-s workspace-write` unless a named
|
||||
profile governs its sandbox; the default does not force an approval-policy flag.
|
||||
On that plain launch OpenRig first asks Codex for its own configuration and adds
|
||||
`-c sandbox_workspace_write.network_access=true` only when Codex answers that no
|
||||
configuration layer sets network access and no managed requirement could restrict
|
||||
it, so the seat can reach the local daemon. If Codex can't be asked or doesn't
|
||||
answer in time, the launch is left unchanged.
|
||||
Fresh Codex launches also add writable access to the workspace's `.git` and the
|
||||
pod's shared queue-state directory with `--add-dir`; the shared root comes from
|
||||
`OPENRIG_SHARED_DOCS_ROOT` or `~/.openrig/shared-docs`.
|
||||
A team seat with no permission policy, per-seat choice or (for Codex) named
|
||||
profile also gets the per-launch team default: Claude gets `--settings` allowing
|
||||
ordinary `rig` commands, project reads and common tests, with a session-only
|
||||
PreToolUse hook that asks before lifecycle commands, and Codex also gets the
|
||||
OpenRig workspace root as a writable directory. Neither writes a permission file.
|
||||
Seats of the rig named `kernel` launch with an operational default instead,
|
||||
unless a permission policy, a per-seat choice or (for Codex) a named profile
|
||||
applies: Claude in `acceptEdits` with a per-launch allow list for its file tools,
|
||||
web fetches and operational commands (including reads under your home folder),
|
||||
and Codex with `-s danger-full-access -a never`. Neither writes a permission file.
|
||||
YOLO is **off by default**. An explicitly selected full-bypass policy selects
|
||||
Claude's `--dangerously-skip-permissions` or Codex's
|
||||
`-s danger-full-access -a never`. The legacy environment-only `OPENRIG_YOLO=1`
|
||||
path, with no policy attached, selects Claude's bypass flag, Pi's `--approve` and
|
||||
Codex's `-s danger-full-access` without `-a never`; a resolved policy overrides
|
||||
that environment setting. The `builtin:auto` policy launches Claude with
|
||||
`--permission-mode auto`; Codex and Pi seats launch as they do without a policy (a
|
||||
named Codex profile still governs its sandbox, and Pi keeps its configured
|
||||
resource trust).
|
||||
`--non-interruptive` on `rig up` or `rig bundle install` additionally passes
|
||||
per-launch flags to full-bypass seats so Claude's bypass warning and Codex's
|
||||
full-access and GPT-5.1 migration notices don't stop startup; it writes nothing to
|
||||
their settings files and is saved on the rig. See
|
||||
[non-interruptive mode](docs/reference/non-interruptive-mode.md).
|
||||
|
||||
Permission mode controls native execution permissions; work posture is separate
|
||||
project guidance. Use `rig policy permissions list|show|current|apply` for rig
|
||||
policy configuration (the four `rig policy` aliases remain compatible). Use
|
||||
`rig seat set-permissions <seat> --mode <mode> --reason <text>` for an audited
|
||||
future-launch choice: `floor`, `full_bypass`, or `inherit` to clear the seat
|
||||
override. Additional Claude modes such as `auto` require support from the exact
|
||||
managed Claude executable at the seat's working directory; selection and launch
|
||||
each check it. Unsupported or changed contexts refuse without a fallback.
|
||||
This does not relaunch the seat
|
||||
or change its current native process, history, rules or hooks. `rig seat status`
|
||||
separates the desired selection from the last launch arguments; neither proves
|
||||
native enforcement. See the [permission guide](docs/reference/getting-started.md#per-seat-permission-mode).
|
||||
|
||||
Managed hook blocks target OpenRig's entries and retain unrelated hooks, but
|
||||
trust entries and selected resource keys can be replaced. Claude's collector,
|
||||
activity-hook and JSON resource writers leave unreadable or malformed settings
|
||||
untouched; this is not a product-wide preservation guarantee. Codex's workspace
|
||||
trust writer can treat an unreadable `config.toml` as empty and write a new trust
|
||||
configuration. Read-modify-write operations can also race other writers; there
|
||||
is no complete preservation or rollback guarantee. Back up relevant
|
||||
files before first use. Daemon/bootstrap writes are automatic and do not each
|
||||
have an interactive preview; `rig setup --dry-run` does not preview every later
|
||||
startup effect.
|
||||
> From here, give the intake agent a packet of work and watch it move through planning, build, review, and closeout.
|
||||
|
||||
## What It Does
|
||||
|
||||
@@ -320,86 +44,75 @@ OpenRig is a multi-agent harness — it manages the system that coding agents fo
|
||||
|
||||
- **Define** topologies in YAML (RigSpec) with pods, edges, and continuity policies
|
||||
- **Boot** everything with `rig up` — tmux sessions, harnesses, startup files, readiness checks
|
||||
- **See** rigs, pods, and seats in the TUI topology table and graph; inspect projects, specs, feeds, and instance health
|
||||
- **See** the topology in a live graph with explorer, node detail, and system log
|
||||
- **Discover** existing Claude Code and Codex sessions in tmux and adopt them into a managed rig
|
||||
- **Snapshot** the topology with `rig down --snapshot`, restore by name with `rig up <name>`
|
||||
- **Snapshot** the full topology on `rig down`, restore by name with `rig up <name>`
|
||||
- **Communicate** across agents with `rig send`, `rig broadcast`, and `rig chatroom`
|
||||
- **Protect** a seat where you type by hand: `rig seat set-typing-guard <seat> --enabled true --reason <text>` holds automatic messages and wakes instead of typing them into that seat (off by default; see `rig seat set-typing-guard --help`)
|
||||
- **Connect** Slack through an app you create in your own workspace; the experimental `rig slack manifest` prints that app's manifest ([setup guide](docs/reference/slack-app-setup.md))
|
||||
- **Evolve** running topologies with `rig grow`, `rig shrink`, `rig launch`, `rig remove`
|
||||
- **Evolve** running topologies with `rig expand`, `rig shrink`, `rig launch`, `rig remove`
|
||||
|
||||
Every agent runs in a tmux session you can attach to, inspect, and work with directly.
|
||||
|
||||
## Teams
|
||||
## Starter Rigs
|
||||
|
||||
Three teams grow with you, and the kernel operator helps you pick one:
|
||||
|
||||
- **`starter`**: a builder and a reviewer for one bounded change.
|
||||
- **`workshop`**: a lead, a builder, QA and a reviewer for ongoing work in one repository. It's a rig bundle, installed from its pinned listing.
|
||||
- **`factory`**: seven agents (a lead, an advisor, build, QA, design and two independent reviewers) for sustained product work. You bring ideas to the advisor, and the lead runs the team.
|
||||
OpenRig ships with a conveyor starter rig for learning workflow handoff:
|
||||
|
||||
```bash
|
||||
rig specs preview factory --kind rig
|
||||
rig up factory
|
||||
rig specs preview conveyor --kind rig
|
||||
```
|
||||
|
||||
Specialist teams: `code-review` (two independent reviews), `research` (an analyst and a synthesizer) and `pm` (a product lead, a researcher and a builder for prototypes).
|
||||
```
|
||||
conveyor (rig, pod_aware)
|
||||
Starter workflow rig: a station pipeline that can move multiple
|
||||
work packets at once, with queue depth acting as natural backpressure.
|
||||
|
||||
Also built in: `secrets-manager`, a HashiCorp Vault instance run by a specialist agent.
|
||||
Pod: intake (1 member)
|
||||
lead — claude-code
|
||||
Pod: plan (1 member)
|
||||
planner — codex
|
||||
Pod: build (1 member)
|
||||
builder — claude-code
|
||||
Pod: review (1 member)
|
||||
reviewer — codex
|
||||
```
|
||||
|
||||
Browse the library:
|
||||
For a larger human-operated product-development topology, inspect `product-team`:
|
||||
|
||||
```bash
|
||||
rig specs ls
|
||||
rig specs preview product-team
|
||||
```
|
||||
|
||||
Also ships: `implementation-pair`, `adversarial-review`, `research-team`, and `secrets-manager` (HashiCorp Vault managed by a specialist agent).
|
||||
|
||||
Browse the library: `rig specs ls`
|
||||
|
||||
## How It Works
|
||||
|
||||
OpenRig is a local daemon + CLI + terminal UI + MCP server, built on tmux. OpenRig itself is under active development; only the older React web UI is in maintenance mode, with best-effort support.
|
||||
OpenRig is a local daemon + CLI + MCP server + React UI, built on tmux.
|
||||
|
||||
```
|
||||
CLI / TUI / MCP
|
||||
CLI / UI / MCP
|
||||
|
|
||||
Hono HTTP daemon
|
||||
|
|
||||
Domain services
|
||||
Domain services (52)
|
||||
|
|
||||
SQLite + tmux + runtime adapters
|
||||
```
|
||||
|
||||
- **CLI**: Commands for both humans and agents to launch teams, inspect state, send messages, track owned work, and manage context.
|
||||
- **TUI**: Topology explorer, table and graph views, seat details, Specs, Projects, Terminals, Feed, and System. Navigate with the keyboard, mouse, or command bar.
|
||||
- **MCP**: Tools so agents can manage their own topology (`rig_up`, `rig_ps`, `rig_send`, `rig_chatroom_send`, etc.)
|
||||
- **Runtimes**: Native Claude Code and Codex sessions, terminal nodes, and Pi and Oh My Pi via RPC runners.
|
||||
|
||||
## Terminal UI and Workspaces
|
||||
|
||||
The TUI shows the team's coordination state; herdr and cmux show the actual agent terminals alongside it. Use `rig tui commands` to list the TUI's command-bar navigation, or [try the interactive TUI tour](https://openrig.dev/tour/workspace).
|
||||
|
||||

|
||||
|
||||
*Captured from the interactive TUI demo using fictional project data.*
|
||||
|
||||
With herdr installed and connected, open the starter's terminals together:
|
||||
|
||||
```bash
|
||||
rig terminal open starter --provider herdr
|
||||
```
|
||||
|
||||
For cmux, use `--provider cmux`. In the TUI, a rig's detail view has an `Open terminals ▸ rig <name>` link that opens a terminal view of the running seats of that rig (in a new window or tab, or the current Herdr session), laid out with herdr when installed, otherwise plain tmux; with herdr that is up to 16 seats per tab, in a workspace named after the rig. A seat it can't attach is reported as absent or degraded. The underlying sessions remain accessible through tmux. See the [terminal workspace guide](docs/reference/getting-started.md#share-the-dashboard-and-return-to-it) for setup and returning to an existing view.
|
||||
- **CLI**: 40+ commands designed for both humans and agents. Every mutating command ends with what happened, current state, and next action.
|
||||
- **UI**: Explorer sidebar, topology graph with pod grouping, node detail panel, system log, chatroom.
|
||||
- **MCP**: 17 tools so agents can manage their own topology (`rig_up`, `rig_ps`, `rig_send`, `rig_chatroom_send`, etc.)
|
||||
- **Runtimes**: Claude Code, Codex, and terminal nodes. Adapters for Pi and OpenCode in development.
|
||||
|
||||
## Key Concepts
|
||||
|
||||
The first words you meet, in plain language: [What these words mean](docs/reference/quickstart.md#what-these-words-mean).
|
||||
|
||||
- **RigSpec**: Declarative multi-agent harness definition in YAML. Pods, members, edges, continuity policies, culture file.
|
||||
- **AgentSpec**: Reusable agent blueprint with skills, guidance, hooks, profiles, and startup contracts.
|
||||
- **Seat**: A stable role and address in a rig, such as `dev-build@starter`. The conversation occupying it can change while its identity and authored context remain.
|
||||
- **Pod**: A group of related seats with shared guidance and context. Each agent still has its own context window.
|
||||
- **Pod**: Bounded context group. Agents in a pod share memory and can maintain each other's context.
|
||||
- **Discovery**: `rig discover` fingerprints existing tmux sessions. `rig adopt` brings them under management.
|
||||
- **Snapshot/Restore**: `rig down --snapshot` captures full state. `rig up <name>` restores from latest snapshot. Restore reports per-node outcomes: resumed, fresh-primed, awaiting-decision (the original conversation can't resume; choose `--fresh`), attention_required, or failed.
|
||||
- **RigBundle**: Portable archive with vendored AgentSpecs and SHA-256 integrity. Share topologies across machines. `rig up`, `rig bundle create`, `inspect` and `install` also take a GitHub folder link, `rig bundle check` checks a folder before you share it, and `rig bundle inspect` shows what a bundle will do before you install it. See [publishing a rig bundle](docs/reference/publishing-a-rig-bundle.md).
|
||||
- **Culture**: A rig's `CULTURE.md` (its `culture_file`) sets coordination norms for the group, on top of OpenRig's default culture, which every seat receives.
|
||||
- **Snapshot/Restore**: `rig down --snapshot` captures full state. `rig up <name>` restores from latest snapshot. Restore reports per-node outcomes (resumed, fresh, or failed).
|
||||
- **RigBundle**: Portable archive with vendored AgentSpecs and SHA-256 integrity. Share topologies across machines.
|
||||
- **Culture**: CULTURE.md sets coordination norms for the group. Research rigs get exploratory culture. Implementation rigs get conservative, trust-but-verify culture.
|
||||
|
||||
## Agent-Managed Software
|
||||
|
||||
@@ -413,121 +126,47 @@ rig send vault-specialist@secrets-manager "Check Vault health and report status.
|
||||
|
||||
Requires Docker for service-backed rigs.
|
||||
|
||||
## Upgrading an existing instance
|
||||
|
||||
For an existing installation, follow the [upgrade procedure](skills/_canonical/core/openrig-upgrade/SKILL.md) and the [0.5.14 release notes](docs/releases/v0.5.14.md); notes for later releases are in [CHANGELOG.md](CHANGELOG.md). Preserve live seats during the upgrade; `rig down` is not an upgrade step. Upgrading to 0.6.0 also requires Node.js 22 or 24: see [Moving off Node 20](#moving-off-node-20) and the [0.6.0 release notes](docs/releases/v0.6.0.md).
|
||||
|
||||
### Moving off Node 20
|
||||
|
||||
OpenRig 0.6.0 supports Node.js 22 and 24 only. Its SQLite binding
|
||||
(better-sqlite3 13) requires Node 22 or newer. Node 20 is no longer supported;
|
||||
the install check refuses it with an explanation.
|
||||
|
||||
If you run OpenRig on Node 20, switch Node first, then reinstall the CLI under
|
||||
the new Node (a version manager keeps a separate global package set for each
|
||||
Node):
|
||||
|
||||
```bash
|
||||
nvm install 22 # or 24; fnm or your package manager work the same way
|
||||
npm install -g @openrig/cli
|
||||
rig --version
|
||||
```
|
||||
|
||||
Your existing OpenRig data stays where it is. The daemon reopens the same
|
||||
database under the new binding and applies any pending migrations in place.
|
||||
Restart the daemon under the new Node by following the upgrade procedure above.
|
||||
|
||||
### Crossing the 0.5.9 layout boundary
|
||||
|
||||
The migration below still applies when upgrading from a pre-0.5.9 instance.
|
||||
|
||||
0.5.9 makes `$OPENRIG_HOME/context` the addressable context library, writes
|
||||
Claude telemetry to `state/context-usage` (and provider telemetry to
|
||||
`state/provider-usage`), and installs the default System World at
|
||||
`context/system/system-world.yaml`. Existing instances cross this boundary by
|
||||
an **Agent-Operated Migration** from the shipped `openrig-upgrade` skill. The
|
||||
target runtime reads canonical-first with legacy-fallback while new writes use
|
||||
the canonical roots; a custom context-library root stays stable during
|
||||
activation. This is not a directory rename to do while an old collector writes.
|
||||
|
||||
```bash
|
||||
# SKILL_DIR is the installed openrig-upgrade skill directory.
|
||||
node "$SKILL_DIR/scripts/migrate-telemetry-state-0.5.9.mjs" --help
|
||||
node "$SKILL_DIR/scripts/migrate-telemetry-state-0.5.9.mjs" --home "$OPENRIG_HOME"
|
||||
node "$SKILL_DIR/scripts/migrate-telemetry-state-0.5.9.mjs" --home "$OPENRIG_HOME" --apply-state --preimage /safe/path/layout-0.5.9-before
|
||||
|
||||
# Activate the exact target runtime separately. After every bounded legacy tail is followed by newer paired samples at both new state roots:
|
||||
node "$SKILL_DIR/scripts/migrate-telemetry-state-0.5.9.mjs" --home "$OPENRIG_HOME" --verify --preimage /safe/path/layout-0.5.9-before > /safe/path/layout-0.5.9-verify.json
|
||||
|
||||
# Run the separately invoked non-destructive finalizer only with that exact receipt:
|
||||
node "$SKILL_DIR/scripts/migrate-telemetry-state-0.5.9.mjs" --home "$OPENRIG_HOME" --apply-library --preimage /safe/path/layout-0.5.9-before --verification /safe/path/layout-0.5.9-verify.json
|
||||
|
||||
# Restore only helper-owned preparation/finalizer effects if the observed upgrade must be reversed:
|
||||
node "$SKILL_DIR/scripts/migrate-telemetry-state-0.5.9.mjs" --home "$OPENRIG_HOME" --rollback /safe/path/layout-0.5.9-before
|
||||
```
|
||||
|
||||
`--help` prints the phase grammar without inventorying the instance. No phase
|
||||
flag intentionally runs the read-only plan; unknown options fail nonzero before
|
||||
plan or mutation.
|
||||
|
||||
Every phase emits JSON. Stop on any issue or incomplete receipt and follow its
|
||||
`next` action; do not continue from copied legacy telemetry or retry a partial
|
||||
mutation blindly. Preparation leaves legacy state and collector settings in
|
||||
place. Verification accepts exact tail bytes only when that same seat has newer
|
||||
paired context and provider samples under `state/`; finalization revalidates the
|
||||
accepted tails, copies the library without overwrite, and switches config last.
|
||||
The helper never removes the legacy telemetry or library. Retirement follows
|
||||
separate stable runtime, writer, reader, and recovery proof. Daemon, database,
|
||||
seat, plugin, and release lifecycle actions remain agent-owned.
|
||||
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js 22 or 24 (the supported versions in this release). Node 20 is no
|
||||
longer supported. Node 26 and other versions are untested. On a Mac with Apple
|
||||
silicon, use Node.js 22: see the [compatibility history](docs/releases/v0.5.15.md#known-compatibility-limitation).
|
||||
- Node.js 20, 22, or 24 (even-numbered LTS releases; odd releases like 25 lack native addon prebuilds)
|
||||
- tmux
|
||||
- macOS or Linux. On Windows, use WSL2, the Windows route OpenRig supports; native
|
||||
Windows isn't supported. OpenRig's automated tests don't run on WSL2 yet; one user's
|
||||
working setup is in the [getting-started guide](docs/reference/getting-started.md#wsl2-a-reported-working-setup)
|
||||
|
||||
Optional:
|
||||
- herdr or cmux for terminal workspaces showing the agents together
|
||||
- cmux for `Open CMUX` node surface controls
|
||||
- Docker for service-backed rigs and managed apps
|
||||
|
||||
## Setup and Troubleshooting
|
||||
|
||||
- `rig setup` attempts core machine preparation: it installs a missing Claude Code or Codex and writes tmux defaults. It installs Herdr by default on macOS and Linux; use `--no-herdr` if you decline it. On macOS it also installs a missing tmux with an existing Homebrew; elsewhere install tmux yourself (setup only checks it). On a Mac, the installing agent offers Ghostty once; accepting uses `rig setup --ghostty`, and declining leaves Terminal.app available. Existing cmux settings are preserved. It reports what it tried and what actually succeeded. If something fails, it gives the local agent enough context to finish the job.
|
||||
- `rig setup --full` attempts a broader operator workstation setup (jq, gh, via Homebrew, so macOS only) on top of core.
|
||||
- `rig doctor` inspects current system health and helps diagnose problems after setup. Use it when something stops working or after machine changes. It shares setup's local provider login checks. These do not prove provider acceptance or a working agent; also inspect `rig ps --nodes --rig <rig>`.
|
||||
- `rig setup` attempts core machine preparation: tmux, cmux, Claude Code, Codex, and tmux defaults. It reports what it tried and what actually succeeded. If something fails, it gives the local agent enough context to finish the job.
|
||||
- `rig setup --full` attempts a broader operator workstation setup (jq, gh) on top of core.
|
||||
- `rig doctor` inspects current system health and helps diagnose problems after setup. Use it when something stops working or after machine changes.
|
||||
|
||||
Both commands support `--json` for agent-driven workflows.
|
||||
|
||||
Before setup or managed launch, review [what OpenRig changes on your machine](#what-openrig-changes-on-your-machine), including provider trust, hooks and selected runtime resources.
|
||||
Managed runtime boot (during `rig up`) may modify runtime config for core bootstrap and spec-selected runtime resources. `rig setup` discloses these paths so agents know what may be changed:
|
||||
- global Claude: `~/.claude/settings.json` for minimal OpenRig command allowlisting
|
||||
- global Claude state: `~/.claude.json` for managed workspace trust and onboarding completion
|
||||
- project Claude: `.claude/settings.local.json` for context collector/activity hooks and selected `claude_settings_fragment` resources
|
||||
- project Claude MCP: `.mcp.json` for selected `claude_mcp_fragment` resources
|
||||
- global Codex: `~/.codex/config.toml` for workspace trust and selected `codex_config_fragment` resources
|
||||
|
||||
Already-running adopted sessions may need restart before they pick up newly written runtime config.
|
||||
|
||||
**For agents:** First ask which available provider(s) the user wants, reusing an explicit choice, and pass it to setup (`rig setup --providers claude`, `codex` or `both`). Recommend the working account they already have and follow the selected-provider guide above. Offer core setup (`rig setup`) or the fuller workstation path (`rig setup --full`) only when they want those additional tools. Inspect the result with `--json` and use `rig doctor` to finish any remaining machine-specific issues. Starting OpenRig and opening its view are the last steps of installing, not extras, so do them without stopping to ask whether to go on: start the daemon if it is stopped, open the OpenRig TUI and operator for them (say so, then run `rig terminal open saved:kernel --window --json`; no commands for the person to copy), and hand the person's goal and project folder to the kernel operator ([installing-agent handoff](docs/reference/getting-started.md#installing-agent-handoff)). Installation is finished when the person is talking to the operator; don't implement the project yourself. When OpenRig itself misbehaves, read [docs/reference/help.md](docs/reference/help.md) (installed agents can run `rig context get help`; the same text is at [openrig.dev/help/agents](https://www.openrig.dev/help/agents)): it covers the next step, known problems, and how to reach the team if you're still stuck.
|
||||
**For agents:** Ask the user whether they want core setup (`rig setup`) or the fuller workstation path (`rig setup --full`) before choosing the invocation. Inspect the result with `--json` and use `rig doctor` to finish any remaining machine-specific issues.
|
||||
|
||||
## Comparison with Claude Managed Agents
|
||||
|
||||
OpenRig is open source and self-hosted, with Claude Code and Codex in the same team. You operate it on your own infrastructure; the selected providers' model usage costs still apply.
|
||||
Anthropic shipped Claude Managed Agents — a cloud-hosted, Claude-only runtime at $0.08/session-hour. OpenRig is the local side: open source, cross-harness, runs on your machine, costs nothing.
|
||||
|
||||
[Full comparison](https://openrig.dev/compare/claude-managed-agents)
|
||||
|
||||
## Links
|
||||
|
||||
- **Website**: [openrig.dev](https://openrig.dev)
|
||||
- **Docs**: [openrig.dev/docs](https://openrig.dev/docs) ([documentation index for agents](https://openrig.dev/llms.txt))
|
||||
- **Blog**: [openrig.dev/blog](https://openrig.dev/blog) · [Why I Built OpenRig](https://esoteric.run/blog/why-i-built-openrig)
|
||||
- **Blog**: [Why I Built OpenRig](https://esoteric.run/blog/why-i-built-openrig)
|
||||
- **Docs**: [openrig.dev/docs](https://openrig.dev/docs)
|
||||
- **Open Specification**: [openrig.dev/specs](https://openrig.dev/specs)
|
||||
- **Videos**: [youtube.com/@openrig](https://www.youtube.com/@openrig)
|
||||
- **X**: [@_feralmachine](https://twitter.com/_feralmachine)
|
||||
- **Follow the project**: [openrig.dev/follow](https://openrig.dev/follow)
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#mvschwarz/openrig&Date)
|
||||
- **Twitter**: [@_feralmachine](https://twitter.com/_feralmachine)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
# Roadmap
|
||||
|
||||
These are things we'd welcome help with. Most came from the community, and most are small enough for one pull request.
|
||||
This isn't a schedule or a promise: we release often, and an item ships when a good change for it lands.
|
||||
|
||||
To see what's being worked on, by release and theme, open the [OpenRig roadmap board](https://github.com/users/mvschwarz/projects/1).
|
||||
Issues labelled `good first issue` are also listed on the [contribute page](https://github.com/mvschwarz/openrig/contribute).
|
||||
|
||||
## Released in 0.7.10
|
||||
|
||||
[0.7.10 is published](https://github.com/mvschwarz/openrig/releases/tag/v0.7.10): it is a patch for people running their own agent teams. A step held after a managed compaction now goes in instead of waiting forever. See its release notes for what shipped, known issues, upgrade instructions and contributor credits.
|
||||
|
||||
## How to help
|
||||
|
||||
1. Pick an item and comment on its issue to say you're working on it and coordinate with others. A maintainer can assign
|
||||
you to make that work visible. If it has no issue yet, open one first.
|
||||
2. Read `CONTRIBUTING.md`, and ask your agent to use the `developing-openrig` skill. It's a map of how OpenRig is
|
||||
built, tested and changed.
|
||||
3. Open a pull request that says what a user gets and how you verified it.
|
||||
|
||||
If more than one pull request arrives for the same item, we take the best one, and the first equivalent one breaks a
|
||||
tie. Feel free to coordinate on the issue first.
|
||||
|
||||
## Testing
|
||||
|
||||
- **Behavioural scenarios for more command families.** `docs/as-built/test-layers.md` has a "Help wanted" list of
|
||||
`rig` command families that have no scenario yet, each with a suggested first check. One family per pull request is
|
||||
ideal.
|
||||
|
||||
## Agents and runtimes
|
||||
|
||||
- **Operator recovery for Pi and Oh My Pi seats** (#41).
|
||||
- **More harnesses:** we're working out one way for any coding-agent CLI to plug into OpenRig. More on this soon
|
||||
(design discussion: #43).
|
||||
|
||||
## Slack
|
||||
|
||||
- **A DM or private channel for single-user setups** (#313).
|
||||
|
||||
## Reliability and everyday use
|
||||
|
||||
- **Draft-aware delivery** (#48): automated sends wait while someone is typing in a seat, or a seat can be set to
|
||||
inbox-only.
|
||||
- **A host-aware resource budget for seats** (#80).
|
||||
- **Predictable `rig up` over a stopped rig with the same name.**
|
||||
- **Codex seats that leave a repository's tracked `AGENTS.md` alone** (related: #64).
|
||||
- **Install and run on Node 26.**
|
||||
|
||||
Bugs aren't listed here. Please file them as issues; anything labelled `good first issue` is a good place to start.
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
# Security policy
|
||||
|
||||
## Supported versions
|
||||
|
||||
Only the latest published version on npm (`@openrig/cli`) is supported. Fixes are developed on
|
||||
`main` and released after verification; a merged fix is not necessarily in the published package.
|
||||
Include the affected version even when reporting a problem in an older installation.
|
||||
|
||||
## What OpenRig touches
|
||||
|
||||
OpenRig runs a local daemon, drives coding agents in tmux sessions on your machine, and writes
|
||||
configuration for those harnesses, including workspace trust records and executable hooks: for
|
||||
example `~/.claude.json`, skills under `~/.claude`, `$CODEX_HOME/config.toml` (default `~/.codex`),
|
||||
and, in the project, `.claude/settings.local.json`, `.mcp.json` and managed blocks in `CLAUDE.md`,
|
||||
`CLAUDE.local.md` or `AGENTS.md`. `rig setup` also edits `~/.tmux.conf` and, on macOS, cmux's
|
||||
settings. OpenRig's own state lives under `$OPENRIG_HOME` (default `~/.openrig`). The README
|
||||
section [What OpenRig changes on your machine](README.md#what-openrig-changes-on-your-machine)
|
||||
describes these effects and the trust/permission choices. Unexpected access, disclosure or
|
||||
permission changes are useful reports; include what you expected and what you observed.
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
Please do not open a public issue for a vulnerability.
|
||||
|
||||
Open the repository's [Security page](https://github.com/mvschwarz/openrig/security). If
|
||||
**Report a vulnerability** is available, use it to send a private report to the maintainers.
|
||||
The reporter and authorized advisory collaborators can also see that report; it is not public.
|
||||
Availability depends on the repository setting, not on this file being present.
|
||||
|
||||
If that option is unavailable, ask in [Discussions Q&A](https://github.com/mvschwarz/openrig/discussions/categories/q-a)
|
||||
for a private security contact. Post only the request for a channel: do not include the
|
||||
vulnerability details, reproduction, logs, credentials or affected private systems in public.
|
||||
Wait for the private channel before sharing the report.
|
||||
|
||||
Include: the OpenRig version, the harnesses involved, the steps to reproduce, and what an attacker
|
||||
could do. A proof of concept is welcome; a working exploit against a third party's machine is not.
|
||||
|
||||
## What to expect
|
||||
|
||||
- We will acknowledge the report and coordinate assessment and next steps privately.
|
||||
- Assessment and fix timing depend on impact and reproduction; no fixed deadline is promised.
|
||||
- We will agree on disclosure and credit with you. You can ask to remain anonymous.
|
||||
|
||||
## Scope notes
|
||||
|
||||
- OpenRig is designed for your own machine or a trusted private network, for you and people you trust, not for the
|
||||
open internet. We assess reports by their practical impact on a user in that setup. Exposing an install to the open
|
||||
internet isn't a supported deployment. OpenRig's own gateway integrations with outside services, such as Slack, and
|
||||
anything that lets an untrusted party reach the daemon or an agent are in scope.
|
||||
- OpenRig assumes the machine and the accounts it runs under are trusted by their owner. Reports
|
||||
that require a hostile local user with the same account are still welcome but are unlikely to be
|
||||
treated as high severity.
|
||||
- A harness carrying out an intentionally authorized operation is not by itself evidence of an
|
||||
OpenRig vulnerability. Reports of OpenRig bypassing a chosen permission, changing trust
|
||||
unexpectedly, or routing data or actions to the wrong destination are in scope.
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 MiB |
@@ -2,8 +2,8 @@
|
||||
|
||||
Source capture:
|
||||
- packaged OpenRig UI on the clean rerun against the authenticated VM lane
|
||||
- host-forwarded UI: `http://127.0.0.1:<host-port>`
|
||||
- guest daemon: `<guest-ip>:<guest-port>`
|
||||
- host-forwarded UI: `http://127.0.0.1:17439`
|
||||
- guest daemon: `192.168.64.9:7433`
|
||||
|
||||
Purpose:
|
||||
- README walkthrough
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 71 KiB |
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"url": "https://context7.com/mvschwarz/openrig",
|
||||
"public_key": "pk_HcRwWEoFBsmU1mo5VIvWw",
|
||||
"projectTitle": "OpenRig",
|
||||
"description": "Build your own network of agents from Claude Code, Codex and Pi: persistent teams with roles, shared context and owned work.",
|
||||
"folders": [],
|
||||
"excludeFolders": [
|
||||
"docs/releases",
|
||||
"packages/daemon/specs",
|
||||
"packages/daemon/test",
|
||||
"packages/test-system",
|
||||
"skills/_canonical/process",
|
||||
"skills/_canonical/pm",
|
||||
"spike",
|
||||
"docker",
|
||||
"**/node_modules",
|
||||
"**/fixtures"
|
||||
],
|
||||
"excludeFiles": [],
|
||||
"rules": [
|
||||
"Install with npm install -g @openrig/cli. OpenRig requires Node.js 22 or 24, tmux, and Claude Code or Codex signed in. The starter team uses both, and the kernel operator adapts it when you have only one.",
|
||||
"Preview before launching: rig specs preview <spec>, then rig up <spec> --cwd . --plan, then rig up <spec> --cwd .",
|
||||
"Check seat readiness with rig ps --nodes --rig <rig>, not only daemon health from rig status.",
|
||||
"Inside a rig, an agent runs rig whoami --json first and treats its output as its identity.",
|
||||
"rig send <session> \"message\" types into a peer's terminal and presses Enter; rig capture <session> reads that peer's screen.",
|
||||
"Work another agent must act on goes in the queue (rig queue create or rig queue handoff); a message informs but does not assign work.",
|
||||
"Use rig start to bring rigs back after a reboot or daemon restart.",
|
||||
"Run rig --help for the current command surface; the CLI is authoritative over older examples."
|
||||
]
|
||||
}
|
||||
+2
-2
@@ -80,7 +80,7 @@ This produces automated proof artifacts in `demo/proof/`:
|
||||
| Artifact | Source | Type |
|
||||
|----------|--------|------|
|
||||
| `up-transcript.txt` | `rig up demo/rig.yaml` output | Automatic |
|
||||
| `ps-nodes.txt` | `rig ps --nodes --rig <rig>` after boot | Automatic |
|
||||
| `ps-nodes.txt` | `rig ps --nodes` after boot | Automatic |
|
||||
| `health-after-boot.json` | `check-demo-health.ts` after boot | Automatic |
|
||||
| `native-resume-after-boot.txt` | immediate native probe after boot | Automatic |
|
||||
| `native-resume-after-boot.json` | immediate native probe machine output | Automatic |
|
||||
@@ -91,7 +91,7 @@ This produces automated proof artifacts in `demo/proof/`:
|
||||
| `down-transcript.txt` | `rig down` output | Automatic |
|
||||
| `tmux-check.txt` | `tmux ls` after teardown | Automatic |
|
||||
| `restore-transcript.txt` | `rig restore <snapshotId> --rig <rigId>` output | Automatic |
|
||||
| `ps-restored.txt` | `rig ps --nodes --rig <rig>` after restore | Automatic |
|
||||
| `ps-restored.txt` | `rig ps --nodes` after restore | Automatic |
|
||||
| `browser-screenshot.png` | Explorer + Graph + Detail Panel | **Manual** |
|
||||
| `resume-test.txt` | Post-restore agent context check | **Manual** |
|
||||
|
||||
|
||||
+2
-3
@@ -35,8 +35,7 @@ echo ""
|
||||
|
||||
# 2. Node status
|
||||
echo "Step 2: Node status after boot..."
|
||||
# OPR.0.4.4.21: --nodes requires an explicit target outside a managed session.
|
||||
rig ps --nodes --rig "$RIG_ID" 2>&1 | tee "$PROOF_DIR/ps-nodes.txt"
|
||||
rig ps --nodes 2>&1 | tee "$PROOF_DIR/ps-nodes.txt"
|
||||
echo ""
|
||||
|
||||
# 3. Health check after boot
|
||||
@@ -96,7 +95,7 @@ echo ""
|
||||
|
||||
# 10. Node status after restore
|
||||
echo "Step 10: Node status after restore..."
|
||||
rig ps --nodes --rig "$RIG_ID" 2>&1 | tee "$PROOF_DIR/ps-restored.txt"
|
||||
rig ps --nodes 2>&1 | tee "$PROOF_DIR/ps-restored.txt"
|
||||
echo ""
|
||||
|
||||
echo "=== Automated proof artifacts produced ==="
|
||||
|
||||
+2
-3
@@ -32,8 +32,7 @@ fi
|
||||
# Show node status
|
||||
echo ""
|
||||
echo "Node status:"
|
||||
# OPR.0.4.4.21: --nodes requires an explicit target outside a managed session.
|
||||
rig ps --nodes --rig "$RIG_ID"
|
||||
rig ps --nodes
|
||||
|
||||
# Establish a restore-safe baseline for the demo rig.
|
||||
echo ""
|
||||
@@ -56,6 +55,6 @@ echo "=== Demo topology is running ==="
|
||||
echo "Dashboard: http://localhost:5173"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " rig ps --nodes --rig '$RIG_ID' # Check node status"
|
||||
echo " rig ps --nodes # Check node status"
|
||||
echo " rig down $RIG_ID # Tear down (auto-snapshots)"
|
||||
echo " rig restore <snapshotId> --rig $RIG_ID # Restore the exact snapshot you just created"
|
||||
@@ -1,69 +0,0 @@
|
||||
# 51-04 testbed image (openrig-testbed) — see IMPL-PLAN §1. Built HOST-side by
|
||||
# scripts/build-testbed-image.sh, which resolves + records the base digest, pins node, stages the
|
||||
# stub assets, and COPYs the local openrig pack tarball. Byte-reproducible BY CONTRACT: the base is
|
||||
# digest-pinned (the verb refuses a tag-floating reference), node is pinned, and openrig comes from
|
||||
# the tree — never the npm registry (0.5.1 is unreleased). Layers are ordered stable -> volatile.
|
||||
|
||||
# --- base: a digest-pinned LTS-slim Linux, supplied by the build verb (docker/testbed/base-image,
|
||||
# which REFUSES a non-@sha256 reference). No floating tag is baked into this file. ---
|
||||
ARG BASE_IMAGE
|
||||
FROM ${BASE_IMAGE}
|
||||
|
||||
# --- layer 1: userland + tmux + git + PTY/process essentials + the native-build toolchain (most stable) ---
|
||||
# python3/make/g++ are REQUIRED, not optional: the sealed Q2 packaging ruling builds better-sqlite3
|
||||
# FRESH ON TARGET (never a prebuilt/nested binary — that would risk a wrong-arch native module). Its
|
||||
# install is `prebuild-install || node-gyp rebuild`; with a self-contained tarball npm reaches that
|
||||
# step, prebuild-install is absent, and node-gyp needs python3+make+g++ to compile from source. Without
|
||||
# them layer 3 dies ('prebuild-install: not found' → 'Could not find any Python installation'). This is
|
||||
# testbed-image scope only (no multi-stage gold-plating — the image is a throwaway test target).
|
||||
RUN set -eux; \
|
||||
apt-get update; \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
ca-certificates curl xz-utils git tmux tini procps \
|
||||
python3 make g++; \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# --- layer 2: pinned node (an in-range engines version — see scripts/check-engines) ---
|
||||
ARG NODE_VERSION=22.23.4
|
||||
ARG TARGETARCH
|
||||
RUN set -eux; \
|
||||
case "${TARGETARCH}" in \
|
||||
amd64) NODE_ARCH=x64 ;; \
|
||||
arm64) NODE_ARCH=arm64 ;; \
|
||||
"") echo "TARGETARCH is empty — the legacy (non-BuildKit) builder does not populate it. Pass --build-arg TARGETARCH explicitly (build-testbed-image.sh does) or enable BuildKit. REFUSING to default to amd64, which would install wrong-arch Node." >&2; exit 1 ;; \
|
||||
*) echo "unsupported TARGETARCH=${TARGETARCH}" >&2; exit 1 ;; \
|
||||
esac; \
|
||||
NODE_TARBALL="node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz"; \
|
||||
curl -fsSLo "/tmp/${NODE_TARBALL}" \
|
||||
"https://nodejs.org/dist/v${NODE_VERSION}/${NODE_TARBALL}"; \
|
||||
curl -fsSLo /tmp/SHASUMS256.txt \
|
||||
"https://nodejs.org/dist/v${NODE_VERSION}/SHASUMS256.txt"; \
|
||||
(cd /tmp && awk -v tarball="${NODE_TARBALL}" '$2 == tarball' SHASUMS256.txt | sha256sum -c -); \
|
||||
tar -xJf "/tmp/${NODE_TARBALL}" -C /usr/local --strip-components=1 --no-same-owner; \
|
||||
rm -f "/tmp/${NODE_TARBALL}" /tmp/SHASUMS256.txt; \
|
||||
node --version; npm --version
|
||||
|
||||
# --- layer 3: the OpenRig package from the LOCAL pack tarball, never the npm registry ---
|
||||
ARG OPENRIG_TARBALL
|
||||
COPY ${OPENRIG_TARBALL} /tmp/openrig.tgz
|
||||
RUN set -eux; \
|
||||
npm install -g /tmp/openrig.tgz; \
|
||||
rm -f /tmp/openrig.tgz; \
|
||||
rig --version; \
|
||||
rig --help
|
||||
|
||||
# --- layer 4: stub runtime assets (zero-token by construction; staged by the build verb into the
|
||||
# build context as ./stub-assets/, hashed into the manifest via deriveStubAssetsHash) ---
|
||||
COPY stub-assets/ /opt/openrig-testbed/stub-assets/
|
||||
|
||||
# --- layer 5: a non-root openrig user + a tini entrypoint into tmux-server-friendly init ---
|
||||
RUN set -eux; \
|
||||
useradd --create-home --shell /bin/bash openrig; \
|
||||
mkdir -p /opt/openrig-testbed; \
|
||||
chown -R openrig:openrig /opt/openrig-testbed
|
||||
COPY entrypoint.sh /opt/openrig-testbed/entrypoint.sh
|
||||
RUN chmod 0755 /opt/openrig-testbed/entrypoint.sh
|
||||
USER openrig
|
||||
WORKDIR /home/openrig
|
||||
ENTRYPOINT ["/usr/bin/tini", "--", "/opt/openrig-testbed/entrypoint.sh"]
|
||||
CMD ["sleep", "infinity"]
|
||||
@@ -1,8 +0,0 @@
|
||||
# Thin CI layer over the EXISTING installed-package testbed. No credentials,
|
||||
# host mounts, provider binaries or additional package installation.
|
||||
ARG TESTBED_IMAGE
|
||||
FROM ${TESTBED_IMAGE}
|
||||
COPY runner.mjs /opt/openrig-testbed/runner.mjs
|
||||
COPY scenarios/ /opt/openrig-testbed/scenarios/
|
||||
COPY library/ /opt/openrig-testbed/library/
|
||||
CMD ["node", "/opt/openrig-testbed/runner.mjs", "healthy"]
|
||||
@@ -1,4 +0,0 @@
|
||||
# Docker Hub library/debian bookworm-slim OCI index, resolved 2026-09-29.
|
||||
# Registry response bytes SHA256 verified against Docker-Content-Digest.
|
||||
# amd64 and arm64 are present; image execution is verified in CI, not by this pin.
|
||||
debian:bookworm-slim@sha256:3783cc01769c7b2b1b83a5c5ad96c815348e28ed7da68e2e3687004faa906251
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# 51-04 testbed entrypoint — runs under tini (PID1 reaping, layer 5). Keeps the container init
|
||||
# tmux-server-friendly (tini reaps the tmux server's children) and seeds the per-container
|
||||
# self-host identity when the operator supplies one (plan §3 — the 51-09 OPENRIG_SELF_HOST_ID
|
||||
# adopt path; N containers boot as N distinct named hosts). Then execs the container command.
|
||||
set -euo pipefail
|
||||
|
||||
if [ -n "${OPENRIG_SELF_HOST_ID:-}" ]; then
|
||||
# Announce the adopted self-host id so `docker logs` shows which simulated host this is.
|
||||
echo "[testbed] self-host-id=${OPENRIG_SELF_HOST_ID}" >&2
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
@@ -1,54 +0,0 @@
|
||||
# L0 — resolve the host-side build inputs (prerequisite to every build)
|
||||
|
||||
**Runs host-side.** Populates the two documented slots the build verb refuses-until-resolved, so the
|
||||
image is byte-reproducible against a pinned base + a known stub census. Do this once per base/stub
|
||||
change; commit the resolved slots so the pin is durable.
|
||||
|
||||
## L0.1 — resolve the digest-pinned base
|
||||
|
||||
The VM seat cannot pull/inspect a registry digest; do it on the host with a runtime present.
|
||||
|
||||
```bash
|
||||
# Pull the intended LTS-slim base, then read its immutable RepoDigest.
|
||||
docker pull debian:bookworm-slim
|
||||
BASE_REF="$(docker inspect --format '{{index .RepoDigests 0}}' debian:bookworm-slim)"
|
||||
echo "${BASE_REF}" # e.g. debian@sha256:<64-hex> (or docker.io/library/debian@sha256:...)
|
||||
```
|
||||
|
||||
Write the single resolved reference into the slot (replacing the comment block):
|
||||
|
||||
```bash
|
||||
printf '%s\n' "${BASE_REF}" > docker/testbed/base-image
|
||||
# Prove the fence accepts it (readBaseImage returns the digest, or exits non-zero):
|
||||
node -e 'import("./scripts/testbed-build-inputs.mjs").then(m=>console.log(m.readBaseImage("docker/testbed/base-image")))'
|
||||
```
|
||||
|
||||
**PASS:** `readBaseImage` prints `{ ref, name, digest: 'sha256:<64-hex>' }`. **FAIL:** any non-zero /
|
||||
"tag-floating" / "unresolved" — the slot is not a valid digest pin; do not proceed to a build.
|
||||
|
||||
## L0.2 — finalize the stub-assets census list
|
||||
|
||||
The zero-token stub payload (plan §1 layer 4) is the container-local stub `rig.yaml` + its agent
|
||||
fixture + `culture.md` that L3 boots. Add each repo-relative path, one per line, to
|
||||
`docker/testbed/stub-assets.list` (comments/blanks ignored). Ground the exact set against L3 (the
|
||||
`rig up` stub topology it settles) — do not guess; the list IS the census scope the manifest hashes.
|
||||
|
||||
```bash
|
||||
# After populating the list, prove the census resolves over the staged set the build will stage:
|
||||
node -e 'import("./scripts/testbed-build-inputs.mjs").then(m=>{
|
||||
const fs=require("fs");
|
||||
const files=fs.readFileSync("docker/testbed/stub-assets.list","utf8").split("\n").map(s=>s.replace(/#.*/,"").trim()).filter(Boolean);
|
||||
console.log(m.deriveStubAssetsHash(".", files).receipt);
|
||||
})'
|
||||
```
|
||||
|
||||
**PASS:** the receipt lists exactly the intended stub assets, each with a 64-hex content digest.
|
||||
**FAIL:** empty set / a missing file / a path escaping the root — fix the list, never a partial census.
|
||||
|
||||
## L0.3 — record evidence
|
||||
|
||||
```bash
|
||||
GIT_SHA="$(git rev-parse HEAD)"; EVID="dist/testbed-image/evidence/${GIT_SHA}"; mkdir -p "${EVID}"
|
||||
{ echo "docker: $(docker --version)"; echo "base: ${BASE_REF}"; echo "stub-assets.list:"; cat docker/testbed/stub-assets.list; } > "${EVID}/L0-inputs.txt"
|
||||
echo "VERDICT: PASS — base digest pinned + stub census resolved" >> "${EVID}/L0-inputs.txt"
|
||||
```
|
||||
@@ -1,47 +0,0 @@
|
||||
# L1 — PTY allocation (docker run -t + tmux capture returns real bytes; resize propagates)
|
||||
|
||||
**Runs host-side, after L0 + a build.** Proves the image gives a real PTY and that tmux inside it
|
||||
produces genuine captured bytes — the TUI-spike fixed-viewport pattern, reused. Never assert the
|
||||
observable from memory; verdict against the captured bytes.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
GIT_SHA="$(git rev-parse HEAD)"; IMAGE="openrig-testbed:${GIT_SHA}"; EVID="dist/testbed-image/evidence/${GIT_SHA}"; mkdir -p "${EVID}"
|
||||
docker image inspect "${IMAGE}" >/dev/null # built by scripts/build-testbed-image.sh (FAIL loudly if absent)
|
||||
NAME="orig-l1-${GIT_SHA:0:8}"
|
||||
```
|
||||
|
||||
## L1.1 — allocate a PTY + start a tmux session inside
|
||||
|
||||
```bash
|
||||
# -t allocates a PTY; the entrypoint (tini) holds the container. Detached, fixed 80x24 viewport.
|
||||
docker run -d -t --name "${NAME}" "${IMAGE}"
|
||||
docker exec "${NAME}" tmux new-session -d -s l1 -x 80 -y 24
|
||||
docker exec "${NAME}" tmux send-keys -t l1 'printf "PTY-OK:%s\n" "$TERM"; tty' Enter
|
||||
sleep 1
|
||||
docker exec "${NAME}" tmux capture-pane -p -t l1 | tee "${EVID}/L1-capture.txt"
|
||||
```
|
||||
|
||||
**PASS:** the capture contains a real `PTY-OK:` line and `tty` prints a real pts device
|
||||
(`/dev/pts/...`), not "not a tty". **FAIL:** "not a tty" / empty capture.
|
||||
|
||||
## L1.2 — resize propagates
|
||||
|
||||
```bash
|
||||
docker exec "${NAME}" tmux resize-window -t l1 -x 120 -y 40
|
||||
docker exec "${NAME}" tmux display-message -p -t l1 '#{window_width}x#{window_height}' | tee "${EVID}/L1-resize.txt"
|
||||
```
|
||||
|
||||
**PASS:** reports `120x40` (the resize propagated to the in-container tmux). **FAIL:** stays `80x24`.
|
||||
|
||||
## Teardown + evidence
|
||||
|
||||
```bash
|
||||
docker rm -f "${NAME}" >/dev/null
|
||||
{ grep -q 'PTY-OK:' "${EVID}/L1-capture.txt" && grep -q '/dev/pts/' "${EVID}/L1-capture.txt" && grep -q '120x40' "${EVID}/L1-resize.txt" \
|
||||
&& echo "VERDICT: PASS — PTY allocated, real capture bytes, resize propagated" \
|
||||
|| echo "VERDICT: FAIL — see L1-capture.txt / L1-resize.txt"; } | tee "${EVID}/L1-verdict.txt"
|
||||
```
|
||||
|
||||
**Fence:** no `-v` HOME/workspace mounts; the container is disposable (fresh HOME).
|
||||
@@ -1,55 +0,0 @@
|
||||
# L2 — tmux server lifecycle (detach-survive, multi-pane, send-keys + capture round-trip)
|
||||
|
||||
**Runs host-side, after L1.** Proves the tmux server — the substrate the whole product rides —
|
||||
behaves in-container: sessions survive client detach, multiple panes coexist, and a send-keys →
|
||||
capture round-trip returns the exact bytes.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
GIT_SHA="$(git rev-parse HEAD)"; IMAGE="openrig-testbed:${GIT_SHA}"; EVID="dist/testbed-image/evidence/${GIT_SHA}"; mkdir -p "${EVID}"
|
||||
NAME="orig-l2-${GIT_SHA:0:8}"; docker run -d -t --name "${NAME}" "${IMAGE}"
|
||||
```
|
||||
|
||||
## L2.1 — server survives detach
|
||||
|
||||
```bash
|
||||
docker exec "${NAME}" tmux new-session -d -s l2 -x 80 -y 24
|
||||
docker exec "${NAME}" tmux send-keys -t l2 'echo persist-marker-$$ > /tmp/l2.marker' Enter
|
||||
sleep 1
|
||||
# A detached session is the default with -d; confirm the server + session still list after a beat.
|
||||
docker exec "${NAME}" tmux ls | tee "${EVID}/L2-sessions.txt"
|
||||
docker exec "${NAME}" cat /tmp/l2.marker | tee "${EVID}/L2-marker.txt"
|
||||
```
|
||||
|
||||
**PASS:** `tmux ls` shows `l2` still alive; the marker file exists. **FAIL:** "no server running" / no `l2`.
|
||||
|
||||
## L2.2 — multiple panes
|
||||
|
||||
```bash
|
||||
docker exec "${NAME}" tmux split-window -t l2 -h
|
||||
docker exec "${NAME}" tmux list-panes -t l2 -F '#{pane_index}' | tee "${EVID}/L2-panes.txt"
|
||||
```
|
||||
|
||||
**PASS:** at least two pane indices (`0`,`1`). **FAIL:** one pane / error.
|
||||
|
||||
## L2.3 — send-keys + capture round-trip (exact bytes)
|
||||
|
||||
```bash
|
||||
TOKEN="roundtrip-$(date +%s 2>/dev/null || echo fixed)-marker"
|
||||
docker exec "${NAME}" tmux send-keys -t l2.0 "printf '%s\n' '${TOKEN}'" Enter
|
||||
sleep 1
|
||||
docker exec "${NAME}" tmux capture-pane -p -t l2.0 | tee "${EVID}/L2-roundtrip.txt"
|
||||
```
|
||||
|
||||
**PASS:** the capture contains `${TOKEN}` verbatim (the exact bytes round-tripped through the server).
|
||||
**FAIL:** token absent / garbled.
|
||||
|
||||
## Teardown + evidence
|
||||
|
||||
```bash
|
||||
docker rm -f "${NAME}" >/dev/null
|
||||
{ grep -q 'l2' "${EVID}/L2-sessions.txt" && [ "$(wc -l < "${EVID}/L2-panes.txt")" -ge 2 ] && grep -q "${TOKEN}" "${EVID}/L2-roundtrip.txt" \
|
||||
&& echo "VERDICT: PASS — server survives detach, multi-pane, exact round-trip" \
|
||||
|| echo "VERDICT: FAIL — see L2-*.txt"; } | tee "${EVID}/L2-verdict.txt"
|
||||
```
|
||||
@@ -1,146 +0,0 @@
|
||||
# L3 — daemon-in-container (boots vs container-local sqlite; healthz on the PUBLISHED port; `rig up` settles a zero-token stub topology)
|
||||
|
||||
**Runs host-side, after L2.** This is the leg that exercises the stub payload — and where
|
||||
`docker/testbed/stub-assets.list` is finalized (L0.2): the exact stub `rig.yaml` + agent fixture +
|
||||
`culture.md` this `rig up` settles ARE the census scope. Zero tokens by construction (runtime: stub).
|
||||
|
||||
## THE PUBLISHED-DAEMON PROCEDURE (identical on both A/B arms)
|
||||
|
||||
The prior form of this runbook started a LOOPBACK-BOUND daemon and probed it through a published
|
||||
port — unreachable by construction, so the probe failed on BOTH runtimes (operator A/B receipt
|
||||
`Q2-AB-d121568ad-20260807-host/AB-RESULT.md`, sha256 3433e95f427828ce: Docker curl 52 / Apple curl
|
||||
56). A control arm that cannot pass cannot judge the variable arm. The four coupled mechanics
|
||||
below are the fix; all four are grounded at source and all four apply to BOTH arms verbatim.
|
||||
|
||||
**(a) BIND ADDRESS — explicit, never the default.** `packages/daemon/src/index.ts:148` reads
|
||||
`OPENRIG_HOST` (alias `RIGGED_HOST`); when set, `:163` binds exactly that host; when UNSET, `:167`
|
||||
defaults to `127.0.0.1` (+ tailscale when detected). A container reached through published ports
|
||||
MUST set `OPENRIG_HOST=0.0.0.0`.
|
||||
|
||||
**(b) BEARER — required for the daemon to START on this bind, and separately PROVEN.**
|
||||
`assertBindAuthInvariant` (`packages/daemon/src/middleware/auth-bearer-token.ts:240-264`)
|
||||
short-circuits for loopback and tailscale binds; for any other bind it REFUSES TO START unless
|
||||
`OPENRIG_AUTH_BEARER_TOKEN` is non-empty. That refusal is the product being honest — the procedure
|
||||
satisfies it, never routes around it.
|
||||
**VERIFIED-AT-SOURCE CORRECTION (do not "fix" the probe to match a wrong expectation):**
|
||||
`/healthz` is registered directly on the app (`packages/daemon/src/server.ts:574`) and is
|
||||
**UNAUTHENTICATED** — there is no global auth middleware (`app.use("*")` at `:441` only injects
|
||||
deps), and `authBearerTokenMiddleware` is mounted inside exactly six route modules
|
||||
(compaction, hosts, mission-control, rig-policy, sessions, transport). So the health probe does
|
||||
NOT need an Authorization header and MUST NOT be scored on one. The bearer is therefore proven
|
||||
separately, on a genuinely guarded surface: `/api/transport/*` guards its whole router
|
||||
(`packages/daemon/src/routes/transport.ts:9`). Two probes, two distinct facts: the bind, and the
|
||||
auth path.
|
||||
|
||||
**(c) PORT ALLOCATION — an EXPLICIT host port, never `0`.** Apple `container` 1.2.0 rejects the
|
||||
ephemeral publish form (`Error: invalid publish host port range: 0`) where Docker accepts it —
|
||||
a captured runtime-parity difference (operator receipt above). The procedure pins one explicit
|
||||
port so both arms publish identically; ephemeral allocation is not a difference the A/B is
|
||||
measuring.
|
||||
|
||||
**(d) PROBE — byte-identical on both arms.** Probe 1 (`/healthz`, no auth) proves the published
|
||||
path reaches the daemon. Probe 2 (a guarded route WITH the bearer) proves the auth path. Both are
|
||||
recorded; a runbook that records only probe 1 has not tested the bearer at all.
|
||||
|
||||
## Setup (both arms — only the runtime binary differs)
|
||||
|
||||
```bash
|
||||
GIT_SHA="$(git rev-parse HEAD)"; IMAGE="openrig-testbed:${GIT_SHA}"; EVID="dist/testbed-image/evidence/${GIT_SHA}"; mkdir -p "${EVID}"
|
||||
NAME="orig-l3-${GIT_SHA:0:8}"
|
||||
HOSTPORT=19433 # (c) EXPLICIT — never 0 (Apple 1.2.0 rejects ephemeral)
|
||||
TESTBEARER="l3-testbed-$(date +%s)" # (b) throwaway, container-scoped, never a real credential
|
||||
RUNTIME=docker # the Apple arm substitutes its CLI here; ALL ELSE IDENTICAL
|
||||
"${RUNTIME}" run -d -t --name "${NAME}" \
|
||||
-p "${HOSTPORT}:7433" \
|
||||
-e OPENRIG_HOST=0.0.0.0 \
|
||||
-e OPENRIG_AUTH_BEARER_TOKEN="${TESTBEARER}" \
|
||||
-e OPENRIG_SELF_HOST_ID=testbed-l3 \
|
||||
"${IMAGE}"
|
||||
```
|
||||
|
||||
*Apple-arm note (captured, not a workaround): the loopback-qualified publish form
|
||||
(`127.0.0.1:PORT:7433`) resets on Apple 1.2.0 while the unqualified `PORT:7433` works. The
|
||||
unqualified form is therefore the procedure's single published form on BOTH arms — identical
|
||||
input, not an Apple-only concession.*
|
||||
|
||||
## L3.1 — daemon boots vs a container-local sqlite + BOTH probes answer
|
||||
|
||||
```bash
|
||||
"${RUNTIME}" exec "${NAME}" bash -lc 'rig daemon start && sleep 2 && rig status || true' | tee "${EVID}/L3-start.txt"
|
||||
# PROBE 1 — the BIND, unauthenticated by design:
|
||||
curl -fsS "http://127.0.0.1:${HOSTPORT}/healthz" | tee "${EVID}/L3-healthz.txt"; echo
|
||||
# PROBE 2 — the AUTH path, on a genuinely guarded router:
|
||||
curl -sS -o "${EVID}/L3-auth-probe.txt" -w '%{http_code}\n' \
|
||||
-X POST "http://127.0.0.1:${HOSTPORT}/api/transport/send" \
|
||||
-H "Authorization: Bearer ${TESTBEARER}" -H 'Content-Type: application/json' \
|
||||
-d '{"session":"nonexistent@testbed","text":"auth-path probe"}' | tee "${EVID}/L3-auth-code.txt"
|
||||
# NEGATIVE CONTROL — the same call with NO bearer must be refused:
|
||||
curl -sS -o /dev/null -w '%{http_code}\n' \
|
||||
-X POST "http://127.0.0.1:${HOSTPORT}/api/transport/send" \
|
||||
-H 'Content-Type: application/json' -d '{"session":"nonexistent@testbed","text":"x"}' \
|
||||
| tee "${EVID}/L3-auth-negative.txt"
|
||||
"${RUNTIME}" exec "${NAME}" bash -lc 'ls -l "${OPENRIG_HOME:-$HOME/.openrig}"/*.sqlite* 2>/dev/null || find "$HOME" -name "*.sqlite*" 2>/dev/null' | tee "${EVID}/L3-db.txt"
|
||||
```
|
||||
|
||||
**PASS:** probe 1 returns healthy JSON on the PUBLISHED port; probe 2 is NOT 401 (the bearer is
|
||||
accepted — any application-level status such as an unknown-session 4xx still proves the auth path);
|
||||
the negative control IS 401; a container-local sqlite exists.
|
||||
**FAIL:** no healthz on the published port (bind wrong) / probe 2 401 (bearer path wrong) /
|
||||
negative control NOT 401 (the guard is not guarding) / DB outside the container / a mounted real
|
||||
HOME (fence breach).
|
||||
|
||||
## L3.2 — `rig up` settles a zero-token stub topology
|
||||
|
||||
Ship a minimal `runtime: stub` `rig.yaml` (+ agent fixture + culture.md) as the staged stub assets;
|
||||
copy it into a container-local workspace and `rig up` it BY EXPLICIT SOURCE.
|
||||
|
||||
**STAGED PAYLOAD PATH — nested, and deliberately so.** The build verb stages each census entry
|
||||
preserving its full REPO-RELATIVE path (`scripts/build-testbed-image.sh:51-52`:
|
||||
`cp "${REPO_ROOT}/${rel}" "${CONTEXT}/stub-assets/${rel}"`), and the Dockerfile copies that context
|
||||
dir wholesale (`docker/testbed/Dockerfile:52`). So in-container the trio lands at
|
||||
`/opt/openrig-testbed/stub-assets/docker/testbed/stub-assets/`, NOT at the stub-assets root. That
|
||||
nesting is load-bearing, not an accident: the same relative paths are what the manifest hashes into
|
||||
the byte-reproducible census receipt (`build-testbed-image.sh:101`), so flattening the staging would
|
||||
change the receipt bytes r1 verified. The RUNBOOK adapts to the staged reality; the staging stands.
|
||||
(This exact failure was PREDICTED and scoped to this runbook —
|
||||
`51-04-STUB-ASSET-TRIO-REVIEW-VERDICT-review-r1.md` sha-16 5d4a8bb1d0a1605b, "Honest forward-flag";
|
||||
the note simply never reached the text until now.)
|
||||
|
||||
**EXPLICIT SOURCE — never bare `rig up`.** `rig up` takes a REQUIRED positional
|
||||
(`packages/cli/src/commands/up.ts:77`: `.argument("<source>", "Path to a .yaml rig spec or
|
||||
.rigbundle, or a library name…")`); a bare invocation exits `missing required argument 'source'`,
|
||||
which is what the operator hit.
|
||||
|
||||
```bash
|
||||
"${RUNTIME}" exec "${NAME}" bash -lc '
|
||||
set -e
|
||||
STAGED=/opt/openrig-testbed/stub-assets/docker/testbed/stub-assets
|
||||
# Pre-flight fence: assert the payload where the build actually stages it. A staging change must
|
||||
# fail LOUD here naming the path, never as a downstream "missing required argument".
|
||||
[ -f "${STAGED}/rig.yaml" ] || { echo "L3.2 FAIL: no rig.yaml at ${STAGED} — staged payload moved; reconcile the runbook against scripts/build-testbed-image.sh"; ls -R /opt/openrig-testbed/stub-assets | head -40; exit 1; }
|
||||
mkdir -p ~/work && cp -r "${STAGED}/." ~/work/
|
||||
cd ~/work && rig up rig.yaml && sleep 3 && rig ps --json' | tee "${EVID}/L3-topology.txt"
|
||||
```
|
||||
|
||||
**PASS:** the stub seat(s) reach a settled/ready state (zero LLM tokens consumed — `runtime: stub`).
|
||||
**FAIL:** topology never settles / a non-stub runtime is invoked.
|
||||
|
||||
## Teardown + evidence
|
||||
|
||||
```bash
|
||||
"${RUNTIME}" exec "${NAME}" bash -lc 'rig down || true' ; "${RUNTIME}" rm -f "${NAME}" >/dev/null
|
||||
{ grep -qi 'health' "${EVID}/L3-healthz.txt" \
|
||||
&& [ "$(cat "${EVID}/L3-auth-code.txt")" != "401" ] \
|
||||
&& [ "$(cat "${EVID}/L3-auth-negative.txt")" = "401" ] \
|
||||
&& echo "VERDICT: PASS — published bind reachable, bearer path proven, negative control refused, stub topology settles" \
|
||||
|| echo "VERDICT: FAIL — see L3-*.txt"; } | tee "${EVID}/L3-verdict.txt"
|
||||
```
|
||||
|
||||
## ACCEPTANCE GATE (non-negotiable, before any A/B rerun is requested)
|
||||
|
||||
Run this procedure on the **DOCKER** arm alone and prove it GREEN. A control that cannot pass is
|
||||
not a control; the A/B stays blocked until the Docker baseline is green under this exact text.
|
||||
Only then does the Apple arm run the identical procedure with its CLI substituted.
|
||||
|
||||
**Note:** the exact `rig ps` shape / ready predicate is grounded against the shipped stub adapter at
|
||||
execution time — do not assert a remembered field name; read the real `--json` output.
|
||||
@@ -1,51 +0,0 @@
|
||||
# L4 — the 51-02 hermetic contract holds INSIDE the container (fail-closed not weakened)
|
||||
|
||||
**Runs host-side, after L3.** The hermeticity floor is "fresh container = fresh HOME" **by
|
||||
construction** — no real-HOME/workspace mounts (verified in L1–L3 by the no-`-v` fence). This leg
|
||||
proves the *active* half: the 51-02 env-helper still **REFUSES an injected foreign `OPENRIG_URL`**
|
||||
inside the container. The container is not an excuse to weaken the fail-closed guard — a leaked daemon
|
||||
target must hard-refuse exactly as it does host-side (the `DAEMON_TARGET_ENV_VARS` guard class).
|
||||
|
||||
## Claim
|
||||
|
||||
With a foreign `OPENRIG_URL` (a daemon target the helper did not set itself) present in the
|
||||
environment, the 51-02 hermetic env prep **hard-refuses** (non-zero + a refusal naming the foreign
|
||||
target) rather than silently adopting it — inside the container, same as host-side.
|
||||
|
||||
## Grounding note (confirm at execution — do NOT assert from memory)
|
||||
|
||||
Where the helper runs depends on whether the 51-02 hermetic-env module is **packaged** in the openrig
|
||||
tarball the image installs (it currently lives at `packages/daemon/test/helpers/hermetic-env.ts` — a
|
||||
test helper, which may not be in the pack set):
|
||||
- **If packaged / resolvable in-container:** invoke the assertion directly in the container (L4.1).
|
||||
- **If not packaged:** this leg is exercised via the **runner's container-execution mode (step-3)** —
|
||||
see **L6.3** (`L6-container-runner-e2e.md`), which drives a run with a foreign target injected into the
|
||||
base env and the host-side hermetic prep refusing before any container is created — record that as the
|
||||
L4 form. Either way, the leg is real; pick the form the shipped surface supports and say which.
|
||||
|
||||
## L4.1 — in-container assertion (when the helper is resolvable)
|
||||
|
||||
```bash
|
||||
GIT_SHA="$(git rev-parse HEAD)"; IMAGE="openrig-testbed:${GIT_SHA}"; EVID="dist/testbed-image/evidence/${GIT_SHA}"; mkdir -p "${EVID}"
|
||||
NAME="orig-l4-${GIT_SHA:0:8}"; docker run -d -t --name "${NAME}" "${IMAGE}"
|
||||
|
||||
# Inject a foreign daemon target and drive the hermetic prep; expect a LOUD non-zero refusal.
|
||||
docker exec -e OPENRIG_URL='http://foreign-daemon.invalid:9999' "${NAME}" bash -lc '
|
||||
node --input-type=module -e "
|
||||
import { prepareHermeticEnv } from \"@openrig/daemon/test/helpers/hermetic-env.js\";
|
||||
try { prepareHermeticEnv({ baseEnv: process.env }); console.log(\"NO-REFUSAL\"); process.exit(0); }
|
||||
catch (e) { console.error(\"REFUSED: \" + e.message); process.exit(7); }
|
||||
"' 2>&1 | tee "${EVID}/L4-refusal.txt"; RC=${PIPESTATUS[0]:-$?}
|
||||
docker rm -f "${NAME}" >/dev/null
|
||||
```
|
||||
|
||||
**PASS:** non-zero exit + `REFUSED:` naming the foreign `OPENRIG_URL`. **FAIL:** `NO-REFUSAL` /
|
||||
exit 0 (the guard was weakened in-container). Adjust the import specifier to the helper's real shipped
|
||||
path; if it does not resolve, switch to the step-3 form (grounding note) — do not green-pass by memory.
|
||||
|
||||
## Evidence
|
||||
|
||||
```bash
|
||||
{ grep -q 'REFUSED:' "${EVID}/L4-refusal.txt" && echo "VERDICT: PASS — foreign OPENRIG_URL hard-refused inside the container" \
|
||||
|| echo "VERDICT: FAIL / FORM-DEFERRED — see L4-refusal.txt + the grounding note"; } | tee "${EVID}/L4-verdict.txt"
|
||||
```
|
||||
@@ -1,104 +0,0 @@
|
||||
# L5 — multi-host topology (N named self-hosts on one network) + the 51-09 live-leg rider
|
||||
|
||||
**Runs host-side, after L4.** Proves plan §3: N containers each = one simulated **named host**,
|
||||
composed via the shipped **host registry over HTTP** (registry rows point at container-DNS names), one
|
||||
daemon per container, container-local DB/HOME, no real-HOME mounts. The **only** shared surface is the
|
||||
docker network.
|
||||
|
||||
**51-09 live-leg rider (orch, merge desk):** this same two-host setup is the executor for the shipped
|
||||
51-09 collision/alignment runbook. After the topology settles, run BOTH 51-09 legs **in this same
|
||||
host-side session** and return both evidence sets. Fixture + steps:
|
||||
`packages/daemon/test/fixtures/self-host-live-legs/RUNBOOK.md` — **LEG A** (cross-host stamped-triple
|
||||
round-trip: a cross-host send stores the ORIGIN host in the sender identity and the `↩ Reply:` hint
|
||||
round-trips verbatim to the origin) and **LEG B** (founder-collision: a same-named rig on both hosts;
|
||||
the received signature names the origin and the reply lands on the origin, not the local lookalike).
|
||||
|
||||
## L5.1 — boot two named self-hosts on one network
|
||||
|
||||
**IDENTITY IS READ, NEVER PREDICTED (PM ruling — capture-not-compose applied to identity).** The
|
||||
self-host id is minted daemon-side and stored as a never-rewritten singleton; the runbook CAPTURES
|
||||
each container's id from its own surface (`/healthz`, whose `selfHostId` field is emitted at
|
||||
`packages/daemon/src/server.ts:589`) and then uses that CAPTURED value verbatim. It never predicts,
|
||||
hardcodes, or derives an id. (`OPENRIG_SELF_HOST_ID` was NOT the knob: `docker/testbed/entrypoint.sh:8`
|
||||
only ECHOES it for `docker logs`; nothing in the daemon reads it. Do not reintroduce it.)
|
||||
|
||||
**BIND + BEARER, carried from L3 verbatim.** L5 needs cross-container HTTP, so each daemon must be
|
||||
reachable off-loopback — the same three coupled parts L3 proved, for the same reasons, with the same
|
||||
never-weaken-the-guard rail: explicit `OPENRIG_HOST=0.0.0.0`, `OPENRIG_AUTH_BEARER_TOKEN` set (the
|
||||
guard at `packages/daemon/src/middleware/auth-bearer-token.ts:240-264` REFUSES a non-loopback bind
|
||||
without it), and an explicit published port when a host-side probe is used. Registry rows carry a
|
||||
bearer POINTER (`--bearer-env` NAME), never a token value.
|
||||
|
||||
```bash
|
||||
GIT_SHA="$(git rev-parse HEAD)"; IMAGE="openrig-testbed:${GIT_SHA}"; EVID="dist/testbed-image/evidence/${GIT_SHA}"; mkdir -p "${EVID}"
|
||||
NET="orig-net-${GIT_SHA:0:8}"; docker network create "${NET}" >/dev/null
|
||||
TESTBEARER="l5-testbed-$(date +%s)" # throwaway, container-scoped, never a real credential
|
||||
|
||||
for h in H_A H_B; do
|
||||
docker run -d -t --name "$h" --hostname "$h" --network "${NET}" \
|
||||
-e OPENRIG_HOST=0.0.0.0 \
|
||||
-e OPENRIG_AUTH_BEARER_TOKEN="${TESTBEARER}" \
|
||||
-e OPENRIG_TESTBED_BEARER_ENV=OPENRIG_AUTH_BEARER_TOKEN \
|
||||
"${IMAGE}"
|
||||
done
|
||||
for h in H_A H_B; do docker exec "$h" bash -lc 'rig daemon start && sleep 2 && rig status || true'; done | tee "${EVID}/L5-start.txt"
|
||||
|
||||
# CAPTURE each minted id from its own daemon (adopt-by-read; ids are random per boot).
|
||||
ID_A="$(docker exec H_A bash -lc 'curl -fsS http://127.0.0.1:7433/healthz' | python3 -c "import json,sys; print(json.load(sys.stdin).get('selfHostId',''))")"
|
||||
ID_B="$(docker exec H_B bash -lc 'curl -fsS http://127.0.0.1:7433/healthz' | python3 -c "import json,sys; print(json.load(sys.stdin).get('selfHostId',''))")"
|
||||
printf 'H_A selfHostId=%s\nH_B selfHostId=%s\n' "${ID_A}" "${ID_B}" | tee "${EVID}/L5-selfids.txt"
|
||||
[ -n "${ID_A}" ] && [ -n "${ID_B}" ] && [ "${ID_A}" != "${ID_B}" ] || { echo "L5.1 FAIL: missing or identical self-host ids"; exit 1; }
|
||||
```
|
||||
|
||||
**PASS:** both containers report a NON-EMPTY, DISTINCT `selfHostId` on their own `/healthz`, and each
|
||||
id is stable across a daemon restart in that container (51-09 incr 1 — the singleton is never
|
||||
re-keyed). **FAIL:** an empty id / identical ids / an id that changes on restart.
|
||||
|
||||
*Readback note (captured product observation, NOT worked around here): `rig whoami` reports a SEAT
|
||||
identity and a bare host container has no seat, so it cannot answer "which host am I". The
|
||||
daemon-surface read above is the shipped substitute. The declared-identity + host-readback gap is
|
||||
pooled at PM's desk (identity family); this runbook consumes what ships.*
|
||||
|
||||
## L5.2 — compose via the host registry over HTTP
|
||||
|
||||
Register `H_B` in `H_A`'s host registry pointing at the container-DNS name `H_B` (reachable on `${NET}`):
|
||||
|
||||
```bash
|
||||
# CURRENT grammar (packages/cli/src/commands/host.ts:427-433): --id and --transport are REQUIRED;
|
||||
# http transport takes --url; the bearer rides as a POINTER (env var NAME), never a value.
|
||||
# The id is the CAPTURED ${ID_B} — adopt-by-read, never an authored name.
|
||||
docker exec H_A bash -lc "rig host add --id '${ID_B}' --transport http --url http://H_B:7433 --bearer-env OPENRIG_AUTH_BEARER_TOKEN && rig host ls --json" | tee "${EVID}/L5-registry.txt"
|
||||
```
|
||||
|
||||
**PASS:** `H_A`'s registry lists the CAPTURED id `${ID_B}` at the container-DNS URL; a probe reaches `H_B`'s daemon over
|
||||
the network only. **FAIL:** unreachable / resolves off-network. (Ground the exact `rig host` flag shape
|
||||
against the shipped CLI at execution — read `--json`, don't assert a remembered field.)
|
||||
|
||||
## L5.3 — execute the 51-09 live legs (the rider) in this session
|
||||
|
||||
Using `H_A`/`H_B` as the two hosts, follow the shipped runbook end-to-end and bank its evidence here:
|
||||
|
||||
```bash
|
||||
# Source of truth for the exact seats/fixtures/steps + PASS predicates:
|
||||
sed -n '1,200p' packages/daemon/test/fixtures/self-host-live-legs/RUNBOOK.md
|
||||
# LEG A: cross-host send orch@rig-a -> dev@rig-b --host B; expect From: orch@rig-a@A + verbatim reply lands on H_A.
|
||||
# LEG B: same-named rig 'shared' on both; expect From: lead@shared@A + reply lands on H_A's lead@shared.
|
||||
# Capture each received pane + the stored source_session; write to L5-leg-a.txt / L5-leg-b.txt.
|
||||
```
|
||||
|
||||
**PASS:** LEG A signature names `A` + stored `source_session == orch@rig-a@A` + verbatim reply lands on
|
||||
`H_A`; LEG B signature names the origin `A` + reply lands on `H_A`'s lookalike, not `H_B`'s. Include the
|
||||
51-09 fail-open control (stop `H_A`'s daemon → `From:` degrades to 2-part, no crash).
|
||||
|
||||
## Teardown + evidence
|
||||
|
||||
```bash
|
||||
docker rm -f H_A H_B >/dev/null; docker network rm "${NET}" >/dev/null
|
||||
{ echo "topology: 2 named hosts A/B on ${NET}, registry-composed"; echo "51-09 rider: LEG A + LEG B executed against the testbed containers"; \
|
||||
echo "VERDICT: PASS|FAIL — per L5-selfids/registry/leg-a/leg-b captures (verdict on bytes, not memory)"; } | tee "${EVID}/L5-verdict.txt"
|
||||
```
|
||||
|
||||
**Fences:** no real-HOME mounts; the docker network is the only shared surface; scenarios byte-unchanged.
|
||||
**Docker-gap watch (plan §3):** if per-container network identity proves insufficient for a topology the
|
||||
design needs, THAT is the named concrete gap the ruling requires before any Apple-stack revisit — route
|
||||
to PM with the evidence, never install.
|
||||
@@ -1,142 +0,0 @@
|
||||
# L6 — the 51-02 runner drives a REAL scenario through the container (container-mode e2e) + host-mode parity
|
||||
|
||||
**Runs host-side, after L3** (needs the built image + a container-local daemon that boots). This is the
|
||||
live proof of the 51-04 step-3 mechanism: the runner's **container-mode opt-in** stands the
|
||||
scenario-local daemon up INSIDE the testbed image (by manifest identity), drives the SAME shipped
|
||||
scenario through the SAME host-side reads pointed at the container's **published port**, and stamps the
|
||||
image manifest id onto every results-ledger row. It also carries the **L4 hermetic fail-closed leg's
|
||||
step-3 form** (the grounding note in `L4-hermetic-fail-closed.md` points here) and the **host-mode
|
||||
byte-intact parity** check that closes the additive-opt-in fence behaviorally.
|
||||
|
||||
The step-3 pieces this exercises are unit-proven in the VM (`test/scenario-container.test.ts`,
|
||||
`test/scenario-daemon-mode.test.ts`) with an INJECTED docker runner; L6 is where the REAL `docker`
|
||||
invoker + a live container replace the injection — the leg no source-only proof can give.
|
||||
|
||||
## Topology of a container-mode run (what talks to what)
|
||||
|
||||
- The **daemon** runs inside the container (`rig daemon start` on the container port, container-local
|
||||
HOME/DB, no mounts).
|
||||
- The host-side **`rig` reads/writes** (the shipped CLI subprocesses the runner already uses) run on the
|
||||
HOST against `OPENRIG_URL = http://127.0.0.1:<published port>` — the adapter self-sets that URL to its
|
||||
OWN container (never a foreign target). So the host still needs the built CLI bin for the READ side.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
```bash
|
||||
# L0 resolved the base + stub-assets slots; the build verb produced the image + manifest.
|
||||
scripts/build-testbed-image.sh # -> dist/testbed-image/manifest.json
|
||||
GIT_SHA="$(git rev-parse HEAD)"; IMAGE="openrig-testbed:${GIT_SHA}"
|
||||
EVID="dist/testbed-image/evidence/${GIT_SHA}"; mkdir -p "${EVID}"
|
||||
# Built HOST rig bin for the read side (the same bin run-scenarios.mjs uses).
|
||||
npm run build -w packages/cli && npm run build -w packages/daemon
|
||||
test -x packages/cli/dist/bin-wrapper.js || { echo "BLOCKER: build the CLI bin first"; exit 1; }
|
||||
command -v docker >/dev/null || { echo "BLOCKER: docker not on PATH"; exit 1; }
|
||||
```
|
||||
|
||||
## L6.1 — drive a real scenario through container-mode (the runner as a host-side wrapper)
|
||||
|
||||
The operator runs the runner in container-mode by supplying the step-3 `daemon` spawner + `imageId`.
|
||||
This wiring composes only already-tested helpers (`spawnContainerDaemon`, `runScenarioFile`,
|
||||
`withImageId`, `makeRealStagingDocker`). The REAL `docker` invoker is a COMMITTED helper, imported —
|
||||
never re-inlined here. (The prior inline `execFile` invoker predated the `stdinFrom` tar-pipe
|
||||
contract and ignored it, leaving the in-container `tar -xf -` an open never-fed stdin: the 08-12
|
||||
seven-minute extraction hang, row 42576855. The committed helper feeds the pipe, checks BOTH exits,
|
||||
and bounds every step with a timeout so a stall is a NAMED failure — proven hermetically in
|
||||
`test/staging-docker-invoker.test.ts`. That proof covers the invoker only; live containment on a
|
||||
real engine still rides THIS leg.) Run it host-side with the tsx loader:
|
||||
|
||||
```bash
|
||||
node --import tsx - "$IMAGE" "$(jq -r .manifestDigest dist/testbed-image/manifest.json)" <<'RUN' | tee "${EVID}/L6-container.txt"
|
||||
import { resolve } from "node:path";
|
||||
import { runScenarioFile } from "./packages/daemon/test/helpers/scenario-pipeline.ts";
|
||||
import { spawnContainerDaemon } from "./packages/daemon/test/helpers/scenario-container.ts";
|
||||
import { makeRealStagingDocker } from "./packages/daemon/test/helpers/staging-docker-invoker.ts";
|
||||
|
||||
const [image, imageId] = process.argv.slice(2);
|
||||
const RIG_BIN = resolve("packages/cli/dist/bin-wrapper.js");
|
||||
const SCENARIO = resolve("packages/daemon/test/fixtures/scenarios"); // pick one committed scenario file below
|
||||
|
||||
// The committed real invoker: two-process tar pipe, dual-exit check, per-step timeout.
|
||||
const docker = makeRealStagingDocker();
|
||||
|
||||
// Container-mode: inject the ScenarioDaemon-shaped container adapter + the manifest id.
|
||||
const daemon = (scaffold /*, opts */) => spawnContainerDaemon(scaffold, { image, docker });
|
||||
// COMMITTED scenario (the prior literal `scenario-collision.yaml` DOES NOT EXIST in the tree —
|
||||
// operator-caught at the L4/L6 re-entry). L6 proves the container RUNNER, so it drives a real
|
||||
// shipped scenario; override with L6_SCENARIO to run another COMMITTED file, never an improvised one.
|
||||
const scenario = `${SCENARIO}/` + (process.env.L6_SCENARIO ?? "scenario-02-baton.yaml");
|
||||
const baseEnv = { HOME: process.env.HOME, PATH: process.env.PATH, TERM: "xterm" };
|
||||
|
||||
const records = [];
|
||||
const result = await runScenarioFile(scenario, {
|
||||
rigBin: RIG_BIN, baseEnv, daemon, imageId,
|
||||
deps: { appendRecord: (r) => records.push(r), normalizer: (_s, v) => v },
|
||||
});
|
||||
console.log("VERDICT:", result.verdict, "scenario:", result.scenario);
|
||||
console.log("LEDGER:", JSON.stringify(records));
|
||||
process.exit(result.verdict === "PASS" ? 0 : 1);
|
||||
RUN
|
||||
```
|
||||
|
||||
**PASS:** the scenario reaches its expected verdict driven against the in-container daemon, AND every
|
||||
ledger row carries `imageId == manifest.manifestDigest` (grep the `LEDGER:` line). **FAIL:** the daemon
|
||||
never comes up in the container / a read cannot reach the published port / a ledger row is missing the
|
||||
image id. (Ground the exact expected verdict against the chosen scenario — a 51-03-dependent
|
||||
cross-surface scenario is not expected green until its normalizer ships; pick a settled scenario.)
|
||||
|
||||
## L6.2 — host-mode parity (the additive-opt-in fence, behaviorally)
|
||||
|
||||
Run the IDENTICAL scenario in **host-mode** (no `daemon` override, no `imageId`) and compare verdicts:
|
||||
|
||||
```bash
|
||||
L6_MODE=host node --import tsx packages/daemon/scripts/run-scenarios.mjs \
|
||||
packages/daemon/test/fixtures/scenarios/scenario-02-baton.yaml | tee "${EVID}/L6-host.txt"
|
||||
```
|
||||
|
||||
**PASS:** the host-mode verdict for the same scenario matches the container-mode verdict from L6.1 —
|
||||
container-mode changed nothing about the host-mode path (the 51-02 contract is byte-intact; the ledger
|
||||
rows differ ONLY by the added `imageId` key in container-mode). **FAIL:** host-mode verdict diverges
|
||||
from pre-51-04 for the same scenario (a fence breach — that would demand a PM 51-02 gate).
|
||||
|
||||
## L6.3 — container-mode fail-closed (the L4 step-3 form)
|
||||
|
||||
Inject a FOREIGN daemon target into the base env and confirm the run HARD-REFUSES before standing any
|
||||
container up (the container is not an excuse to weaken the `DAEMON_TARGET` guard — same as host-mode):
|
||||
|
||||
```bash
|
||||
OPENRIG_URL='http://foreign-daemon.invalid:9999' node --import tsx - "$IMAGE" <<'RUN' 2>&1 | tee "${EVID}/L6-failclosed.txt"; RC=${PIPESTATUS[0]:-$?}
|
||||
import { resolve } from "node:path";
|
||||
import { runScenarioFile } from "./packages/daemon/test/helpers/scenario-pipeline.ts";
|
||||
import { spawnContainerDaemon } from "./packages/daemon/test/helpers/scenario-container.ts";
|
||||
const [image] = process.argv.slice(2);
|
||||
const daemon = (scaffold) => spawnContainerDaemon(scaffold, { image, docker: async () => ({ stdout: "", stderr: "", code: 0 }) });
|
||||
try {
|
||||
await runScenarioFile(resolve("packages/daemon/test/fixtures/scenarios/scenario-02-baton.yaml"),
|
||||
{ rigBin: resolve("packages/cli/dist/bin-wrapper.js"), baseEnv: process.env, daemon });
|
||||
console.log("NO-REFUSAL"); process.exit(0);
|
||||
} catch (e) { console.error("REFUSED: " + e.message); process.exit(7); }
|
||||
RUN
|
||||
```
|
||||
|
||||
**PASS:** non-zero exit + `REFUSED:` naming the foreign `OPENRIG_URL` — `prepareHermeticEnv` fail-closes
|
||||
on the foreign target in `baseEnv` and NO container is created. **FAIL:** `NO-REFUSAL` / exit 0 (the
|
||||
guard was bypassed in container-mode). This is the step-3 form of the L4 leg (the packaged-helper form
|
||||
lives in `L4-hermetic-fail-closed.md`); either form is real — record which one the shipped surface ran.
|
||||
|
||||
## Teardown + evidence
|
||||
|
||||
Container-mode tears its own container down (the adapter's `stop` = `docker rm -f`), so a clean run
|
||||
leaves nothing. Sweep any strays from a failed run, then verdict on the captured bytes:
|
||||
|
||||
```bash
|
||||
docker ps -aq --filter "ancestor=${IMAGE}" | xargs -r docker rm -f >/dev/null
|
||||
{ grep -q 'VERDICT: PASS' "${EVID}/L6-container.txt" \
|
||||
&& grep -q "$(jq -r .manifestDigest dist/testbed-image/manifest.json)" "${EVID}/L6-container.txt" \
|
||||
&& grep -q 'REFUSED:' "${EVID}/L6-failclosed.txt" \
|
||||
&& echo "VERDICT: PASS — container-mode drives a real scenario by image identity, ledger carries the manifest id, host-mode parity holds, fail-closed refuses" \
|
||||
|| echo "VERDICT: FAIL / FORM-DEFERRED — see L6-*.txt"; } | tee "${EVID}/L6-verdict.txt"
|
||||
```
|
||||
|
||||
**Fences (binding):** the scenario YAML is byte-unchanged (51-03 fence); host-mode stays byte-intact
|
||||
(L6.2 is the behavioral proof — any divergence is a PM 51-02 gate, not a fold); no real-HOME mounts;
|
||||
the image is selected BY MANIFEST IDENTITY and its digest is the ledger's cross-version key.
|
||||
@@ -1,61 +0,0 @@
|
||||
# openrig-testbed verification runbooks (51-04 step-2, plan §2)
|
||||
|
||||
These are **host-executed, evidence-defined** runbooks — the 51-09 live-legs pattern. The VM seat has
|
||||
no container runtime (locus ruling (a) host-side), so the daemon/operator lane runs them **host-side**
|
||||
against the image `scripts/build-testbed-image.sh` produces, and returns evidence per the
|
||||
census-receipt discipline. Each leg is a scripted check that never asserts from memory (the clause
|
||||
that caught the Apple-container premise): run it, capture the real bytes, verdict against them.
|
||||
|
||||
## Legs
|
||||
|
||||
| Leg | Proves | Runbook |
|
||||
|---|---|---|
|
||||
| L0 | the digest-pinned base + stub-assets census are resolved (prerequisite to a build) | `L0-resolve-inputs.md` |
|
||||
| L1 | PTY allocation: `docker run -t` + tmux capture-pane returns real bytes; resize propagates | `L1-pty-allocation.md` |
|
||||
| L2 | tmux server lifecycle: detach-survive, multi-pane, send-keys + capture round-trip | `L2-tmux-server-lifecycle.md` |
|
||||
| L3 | daemon-in-container: boots vs container-local sqlite, healthz on the published port, `rig up` settles a zero-token stub topology | `L3-daemon-in-container.md` |
|
||||
| L4 | the 51-02 hermetic contract: the env-helper still REFUSES a foreign `OPENRIG_URL` **inside** the container (fail-closed not weakened by "we're in a container anyway") | `L4-hermetic-fail-closed.md` |
|
||||
| L5 | multi-host: N containers as N distinct named self-hosts on one docker network, composed via the shipped host registry over HTTP (plan §3) — **and** the 51-09 live-leg rider | `L5-multi-host-and-51-09.md` |
|
||||
| L6 | the 51-02 runner's **container-mode** (step-3) drives a REAL scenario through a container by manifest identity + host-mode byte-intact parity + the L4 fail-closed step-3 form; the ledger carries the image manifest id | `L6-container-runner-e2e.md` |
|
||||
|
||||
Sequencing: **L0 → build → L1 → L2 → L3 → L4 → L5**, with **L6 after L3** (it needs only the built image
|
||||
+ an in-container daemon, plus the built HOST CLI bin for the read side). L3 is where the stub payload is
|
||||
exercised (and where `docker/testbed/stub-assets.list` is finalized). L4 depends on the 51-02 hermetic
|
||||
env-helper being present in the packaged image (it ships in the openrig package the Dockerfile installs);
|
||||
if it is NOT packaged, its fail-closed leg is exercised in the **step-3 form** — L6.3.
|
||||
|
||||
**51-09 live-leg rider (orch, merge desk).** The testbed image's first multi-host run doubles as the
|
||||
51-09 live-leg executor: L5 boots the containers as the two named hosts `H_A`/`H_B` and then runs the
|
||||
shipped 51-09 collision/alignment runbook **in the same host-side session** —
|
||||
`packages/daemon/test/fixtures/self-host-live-legs/RUNBOOK.md` (LEG A cross-host stamped-triple
|
||||
round-trip, LEG B founder-collision). Execute both families together; return both evidence sets.
|
||||
|
||||
## Shared conventions
|
||||
|
||||
**Image identity.** All legs run against the image the build verb tagged, by manifest identity:
|
||||
```bash
|
||||
GIT_SHA="$(git rev-parse HEAD)"
|
||||
IMAGE="openrig-testbed:${GIT_SHA}" # == manifest.image
|
||||
```
|
||||
Cross-check the running image against the emitted manifest before trusting a leg's result:
|
||||
```bash
|
||||
test "$(docker image inspect "${IMAGE}" --format '{{.Id}}')" # exists
|
||||
cat dist/testbed-image/manifest.json # the census identity this run is comparable under
|
||||
```
|
||||
|
||||
**Evidence dir (hashed, per the census-receipt discipline).** Each leg writes its captures + a
|
||||
`VERDICT` line into a per-run evidence dir, then the operator hashes the dir so the evidence is
|
||||
tamper-evident and comparable across image versions:
|
||||
```bash
|
||||
EVID="dist/testbed-image/evidence/${GIT_SHA}"
|
||||
mkdir -p "${EVID}"
|
||||
# ... leg writes L1-capture.txt, L1-resize.txt, etc. ...
|
||||
( cd "${EVID}" && find . -type f -print0 | sort -z | xargs -0 sha256sum ) > "${EVID}.sha256"
|
||||
```
|
||||
Record in each leg's evidence: the exact command, the captured bytes, and a single
|
||||
`VERDICT: PASS|FAIL — <one line>` on the observed bytes (never from memory).
|
||||
|
||||
**Fences (binding on every leg).** No real-HOME / real-workspace volume mounts (fresh container =
|
||||
fresh HOME is the hermeticity floor); scenarios byte-unchanged; the only shared surface between
|
||||
multi-host containers is the docker network. A leg that cannot run its check reports a **loud
|
||||
blocker** with the exact command + the missing capability — it never green-passes from memory.
|
||||
@@ -1,14 +0,0 @@
|
||||
# openrig-testbed stub runtime assets (plan §1 layer 4: "the 51-01 stub script + its registry
|
||||
# wiring — zero tokens by construction"). This file is the EXACT census scope the manifest hashes
|
||||
# and the build verb stages into the image (census-scope-match-code-path — this list IS the code
|
||||
# path; never a recursive walk that would over-count).
|
||||
#
|
||||
# One repo-relative path per line; comments (#) and blank lines are ignored.
|
||||
#
|
||||
# RESOLVED (2026-08-06 VM-side) — the concrete zero-token stub payload: a runtime:stub rig.yaml +
|
||||
# its agent fixture + culture.md that boot a settled topology for the L3 daemon-in-container leg.
|
||||
# Shape grounded on the proven 51-02 runner fixture (test/fixtures/scenarios/topo-stub-baton.yaml),
|
||||
# not guessed. The base-image digest slot (docker/testbed/base-image) remains host-resolved (L0.1).
|
||||
docker/testbed/stub-assets/rig.yaml
|
||||
docker/testbed/stub-assets/culture.md
|
||||
docker/testbed/stub-assets/agents/worker/agent.yaml
|
||||
@@ -1,8 +0,0 @@
|
||||
name: worker
|
||||
version: "1.0.0"
|
||||
resources:
|
||||
skills: []
|
||||
profiles:
|
||||
default:
|
||||
uses:
|
||||
skills: []
|
||||
@@ -1,6 +0,0 @@
|
||||
# testbed-stub (51-04 container stub payload)
|
||||
|
||||
A minimal zero-token stub-runtime rig baked into the openrig-testbed image so the L3
|
||||
daemon-in-container leg can `rig up` a settled topology without a live LLM. The single
|
||||
seat runs the deterministic 51-01 stub harness. Not a product rig — it exists only to
|
||||
prove the container boots a real daemon and settles a real topology headlessly.
|
||||
@@ -1,23 +0,0 @@
|
||||
# 51-04 testbed image — the container-local zero-token stub topology (plan §1 layer 4).
|
||||
#
|
||||
# One pod (dev) with one stub seat (worker). The stub runtime is the deterministic 51-01
|
||||
# node-script harness, so this topology stands up headlessly in real tmux INSIDE the
|
||||
# testbed container without a live LLM (the L3 daemon-in-container leg: `rig up` settles
|
||||
# it, zero tokens by construction). Shape mirrors the proven 51-02 runner fixture
|
||||
# (test/fixtures/scenarios/topo-stub-baton.yaml), which the scenario runner settles
|
||||
# headlessly end-to-end — not a product rig.
|
||||
#
|
||||
# Canonical session of the seat = <pod>-<member>@<name> = dev-worker@testbed-stub.
|
||||
version: "0.2"
|
||||
name: testbed-stub
|
||||
culture_file: culture.md
|
||||
pods:
|
||||
- id: dev
|
||||
label: Dev
|
||||
members:
|
||||
- id: worker
|
||||
agent_ref: "local:agents/worker"
|
||||
profile: default
|
||||
runtime: stub
|
||||
cwd: .
|
||||
edges: []
|
||||
@@ -1,105 +0,0 @@
|
||||
---
|
||||
kind: as-built
|
||||
title: As-Built Docs — Module Index
|
||||
status: active
|
||||
topics: [knowledge-and-context, observability]
|
||||
domains: [engineering-advisor, operating-advisor, product-advisor]
|
||||
applies-when: |
|
||||
Starting a technical task and choosing the as-built module or source entry
|
||||
point to read. This index describes the document tree; each module carries
|
||||
its own source-verification stamp.
|
||||
siblings: [codemap.md, arteries.md, test-layers.md, cli-reference.md, frontmatter-schema.md]
|
||||
prerequisite-reads: []
|
||||
last-verified-against-source: e8f0ab340db773392ec8be75b072d1c0f3068a50
|
||||
last-updated: 2026-10-08
|
||||
---
|
||||
|
||||
# OpenRig as-built docs
|
||||
|
||||
These pages describe the code and point to its owning files. This index was checked against
|
||||
main commit `e8f0ab340db773392ec8be75b072d1c0f3068a50`. A linked module's
|
||||
`last-verified-against-source` is its own verification boundary; updating this index does not
|
||||
revalidate that module or identify a running daemon or published package.
|
||||
|
||||
For the repository overview and contributor recipes, start with [ARCHITECTURE.md](../../ARCHITECTURE.md).
|
||||
Use [codemap.md](codemap.md) to locate source, [arteries.md](arteries.md) to identify dependent
|
||||
paths before a change, and [test-layers.md](test-layers.md) to choose checks.
|
||||
|
||||
## Repository surfaces
|
||||
|
||||
The npm workspaces are `packages/daemon`, `packages/cli`, `packages/tui`, and `packages/ui`
|
||||
([root package manifest](../../package.json)). The daemon provides the SQLite-backed HTTP
|
||||
services; CLI, TUI and MCP clients consume them. Runtime adapters include Claude Code, Codex,
|
||||
Pi, OMP, terminal and stub implementations under `packages/daemon/src/adapters/`.
|
||||
|
||||
The CLI package contains the assembled daemon, TUI and web UI. User reference documents ship
|
||||
from `docs/reference/`; this as-built tree is developer documentation. The assembly is in
|
||||
[scripts/build-package.sh](../../scripts/build-package.sh), with the published file list in
|
||||
[packages/cli/package.json](../../packages/cli/package.json).
|
||||
|
||||
## Module index
|
||||
|
||||
### Daemon and runtime
|
||||
|
||||
| Module | Read for |
|
||||
|---|---|
|
||||
| [daemon-core.md](architecture/daemon-core.md) | Process startup, service wiring, database migrations and HTTP mounts. |
|
||||
| [adapters-and-runtimes.md](architecture/adapters-and-runtimes.md) | Runtime adapter methods, harness launch/resume and native-session checks. |
|
||||
| [agent-spec-and-startup.md](architecture/agent-spec-and-startup.md) | Spec resolution, resource projection, startup delivery and identity. |
|
||||
| [coordination-primitive.md](architecture/coordination-primitive.md) | Stream, queue, inbox/outbox, handoff and closure. |
|
||||
| [workflow-runtime.md](architecture/workflow-runtime.md) | Workflow specifications, instances, projection, trails and watchdog policies. |
|
||||
| [mission-control.md](architecture/mission-control.md) | Queue observations, actions and their audit records. |
|
||||
| [lifecycle-snapshot-restore.md](architecture/lifecycle-snapshot-restore.md) | Snapshot capture, restore decisions and continuity checks. |
|
||||
| [transport-and-transcripts.md](architecture/transport-and-transcripts.md) | Session messaging, capture, transcripts, chat and ask. |
|
||||
| [workspace-primitive.md](architecture/workspace-primitive.md) | Workspace and repository declarations, project resolution and scope. |
|
||||
| [content-surfaces.md](architecture/content-surfaces.md) | File reads/writes, path checks, progress indexing and steering composition. |
|
||||
| [living-notes-review.md](architecture/living-notes-review.md) | Review gathering/composition, proof records, approvals, export and media. |
|
||||
| [plugin-agent-image-context-pack.md](architecture/plugin-agent-image-context-pack.md) | Plugin discovery, agent images, context packs and compaction policy. |
|
||||
| [packaging-bootstrap-bundles.md](architecture/packaging-bootstrap-bundles.md) | Package assembly, reusable topology bundles and bootstrap/install paths. |
|
||||
| [architecture-rules-and-event-system.md](architecture/architecture-rules-and-event-system.md) | Cross-cutting invariants, events and compatibility notes. |
|
||||
|
||||
### Navigation, checks and reference
|
||||
|
||||
| Document | Read for |
|
||||
|---|---|
|
||||
| [codemap.md](codemap.md) | Question-to-module navigation and source entry points. |
|
||||
| [arteries.md](arteries.md) | Code paths where changes affect several subsystems. |
|
||||
| [test-layers.md](test-layers.md) | Test scope, isolation, scenarios and hosted CI. |
|
||||
| [cli-reference.md](cli-reference.md) | CLI commands and options; confirm changing details in `packages/cli/src/index.ts` and command help. |
|
||||
| [frontmatter-schema.md](frontmatter-schema.md) | Metadata fields used by these documents. |
|
||||
| [architecture.md](architecture.md) | Compatibility pointer from the former monolithic architecture page. |
|
||||
| [DESIGN.md](../DESIGN.md) | Visual and design-system specification. |
|
||||
|
||||
### Web UI reference
|
||||
|
||||
Each page below carries its own verification stamp. This index checks that the pages exist, not
|
||||
their contents.
|
||||
|
||||
| Document | Topic |
|
||||
|---|---|
|
||||
| [ui.md](ui.md) | Compatibility pointer to the UI modules. |
|
||||
| [shell-and-routing.md](ui/shell-and-routing.md) | Web UI shell and routing. |
|
||||
| [topology.md](ui/topology.md) | Web topology views. |
|
||||
| [project-and-for-you.md](ui/project-and-for-you.md) | Project and attention surfaces. |
|
||||
| [library-specs-and-design-system.md](ui/library-specs-and-design-system.md) | Library/spec views and design-system pointers. |
|
||||
|
||||
For the terminal UI, start at `packages/tui/src/main.ts`, its HTTP client
|
||||
`packages/tui/src/daemon-client.ts`, and the TUI sections of [test-layers.md](test-layers.md).
|
||||
|
||||
## Derive the document inventory
|
||||
|
||||
At the named commit there are **26 Markdown files**: **14** under `architecture/`, **4** under
|
||||
`ui/`, and **8** at the root of this tree (including both compatibility pointers). Reproduce
|
||||
these counts from the repository root:
|
||||
|
||||
```bash
|
||||
source_commit=e8f0ab340db773392ec8be75b072d1c0f3068a50
|
||||
git ls-tree -r --name-only "$source_commit" -- docs/as-built | grep -E '\.md$' | wc -l
|
||||
git ls-tree -r --name-only "$source_commit" -- docs/as-built/architecture | grep -E '\.md$' | wc -l
|
||||
git ls-tree -r --name-only "$source_commit" -- docs/as-built/ui | grep -E '\.md$' | wc -l
|
||||
git ls-tree -r --name-only "$source_commit" -- docs/as-built | grep -E '^docs/as-built/[^/]+\.md$' | wc -l
|
||||
```
|
||||
|
||||
Read a module's source stamp and prerequisite pointers before relying on it. Source code at the
|
||||
named commit is the authority when prose disagrees; [frontmatter-schema.md](frontmatter-schema.md)
|
||||
describes how the stamp is represented.
|
||||
+1137
-41
File diff suppressed because it is too large.
Load diff
@@ -1,539 +0,0 @@
|
||||
---
|
||||
kind: as-built
|
||||
title: Adapters and Runtimes — Claude/Codex/Pi/Stub/Terminal, tmux/cmux, Resume Honesty
|
||||
status: active
|
||||
topics: [agent-runtime, runtime-control]
|
||||
domains: [engineering-advisor, operating-advisor]
|
||||
applies-when: |
|
||||
Need the runtime-adapter contract — how OpenRig launches and resumes a
|
||||
Claude Code, Codex, Pi, Oh My Pi, stub or terminal harness inside tmux, what
|
||||
the five required adapter methods do, or how the daemon honestly assesses
|
||||
whether a harness actually resumed vs fresh-launched (the resume-honesty
|
||||
layer).
|
||||
siblings: [daemon-core.md, agent-spec-and-startup.md, lifecycle-snapshot-restore.md]
|
||||
prerequisite-reads: [../README.md, daemon-core.md]
|
||||
last-verified-against-source: e8f0ab340db773392ec8be75b072d1c0f3068a50
|
||||
last-updated: 2026-10-08
|
||||
---
|
||||
|
||||
# Adapters and Runtimes
|
||||
|
||||
How OpenRig drives the agent harnesses. Pod-aware launches go through a
|
||||
`RuntimeAdapter`: five adapter classes implement one contract of five required
|
||||
methods. Legacy (non-pod-aware) restore resumes through separate resume
|
||||
adapters (`ClaudeResumeAdapter`, `CodexResumeAdapter`, `PiResumeAdapter` and
|
||||
`OmpResumeAdapter` in `packages/daemon/src/adapters/*-resume.ts`), which are not
|
||||
`RuntimeAdapter`s. A separate resume-honesty layer answers the question "did
|
||||
this harness *actually* resume, or did it silently fresh-launch?" truthfully
|
||||
rather than optimistically.
|
||||
|
||||
> Verified against source at main `e8f0ab340db773392ec8be75b072d1c0f3068a50`. Each count below sits beside the
|
||||
> command that produces it; run the command from the repository root to refresh
|
||||
> it.
|
||||
|
||||
## 1. The five-method RuntimeAdapter contract
|
||||
|
||||
`RuntimeAdapter` is `packages/daemon/src/domain/runtime-adapter.ts:143`
|
||||
(`interface RuntimeAdapter`). Every adapter declares a `readonly runtime`
|
||||
string (`runtime-adapter.ts:146`) and implements five required methods
|
||||
(`runtime-adapter.ts:152–176`; **5** =
|
||||
`sed -n '/^export interface RuntimeAdapter/,/^}/p' packages/daemon/src/domain/runtime-adapter.ts | grep -c -E '^ [a-zA-Z]+\('`):
|
||||
|
||||
| Method | Signature (`runtime-adapter.ts`) | Responsibility |
|
||||
|---|---|---|
|
||||
| `listInstalled` | `(binding)` `:153` | List currently installed/projected resources for a node. |
|
||||
| `project` | `(plan, binding)` `:156` | Project resources from a `ProjectionPlan` to the runtime's target locations. |
|
||||
| `deliverStartup` | `(files, binding, sendInteractiveText?)` `:160` | Deliver resolved startup files to the runtime. The startup orchestrator passes the optional third argument for Claude Code only, so Claude's `send_text` files go through the checked send described in `agent-spec-and-startup.md`. |
|
||||
| `launchHarness` | `(binding, opts)` `:170` | Launch the harness inside the bound tmux session; return a resume token. |
|
||||
| `checkReady` | `(binding)` `:176` | Probe whether the harness is responsive and ready. |
|
||||
|
||||
The interface also has two optional members: `claudeManagedLaunch` (`:145`)
|
||||
and `skillTargetPath?()` (`:150`), implemented by `PiRuntimeAdapter`
|
||||
(`pi-runtime-adapter.ts:118`) and overridden by `OmpRuntimeAdapter`
|
||||
(`omp-runtime-adapter.ts:12`).
|
||||
|
||||
Startup *action* execution (`slash_command` / `send_text`) is explicitly **not**
|
||||
part of this contract — the contract docstring (`runtime-adapter.ts:137–142`)
|
||||
states actions belong to the `StartupOrchestrator` *after* `checkReady()`. The
|
||||
orchestrator delivery split is in `agent-spec-and-startup.md`.
|
||||
|
||||
### `launchHarness` opts and the fork seam
|
||||
|
||||
`launchHarness` opts is `{ name: string; resumeToken?: string; forkSource?:
|
||||
ForkSource }` (`runtime-adapter.ts:172`).
|
||||
|
||||
Per the contract docstring (`runtime-adapter.ts:162–169`) `resumeToken` and
|
||||
`forkSource` are mutually exclusive — if both are provided the adapter **must
|
||||
refuse** with a clear error, not guess; `forkSource` triggers a fork and the
|
||||
captured token is the NEW post-fork token, never the parent. `ForkSource` is
|
||||
`runtime-adapter.ts:132` (`kind: "native_id" | "artifact_path" | "name" |
|
||||
"last"`, `:133`; v1 MVP accepts `native_id` only — other shapes rejected at
|
||||
schema validation, docstring `:122–131`, and by the Claude, Codex and Pi
|
||||
adapters themselves).
|
||||
|
||||
### `HarnessLaunchResult` is a discriminated union with an honest failure arm
|
||||
|
||||
`HarnessLaunchResult` is a **discriminated union** (`runtime-adapter.ts:97–102`):
|
||||
`| { ok: true; resumeToken?; resumeType?; appliedLaunch? }`
|
||||
`| { ok: false; error: string; recovery?: HarnessLaunchRecovery; evidence? }`.
|
||||
The failure arm carries a typed `recovery` hint
|
||||
(`HarnessLaunchRecovery = "retry_fresh" | "attention_required"`, `:95`) and
|
||||
optional `evidence` (last-N pane lines, flowed through to
|
||||
`RestoreNodeResult.attentionEvidence` for `attention_required` outcomes,
|
||||
`:99–102`). This is the honest-failure shape, not a smoothed optional `error`.
|
||||
|
||||
Readiness has its own attention codes (`runtime-adapter.ts:74–87`):
|
||||
`trust_gate`, `hook_trust_gate`, `update_gate`, `login_required`, `mcp_gate`,
|
||||
`bypass_consent_gate`, `codex_auth_refusal` and `codex_client_incompatible`.
|
||||
When `checkReady` reports one, the readiness wait returns at once and startup
|
||||
ends `attention_required`.
|
||||
|
||||
### Evidence for a failed Claude resume
|
||||
|
||||
Previously, a failed Claude launch recorded its error without the probe code or
|
||||
the captured input's identity, and a refused strict full-restore attention clear
|
||||
had no corresponding refusal event. Probe-backed Claude launch failures now
|
||||
carry an optional `probe` in `HarnessLaunchResult` and `node.startup_failed`.
|
||||
Strict full-restore refusals emit `seat.attention_clear_refused`; a
|
||||
`pane_not_usable` refusal includes that probe and the restore attempt ID.
|
||||
A missing strict reconciler emits `strict_reconciler_unavailable` without
|
||||
inventing an attempt ID or probe evidence, and still refuses the clear.
|
||||
|
||||
`native-resume-probe-evidence.ts` records the status, code, a closed-set login
|
||||
refusal and row kind when matched, and the exact capture's line count, byte
|
||||
count and SHA-256. Its new fields contain no captured pane text or free-form
|
||||
probe detail. Existing attention evidence and recovery decisions keep their
|
||||
previous behavior. MCP approval detection still takes precedence over the
|
||||
login-refusal check.
|
||||
|
||||
Pane dimensions are optional, observed after the capture. Their read has a
|
||||
250 ms deadline so a hung `listPanes` cannot indefinitely delay the failure
|
||||
result or its event. On timeout, error, ambiguous pane identity or invalid
|
||||
dimensions, the unavailable dimensions are `null`; late results are ignored.
|
||||
The deadline bounds this diagnostic wait, not the underlying tmux command.
|
||||
|
||||
`native-resume-probe-evidence.test.ts` covers unavailable and late geometry;
|
||||
`startup-orchestrator.test.ts` and `restore-orchestrator.test.ts` exercise the
|
||||
real adapter/orchestrator and SQLite event paths with available, failing and
|
||||
never-settling geometry reads. `seat-attention-reconciler.test.ts` checks that
|
||||
a missing strict reconciler records its refusal without changing restore
|
||||
history or sending input. These use synthetic terminal captures, not a
|
||||
live Claude session. The stub scenario runtime cannot draw Claude's native
|
||||
login-refusal UI, and its scenario runner does not bind the restore action.
|
||||
|
||||
## 2. The runtime adapters
|
||||
|
||||
**5** classes implement `RuntimeAdapter`
|
||||
(`git grep -l 'implements RuntimeAdapter' packages/daemon/src | wc -l`):
|
||||
`ClaudeCodeAdapter`, `CodexRuntimeAdapter`, `PiRuntimeAdapter`,
|
||||
`StubRuntimeAdapter` and `TerminalAdapter`. All live under
|
||||
`packages/daemon/src/adapters/` (Architecture Rule 1: **0** files there import
|
||||
Hono, `git grep -l 'from "hono' -- packages/daemon/src/adapters | wc -l`).
|
||||
`OmpRuntimeAdapter` (Oh My Pi) extends `PiRuntimeAdapter`, so the daemon wires
|
||||
**6** runtime keys — `claude-code`, `codex`, `pi`, `omp`, `stub`, `terminal`
|
||||
(`startup.ts:989`, and the same map at `:1259`;
|
||||
`grep 'adapters: {' packages/daemon/src/startup.ts | grep -o -E '"[a-z-]+": ' | wc -l`).
|
||||
|
||||
### Launch posture
|
||||
|
||||
A seat's launch posture is `floor`, `auto` or `full_bypass`. It comes from the
|
||||
member's or rig's `permission_policy`: `builtinLaunchPosture()`
|
||||
(`packages/daemon/src/domain/permission-policy/policy-ref.ts:165`) maps
|
||||
`builtin:yolo` to `full_bypass`, `builtin:auto` to `auto`, and every other
|
||||
built-in (`locked`, `standard`, `open`) to `floor`.
|
||||
`resolvePermissionPolicyAttachment()` (`:172`) resolves the attachment, and
|
||||
`rigspec-instantiator.ts` binds the result: the member attachment
|
||||
(`resolveMemberPolicyAttachment`, `:1922`), then the persisted node or rig
|
||||
attachment (`:2007`), then the floor, set on the binding as `launchPosture`
|
||||
(`:2159`). `NativePermissionStore.apply()`
|
||||
(`native-permission-store.ts:238`) then overlays a seat's explicit
|
||||
`rig seat set-permissions` choice, or the kernel operational or team default
|
||||
described below. `bundle-behaviour.ts` is the read-only
|
||||
preview of the same mapping for an unopened bundle (see
|
||||
`packaging-bootstrap-bundles.md`).
|
||||
|
||||
`packages/daemon/src/adapters/yolo-mode.ts` turns the posture into flags:
|
||||
|
||||
- **Claude Code** (`claudePostureFlag`): an explicit `permissionMode` becomes
|
||||
`--permission-mode <mode>` (`:43`); `auto` becomes
|
||||
`--permission-mode auto` (`:47`); `full_bypass` becomes
|
||||
`--dangerously-skip-permissions`; otherwise the floor is
|
||||
`--permission-mode acceptEdits` (`:50`). For a seat with no resolved
|
||||
posture, the instance-wide YOLO setting `OPENRIG_YOLO=1` (`:27`; off by
|
||||
default) selects the bypass; a resolved posture always wins (`:26`).
|
||||
- **Codex** (`codexPostureArg`, `:62`): `full_bypass` becomes
|
||||
`-s danger-full-access -a never` (`:67`); for a seat with no resolved
|
||||
posture, `OPENRIG_YOLO=1` becomes `-s danger-full-access`; otherwise the
|
||||
seat's `-p <profile>` or `-s workspace-write`. Codex with `auto` gets the
|
||||
same: its selected profile, or the `-s workspace-write` floor without one.
|
||||
|
||||
**Non-interruptive mode** (`nonInterruptiveArgs()` in
|
||||
`packages/daemon/src/adapters/non-interruptive.ts:12`) adds per-launch
|
||||
arguments only for a `full_bypass` seat whose rig has it on
|
||||
(`rigs.non_interruptive`): Claude Code gets
|
||||
`--settings '{"skipDangerousModePermissionPrompt":true}'` (unless an explicit
|
||||
non-bypass `permissionMode` is set), and Codex gets
|
||||
`-c notice.hide_full_access_warning=true -c notice.hide_gpt5_1_migration_prompt=true`.
|
||||
It never changes permissions or native settings files. `rig up
|
||||
--non-interruptive` and `--no-non-interruptive` save the choice; otherwise the
|
||||
rig spec's `non_interruptive`, then the setting `launch.non_interruptive`,
|
||||
supplies the default (`packages/daemon/src/domain/bootstrap-orchestrator.ts:739`).
|
||||
|
||||
**Kernel operational default.** `NativePermissionStore.launchOverride()`
|
||||
(`native-permission-store.ts:47`) gives a Claude Code or Codex seat in the
|
||||
persisted rig named `kernel` operational launch arguments when the seat has no
|
||||
explicit permission choice, its member and rig declare no permission policy,
|
||||
and (for Codex) it has no `-p` profile (`launchDefault`, `:28`). Fresh start,
|
||||
continue, restore and handover all take this decision per launch.
|
||||
`operationalLaunchArgs()` (`packages/daemon/src/adapters/kernel-authority.ts:56`)
|
||||
then replaces the non-interruptive arguments for that seat:
|
||||
|
||||
- Claude Code keeps the floor, `--permission-mode acceptEdits`, and gets
|
||||
`--settings` whose `permissions.allow` is `KERNEL_CLAUDE_ALLOW`
|
||||
(`:9`): `Skill`, `Read`, `Edit`, `Write`, `Glob`, `Grep`, `WebFetch`, `Bash(<command>:*)`
|
||||
for 39 operational commands (`rig`, `tmux`, `node`, `npm`, `git`, `ssh`, `python3` and
|
||||
others), `Bash(command -v:*)` (`:16`), two read-only provider login checks (`Bash(claude auth status:*)`,
|
||||
`Bash(codex login status:*)`, `:18`), and `Read(~/**)` (`:19`), plus the `PreToolUse`
|
||||
hook described under the team default. It is not
|
||||
the bypass flag, and the user's own ask and deny rules still apply.
|
||||
- Codex gets the `full_bypass` posture, `-s danger-full-access -a never`, plus
|
||||
the two notice flags above.
|
||||
|
||||
It writes no settings file. If the rig lookup fails, the seat keeps its floor.
|
||||
`rig seat status` reports the source as "kernel operational default"
|
||||
(`kernel_default`, `native-permission-store.ts:182`).
|
||||
|
||||
**Team default.** Under the same conditions, a Claude Code or Codex seat in any
|
||||
other rig gets `teamPermissionDefault` instead (`native-permission-store.ts:38`,
|
||||
`:56`), on the same launch paths. A Claude Code seat at the floor with no native
|
||||
permission mode keeps `--permission-mode acceptEdits` and gets
|
||||
`--settings` (`kernel-authority.ts:64–66`):
|
||||
`TEAM_CLAUDE_ALLOW` (`:33`) is `Skill`, `Read(./**)`, `Glob`, `Grep`,
|
||||
`Bash(rig:*)`, eight read-only shell commands and 17 test-runner commands, and
|
||||
`TEAM_CLAUDE_ASK` (`:40`) asks before 29 `rig` commands: the 23 that start, stop or
|
||||
remove teams or agents (such as `up`, `down`, `seat stop`, `seat restart` and `bundle install`),
|
||||
and six kept for a person: `import` (its `--instantiate` mode launches nodes), `seat
|
||||
set-permissions` and `policy apply` under both spellings (an explicit permission choice),
|
||||
`setup` (kept for its `--policy` path, which records one; a prefix rule can't tell that path
|
||||
from ordinary setup, so ordinary setup prompts too), and `compact` (it replaces a peer's
|
||||
working context). Other `rig` commands fall under the `Bash(rig:*)` allowance. The list was
|
||||
42 before: 15 commands were removed and 2 added (`seat restart` and `policy permissions
|
||||
apply`), 13 fewer prompts. The settings carry the allow list and a
|
||||
`PreToolUse` Bash hook, `packages/daemon/assets/claude-team-permissions.cjs`, that applies
|
||||
both lists (`kernel-authority.ts:43–53`): it asks before a lifecycle command unless the
|
||||
command only asks for `--help` or `-h`, allows a simple literal command that matches an
|
||||
allow rule, and otherwise leaves the decision to Claude (`claude-team-permissions.cjs:234`,
|
||||
`:240`). Without the hook file, the settings carry both lists as plain `allow` and `ask`
|
||||
rules (`kernel-authority.ts:46`). A Codex seat launching at
|
||||
`-s workspace-write` gets `--add-dir` for the workspace root (`workspace.root`)
|
||||
and its pod's state directory, each created first and skipped with a warning if
|
||||
it is the home directory or an ancestor of it
|
||||
(`packages/daemon/src/domain/codex-team-workspace.ts:18`), on fresh, fork and
|
||||
resume launches (`codex-runtime-adapter.ts:378`, `codex-resume.ts:103`).
|
||||
|
||||
### ClaudeCodeAdapter (`claude-code-adapter.ts:55`)
|
||||
|
||||
- `readonly runtime = "claude-code"` (`:56`).
|
||||
- **Projects** to `.claude/` targets: guidance managed blocks →
|
||||
`<cwd>/CLAUDE.md` by default (`:194`, `:501`; default at
|
||||
`domain/managed-blocks.ts:17`, and the rig may select `CLAUDE.local.md`
|
||||
instead with `managed_blocks: { claude-code: <file> }`,
|
||||
`managed-blocks.ts:15`); `skill_install` and skill resources → the seat's
|
||||
skill root, `binding.seatSkillsRoot`, which is the `skills/` folder of the
|
||||
seat's generated plugin in its own storage
|
||||
(`domain/startup-orchestrator.ts:353–369`), or
|
||||
`<cwd>/.claude/skills/<name>/` for a seat without one (`:215–216`,
|
||||
`:849`); subagents → `.claude/agents`, plugins →
|
||||
`.claude/plugins/<id>` (only plugins that apply to Claude), runtime
|
||||
resources → `.claude/extensions/<id>` (`:584–586`); settings fragments
|
||||
merged into `.claude/settings.local.json` (`:594`) and MCP fragments into
|
||||
`<cwd>/.mcp.json` (`:597`).
|
||||
- **Launches**: fresh = `claude <posture> --session-id <generatedId> --name
|
||||
<name>` (`:329`); resume = `claude <posture> --resume <token> --name <name>`
|
||||
(`:328`); fork = `claude <posture> --resume <parentId> --fork-session
|
||||
--session-id <generatedId> --name <seat>`, which needs Claude Code 2.0.73 or
|
||||
later. `<posture>` is the flag above plus any non-interruptive,
|
||||
kernel operational or team default argument. Each command may carry a `CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1`
|
||||
env prefix (`yolo-mode.ts:101–103`; on by default, off with
|
||||
`OPENRIG_CLAUDE_DISABLE_ALTERNATE_SCREEN=0`), a `--model <model>` argument
|
||||
(`:275`) and `--effort <effort>` (`:277`). A fresh launch and a fork both
|
||||
return the generated `--session-id` as their `claude_id` token, so a forked
|
||||
seat's process names its own conversation and strict proof can verify it.
|
||||
Neither reads Claude's session files back.
|
||||
- **Managed launch.** When the binding selects a native permission mode, the
|
||||
command comes from `ClaudeManagedLaunch.prepare()`
|
||||
(`domain/claude-managed-launch.ts:97`), which resolves `claude` on the
|
||||
managed `PATH`, validates the mode against `claude --help` (5-second limit),
|
||||
and launches through `/usr/bin/env -i` with only the seat's environment
|
||||
(`PATH`, `HOME`, an optional `CLAUDE_CONFIG_DIR`, the OpenRig seat identity
|
||||
and forwarded keys).
|
||||
- **Managed bootstrap.** Before delivery the adapter marks the seat's working
|
||||
directory as trusted and onboarding as completed in Claude's state file,
|
||||
writing only when a flag is missing (`provisionManagedBootstrap`, `:697`).
|
||||
It writes no permission settings. The Codex adapter does the same for
|
||||
project trust in its `config.toml` (`codex-runtime-adapter.ts:736`).
|
||||
- **Readiness** (`checkReady`, `:356`): verifies tmux session alive (`:361`),
|
||||
captures the pane command and 40 pane lines, and assesses them with
|
||||
`assessNativeResumeProbe` (§3) through `assessManagedProbe` (called `:368`,
|
||||
defined `:386`); ready only when probe `status === "resumed"`. The
|
||||
resume-launch verification loop `verifyResumeLaunch` (`:420`) retries up to
|
||||
**16 attempts** (`:422`), failing loudly with `recovery: "retry_fresh"` on
|
||||
`no_conversation_found` (`:429–434`) and with `recovery:
|
||||
"attention_required"` plus pane evidence on an `attention_required` probe
|
||||
(`:444–450`) — the adapter never relaunches on its own.
|
||||
- **Fresh in-place handover readiness.** `assessFreshInPlaceProbe` qualifies an
|
||||
otherwise ready history probe only for the matching fresh launch, binding and
|
||||
launch generation. The current pane must show a prompt, and `assessManagedProbe`
|
||||
must confirm the new launch's exact session token in the same foreground process
|
||||
across two observations, with the pane and launch still current. It reuses the
|
||||
existing managed identity proof, including its process fingerprint. A process
|
||||
label alone, old scrollback, an unavailable screen, an unreadable process list
|
||||
or an unidentified Claude process cannot admit the handover. Those cases stay
|
||||
`awaiting_runtime`; a positive identity conflict and visible approval or login
|
||||
gates keep their specific results. The handover waits within its existing
|
||||
readiness budget before sending the context packet.
|
||||
|
||||
The fresh path requires the last `❯` prompt to sit inside Claude's complete
|
||||
input frame: its top rule directly above the prompt and a bottom rule below.
|
||||
A shell's echoed launch command cannot borrow an earlier header or input
|
||||
frame, even when the matching Claude process is already running. A header
|
||||
and prompt alone, or a partially drawn frame, remain pending. This may delay
|
||||
readiness until the full frame is visible; other launch paths keep their
|
||||
existing screen predicates.
|
||||
|
||||
Previously, unavailable observations and ambiguous headerless prompts could
|
||||
retain the old ready result. `claude-fresh-in-place-scope.test.ts` now checks
|
||||
missing observations, unknown or changing process identity, retained UI above
|
||||
direct and script launch commands, partial frames, recovery and normal prompt
|
||||
controls. `claude-handover-readiness.test.ts` exercises the real handover
|
||||
service with migrated SQLite, modeled pane/process observations and one synthetic
|
||||
clock. Readiness and the later packet-submission receipt remain separate checks;
|
||||
these tests do not run a live Claude account.
|
||||
|
||||
### CodexRuntimeAdapter (`codex-runtime-adapter.ts:65`)
|
||||
|
||||
- `readonly runtime = "codex"` (`:66`).
|
||||
- **Projects** to `.agents/` targets: `guidance_merge` → `<cwd>/AGENTS.md`
|
||||
(`:309`, `:576`); `skill_install` and skill resources → the seat's skill
|
||||
root, `binding.seatSkillsRoot`, which is the `skills/` folder beside the
|
||||
seat's catalog in its own storage
|
||||
(`domain/startup-orchestrator.ts:360–369`), or
|
||||
`<cwd>/.agents/skills/<name>/` for a seat without one (`:563–564`,
|
||||
`:1060`); plugins go to `.codex/plugins/<id>` (`:1063`).
|
||||
- **Launches/resumes**: fresh = `codex[ --no-daemon]<posture>[<network>] -C
|
||||
<cwd> …` (`:442`), then capture a fresh thread id (`:460`); resume is built
|
||||
by `buildCodexResumeCore` (`:441`) as `codex<daemon><posture> resume
|
||||
[<queueStateDirArg>] <token>` (`native-resume-probe.ts:81`); fork =
|
||||
`codex<posture> fork<queueStateDirArg> <parentId>` (`:411`). `--no-daemon`
|
||||
(`:389`) comes first, and only when `codex --help` lists it
|
||||
(`domain/codex-daemon-support.ts`; an unreadable answer refuses the launch).
|
||||
`-m <model>` (`:356`) and `-c 'model_reasoning_effort="<effort>"'`
|
||||
(`:358`) are added when selected. With a thread id it returns `{ ok: true,
|
||||
resumeToken: threadId, resumeType: "codex_id" }` (`:428`, `:457`, `:463`); a
|
||||
fresh launch whose thread id is not captured returns `{ ok: true }` with its
|
||||
applied launch but no token (`:466`). Every Codex launch, fresh, fork or resume,
|
||||
also carries `-c check_for_update_on_startup=false`, so a managed seat leaves Codex
|
||||
upgrades to the operator (`kernel-authority.ts:60`, added to the posture at
|
||||
`codex-runtime-adapter.ts:363`).
|
||||
- **Network default.** On the plain `-s workspace-write` floor,
|
||||
`domain/codex-network-default.ts` asks a one-shot `codex app-server` for the
|
||||
effective configuration; when it allows it, the launch adds
|
||||
`-c sandbox_workspace_write.network_access=true` (`:19`). Otherwise it adds
|
||||
nothing.
|
||||
- Refuses `resumeToken` + `forkSource` together with a clear error
|
||||
(`:350–351`) — honors the mutual-exclusivity contract.
|
||||
|
||||
### TerminalAdapter (`terminal-adapter.ts:19`)
|
||||
|
||||
- `readonly runtime = "terminal"` (`:20`).
|
||||
- **All operations are no-ops** — "the shell is immediately interactive"
|
||||
(`terminal-adapter.ts:15`): no-op `project`/`deliverStartup`, a
|
||||
`launchHarness` (`:34`) that returns `{ ok: true }`, and a `checkReady` that
|
||||
returns ready unconditionally (`:47–48`). Used for infrastructure nodes. A
|
||||
terminal node cannot fork: `launchHarness` refuses a `forkSource` with a
|
||||
clear error (`:38–42`), as the runtime-adapter docstring requires of
|
||||
adapters without fork (`runtime-adapter.ts:129–130`).
|
||||
|
||||
### Pi, Oh My Pi and stub adapters
|
||||
|
||||
- `PiRuntimeAdapter` (`pi-runtime-adapter.ts:64`), `runtime` `"pi"` (`:65`).
|
||||
- `OmpRuntimeAdapter` (`omp-runtime-adapter.ts:7`) extends it with
|
||||
`runtime = "omp"` (`:8`).
|
||||
- `StubRuntimeAdapter` (`stub-runtime-adapter.ts:69`), `runtime` defaulting to
|
||||
`"stub"` (`:81`).
|
||||
|
||||
Pi and stub launch an OpenRig runner inside the seat's tmux pane (header
|
||||
comments `pi-runtime-adapter.ts:1–10`, `stub-runtime-adapter.ts:1–14`). Pi
|
||||
and Oh My Pi support fork and report `pi_session_file` or `omp_session_file`
|
||||
resume tokens; the stub refuses fork and reports `stub_session`.
|
||||
|
||||
`createDaemon` constructs the adapters (`startup.ts:778`, `:779`, `:782`,
|
||||
`:783`, `:788`) and creates the terminal adapter inline in the runtime adapter
|
||||
maps (`startup.ts:989`, `:1259`); see `daemon-core.md` §4 "Startup sequence".
|
||||
|
||||
### Terminal providers
|
||||
|
||||
Opening a seat in a terminal app goes through `TerminalProvider`s under
|
||||
`packages/daemon/src/domain/terminal/`, served by `/api/terminal`:
|
||||
`HerdrAdapter` (`herdr-adapter.ts:351`), the default
|
||||
(`terminal-service.ts:55–58`), and `CmuxProviderAdapter`
|
||||
(`cmux-provider-adapter.ts:52`), which is best effort and refuses with
|
||||
`cmux_unavailable` when cmux is not connected. A request that names no
|
||||
provider takes the first of Herdr, then cmux, that is available and alive,
|
||||
and stays on Herdr when neither is (`providerNameFor`,
|
||||
`terminal-service.ts:175`); a named provider is used as named. These place existing tmux
|
||||
sessions in views; they do not launch harnesses.
|
||||
|
||||
`RexAdapter` (`rex-adapter.ts`) opens a view as a new Rex session through
|
||||
the `rex` CLI (`rex --autostart=false api call <method> -`, `rex-transport.ts`):
|
||||
it creates an empty session (`session.create`), adds one window per grid page
|
||||
(`session.new_window`), and checks the tiles once after a short wait. It is used only when named;
|
||||
the CLI and TUI name it when they run inside Rex (`openRexView`,
|
||||
`packages/cli/src/terminal-window-rex.ts`), and an omitted provider never picks
|
||||
it. It refuses with `rex_unavailable` when the Rex server does not answer.
|
||||
Discovery (`server.version` and `server.status`) has a 1 s budget, and
|
||||
`status()` and `liveness()` share one probe. A request whose reply is lost (a
|
||||
timeout, a killed client, an unreadable reply) is `unknown`, not failed: the
|
||||
adapter never closes a session on it and reports `rex_outcome_unknown`.
|
||||
Herdr and Rex build their layout trees from one grid helper
|
||||
(`buildTileGrid`, `tile-layout.ts`).
|
||||
|
||||
### Tmux server selection
|
||||
|
||||
Before reconciliation, `startup.ts` calls `selectTmuxServer` in
|
||||
`domain/tmux-server-selection.ts`. `terminal.tmux_server` defaults to `inherit`;
|
||||
`isolated` selects a socket derived from the canonical OpenRig home. The effective
|
||||
choice is persisted in `tmux-server.json`. A registered live seat, or an
|
||||
uninspectable old server, defers a requested change until a later restart.
|
||||
The persisted `pinned` flag and active marker make isolation and migration records
|
||||
mandatory, while ordinary inherited records remain advisory. Returning to inheritance
|
||||
commits an unpinned record before clearing the marker, so offline cleanup cannot reuse
|
||||
a stale inherited socket or keep enforcing a completed isolation choice.
|
||||
|
||||
`TmuxAdapter` receives the selection's `executionArgs` for both shell and argv
|
||||
transports, including its direct probe/cleanup calls. They are empty for ordinary
|
||||
`inherit` mode, preserving tmux's environment-based selection; `serverArgs` still
|
||||
names the server for attachments. The opt-in control client uses the same execution
|
||||
arguments for its connection, probes and fallback commands. The same startup choice feeds node
|
||||
inventory, `TerminalService.status()` and previews, composed panes, shared TUI attachment and
|
||||
CLI viewer management. Inherited SSH panes retain the remote-tmux replacement
|
||||
wrapper and plain recovery command without requiring remote OpenRig. Isolated
|
||||
SSH panes resolve the remote instance through its own `rig terminal attach`.
|
||||
For isolation, self-attachment checks the caller's socket before binding;
|
||||
offline destruction reads the persisted socket before removing state.
|
||||
|
||||
See [tmux server selection](../../reference/tmux-servers.md) for configuration
|
||||
loading, first-upgrade migration, discovery scope and platform behavior.
|
||||
|
||||
## 3. Resume honesty
|
||||
|
||||
The daemon does not assume a harness resumed just because the launch command
|
||||
ran. Three domain files (`packages/daemon/src/domain/`) carry the resume
|
||||
assessment:
|
||||
|
||||
### `native-resume-probe.ts`
|
||||
|
||||
`assessNativeResumeProbe(input)` (`native-resume-probe.ts:84`) reads pane
|
||||
command + pane content and returns one of four honest statuses
|
||||
(`NativeResumeProbeStatus`, `:7`):
|
||||
|
||||
- `resumed` — runtime-specific indicators confirm a resumed session.
|
||||
- `failed` — terminal failure (e.g. Claude printed "No conversation found" →
|
||||
code `no_conversation_found`, `:99–104`).
|
||||
- `inconclusive` — we don't know yet (e.g. Claude trust gate, code
|
||||
`trust_gate`, `:113–118`).
|
||||
- `attention_required` — alive and recoverable but **needs operator action**
|
||||
(e.g. Claude resume-selection prompt, code `claude_resume_selection_prompt`,
|
||||
`:106–111`). This is the proxy for "an operator must choose the
|
||||
conversation"; it is *distinct* from `inconclusive` and `failed` (comment
|
||||
`:4–6`).
|
||||
|
||||
Other codes include `bypass_consent_gate`, `mcp_gate`, `login_required` and
|
||||
`returned_to_shell` for Claude, and `codex_client_incompatible`,
|
||||
`no_saved_session`, `codex_auth_refusal` and `model_selection_gate` for Codex;
|
||||
an unknown runtime gets `unsupported_runtime`. For Claude, `assessManagedProbe`
|
||||
upgrades an auto-mode or composer-only screen to `resumed` only after a stable
|
||||
native process match on a single pane.
|
||||
|
||||
`buildNativeResumeCommand` (`:32`) builds the resume command per runtime:
|
||||
claude → `claude --resume <token> [--name <name>]` (`:41`); codex →
|
||||
`buildCodexResumeCore` (`:44`), i.e. `codex<posture> resume <token>`
|
||||
(`:81`); other runtimes → `null` (`:46`).
|
||||
|
||||
At the adapter level (Architecture Rule 15, §4), the adapter's
|
||||
`verifyResumeLaunch` returns `ok:false` with a `retry_fresh` recovery hint and
|
||||
never relaunches itself. Acting on that hint is the caller's choice:
|
||||
`StartupOrchestrator` retries once fresh on `retry_fresh` unless the caller
|
||||
passes `allowFreshFallback: false` (`startup-orchestrator.ts:319–330`), and the
|
||||
restore orchestrator passes `false` when a pod-aware node requested resume
|
||||
(`restore-orchestrator.ts:1224`).
|
||||
|
||||
### `resume-metadata-refresher.ts`
|
||||
|
||||
`ResumeMetadataRefresher` (`resume-metadata-refresher.ts:59`) records which
|
||||
sessions can be resumed, without overwriting a token it can't re-derive
|
||||
exactly. `refresh(sessions, opts?)` (`:124`) works in two modes:
|
||||
|
||||
- **At shutdown** (default): a Codex session without a token gets its thread
|
||||
id captured. For a Claude session with a token, it first reads Claude's live
|
||||
per-process session file, and only if that read fails runs
|
||||
`probeClaudeResume` (`:216`) — a real launch of the resume command in a
|
||||
throwaway probe tmux session (`rigged-refresh-<name>-<ts>`, `:301`),
|
||||
returning `"resumable" | "not_resumable" | "inconclusive"`. The probe result
|
||||
is recorded without clearing the token.
|
||||
- **`fillNullOnly`** (`:127`): a missing Claude token is filled from the
|
||||
status-line sidecar, and a present Claude or Codex token is re-stamped
|
||||
resumable only when it matches what the runtime reports. No probe runs.
|
||||
|
||||
### `codex-thread-id.ts`
|
||||
|
||||
Codex thread-id extraction (`codex-thread-id.ts`). Reads the Codex thread id
|
||||
from the Codex *logs* SQLite databases in the Codex home (`CODEX_HOME` when
|
||||
set, else `~/.codex`, `:40`): `readCodexThreadIdFromCandidateHomes(...)`
|
||||
(`:34`) → `readCodexThreadIdFromLogs(...)` (`:244`) →
|
||||
`resolveCodexDbPaths(homeDir, kind)` (`:302`), which globs
|
||||
`logs_<N>.sqlite` (`:308`) and falls back to `logs_1.sqlite`. The logged
|
||||
thread ids are then checked against the `threads` table in `state_<N>.sqlite`
|
||||
(falling back to `state_5.sqlite`, `:320`; query `:269–280`); an id is
|
||||
returned only when exactly one CLI conversation matches (`:282`). Uses
|
||||
`better-sqlite3` (`:6`). Resolves the home dir by the harness PID
|
||||
(`defaultResolveHomeDirByPid`, `:16`).
|
||||
|
||||
## 4. Relevant Architecture Rules
|
||||
|
||||
The rule texts live in `architecture-rules-and-event-system.md` §1; this
|
||||
section cites them by number with the adapter-level evidence checked for this
|
||||
module.
|
||||
|
||||
- **Rule 1** — §2: **0** files in `adapters/` import Hono.
|
||||
- **Rule 5** — restore picks a node's adapter by that node's own runtime: the
|
||||
saved startup context's runtime, else the node's (`restore-orchestrator.ts:1115–1116`),
|
||||
used to choose the replay adapter (`:1138`).
|
||||
- **Rule 13** — the readiness loop is `StartupOrchestrator.waitForReady`
|
||||
(`startup-orchestrator.ts:562`; 1 s doubling to a 16 s cap), which calls each
|
||||
adapter's `checkReady` (§2). The timeout comes from the
|
||||
`runtime.readiness_timeout_seconds` setting (default 30, range 1–600,
|
||||
`domain/readiness-timeout.ts:3`).
|
||||
- **Rule 14** — the probe's `attention_required` (§3) reaches the restore
|
||||
result through `HarnessLaunchResult.recovery` and `evidence` (§1), which
|
||||
become `RestoreNodeResult.status` (`types.ts:457`, `:475`) and
|
||||
`attentionEvidence` (`types.ts:478`). A startup's `continuityOutcome` is a
|
||||
separate field that also allows `forked` (`startup-orchestrator.ts:101`).
|
||||
- **Rule 15** — adapters return `ok:false` and never relaunch (§2, §3);
|
||||
restore passes `allowFreshFallback: false` for pod-aware resume
|
||||
(`restore-orchestrator.ts:1224`), and a resume that concludes failed rolls
|
||||
back to zero sessions as `awaiting-decision` (`restore-orchestrator.ts:1055`,
|
||||
`:1243`).
|
||||
|
||||
## See also
|
||||
|
||||
- `daemon-core.md` — where `createDaemon` constructs the runtime adapters.
|
||||
- `agent-spec-and-startup.md` — the `StartupOrchestrator` that calls these
|
||||
adapters and owns startup-action execution after `checkReady()`.
|
||||
- `lifecycle-snapshot-restore.md` — how persisted resume tokens flow into
|
||||
snapshot/restore (resume vs rebuild vs fresh).
|
||||
- `architecture-rules-and-event-system.md` — the full architecture rule list.
|
||||
- Source roots: `packages/daemon/src/domain/runtime-adapter.ts`,
|
||||
`packages/daemon/src/adapters/{claude-code-adapter,codex-runtime-adapter,pi-runtime-adapter,omp-runtime-adapter,stub-runtime-adapter,terminal-adapter,yolo-mode,non-interruptive,kernel-authority}.ts`,
|
||||
`packages/daemon/src/domain/{native-resume-probe,resume-metadata-refresher,codex-thread-id,claude-managed-launch,codex-network-default,native-permission-store}.ts`,
|
||||
`packages/daemon/src/domain/permission-policy/policy-ref.ts`.
|
||||
@@ -1,403 +0,0 @@
|
||||
---
|
||||
kind: as-built
|
||||
title: Agent/Rig Spec, Resolution, Startup, Identity
|
||||
status: active
|
||||
topics: [specification-and-bundles, agent-runtime]
|
||||
domains: [engineering-advisor, operating-advisor]
|
||||
applies-when: |
|
||||
Need to know the canonical AgentSpec/RigSpec/pod-aware reboot types, how
|
||||
profile resolution and additive startup layering work, the StartupOrchestrator
|
||||
pre-launch-vs-interactive delivery split, or how whoami/materialize/bind/adopt
|
||||
resolve and preserve identity.
|
||||
siblings: [daemon-core.md, adapters-and-runtimes.md, lifecycle-snapshot-restore.md, packaging-bootstrap-bundles.md]
|
||||
prerequisite-reads: [../README.md, daemon-core.md]
|
||||
last-verified-against-source: e8f0ab340db773392ec8be75b072d1c0f3068a50
|
||||
last-updated: 2026-10-08
|
||||
---
|
||||
|
||||
# Agent/Rig Spec, Resolution, Startup, Identity
|
||||
|
||||
How the daemon turns authored YAML specs into a resolved, projected, launched,
|
||||
and identity-addressable topology. The spec-and-startup contract: parse →
|
||||
resolve → project → deliver pre-launch files → persist replay context → launch
|
||||
→ wait → deliver post-launch files.
|
||||
|
||||
> Verified against source at main `e8f0ab340db773392ec8be75b072d1c0f3068a50`. `domain/…` and `routes/…` paths
|
||||
> are under `packages/daemon/src/`; a bare file name such as
|
||||
> `rigspec-schema.ts:119` is in `packages/daemon/src/domain/`. Each count sits
|
||||
> beside the command that produces it; run the command from the repository
|
||||
> root to refresh it.
|
||||
|
||||
## 1. Canonical spec and topology types
|
||||
|
||||
- **AgentSpec** (`types.ts:941`) — parsed from `agent.yaml`
|
||||
(`agent-resolver.ts:150`). Owns imports, defaults, startup, resources, and
|
||||
profiles. Canonical parse/normalize/validate is `domain/agent-manifest.ts`.
|
||||
- **RigSpec** (`types.ts:1172`) — canonical pod-aware rig topology. Uses
|
||||
`version: "0.2"` and `pods[]`; owns cross-pod `edges[]`, rig-level startup
|
||||
overlays, and `cultureFile`, plus the optional `summary`, `permissionPolicy`,
|
||||
`nonInterruptive`, `managedBlocks`, `docs`, `services` and `workspace` fields.
|
||||
An authored `non_interruptive` must be a boolean (`rigspec-schema.ts:179`) and
|
||||
sets the rig's saved non-interruptive choice at instantiation unless an
|
||||
explicit launch option is given (`rigspec-instantiator.ts:1345`).
|
||||
|
||||
`version` is the spec-schema version, not the package version. It is not a
|
||||
code constant: `RigSpecSchema.validate` requires only a non-empty string
|
||||
(`rigspec-schema.ts:119`) and `RigSpecSchema.normalize` carries the authored
|
||||
value through (`rigspec-schema.ts:253`). `"0.2"` is the canonical authored
|
||||
value, and the pod-aware exporter writes it (`rigspec-exporter.ts:223`).
|
||||
|
||||
- **RigServicesSpec** (`types.ts:1221`) — optional `services` block on a
|
||||
pod-aware RigSpec. Shipped kind is Compose-backed env management
|
||||
(`kind: "compose"`) with `composeFile`, `projectName?`, `profiles?`,
|
||||
`downPolicy?`, `waitFor?`, `surfaces?`, `checkpoints?`.
|
||||
- **RigSpecPod** (`types.ts:1128`) — pod-local bounded context with
|
||||
`members[]`, pod-local `edges[]`, pod startup, optional continuity policy.
|
||||
- **RigSpecPodMember** (`types.ts:1072`) — member-level runtime/startup
|
||||
surface: `agentRef`, `profile`, `runtime`, `model?`, `effort?`, `cwd`, `restorePolicy?`,
|
||||
member startup overlays (`startup?`), plus `label?`, `codexConfigProfile?`,
|
||||
`role?`, `permissionPolicy?`, `compactionStrategy?`, `mechanic?`,
|
||||
`sessionSource?` and `starterRef?`.
|
||||
- **Pod** (`types.ts:10`) — persisted DB entity for a pod.
|
||||
- **ContinuityState** (`types.ts:20`) — persisted live continuity row keyed by
|
||||
`podId + nodeId` (`PRIMARY KEY (pod_id, node_id)`,
|
||||
`packages/daemon/src/db/migrations/014_agentspec_reboot.ts:24`).
|
||||
|
||||
## 2. Execution and projection types
|
||||
|
||||
The member's optional `claude_mcp: none` is normalized as `claudeMcp` and
|
||||
persisted independently in `nodes.claude_mcp` (migration 142). The
|
||||
expansion HTTP route preserves both `claude_mcp` and `claudeMcp`, including
|
||||
invalid values for the canonical validator to reject. An authored relaunch
|
||||
updates the seat choice after launch eligibility and startup-source resolution;
|
||||
an already-bound seat or a failed tmux launch keeps its prior value. Export
|
||||
describes the current runtime; a dormant choice on another runtime is omitted
|
||||
and must be restored in the Claude declaration when rebuilding from that export.
|
||||
The YAML export route names those agents in the existing `# Not carried:` report;
|
||||
the JSON export is unchanged. `rigspec-routes.test.ts` covers both pod-aware and
|
||||
legacy exports, omission controls and successful JSON export.
|
||||
|
||||
The shared `StartupOrchestrator` reads the current seat record, so both an authored start
|
||||
and saved-context restore pass the choice to Claude. Legacy resume and handover
|
||||
successors read it separately; a runtime-switch successor reads it only when
|
||||
the target is Claude. `claude-mcp.ts` builds the strict empty-server arguments
|
||||
for the adapters, shutdown resume probes and displayed recovery commands,
|
||||
including the conversation picker. Shutdown skips a probe with an unreadable
|
||||
saved choice and keeps the resume token without adding a shutdown refusal.
|
||||
The native lineage selectors recognize these exact
|
||||
arguments without treating JSON contents as session options. Invalid stored
|
||||
values refuse launch. With no choice, the adapters add no MCP arguments.
|
||||
|
||||
`claude-mcp-lifecycle.test.ts` exercises real schema/storage, export, snapshot
|
||||
and restore with simulated tmux/provider processes. `claude-mcp-launch.test.ts`
|
||||
executes managed and unmanaged shell commands against a private argv-printing
|
||||
fake, including fresh/resume/fork and legacy resume. These tests verify command
|
||||
plumbing and identity handling; they do not claim live provider enforcement.
|
||||
|
||||
Restore/snapshot types are detailed in `lifecycle-snapshot-restore.md`; the
|
||||
spec/projection types live here.
|
||||
|
||||
- **ResolvedNodeConfig** (`profile-resolver.ts:33`) — output of profile
|
||||
resolution. Carries effective runtime/model/effort/cwd, narrowed restore policy,
|
||||
selected resources, layered startup block, resolved spec identity, and the
|
||||
resolved compaction strategy, continuity mechanic, lifecycle, activity and
|
||||
skill loadout.
|
||||
- **ProjectionPlan** (`projection-planner.ts:42`) — runtime projection plan for
|
||||
a node: runtime, cwd, projection entries, startup block, diagnostics,
|
||||
conflict/no-op classifications.
|
||||
- **RuntimeAdapter** (`runtime-adapter.ts:143`) — the five-method contract
|
||||
(adapter detail in `adapters-and-runtimes.md`): `listInstalled(binding)`
|
||||
(`runtime-adapter.ts:153`), `project(plan, binding)` (`:156`),
|
||||
`deliverStartup(files, binding, sendInteractiveText?)` (`:160`),
|
||||
`launchHarness(binding, opts)` (`:170`), `checkReady(binding)` (`:176`). The
|
||||
interface also declares an optional `claudeManagedLaunch` property (`:145`)
|
||||
and an optional method, `skillTargetPath?(...)` (`:150`). Required methods:
|
||||
**5**
|
||||
(`sed -n '/^export interface RuntimeAdapter /,/^}/p' packages/daemon/src/domain/runtime-adapter.ts | grep -c -E '^ [a-zA-Z]+\('`).
|
||||
- **HarnessLaunchResult** (`runtime-adapter.ts:97`) — returned by
|
||||
`launchHarness`: either `{ ok: true, resumeToken?, resumeType?,
|
||||
appliedLaunch? }` or `{ ok: false, error, recovery?, evidence? }`, where
|
||||
`recovery` is `"retry_fresh"` or `"attention_required"` (`:95`).
|
||||
- **StartupOrchestrator** (`startup-orchestrator.ts:140`) — drives the full
|
||||
startup sequence (§4 below).
|
||||
|
||||
## 3. Parsing, validation, resolution pipeline
|
||||
|
||||
All of the files below are under `packages/daemon/src/domain/`, and none of
|
||||
them imports Hono: **0**
|
||||
(`git grep -l '"hono' -- packages/daemon/src/domain/{agent-manifest,rigspec-schema,rigspec-codec,startup-validation,path-safety,spec-validation-service,spec-review-service,agent-resolver,agent-preflight,profile-resolver,startup-resolver,projection-planner}.ts | wc -l`).
|
||||
|
||||
**Parse / validate:**
|
||||
|
||||
- `agent-manifest.ts` — canonical AgentSpec parse/normalize/validate.
|
||||
- `rigspec-schema.ts` — dual-format RigSpec validation.
|
||||
- `rigspec-codec.ts` — dual-format YAML codec.
|
||||
- `startup-validation.ts` — shared startup-block validation.
|
||||
- `path-safety.ts` — shared relative-path safety checks.
|
||||
- `spec-validation-service.ts` — pure raw-YAML validation helpers.
|
||||
- `spec-review-service.ts` — daemon-owned structured review model for
|
||||
RigSpec/AgentSpec YAML, incl. topology preview, provenance state, managed-app
|
||||
services metadata (`waitFor`, `surfaces`, `composePreview`).
|
||||
|
||||
**Resolve:**
|
||||
|
||||
- `agent-resolver.ts` — resolves `agent_ref`, imports, collision metadata.
|
||||
- `agent-preflight.ts` — single-agent resolution/preflight.
|
||||
- `profile-resolver.ts` — applies defaults, profile uses, resource selection,
|
||||
startup layering, restore-policy narrowing.
|
||||
- `startup-resolver.ts` — additive startup layering.
|
||||
- `projection-planner.ts` — runtime resource projection planning.
|
||||
|
||||
### Profile resolution during launch
|
||||
|
||||
Preflight already resolves profiles in `profile-resolution-worker.ts`. Previously,
|
||||
the pod-aware apply path, materialized-member launch and first-start retry called
|
||||
the synchronous resolver again on the daemon's request thread. A fallback skill
|
||||
scan or managed-catalog inspection could therefore delay every other request even
|
||||
after preflight had finished ([#1653](https://github.com/mvschwarz/openrig/issues/1653)).
|
||||
Those three launch paths now await the same resolver in a worker before projecting
|
||||
resources or starting the member. Configuration precedence and launch order still
|
||||
come from the existing resolver and instantiator.
|
||||
|
||||
An apply operation reuses one lazy worker across its members and terminates it on
|
||||
success or failure. Unlike preflight, launch supplies no `scanScope`: each member
|
||||
reads current files, including changes made by an earlier member's startup.
|
||||
Standalone member launches and first-start retries use a short-lived worker for
|
||||
their resolution. This adds worker startup and context-copy costs; it does not
|
||||
reuse the preflight result or cache configuration between requests. The helper's
|
||||
existing synchronous fallback for a deleted process working directory remains.
|
||||
|
||||
Worker errors become configuration-failure results. Apply can then follow the
|
||||
existing all-members-failed cleanup, including service rollback and restoration
|
||||
of replaced generations. Materialized launches report a failed member, and
|
||||
first-start retries refuse before writing retained state.
|
||||
|
||||
`profile-resolution-worker.test.ts` holds a real fallback scan open with a FIFO,
|
||||
requires an HTTP health response while that FIFO is still blocked, then checks
|
||||
that the selected skill reaches projection. It covers all three launch paths and
|
||||
a skill edit between members. These fixtures use SQLite and the real instantiator
|
||||
with a simulated runtime; they do not establish native provider behavior or
|
||||
responsiveness during other synchronous parts of startup.
|
||||
|
||||
## 4. Startup orchestration (the spec-startup contract)
|
||||
|
||||
`StartupOrchestrator.startNode` (`startup-orchestrator.ts:172`, a thin wrapper
|
||||
over `startNodeWithWarnings`, `:178`) drives, in source order: apply the seat's
|
||||
permission selection and the rig's non-interruptive setting to the binding
|
||||
(`NativePermissionStore.apply`, `:183`; `:193`) → mark pending (`:209`) and
|
||||
select the startup proof (`:210`) → project resources (`:224`) → deliver
|
||||
pre-launch files (`:266`) → persist startup context (`:282`) → launch harness,
|
||||
recording any resume token (`:301`, `:313`) → wait for ready (`:384`) → deliver
|
||||
the session identity prompt and interactive files (`:420`, `:473`) → execute
|
||||
`after_files` then the remaining `after_ready` actions (`:495`, `:502`; see
|
||||
the resume preload below) → check readiness
|
||||
again and, for a Claude resume, that the launched session agrees with the
|
||||
requested token (`:507`–`535`) → mark ready (`:538`). The class doc comment
|
||||
(`startup-orchestrator.ts:120`–`139`) lists ten steps; the code persists the
|
||||
startup context before the harness launch.
|
||||
|
||||
**Pre-launch vs interactive delivery split** — the load-bearing seam:
|
||||
|
||||
- Pre-launch (filesystem, before harness boot): `guidance_merge`,
|
||||
`skill_install` (`startup-orchestrator.ts:126` — "Deliver pre-launch files
|
||||
(guidance_merge, skill_install → filesystem)"; delivered at `:266`). Any
|
||||
file that is not `send_text` is delivered here.
|
||||
- Post-launch (TUI, after harness is ready): `send_text`
|
||||
(`startup-orchestrator.ts:130`; held for post-launch and delivered after
|
||||
readiness at `:473`). Rebuild artifacts are put in front of these files.
|
||||
- **Fresh launch:** the builtin `session_identity` action
|
||||
(`rigspec-instantiator.ts:2655`) is sent as one turn together with the first
|
||||
`send_text` file (`deliverInitialSessionPrompt`, `startup-orchestrator.ts:663`).
|
||||
- **Resume, fork or rebuild:** applicable `after_ready` `send_text` actions are
|
||||
sent ahead of the first `send_text` file as one turn.
|
||||
|
||||
The orchestrator persists replay context for future restores (consumed by
|
||||
`lifecycle-snapshot-restore.md`): the projection entries, resolved startup
|
||||
files, startup actions and runtime go to `node_startup_context`
|
||||
(`startup-orchestrator.ts:282`), and a resume token returned by the launch is
|
||||
written to the session row (`updateResumeToken`, `:313`).
|
||||
|
||||
**Checked startup text (Claude Code).** Interactive text for a Claude seat goes
|
||||
through `sendInteractiveText` (`:777`): paste, wait 200 ms, press Enter, wait
|
||||
200 ms, then read 200 lines of the pane. `inspectStartupStagedText`
|
||||
(`startup-submission-evidence.ts:36`) classifies the composer as clear, staged
|
||||
or unverified. While the composer shows Claude's own collapsed-paste label for
|
||||
this text (`startupOwnCollapsedPaste`, `:49`), `settleOwnPaste`
|
||||
(`startup-orchestrator.ts:867`) looks again, up to 25 times 200 ms apart,
|
||||
without pressing anything. Text still staged gets one guarded Enter-only retry
|
||||
(`:823`), and then a "press Enter in that pane" warning (`:836`). An
|
||||
unverified or staged result is a warning, never a startup failure, and is
|
||||
reported in a `submission` block on `node.startup_ready` and on the result.
|
||||
|
||||
**Startup proof.** A seat can be `ready` without being oriented.
|
||||
`resolveStartupProof` (`startup-resolver.ts:81`) selects the last applicable
|
||||
authored `startup_proof` action; the default is none. For a fresh launch on a
|
||||
non-terminal runtime in `authenticated` mode, the orchestrator issues a
|
||||
challenge after readiness (`issueStartupChallenge`, `startup-proof.ts:90`),
|
||||
recorded as `node.startup_challenged` with a hash of the resolved startup files.
|
||||
The expected answer is derived from the challenge and that hash
|
||||
(`computeExpectedAnswer`, `:53`) and is never stored. The challenge text is
|
||||
appended to the identity prompt, or sent on its own when there is no identity
|
||||
action (`startup-orchestrator.ts:487`). On Claude, a short proof instruction
|
||||
line (`STARTUP_PROOF_INSTRUCTION_LINE`, `startup-proof.ts:63`) is sent only
|
||||
after the startup prompt was confirmed submitted (`sendProofInstruction`,
|
||||
`startup-orchestrator.ts:760`). The seat answers with
|
||||
`rig startup-proof submit --challenge-id … --answer …`
|
||||
(`packages/cli/src/commands/startup-proof.ts:99`), which posts to the activity
|
||||
hook route (`routes/activity.ts:239`); `verifyStartupProof`
|
||||
(`startup-proof.ts:133`) records `node.startup_proof_verified` or
|
||||
`node.startup_proof_rejected`. Orientation status is derived from those events
|
||||
(`startup-proof.ts:239`), never from `startup_status`. Other fresh launches (no
|
||||
proof selected, or a terminal runtime) record `node.startup_proof_skipped`
|
||||
(`startup-orchestrator.ts:374`); resume, fork and rebuild keep the existing proof
|
||||
history.
|
||||
|
||||
**Pending startup context and `rig seat continue`.** When a fresh launch stops at
|
||||
a native prompt before its context is delivered, `fail()` (`:806`) marks the
|
||||
failure `freshContextPending` and adds "run: rig seat continue <session>". A
|
||||
Codex fork whose own id isn't verified at launch, or whose readiness times out,
|
||||
ends `attention_required` with `forkContextPending` (its parent token) instead:
|
||||
the role waits, and nothing is saved for an unverified child.
|
||||
`canContinueStartup` (`:764`, using `readPendingStartupContext`, `:46`) allows
|
||||
continuation only while that failure is the occupant's latest startup event.
|
||||
`continueStartup` (`seat-lifecycle-service.ts:723`) re-checks the binding,
|
||||
pane and runtime, then runs startup again on the same conversation without
|
||||
relaunching the harness. A fork continuation first re-verifies the child with
|
||||
`captureForkIdentity` (`codex-runtime-adapter.ts:668`; `startup-orchestrator.ts:559`)
|
||||
and needs the exact pending event it started from; a fresh continuation keeps
|
||||
the plain "still pending" check. It is reached by `rig seat continue`,
|
||||
`POST /api/seat/continue/:seatRef` and the per-seat startup route's `continue`
|
||||
action (`routes/startup.ts:159`).
|
||||
|
||||
**Startup layering is additive and ordered** (`resolveStartup`,
|
||||
`startup-resolver.ts:15`–`22`): (1) agent base, (2) profile, (3) rig culture
|
||||
file, (4) rig startup, (5) pod startup, (6) member startup, (7) operator debug
|
||||
append. Files and actions are concatenated in that order with no
|
||||
deduplication (`startup-resolver.ts:24`). This is the spec-startup contract's
|
||||
invariant; the cross-cutting architecture rules are collected in
|
||||
`architecture-rules-and-event-system.md`.
|
||||
|
||||
The launch path takes only the actions from `resolveStartup`. It builds the
|
||||
startup *file* chain itself (`buildResolvedStartupFiles`,
|
||||
`rigspec-instantiator.ts:2518`), in this order: agent base, profile, the shipped
|
||||
`CULTURE-default.md` floor (`:327`), the rig `culture_file`, rig, pod, member,
|
||||
the shipped `openrig-start.md` (`:2572`), and, for fresh launches when enabled,
|
||||
the onboarding files (`:2582`). Managed-block guidance is deduplicated, and a
|
||||
`starter_ref` layer is put in front when a member names one (`:2243`).
|
||||
|
||||
**Startup action constraints** (`startup-validation.ts`): startup entries must
|
||||
be files (`:22`); no shell startup actions (`:65`); action types are
|
||||
`slash_command`, `send_text` and `startup_proof` (`:7`), and a `startup_proof`
|
||||
action must be idempotent with a value of `authenticated` or `none` (`:73`);
|
||||
non-idempotent actions must not apply on restore (`:100`). Retrying a failed
|
||||
startup is handled as a restore, which is why the orchestrator skips
|
||||
non-idempotent actions on restore (`startup-orchestrator.ts:634`,
|
||||
"retry-as-restore safety"). The exception is
|
||||
`PodRigInstantiator.retryFirstStart` (`rigspec-instantiator.ts:1147`), which
|
||||
re-runs a first start that failed at projection, before any harness launch.
|
||||
|
||||
**Remote import constraints**: `agent_ref` and AgentSpec imports accept
|
||||
`local:<relative path>` and `path:<absolute path>` only
|
||||
(`rigspec-schema.ts:41`, `agent-manifest.ts:40`); a terminal member uses the
|
||||
`builtin:terminal` sentinel (`rigspec-schema.ts:532`). Remote `agent_ref`
|
||||
sources remain unsupported: RigSpec validation rejects them
|
||||
(`rigspec-schema.ts:563`), `rigPreflight` runs that validation
|
||||
(`rigspec-preflight.ts:252`), and the resolver refuses a remote import again
|
||||
(`agent-resolver.ts:203`).
|
||||
|
||||
## 5. Instantiation, preflight, export
|
||||
|
||||
- `runtime-adapter.ts` — adapter contract + bridge types.
|
||||
- `rigspec-preflight.ts` — dual-stack legacy preflight (`RigSpecPreflight`,
|
||||
`rigspec-preflight.ts:26`) plus rebooted `rigPreflight(...)` (`:246`).
|
||||
- `rigspec-instantiator.ts` — dual-stack `RigInstantiator`
|
||||
(`rigspec-instantiator.ts:33`) plus `PodRigInstantiator` (`:475`).
|
||||
- `rigspec-exporter.ts` — dual-format live rig export to YAML/JSON.
|
||||
- `pod-repository.ts` — pod CRUD plus live continuity-state CRUD.
|
||||
|
||||
`routes/rigspec.ts` is the dual-format seam; a parsed spec with a `pods` array
|
||||
takes the pod-aware path (`routes/rigspec.ts:83`): validate
|
||||
(pod-aware → `RigSpecSchema.validate`; legacy → `LegacyRigSpecSchema.validate`),
|
||||
preflight (`rigPreflight({ rigSpecYaml, rigRoot, … })` vs
|
||||
`RigSpecPreflight.check(spec)`), import
|
||||
(`podInstantiator.instantiate(yaml, rigRoot, …)` vs
|
||||
`RigInstantiator.instantiate(spec)`), export (pod-aware exports canonical
|
||||
`version: "0.2"` RigSpec; legacy exports flat-node `schemaVersion: 1`,
|
||||
`rigspec-exporter.ts:105`).
|
||||
|
||||
The import validation route delegates to `validateRigSpecImport`
|
||||
(`routes/rigspec.ts:215–223`, `spec-validation-service.ts:10–19`). The same
|
||||
helper powers local `rig spec validate` without contacting the daemon
|
||||
(`packages/cli/src/commands/rig.ts:204–207`); it selects the pod-aware or legacy
|
||||
validator and distinguishes YAML parse failures from validator exceptions.
|
||||
|
||||
## 6. Identity: whoami, materialize, bind, adopt
|
||||
|
||||
**Whoami resolution** — the daemon owns the truth surface through
|
||||
`/api/whoami` (`routes/whoami.ts`); tmux metadata is an adopted-session
|
||||
anchor, not sovereign truth. `whoami-service.ts:8` declares
|
||||
`resolvedBy: "node_id" | "session_name"`. The route requires `nodeId` or
|
||||
`sessionName` (`routes/whoami.ts:19`–`23`). The CLI resolves identity
|
||||
(`packages/cli/src/commands/whoami.ts:142`–`148`) in this order: explicit
|
||||
`--node-id` → explicit `--session` → env vars (`OPENRIG_NODE_ID` /
|
||||
`OPENRIG_SESSION_NAME`, or the legacy `RIGGED_NODE_ID` /
|
||||
`RIGGED_SESSION_NAME`, `whoami.ts:158`) → tmux metadata → raw tmux
|
||||
session-name fallback.
|
||||
|
||||
- Managed sessions prefer projected `OPENRIG_NODE_ID` /
|
||||
`OPENRIG_SESSION_NAME`, set when the tmux session is created
|
||||
(`node-launcher.ts:143`–`144`).
|
||||
- Adopted sessions use tmux-owned metadata written at bind time.
|
||||
|
||||
The tmux metadata keys use the `@rigged_` prefix:
|
||||
`ClaimService.setRiggedMetadata` writes `@rigged_node_id` /
|
||||
`@rigged_session_name` / `@rigged_rig_id` / `@rigged_rig_name` /
|
||||
`@rigged_logical_id` (`claim-service.ts:182`–`186`), called from `bind`,
|
||||
`reconcileSession` and `createAndBindToPod`;
|
||||
`RigLifecycleService.unclaimSession` clears the same five keys
|
||||
(`rig-lifecycle-service.ts:209`–`213`); and `rig whoami` reads
|
||||
`@rigged_node_id` and `@rigged_session_name`
|
||||
(`packages/cli/src/commands/whoami.ts:173`, `:179`). MCP tool names use a
|
||||
different prefix, `rig_*` (`packages/cli/src/mcp-server.ts`).
|
||||
|
||||
**Materialize / bind / adopt**: `POST /api/rigs/import/materialize`
|
||||
(`routes/rigspec.ts:176`) creates a pod-aware topology without launching
|
||||
sessions and refuses a legacy spec (`routes/rigspec.ts:189`);
|
||||
`POST /api/discovery/:id/bind` (`routes/discovery.ts:77`) attaches a
|
||||
discovered live session to an existing logical node (`logicalId`), or creates
|
||||
a member in a pod (`podNamespace` + `memberName`) and binds it;
|
||||
`POST /api/discovery/:id/adopt` (`routes/discovery.ts:139`) is the composite
|
||||
route (bind to existing node, or create a new member in a target pod and bind
|
||||
immediately). On the CLI, `rig bind` calls the bind route
|
||||
(`packages/cli/src/commands/bind.ts:56`), and `rig adopt` materializes the
|
||||
topology and then calls the bind route for each session
|
||||
(`packages/cli/src/commands/adopt.ts:163`, `:212`). Authored pod namespace is
|
||||
preserved through adoption so logical ids stay `${podNamespace}.${memberName}`
|
||||
(`claim-service.ts:663`). Adopted-session parity is tmux-metadata parity, not
|
||||
fake env-var parity.
|
||||
|
||||
## 7. Specialist rosters
|
||||
|
||||
A roster is an authored list of recommended specialist seats, kept outside the
|
||||
specs. `rig roster list`, `show <id>` and `find <query>`
|
||||
(`packages/cli/src/commands/roster.ts`) read version-1 roster JSON files from
|
||||
`<workspace.root>/rosters/` (or `--folder`); they are read-only and need no
|
||||
daemon route. Each roster names a purpose and a curator, and each member names
|
||||
a seat, host, capabilities, engagement, `use_when` and `why`. `show` and
|
||||
`find` add current observations of each member by matching host and seat
|
||||
against `rig ps --nodes`, read from the configured daemon and again across
|
||||
registered hosts, and report a gap rather than guessing when no node matches. A roster is a
|
||||
recommendation, not an assignment or an authority. It is unrelated to the
|
||||
topology roster a snapshot records (see `lifecycle-snapshot-restore.md`).
|
||||
|
||||
## See also
|
||||
|
||||
- `daemon-core.md` — createDaemon wiring; the migration/route surface.
|
||||
- `adapters-and-runtimes.md` — the five-method RuntimeAdapter contract detail.
|
||||
- `lifecycle-snapshot-restore.md` — snapshot/restore consumes persisted replay
|
||||
context.
|
||||
- `architecture-rules-and-event-system.md` — the cross-cutting architecture
|
||||
rules.
|
||||
- Source roots: `packages/daemon/src/domain/{agent-manifest,rigspec-schema,
|
||||
profile-resolver,startup-orchestrator,whoami-service,claim-service}.ts`,
|
||||
`packages/daemon/src/routes/{rigspec,whoami}.ts`.
|
||||
@@ -1,325 +0,0 @@
|
||||
---
|
||||
kind: as-built
|
||||
title: Architecture Invariants, Event System, Compatibility Notes
|
||||
status: active
|
||||
topics: [runtime-control, observability, doctrine]
|
||||
domains: [engineering-advisor, operating-advisor, review]
|
||||
applies-when: |
|
||||
Need the cross-cutting architecture invariants the codebase enforces (the
|
||||
25 architecture rules + startup/import constraints), the shape of the
|
||||
RigEvent union and its SSE delivery surfaces, or the intentional
|
||||
compatibility limits that still describe the shipped system.
|
||||
siblings: [daemon-core.md, coordination-primitive.md]
|
||||
prerequisite-reads: [../README.md, daemon-core.md]
|
||||
last-verified-against-source: e8f0ab340db773392ec8be75b072d1c0f3068a50
|
||||
last-updated: 2026-10-08
|
||||
---
|
||||
|
||||
# Architecture Invariants, Event System, Compatibility Notes
|
||||
|
||||
This module collects the cross-cutting invariants that do not belong to any
|
||||
single subsystem: the architecture rules the codebase holds itself to, the
|
||||
event-system shape, and the intentional compatibility limits.
|
||||
|
||||
> Verified against source at main `e8f0ab340db773392ec8be75b072d1c0f3068a50`. Each count below sits beside the
|
||||
> command that produces it; run the command from the repository root to refresh
|
||||
> it.
|
||||
|
||||
## 1. Architecture rules
|
||||
|
||||
These are the invariants the codebase is built to preserve. Rule 6 (startup
|
||||
layering) and rule 7 (restore-policy narrowing) are the spec/startup contract —
|
||||
see `agent-spec-and-startup.md` for their flow detail; they are restated here as
|
||||
system-level invariants.
|
||||
|
||||
1. No runtime Hono in `domain/` and `adapters/`. `adapters/` has none; `domain/`
|
||||
has type-only Hono imports (`import type`) in **4** files
|
||||
(`git grep -l 'from "hono' -- packages/daemon/src/domain packages/daemon/src/adapters | wc -l`).
|
||||
2. Routes depend on the domain; the domain does not depend on routes at
|
||||
runtime. **1** type-only import crosses back:
|
||||
`domain/project-classifier.ts:1` imports `IdentityProvenance` from
|
||||
`routes/require-sender-identity.js`
|
||||
(`git grep -lE 'from "(\.\./)+routes/' -- packages/daemon/src/domain packages/daemon/src/adapters | wc -l`).
|
||||
3. Shared DB-handle invariants are enforced at construction time.
|
||||
4. The reboot is engine-first: domain services land before public-surface
|
||||
rewiring.
|
||||
5. Runtime is member-authoritative in the pod-aware model.
|
||||
6. Startup layering is additive and ordered: agent base → profile → rig
|
||||
culture file → rig startup → pod startup → member startup → operator debug
|
||||
append.
|
||||
7. Restore-policy narrowing is one-way only: `resume_if_possible` →
|
||||
`relaunch_fresh` → `checkpoint_only`.
|
||||
8. Base/import collisions warn; ambiguous import/import unqualified refs fail
|
||||
loudly.
|
||||
9. Bundle assembly and startup-file resolution use containment checks rooted
|
||||
in the owning artifact.
|
||||
10. Restore replay uses classification-free projection intent, not stale
|
||||
startup-time `no_op` / conflict classifications.
|
||||
11. Startup status is explicit session state: `pending`, `ready`,
|
||||
`attention_required`, `failed` (`types.ts:102`).
|
||||
12. Session recency depends on monotonic ULIDs: `session-registry.ts` uses
|
||||
`monotonicFactory()`. Restore does not pick the newest session by max
|
||||
ULID; it resolves the active occupant recorded in the snapshot
|
||||
(`resolveActiveSnapshotSession`, `active-occupant.ts:74`, called at
|
||||
`restore-orchestrator.ts:774` and `:971`).
|
||||
13. Readiness checking is a retry loop with exponential backoff and
|
||||
configurable timeout, using adapter-specific probes (Claude TUI
|
||||
indicator, Codex ready message, terminal immediate).
|
||||
14. Restore outcome states are a fixed set (`RestoreNodeResult.status`,
|
||||
`types.ts:475`): `resumed`, `rebuilt`, `fresh-primed`,
|
||||
`awaiting-decision`, `attention_required`, `failed`, plus
|
||||
`operator_recovered` (set only by later reconciliation), `fresh`
|
||||
(retained only for the legacy continuity-restoring skip path) and
|
||||
`left_attached` (a seat joined with `rig attach` as an external CLI, which
|
||||
restore leaves as it is). `rebuilt` = new process assembled from artifacts.
|
||||
15. Restore honesty: a failed resume stops loudly as `awaiting-decision`, with
|
||||
the blank session rolled back and no session running
|
||||
(`restore-orchestrator.ts:1055`). No automatic fresh fallback. Fresh launch
|
||||
is explicit follow-up only (`rig up --fresh <seats...>`,
|
||||
`packages/cli/src/commands/up.ts:99`).
|
||||
16. Post-command handoff required on `up`, `down`, `restore`,
|
||||
`snapshot create`: what happened + current state + next action.
|
||||
17. Session naming: `{pod}-{member}@{rig}` — human-authored,
|
||||
system-validated. No generation, no slugification.
|
||||
18. Communication: tmux is transport, not truth. `send/capture/broadcast`
|
||||
wrap tmux reliably with honest errors.
|
||||
19. Transcripts: bounded capture — a periodic `tmux capture-pane` snapshot of
|
||||
the trailing lines (default 1000, every 2 s for an active seat) replaces
|
||||
the transcript file, replacing pipe-pane (`transcript-rotation.ts:3`,
|
||||
`:27`–`28`; started at `node-launcher.ts:181`). An idle seat is captured
|
||||
less often, within the 10-second freshness window (`:29`–`33`,
|
||||
`:162`–`163`), and the file is rewritten only when the captured bytes
|
||||
change (`:270`). `readTail` and `grep` strip ANSI; `readFull` returns the
|
||||
content unfiltered. `rig ask` transcript search: `rg`
|
||||
preferred, `grep -E` fallback.
|
||||
20. Config precedence: CLI flag > env var > config file
|
||||
(`~/.openrig/config.json`, or `$OPENRIG_HOME/config.json` when
|
||||
`OPENRIG_HOME` is set; `packages/cli/src/config-store.ts:939`) > default.
|
||||
21. Semi-deterministic calibration: build what agents use constantly. Agent
|
||||
handles edge cases from error messages.
|
||||
22. `rig ask` is context engineering: gathers evidence, does NOT call an
|
||||
external LLM. The agent IS the LLM. The exception is the explicit `--wake`
|
||||
flag (`packages/cli/src/commands/ask.ts:88`), which runs one headless
|
||||
question against an existing agent session (`codex exec resume` or
|
||||
`claude -p --resume`, `packages/cli/src/ask-wake.ts:58`–`60`).
|
||||
23. Spec library truth is YAML on disk; daemon owns the structured
|
||||
review/index/cache layer.
|
||||
24. Adopted-session parity is tmux-metadata parity, not fake env-var parity.
|
||||
25. Human-readable IDs are UI-only presentation helpers. CLI/API/MCP/backend
|
||||
keep full canonical ids.
|
||||
|
||||
### Startup action constraints
|
||||
|
||||
- No shell startup actions.
|
||||
- Action types are `slash_command`, `send_text` and `startup_proof` only
|
||||
(`startup-validation.ts:7`).
|
||||
- Non-idempotent actions must not apply on restore.
|
||||
- Retrying failed startup is handled as restore.
|
||||
|
||||
### Remote import constraints
|
||||
|
||||
The reboot supports `local:...` and `path:/abs/...` agent refs. Remote
|
||||
`agent_ref` sources remain unsupported and fail in preflight (schema
|
||||
validation, `rigspec-schema.ts:563`; restated in compat note 1).
|
||||
|
||||
### Startup, delivery and launch invariants
|
||||
|
||||
These hold across the startup, transport and adapter code and are stated in its
|
||||
comments:
|
||||
|
||||
- **`ready` is not "submitted" and not "oriented".** A seat can reach
|
||||
`startupStatus: ready` while its startup text is unverified or still staged;
|
||||
`node.startup_ready` carries a `submission` block saying which
|
||||
(`types.ts:220`). Orientation is a separate, challenge-verified proof:
|
||||
`ready` never means oriented (`types.ts:222`–`227`). See
|
||||
`agent-spec-and-startup.md`.
|
||||
- **Default sends never block on busy or unknown.** `--wait-for-idle` is the
|
||||
caller's opt-in to wait, and a failed wait returns without sending. Otherwise
|
||||
only positive evidence of an open
|
||||
picker or approval prompt refuses a send; a busy or unknown seat gets the
|
||||
message with an advisory warning (`session-transport.ts:1427`–`1433`). The
|
||||
audited `--dangerously-interact` override is the only way past an open prompt.
|
||||
- **Non-interruptive mode is per-launch flags only.** It never changes
|
||||
permissions or native settings files, applies only to full-bypass Claude Code
|
||||
and Codex seats, and is saved per rig, off by default
|
||||
(`adapters/non-interruptive.ts:9`–`26`; `rigs.non_interruptive`, migration
|
||||
`095`). A rig spec can author it as `non_interruptive` (`rigspec-schema.ts:179`).
|
||||
- **Inside a notify envelope, events are delivered from the log.** Within
|
||||
`withNotifyEnvelope` (`event-bus.ts:106`–`112`), every event persisted through
|
||||
the event bus must be registered before the callback returns; after commit
|
||||
the bus delivers the committed rows from the log, and an unreadable row
|
||||
emits `event.delivery_poisoned`. Legacy callers outside an envelope persist
|
||||
inside their own transaction and notify subscribers explicitly after commit
|
||||
(`persistWithinTransaction`, `:70`; for example `node-launcher.ts:241`).
|
||||
|
||||
## 2. Event system
|
||||
|
||||
The daemon's event surface is the single `RigEvent` discriminated union.
|
||||
|
||||
`RigEvent` is declared at `packages/daemon/src/domain/types.ts:108`
|
||||
(`export type RigEvent =`) and runs through `types.ts:313`. It has **98 union
|
||||
members** declaring **99** `type` literals: one member (`types.ts:109`) carries
|
||||
both `proof.judged` and `proof.sources_changed`.
|
||||
|
||||
- Members:
|
||||
`sed -n '/^export type RigEvent =/,/^export type PersistedEvent/p' packages/daemon/src/domain/types.ts | grep -c 'type: "'`
|
||||
- Type literals:
|
||||
`sed -n '/^export type RigEvent =/,/^export type PersistedEvent/p' packages/daemon/src/domain/types.ts | grep -oE '"[a-z_]+(\.[a-z_]+)+"' | sort -u | wc -l`
|
||||
|
||||
**96** of the 99 literals appear as a `type: "<x>"` literal somewhere in
|
||||
`packages/daemon/src` outside `types.ts` (mostly in `domain/` and `routes/`;
|
||||
`seat.model_divergence` is built in `startup.ts`). **3** are declared but never
|
||||
constructed: `session.status_changed`, `continuity.sync`, and `continuity.degraded`
|
||||
(per #490, `qitem.closure_overdue` is produced by `QueueRepository.recordClosureOverdue`
|
||||
when swept by `queue-stuck-sweep`, and `mission_control.view_refreshed` was retired).
|
||||
List them with:
|
||||
|
||||
`for t in $(sed -n '/^export type RigEvent =/,/^export type PersistedEvent/p' packages/daemon/src/domain/types.ts | grep -oE '"[a-z_]+(\.[a-z_]+)+"' | tr -d '"'); do git grep -q -F "type: \"$t\"" -- packages/daemon/src ':!packages/daemon/src/domain/types.ts' || echo "$t"; done`
|
||||
|
||||
### Per-prefix event families
|
||||
|
||||
Each count below is the number of `type` literals per prefix in the union body
|
||||
(`types.ts:108`–`313`):
|
||||
`sed -n '/^export type RigEvent =/,/^export type PersistedEvent/p' packages/daemon/src/domain/types.ts | grep -oE '"[a-z_]+(\.[a-z_]+)+"' | tr -d '"' | cut -d. -f1 | sort | uniq -c | sort -rn`.
|
||||
|
||||
| Prefix | Types | Sample / role |
|
||||
|---|---|---|
|
||||
| `node.*` | 15 | `node.added` (`types.ts:126`) … `node.startup_proof_rejected` (`:229`) — lifecycle/startup |
|
||||
| `workflow.*` | 8 | workflow runtime (detail in `workflow-runtime.md`) |
|
||||
| `session.*` | 8 | session discovery / status / detach / vanish / stop / clean / resume-token audit |
|
||||
| `rig.*` | 7 | `rig.created` / `rig.deleted` / `rig.imported` / `rig.stopped` / `rig.archived` / `rig.unarchived` / `rig.expanded` (`:241`) |
|
||||
| `watchdog.*` | 5 | `watchdog.evaluation_fired` (`:280`) … `watchdog.job_stopped` (`:284`) |
|
||||
| `seat.*` | 5 | `seat.model_divergence` (`:115`) … `seat.handover_completed` (`:200`) — model divergence, fresh launch, attention clear, handover |
|
||||
| `queue.*` | 5 | queue lifecycle (`:249`–`:252`, `:262`; detail in `coordination-primitive.md`) |
|
||||
| `package.*` | 5 | legacy package/install engine events |
|
||||
| `mission_control.*` | 4 | audit/notification (`:308`–`:313`; detail in `mission-control.md`) |
|
||||
| `bootstrap.*` | 5 | legacy bootstrap-run events |
|
||||
| `restore.*` | 4 | restore start/complete/subset-complete/reconcile (detail in `lifecycle-snapshot-restore.md`) |
|
||||
| `classifier.*` | 4 | classifier-lease lifecycle |
|
||||
| `qitem.*` | 2 | `qitem.fallback_routed` (`:253`), `qitem.closure_overdue` (`:254`) |
|
||||
| `proof.*` | 2 | `proof.judged`, `proof.sources_changed` — one union member (`:109`) |
|
||||
| `pod.*` | 2 | `pod.created` (`:215`), `pod.deleted` (`:216`) |
|
||||
| `inbox.*` | 2 | `inbox.absorbed` (`:255`), `inbox.denied` (`:256`) |
|
||||
| `continuity.*` | 2 | `continuity.sync` (`:230`), `continuity.degraded` (`:231`) |
|
||||
| `agent.*` | 2 | `agent.activity` (`:151`), `agent.session_identity` (`:158`) |
|
||||
| singletons | 12 | one type each: `workflow_spec.*`, `view.*`, `transport.*` (`:155`), `topology.*` (`:138`), `stream.*` (`:245`), `snapshot.*`, `project.*`, `kernel.*`, `event.*` (`:110`), `chat.*` (`:239`), `bundle.*`, `binding.*` |
|
||||
|
||||
Family counts sum to 99 type literals (18 multi-type families totalling 87 +
|
||||
12 singletons).
|
||||
|
||||
### Emission and delivery
|
||||
|
||||
Events are emitted via `eventBus.emit({ type: ... })` (`event-bus.ts:57`) or,
|
||||
inside a caller-managed transaction, `eventBus.persistWithinTransaction(...)`
|
||||
(`event-bus.ts:70`) with subscribers notified after commit — across domain
|
||||
services (`stream-store.ts`, `workflow-runtime.ts`, `restore-orchestrator.ts`,
|
||||
`node-launcher.ts`, etc.) and route handlers. The event log is append-only and
|
||||
SQLite-backed, and it holds stored events only. `view.changed`, which the view
|
||||
event bridge (`view-event-bridge.ts`) produces for each queue, inbox, proof or project-classification change,
|
||||
is transient: `eventBus.publishTransient(...)` delivers it to the live
|
||||
subscribers without a row, without a `seq` of its own and without moving the
|
||||
notify watermark. It carries `transient: true` and, as its `seq`, the `seq` of
|
||||
the stored event that caused it.
|
||||
|
||||
Upgrade note: `view.changed` rows stored by older versions stay in the log, so
|
||||
`/api/events` replays and the telemetry export still return them until an events retention
|
||||
(a separate change) removes them.
|
||||
|
||||
SSE delivery surfaces include the following. The daemon has **11**
|
||||
`streamSSE(` call sites across **10** files in `packages/daemon/src/routes/`
|
||||
(`git grep -o 'streamSSE(' -- packages/daemon/src/routes | wc -l`;
|
||||
`git grep -l 'streamSSE(' -- packages/daemon/src/routes | wc -l`).
|
||||
|
||||
- `GET /api/events` — global stream of the stored events (`server.ts:730`
|
||||
`app.route("/api/events", eventsRoute)`); `?rigId=` keeps one rig's.
|
||||
Transient events (`view.changed`) are not stored, so they appear here
|
||||
neither live nor in a replay. Each event's SSE `id` is its `seq`. Before going live, a connection replays
|
||||
stored events, read and written 500 at a time (`routes/events.ts`):
|
||||
- with a `Last-Event-ID` header (an `EventSource` sends it when it
|
||||
reconnects), every event after that `seq`, whatever `replay` says;
|
||||
- otherwise `?replay=<n>`: the newest `n` events (of the rig, with
|
||||
`rigId`); `replay=0` starts from now; `replay=all`, no `replay`, a
|
||||
count past the log's size, or any other value, replays the whole log.
|
||||
|
||||
Either way the replay ends at the newest event stored as the connection
|
||||
subscribes; the events stored meanwhile were buffered and follow it, each
|
||||
once and in order, before the stream goes live.
|
||||
- `GET /api/views/sse` (alias `/api/views/watch`) and
|
||||
`GET /api/views/:viewName/sse` — `view.changed`, live only (`routes/views.ts`).
|
||||
An event's `id` there is the `seq` of the stored event that caused it, so it
|
||||
repeats across the views one change touches; it is not a replay cursor.
|
||||
- `GET /api/stream/watch` — new stream items (`routes/stream.ts:194`).
|
||||
- `GET /api/queue/watch` — queue/inbox coordination events
|
||||
(`routes/queue.ts:1054`, with a `/sse` alias at `:1055`).
|
||||
- The chat SSE stream `GET /api/rigs/:rigId/chat/watch` delivers
|
||||
`chat.message` for one rig (`routes/chat.ts:82`, mounted at `server.ts:782`;
|
||||
rig-scoped; see compat note 6).
|
||||
- The other SSE routes are activity events and the watch or `sse` routes for
|
||||
mission control, projects, views, watchdog and workflow; list them with
|
||||
`git grep -n 'streamSSE(' -- packages/daemon/src/routes`.
|
||||
|
||||
### Telemetry read surface
|
||||
|
||||
`/api/telemetry` (`server.ts:822`; `routes/telemetry.ts`) is a read-only,
|
||||
paged view over the event log and queue history, built by
|
||||
`readTelemetryPage()` in `packages/daemon/src/domain/finite-telemetry.ts`. It
|
||||
serves three streams: `GET /api/telemetry/v1/events`, `/v1/queue-transitions`
|
||||
(active and archived transitions) and `/v1/nodes/:nodeId/tenures` (a node's
|
||||
occupant tenures). Each page is one read with no writes or subscription,
|
||||
bounded at 128 rows and 128 KiB (`TELEMETRY_LIMITS`), and resumed with a
|
||||
cursor. Rows carry metadata only, never event payloads, notes or bodies, and
|
||||
the response names gaps in the history it could not vouch for (for example a
|
||||
daemon restart boundary or rows before the retained floor) instead of
|
||||
presenting a partial history as complete. The page records the source host and
|
||||
the daemon's boot epoch. Bad input returns `400 telemetry_invalid_request`; a
|
||||
failed read returns `503 telemetry_read_unavailable`. The same route also
|
||||
serves the usage series behind `rig usage` (`/usage/series`, `/usage/top`).
|
||||
`rig telemetry events|transitions|tenures`
|
||||
(`packages/cli/src/commands/telemetry.ts`) prints one bounded page.
|
||||
|
||||
## 3. Remaining compatibility notes
|
||||
|
||||
Intentional limits that still describe the shipped system:
|
||||
|
||||
1. Remote `agent_ref` imports remain unsupported (see §1 remote import
|
||||
constraints).
|
||||
2. Startup actions remain intentionally constrained (`slash_command`,
|
||||
`send_text`, `startup_proof`).
|
||||
3. Legacy compatibility seams still ship for pre-reboot data and v1
|
||||
artifacts.
|
||||
4. `rig ask` gathers context only — does not call an external LLM — unless
|
||||
`--wake` is passed (rule 22).
|
||||
5. `rig ask` transcript search prefers `rg`, falls back to `grep -E`
|
||||
(`history-query.ts:252`, `:267`); quality/perf varies by backend.
|
||||
6. Chat is rig-scoped only — no cross-rig channels or DMs.
|
||||
7. `--verify` on `rig send` reports what the pane shows, not agent
|
||||
acknowledgement. By default it compares the message (whole for a
|
||||
header-carrying send, the first 40 characters for a raw one) in captures
|
||||
before and after the send, leaving out the current input box; with
|
||||
`transport.verify_input_box` on, Claude Code and Codex sends are verified
|
||||
only for a seat proven idle before the paste and on input-box evidence; a
|
||||
busy or unknown seat reads unconfirmed (`transport-and-transcripts.md` §3
|
||||
step 5).
|
||||
When it cannot confirm, it reports `rendered-unconfirmed`, not a failure.
|
||||
8. Terminal node readiness is shell-ready only — no service health probes.
|
||||
9. Managed-app service surfaces are descriptive only — OpenRig does not
|
||||
auto-inject service URLs/tokens into agent prompts beyond authored
|
||||
startup/context files.
|
||||
10. Specialist delegation is conventional, not automatic — addressed by
|
||||
session name or normal communication surfaces.
|
||||
|
||||
## 4. Cross-references
|
||||
|
||||
Under the modular as-built, this module owns the invariants and event shape;
|
||||
`../codemap.md` is the navigation index for file-by-file structure.
|
||||
|
||||
## See also
|
||||
|
||||
- `daemon-core.md` — wiring, DB, migrations, startup.
|
||||
- `coordination-primitive.md` — queue/stream/inbox/outbox events.
|
||||
- `workflow-runtime.md` — `workflow.*` events.
|
||||
- `mission-control.md` — `mission_control.*` events.
|
||||
- Source roots: `packages/daemon/src/domain/types.ts` (RigEvent union),
|
||||
`packages/daemon/src/domain/event-bus.ts` (emit/persist),
|
||||
`packages/daemon/src/routes/{stream,queue}.ts` (SSE watch),
|
||||
`packages/daemon/src/server.ts` (`/api/events`).
|
||||
@@ -1,191 +0,0 @@
|
||||
---
|
||||
kind: as-built
|
||||
title: Content Surfaces — Files, Progress, Steering, and Health
|
||||
status: active
|
||||
topics: [observability, specification-and-bundles]
|
||||
domains: [engineering-advisor, operating-advisor]
|
||||
applies-when: |
|
||||
Tracing allowlisted file access, conflict-checked writes, progress indexing,
|
||||
steering composition, or the daemon health-summary endpoints.
|
||||
last-verified-against-source: e8f0ab340db773392ec8be75b072d1c0f3068a50
|
||||
last-updated: 2026-10-08
|
||||
---
|
||||
|
||||
# Content Surfaces — Files, Progress, Steering, and Health
|
||||
|
||||
Source snapshot: `e8f0ab340db773392ec8be75b072d1c0f3068a50`. This describes the source at that commit;
|
||||
it does not establish the version or behavior of a running daemon.
|
||||
|
||||
The daemon exposes filesystem content through the files, progress, and
|
||||
steering route families. Health summaries read existing daemon state. These
|
||||
are separate projections with separate configuration; configuring one root
|
||||
is not a universal filesystem-access policy.
|
||||
|
||||
## Configuration and wiring
|
||||
|
||||
[`startup.ts`](../../../packages/daemon/src/startup.ts) constructs
|
||||
`SettingsStore`, decodes `cfg.filesAllowlistRaw` and `cfg.progressScanRootsRaw`,
|
||||
and injects `FileWriteService`, `ProgressIndexer`, and `SteeringComposer`.
|
||||
The write service exists only when the decoded file allowlist is nonempty.
|
||||
Its audit path is explicitly `${OPENRIG_HOME}/file-edit-audit.jsonl`.
|
||||
|
||||
[`SettingsStore`](../../../packages/daemon/src/domain/user-settings/settings-store.ts)
|
||||
resolves environment overrides, then settings-file values, then defaults.
|
||||
`files.allowlist` and `progress.scan_roots` both default to
|
||||
`workspace:<resolved-workspace-root>`. Their environment overrides are
|
||||
`OPENRIG_FILES_ALLOWLIST` and `OPENRIG_PROGRESS_SCAN_ROOTS`. Thus, an unset
|
||||
or empty environment variable, or an empty settings-file value, does **not**
|
||||
imply an empty allowlist in normal daemon startup. A non-empty value is not
|
||||
validated: the decoders silently skip malformed entries (no `name:` prefix, or
|
||||
a relative path), so a malformed value can leave no configured roots. Some source comments and route hints still say
|
||||
otherwise or point only at the environment variable. The standalone `readAllowlistFromEnv` helper has different inputs
|
||||
and retains its legacy environment fallback.
|
||||
|
||||
Roots use comma-separated `name:/absolute/path` pairs. The decoders skip
|
||||
invalid pairs, canonicalize existing paths, and let the last duplicate name
|
||||
win. See [`decodeAllowlist`](../../../packages/daemon/src/domain/files/path-safety.ts)
|
||||
and [`decodeProgressScanRoots`](../../../packages/daemon/src/domain/progress/progress-indexer.ts).
|
||||
|
||||
## Path resolution and reads
|
||||
|
||||
[`resolveAllowedPath`](../../../packages/daemon/src/domain/files/path-safety.ts)
|
||||
requires a configured root, rejects `..` path segments and absolute relative
|
||||
paths, resolves existing symlinks, and checks containment with a path-separator
|
||||
boundary. An empty relative path denotes the root. If realpath fails, the
|
||||
helper falls back to the resolved candidate; file/directory helpers then
|
||||
perform a stat check. These are the implemented checks, not a claim of
|
||||
race-free isolation against concurrent filesystem changes.
|
||||
|
||||
[`readAllowedFile`](../../../packages/daemon/src/domain/files/file-read.ts)
|
||||
is shared by the HTTP reader and the local TUI reader. It returns content,
|
||||
absolute and root-relative resolved paths, mtime, full-file SHA-256, size,
|
||||
binary classification, and truncation metadata. The returned text is capped
|
||||
at **1,048,576 bytes**, but the full file is read and hashed. The cap limits
|
||||
the response, not the memory required to read the file. The TUI's local
|
||||
reader runs as a CLI subprocess without the daemon: it resolves
|
||||
`files.allowlist` from the CLI configuration, offers four fixed workspace
|
||||
targets as roots, and hides dot-prefixed entries in listings
|
||||
(`packages/cli/src/local-reading.ts`).
|
||||
|
||||
## Writes and audit
|
||||
|
||||
[`FileWriteService.writeAtomic`](../../../packages/daemon/src/domain/files/file-write-service.ts)
|
||||
reads the existing target and compares **both** the expected mtime and content
|
||||
hash. A mismatch raises `WriteConflictError` with the current values. It then
|
||||
writes a temporary file in the same directory, preserves the target's
|
||||
ordinary permission bits, fsyncs the temporary file, and renames it over the
|
||||
target. This is optimistic conflict detection; it does not hold a lock over
|
||||
the read/compare/replace sequence.
|
||||
|
||||
`createAtomic` handles new files. It creates and fsyncs a temporary file, then
|
||||
uses an exclusive hard link to establish the target. If the target already
|
||||
exists, it returns the `target_exists` error instead of overwriting it.
|
||||
`createAtomic` is used by the Living Notes freeze export, and `writeAtomic`
|
||||
also backs the freeze-time `MISSION_BRIEF.md` spine update; both append to the
|
||||
same audit file.
|
||||
|
||||
Both operations append a JSONL audit record containing actor/provenance,
|
||||
paths, timestamps, hashes, and byte-count delta. An audit append failure
|
||||
raises `audit_write_failed` **after the content has landed**; the write is not
|
||||
rolled back. The service appends records without implementing rotation.
|
||||
The service's standalone default is under the user's `.openrig` directory;
|
||||
daemon startup overrides it with the active OpenRig home as described above.
|
||||
|
||||
## HTTP surface
|
||||
|
||||
[`server.ts`](../../../packages/daemon/src/server.ts) mounts these families.
|
||||
All of them sit behind the daemon's `/api/*` browser boundary
|
||||
(`middleware/browser-boundary.ts`), which accepts only known host names, and
|
||||
browser `Origin`s only from the daemon's own UI or `OPENRIG_ALLOWED_ORIGINS`;
|
||||
the file allowlist is a separate check.
|
||||
[`filesRoutes`](../../../packages/daemon/src/routes/files.ts) implements:
|
||||
|
||||
| Method and path | Result / important behavior |
|
||||
|---|---|
|
||||
| `GET /api/files/roots` | Configured roots; an empty list carries a setup hint. |
|
||||
| `GET /api/files/list?root=…&path=…` | Directory entries with metadata, including dotfiles. |
|
||||
| `GET /api/files/read?root=…&path=…` | `readAllowedFile` result. |
|
||||
| `GET /api/files/asset?root=…&path=…` | Raw asset; single byte-range support (`206`, invalid range `416`), five-minute cache. HTML and source files default to plain text and unknown types to `application/octet-stream`; `render=1` opts into HTML rendering. SVG is served as `image/svg+xml`. A `render=1` HTML preview or an SVG is served with no content-security or frame headers, so it can run script in the daemon's origin (`routes/files.ts:153–157`). |
|
||||
| `POST /api/files/write` | Requires root, path, string content, expected mtime and hash. Stale reads return `409 write_conflict`; an absent write service returns `503`. |
|
||||
|
||||
Writes resolve actor/provenance through `resolveActorWithDeferral`. Missing
|
||||
route dependencies return `503`; unknown roots and invalid paths return
|
||||
`400`. A failed stat returns `404` on reads, lists and assets; on a write, a
|
||||
missing or unreadable target returns `500 stat_failed`, because
|
||||
`POST /api/files/write` only replaces existing files
|
||||
(`domain/files/file-write-service.ts:120–128`; `routes/files.ts:264–265`). An
|
||||
audit failure can return `500` even though the file write succeeded, so an
|
||||
error is not proof of no effect.
|
||||
|
||||
## Progress indexing
|
||||
|
||||
[`ProgressIndexer.scan`](../../../packages/daemon/src/domain/progress/progress-indexer.ts)
|
||||
walks configured roots on each request, to a default maximum depth of **6**.
|
||||
It skips dot-prefixed entries and named build/dependency directories, and
|
||||
collects regular `PROGRESS.md` and `STEERING.md` files. Unreadable directories
|
||||
and files are skipped. Consequently, an empty scan does not prove all files
|
||||
were readable.
|
||||
|
||||
The parser skips frontmatter, takes the first H1 as the title, turns
|
||||
H2–H4 headings into hierarchy rows, and reads checkbox rows as `[x]`/`[X]`
|
||||
= done, `[~]` = blocked, and `[ ]` = active. Checkbox depth uses two-space
|
||||
indentation; headings use heading depth. Per-file and aggregate counts cover
|
||||
checkbox rows. This is a progress-text projection, not proof readiness.
|
||||
|
||||
[`GET /api/progress/tree`](../../../packages/daemon/src/routes/progress.ts)
|
||||
returns the scan. Missing indexer or empty configured roots returns `503`.
|
||||
|
||||
## Steering composition
|
||||
|
||||
[`steeringOptsFromSettings`](../../../packages/daemon/src/domain/steering/steering-composer.ts)
|
||||
combines resolved workspace settings with `OPENRIG_STEERING_WORKSPACE`,
|
||||
`OPENRIG_STEERING_PATH`, `OPENRIG_ROADMAP_PATH`, and
|
||||
`OPENRIG_DELIVERY_READY_DIR` overrides. `SteeringComposer.compose` reads:
|
||||
|
||||
| Field | Source and selection |
|
||||
|---|---|
|
||||
| `priorityStack` | Verbatim `STEERING.md`, mtime, and byte count. |
|
||||
| `roadmapRail` | `roadmap/PROGRESS.md` `[ ]`/`[x]` rows (`[~]` rows are not read); the first unchecked row is marked next. |
|
||||
| `laneRails` | Every `PROGRESS.md` and `STEERING.md` under `delivery-ready`, using a depth-3 `ProgressIndexer` (lane ID `mode-N` for `mode-N/PROGRESS.md`, else the relative path); first non-done/non-blocked checkbox is next pull. Default top 3 prefers non-done rows, then fills with done rows. |
|
||||
|
||||
Missing/unreadable sections become `unavailableSources`. `isReady` means at
|
||||
least one path resolves; it does not guarantee that every read succeeds.
|
||||
[`GET /api/steering`](../../../packages/daemon/src/routes/steering.ts)
|
||||
returns `503` when the composer is absent or no source path resolves. Because
|
||||
`workspace.steering_path` always has a value (default
|
||||
`<workspace.root>/STEERING.md`), the daemon's composer is normally ready, and
|
||||
a missing `STEERING.md` appears as a `priorityStack` read failure in
|
||||
`unavailableSources` with a `200`, not a `503`.
|
||||
Queue and health state are fetched through their own endpoints.
|
||||
|
||||
## Health summaries
|
||||
|
||||
[`healthSummaryRoutes`](../../../packages/daemon/src/routes/health-summary.ts)
|
||||
exposes four reads under `/api/health-summary`:
|
||||
|
||||
| Suffix | Source |
|
||||
|---|---|
|
||||
| `/nodes` | `computeNodeHealthSummary`: node inventory aggregated across rigs, grouped by session/lifecycle status, with an attention-required count. |
|
||||
| `/context` | `computeContextHealthSummary`: `context_usage` rows grouped by urgency and sample freshness. |
|
||||
| `/version` | `getDaemonVersion`: the build-stamped version, else the daemon package version, else `unknown`; always `200`. |
|
||||
| `/gateway` | The injected gateway subsystem's `status()`; `503` if unavailable. |
|
||||
|
||||
`/nodes` and `/context` return `503 health_summary_unavailable` when the rig
|
||||
repository is not wired.
|
||||
|
||||
The [context aggregation](../../../packages/daemon/src/domain/steering/health-summary.ts)
|
||||
uses **80%** for critical, **60%** for warning, and **300 seconds** for
|
||||
freshness. These are constants in this module: they do not follow the
|
||||
`health.context_pressure.*` settings (defaults 95% and 99%) that the health
|
||||
detectors use, so the two surfaces can disagree about the same session. A missing usage value is unknown; a missing timestamp has no
|
||||
freshness sample. The SQL read catches errors and falls back to an empty
|
||||
sample set, so zero counts alone are not a database-health guarantee.
|
||||
|
||||
## Related source
|
||||
|
||||
- [Living Notes review](living-notes-review.md) composes proof and scope content.
|
||||
- [Workspace primitive](workspace-primitive.md) covers workspace identity.
|
||||
- [File-path tests](../../../packages/daemon/test/files-path-safety.test.ts),
|
||||
[progress tests](../../../packages/daemon/test/progress-indexer.test.ts), and
|
||||
[steering tests](../../../packages/daemon/test/steering-composer.test.ts)
|
||||
exercise these helpers; their presence is not a runtime verification claim.
|
||||
@@ -1,519 +0,0 @@
|
||||
---
|
||||
kind: as-built
|
||||
title: Coordination Primitive — Stream/Queue/Inbox/Outbox
|
||||
status: active
|
||||
topics: [coordination, observability]
|
||||
domains: [engineering-advisor, operating-advisor, orchestrator]
|
||||
applies-when: |
|
||||
Need to know how the daemon-backed coordination primitive works — the
|
||||
stream/queue/inbox/outbox tables, the hot-potato closure contract, the
|
||||
transactional handoff guarantee, or where queue closure is enforced.
|
||||
siblings: [workflow-runtime.md, mission-control.md, daemon-core.md]
|
||||
prerequisite-reads: [../README.md, daemon-core.md]
|
||||
last-verified-against-source: e8f0ab340db773392ec8be75b072d1c0f3068a50
|
||||
last-updated: 2026-10-08
|
||||
---
|
||||
|
||||
# Coordination Primitive — Stream/Queue/Inbox/Outbox
|
||||
|
||||
The coordination primitive is the SQLite-canonical durable-work layer the
|
||||
daemon exposes through `/api/stream` and `/api/queue`. It replaces the POC
|
||||
filesystem `rigx queue` / `rigx stream` path for daemon-backed work; the POC
|
||||
filesystem path remains untouched, and the daemon-backed `rig queue` /
|
||||
`rig stream` commands operate only through the daemon HTTP API, so they write
|
||||
only to SQLite (`packages/cli/src/commands/queue.ts:19`,
|
||||
`packages/cli/src/commands/stream.ts:11`).
|
||||
|
||||
> Verified against source at main `e8f0ab340db773392ec8be75b072d1c0f3068a50`. Each count below sits beside the
|
||||
> command that produces it; run the command from the repository root to refresh
|
||||
> it.
|
||||
|
||||
## 1. The five host-scoped tables
|
||||
|
||||
Five host-scoped tables back the primitive, one per migration `023`–`027` in
|
||||
`packages/daemon/src/db/migrations/` (**5** tables:
|
||||
`cat packages/daemon/src/db/migrations/02[3-7]_*.ts | grep -c 'CREATE TABLE'`):
|
||||
|
||||
- **`stream_items`** (`023_stream_items.ts`) — L1 append-only intake/audit
|
||||
root. Columns: `stream_item_id` (TEXT PK; a ULID unless the caller supplies
|
||||
one, `packages/daemon/src/domain/stream-store.ts:88`), `ts_emitted`,
|
||||
`stream_sort_key`, `source_session`, `body`, `format` (default `text`),
|
||||
`hint_type`, `hint_urgency`, `hint_destination`, `hint_tags` (JSON),
|
||||
`interrupt`, `archived_at`, and `identity_provenance` (added by
|
||||
`067_i3_identity_provenance.ts`). Items immutable after emit (only
|
||||
`archived_at` may be set, `stream-store.ts:217`).
|
||||
- **`queue_items`** (`024_queue_items.ts`) — L3 owned-work queue.
|
||||
Unless `--id` is supplied, the CLI generates the create request's `qitem_id` (TEXT PK) before sending as `qitem-<UTC YYYYMMDDHHMMSS>-<16 hex>` (64 random bits) and prints it to stderr as a request identity, not proof of persistence; reuse that ID with `--id` and the unchanged payload after an unknown outcome, while daemon callers without an ID still use `newQitemId()` and its `qitem-<UTC YYYYMMDDHHMMSS>-<8 hex>` form.
|
||||
State enum (**8** values, `QUEUE_STATES` at `queue-repository.ts:33`;
|
||||
`sed -n '/^export const QUEUE_STATES/,/] as const/p' packages/daemon/src/domain/queue-repository.ts | grep -c '^ "'`):
|
||||
`pending | in-progress | done | blocked | failed | denied | canceled |
|
||||
handed-off`. Carries `closure_reason`, `closure_target`,
|
||||
`closure_required_at`, `chain_of_record` (JSON), `blocked_on`,
|
||||
`handed_off_to`/`handed_off_from`, nudge/heartbeat columns.
|
||||
- **`queue_transitions`** (`025_queue_transitions.ts`) — L3 append-only
|
||||
transition log; authoritative audit trail for state evolution. Domain code
|
||||
never updates its rows. The one delete is the retention runner, which moves
|
||||
every transition of a terminal item whose last transition is older than the
|
||||
retention window into `queue_transitions_archive`
|
||||
(`054_queue_transitions_archive.ts`), insert and delete in one transaction
|
||||
(`packages/daemon/src/domain/queue-retention.ts:203`). Only `done` and
|
||||
`handed-off` items are archived; the default window is 30 days, and items
|
||||
still on a live workflow frontier are skipped (`queue-retention.ts:69`–`75`,
|
||||
`:158`–`171`).
|
||||
- **`inbox_entries`** (`026_inbox_entries.ts`) — mailbox-style asynchronous
|
||||
deposit; idempotent on `inbox_id`. State: `pending | absorbed | denied`
|
||||
(`INBOX_STATES`, `inbox-handler.ts:5`).
|
||||
- **`outbox_entries`** (`027_outbox_entries.ts`) — sender-side record;
|
||||
symmetric to inbox; idempotent on `outbox_id`. Delivery state (**7** values,
|
||||
`OUTBOX_DELIVERY_STATES` at `outbox-handler.ts:28`;
|
||||
`grep '^export const OUTBOX_DELIVERY_STATES' packages/daemon/src/domain/outbox-handler.ts | grep -o '"[a-z]*"' | wc -l`):
|
||||
`pending | sending | delivered | failed | indeterminate | retained |
|
||||
retired`.
|
||||
|
||||
These five are migrations `023`–`027` of the daemon's **96**
|
||||
(`git ls-files packages/daemon/src/db/migrations | wc -l`), applied in the
|
||||
order of `ALL_MIGRATIONS` (`packages/daemon/src/db/all-migrations.ts:107`).
|
||||
Later migrations add columns to these tables; `daemon-core.md` covers the
|
||||
migration set.
|
||||
|
||||
## 2. The six host-scoped services
|
||||
|
||||
Six host-scoped domain services in `packages/daemon/src/domain/` implement the
|
||||
layer (**6** files:
|
||||
`ls packages/daemon/src/domain/{stream-store,queue-repository,queue-transition-log,hot-potato-enforcer,inbox-handler,outbox-handler}.ts | wc -l`).
|
||||
Routes import these; services are Hono-free:
|
||||
|
||||
- **`stream-store.ts`** — L1 stream: idempotent emit (on `stream_item_id`),
|
||||
chronological list with cursor pagination plus source, destination, exact-tag,
|
||||
and inclusive time-window filters, soft archive. `direction=latest` applies
|
||||
every filter before taking the newest bounded page, then returns that page
|
||||
chronologically.
|
||||
- **`queue-repository.ts`** — L3 queue: create (`:1391`), claim/unclaim
|
||||
(`:2186`, `:2271`), update (general state mutator with hot-potato
|
||||
strict-rejection on `done`, `:2334`), transactional handoff (close source as
|
||||
`handed-off` plus create new owned qitem in a single transaction, `:1656`;
|
||||
`handoffAndComplete`, `:1833`, closes the source as `done` instead),
|
||||
pod-fallback rerouting (`routeToFallback`, `:3678`), overdue lookup
|
||||
(`findOverdue`, `:3378`), nudge-result tracking (`recordNudgeAttempt`,
|
||||
`:3663`). Nothing in the daemon writes the `last_heartbeat` column
|
||||
(`queue-pickup.ts:16`). Cross-rig validation hook exposed as `validateRig`
|
||||
constructor option (`queue-repository.ts:717`).
|
||||
- **`queue-transition-log.ts`** — append-only state-transition log; used by
|
||||
`queue-repository.ts`, exposed as the read-only property
|
||||
`QueueRepository.transitionLog` (`queue-repository.ts:666`).
|
||||
- **`hot-potato-enforcer.ts`** — pure validator for the load-bearing API
|
||||
contract (see §3).
|
||||
- **`inbox-handler.ts`** — mailbox handler: drop (idempotent on `inbox_id`,
|
||||
`:100`), absorb (promotes a pending entry to a `queue_item`, idempotent,
|
||||
`:155`), deny (records reason, `:215`). The handler has no auth hook
|
||||
(`inbox-handler.ts:71`); identity comes from the route. `POST /inbox/drop`
|
||||
takes the sender only from the `X-OpenRig-Session` header through
|
||||
`requireSenderIdentity` (`routes/queue.ts:1094`) and answers 400
|
||||
`actor_required` without it. Absorb and deny (`:1121`, `:1135`) use the
|
||||
header when present; otherwise they accept the body's `receiverSession` and
|
||||
record it as `claimed:v1` (`routes/require-sender-identity.ts:83`–`92`).
|
||||
- **`outbox-handler.ts`** — sender-side outbox: idempotent record (`:130`),
|
||||
delivery-state marks (`markDelivered` `:239`, `markFailed` `:256`,
|
||||
`markIndeterminate` `:280`), list (`listForSender` `:339`). It also holds the
|
||||
queue's durable wake intents (ids prefixed `wake-intent-`, `:15`) and the
|
||||
messages the seat delivery guard retains (`retain` `:181`, `retire` `:217`),
|
||||
so it is not audit-only. Emits no event-bus events.
|
||||
|
||||
Create, handoff and handoff-and-complete return the committed row without
|
||||
waiting for terminal delivery. Each stages a deterministic
|
||||
`wake-intent-<qitem>` outbox row in its transaction and delivers it after
|
||||
commit (`queue-repository.ts:1412`–`1424`, `:1475`–`1483`). The returned
|
||||
`lastNudgeResult` is therefore normally null; `rig queue show <id>` reads the
|
||||
wake result later. If create cannot retain the intent, the task is still saved
|
||||
and the row records `failed:wake not retained: <reason>`. Startup reconciles
|
||||
and drains intents left pending by a crash once (`startup.ts:2361`–`2362`).
|
||||
`maybeNudge` remains only as the path for a repository with no outbox.
|
||||
|
||||
**Which field answers "did it reach them".** `deliveryOutcome` is the gateway
|
||||
(human-delivery) ledger, derived at read time
|
||||
(`QueueRepository.deliveryOutcomeFor`): `posted` or `transport-failed` from the
|
||||
row's Slack receipt notes, or `never-posted` when a gateway-routed row still
|
||||
has no receipt once the post window has passed. A task delivered only to an
|
||||
agent's pane has no such receipt, so it reads `null`. A task that also has a
|
||||
gateway or human-notification side can carry a value, and the CLI then reports
|
||||
that gateway side too. For the pane side, read:
|
||||
|
||||
- `lastNudgeWireResult`: the wake send's result as stored (for example
|
||||
`verified`, `delivered-ack-pending`, `indeterminate:…`, `retained:…`,
|
||||
`failed:…`). A claim never rewrites it; a later wake replaces it.
|
||||
- `lastNudgeResult`: the result as presented. It is the stored result, except
|
||||
that a claimed row whose stored result is `delivered-ack-pending` or
|
||||
`indeterminate:…` reads `claimed` (`presentedNudgeResult`). Unclaiming shows
|
||||
the stored result again.
|
||||
- `claimedAt`: when the destination claimed the row. Closing doesn't clear it.
|
||||
- `pickup`: the derived pickup receipt (`unclaimed`, `working`, `needs-input`,
|
||||
`stalled-after-claim`, `parked`, `terminal`), computed at read time.
|
||||
|
||||
A claimed item whose owner's activity reports outstanding input reads
|
||||
`needs-input`, with the reason in `pickup.evidence`. That covers an item in
|
||||
progress and a `pending` item that kept its claim, after an update back to
|
||||
`pending` or a reopen. A Codex permission request the daemon couldn't confirm on
|
||||
screen reads `owner may need input (not verified: permission_request_unverified)`.
|
||||
The input signal can outlast the prompt it reported, so it never pauses
|
||||
recovery. When the claim would otherwise read `stalled-after-claim`, the receipt
|
||||
keeps that evidence in `pickup.stall`, and the stuck sweep's finding, its
|
||||
backstop and manual dispositions behave exactly as for `stalled-after-claim`.
|
||||
Queue reads, the pickup view and the execution view share this receipt
|
||||
(`queue-pickup.ts`). Unclaimed, blocked and terminal items keep their receipts.
|
||||
|
||||
A claim isn't proof that the body was read or that the terminal render was
|
||||
verified, and closing a task isn't a new delivery receipt: a claimed task that
|
||||
is later closed still reads `claimed` when its stored result was unconfirmed
|
||||
(`delivered-ack-pending` or `indeterminate:…`); a `verified` or `failed:…`
|
||||
result reads as stored. For such an unconfirmed, claimed item,
|
||||
`rig queue create --verify` says "Terminal receipt unconfirmed, but the seat
|
||||
has claimed the item (claimed)."; for any claimed item it adds "A claim shows
|
||||
the seat took the item; it does not prove the body was read."
|
||||
(`terminalDeliveryResult`, `packages/cli/src/commands/queue.ts`).
|
||||
|
||||
The standing detector in `queue-stuck-sweep.ts` creates findings through the
|
||||
queue repository, with `evidenceRef: rig queue show <source-qitem-id>` pointing
|
||||
to the underlying durable work row. Unclaimed and undelivered findings go to
|
||||
the destination's orchestrator when one is derivable, otherwise to the
|
||||
destination itself, and that seat may be dead. Overdue claims go to their
|
||||
destination. Repeated detections reuse the existing finding without changing
|
||||
its route; when the source condition resolves, the sweep closes its own finding.
|
||||
Previously, falling out of a reporting query could itself close a delivery or
|
||||
unclaimed finding. An unconfirmed retry, a fresh human-notification post window,
|
||||
an unreadable human registry or a larger reporting-age setting could therefore
|
||||
look like successful recovery. These findings now require source evidence:
|
||||
closure, pickup or parking of the work, a verified/accepted wake, or a posted
|
||||
notification for the current human recipient and episode, as appropriate to the
|
||||
finding. `delivered-ack-pending`, `indeterminate:…`, missing results and unrelated
|
||||
legacy human receipts do not prove resolution. Uncertainty keeps an existing
|
||||
finding open without itself sending another wake or creating another finding.
|
||||
A verified wake resolves a wake failure, not an unclaimed obligation: delivery
|
||||
does not prove pickup.
|
||||
|
||||
A legacy human notice with no owner-notification history can still resolve from
|
||||
the existing item-scoped posted ledger when its recipient is confirmed human.
|
||||
This covers older pending asks and parks without inventing an episode that was
|
||||
never recorded. A registered alias needs a readable registry to establish its
|
||||
human recipient; reserved human references remain known during an outage.
|
||||
Legacy receipts carry no recipient key, so a destination reroute requires a new
|
||||
matching notification episode. A legacy park is accepted only when no earlier
|
||||
notice or reroute predates it, and later history contains only receipts or
|
||||
asking-agent reroutes within that park. A re-park, an exit from the park or an
|
||||
ambiguous append cannot establish the same recipient without owner history;
|
||||
the finding waits for a new matching episode or other source resolution.
|
||||
Rerouting only the asking agent within an otherwise unchanged park preserves
|
||||
its human recipient (`blockedOn`).
|
||||
Modern notifications still require their current episode, and an unrelated
|
||||
legacy human receipt cannot resolve an agent wake.
|
||||
|
||||
A known hold still leaves recovery with the existing mechanism: an ask on
|
||||
Steering without an alert, a wake held by the seat typing guard, or a live
|
||||
quiet-release job. Its closure note names that disposition rather than claiming
|
||||
delivery or pickup. An unreadable quiet hold is not positive closure evidence.
|
||||
Claimed work in either `pending` or `in-progress` uses the same pickup derivation;
|
||||
returning a row to `pending` while retaining its claim no longer hides a later
|
||||
stall or closes its existing stalled-claim finding. A retained failed wake cannot
|
||||
override that pickup evidence. The agent's claim still does not establish delivery
|
||||
of a separate notification to a person.
|
||||
|
||||
That automatic closure records `stuck-sweep@system` as the actor in both the
|
||||
transition and the `queue.updated` audit event, with the source evidence in the
|
||||
delivery and unclaimed-obligation closure notes. Previously it credited the finding's source agent
|
||||
with the detector's action. The finding's source,
|
||||
destination and claim remain its existing ownership records; the underlying work
|
||||
is not reassigned, and existing transition history is not rewritten.
|
||||
|
||||
`queue-stuck-sweep-resolution.test.ts` checks unconfirmed retries, retained
|
||||
claims, current human-notification episodes and registry outages with migrated
|
||||
SQLite, including positive recovery controls and no repeated send on an
|
||||
unresolved retry. `quiet-hold-custody.test.ts` checks existing findings with
|
||||
readable and unreadable quiet custody. `queue-stuck-sweep.test.ts` checks that
|
||||
pending and claimed findings close once, name the sweep in their transition and
|
||||
event, and keep their ownership. The posted-human-notice case also checks the actor while leaving
|
||||
the person's original ask open. These checks exercise the detector and queue
|
||||
repository, not a live agent or connector.
|
||||
|
||||
The sweep reports six finding kinds. Its custody check covers closures that
|
||||
name a successor qitem or a seat (`closure_reason = handed_off_to`) in the last
|
||||
24 hours, plus older closures with open findings. A seat target is satisfied by
|
||||
a local successor carrying this row's lineage, and any target by a
|
||||
`custody-verified: <target> …` note on the closed row. A new custody finding for
|
||||
a human destination goes to the agent that closed the item, using the closing
|
||||
transition from active or archived history. Later notes and notification
|
||||
receipts do not change that owner. If the closing actor is missing, human, system
|
||||
machinery or not admitted by this host's queue, the asking agent receives the
|
||||
finding if its address is admitted. Human identity includes reserved references
|
||||
and registered aliases, independent of terminal bindings. Admission uses the
|
||||
queue's existing address check and does not require a live seat. An agent
|
||||
destination keeps its existing route; if neither closer nor source is an admitted
|
||||
agent, the human destination remains the durable fallback. This routing does not
|
||||
change custody verification or suppress unresolved findings.
|
||||
|
||||
If the human registry is unavailable when a custody candidate's owner needs
|
||||
classification, that candidate keeps its original destination, and one log line
|
||||
per candidate names it and the registry error. This doesn't infer an agent
|
||||
identity from an unreadable registry, which may not exist before the first human
|
||||
is registered. In a pass with no finding to save for a `<name>@external` person,
|
||||
which includes an install with no registry yet, work bound for agents keeps
|
||||
routing and repaired findings still close. Such a finding still needs the
|
||||
registry to admit it when it's saved, so during an outage a pass that must save
|
||||
one fails at that finding, as it did before this routing. Findings not yet
|
||||
created in that pass, and the closing of repaired findings, wait for the next
|
||||
pass that completes. Once the registry can be read again, the next pass creates
|
||||
the finding and routes it to the closer. Registry recovery restores owner refinement
|
||||
for new findings; existing findings keep their established route. Reserved human
|
||||
references and legacy agent names keep their known classifications without a
|
||||
registry read.
|
||||
|
||||
## 3. The hot-potato closure contract (where queue closure is enforced)
|
||||
|
||||
`hot-potato-enforcer.ts` is the pure validator for the load-bearing API
|
||||
contract. `validateClosure` (`hot-potato-enforcer.ts:59`) checks a transition
|
||||
against `CLOSURE_REASONS` (`:18`; **7** values:
|
||||
`sed -n '/^export const CLOSURE_REASONS/,/] as const/p' packages/daemon/src/domain/hot-potato-enforcer.ts | grep -c '^ "'`):
|
||||
|
||||
`state=done` requires `closure_reason ∈ {handed_off_to, denied,
|
||||
canceled, no-follow-on, escalation, superseded}`. The reasons
|
||||
`handed_off_to | escalation` additionally require
|
||||
`closure_target` (`:86`):
|
||||
|
||||
- `handed_off_to` — work continues with a different seat (`closure_target` =
|
||||
new owner).
|
||||
- `blocked_on` — an active `state=blocked` park record, rejected with `state=done`.
|
||||
To wait for a qitem, use `rig queue block <id> --on <child>` or
|
||||
`rig queue update <id> --state blocked --blocked-on <child>`; the item returns
|
||||
to pending when its blocker completes.
|
||||
- `denied` — receiver rejected the work (`closure_target` = reason text).
|
||||
- `canceled` — sender or receiver withdrew (`closure_target` = note).
|
||||
- `no-follow-on` — terminal completion, nothing else needed.
|
||||
- `escalation` — kicked up to a higher tier (`closure_target` = escalation
|
||||
target).
|
||||
- `superseded` — the row was replaced by cancel-and-replace
|
||||
(`closure_target` = the successor qitem). The update path records it on
|
||||
`state=canceled` and refuses it there without a `closure_target`
|
||||
(`queue-repository.ts:2629`).
|
||||
|
||||
Tier→SLA mapping for `closure_required_at` also lives in
|
||||
`hot-potato-enforcer.ts` (`TIER_SLA_SECONDS`, `:115`). This validator is
|
||||
invoked by `QueueRepository.update()` (and `updateWithinTransaction()`), both
|
||||
through the call at `queue-repository.ts:2531`, so closure is enforced at the
|
||||
daemon transaction boundary — the workflow runtime *projects* on closure but
|
||||
does not otherwise gate it (see `workflow-runtime.md`). The one
|
||||
workflow-aware check is in the queue: `update()` refuses a terminal close of a
|
||||
live workflow frontier packet from a non-workflow verb
|
||||
(`workflow_frontier_packet`, `queue-repository.ts:2557`), through a predicate
|
||||
startup injects. "Terminal" there is the queue's terminal set, `done` and
|
||||
`handed-off` (`queue-repository.ts:52`).
|
||||
|
||||
Closure fields are stored only on `state=done`, the park record (`blocked`
|
||||
with `blocked_on`), the handoff close (`handed-off` with `handed_off_to`) and
|
||||
the superseded cancel; any other transition carrying them is refused with
|
||||
`closure_fields_not_admitted`, and `blocked_on` outside `blocked` with
|
||||
`blocked_on_not_admitted`. A `done`, `canceled` or `handed-off` row accepts a
|
||||
note without a state change as an append-only transition; moving it to
|
||||
another state requires an explicit reopen with a note. A `qitem-` blocker must
|
||||
exist and be live on this daemon; when a blocker leaves the active states,
|
||||
each row parked on it follows the blocker's handoff successor or returns to
|
||||
`pending` with a durable wake (`queue-repository.ts:2407`–`2529`,
|
||||
`:2612`–`2656`, `:2931`–`3008`). Claiming a parked row clears its
|
||||
`blocked_on`; the claim transition keeps the former gate as a `closure_target`
|
||||
audit pointer with no closure reason, which a re-park without `--blocked-on`
|
||||
reuses (`queue-repository.ts:2233`–`2254`, `:2571`–`2577`).
|
||||
`human-route-enforcer.ts` is a second pure validator at the same boundary: a
|
||||
`human-gate` row, a row addressed to a human seat, or a park on one must carry
|
||||
`summary` and `evidence_ref`.
|
||||
|
||||
## 3b. Cross-host queue routing
|
||||
|
||||
Three queue write routes are host-aware — `POST /create`,
|
||||
`POST /:qitemId/handoff` and `POST /:qitemId/handoff-and-complete` (**3**:
|
||||
`grep -c '!resolvesToLocalHost(body.hostId, getSelfHostId())' packages/daemon/src/routes/queue.ts`):
|
||||
a write body may carry an out-of-band `hostId` envelope. The destination
|
||||
session stays `member@rig`; the 3-part `agent@rig@host` form is CLI input
|
||||
sugar that `resolveQueueHostDestination`
|
||||
(`packages/cli/src/commands/queue.ts:394`) splits into the destination and
|
||||
`hostId` before the request leaves the CLI. The same split applies to
|
||||
`--host <id>`; queue verbs never follow `rig host select`, and naming two
|
||||
different hosts is refused with `host_qualifier_conflict`. The mechanism
|
||||
follows the forward-then-strip shape of mission-control's remote action
|
||||
(`routes/mission-control.ts:352`): one shared route-layer helper
|
||||
(`forwardQueueWrite`, `routes/queue.ts:214`) resolves the host registry
|
||||
daemon-side (bearers never reach the caller), rejects ssh-declared hosts
|
||||
(`unsupported-transport` — the daemon→daemon path is http-only), and forwards
|
||||
the whole body, with `hostId` stripped, via `remoteJsonRequest` (`:248`) under
|
||||
a named write-class deadline (`QUEUE_FORWARD_TIMEOUT_MS`, `:41`). The origin's
|
||||
response returns verbatim; failures map to a structured host-named error
|
||||
(`remote_queue_write_failed`, HTTP 502, `:234`) whose `failureClass` is one of
|
||||
registry / unknown-host / unsupported-transport / unreachable / auth-failed /
|
||||
remote-error. No local row is ever written on the cross-host path.
|
||||
|
||||
**The model: origin-owns-the-record, at-least-once + idempotent,
|
||||
message-passing closure (never 2PC).**
|
||||
|
||||
- **Origin-owns-the-record.** The qitem lives in the TARGET host's DB; that
|
||||
row is THE record. The target daemon wakes the destination on ITS local
|
||||
tmux: its own `create` stages a durable wake intent and delivers it after
|
||||
commit (§2; the forwarded body carries the `nudge` flag) — the sending
|
||||
daemon never reaches across a host boundary. Before forwarding, the
|
||||
forwarding daemon stamps its own host id, when it has one, onto the source
|
||||
session (`stampSelfHostSuffix`, `queue-repository.ts:548`; called at
|
||||
`routes/queue.ts:378` and `:509`), so the target row records the sender as
|
||||
`member@rig@<forwarding host>`.
|
||||
- **Idempotency.** On create, the forwarding daemon mints the `qitemId` before
|
||||
the forward unless the caller supplied one (`routes/queue.ts:501`). The
|
||||
forward is a single request, so a retry dedups only when it carries the same
|
||||
id (a caller re-sending `--id`, or a handoff re-drive, whose successor id is
|
||||
derived). Dedup rides the existing `qitem_id TEXT PRIMARY KEY`. On PK
|
||||
conflict the origin returns the stored row when destination and source
|
||||
match (idempotent absorb) and a structured `qitem_id_reuse` error (409) when
|
||||
they differ (`QueueRepository.create()` catch path,
|
||||
`queue-repository.ts:1431`–`1472`, with `isQitemPrimaryKeyConflict`,
|
||||
`:461`). When destination and source match but the body differs, the stored
|
||||
row is returned unchanged with `createWarning: qitem_body_not_saved`; the
|
||||
supplied body is not saved (`:1450`–`1454`).
|
||||
- **Cross-host handoff choreography.** The local atomic close+create cannot
|
||||
span two DBs, so the route-layer choreography (`crossHostHandoff`,
|
||||
`routes/queue.ts:316`) runs: successor-create on the target host FIRST (via
|
||||
the one forward helper, `:397`), local source-close SECOND
|
||||
(`QueueRepository.closeCrossHostHandoffSource`, `queue-repository.ts:2031`)
|
||||
— never the reverse. A crash between the two leaves a live duplicate that
|
||||
the idempotent re-drive converges; the reverse order would leave a closed
|
||||
source pointing at a successor that does not exist (a dropped potato — the
|
||||
one forbidden outcome). The successor id is DERIVED, not minted:
|
||||
`deriveCrossHostSuccessorId(source, destination, host)`
|
||||
(`queue-repository.ts:489`) → `qitem-xh-<sha256[:16]>` — a pure stateless
|
||||
function, so a re-drive re-derives the same id across daemon restarts and
|
||||
absorbs on the target PK. *(Residual, inherent to at-least-once delivery
|
||||
without 2PC: a re-drive naming a DIFFERENT destination derives a different
|
||||
id and cannot absorb the earlier successor — that orphan stays visible via
|
||||
the chain + provenance tags; the source-close conflict check surfaces the
|
||||
disagreement.)*
|
||||
- **Closure across the boundary.** The source closes with
|
||||
`closure_reason=handed_off_to` and
|
||||
`closure_target=<successor qitem id>@<host>` (`routes/queue.ts:351`); a
|
||||
source already closed with the older `member@rig@<host>` target keeps it,
|
||||
and a re-drive matches against it (`:352`–`356`). `closure_target` is OPAQUE
|
||||
audit metadata, never parsed for routing; the standing stuck sweep reads it
|
||||
only to check successor custody (`queue-stuck-sweep.ts:519`–`554`). The
|
||||
host-qualified key appears only in the `closure_target` column on
|
||||
`queue_items` and its verbatim mirror on `queue_transitions`: the minted
|
||||
cross-host close note names the 2-part `toSession` only
|
||||
(`queue-repository.ts:2087`), and `handed_off_to` and the
|
||||
`queue.handed_off` event stay 2-part.
|
||||
Re-drive semantics: already-terminal + MATCHING `closure_target` = absorb;
|
||||
MISMATCH = structured `cross_host_close_conflict` (409), checked in the route
|
||||
before any forward (`routes/queue.ts:359`) and again in the repository. The
|
||||
successor carries `chain_of_record = [...source.chain, source.qitemId]`
|
||||
(`routes/queue.ts:384`) — A-side ids are opaque lineage identifiers on B
|
||||
(they do not dereference in B's DB) — plus provenance tags `cross-host` +
|
||||
`from-host:<self-declared name>` (the forwarding daemon's OS hostname,
|
||||
`routes/queue.ts:54`; honest best-effort, not authenticated identity).
|
||||
- **Boundary discipline.** Claim, update and inbox routes take no `hostId` and
|
||||
stay local (after a cross-host handoff the successor lives where its worker
|
||||
lives). Without a `hostId`, with `local`, or with this daemon's own resolved
|
||||
host id (an exact, case-sensitive match), create and handoff take the
|
||||
local path; a self-addressed handoff whose deterministic cross-host
|
||||
successor an earlier self-forward already created runs that cross-host
|
||||
create and close in process. The
|
||||
hot-potato validation contract (§3) is unweakened across the boundary: the
|
||||
cross-host close always records `handed_off_to` with a target.
|
||||
|
||||
## 4. Coordination events
|
||||
|
||||
The `RigEvent` union (`packages/daemon/src/domain/types.ts:108`) has **98**
|
||||
members
|
||||
(`sed -n '/^export type RigEvent =/,/^export type PersistedEvent/p' packages/daemon/src/domain/types.ts | grep -c 'type: "'`);
|
||||
this module covers only the coordination families below.
|
||||
|
||||
Coordination events emitted by these services: `stream.emitted`
|
||||
(`StreamStore.emit`, declared at `types.ts:245`); `queue.created` /
|
||||
`queue.handed_off` / `queue.claimed` / `queue.unclaimed` / `queue.updated` /
|
||||
`qitem.fallback_routed` / `qitem.closure_overdue` (QueueRepository); `inbox.absorbed` (`types.ts:255`) /
|
||||
`inbox.denied` (InboxHandler). `qitem.closure_overdue` (`types.ts:254`) is
|
||||
emitted by `QueueRepository.recordClosureOverdue` when swept by `queue-stuck-sweep`
|
||||
and passed through the queue watch filter (**2** files contain its literal:
|
||||
`git grep -l -F 'type: "qitem.closure_overdue"' -- packages/daemon/src | wc -l`).
|
||||
The `stream|queue|inbox|qitem` families declare **10** event types (`stream` 1
|
||||
+ `queue` 5 + `inbox` 2 + `qitem` 2;
|
||||
`sed -n '/^export type RigEvent =/,/^export type PersistedEvent/p' packages/daemon/src/domain/types.ts | grep -oE '"(stream|queue|inbox|qitem)\.[a-z_]+"' | sort -u | wc -l`).
|
||||
|
||||
Two SSE surfaces stream coordination events (**2**:
|
||||
`cat packages/daemon/src/routes/stream.ts packages/daemon/src/routes/queue.ts | grep -c 'return streamSSE('`):
|
||||
`/api/stream/watch` (aliased `/api/stream/sse`, `routes/stream.ts:194`–`195`),
|
||||
which first replays up to 50 unarchived items in chronological order (the
|
||||
oldest, `:174`–`181`) and then streams new items, and `/api/queue/watch`
|
||||
(aliased `/api/queue/sse`, `routes/queue.ts:1054`–`1055`) for queue/inbox
|
||||
events — every coordination type except `stream.emitted` and `queue.updated`
|
||||
(filter at `routes/queue.ts:1031`–`1038`). The event log remains append-only and
|
||||
SQLite-backed. `rig stream watch` is a thin, single-connection consumer of
|
||||
`/api/stream/sse` (`packages/cli/src/commands/stream.ts:191`); it does not add
|
||||
a daemon route or reconnect policy.
|
||||
|
||||
## 5. Route surface
|
||||
|
||||
The TUI FEED reads `/api/attention` for human requests and current explicit
|
||||
human-addressed updates. Its independent update query filters open states and
|
||||
strict human destinations before the 1001-row sentinel, serves at most 1000
|
||||
updates with critical then urgent rows first, and requires no transport receipt. These local
|
||||
FYIs create no approval obligation. `/api/queue/human-updates` remains confirmed
|
||||
delivery history; the TUI suppresses a history card when the same queue item is
|
||||
in the current update window. Current-update coverage is independent of the
|
||||
receipt window, so an unavailable current reader cannot retain closed delivery
|
||||
history beyond a successful history read. Both reads leave queue state intact.
|
||||
|
||||
Clients that need only current FYIs can use `GET /api/queue/open-human-updates`.
|
||||
It returns `{items, limit, truncated}` with open human-addressed updates, ordered
|
||||
critical then urgent then routine, without requiring a delivery receipt. `limit`
|
||||
defaults to 20 and accepts integers from 1 to 100. Optional `view` follows the
|
||||
same person perspective as `/api/attention`; a perspective read scans at most
|
||||
1000 rows and signals `truncated` when the window may omit matching items.
|
||||
|
||||
A finished item's result file is its `evidenceRef`, read through the daemon's
|
||||
shared roots with `rig_artifact_read` (MCP) or `rig queue artifact <qitemId>
|
||||
--json` (CLI). For a code result, the assignee writes a JSON file of kind
|
||||
`openrig.code-result/v1`, with `repo` (an absolute path or a URL), `base` and
|
||||
`revision` (full commit ids, SHA-1 or SHA-256), and optionally `paths`, `branch`
|
||||
and `fetch: {remote, ref}`. Both reads then add `codeResult`, the exact pair a
|
||||
client can diff. A manifest whose base or revision isn't a full commit id gets
|
||||
`codeResultIssue` instead. `branch` and `fetch` are hints for finding the commits,
|
||||
never the revision. Other files read as before.
|
||||
|
||||
- `/api/stream` (`server.ts:783`) — `POST /emit` (`routes/stream.ts:56`),
|
||||
`GET /list` (`:91`, including `sourceSession`, `hintDestination`, `hintTag`,
|
||||
`since`, `until` and `direction` filters), `GET /watch` + `/sse` SSE
|
||||
(`:194`), `GET /:streamItemId` (`:198`), `POST /:streamItemId/archive`
|
||||
(`:207`).
|
||||
- `/api/queue` (`server.ts:784`) — `POST /create` (`routes/queue.ts:450`),
|
||||
`POST /:qitemId/claim` (`:545`), `POST /:qitemId/unclaim` (`:561`),
|
||||
`POST /:qitemId/update` (`:600`), plus handoff (`:650`, `:732`),
|
||||
`POST /:qitemId/fallback` (`:808`), list (`:902`) and watch (`:1054`)
|
||||
surfaces; reads `GET /whoami`, `/attention-aggregate`, `/human-updates`,
|
||||
`/overdue`, `/undelivered`, `/recent-transitions`, `/:qitemId/transitions`
|
||||
and `/:qitemId` (`:822`–`1067`); inbox routes `POST /inbox/drop`,
|
||||
`/inbox/:id/absorb`, `/inbox/:id/deny` and `GET /inbox/pending`,
|
||||
`/inbox/list` (`:1076`–`1152`); outbox routes `POST /outbox/record` and
|
||||
`GET /outbox/list` (`:1161`, `:1190`).
|
||||
|
||||
Every write route derives its actor through `requireSenderIdentity`
|
||||
(`routes/require-sender-identity.ts:76`): the `X-OpenRig-Session` header wins
|
||||
over a body actor. On a direct request from a known origin it is recorded as
|
||||
`transport:v1`; `resolveRecordedProvenance` (`:199`) instead records
|
||||
`origin-unknown:v1` when the origin-unknown marker is set or a relayed request
|
||||
carried `origin-unknown:v1` (`:207`), `relay:v1` for a relayed request that
|
||||
carried `transport:v1`, and `claimed:v1` for other relayed requests. `transport:v1`
|
||||
says the name came from that header, which the CLI sets from the seat's
|
||||
`OPENRIG_SESSION_NAME`; it isn't an authentication. Without the
|
||||
header, the
|
||||
body actor is recorded as `claimed:v1`; with neither (or, for inbox drop,
|
||||
without the header) the route answers 400 `actor_required`. The stamp lands in
|
||||
`identity_provenance` on transitions, inbox, outbox and stream rows
|
||||
(migrations `067`, `082`).
|
||||
|
||||
## See also
|
||||
|
||||
- `daemon-core.md` — daemon wiring, the migration set, route surface.
|
||||
- `workflow-runtime.md` — the workflow runtime that projects on closure.
|
||||
- `mission-control.md` — queue observability over `queue_items`.
|
||||
- Source roots: `packages/daemon/src/domain/{stream-store,queue-repository,
|
||||
queue-transition-log,hot-potato-enforcer,inbox-handler,outbox-handler}.ts`,
|
||||
`packages/daemon/src/routes/{stream,queue}.ts`.
|
||||
@@ -1,242 +0,0 @@
|
||||
---
|
||||
kind: as-built
|
||||
title: Daemon Core — Wiring, DB, Migrations, Startup
|
||||
status: active
|
||||
topics: [runtime-control, observability]
|
||||
domains: [engineering-advisor, operating-advisor]
|
||||
applies-when: |
|
||||
Need to know how the daemon boots, how createDaemon wires the dependency
|
||||
graph, the SQLite schema/migration set, or the route-mount surface.
|
||||
siblings: [coordination-primitive.md, agent-spec-and-startup.md, lifecycle-snapshot-restore.md]
|
||||
prerequisite-reads: [../README.md]
|
||||
last-verified-against-source: e8f0ab340db773392ec8be75b072d1c0f3068a50
|
||||
last-updated: 2026-10-08
|
||||
---
|
||||
|
||||
# Daemon Core — Wiring, DB, Migrations, Startup
|
||||
|
||||
OpenRig is a local control plane for multi-agent coding topologies. The daemon
|
||||
(`@openrig/daemon`) is the framework-free SQLite-backed core that the CLI
|
||||
(`@openrig/cli`), the terminal UI (`@openrig/tui`), the web UI (`@openrig/ui`)
|
||||
and the MCP server all sit on top of.
|
||||
|
||||
> Verified against source at main `e8f0ab340db773392ec8be75b072d1c0f3068a50`. Each count below sits beside the
|
||||
> command that produces it; run the command from the repository root to refresh
|
||||
> it.
|
||||
|
||||
## 1. System overview
|
||||
|
||||
For what OpenRig is and how its packages fit together, read `ARCHITECTURE.md` at
|
||||
the repository root. This module covers the daemon's own wiring.
|
||||
|
||||
### Source footprint at `e8f0ab340db773392ec8be75b072d1c0f3068a50`
|
||||
|
||||
The footprint counts use non-test TypeScript files under each package's `src/`
|
||||
(tests live in separate `packages/*/test/` directories):
|
||||
`find <dir> -type f \( -name '*.ts' -o -name '*.tsx' \) ! -name '*.test.*' | wc -l`.
|
||||
|
||||
| Metric | Count | Directory or command |
|
||||
|---|---|---|
|
||||
| All packages | **1205** | `packages/*/src` |
|
||||
| Daemon | **660** (**431** under `domain/`, **29** under `adapters/`) | `packages/daemon/src`, `…/src/domain`, `…/src/adapters` |
|
||||
| CLI | **174** | `packages/cli/src` |
|
||||
| Web UI | **304** | `packages/ui/src` |
|
||||
| TUI | **67** | `packages/tui/src` |
|
||||
| Database migrations | **96** (`001_core_schema.ts` … `097_thread_part_map.ts`; no `093`) | `git ls-files packages/daemon/src/db/migrations \| wc -l` |
|
||||
| Files in `routes/` | **68**, of which **65** create a Hono router | `git ls-files packages/daemon/src/routes \| wc -l`; `git grep -l 'new Hono' -- packages/daemon/src/routes \| wc -l` |
|
||||
| `app.route(...)` mounts in `server.ts` | **69** | `grep -c 'app.route(' packages/daemon/src/server.ts` |
|
||||
| Top-level `rig` commands | **87** | `grep -c 'program.addCommand(' packages/cli/src/index.ts` |
|
||||
| MCP tools | **18**, all named `rig_*` | `grep -c 'server.tool(' packages/cli/src/mcp-server.ts` |
|
||||
| Runtime adapter classes | **5**: Claude Code, Codex, Pi, Stub, Terminal | `git grep -l 'implements RuntimeAdapter' packages/daemon/src \| wc -l` |
|
||||
|
||||
`OmpRuntimeAdapter` (Oh My Pi) extends `PiRuntimeAdapter`, so the daemon wires
|
||||
six runtime keys — `claude-code`, `codex`, `pi`, `omp`, `stub`, `terminal`
|
||||
(`startup.ts:989`).
|
||||
|
||||
### The stack
|
||||
|
||||
```text
|
||||
CLI (87 top-level commands) / TUI / web UI / MCP (24 tools)
|
||||
|
|
||||
v
|
||||
Hono daemon routes (69 app.route() mounts + direct handlers for /healthz,
|
||||
spec export, unknown /api paths and the static UI)
|
||||
|
|
||||
+-- rigspec routes (pod-aware and legacy spec formats)
|
||||
+-- env routes (status / logs / down)
|
||||
+-- transport routes (send / capture / broadcast)
|
||||
+-- transcript routes (tail / grep)
|
||||
+-- ask routes (context evidence)
|
||||
+-- chat routes (durable rig messaging + SSE)
|
||||
+-- spec review / spec library routes
|
||||
+-- whoami identity route
|
||||
+-- coordination routes (stream / queue / workflow / mission control)
|
||||
|
|
||||
v
|
||||
Framework-free domain services (431 files under packages/daemon/src/domain)
|
||||
|
|
||||
+-- SQLite state (96 migrations)
|
||||
+-- tmux / cmux / resume adapters
|
||||
+-- runtime adapters (Claude Code / Codex / Pi / Oh My Pi / Stub / Terminal)
|
||||
+-- rig environment services (compose adapter, service readiness, orchestrator)
|
||||
+-- transport / transcript / chat / ask layers
|
||||
+-- whoami identity service (including context usage)
|
||||
```
|
||||
|
||||
The core product loop: `down (auto-snapshot) → up <rig-name> (auto-restore) →
|
||||
handoff → inspect/attach → work → repeat`.
|
||||
|
||||
## 2. Database schema
|
||||
|
||||
The migrations live in `packages/daemon/src/db/migrations/` (96 files,
|
||||
`001_core_schema.ts` … `097_thread_part_map.ts`, with no `093`).
|
||||
`ALL_MIGRATIONS` (`packages/daemon/src/db/all-migrations.ts:107`) lists all 96, and `createDaemon`
|
||||
applies them with `migrate(db, ALL_MIGRATIONS)` (`startup.ts:298`). `migrate.ts`
|
||||
sorts them by name (`:29`) and records each applied name in `schema_migrations`
|
||||
(`:15`).
|
||||
|
||||
### Core state tables
|
||||
|
||||
`rigs` (topology container, `001_core_schema.ts:7`), `nodes` (logical node
|
||||
identity, `:17`) and `edges` (logical topology relationships, `:30`); then
|
||||
`bindings` (physical tmux/cmux surface attachment) and `sessions` (live
|
||||
execution state) in `002_bindings_sessions.ts`, `events` (append-only event log)
|
||||
in `003_events.ts`, `snapshots` (serialized rig state) in `004_snapshots.ts`, and
|
||||
`checkpoints` (per-node recovery state) in `005_checkpoints.ts`.
|
||||
|
||||
### Pod-aware schema
|
||||
|
||||
- `014_agentspec_reboot.ts` — the pod-aware schema; adds the `pods` and
|
||||
`continuity_state` tables and pod-aware columns on `nodes`, `sessions` and
|
||||
`checkpoints`.
|
||||
- `015_startup_context.ts` — persisted startup replay context for restore.
|
||||
- `016_chat_messages.ts` — durable rig-scoped chat (SQLite-backed; transcripts
|
||||
remain filesystem-backed, written by bounded periodic `capture-pane` snapshots;
|
||||
see `transport-and-transcripts.md`).
|
||||
- `017_pod_namespace.ts` — first-class authored pod namespace for export/adoption.
|
||||
- `018_context_usage.ts` — per-node context-usage snapshots.
|
||||
- `019_external_cli_attachment.ts` — binding-row extension for external CLI attach.
|
||||
- `020_rig_services.ts` — rig-scoped environment record for service-backed rigs.
|
||||
- `021_seat_handover_observability.ts`, `022_node_codex_config_profile.ts`.
|
||||
|
||||
### Coordination, workflow, mission control and workspace migrations
|
||||
|
||||
- `023_stream_items.ts` … `027_outbox_entries.ts` — the coordination tables
|
||||
(stream, queue, queue transitions, inbox, outbox; detail in
|
||||
`coordination-primitive.md`).
|
||||
- `028_project_classifications.ts`, `029_classifier_leases.ts`,
|
||||
`030_views_custom.ts` — classifier and view tables.
|
||||
- `031_watchdog_jobs.ts`, `032_watchdog_history.ts` — watchdog tables.
|
||||
- `033_workflow_specs.ts`, `034_workflow_instances.ts`,
|
||||
`035_workflow_step_trails.ts` — workflow runtime tables (detail in
|
||||
`workflow-runtime.md`). `036_watchdog_policy_enum_extension.ts` runs no DDL (its
|
||||
SQL is a no-op `SELECT 1 WHERE 0`); it documents an enum extension.
|
||||
- `037_mission_control_actions.ts` — the mission control audit table
|
||||
(`037_mission_control_actions.ts:58`; detail in `mission-control.md`).
|
||||
- `038_workspace_primitive.ts`, `039_queue_target_repo.ts` — the typed workspace
|
||||
primitive.
|
||||
- `040_workflow_specs_diagnostic.ts` — `ALTER TABLE workflow_specs ADD COLUMN`
|
||||
for parser/validator diagnostics (no new table).
|
||||
|
||||
Migrations `041`–`092` and `094`–`097` continue in the same directory; list them with
|
||||
`git ls-files packages/daemon/src/db/migrations`. `095_rig_non_interruptive.ts` adds
|
||||
`rigs.non_interruptive` (0 or 1, default 0), the saved per-rig choice behind
|
||||
`rig up --non-interruptive`; `096_rig_install_root.ts` adds `rigs.install_root`, the rig's
|
||||
bundle materialization folder (`rig-repository.ts:169`). The newest, `097_thread_part_map.ts`,
|
||||
adds `thread_part_map`, which maps each message OpenRig posts into an ask's Slack thread to
|
||||
that ask and its seat.
|
||||
|
||||
The package, bootstrap and discovery tables remain: `packages`
|
||||
(`008_packages.ts`), `package_installs` and `install_journal`
|
||||
(`009_install_journal.ts`), `bootstrap_runs`, `bootstrap_actions` and
|
||||
`runtime_verifications` (`011_bootstrap.ts`), and `discovered_sessions`
|
||||
(`012_discovery.ts`).
|
||||
|
||||
## 3. Route-mount surface
|
||||
|
||||
`createApp(deps)` (`packages/daemon/src/server.ts:442`) mounts **69**
|
||||
`app.route()` route groups (`server.ts:723`–`839`) plus direct handlers:
|
||||
`GET /healthz` (`:664`), `GET /api/rigs/:rigId/spec` (`handleExportYaml`,
|
||||
`:735`), `GET /api/rigs/:rigId/spec.json` (`handleExportJson`, `:736`), a JSON
|
||||
`404 not_found` for any other `/api/*` path (`:844`), and the static/deep-link
|
||||
`app.get("*")` catch-all (`:853`).
|
||||
|
||||
The 69 is the count of `app.route(` lines in `server.ts`; `packages/daemon/src/routes/`
|
||||
has 68 files, 65 of which create a Hono router.
|
||||
|
||||
Mount families include the rig, session and spec routes plus the coordination
|
||||
routes (`/api/stream` `server.ts:783`, `/api/queue` `:784`, `/api/workflow`
|
||||
`:789`, `missionControlRoutes(...)` `:792`), `/api/seat` (`:781`, the seat
|
||||
lifecycle verbs such as handover, launch, continue, stop and the typing guard),
|
||||
`/api/startup` (`:747`), `/api/terminal` (`:809`), `/api/telemetry` (`:822`),
|
||||
`/api/health-summary` (`:829`), `/api/rigs/:rigId/env` (`:835`) and
|
||||
`/api/restore-check` (`:836`). List every mount with
|
||||
`grep -n 'app.route(' packages/daemon/src/server.ts`.
|
||||
|
||||
Before the routes, `createApp` installs middleware in this order: the request-phase
|
||||
observer (`:497`), JSON body-parse error tracking, which turns a malformed body into
|
||||
`400 invalid_json` (`:499`), dependency injection, route timing, slow-operation
|
||||
recording, the browser boundary on `/api/*` (`:647`, Host and Origin checks; see
|
||||
`OPENRIG_ALLOWED_HOSTS` and `OPENRIG_ALLOWED_ORIGINS`), and host read-through.
|
||||
|
||||
`createAppWithWebSocket(deps)` (`server.ts:893`) sets `enableNodeWebSocket` and
|
||||
calls `createApp`; the terminal WebSocket is registered only when the web UI is
|
||||
also enabled (`server.ts:756`). Otherwise only its bearer-auth handler is
|
||||
registered (`registerTerminalAuthOnly`, `:762`).
|
||||
|
||||
## 4. Startup sequence (`createDaemon`)
|
||||
|
||||
`createDaemon(opts?)` is `packages/daemon/src/startup.ts:287` (async, returns
|
||||
`DaemonResult`). It returns `{ app, db, deps, contextMonitor, eventLoopMonitor,
|
||||
injectWebSocket }` (`startup.ts:2506`). In source order, it:
|
||||
|
||||
1. Opens SQLite and applies all 96 migrations (`migrate(db, ALL_MIGRATIONS)`,
|
||||
`startup.ts:298`).
|
||||
2. Constructs the coordination stores early: `StreamStore` (`:330`),
|
||||
`QueueRepository` (`:428`) and `OutboxHandler` (`:443`).
|
||||
3. Constructs `TranscriptStore` (`:555`) and the rig environment services
|
||||
`ComposeServicesAdapter` and `ServiceOrchestrator` (`:644`–`645`).
|
||||
4. Constructs `StartupOrchestrator` (`:776`) and the runtime adapters:
|
||||
`ClaudeCodeAdapter` (`:778`), `CodexRuntimeAdapter` (`:779`),
|
||||
`PiRuntimeAdapter` (`:782`), `OmpRuntimeAdapter` (`:783`) and
|
||||
`StubRuntimeAdapter` (`:788`); the terminal adapter is created inline in the
|
||||
runtime adapter maps (`:989`, `:1259`).
|
||||
5. Constructs `PodRigInstantiator` (`:985`), `PodBundleSourceResolver` (`:1014`)
|
||||
and `BootstrapOrchestrator` (`:1016`).
|
||||
6. Constructs `ContextUsageStore` (`:1095`), `ResumeMetadataRefresher`
|
||||
(`:1129`), `SpecReviewService` (`:1154`) and `WhoamiService` (`:1157`).
|
||||
7. Builds `AppDeps` (`const deps: AppDeps`, `:1191`). `SessionTransport`
|
||||
(`:1262`), `ChatRepository` (`:1296`), `InboxHandler` (`:1300`), `AskService`
|
||||
(`:1349`) and `SpecLibraryService` (`:1387`) are constructed inside that
|
||||
object literal. `InboxHandler` receives the same queue repository instance
|
||||
(`queueRepoInstance`) that serves `/api/queue`, so absorbed inbox items and the
|
||||
queue route write to one store.
|
||||
8. Constructs `ContextMonitor` (`:2407`), then assigns later fields onto `deps`
|
||||
(among them the terminal service, the request-phase observer and the gateway
|
||||
subsystem).
|
||||
9. Calls `createAppWithWebSocket(deps)` (`startup.ts:2504`) to mount the full
|
||||
route tree.
|
||||
|
||||
Node inventory is a set of functions (`getNodeInventory` and friends in
|
||||
`domain/node-inventory.ts`), imported where needed rather than constructed at
|
||||
startup.
|
||||
|
||||
The daemon entrypoint `packages/daemon/src/index.ts:306` calls
|
||||
`createDaemon({ dbPath, bearerToken, terminalBearerToken, … })`.
|
||||
|
||||
## 5. Test files
|
||||
|
||||
A static count of tracked test files at `e8f0ab340db773392ec8be75b072d1c0f3068a50` (no pass counts are claimed
|
||||
here; CI runs the suites in `.github/workflows/tests.yml`): daemon **927**,
|
||||
CLI **261**, web UI **198**, TUI **105**
|
||||
(`git ls-files 'packages/<package>/**/*.test.ts' 'packages/<package>/**/*.test.tsx' | wc -l`).
|
||||
|
||||
## See also
|
||||
|
||||
- `ARCHITECTURE.md` (repository root) — what OpenRig is and where to add things.
|
||||
- `coordination-primitive.md` — the stream/queue/inbox/outbox coordination layer.
|
||||
- `agent-spec-and-startup.md` — spec parsing/resolution/startup contract.
|
||||
- `lifecycle-snapshot-restore.md` — snapshot/restore/continuity.
|
||||
- Source roots: `packages/daemon/src/{startup.ts,server.ts,index.ts}`,
|
||||
`packages/daemon/src/db/{all-migrations.ts,migrate.ts}`,
|
||||
`packages/daemon/src/db/migrations/`, `packages/cli/src/index.ts`.
|
||||
@@ -1,437 +0,0 @@
|
||||
---
|
||||
kind: as-built
|
||||
title: Lifecycle — Snapshot, Restore, Continuity
|
||||
status: active
|
||||
topics: [continuity, runtime-control]
|
||||
domains: [engineering-advisor, operating-advisor]
|
||||
applies-when: |
|
||||
Need to know how OpenRig captures a snapshot, restores a rig (resume vs
|
||||
rebuild vs fresh-primed vs stop-and-ask), enforces restore honesty, consults
|
||||
live continuity state, or how the daemon-side restore-check readiness probe
|
||||
and the CLI restore-packet command work.
|
||||
siblings: [daemon-core.md, agent-spec-and-startup.md, transport-and-transcripts.md]
|
||||
prerequisite-reads: [../README.md, agent-spec-and-startup.md]
|
||||
last-verified-against-source: e8f0ab340db773392ec8be75b072d1c0f3068a50
|
||||
last-updated: 2026-10-08
|
||||
---
|
||||
|
||||
# Lifecycle — Snapshot, Restore, Continuity
|
||||
|
||||
The durable-state half of the core product loop:
|
||||
`down (auto-snapshot) → up <rig-name> (auto-restore) → handoff`. Snapshot
|
||||
captures serialized rig state; restore replays it honestly (no silent
|
||||
fresh-fallback); restore-check is a separate read-only readiness probe.
|
||||
|
||||
> Verified against source at main `e8f0ab340db773392ec8be75b072d1c0f3068a50`. Each count below sits beside the
|
||||
> command that produces it; run the command from the repository root to refresh
|
||||
> it.
|
||||
|
||||
## 1. Snapshot / restore / continuity types
|
||||
|
||||
All in `packages/daemon/src/domain/types.ts`. The spec/projection types live in
|
||||
`agent-spec-and-startup.md`; the snapshot/restore types live here.
|
||||
|
||||
- **NodeRestoreOutcome** (`types.ts:500`) — the per-node restore outcome
|
||||
projected into `rig ps`. **9** values
|
||||
(`grep '^export type NodeRestoreOutcome' packages/daemon/src/domain/types.ts | grep -o '"[^"]*"' | wc -l`):
|
||||
`resumed`, `rebuilt`, `fresh`, `fresh-primed`, `awaiting-decision`,
|
||||
`failed`, `attention_required`, `operator_recovered`, `n-a`. The restore
|
||||
result itself, `RestoreNodeResult.status` (`types.ts:475`), has the same
|
||||
values without `n-a`, plus `left_attached`: **9**
|
||||
(`grep -E '^ status: "resumed"' packages/daemon/src/domain/types.ts | grep -o '"[^"]*"' | wc -l`).
|
||||
`left_attached` isn't a `NodeRestoreOutcome`; the inventory projects it as `n-a`.
|
||||
Where `restore-orchestrator.ts` sets each one:
|
||||
- `resumed` — `baseStatus = "resumed"` after a legacy resume (`:1030`); the
|
||||
status is returned only by `finishJoinedResume` (defined `:1320`, called
|
||||
`:1244` and `:1311`, returning `resumed` at `:1380`) after it has rebound
|
||||
and verified the pane; otherwise that becomes `attention_required`
|
||||
(`:1352`, `:1376`).
|
||||
- `rebuilt` — a checkpoint file was written (`:1090`).
|
||||
- `fresh-primed` — the default for a deliberate non-resume launch
|
||||
(`:987`).
|
||||
- `fresh` — only the skip path for a pod node whose live continuity state
|
||||
is already `restoring` (`:785`).
|
||||
- `awaiting-decision` — the seat could not resume and nothing is running:
|
||||
before launch (`:820`), or after launch once the blank session is rolled
|
||||
back (`:1026`, `:1048`, `:1061`, `:1077`, `:1236`).
|
||||
- `attention_required` — a live session waiting on a runtime prompt
|
||||
(`:1039`, `:1262`) or a harness that never started (`:1125`).
|
||||
- `failed` — occupant ambiguity (`:772`, `:969`), launch failure (`:911`),
|
||||
a checkpoint that cannot be delivered (`:1086`, `:1092`), a missing
|
||||
required startup file (`:1161`), or a startup error (`:1271`, `:1282`,
|
||||
`:1292`, `:1300`).
|
||||
- `operator_recovered` — only from later reconciliation,
|
||||
`reconcileNodeRuntimeTruth` (`:1590`, event at `:1727`).
|
||||
- `left_attached` — a seat joined with `rig attach` as an external CLI,
|
||||
returned first in `restoreNodeWithCompensation`: nothing is launched and
|
||||
its binding and claim stay as they were. It doesn't count against the
|
||||
rollup, and its forecast in `restore-plan-preview.ts` is `leave-attached`.
|
||||
- **RestoreRigResult** (`types.ts:444`) — the rig-level rollup, **4** values
|
||||
(`grep '^export type RestoreRigResult' packages/daemon/src/domain/types.ts | grep -o '"[^"]*"' | wc -l`):
|
||||
`fully_restored`, `partially_restored`, `failed`, `not_attempted`.
|
||||
`rollupRestoreRigResult` (`restore-orchestrator.ts:81`) produces only the
|
||||
first three; `not_attempted` is set when pre-restore validation fails
|
||||
(`:281`).
|
||||
- **SnapshotData** (`types.ts:351`) — the serialized snapshot payload. It has
|
||||
**8** optional fields, kept optional so older snapshots still load
|
||||
(`sed -n '/^export interface SnapshotData/,/^}/p' packages/daemon/src/domain/types.ts | grep -c '?:'`):
|
||||
`activeSessionIdByNode?`, `activeOccupantsByNode?`, `periodicResumeSourcesByNode?`,
|
||||
`topologyRoster?`, `pods?`, `continuityStates?`, `nodeStartupContext?`, `envReceipt?`.
|
||||
Periodic snapshots retain a down agent's whole authoritative resume-source
|
||||
session in `periodicResumeSourcesByNode`, without marking it running. A named
|
||||
single-agent launch using automatic periodic selection can resume that exact
|
||||
carried source; an explicit snapshot or whole-rig restore still asks about an
|
||||
absent agent with resumable history. Tokenless and non-resume-policy agents
|
||||
keep their existing fresh path. Only the node's newest session with a token
|
||||
and `resume_if_possible` policy is carried; a current fresh-successor relation
|
||||
must agree. Named launch rechecks the live newest session, resume policy and
|
||||
fresh-successor relation under its lifecycle lease, before changing any seat
|
||||
state; a source retired after capture stops and asks without launching.
|
||||
Idle-shell carry advice uses automatic selection again, not an
|
||||
explicit snapshot that disables carry. The selected source is shared by preflight and post-launch
|
||||
restore; explicit reviewed-history selection keeps its existing checks.
|
||||
Ranking, periodic cadence and `--fresh` are unchanged. Retention is best-effort
|
||||
if the earlier-source lookup fails: capture still records current crash
|
||||
insurance and logs that the resume sources were unavailable.
|
||||
- **NodeStartupSnapshot** (`types.ts:344`) — persisted restore replay input:
|
||||
classification-free projection entries, resolved startup files, startup
|
||||
actions, runtime.
|
||||
- **PersistedProjectionEntry** (`types.ts:332`) — the classification-free
|
||||
restore replay seam: persists only entry identity + source metadata, NOT
|
||||
stale `classification`, `conflicts`, or `noOps` (Architecture Rule 10).
|
||||
Restore rebuilds each entry as `safe_projection` with empty `conflicts` and
|
||||
`noOps` (`restore-orchestrator.ts:1177`, `:1182`, `:1183`).
|
||||
|
||||
## 2. Snapshot, restore, continuity domain services
|
||||
|
||||
All under `packages/daemon/src/domain/`:
|
||||
|
||||
- `checkpoint-store.ts` — checkpoint persistence with pod/continuity context.
|
||||
- `snapshot-capture.ts` — captures pods, continuity state, startup replay
|
||||
context, the latest env receipt, the active-occupant maps and the intended
|
||||
topology roster. `captureSnapshot` (`:60`) derives the occupant maps
|
||||
(`:73–79`), resolves the roster (`:81–90`), then reads `pods` (`:96`),
|
||||
`continuity_state` (`:100`) and `node_startup_context` (`:106`) rows per rig
|
||||
and the env receipt (`:116–123`).
|
||||
- `snapshot-repository.ts` — snapshot CRUD and restore-source selection:
|
||||
`findLatestRestoreUsable` (`:72`) and `selectRestoreUsable` (`:94`), which
|
||||
accept only snapshots that pass `isRestoreUsableSnapshotData` (`:250`).
|
||||
- `restore-orchestrator.ts` — resume, checkpoint delivery, startup replay,
|
||||
live continuity consultation, topology ordering (`computeRestorePlan`,
|
||||
`:676`, ordering only `delegates_to` and `spawned_by` edges, `:71`, among
|
||||
the roster's intended nodes, `:677`), and service boot gating before agent
|
||||
restore (`serviceOrchestrator.boot`, `:331`).
|
||||
- `restore-preconditions.ts` — `validatePreRestore` (`:7`), the one
|
||||
pre-restore validation shared by restore and restore-check (§3, §5.1).
|
||||
- `restore-topology.ts` — `resolveSnapshotRestoreTopology` (`:12`), which
|
||||
splits a snapshot's nodes into intended and excluded (§3).
|
||||
- `restore-attempt-receipt.ts` — `deriveRestoreAttemptReceipt` (`:42`), the
|
||||
read-only per-attempt receipt (§4).
|
||||
- `rehydrate-eligibility.ts` — `snapshotMatchesCurrentOccupants` (`:20`) and
|
||||
`assessCurrentStateRehydrateEligibility` (`:69`), shared by `routes/up.ts`
|
||||
and `routes/restore-check.ts`.
|
||||
- `active-occupant.ts` — the shared rule for which snapshot session row is a
|
||||
node's occupant (`resolveActiveSnapshotSession`, `:73`).
|
||||
|
||||
## 3. Restore flow and restore honesty
|
||||
|
||||
Restore behavior, each point checked in `restore-orchestrator.ts`:
|
||||
|
||||
- Selects the snapshot with `selectRestoreUsable` even when a snapshot id is
|
||||
given (`:251–252`), so wrong-rig and unusable snapshots are refused before
|
||||
any mutation. The selection (`RestoreSnapshotSelection`, `types.ts:396`)
|
||||
records whether it was explicit or automatic, why, and any newer usable
|
||||
alternative; it is carried on the result and on `restore.started`.
|
||||
- Restores only the nodes the snapshot's topology roster intends
|
||||
(`SnapshotData.topologyRoster`, `types.ts:370`). The roster comes from the
|
||||
latest `topology.roster_recorded` event (`snapshot-capture.ts:171`), from
|
||||
`rig snapshot <rigId> --intended-seats` (`packages/cli/src/commands/snapshot.ts:20`),
|
||||
or, for older snapshots, all current nodes. `resolveSnapshotRestoreTopology`
|
||||
(`restore-topology.ts:12`) reports every other node as excluded with reason
|
||||
`historical_not_in_intended_roster` (`:27`); the result carries
|
||||
`intendedRoster` and `excludedNodes` (`types.ts:440–441`).
|
||||
- Resolves each node's occupant from the snapshot's recorded active-occupant
|
||||
maps, not by picking the newest session row
|
||||
(`resolveActiveSnapshotSession`, `active-occupant.ts:74`, called at
|
||||
`restore-orchestrator.ts:774` and `:971`). An ambiguous occupant fails the
|
||||
node loudly (`activeOccupantAmbiguityError`, `active-occupant.ts:97`).
|
||||
- Consults live `continuity_state`; preserves state when a node is already
|
||||
`restoring` (`:777–791` — `SELECT status FROM continuity_state …` at
|
||||
`:780`; if `status === "restoring"` the node is skipped with a warning and
|
||||
reported as `fresh`, `:783–785`; `degraded` only adds a warning).
|
||||
- Reuses the seat's durably bound tmux session name when the prior binding
|
||||
has a usable one, and otherwise falls back to the derived name. A missing
|
||||
binding after the latest session's recorded intentional exit is quiet;
|
||||
unclean/legacy missing bindings and incomplete or invalid binding rows still warn.
|
||||
- Replays restore-safe startup using persisted startup context;
|
||||
prefilters missing optional artifacts into warnings; **hard-fails a node if
|
||||
a required startup file is missing** (`:1161` — status `"failed"`, error
|
||||
"Missing required startup files: …"). Before any mutation,
|
||||
`validatePreRestore` (`restore-preconditions.ts:7`, called from
|
||||
`RestoreOrchestrator.restore`) finds a node that will consume replay and is
|
||||
missing a required startup file (`required_startup_file_missing`, or
|
||||
`startup_owner_root_missing`). Restore holds only that node: it is not
|
||||
launched and is reported `failed` ("Not launched: …" with the blocker's
|
||||
message and remediation), and the rest of the rig restores. When every node
|
||||
the restore would launch is held, or for any other blocker, the whole restore
|
||||
stops before any mutation (outcome `pre_restore_validation_failed`). Its other
|
||||
blockers are `invalid_snapshot_data` (`restore-preconditions.ts:27`),
|
||||
`invalid_topology_roster` (`:78`), `checkpoint_missing_node_cwd` (`:91`),
|
||||
`service_rig_root_missing` (`:187`) and `service_compose_file_missing`
|
||||
(`:197`). The `/api/up` route returns
|
||||
that outcome as HTTP 409 with `status: "not_attempted"`
|
||||
(`routes/up.ts:162`). A node that resumes its exact native session replays
|
||||
no startup files, actions, guidance or skills (`replayContained`,
|
||||
`restore-orchestrator.ts:1114`); its saved Claude activity-hook selection
|
||||
is reapplied before native resume. A skipped hook reapply adds a warning
|
||||
without changing the restore outcome.
|
||||
- Writes a **transcript boundary marker before re-launch**
|
||||
(`writeBoundaryMarker`, `:900`, called before `launchNode` at `:911`) when
|
||||
the transcript store is enabled.
|
||||
- Refuses to restore over live sessions (`:259–262` — `rig_not_stopped`: "Rig …
|
||||
has live sessions. Stop the rig with 'rig down' before restoring, or use the
|
||||
latest auto-pre-down snapshot."). A tmux probe that fails also blocks it
|
||||
(`classifyRunningSessions`, `:612`).
|
||||
- Checks that the harness actually resumed. The resume adapters judge the
|
||||
pane with `assessNativeResumeProbe` (`domain/native-resume-probe.ts:84`). A
|
||||
pod-aware resume counts as resumed only when startup reports `resumed`
|
||||
continuity or the launched session row carries the snapshot's resume type
|
||||
and token (`launchedSessionMatchesSnapshotResume`,
|
||||
`restore-orchestrator.ts:1390`); a reported `fresh` continuity without that
|
||||
proof rolls back to `awaiting-decision` (`:1228–1245`).
|
||||
- Saves a `rig up --non-interruptive` or `--no-non-interruptive` choice on
|
||||
the rig only after the live-session check and pre-restore validation pass
|
||||
(`:301`); see [adapters-and-runtimes.md](adapters-and-runtimes.md) for the
|
||||
launch flags.
|
||||
|
||||
`launchNodeSubset` (`:392`) and `launchSingleNode` restore chosen seats rather
|
||||
than the whole rig; unlike a full restore, a failed tmux probe there proceeds
|
||||
with a `liveness_probe_unknown` warning (`:517`).
|
||||
|
||||
**Restore-honesty rules** (texts in `architecture-rules-and-event-system.md`;
|
||||
cited here by number with the code that carries them):
|
||||
|
||||
- Rule 7 — `RESTORE_POLICY_LEVEL`
|
||||
(`packages/daemon/src/domain/profile-resolver.ts:106`) orders the three
|
||||
policies, and `resolveRestorePolicy` (`profile-resolver.ts:589`) rejects a
|
||||
profile or member value that broadens it.
|
||||
- Rule 14 — the outcome set is `RestoreNodeResult.status` (`types.ts:475`,
|
||||
§1); `rebuilt` is set only when a checkpoint is written
|
||||
(`restore-orchestrator.ts:1097`).
|
||||
- Rule 15 — in `restore-orchestrator.ts`, a `resume_if_possible` seat with a
|
||||
session but no token stops as `awaiting-decision` before launch unless
|
||||
`--fresh` names it (`:812–831`); a failed resume kills the blank session
|
||||
(`rollbackToZeroSession`, `:940`) and returns `awaiting-decision`
|
||||
(`:1055`); pod-aware resume passes
|
||||
`allowFreshFallback: !(isPodAware && resumeRequested)` (`:1224`). Fresh
|
||||
launch is explicit: `rig up --existing <rig> --fresh <seats...>`
|
||||
(`packages/cli/src/commands/up.ts:99`). On a terminal, `rig up` asks
|
||||
yes or no for each `awaiting-decision` seat and re-posts with the accepted
|
||||
seats as fresh (`up.ts:589`).
|
||||
- Rule 16 — `rig down` prints the snapshot id and a restore command
|
||||
(`packages/cli/src/commands/down.ts:293–303`); `rig up` on an existing rig
|
||||
prints per-node statuses and the attach command
|
||||
(`packages/cli/src/commands/up.ts:554–587`).
|
||||
|
||||
(The full rule list lives in `architecture-rules-and-event-system.md`.)
|
||||
|
||||
## 4. Auto-snapshot and existing-rig power-on
|
||||
|
||||
- `rig down <rig>` (name or id, `packages/cli/src/commands/down.ts:144`)
|
||||
first prints the rig's recorded agent sessions and their activity, best
|
||||
effort and only without `--json` (`showAgentsBeforeDown`, `down.ts:28`), then
|
||||
auto-captures an `auto-pre-down` snapshot before teardown when the rig has
|
||||
live sessions (`packages/daemon/src/domain/rig-teardown.ts:117`, capture
|
||||
at `:128`, after refreshing resume metadata at `:119`; an already-stopped
|
||||
rig returns at `:93–115` without one).
|
||||
- `rig up <rig-name>`: a source with no `/` and no
|
||||
`.yaml`/`.yml`/`.rigbundle`/`.rigtopology` extension is a rig name
|
||||
(`packages/daemon/src/domain/up-command-router.ts:49–50`). The CLI refuses
|
||||
a name that matches only archived rigs, with `rig unarchive` guidance
|
||||
(`packages/cli/src/commands/up.ts:256`). It then checks the spec library
|
||||
unless `--existing` is given, and refuses a name that matches both
|
||||
(`up.ts:306–325`); when `first-project` resolves to the built-in `starter`
|
||||
spec, the archived and existing-rig checks also apply to `starter`
|
||||
(`up.ts:314–317`). The daemon finds the rig by name, preferring unarchived
|
||||
rigs (`packages/daemon/src/routes/up.ts:223–226`), and restores from
|
||||
`selectRestoreUsable` (`routes/up.ts:106`), which prefers the newest
|
||||
`auto-pre-down` or `auto-periodic` snapshot, then the newest other usable
|
||||
one (`snapshot-repository.ts:75`).
|
||||
- `rig up <rig-name> --plan` is a read-only restore preview that never
|
||||
captures a snapshot (`routes/up.ts:127`).
|
||||
- If no usable snapshot exists, or the chosen one names an older occupant
|
||||
(`snapshotMatchesCurrentOccupants`, `routes/up.ts:110`), `rig up` captures
|
||||
an `auto-rehydrate` snapshot of current DB state when that state is
|
||||
eligible (`routes/up.ts:137–146`). Otherwise it errors with code
|
||||
`no_snapshot` and guidance ("… current DB state is insufficient for
|
||||
rehydrate. Start fresh with: rig up <spec-path>", `routes/up.ts:118–123`).
|
||||
- Post-command handoff: `down` output includes the snapshot ID and
|
||||
`To restore: rig up <name>` (or `rig restore <snapshotId> --rig <rigId>`
|
||||
when the name is not unique); `up` output includes node statuses and an
|
||||
`Attach:` command (anchors under Rule 16 above).
|
||||
- `rig restore status <attemptId> --rig <rigId>`
|
||||
(`packages/cli/src/commands/restore.ts:120`) reads
|
||||
`GET /api/rigs/:rigId/restore/status/:attemptId`
|
||||
(`packages/daemon/src/routes/snapshots.ts:76`). `deriveRestoreAttemptReceipt`
|
||||
(`restore-attempt-receipt.ts:42`) folds that attempt's events into a
|
||||
per-node view without writing anything.
|
||||
|
||||
### Single-seat recovery and handover
|
||||
|
||||
- **`rig seat continue <seat>`** — when a fresh launch stops at a native
|
||||
prompt before its startup context is delivered, the startup orchestrator
|
||||
fails it as `attention_required` and adds "run: rig seat continue
|
||||
<session>" (`startup-orchestrator.ts:822`). The command
|
||||
(`packages/cli/src/commands/seat.ts:816`) calls
|
||||
`POST /api/seat/continue/:seatRef` (`routes/seat.ts:367`), which runs
|
||||
`continueStartup` (`seat-lifecycle-service.ts:723`): it re-checks the
|
||||
binding, pane and runtime, then delivers the pending context to the same
|
||||
conversation without relaunching. In a restore, a pod-aware seat in this
|
||||
state reports `attention_required` (`restore-orchestrator.ts:1265`). See
|
||||
[agent-spec-and-startup.md](agent-spec-and-startup.md) for the startup side.
|
||||
- **Per-seat startup decision** — `POST /api/startup/:rigId/:logicalId`
|
||||
(`routes/startup.ts:142`) takes `resume`, `start`, `fresh` or `continue`
|
||||
for one seat. `resume` captures an eligibility-checked `auto-rehydrate`
|
||||
snapshot when none exists and goes through `launchSingleNode`.
|
||||
- **Seat handover** — `rig seat handover <seat> --source
|
||||
fresh|rebuild|fork:<seat>|discovered:<id> --reason <reason> [--dry-run]`
|
||||
(the reason is required, dry run included; without it the command exits 2
|
||||
before contacting the daemon)
|
||||
(`packages/cli/src/commands/seat.ts:364`) calls
|
||||
`POST /api/seat/handover/:seatRef` (`routes/seat.ts:94`), which runs
|
||||
`SeatHandoverService.handover` (`seat-handover-service.ts:266`).
|
||||
`SuccessorSessionLauncher` (`successor-session-launcher.ts:92`) respawns
|
||||
the same pane in place, so the session name stays the same. The fresh
|
||||
source delivers a daemon-built restore packet (`buildRestorePacket`,
|
||||
`seat-handover-service.ts:1151`), which is separate from the CLI
|
||||
`rig restore-packet` below. The rebuild source primes from the seat's
|
||||
durable chain (`buildRebuildPrimingChain`, `rebuild-priming-chain.ts:22`).
|
||||
|
||||
## 5. Daemon-side restore-check and CLI restore-packet
|
||||
|
||||
### 5.1 `rig restore-check` — readiness probe
|
||||
|
||||
`GET /api/restore-check?rig=<name>&noQueue=true&noHooks=true&compact=1&ready=1`
|
||||
(`routes/restore-check.ts:220–221`). The route calls
|
||||
`createRestoreCheckService` (`:151`), which assembles a framework-free
|
||||
`RestoreCheckDeps` (`:155–216`) over existing daemon projections —
|
||||
`listRigs` from `rigRepo` (`:175–178`), `getNodeInventory` (joining
|
||||
`node_id` by `logical_id`, `:26–31`, `:179–185`), `getStartupContext` (reads
|
||||
`node_startup_context`, parses `projection_entries_json` /
|
||||
`resolved_files_json` / `startup_actions_json`, `:49–143`), `hasSnapshot`
|
||||
/ `getLatestSnapshot` from `snapshotRepo` (`:189–195`), `getRestoreInputs`
|
||||
(`:196–207`), `probeQueueStore` (`:158–170`), `getClaudeActivityHookEvents`
|
||||
(`:171–174`), and `probeDaemonHealth` (self-evident: "We're inside the
|
||||
daemon — if this route is responding, daemon is healthy", `:208–211`). It
|
||||
then runs `service.check(...)` (`:231–232`).
|
||||
|
||||
`RestoreCheckService.check()` (`restore-check-service.ts:259`) layers:
|
||||
|
||||
1. **Host checks** — `checkDaemonReachable` (probe-throw → `verdict:
|
||||
unknown`, NOT `not_restorable`, `:270–280`), `checkStateDirWritable`
|
||||
(`:282`, impl `:498`), `checkHostInfraDeclaration` (`:283`, impl `:515`).
|
||||
2. **Rig enumeration** — `listRigs()` throw → `buildUnknown` (`:286–295`);
|
||||
`--rig` filter; unknown rig → red `rig.<name>.exists` (`:297–305`).
|
||||
3. **Per-rig checks** — `checkSnapshot` (`:311`, impl `:784`),
|
||||
`checkSpecPresent` (`:316`, impl `:1161`), and
|
||||
`rig.<name>.restore-preconditions` (`checkRestorePreconditions`, `:338`,
|
||||
impl `:422`). The last runs the same `validatePreRestore` that restore
|
||||
runs (`:446`) on the inputs from `getRestoreInputs`: red on a blocker,
|
||||
yellow on a warning, green otherwise. Without a usable snapshot it is
|
||||
yellow when current-state rehydrate is eligible and red when it isn't;
|
||||
when the inputs can't be read it is yellow and the rig's rollup becomes
|
||||
`unknown` (see Verdict). The rig-status route sets
|
||||
`recoveryOnly` (`routes/rigs.ts:272`), which skips this check when every
|
||||
seat is ready (`restore-check-service.ts:337`).
|
||||
4. **Per-seat checks** — `checkSeatReadiness` (`:346`), `checkStartupContext`
|
||||
(`:360`, impl `:886`; `unknownChecks` → `buildUnknown`, `:361–366`),
|
||||
`checkTranscript` (`:382`), `checkResumePath` (`:386`), and unless opted
|
||||
out: the daemon SQLite `queue_items` availability probe `checkQueueStore`
|
||||
(`:390–394`, gated by `--no-queue`; impl `:1007`) and `checkHooks`
|
||||
(`:395–399`, gated by `--no-hooks`; impl `:1030`). Queue continuity is
|
||||
represented by the shared daemon store; an empty queue is valid
|
||||
(`routes/restore-check.ts:160–163`). Claude hook readiness follows the
|
||||
persisted `claude_activity_hooks` runtime-resource selection
|
||||
(`restore-check-service.ts:1053–1060`) and the adapter's projection: the
|
||||
hook settings in the seat CWD and the activity relay under the OpenRig
|
||||
home's `state/claude-activity-hooks/` (`restore-check-service.ts:1071–1072`). A seat
|
||||
that deliberately omits that resource is not applicable. In compact mode
|
||||
without `ready=1`, a seat whose readiness check is green gets only the
|
||||
startup-context check (`restore-check-service.ts:358`, `:368–380`); that
|
||||
check still counts toward the verdict.
|
||||
5. **Verdict** — `buildResult` (`:1205`) aggregates: any red →
|
||||
`not_restorable`; else any rig rollup with status `unknown` → `unknown`
|
||||
(`:1223–1224`); else any yellow → `restorable_with_caveats`; else
|
||||
`restorable` (`:1220–1229`). A rig rollup is `unknown` when its restore
|
||||
inputs could not be read (`:1549–1551`). A probe that can't be inspected
|
||||
at all also gives `unknown` (`buildUnknown`, `:1238`). Plus a
|
||||
`RecoveryPlan` (`buildRecovery`, `:1342`) and a `RepairStep[]` packet
|
||||
(`buildRepairPacket`, `:1581`; `null` when fully restorable, `:1582`).
|
||||
|
||||
The result shape is `RestoreCheckResult` (`restore-check-service.ts:148–160`):
|
||||
`verdict`, `readiness`, `continuity`, `rigs[]`, `hostInfra`, `recovery`,
|
||||
`counts {red,yellow,green}`, `classCounts`, `checks[]`, `repairPacket`. With
|
||||
`compact=1` the route returns a reduced body (`routes/restore-check.ts:234–261`).
|
||||
|
||||
**Honest-error design:** a daemon-probe *exception* produces
|
||||
`verdict: unknown` (uninspectable state), distinct from a daemon
|
||||
definitely-down state which is `red` / `not_restorable`
|
||||
(`restore-check-service.ts:270–280`, `checkDaemonReachable` `:476–496`). The
|
||||
route's catch-all returns the same `unknown`-shaped body with HTTP 500 + a
|
||||
`probe.error` red check (`routes/restore-check.ts:264–312`).
|
||||
`CheckEntry.remediationSafe` defaults to `false` (conservative —
|
||||
unclassified remediations are NOT auto-execution-safe,
|
||||
`restore-check-service.ts:23–28`, applied at `:1596`).
|
||||
|
||||
CLI surface (`../cli-reference.md` `### restore-check`; options in
|
||||
`packages/cli/src/commands/restore-check.ts:222–227`):
|
||||
`rig restore-check [--rig <name>] [--full] [--ready] [--no-queue] [--no-hooks] [--json]`.
|
||||
Output is compact unless `--full` (`:313`). Exit codes: `0` restorable (or
|
||||
with caveats), `1` not restorable (red), `2` unknown / probe error
|
||||
(`:214–217`). When the daemon is down, the CLI builds a local
|
||||
`not_restorable` result and exits 1 (`:269`).
|
||||
|
||||
### 5.2 `rig restore-packet` — cross-runtime restore packet
|
||||
|
||||
CLI-side, no restore-packet daemon route. `commands/restore-packet.ts`
|
||||
(**541** lines, `wc -l < packages/cli/src/commands/restore-packet.ts`)
|
||||
implements **3** subcommands
|
||||
(`grep -c 'cmd.command(' packages/cli/src/commands/restore-packet.ts`;
|
||||
`../cli-reference.md` `### restore-packet`): `write [options]` (`:218`;
|
||||
generate a packet directory from a source session or JSONL file, with
|
||||
`omitted-records` accounting; `--source-session` reads the transcript from the
|
||||
daemon's `/api/transcripts/<session>/full`, `:156`), `read <packet-dir> [--json]`
|
||||
(`:373`; render contents; non-mutating), `validate <packet-dir> [--json]`
|
||||
(`:452`; validate against the v0 schema; non-mutating). Packet shape is the
|
||||
cross-runtime v0 standard — Claude Code and Codex transcripts both supported
|
||||
via runtime parsers + redaction (`packages/cli/src/restore-packet/`).
|
||||
|
||||
### Shared history inspection
|
||||
|
||||
`GET /api/startup/:rigId/nodes/:nodeId/history` lists captured histories for one durable node in the current, policy-ranked restore snapshot. `POST` to the same path plus `/validate` with exactly `{ "snapshotId": "…", "historySessionId": "…" }` rechecks one listed choice. Both routes use startup's terminal bearer authentication. The CLI exposes the same server decisions with `rig seat history --rig <rig-id> --node-id <node-id> --json`; pass both `--snapshot-id` and `--history-session-id` to validate. These calls perform no snapshot capture, token refresh, launch, or database write.
|
||||
|
||||
The list includes captured Claude and Codex rows that the current restore policy and native resume adapter can structurally attempt. Codex requires an explicit `codex_id` and token; implicit `codex_last` cannot name a particular history. Claude accepts `claude_id` and `claude_name` with tokens. The server checks exact rig and node membership, the snapshot's intended roster and runtime, and the node's current occupant compatibility. A change to a sibling node does not retarget or invalidate this node's list. A changed or pruned snapshot, removed history row, same-name replacement node, ambiguous current occupant, or incompatible source is refused; validation never substitutes another history.
|
||||
|
||||
`tokenState` (`present`, `stale`, or `unverified` for listed rows) describes the saved verification record and its age. Eligibility means a future launch may **attempt** native resume. A stored row can share a native token with another row, and neither listing nor validation proves that a native conversation still exists.
|
||||
|
||||
### Selected history startup
|
||||
|
||||
`GET /api/startup/:rigId/nodes/:nodeId?snapshotId=<listed-id>&historySessionId=<listed-id>` returns a read-only review for one listed choice: the selected row's forecast, current terminal observation, consumed launch setting sources (including a member permission envelope or Claude rules), and an opaque revision. The permission view uses restore's pure policy resolution: a readable custom policy's current launch posture can supersede its persisted cache, and a changed resolved decision requires re-review. It refuses a captured row whose native type/token, provenance, restore policy, or harness no longer agrees with the live registry row. The response contains no native token. `POST /api/startup/:rigId/nodes/:nodeId` accepts exactly `{ "action": "resume", "snapshotId": "…", "historySessionId": "…", "revision": "…" }`. Both routes require startup's terminal bearer token. The selected route accepts no fresh, continue, arbitrary token, or alternate target. Review does not capture a snapshot or prepare a provider. `rig seat history` also uses a strictly read-only daemon status check, so inspection does not remove stale daemon state.
|
||||
|
||||
Apply revalidates the same current compatible source, durable node and saved launch settings after prerequisite and presence checks. The existing narrow restore engine carries that exact historical row through legacy and pod-aware native resume, with a second identity check at native dispatch after preparation. Before the new registry row is committed, selected startup also checks that no other attachment or running occupant claimed the node during terminal creation or pane observation; a collision leaves that claimant intact and reports the terminal outcome for inspection. An occupied or uncertain terminal blocks the attempt. No alternate history or fresh conversation is selected. The response projects exact typed references and outcome fields; only diagnostic text is redacted, so even a short native name cannot corrupt an ID. It names the selected source row and the newly registered result row when one exists; only the engine's joined native continuity proof produces success. If a change occurs after shell creation, the result reports a partial or uncertain attempt for inspection. The existing engine handles native prerequisites, compensation and proof. This surface adds no separate per-agent prelaunch probe; native checks remain at execution. Physical tmux cleanup still addresses sessions by name, so it cannot prove ownership against an external same-name replacement outside the selected registry guard.
|
||||
|
||||
## See also
|
||||
|
||||
- `agent-spec-and-startup.md` — StartupOrchestrator persists the replay
|
||||
context that restore consumes.
|
||||
- `daemon-core.md` — `/api/restore-check` is one of the **69** route mounts
|
||||
in `server.ts` (`grep -c 'app.route(' packages/daemon/src/server.ts`),
|
||||
mounted at `server.ts:836`.
|
||||
- `transport-and-transcripts.md` — transcript boundary markers written on
|
||||
restore.
|
||||
- Source roots: `packages/daemon/src/domain/{restore-orchestrator,
|
||||
restore-preconditions,restore-topology,restore-attempt-receipt,
|
||||
rehydrate-eligibility,snapshot-capture,snapshot-repository,checkpoint-store,
|
||||
active-occupant,restore-check-service}.ts`,
|
||||
`packages/daemon/src/routes/{restore-check,up,snapshots}.ts`,
|
||||
`packages/cli/src/commands/{restore-packet,restore-check,restore}.ts`.
|
||||
@@ -1,205 +0,0 @@
|
||||
---
|
||||
kind: as-built
|
||||
title: Living Notes — Composed Review and Frozen Exports
|
||||
status: active
|
||||
topics: [observability, knowledge-and-context]
|
||||
domains: [engineering-advisor, operating-advisor]
|
||||
applies-when: |
|
||||
Tracing slice, mission, rig, or fleet review data back to scope documents,
|
||||
proof artifacts, judgments, queue state, or frozen exports.
|
||||
last-verified-against-source: e8f0ab340db773392ec8be75b072d1c0f3068a50
|
||||
last-updated: 2026-10-08
|
||||
---
|
||||
|
||||
# Living Notes — Composed Review and Frozen Exports
|
||||
|
||||
Source snapshot: `e8f0ab340db773392ec8be75b072d1c0f3068a50`. This describes the source at that commit;
|
||||
it does not establish the version or behavior of a running daemon.
|
||||
|
||||
Living Notes builds review payloads from existing documents and recorded
|
||||
state. The composition layer does not make an agent judgment or contact an
|
||||
agent to obtain one. Its output distinguishes intent, plan, delivered proof,
|
||||
attention, agents, and source lineage.
|
||||
|
||||
## Read and compose boundaries
|
||||
|
||||
[`ReviewGatherer`](../../../packages/daemon/src/domain/review/gather.ts)
|
||||
performs filesystem, database, telemetry, and optional Git reads.
|
||||
[`compose.ts`](../../../packages/daemon/src/domain/review/compose.ts) turns
|
||||
those inputs into the contracts defined in
|
||||
[`types.ts`](../../../packages/daemon/src/domain/review/types.ts), including
|
||||
`ComposedSliceReview`, `ComposedMissionReview`, and `ComposedRigAgents`.
|
||||
|
||||
`gatherSlice` uses `resolveNodeFileVia` to select the scope node document,
|
||||
reads `IMPLEMENTATION-PRD.md` and `PROOF.md`, and loads the `proof/` artifacts.
|
||||
It also gathers approval stamps, locked artifacts, tagged queue attention,
|
||||
agents, workflow exceptions, and `readSliceReadiness`. A current `SPEC.md`
|
||||
is carried distinctly from a legacy `README.md`; the composer does not
|
||||
blindly treat every file as an interchangeable source.
|
||||
|
||||
[`readProofArtifacts`](../../../packages/daemon/src/domain/review/proof-io.ts)
|
||||
reads sorted top-level `.md` files in `proof/`, parses their headers with
|
||||
`parseC1Header`, and uses file mtime as `droppedAt`. Missing directories and
|
||||
unreadable entries yield no artifact for those entries. No claim that all
|
||||
possible evidence was discovered follows from that read.
|
||||
|
||||
`composeMission` batches slice-index membership reads, gathers each slice,
|
||||
and composes a mission board, completion ledger, attention union, agents,
|
||||
and brief spine. `composeRig` reads queue/activity state and reports current
|
||||
holders, recent holders, overdue work, settled work, and workflow exceptions.
|
||||
"Recent" is the current UTC day; overdue is in-progress slice-tagged work past
|
||||
`closure_required_at`; settled is today's `handed_off_to` transitions; and
|
||||
attention is any active item that is `human-gate`, addressed to a `human…`
|
||||
session, or blocked on one (`gather.ts`). Of the three review reads, the TUI
|
||||
calls only `/fleet`, from its broad hydration (`packages/tui/src/hydrate.ts`);
|
||||
its client also defines methods for `/agents` and `/rig`, which nothing in the
|
||||
TUI calls.
|
||||
|
||||
## Slice source selection and phase
|
||||
|
||||
In [`composeSliceReview`](../../../packages/daemon/src/domain/review/compose.ts),
|
||||
intent comes from the node document's `Intent` section. Placeholder-only
|
||||
intent is treated as absent. Mini-requirements use current `SPEC.md` when
|
||||
present; legacy selection can fall back from a pristine scaffold PRD section
|
||||
to authored README material. Proof-contract selection delegates to
|
||||
`selectProofContractBody` through `extractProofContractSelected`.
|
||||
|
||||
The phase is derived by `derivePhase`, in this precedence order:
|
||||
|
||||
| Phase | Signal |
|
||||
|---|---|
|
||||
| `locked` | Delivery approval stamp. |
|
||||
| `review` | Recorded verdict or claimed proof pass. |
|
||||
| `building` | Real proof artifacts, or active queue work paired with an authored/locked spec. |
|
||||
| `spec` | Authored structure or a spec approval stamp. |
|
||||
| `intent` | None of the above. |
|
||||
|
||||
File presence alone is insufficient: a scaffolded `PROOF.md` is not build
|
||||
evidence, and a tracking queue item alone does not promote an unplanned
|
||||
slice. Phase and proof readiness are separate output fields.
|
||||
|
||||
## Delivered items and current judgments
|
||||
|
||||
`extractProofContract` reads logical checkbox items, retaining authored raw
|
||||
text for identity. `proofItemIdentity` uses an explicit `proof-item` marker
|
||||
when supplied, otherwise an ID derived from the item's text. In the legacy
|
||||
artifact join, `evidences` references match a promised item's exact display
|
||||
text or its one-based index.
|
||||
|
||||
`composeDelivered` pairs each promise with curated media and a verification
|
||||
state:
|
||||
|
||||
- With configured [proof readiness](../../../packages/daemon/src/domain/proof/judgments.ts),
|
||||
the matching current item must be `accepted`, and readiness must have no
|
||||
issues, to render `verified`. The note reports the current judgment state
|
||||
or why it is unavailable.
|
||||
- Without configured readiness, a winning covering `qa` or `adjudication`
|
||||
artifact with a non-null `self_check` and a passing verdict supplies legacy
|
||||
verification. The note explicitly labels it **item revision unbound**.
|
||||
- Other covering artifacts are `unverified`; no covering evidence/judgment
|
||||
is `missing`. Unmapped artifact media appears separately as `extraProof`.
|
||||
|
||||
This distinction matters after a proof contract changes: a legacy PASS
|
||||
header is not automatically a current revision-bound judgment. Approval
|
||||
stamps are also not a substitute for the delivered-item join.
|
||||
|
||||
## Approval, attention, and lineage
|
||||
|
||||
`ReviewGatherer.gatherApproval` reads `approved-spec-by`/`approved-spec-at`
|
||||
and `approved-by`/`approved-at`, alongside matching `mission_control_actions`
|
||||
approval audit records. The resulting locks preserve who/when/audit facts.
|
||||
A frontmatter stamp with no matching `approve` audit row still locks the
|
||||
phase, but carries `auditVerified: false`, and the frozen export shows it as
|
||||
an UNVERIFIED stamp. The approval writer is the separate
|
||||
[`scope-approve` route](../../../packages/daemon/src/routes/scope-approve.ts).
|
||||
|
||||
`composeNeedsYou` combines recorded queue attention with derived exceptions;
|
||||
`deriveWorkflowExceptions` adds workflow-specific observations. When
|
||||
readiness is not configured, a slice whose `PROOF.md` claims PASS without a
|
||||
recorded green verdict also gets a `confirm-faithful` needs-you item. Mission
|
||||
composition deduplicates attention identities across slice and mission views.
|
||||
Media references outside the slice's relative-path contract produce defect
|
||||
entries instead of disappearing silently.
|
||||
|
||||
Git facts are optional. [`startup.ts`](../../../packages/daemon/src/startup.ts)
|
||||
passes `OPENRIG_REVIEW_GIT_REPO` to the gatherer. The gatherer reads that
|
||||
repository's `HEAD`, searches merge messages keyed by scope ID, and computes
|
||||
ancestry/behind facts when possible. Each git read has a 4-second timeout; the
|
||||
merge search looks for `Merge <id>` in `HEAD` history and needs the scope's
|
||||
frontmatter `id`; the behind-tip count is computed only for unmerged slices
|
||||
with a candidate SHA. Unavailable facts remain unknown; this is not a fresh
|
||||
remote GitHub merge-status query.
|
||||
|
||||
`composeMissionReview` derives `cutComplete` from a nonempty ledger in which
|
||||
every slice is green, has a merge SHA, and has no needs-human items. Configured
|
||||
slice readiness supplies the green predicate; otherwise recorded verdicts
|
||||
do. With configured readiness, a slice's board cell also shows the proof
|
||||
readiness state and revision. That computed field is not itself a
|
||||
publication or higher-level outcome decision.
|
||||
|
||||
## HTTP routes
|
||||
|
||||
[`reviewRoutes`](../../../packages/daemon/src/routes/review.ts), mounted at
|
||||
`/api/review` by [`server.ts`](../../../packages/daemon/src/server.ts):
|
||||
|
||||
| Method and suffix | Payload / behavior |
|
||||
|---|---|
|
||||
| `GET /slice/:name` | Composed slice plus proof-source observation. |
|
||||
| `GET /mission/:name` | Composed mission plus proof-source observation. |
|
||||
| `GET /agents?scope=…` | Agents band; scope is `slice:<id>`, `mission:<id>`, or `rig`. |
|
||||
| `GET /rig` | Local rig review root. |
|
||||
| `GET /fleet` | Local and registered remote host review union. |
|
||||
| `POST /freeze` | Compose and export an approved slice; body requires `scope: slice` and `name`. |
|
||||
|
||||
Missing gatherer returns `503`; invalid agents scope returns `400`; unknown
|
||||
slice/mission returns `404`. Fleet is a sibling aggregate, not another value
|
||||
of the agents-scope parameter.
|
||||
|
||||
## Freeze and media
|
||||
|
||||
Nothing invokes the freeze automatically: `POST /api/scope/approve` writes
|
||||
the stamp and audit row and always returns `freezeFired: false`
|
||||
(`domain/scope/scope-approve.ts:326`), and the route comment that says the
|
||||
approve flow invokes the freeze describes wiring that does not exist. A
|
||||
frozen export exists only after something calls `POST /api/review/freeze`.
|
||||
|
||||
[`freezeSliceExport`](../../../packages/daemon/src/domain/review/freeze.ts)
|
||||
requires a delivery lock and an allowlisted slice directory. It writes
|
||||
`REVIEW-<scope-id-or-name>-<approval-date>.html` through
|
||||
`FileWriteService.createAtomic`. An existing target is treated as already
|
||||
frozen; it is not rewritten. A failed export does not undo approval. The
|
||||
route needs the file write service (a non-empty `files.allowlist`; otherwise
|
||||
503) and returns 409 `stamp_missing` without a delivery stamp, 403
|
||||
`allowlist_missing` outside every allowlist root, and 500 otherwise; success
|
||||
returns `{ ok, path, alreadyFrozen, briefWrite }` (`routes/review.ts`).
|
||||
|
||||
The export embeds contained images as data URIs and links videos with an
|
||||
optional poster. `containedMediaPath` checks both resolved paths and existing
|
||||
symlink targets against the slice directory. Unavailable/outside media gets
|
||||
an explicit rendered message.
|
||||
|
||||
After a new freeze, the route may update the generated spine in a conforming
|
||||
`MISSION_BRIEF.md` through the conflict-checked file writer. A malformed brief
|
||||
is skipped, and brief-write failures are reported without undoing the export.
|
||||
The [`slices` routes](../../../packages/daemon/src/routes/slices.ts) separately
|
||||
serve proof assets; [`files` asset serving](content-surfaces.md) has its own
|
||||
allowlist and range behavior.
|
||||
|
||||
## Fleet composition
|
||||
|
||||
[`composeFleet`](../../../packages/daemon/src/domain/review/fleet-compose.ts)
|
||||
composes local data in-process and reads registered hosts' composed rig
|
||||
payloads. Its default fan-out concurrency is **4**, with a shared
|
||||
**4-second** read budget. Synchronous local work cannot be preempted by that
|
||||
timer; elapsed local work reduces the remaining remote budget.
|
||||
|
||||
Each remote host is read through its own `GET /api/review/rig`: SSH-only
|
||||
registry entries report `unsupported-transport`, auth refusals report
|
||||
`auth-failed`, and timeouts, malformed payloads and other errors report
|
||||
`unreachable`.
|
||||
|
||||
`unionFleet` keys attention by host plus item identity and retains host
|
||||
status. Unavailable hosts have absent counts, not synthetic zeros. A missing
|
||||
registry produces a local-only result; an unreadable existing registry is
|
||||
reported. Host-derived time/status facts are carried through, not recomputed
|
||||
as if every host shared one clock.
|
||||
@@ -1,272 +0,0 @@
|
||||
---
|
||||
kind: as-built
|
||||
title: Mission Control — Queue Observability + Verb Contract
|
||||
status: active
|
||||
topics: [coordination, observability]
|
||||
domains: [engineering-advisor, operating-advisor, orchestrator, human-operator]
|
||||
applies-when: |
|
||||
Need to know how the daemon-backed Mission Control surface works — the
|
||||
seven views, the nine write verbs, the action audit table, the
|
||||
bearer-token middleware, or how queue observability maps to the
|
||||
coordination primitive's sources.
|
||||
siblings: [coordination-primitive.md, workflow-runtime.md, ../ui/project-and-for-you.md]
|
||||
prerequisite-reads: [../README.md, coordination-primitive.md]
|
||||
last-verified-against-source: e8f0ab340db773392ec8be75b072d1c0f3068a50
|
||||
last-updated: 2026-10-08
|
||||
---
|
||||
|
||||
# Mission Control — Queue Observability + Verb Contract
|
||||
|
||||
Mission Control is the daemon's queue-observability and operator-action
|
||||
surface, mounted at `/api/mission-control` over the coordination primitive's
|
||||
`queue_items` and `stream_items` (see `coordination-primitive.md`). It has
|
||||
seven views, nine verbs, a recent-ships cap of 10
|
||||
(`mission-control-read-layer.ts:107`), and a daemon-backed action audit
|
||||
table. In the web UI the old `/mission-control` page now redirects to
|
||||
`/for-you`, and the For You feed carries the verb actions (§6).
|
||||
|
||||
> Verified against source at main `e8f0ab340db773392ec8be75b072d1c0f3068a50`. A bare file name such as
|
||||
> `mission-control-read-layer.ts:31` is in
|
||||
> `packages/daemon/src/domain/mission-control/`; `domain/…`, `routes/…`,
|
||||
> `middleware/…`, `db/…`, `index.ts`, `server.ts` and `startup.ts` are under
|
||||
> `packages/daemon/src/`. Each count sits beside the command that produces
|
||||
> it; run the command from the repository root to refresh it.
|
||||
|
||||
## 1. The seven views
|
||||
|
||||
`MISSION_CONTROL_VIEWS` is the canonical view list
|
||||
(`mission-control-read-layer.ts:31–39`): `my-queue`, `human-gate`, `fleet`,
|
||||
`active-work`, `recent-ships`, `recently-active`, `recent-observations`.
|
||||
|
||||
Views: **7** — `sed -n '/^export const MISSION_CONTROL_VIEWS = \[/,/\] as const;/p' packages/daemon/src/domain/mission-control/mission-control-read-layer.ts | grep -c '^ "'`
|
||||
|
||||
All seven return rows in the 9-field phone-friendly content model,
|
||||
`CompactStatusRow` (`mission-control-read-layer.ts:60`): rig/mission name,
|
||||
current phase, active|idle|attention|blocked|degraded, next-action,
|
||||
pending-human-decision, read-cost, last-update timestamp,
|
||||
confidence/freshness, evidence link. The type's doc comment
|
||||
(`mission-control-read-layer.ts:43–59`) makes the model non-negotiable across
|
||||
all seven views: UI may render compact, JSON preserves all nine. Four further
|
||||
optional fields carry the queue item's id and context.
|
||||
|
||||
Required row fields: **9** — `sed -n '/^export interface CompactStatusRow {/,/^}/p' packages/daemon/src/domain/mission-control/mission-control-read-layer.ts | grep -c -E '^ [a-zA-Z]+:'`
|
||||
|
||||
`MissionControlReadLayer.readView` (`mission-control-read-layer.ts:143`) maps
|
||||
each view to its source:
|
||||
|
||||
- `my-queue` / `human-gate` / `active-work` / `recent-ships` query
|
||||
`queue_items` via `QueueRepository`. `my-queue` uses the `operatorSession`
|
||||
query parameter when given, else `workspace.operator_seat_name` (default
|
||||
empty), else the only registered human; when none is found, or more than
|
||||
one, it returns no rows and a `degradedFields` note rather than widening the
|
||||
view (`mission-control-read-layer.ts:137–141`, `:166–168`;
|
||||
`routes/mission-control.ts:521`).
|
||||
- `fleet` calls `MissionControlFleetCliCapability.rollupFleet()`
|
||||
(`mission-control-read-layer.ts:197`;
|
||||
`mission-control-fleet-cli-capability.ts:109`), which walks the rig
|
||||
registry (`:110`), summarises each rig's `queue_items` (`:135`), and runs a
|
||||
per-rig CLI capability probe (`makeLocalCliCapabilityProbe`, `:77`, wired
|
||||
at `startup.ts:1597`).
|
||||
- `recently-active` delegates to the view projector's built-in
|
||||
`ViewProjector.show("recently-active")` (`mission-control-read-layer.ts:246`).
|
||||
- `recent-observations` reads the latest 50 `stream_items` via `StreamStore`
|
||||
with `direction: "latest"` (`mission-control-read-layer.ts:265–268`).
|
||||
|
||||
There is no filesystem fallback. When no `StreamStore` is wired,
|
||||
`recent-observations` returns no rows with
|
||||
`sourceFallback: "stream-store-not-wired"`
|
||||
(`mission-control-read-layer.ts:258–262`), and `fleet` always reports
|
||||
`sourceFallback: "daemon-internal-projection"`
|
||||
(`mission-control-fleet-cli-capability.ts:156`).
|
||||
|
||||
> Scope note: this module describes the daemon's verb vocabulary. It does
|
||||
> NOT enumerate which verbs the For You cards offer; that surface is
|
||||
> described in `../ui/project-and-for-you.md`. The terminal UI opened by bare
|
||||
> `rig` or `rig tui` is called the "mission-control TUI" in source
|
||||
> (`packages/tui/README.md:1`), but it does not call these routes.
|
||||
|
||||
## 2. The nine verbs (the write contract)
|
||||
|
||||
`MISSION_CONTROL_VERBS` (`mission-control-action-log.ts:14–29`) is the verb
|
||||
list. The verbs execute through `MissionControlWriteContract.act`
|
||||
(`mission-control-write-contract.ts:131`); the closure mapping is
|
||||
`verbToClosure` (`:509`).
|
||||
|
||||
Verbs: **9** — `sed -n '/^export const MISSION_CONTROL_VERBS = \[/,/\] as const;/p' packages/daemon/src/domain/mission-control/mission-control-action-log.ts | grep -c '^ "'`
|
||||
|
||||
| Verb | Effect |
|
||||
|---|---|
|
||||
| `approve` | `state="done"`, `closure_reason="no-follow-on"` |
|
||||
| `deny` | `state="done"`, `closure_reason="denied"` |
|
||||
| `route` | `state="handed-off"`, `closure_reason="handed_off_to"`, `closure_target`+`handed_off_to`=route target; creates a new queue item at the route target (1-hop) |
|
||||
| `annotate` | no queue mutation; audit record only |
|
||||
| `hold` | `state="blocked"`, `closure_reason="blocked_on"` |
|
||||
| `drop` | `state="done"`, `closure_reason="canceled"` |
|
||||
| `handoff` | same closure and new destination item as `route`; the 4-step shape (see below) |
|
||||
| `resolve` | for a `blocked` item parked on a human seat or a well-formed typed gate (`external:`, `fold:`, `auth:`): moves it to `in-progress` with required decision/satisfaction evidence in the transition note; typed waits are recorded as blocker resolutions, not human decisions. Qitem dependencies, malformed gates and non-blocked items are refused; no closure and no new item (`:369`, `:386`, `:410–411`). After commit it sends a best-effort nudge with the decision text to the item's owner unless `notify: false` (`:450–467`); the CLI wrapper is `rig queue resolve <qitemId> --decision <text>` (`packages/cli/src/commands/queue.ts:828`) |
|
||||
| `answer` | only for a `pending` ask addressed to the acting person: a choice (`questionId` + `optionId`) through `QueueRepository.recordHumanAnswer`, or their words (`decision`) through `resolveDirectHumanReply`, each with its existing meaning (a partial choice set stays pending; words fill the first unanswered question). Once answered, one reply row goes to the asking seat (`human-answer` tag, `replyTo` the ask, an id derived from the ask) and then the ask closes once with `human-decision-resolved`, the order Slack's answer continuation uses. With neither a choice nor words, a complete question set whose return didn't finish is sent again with exactly its recorded answers. A closed ask answers 409 `qitem_already_terminal` and records nothing; a refused answer is 400 `answer_not_recorded`; 409 `answer_not_closed` means the ask stayed open: its return failed, it changed before its close, or a retry came in other words than the reply already returned (that retry isn't sent). The TUI's Feed sends it |
|
||||
|
||||
`annotate` requires `annotation`, `hold` and `drop` require `reason`, `route`
|
||||
and `handoff` require `destinationSession`, and `resolve` requires a non-empty
|
||||
`decision`. A missing `annotation`, `destinationSession` or `decision` returns
|
||||
400. A `drop` without `reason` passes the queue closure check and is refused by
|
||||
the audit record instead; the wrapped `reason_required` error has no status
|
||||
mapping, so it returns 500 (`mission-control-action-log.ts:128–133`,
|
||||
`mission-control-write-contract.ts:245–248`). Items already `done` or
|
||||
`handed-off` are refused with 409 `qitem_already_terminal`, except under
|
||||
`resolve`, which answers 409 `qitem_not_leg1_parked` for any item that is not
|
||||
blocked on a human seat or a well-formed typed gate (`external:`, `fold:`, `auth:`). An unknown item returns 404 `qitem_not_found`
|
||||
(`routes/mission-control.ts:240–251`, `:330–341`).
|
||||
|
||||
Each verb except `answer` is one atomic daemon transaction (`answer`'s queue writes are the repository's own, in the order above, then its audit row and event): the queue mutation via
|
||||
`QueueRepository.updateWithinTransaction()` (`domain/queue-repository.ts:2371`,
|
||||
which keeps the hot-potato closure validation — see
|
||||
`coordination-primitive.md` §3), an audit row in `mission_control_actions`
|
||||
(`mission-control-write-contract.ts:207`), and a persisted
|
||||
`mission_control.action_executed` event (`:225`), all in one
|
||||
`db.transaction`. For every verb except `annotate`, that transaction is
|
||||
opened by `EventBus.withNotifyEnvelope` (`:157`; `domain/event-bus.ts:112`,
|
||||
`:126`); `annotate` opens `db.transaction` itself
|
||||
(`mission-control-write-contract.ts:308`).
|
||||
|
||||
The 4-step `handoff` shape (source-update + destination-create + best-effort
|
||||
notify + audit-record append) is shared by `route`. The destination item is
|
||||
created in the same transaction (`createWithinTransaction`, `:173`), along
|
||||
with the successor's wake intent (`stageWakeIntent`, `:195`, checked by
|
||||
`assertTerminalClosureHasIntent`, `:242`). The wake itself runs after commit
|
||||
(`deliverWakeForSuccessor`, `:264`). It is on unless the caller passes
|
||||
`notify: false` (`domain/queue-repository.ts:1182`), and a notify failure
|
||||
does NOT roll back durable mutations (`mission-control-write-contract.ts:272–275`).
|
||||
A failed destination create rolls back the source closure, the audit row and
|
||||
the new item together
|
||||
(`packages/daemon/test/mission-control-write-contract.test.ts:220`).
|
||||
|
||||
## 3. The action audit table
|
||||
|
||||
`mission_control_actions` (`db/migrations/037_mission_control_actions.ts:58`
|
||||
`CREATE TABLE IF NOT EXISTS mission_control_actions`) is append-only at the
|
||||
API surface: `record()` is the only writer `MissionControlActionLog` exposes
|
||||
(`mission-control-action-log.ts:113`). It records every operator action
|
||||
through Mission Control with before/after queue item snapshots for forensic
|
||||
reconstruction. Columns include `action_verb` (TEXT, app-layer enum
|
||||
enforcement, `037_mission_control_actions.ts:60`) and `acted_at` (TEXT NOT
|
||||
NULL ISO timestamp, `037_mission_control_actions.ts:63`);
|
||||
indexes `(acted_at DESC, action_verb)`, `(qitem_id, acted_at DESC)`,
|
||||
`(actor_session, acted_at DESC)` (`037_mission_control_actions.ts:72–77`).
|
||||
One later migration adds a nullable `identity_provenance` column
|
||||
(`db/migrations/065_identity_provenance.ts:18`). The acting session comes
|
||||
from the `X-OpenRig-Session` header when present (the CLI sends it); a
|
||||
headerless browser request falls back to the body `actorSession`. The row
|
||||
records how the actor was established: `transport:v1`, `relay:v1` for a
|
||||
forwarded request, `claimed:v1` for a headerless body actor, or
|
||||
`origin-unknown:v1` (`routes/mission-control.ts:347–351`;
|
||||
`routes/require-sender-identity.ts:120`).
|
||||
|
||||
Scope approvals (`POST /api/scope/approve`) also append `approve` rows through
|
||||
the same `MissionControlActionLog`, with `qitem_id` null and the scope target
|
||||
in `audit_notes_json` (`domain/scope/scope-approve.ts:292`). The audit browse
|
||||
(§4) reads only this table.
|
||||
|
||||
## 4. Bearer middleware, notifications, audit browse
|
||||
|
||||
- **Bearer-token middleware** — `middleware/auth-bearer-token.ts`.
|
||||
Constant-time comparison via Node `crypto.timingSafeEqual`
|
||||
(`constantTimeEqual`, `:36`, `:50`). `authBearerTokenMiddleware` (`:93`)
|
||||
passes every request through when no token is configured (`:98`). The
|
||||
token comes from `OPENRIG_AUTH_BEARER_TOKEN` (`index.ts:288`). The daemon
|
||||
refuses to start when the bind host is set explicitly, is neither loopback
|
||||
nor Tailscale (a hostname is resolved first), and no bearer token is set
|
||||
(`assertBindAuthInvariant`, `auth-bearer-token.ts:240`, called at
|
||||
`index.ts:296`). Bearer enforced on the write routes:
|
||||
`app.post("/action", requireAuth)` (`routes/mission-control.ts:307`) and
|
||||
`app.post("/notifications/test", requireAuth)` (`:308`). Reads are not
|
||||
bearer-gated. It is one static token; no OAuth/SSO/per-user model
|
||||
(`auth-bearer-token.ts:11–14`). Every Mission Control request, read or
|
||||
write, first passes the daemon's `/api/*` browser boundary
|
||||
(`server.ts:647`; `middleware/browser-boundary.ts`): only known target host
|
||||
names, and, when an `Origin` header is present, only the daemon's own UI
|
||||
origin or an `OPENRIG_ALLOWED_ORIGINS` entry. A valid bearer token waives
|
||||
only the host-name check.
|
||||
- **Notification dispatcher** — two adapters
|
||||
(`notification-adapter-ntfy.ts`, `notification-adapter-webhook.ts`) plus
|
||||
`notification-dispatcher.ts`. `OPENRIG_NOTIFICATIONS_MECHANISM` selects
|
||||
`ntfy`, `webhook` or `none`, and the default is `none`
|
||||
(`startup.ts:1642`). A dispatcher starts only when
|
||||
`OPENRIG_NOTIFICATIONS_TARGET` is also set (`:1643`, `:1650`) and the
|
||||
target URL passes validation (`:1661`); an unrecognized mechanism with a
|
||||
target set throws during daemon startup (`:1656–1659`). The ntfy adapter
|
||||
posts to a topic URL that the ntfy phone app subscribes to
|
||||
(`notification-adapter-ntfy.ts:1–6`). Notifications fire when a
|
||||
`human-gate` queue item is created, and also on each completed verb when
|
||||
`OPENRIG_NOTIFICATIONS_INCLUDE_VERB_COMPLETION=1` (`startup.ts:1648–1649`);
|
||||
each item, trigger and mechanism is sent at most once per daemon run
|
||||
(`notification-dispatcher.ts:119–134`, `:156–158`). `POST
|
||||
/notifications/test` returns 503 `notifications_unconfigured` when no
|
||||
dispatcher is running (`routes/mission-control.ts:481–492`).
|
||||
|
||||
Adapters: **2** — `git grep -l 'implements NotificationAdapter' -- packages/daemon/src | wc -l`
|
||||
- **Read-only audit-history browse** — `MissionControlAuditBrowse.query`
|
||||
(`audit-browse.ts:78`) over `mission_control_actions`, exposed at
|
||||
`GET /api/mission-control/audit` (`routes/mission-control.ts:441`) with
|
||||
filters (including `scope_tier`, `scope_id`, `scope_path` and
|
||||
`approval_scope` for scope-approval rows) and `(limit, before_id)`
|
||||
pagination cursored on SQLite `rowid` (`audit-browse.ts:134`); the page size
|
||||
defaults to 50 and is capped at 200 (`audit-browse.ts:61–62`).
|
||||
|
||||
## 5. Mission Control events
|
||||
|
||||
The `RigEvent` union starts at `domain/types.ts:108`; its four
|
||||
`mission_control.*` members are at `domain/types.ts:308–313`.
|
||||
|
||||
`RigEvent` members: **98** — `sed -n '/^export type RigEvent =/,/^export type PersistedEvent/p' packages/daemon/src/domain/types.ts | grep -c 'type: "'`
|
||||
|
||||
`mission_control.*` members: **4** — `sed -n '/^export type RigEvent =/,/^export type PersistedEvent/p' packages/daemon/src/domain/types.ts | grep -c 'type: "mission_control\.'`
|
||||
|
||||
- `mission_control.action_executed` — written by the write contract
|
||||
(`mission-control-write-contract.ts:225`).
|
||||
- `mission_control.cli_drift_detected` — emitted by the fleet capability
|
||||
probe (`mission-control-fleet-cli-capability.ts:171`).
|
||||
- `mission_control.notification_sent` / `mission_control.notification_failed`
|
||||
— emitted by the notification dispatcher (`notification-dispatcher.ts:174`,
|
||||
`:184`).
|
||||
- *(retired)* `mission_control.view_refreshed` — retired per #490; views are
|
||||
read-on-demand queries without background refresh caching.
|
||||
|
||||
## 6. Route surface
|
||||
|
||||
`missionControlRoutes({ bearerToken })` is mounted at `/api/mission-control`
|
||||
(`server.ts:790–793`). Routes (`routes/mission-control.ts`): `GET /views`
|
||||
(`:259`), `GET /cli-capabilities` (`:264`), `GET /destinations` (`:276`),
|
||||
`GET /sse` and its alias `GET /watch` (`:303–304`; they forward
|
||||
`action_executed` and `cli_drift_detected` events,
|
||||
`:289–291`), `POST /action` (`:314`, auth-gated), `GET /audit` (`:441`),
|
||||
`POST /notifications/test` (`:481`, auth-gated), and `GET /views/:view-name`
|
||||
(`:509`).
|
||||
|
||||
`POST /action` accepts an optional `hostId`: for a non-local host it forwards
|
||||
the same verb to that host's daemon (http-transport host entries only; 10 s
|
||||
timeout). The origin host writes the audit row and nothing is written
|
||||
locally; a forwarding failure returns 502 `remote_action_failed` with a
|
||||
`failureClass` (`routes/mission-control.ts:13`, `:352–414`).
|
||||
|
||||
In the web UI (`packages/ui/src/routes.tsx`), `/mission-control` is a
|
||||
redirect to `/for-you` (`:495–500`). `/for-you` renders the For You `Feed`
|
||||
(`:130–134`), whose cards call `POST /action` through `VerbActions`
|
||||
(`packages/ui/src/components/for-you/FeedCard.tsx:13`), and `/search` renders
|
||||
`AuditHistoryView` over `GET /audit` (`routes.tsx:304–308`).
|
||||
`MissionControlSurface`
|
||||
(`packages/ui/src/components/mission-control/MissionControlSurface.tsx:9`)
|
||||
still exists and renders the same `Feed`, but nothing imports it. UI detail
|
||||
lands in `../ui/project-and-for-you.md`.
|
||||
|
||||
## See also
|
||||
|
||||
- `coordination-primitive.md` — the `queue_items`/`stream_items` sources
|
||||
Mission Control reads, and the hot-potato closure contract verbs honor.
|
||||
- `workflow-runtime.md` — the workflow transactional-scribe runtime.
|
||||
- `../ui/project-and-for-you.md` — the For You feed that carries the verb
|
||||
actions in the web UI.
|
||||
- Source roots: `packages/daemon/src/domain/mission-control/`,
|
||||
`packages/daemon/src/middleware/auth-bearer-token.ts`,
|
||||
`packages/daemon/src/routes/mission-control.ts`,
|
||||
`packages/daemon/src/db/migrations/037_mission_control_actions.ts`.
|
||||
@@ -1,314 +0,0 @@
|
||||
---
|
||||
kind: as-built
|
||||
title: Packaging, Bootstrap, Bundles, Legacy Install Engine
|
||||
status: active
|
||||
topics: [specification-and-bundles, release-and-versioning]
|
||||
domains: [engineering-advisor, operating-advisor]
|
||||
applies-when: |
|
||||
Locate npm package assembly, topology-bundle creation and validation,
|
||||
source routing, bootstrap plan/apply, or the retained package install engine.
|
||||
siblings: [agent-spec-and-startup.md, plugin-agent-image-context-pack.md]
|
||||
prerequisite-reads: [../README.md, agent-spec-and-startup.md]
|
||||
last-verified-against-source: e8f0ab340db773392ec8be75b072d1c0f3068a50
|
||||
last-updated: 2026-10-08
|
||||
---
|
||||
|
||||
# Packaging, bootstrap and bundles
|
||||
|
||||
This module describes source at main commit
|
||||
`e8f0ab340db773392ec8be75b072d1c0f3068a50`. Source paths below are repository-relative.
|
||||
An npm CLI artifact and a `.rigbundle` have different builders and consumers; neither
|
||||
source verification nor archive integrity establishes that a daemon has adopted an artifact.
|
||||
|
||||
## npm CLI assembly
|
||||
|
||||
`scripts/build-package.sh` builds the daemon, web UI, TUI and CLI, then assembles their
|
||||
outputs under `packages/cli`. The publishable file list and binary declarations live in
|
||||
`packages/cli/package.json`.
|
||||
|
||||
The assembler writes generated `BUILD_INFO` modules into the daemon and CLI outputs with
|
||||
the package version, Git commit, dirty flag and build time. The dirty check covers tracked
|
||||
and untracked inputs under `packages/` and `scripts/`; it is not a whole-worktree status
|
||||
claim.
|
||||
|
||||
The same script:
|
||||
|
||||
- Generates shipped context packs through `scripts/generate-context-packs.mjs`.
|
||||
- Copies daemon output, assets, specs, context packs, policies and `docs/reference/`.
|
||||
- Checks the specs staging input with `scripts/check-internal-leak-guard.mjs` and writes
|
||||
the staged substance-root inventory into the package.
|
||||
- Copies the web UI and TUI outputs.
|
||||
- Runs `scripts/rewrite-daemon-imports.mjs` so staged CLI/TUI JavaScript resolves the
|
||||
shipped daemon output rather than an unpublished workspace package.
|
||||
|
||||
This is the build layout. Native dependency installation and consumer runtime behavior need
|
||||
their own checks; this document does not infer them from a successful assembly.
|
||||
|
||||
## Topology-bundle creation
|
||||
|
||||
`packages/daemon/src/routes/bundles.ts` mounts create, inspect, install and history handlers
|
||||
under `/api/bundles` (the mount is in `packages/daemon/src/server.ts`).
|
||||
|
||||
The create handler validates the source spec before comparing it with recorded live topology.
|
||||
A divergent spec is refused unless the request explicitly allows drift; an allowed divergence
|
||||
is recorded in the bundle's provenance notes. The exported spec is not silently rewritten
|
||||
from live state.
|
||||
|
||||
| Source shape | Builder and output |
|
||||
|---|---|
|
||||
| Pod-aware rig spec | `packages/daemon/src/domain/pod-bundle-assembler.ts`: `PodBundleAssembler.assemble()` produces schema-version-2 metadata, vendors resolved agent trees, and rewrites agent references to local paths. |
|
||||
| Legacy rig spec | `packages/daemon/src/domain/bundle-assembler.ts`: `LegacyBundleAssembler` (imported as `BundleAssembler` by the route) collects the referenced legacy packages and produces the legacy manifest shape. |
|
||||
|
||||
The pod builder collects culture, docs and startup material, preserves file bytes, and skips
|
||||
vendoring the terminal sentinel as an agent. A copied file keeps its source mode with owner
|
||||
read and write added. Required document collection failures stop assembly; missing declared
|
||||
skills can instead produce warnings, which are also written into the provenance notes.
|
||||
Inspect the builder's collection helpers when changing these distinctions.
|
||||
|
||||
Both route branches can consume a source-root `bundle.yaml` to carry declared skills,
|
||||
plugins, workflow specs, context packs, agent images and preconditions into staging before
|
||||
computing integrity. Manifest types, validation and serialization are in
|
||||
`packages/daemon/src/domain/bundle-types.ts`. Provenance and compatibility are metadata;
|
||||
provenance is not an author signature.
|
||||
|
||||
Preconditions are carried as data. Commands containing shell operators produce a warning,
|
||||
inspect reports the block, and the behaviour view lists it under needs. The install and
|
||||
bootstrap code do not reference preconditions.
|
||||
|
||||
Two more inputs are accepted only for a pod-aware spec; the legacy branch refuses them with
|
||||
a 400:
|
||||
|
||||
- `--context-pack <dir>` (`vendorContextPackDir()` in
|
||||
`packages/daemon/src/domain/bundle-carried-context-pack.ts`) carries a pack's
|
||||
`manifest.yaml` and its declared files to `context-packs/<manifest name>/`. The pack may
|
||||
sit outside the rig folder.
|
||||
- `--project-dir <dir>` (`vendorProjectDir()` in
|
||||
`packages/daemon/src/domain/bundle-carried-project.ts`) carries a folder with a
|
||||
`project.yaml` to `project/`.
|
||||
|
||||
Create also refuses content it must not ship. `computeIntegrity()` in
|
||||
`packages/daemon/src/domain/bundle-integrity.ts` throws on sensitive paths (`.env` files,
|
||||
`*.pem`, `*.key`, `*.p12`, `credentials.*`, `tokens.*`, `.git/` and `node_modules/`).
|
||||
`assertShippableSubstance()` in `packages/daemon/src/domain/agent-resolver.ts` runs on
|
||||
vendored content and on the final staging tree, and refuses text containing
|
||||
`substrate/shared-docs/` and files declaring `taxonomy: lore`.
|
||||
|
||||
### Configurations and package identity
|
||||
|
||||
A bundle folder may declare alternate team configurations in `configurations.yaml` beside
|
||||
`rig.yaml` (`packages/cli/src/lib/bundle-configuration.ts`). `rig bundle configurations
|
||||
<spec>` lists them. `rig bundle create --preset <name>` or `--seat <member=runtime>` resolves
|
||||
one and stages a copy of the rig folder in a temporary directory with the chosen runtimes and
|
||||
profiles, so the author's folder is never changed. With a GitHub folder link, `rig up`,
|
||||
`rig bundle create`, `inspect` and `install` take the same two options and stage a copy of the
|
||||
whole checkout (`importGitHubBundle()` in `packages/cli/src/lib/bundle-source.ts`).
|
||||
|
||||
`packages/daemon/src/domain/bundle-identity.ts` defines the two identity values:
|
||||
|
||||
- `configurationId()`: every member's `pod.member=runtime`, sorted and joined with `,`.
|
||||
Create computes it from the normalized spec and returns a 400 if the client sent a
|
||||
different one.
|
||||
- `packageDigest()`: SHA-256 over the sorted `integrity.files` entries (coverage label
|
||||
`openrig.package-digest/v1`). It covers every packaged file's bytes; it does not cover
|
||||
`bundle.yaml`, the junk files the integrity walk skips, or file modes.
|
||||
|
||||
Create and inspect responses carry the source, configuration ID, package digest, archive
|
||||
hash and assembler version.
|
||||
|
||||
### Archive checks
|
||||
|
||||
`packages/daemon/src/domain/bundle-archive.ts` owns `pack()` and `unpack()`;
|
||||
`packages/daemon/src/domain/bundle-integrity.ts` owns the per-file integrity map.
|
||||
|
||||
Packing requires a `.rigbundle` output path, sorts entries, uses portable tar metadata, a
|
||||
fixed tar timestamp and gzip level 9, and writes a sibling SHA-256 file. Unpacking checks that
|
||||
digest, rejects archive links and unsafe entry paths before extraction, requires
|
||||
`bundle.yaml` and its integrity section, and verifies the extracted file inventory. Hashes
|
||||
establish consistency with the supplied manifest and digest; they do not authenticate
|
||||
whoever supplied both.
|
||||
|
||||
The inspect handler does not call `unpack()`, because it must report a broken bundle rather
|
||||
than refuse it. It reports a missing or mismatched digest as `digestValid: false`, applies the
|
||||
same unsafe-entry pre-scan (`collectUnsafeArchiveEntries()`) and extraction filter, and runs
|
||||
`verifyIntegrity()` to report an `integrityResult`. A syntactically valid manifest alone is
|
||||
not an inspected archive.
|
||||
|
||||
Inspect also returns a behaviour view, `describeBundleBehaviour()` in
|
||||
`packages/daemon/src/domain/bundle-behaviour.ts` (schema `openrig.bundle-behaviour/v1`). It
|
||||
reads the extracted archive only, through `bundle-behaviour-inspect.ts` (regular UTF-8 files,
|
||||
at most 1 MiB each and 8 MiB in total, opened without following links). It lists the team,
|
||||
each seat's permission posture, the files seats are told to read, what else runs, where
|
||||
content is written, literal outside addresses, needs, and what can't be known before launch.
|
||||
A schema-1 archive gets `state: not_generated`. Posture comes from the member's or rig's
|
||||
`permission_policy`: `builtin:yolo` is full bypass and `builtin:auto` is auto, both by launch
|
||||
flag; `builtin:locked`, `builtin:standard` and `builtin:open` are configuration; a policy file
|
||||
is read from the archive or marked unresolved; no policy means the product default. A rig-level
|
||||
`non_interruptive` boolean is reported on each full-bypass Claude Code or Codex seat as
|
||||
`nonInterruptiveDefault`, and `true` drops the Claude bypass first-run warning. The CLI prints
|
||||
the view before `rig bundle install` and `rig up` as diagnostics only; it never changes the
|
||||
request or the exit code.
|
||||
|
||||
## Source routing and bootstrap
|
||||
|
||||
`packages/daemon/src/domain/up-command-router.ts` classifies a source as
|
||||
`rig_spec`, `rig_bundle`, `rig_name` or `topology`. It recognizes named rigs,
|
||||
spec paths, `.rigbundle`, `.rigtopology`, and YAML with a top-level rigs list;
|
||||
extensionless paths also have content detection.
|
||||
|
||||
`packages/daemon/src/routes/up.ts` then selects the execution path:
|
||||
|
||||
| Input | Consumer |
|
||||
|---|---|
|
||||
| Existing rig name | Existing-rig restore path; unarchived rigs of that name are preferred. |
|
||||
| Single rig spec or bundle | `BootstrapOrchestrator.bootstrap()` in `packages/daemon/src/domain/bootstrap-orchestrator.ts`. |
|
||||
| Topology manifest | `MultiRigLauncher`, with a single-rig bootstrap or remote-up leaf per entry. |
|
||||
|
||||
Topology plan mode is rejected. Placement belongs on the individual topology entries; a
|
||||
top-level host flag is rejected. The selected topology parser/route also rejects nested
|
||||
topologies, existing-rig-name entries and `.rigbundle` entries: topology entries are spec
|
||||
paths only. This is narrower than the single-rig `up` surface.
|
||||
|
||||
Bootstrap distinguishes pod-aware and legacy specs, and inspects a bundle manifest to choose
|
||||
`PodBundleSourceResolver` or `LegacyBundleSourceResolver` from
|
||||
`packages/daemon/src/domain/bundle-source-resolver.ts`. Thus the router's
|
||||
`rig_bundle` kind does not imply a legacy manifest.
|
||||
|
||||
For pod-aware specs, plan mode validates and runs preflight probes; apply delegates to
|
||||
`PodRigInstantiator`. Plan mode still records a bootstrap run/result: it is not a pure
|
||||
file read. Apply reports partial completion when nodes fail or require attention, preserving
|
||||
the created rig identity rather than claiming every member launched. Apply's non-interruptive
|
||||
choice is the request's, else the spec's top-level `non_interruptive`, else the
|
||||
`launch.non_interruptive` setting.
|
||||
|
||||
### Durable install target
|
||||
|
||||
Bundle apply requires an explicit target root at the HTTP boundary. The CLI supplies the
|
||||
current directory as the default for `rig up <file>.rigbundle` and for a GitHub link through
|
||||
`rig up` or `rig bundle install`. `rig bundle install` with a local `.rigbundle` sends no
|
||||
default, so apply without `--target` gets a 400. For a pod bundle,
|
||||
`materializePodBundle()` copies the verified extraction into that durable root before
|
||||
instantiation, so local agent references and relative working directories survive removal
|
||||
of the extraction directory. It checks destination conflicts before copying, preserves
|
||||
identical files, and refuses differing files or incompatible destination types.
|
||||
|
||||
When an unarchived or running team already has the bundle's rig name (`bundleInstallContext()`
|
||||
in `packages/daemon/src/domain/bundle-install-context.ts`), pod-bundle apply refuses before
|
||||
writing if any of them is running. If the target is that team's own install folder (its
|
||||
`bundle.yaml` names the offered schema-2 bundle, or `my-bundle` or `github-bundle`, the default
|
||||
names before 0.6.7, and it is the folder recorded in `rigs.install_root` when any of those teams
|
||||
recorded one; `isExistingBundleTarget()`), bootstrap first
|
||||
confirms the old sessions are stopped; materialization then copies each conflicting path to a
|
||||
`bundle-backups/reinstall-*` folder under the OpenRig home, with a `RESTORE.json`, before
|
||||
replacing it, and leaves unrelated files in place. Any other target keeps the conflict refusal.
|
||||
The instantiator archives the stopped earlier generation. Pod-bundle apply records the
|
||||
target's real path as the new rig's `install_root` (migration `096`).
|
||||
|
||||
Pod bundles containing service definitions are refused by bootstrap; their service path
|
||||
requires a stable spec directory. Direct spec bootstrap has separate service prelaunch
|
||||
handling. See [agent-spec-and-startup.md](agent-spec-and-startup.md) for instantiation.
|
||||
|
||||
## GitHub folder links and the bundle check
|
||||
|
||||
`packages/cli/src/lib/bundle-source.ts` lets `rig up`, `rig bundle create`, `inspect` and
|
||||
`install` take a GitHub folder link (`isGitHubBundleLink()`). Only a credential-free
|
||||
`https://github.com/owner/repo[/tree/<ref>/<folder>]` link is accepted. The ref is resolved to
|
||||
the longest matching branch or tag, or taken as a full commit hash. The import:
|
||||
|
||||
- needs a healthy local daemon whose `selfHostId` matches the local origin;
|
||||
- runs Git with credential helpers, hooks and non-HTTPS protocols disabled;
|
||||
- shallow-fetches the exact commit into `bundle-imports/` under the OpenRig home, requires a
|
||||
`rig.yaml` in the folder, and refuses symlinks or agent and package references that resolve
|
||||
outside the checkout;
|
||||
- posts the folder to `/api/bundles/create` with the source recorded in provenance, refuses a
|
||||
response without a package digest or with a different source commit, and writes
|
||||
`source.json` and `build.json` in the import folder.
|
||||
|
||||
`rig bundle check <folder>` (`checkBundleFolder()` in `packages/cli/src/lib/bundle-check.ts`)
|
||||
is an advisory, file-read-only check against `openrig.bundle-standard/v1`; it makes no daemon,
|
||||
launch or provider call. Its rules are `pod_aware_rig`, `readme_in_docs`, `referenced_files`,
|
||||
`portable_agents`, `minimum_versions`, `configurations` and `credential_paths` (a filename scan
|
||||
bounded at 10,000 entries that skips `.git`, `node_modules` and symlinks). `readme_coverage`
|
||||
and `embedded_secrets` are always reported as not checked, and host-resolved resources are
|
||||
reported as checked at launch (`host_resources`). Any finding sets exit code 1.
|
||||
|
||||
## Install checks and content routing
|
||||
|
||||
The install handler in `packages/daemon/src/routes/bundles.ts` obtains a source-path
|
||||
lock, reads validated metadata, checks compatibility, and checks a declared rig name
|
||||
against same-name rigs with running sessions before bootstrap; a stopped team of that name
|
||||
doesn't block it. A conflict returns a 400 with `status: "not_attempted"`, the installed team
|
||||
and the offered bundle, and three choices: use the existing team, stop it and retry to replace
|
||||
it, or cancel. The compatibility override `skipVersionCheck` and name-conflict override `force`
|
||||
are explicit request fields; neither skips the metadata pass, which always validates the
|
||||
manifest.
|
||||
|
||||
`packages/daemon/src/domain/bundle-conflict-detector.ts` implements the rig-name check.
|
||||
It is not a complete agent, port or filesystem collision audit; a missing rig name supplies
|
||||
no name comparison. Target-file conflicts are handled separately by materialization.
|
||||
Skipping these prechecks does not skip archive validation in bootstrap.
|
||||
|
||||
Declared content is routed by `routeBundleContents()` in
|
||||
`packages/daemon/src/domain/bundle-content-routing.ts`, which unpacks the bundle once and
|
||||
routes each kind independently. When it runs depends on the bundle schema:
|
||||
|
||||
- **Pod-aware (schema 2):** bootstrap runs it as `PodRigInstantiator`'s prelaunch hook, after
|
||||
the rig record exists and before any seat launches, so every seat's first turn can see the
|
||||
content.
|
||||
- **Legacy (schema 1):** there is no hook, so the install route routes after bootstrap, and
|
||||
only when the status is `completed`.
|
||||
|
||||
| Manifest content | Destination |
|
||||
|---|---|
|
||||
| `skills` | The OpenRig `packages` cache, stripping the legacy `packages/` prefix; this does not import a complete harness skill into the managed skill catalog. |
|
||||
| `plugins` | The OpenRig `plugins` root, through local plugin references. |
|
||||
| `workflow_specs` | `workflows/` beneath the resolved `workspaceSpecsRoot`. If that setting is empty, this kind is recorded as a routing failure. |
|
||||
| `context_packs` | The configured `context.root`, matching context-library discovery. |
|
||||
| `agent_images` | The OpenRig `agent-images` root; declarations address image directories containing a manifest. |
|
||||
| `project` | Registered in the workspace catalog (`workspace.catalog_path`) with the rig associated, and its files placed under `workspace.projects_root/<id>` (`registerBundleProject()` in `packages/daemon/src/domain/workspace/project-registration.ts`). A conflict changes nothing and is a routing failure. |
|
||||
|
||||
Context packs are never merged into an installed pack of the same name: an identical one is
|
||||
reported as `already_installed`, a different one is kept unchanged as `kept_existing`. After
|
||||
routing, the live context library rescans, and a routed pack it can't load becomes a routing
|
||||
failure.
|
||||
|
||||
The per-kind implementations are the `bundle-skills-router.ts`, `bundle-plugins-router.ts`,
|
||||
`bundle-workflow-specs-router.ts`, `bundle-context-packs-router.ts` and
|
||||
`bundle-agent-images-router.ts` files under `packages/daemon/src/domain/`. Routing is best
|
||||
effort in both cases: the hook always lets the launch continue, and a failure never rolls back
|
||||
or fails the install. Failures appear in the result's `routingFailures` and `warnings`, in a
|
||||
`route_bundle_contents` stage for the hook, and in the install audit record.
|
||||
|
||||
Install audit records are appended best effort to `bundle-audit.jsonl` under the OpenRig
|
||||
home; the history endpoint reads that audit. Bootstrap run state and action journals are
|
||||
separate, repository-backed records.
|
||||
|
||||
## Legacy package install path
|
||||
|
||||
The legacy path remains executable, not just an archive parser.
|
||||
`packages/daemon/src/routes/packages.ts` is mounted at `/api/packages`, and legacy
|
||||
bootstrap calls `PackageInstallService` in
|
||||
`packages/daemon/src/domain/package-install-service.ts`.
|
||||
|
||||
The source chain is package resolution/manifest validation, planning and conflict policy,
|
||||
then apply/verification with repository records:
|
||||
|
||||
- `resolvePackage()` in `packages/daemon/src/domain/package-resolve-helper.ts`, which reads
|
||||
and validates through `package-manifest.ts`. `package-resolver.ts` supplies the types; its
|
||||
`PackageResolver` class has no production caller.
|
||||
- `packages/daemon/src/domain/install-planner.ts`, `conflict-detector.ts` and
|
||||
`install-policy.ts`. The planner refuses any planned destination outside the install
|
||||
target.
|
||||
- `packages/daemon/src/domain/install-engine.ts`, `install-verifier.ts`,
|
||||
`install-repository.ts` and `package-repository.ts`.
|
||||
|
||||
Legacy bootstrap also probes requirements and records staged outcomes before rig
|
||||
instantiation. A failed package installation prevents that path from importing the rig.
|
||||
This compatibility implementation does not imply that pod-aware bundles use the same
|
||||
package-install stages.
|
||||
|
||||
## Related maps
|
||||
|
||||
- [plugin-agent-image-context-pack.md](plugin-agent-image-context-pack.md): content library
|
||||
consumers and their state boundaries.
|
||||
- [daemon-core.md](daemon-core.md): bootstrap construction and HTTP wiring.
|
||||
- [agent-spec-and-startup.md](agent-spec-and-startup.md): spec resolution, projection and launch.
|
||||
@@ -1,339 +0,0 @@
|
||||
---
|
||||
kind: as-built
|
||||
title: Content Layer — Plugins, Agent Images, Context Packs, Compaction Policy
|
||||
status: active
|
||||
topics: [extension-and-user-workspace, continuity, skill-management]
|
||||
domains: [engineering-advisor, operating-advisor]
|
||||
applies-when: |
|
||||
Locate plugin discovery and vendoring, context-pack addressing/composition,
|
||||
agent-image capture/fork/protection, or Claude guided-compaction behavior.
|
||||
siblings: [packaging-bootstrap-bundles.md, agent-spec-and-startup.md]
|
||||
prerequisite-reads: [../README.md, agent-spec-and-startup.md]
|
||||
last-verified-against-source: e8f0ab340db773392ec8be75b072d1c0f3068a50
|
||||
last-updated: 2026-10-08
|
||||
---
|
||||
|
||||
# Content libraries and compaction
|
||||
|
||||
This module describes source at main commit
|
||||
`e8f0ab340db773392ec8be75b072d1c0f3068a50`. Source paths below are repository-relative.
|
||||
Context packs, plugins and agent images have filesystem-backed content and daemon-side
|
||||
discovery. Their consumers can read database identity, mutate files, deliver messages or
|
||||
launch sessions; the whole layer is not a read-only catalog.
|
||||
|
||||
## Context packs
|
||||
|
||||
`packages/daemon/src/domain/context-packs/context-pack-library-service.ts` implements
|
||||
`ContextPackLibraryService`. A pack contains `manifest.yaml` and declared members.
|
||||
Its path-like ref is the lookup identity, independently of the manifest name/version;
|
||||
the entry ID is `context-pack:<ref>`.
|
||||
|
||||
### Discovery and mutation
|
||||
|
||||
Startup in `packages/daemon/src/startup.ts` configures the shipped context-pack root (when
|
||||
it exists), the resolved `context.root` and its `system` root, followed by the
|
||||
workspace-local `.openrig/context-packs` root. The workspace root is registered even before
|
||||
it exists, so a folder created later is found by the next sync; it is left out only when it
|
||||
is the same path as one of the other two. The library scans recursively, stops descending
|
||||
when it finds a pack manifest, skips `.tmp-add-*` staging folders, and does not follow
|
||||
directory symlinks.
|
||||
|
||||
The first configured root to encounter a physical pack owns that directory. For distinct
|
||||
physical packs with the same ref, the later root wins. This distinction prevents overlapping
|
||||
roots from giving the same physical pack a second address. Scan replaces the in-memory ref
|
||||
index and reports malformed packs instead of indexing them.
|
||||
|
||||
`composeFromFiles()` creates a durable pack after validating its ref, destination and
|
||||
sources. `removeByRef()` removes the selected pack directory and rescans, but refuses
|
||||
shipped builtin packs. Library mutation and lookup share ref validation.
|
||||
|
||||
`packages/daemon/src/domain/context-packs/manifest-parser.ts` parses manifests.
|
||||
Its supported file suffixes include Markdown/YAML/text and `.sh`, `.ts`, `.mjs`,
|
||||
`.py` helper assets. Those assets are served as text; pack assembly does not execute them.
|
||||
The parser also validates atom metadata and named profiles.
|
||||
|
||||
### Assembly, addresses and profiles
|
||||
|
||||
These are separate projections:
|
||||
|
||||
| Operation | Source and behavior |
|
||||
|---|---|
|
||||
| Framed pack preview | `packages/daemon/src/domain/context-packs/bundle-assembler.ts`, `assembleBundle()`: pack/file headers, file metadata, token estimates and `missingFiles`. Missing members are reported and skipped; a failed read of a present member raises an error. |
|
||||
| Plain file composition | The same file's `assemblePlainFiles()`: present files in declared order, separated by two newlines, without framing or trimming their bytes. |
|
||||
| Addressed read | `packages/daemon/src/routes/context-packs.ts`, `/library/resolve-address`: longest matching pack-ref prefix, a declared file, then optional H2/H3 heading selection through `packages/daemon/src/domain/markdown-address.ts`. |
|
||||
| Situation profile | `packages/daemon/src/domain/context-packs/profile-composer.ts`, `composeProfile()`: select atoms, close dependencies, order and resolve each piece. |
|
||||
| Named profile | The same file's `composeNamedProfile()`: declared phases select atoms or supplied work/seat context, preserving phase IDs in the result. |
|
||||
|
||||
Situation composition selects fresh atoms for `fresh`, fresh plus handover atoms for
|
||||
`handover`, and post-compaction plus handover atoms for `post-compaction`. Runtime
|
||||
selection is Claude or Codex, with `any` atoms applicable to either. Required atoms join
|
||||
the selection; missing dependencies or runtime-incompatible dependencies fail composition.
|
||||
The output is ordered by atom order, then ID. Profile-only atoms are excluded from ordinary
|
||||
situation selection, but the dependency closure doesn't check that flag, so one can still join
|
||||
as a dependency of a selected atom. Post-compaction composition skips an absent
|
||||
`seat:RECAP.md` and reports it as skipped instead of failing.
|
||||
|
||||
The profile route resolves configured tree sources through
|
||||
`packages/daemon/src/domain/context-packs/profile-source-resolver.ts`.
|
||||
Seat context requires explicit rig and seat selectors. Mission/slice selectors and named
|
||||
profile context requirements are validated before reading those trees. The default
|
||||
project/mission/slice walk is bounded to the conventional single-project workspace layout
|
||||
and fresh situation; it is not an arbitrary project-catalog resolver.
|
||||
|
||||
Each resolved piece names its address/source and reports estimated tokens. A token budget
|
||||
reports overage and drop candidates without silently truncating the profile. Atom
|
||||
`regions` are metadata; the profile endpoint has no region-subset selector.
|
||||
|
||||
### HTTP and CLI boundaries
|
||||
|
||||
`packages/daemon/src/server.ts` mounts `contextPacksRoutes()` at
|
||||
`/api/context-packs`. Its library handlers cover list, sync, compose, read/delete by ref,
|
||||
preview, pieces, addressed reads and profile composition.
|
||||
|
||||
`packages/cli/src/commands/context.ts` implements `rig context`, including `list`, `show`,
|
||||
`preview`, `get`, `profile`, `compose`, `add`, `rm` and `sync`, plus `work-install`, `trace`,
|
||||
`source inspect|update` and `recap-write`. The library read/compose routes do not themselves
|
||||
send a pack to a running seat. Several verbs write: `add` installs into `context.root` (from
|
||||
a folder, a URL, or Git with `--git`), `source update` fetches and merges a Git-installed pack
|
||||
and resyncs, `work-install --apply-skills` reconciles skill projections, and `recap-write`
|
||||
writes a seat recap. Inspect the selected CLI verb before treating a context command as
|
||||
read-only. `rig context work-install` resolves a project position and its declared context
|
||||
without the daemon; [workspace-primitive.md](workspace-primitive.md) covers project
|
||||
resolution.
|
||||
|
||||
On a profile read, the route adds each piece's `sha256` and provenance, with a warning when a
|
||||
piece's bytes come from outside the granted folder. Rig and seat selectors grant the seat's
|
||||
folder as a source root and supply a default `seat:LEARNED.md` atom to named profiles whose
|
||||
phases request seat context. An ordinary situation profile includes that file only when the
|
||||
pack authors a seat atom; the selectors alone don't add it.
|
||||
|
||||
### System World, seat recaps and skill loadouts
|
||||
|
||||
`packages/daemon/src/domain/system-world.ts` reads the System World manifest (schema
|
||||
`openrig.system-world/v0alpha1`): an id, a version, the context packs every seat starts from,
|
||||
and system skills. The `context.system_world` setting selects the default manifest at
|
||||
`system/system-world.yaml` under `context.root`, disables it, or names another manifest file.
|
||||
The default (`openrig-default`) names the `onboarding-width` and `world-public` packs and no
|
||||
skills; it is seeded when missing.
|
||||
|
||||
`rig context recap-write --rig <rig> --seat <seat> --file <file>` writes the seat's
|
||||
`RECAP.md` through `packages/daemon/src/domain/context-packs/seat-recap-store.ts`. The previous
|
||||
recap is kept byte-for-byte under `recap-superseded/`. A recap that isn't addressable Markdown
|
||||
is refused; the other content checks are advisory.
|
||||
|
||||
`packages/daemon/src/domain/skill-catalog.ts` resolves a seat's skill loadout from the
|
||||
catalog at `skills.root` (`catalog.yaml`, schema `openrig.skill-catalog/v1`) and the system,
|
||||
topology and project selections. For a working directory it projects skills into
|
||||
`<cwd>/.claude/skills` or `<cwd>/.agents/skills` and tracks what it owns in
|
||||
`<cwd>/.openrig/skill-loadouts/<runtime>.json`. For a Claude or Codex team seat it takes a
|
||||
`location` instead: the seat's own storage outside the project, with its `skill-loadout.json`
|
||||
there, and for Claude the `openrig:<id>` names its generated plugin gives. Each projection
|
||||
reports `current`, `missing`, `shadowed`, `stale` or `conflicting`.
|
||||
`rig skill loadout --runtime <runtime>` inspects the composed projection for a working
|
||||
directory, `rig skill loadout --seat <session|self>` does the same for one seat's own skills,
|
||||
and both reconcile only with `--apply`; `rig skill audit` is a separate
|
||||
read-only provenance and freshness audit. Launch applies the selected loadout per seat.
|
||||
|
||||
For a Claude Code or Codex seat, the profile resolver also adds each selected plugin's skills to
|
||||
that loadout (`packages/daemon/src/domain/profile-resolver.ts:309–330`, with
|
||||
`resolvePluginSkills()` at `skill-catalog.ts:460`). Neither runtime reads skills from the plugin
|
||||
folder projected into the working directory. The skills are projected under their plain names,
|
||||
and a skill the profile already selects keeps its source. A kept plugin copy (an edited or
|
||||
user-owned one, or OpenRig's own copy whose plugin source changed or moved after the loadout
|
||||
was resolved, `skill-catalog.ts:1060–1068`) gives a `plugin_skill_kept` warning, and an unreadable one gives
|
||||
`plugin_skill_skipped`. If plugin skills can't be projected at all, the seat starts without them
|
||||
and gets a `plugin_skills_not_projected` warning (`rigspec-instantiator.ts:2045`).
|
||||
|
||||
## Agent images and forks
|
||||
|
||||
`packages/daemon/src/domain/agent-images/agent-image-library-service.ts` implements
|
||||
`AgentImageLibraryService`. Startup registers the OpenRig `agent-images` root and,
|
||||
when present, the workspace-local `.openrig/agent-images` root. Entries are keyed by
|
||||
`agent-image:<name>:<version>`, with later discovery roots replacing identical IDs.
|
||||
Content is a manifest plus optional files; `stats.json` holds usage data and `.pinned`
|
||||
marks protection from ordinary deletion/pruning.
|
||||
|
||||
Capture and consumption depend on live identity as well as library files:
|
||||
|
||||
- `packages/daemon/src/domain/agent-images/resume-token-discovery.ts`:
|
||||
`discoverResumeToken()` reads session/node/binding state. It supports Claude Code and
|
||||
Codex. Claude prefers matching context-usage identity; Codex prefers the persisted resume
|
||||
token, then an external-CLI binding. Absent native identity remains absent.
|
||||
- `packages/daemon/src/domain/agent-images/snapshot-capturer.ts`:
|
||||
`SnapshotCapturer.capture()` refuses unsupported or missing native identity, writes
|
||||
an image manifest/files and rescans. It records the source working directory when known.
|
||||
- `packages/daemon/src/routes/agent-images.ts`:
|
||||
`POST /fork` composes native identity discovery with an add-member operation. The
|
||||
default path makes no image. The keep-image path captures and pins one before launch,
|
||||
and reports that retained image even if launch fails.
|
||||
|
||||
Library/snapshot responses redact `sourceResumeToken`; the fork composer builds its
|
||||
native-ID session source inside the daemon rather than returning that field to the caller.
|
||||
This describes these response projections, not secrecy of every field in an image manifest.
|
||||
|
||||
`recordConsumption()` accepts a separate fork-count flag: consumption intent can update
|
||||
last-use time without incrementing successful forks. Inspect the instantiation caller as
|
||||
well as library statistics when assessing whether a launch succeeded.
|
||||
|
||||
`packages/daemon/src/domain/agent-images/evidence-guard.ts` implements
|
||||
`evaluateProtection()`: pins, parsed spec references and descendants of protected images
|
||||
contribute protection reasons. The route applies the guard to deletion/pruning unless
|
||||
`force` is explicit. Prune defaults to dry-run. Its protection view depends on the supplied
|
||||
spec roots; it is not an inventory of every reference anywhere on the host.
|
||||
|
||||
The guard keys its result by image name, not version: every version of one name gets the
|
||||
status computed for the last-listed version, carrying that version's image ID. The delete
|
||||
route looks up by image ID, so on delete only that last-listed version of a name is matched,
|
||||
and its other versions are deleted without `force` even when pinned. `rig agent-image prune`
|
||||
sends a dry run unless `--force` is given, and `--force` also overrides the guard, so a
|
||||
guarded real prune is reachable only through the HTTP route.
|
||||
|
||||
The routes mount at `/api/agent-images` and expose list/sync, snapshot/fork,
|
||||
entry/preview, pin/unpin, delete and prune.
|
||||
`packages/cli/src/commands/agent-image.ts` owns the image library commands. The top-level
|
||||
`rig fork` verb (`packages/cli/src/commands/fork.ts`) drives `POST /fork`; `--keep-image`
|
||||
also captures a pinned image. Forking a Claude Code seat needs Claude Code 2.0.73 or later,
|
||||
because the fork is launched with its own `--session-id`.
|
||||
|
||||
## Plugins
|
||||
|
||||
### Discovery and inspection
|
||||
|
||||
`packages/daemon/src/domain/plugin-discovery-service.ts` discovers plugins from:
|
||||
|
||||
| Source kind | Directory shape |
|
||||
|---|---|
|
||||
| `vendored` | OpenRig plugins root, one plugin directory per entry. |
|
||||
| `claude-cache` | Claude cache, grouped by marketplace/plugin/version. |
|
||||
| `codex-cache` | Codex cache, grouped by marketplace/plugin/version. |
|
||||
| `rig-cwd` | `.claude/plugins` and `.codex/plugins` beneath supplied working-directory roots. |
|
||||
|
||||
Detection uses the Claude/Codex plugin manifests. `listPlugins()` returns runtime/source
|
||||
metadata; `getPlugin()` adds manifests, skills, hooks and MCP detail.
|
||||
`findUsedBy()` scans agent YAML in configured spec-library roots for plugin declarations
|
||||
and profile references. These references describe authored specs, not proof of a running
|
||||
harness loading a plugin.
|
||||
|
||||
The service constructor is not read-only: it retires obsolete refocus hook registrations
|
||||
from the older bundled plugin and rejects duplicate vendored hook registrations across
|
||||
providers. Cached plugin versions are excluded from that duplicate-provider check. Startup
|
||||
constructs the service while building the daemon's dependencies and doesn't catch that
|
||||
rejection, so a duplicate vendored hook stops daemon creation.
|
||||
|
||||
`packages/daemon/src/routes/plugins.ts` exposes GET-only inspection at `/api/plugins`:
|
||||
list, detail, used-by and file list/read. The file routes use the discovered plugin root as
|
||||
their path allowlist. `?cwd=` supplies rig-cwd discovery roots.
|
||||
At this pin, the route's source filter accepts vendored, Claude-cache and Codex-cache only;
|
||||
`source=rig-cwd` is not applied as a filter, even though the service supports that kind.
|
||||
|
||||
### Vendoring and projection
|
||||
|
||||
`packages/daemon/src/domain/plugin-vendor-service.ts` owns `PluginVendorService`.
|
||||
`ensureVendored()` seeds absent plugins or advances an older numeric manifest version.
|
||||
Equal/newer installed versions preserve installed bytes; equal versions can reconcile file
|
||||
modes on byte-identical files. Missing target version authority is preserved rather than
|
||||
overwritten; invalid or conflicting manifest versions are errors. Advancing a version writes
|
||||
changed files but never deletes files the new version dropped.
|
||||
|
||||
`ensureSkillGlobally()` projects a named plugin skill using a vendor-version marker.
|
||||
An existing unversioned global skill remains externally owned. Startup wires this service
|
||||
and its filesystem implementation; inspect that wiring when changing projection roots. At
|
||||
startup only `openrig-core` is vendored, and its `openrig-skills` and `refocusing` skills are
|
||||
projected globally into `~/.claude/skills` and `~/.agents/skills`
|
||||
(`packages/daemon/src/startup.ts:877–886`). The packaged `rigs` skill
|
||||
(`packages/daemon/assets/skills/rigs`) is not a plugin skill: `ensureSkillDirGlobally()`
|
||||
projects it into the same roots under the daemon version, so each upgrade refreshes it
|
||||
(`startup.ts:891–899`). A failed global projection is logged and does not stop startup.
|
||||
|
||||
`ensureLatest()` performs local vendoring before `attemptAutoFetch()`. The latter
|
||||
requests a release asset with a bounded timeout and logs failures; even its successful
|
||||
response path does not extract/install the fetched archive at this pin. This does not
|
||||
establish whether an upstream release asset currently exists.
|
||||
|
||||
`packages/cli/src/commands/plugin.ts` implements `rig plugin
|
||||
list|show|used-by|validate`. Discovery, vendoring and a runtime's projected plugin tree
|
||||
are distinct steps; see [agent-spec-and-startup.md](agent-spec-and-startup.md) for launch.
|
||||
|
||||
## Claude guided compaction
|
||||
|
||||
`packages/daemon/src/domain/claude-compaction-enforcer.ts` implements
|
||||
`ClaudeCompactionEnforcer`. Startup shares one instance between `ContextMonitor`
|
||||
and the manual-compaction route consumers.
|
||||
`packages/daemon/src/domain/user-settings/settings-store.ts` resolves the
|
||||
`policies.claude_compaction.*` settings; automatic compaction defaults to disabled.
|
||||
|
||||
`maybeAutoCompact()` applies the delivery guard when one exists and the runtime is Claude
|
||||
Code. Its implementation requires Claude Code, observed usage and a valid integer threshold
|
||||
(default 80). Above-threshold automatic work additionally checks enablement, the
|
||||
post-restore cooldown (10 minutes), the deduplication window (60 seconds) and the
|
||||
already-triggered latch; an active manual attempt also blocks it.
|
||||
|
||||
While the policy is enabled with a valid threshold (`managedCompactionActive` in
|
||||
`packages/daemon/src/adapters/kernel-authority.ts`, using the enforcer's exported `isValidThresholdPercent`),
|
||||
`operationalLaunchArgs` merges `idleCompaction: false` into the Claude launch's one `--settings` value, adding one
|
||||
when there is none. Fresh launch, fork, resume (`claude-code-adapter.ts`) and restore (`claude-resume.ts`) all build
|
||||
their Claude arguments there. The policy is read at each launch; a running session keeps the settings it was launched
|
||||
with. Claude Code before 2.1.290 ignores the key, Claude's managed settings take precedence over `--settings`, and
|
||||
`autoCompactEnabled` is not set.
|
||||
|
||||
`/compact` is held behind a preparation attempt:
|
||||
|
||||
1. The first above-threshold tick starts an attempt with its own ID, the seat's occupant
|
||||
generation, a restore-map path and a completion marker, then sends only the prep prompt.
|
||||
An unknown occupant generation stops the attempt at once.
|
||||
2. The agent writes `RESTORE-MAP.md.tmp` with its file-edit tool, completion marker last.
|
||||
Later ticks return `preparation_pending` until that exact map is complete. The map path is under the seat's working directory
|
||||
(`.openrig/compaction/preparation/<session>/<attempt>/`, with a `.gitignore` of `*`) when
|
||||
that is writable, otherwise under `compaction/preparation/` in the OpenRig home. An explicit
|
||||
`policies.claude_compaction.preparation_root` instead places maps at `<root>/<session>/<attempt>/`
|
||||
for both manual and automatic attempts; failure to prepare that root stops the attempt without fallback.
|
||||
3. The daemon atomically renames the completed temp file to `RESTORE-MAP.md` in its existing
|
||||
final send checks, after rechecking the attempt and occupant. An already-complete final map
|
||||
remains compatible. Only then is `/compact` sent, with the final map path appended to the
|
||||
compact instruction. Publication failure cannot authorize `/compact`.
|
||||
4. The continuation then runs below the threshold: turn boundary, restore prompt, then a
|
||||
read-depth audit prompt (each item marked `FULL`, `PARTIAL` or `NOT_READ`), one stage per
|
||||
poll tick. A seat refreshes its usage sample only when it takes a turn, so when the sample is
|
||||
stale, unknown or has no percentage and a stage is pending, the monitor drains that stage
|
||||
without one (`context-monitor.ts:175`, `claude-compaction-enforcer.ts:436`). Only a fresh,
|
||||
known sample can start a compaction. The sample-less drain reads readiness from the pane alone
|
||||
and counts a timer-less live status row, such as `✻ Compacting conversation…`, as work
|
||||
(`session-transport.ts:153-161`); a recent hook showing work or a waiting person still
|
||||
refuses. It records no width receipt, because the only figure would be the pre-compact one.
|
||||
|
||||
An automatic attempt has a 25-minute ceiling that starts once the prep is delivered. A stale
|
||||
occupant generation, the policy becoming disabled, or the deadline passing stops it. A prep
|
||||
that clearly wasn't sent is retried at most three times; an uncertain prep or `/compact` is
|
||||
never replayed.
|
||||
|
||||
The restore prompt names a restore-pending marker at
|
||||
`compaction/restore-pending/<session>.json` in the OpenRig home, which the `openrig-core`
|
||||
PreCompact hook writes. A per-seat `compaction/post-compact-extra/<session>.md` is preferred
|
||||
over the global extra file, and an extra file that declares a different seat is refused.
|
||||
|
||||
`rig compact <session>` (`packages/cli/src/commands/compact.ts`) runs the same sequence
|
||||
manually through `/api/compaction/trigger`, `cancel` and `state`, which use the terminal
|
||||
bearer-token middleware; it passes every request through when no token is configured.
|
||||
`--skip-map` skips the map requirement once, `--cancel` ends the preparation, and `--state`
|
||||
shows the attempt, its expected map and its deadline. Ordinary approval prompts during preparation can be answered within that same deadline; OpenRig waits without answering them. A manual attempt has 180 seconds for
|
||||
preparation and the idle wait.
|
||||
|
||||
The below-threshold continuation checks policy enablement too, except for an explicitly
|
||||
operator-initiated manual sequence. A known occupant-generation mismatch invalidates the
|
||||
queued stages; an unknown generation does not prove a match. Failed or retained sends
|
||||
do not advance the stage. The manual trigger shares this continuation while having its
|
||||
own initiation checks.
|
||||
|
||||
Stage, deduplication, cooldown and preparation-attempt maps are in memory; the restore maps
|
||||
are files. Startup supplies live generation
|
||||
resolution and a post-restore callback that records a managed-width receipt, so the wider
|
||||
operation also has persistent effects. Do not infer durable lifecycle completion from
|
||||
a library read or one successfully queued message.
|
||||
|
||||
## Related maps
|
||||
|
||||
- [packaging-bootstrap-bundles.md](packaging-bootstrap-bundles.md): bundle export and
|
||||
routing into content libraries.
|
||||
- [agent-spec-and-startup.md](agent-spec-and-startup.md): context/resource projection
|
||||
and image consumption during launch.
|
||||
- [lifecycle-snapshot-restore.md](lifecycle-snapshot-restore.md): broader continuity flows.
|
||||
@@ -1,578 +0,0 @@
|
||||
---
|
||||
kind: as-built
|
||||
title: Transport, Transcripts, Chat, Ask
|
||||
status: active
|
||||
topics: [coordination, observability]
|
||||
domains: [engineering-advisor, operating-advisor]
|
||||
applies-when: |
|
||||
Need to know how rig send/capture/broadcast works, how transcript capture
|
||||
(periodic tmux capture-pane) and rg/grep search behave, how durable rig chat
|
||||
(SQLite + SSE) is modeled, what rig ask gathers, or the exact MCP-tool-name
|
||||
vs tmux-metadata-key naming distinction.
|
||||
siblings: [daemon-core.md, lifecycle-snapshot-restore.md]
|
||||
prerequisite-reads: [../README.md, daemon-core.md]
|
||||
last-verified-against-source: e8f0ab340db773392ec8be75b072d1c0f3068a50
|
||||
last-updated: 2026-10-08
|
||||
---
|
||||
|
||||
# Transport, Transcripts, Chat, Ask
|
||||
|
||||
The communication-and-history layer: tmux is transport, not truth. Send/
|
||||
capture/broadcast wrap tmux with honest errors; transcripts are bounded
|
||||
`tmux capture-pane` snapshots written to files; chat is daemon-backed SQLite;
|
||||
the daemon's `rig ask` service gathers evidence and never calls an LLM.
|
||||
|
||||
> Verified against source at main `e8f0ab340db773392ec8be75b072d1c0f3068a50`. Each count below sits beside the
|
||||
> command that produces it; run the command from the repository root to refresh
|
||||
> it.
|
||||
|
||||
## 1. The naming axes (read this first)
|
||||
|
||||
Two independent naming axes meet in this layer. They must not be conflated or
|
||||
blanket-replaced: `rigged` → `rig` is a per-occurrence question, not a global
|
||||
substitution.
|
||||
|
||||
**Axis 1 — MCP tool names are `rig_*`.** `packages/cli/src/mcp-server.ts`
|
||||
registers **24** tools, all named `rig_*`
|
||||
(`grep -c 'server.tool(' packages/cli/src/mcp-server.ts`), including
|
||||
`rig_rig_nodes` (`mcp-server.ts:290`), `rig_send` (`:307`),
|
||||
`rig_capture` (`:341`), `rig_chatroom_send` (`:366`) and `rig_chatroom_watch`
|
||||
(`:402`), the five queue tools `rig_queue_create`, `_show`, `_list`, `_claim`
|
||||
and `_update`, and `rig_telemetry_events`. No `rigged_*` name other than the tmux keys below remains in product
|
||||
source: **0** hits
|
||||
(`git grep -n 'rigged_' -- ':(glob)packages/*/src/**' | grep -v -c '@rigged_'`).
|
||||
|
||||
**Axis 2 — tmux metadata keys are `@rigged_*`, and are correct as-is.** The
|
||||
tmux metadata keys written at claim/bind time are a separate thing from MCP
|
||||
tool names and were not renamed. `setRiggedMetadata` in
|
||||
`packages/daemon/src/domain/claim-service.ts:182`–`186` writes exactly **5**
|
||||
keys (`grep -c '@rigged_' packages/daemon/src/domain/claim-service.ts`):
|
||||
`@rigged_node_id`, `@rigged_session_name`, `@rigged_rig_id`,
|
||||
`@rigged_rig_name`, `@rigged_logical_id`. Do not blanket-sed `rigged` → `rig`.
|
||||
The metadata-key axis is detailed in `agent-spec-and-startup.md` (identity:
|
||||
whoami, bind, adopt).
|
||||
|
||||
## 2. Transport and communication domain services
|
||||
|
||||
All under `packages/daemon/src/domain/`:
|
||||
|
||||
- `session-transport.ts` — communication primitives: send/capture/broadcast
|
||||
with session resolution, send-readiness classification (interactive-prompt
|
||||
and mid-work detection), honest error reporting, and list/pod/rig/global
|
||||
targeting.
|
||||
- `transcript-store.ts` — transcript file management: path convention, ANSI
|
||||
stripping for tail and grep reads, boundary markers, `readTail`, `readFull`, `grep`.
|
||||
Filesystem-backed, NOT SQLite. The capture that writes the files is
|
||||
`transcript-rotation.ts` (§4).
|
||||
- `history-query.ts` — transcript + chat search. Prefers `rg`, falls back to
|
||||
`grep -E`, surfaces which backend was used: `history-query.ts:8`
|
||||
`backend: "rg" | "grep" | "none"`; `:252` runs
|
||||
`rg -i --no-filename -e <pattern>` over the rig's transcript directory;
|
||||
`:267` falls back to `grep -E -i -h -e <pattern>`.
|
||||
- `ask-service.ts` — context-engineering evidence pack: gathers rig summary
|
||||
plus transcript excerpts, chat excerpts, insufficiency state, guidance.
|
||||
Does NOT call an external LLM.
|
||||
- `chat-repository.ts` — durable rig-scoped chat over the `chat_messages`
|
||||
table (`send`, `sendTopic`, `history`, `latest`, `searchChat`, `clear`). The
|
||||
repository emits no events: `routes/chat.ts` emits `chat.message` on the
|
||||
event bus after each send or topic, and `/watch` streams those as SSE.
|
||||
|
||||
Routes: `packages/daemon/src/routes/{transport,transcripts,ask,chat,whoami}.ts`
|
||||
— all present.
|
||||
|
||||
## 3. Communication flow
|
||||
|
||||
`rig send <session> "message"` → CLI → `POST /api/transport/send` →
|
||||
`SessionTransport.send()`:
|
||||
|
||||
1. Resolve the session name (`resolveBySessionName`,
|
||||
`session-transport.ts:963`): not found → 404; the same name in more than
|
||||
one rig → 409. A tmux probe of the session can also refuse it as missing or
|
||||
tmux as unavailable (`:1317`–`1334`). Pod, rig, global and `--to` list
|
||||
targets go through `POST /api/transport/broadcast`, which resolves them
|
||||
with `resolveSessions` and sends to each recipient in turn.
|
||||
2. Take the seat's delivery lease. Every write runs under a per-seat,
|
||||
serialized lease from the seat delivery guard, installed at daemon start
|
||||
(`send`, `:1155`). With the seat's typing guard on, the message is held
|
||||
instead of typed: the result is `outcome: "retained"` with HTTP 200
|
||||
(`routes/transport.ts:115`); operators use `rig seat set-typing-guard` and
|
||||
`rig seat held-messages`. This same typing guard has `off` (default),
|
||||
`draft-aware`, and `hold` modes. The legacy enabled boolean selects hold/off.
|
||||
`DraftAwareDelivery` makes one attempt for opted-in seats and immediately returns
|
||||
the exact refusal reason and original held IDs when input is unavailable.
|
||||
Migration `100_typing_guard_modes` extends existing guard/audit rows and adds
|
||||
non-executable receipt metadata to the original outbox; it creates no tables
|
||||
or retry scheduler. Combined wakes preserve every original ID/body.
|
||||
Legacy storage bits fail safe to hold on rollback; newer legacy audit writes
|
||||
supersede older saved modes on re-upgrade, including same-value writes.
|
||||
3. Classify send readiness. Only a positive interactive-prompt reading
|
||||
(`needs_input`) refuses, with `target_needs_input`
|
||||
(`session-transport.ts:1482`), unless the caller passes
|
||||
`--dangerously-interact --reason`; that override persists an audit record
|
||||
before sending and refuses the send if it can't
|
||||
(`prompt_override_audit_unavailable`, `:1471`). A pane that reads `unknown` while the seat's
|
||||
latest runtime hook says it waits on a person reads as `needs_input` (`session-transport.ts:1878–1889`). A mid-work reading (`running`, from a
|
||||
fresh runtime hook or the pane's mid-work patterns,
|
||||
`findPatternEvidence(recentLines, [...MID_WORK_PATTERNS, CLAUDE_LIVE_STATUS_PATTERN])`,
|
||||
`:311–315`) or an `unknown` one proceeds with an advisory `warning` (`:1495`,
|
||||
`:1503`). `--force` has no effect on this path, and combining it with
|
||||
`--wait-for-idle` is refused with 400 (`routes/transport.ts:66`–`73`).
|
||||
4. Two-step tmux send: a unique file/buffer pasted with `paste-buffer -d -r -p`
|
||||
(`packages/daemon/src/adapters/tmux.ts:625`) → ~200ms delay
|
||||
(`session-transport.ts:1558`) → separate named `Enter` (`session-transport.ts:1573–1578`). The paste
|
||||
targets the seat's registered pane ID, not the session name, and is refused
|
||||
with no input written if the session no longer holds exactly that pane.
|
||||
Bracketed paste preserves multiline input in supporting TUIs; the payload
|
||||
never enters a shell argument. A `--dangerously-interact` answer is pasted
|
||||
without `-p` (`session-transport.ts:1541`) and Enter is pressed only if the
|
||||
whole answer is still staged (`:1562`–`1570`). A successful paste proves
|
||||
transport execution, not runtime consumption.
|
||||
The transport's prompt reader uses the shared `composer-prompts.ts` matchers
|
||||
from Mr-Neutr0n's #635. Its cursor-aware extension preserves capture styling
|
||||
and excludes a wholly dim autocomplete suffix after the cursor. The daemon
|
||||
directly depends on `string-width` to compare Unicode terminal cell widths
|
||||
with cursor positions and exact staged text; a dependency hoisted from the
|
||||
TUI would not declare this daemon runtime requirement. Default seats retain
|
||||
their existing advisory recognition and staged-text verification behavior.
|
||||
For draft-aware seats, `TmuxAdapter` checks the current composer after buffer
|
||||
preparation and again immediately before keys. The delivery lease records
|
||||
the pasted text and any observed collapsed-paste label; Enter requires that
|
||||
owned input. Late human input, changed recipients or stopped queue wakes refuse
|
||||
further input or hold the message; ambiguous writes remain indeterminate. Nothing
|
||||
is retried automatically. Unicode cell widths use the daemon's direct
|
||||
`string-width` dependency. See [per-seat terminal delivery](../../reference/seat-delivery.md)
|
||||
for activation, persistence, inspection and lifecycle tradeoffs.
|
||||
5. Optional `--verify`: capture the last 30 pane lines before the paste and,
|
||||
500 ms after Enter, again, leaving out the current input box (the last
|
||||
prompt line and its continuation, `verificationTranscript`). A send
|
||||
carrying OpenRig's From/To header is matched as the whole message with
|
||||
whitespace normalized (`countEnvelopeOccurrences`); a raw send is matched
|
||||
by its first 40 characters (`verificationSnippet`). More occurrences after
|
||||
the send report `outcome: "delivered"`; otherwise the outcome is
|
||||
`rendered-unconfirmed`: text and Enter landed but the capture could not
|
||||
re-confirm the render, which is not a failure. The outcome values are
|
||||
`delivered`, `rendered-unconfirmed`, `failed` and `retained`.
|
||||
With `transport.verify_input_box` on (off by default), a Claude Code or
|
||||
Codex send is verified only when the seat was proven idle before the paste
|
||||
and the input box shows the message, and is unconfirmed otherwise:
|
||||
|
||||
- **Seat idle before the paste** (`readSeatBeforePaste`). The `--verify`
|
||||
capture taken before the paste (the whole screen) must match an idle
|
||||
allow-list (`SEAT_IDLE_SHAPES`) built from real Claude Code 2.1.296 and
|
||||
Codex 0.160.0 frames and, where no frame was captured, from the Claude
|
||||
Code 2.1.296 bundle, the Codex 0.160.0 source and OpenRig's own launch
|
||||
code. Claude Code: the empty box or its `Try "…"` suggestion between two
|
||||
rules; the opening rule plain, or carrying this seat's own session name
|
||||
the way Claude Code draws `--name` (`─…─ <session> ─`, or `──` as in
|
||||
earlier versions, as wide as the closing rule; OpenRig launches every
|
||||
Claude seat with `--name`); the footer row of the seat's permission
|
||||
posture, one for each mode OpenRig can launch: bypass
|
||||
(`⏵⏵ bypass permissions on (shift+tab to cycle) · ← for agents`), accept
|
||||
edits (OpenRig's default), auto, don't ask, plan, and default
|
||||
(`⏸ manual mode on · ← for agents`); between the closing rule and the
|
||||
footer, either no row (OpenRig's statusLine collector prints nothing) or
|
||||
the rows a person's own statusLine prints, with the indicator Claude
|
||||
draws at the right of the first one (`● high · /effort`), trusted only by
|
||||
position, as the composer reader trusts them (#2703, #2798): the closing
|
||||
rule is the first rule above the footer, the frame above it must check
|
||||
out, and no row between them may be blank or look like a menu or dialog
|
||||
(`❯`, `›` or `»` anywhere in the row, a row that starts with a numbered
|
||||
choice such as `1. ` or `2) `, with or without a border or gutter before
|
||||
it, a box corner, or a row framed by side borders); so a statusLine that
|
||||
uses `›`, `»` or `❯` as a separator, prints a blank row or starts a row
|
||||
with a number and a dot reads `unknown`; a blank, effort or tmux-notice
|
||||
row above the box; and above
|
||||
that, in the status slot, the completion row of the seat's last turn
|
||||
(`✻ <verb> for <duration> · done <time>` at column 0, matched by the
|
||||
Claude Code 2.1.296 bundle's grammar: one of its eight past-tense verbs,
|
||||
its duration format, and the time of a turn finished today on a 12- or
|
||||
24-hour en-US clock). A blank slot counts only under that completion
|
||||
row, with only blank rows between them (as in the fullscreen renderer),
|
||||
or, before the first turn, under the last row of the Claude Code v2.1.296
|
||||
welcome banner. OpenRig launches Claude in the classic renderer, where the
|
||||
box follows the transcript, so an idle seat that has finished a turn
|
||||
shows that turn's completion row in the slot; real frames of idle seats
|
||||
OpenRig launched show it. A configured statusLine, which OpenRig installs
|
||||
on every Claude seat, makes Claude Code drop the footer's hints, so a
|
||||
running turn draws the same footer row as an idle one, without
|
||||
`esc to interrupt`: the status slot is what tells them apart. The 2.1.296
|
||||
bundle draws the live status row (the spinner) there only while no
|
||||
streamed text with a complete line is on screen
|
||||
(`isLoading && (!hasDisplayedStreamingText || tailBlank || …)`); while
|
||||
streamed text is shown it draws nothing in its place, and the last
|
||||
streamed row moves into the slot. That row never matches the completion
|
||||
row, because Claude Code draws a message behind its `●` column. The
|
||||
bundle does not show that the last streamed row is never blank, so a
|
||||
blank slot under transcript text reads `unknown`: that case is left
|
||||
unconfirmed rather than read as idle. Codex: the empty box over its
|
||||
status line and a `? for shortcuts` hint row, with the completion row or
|
||||
the fresh session's greeting as the last row above the box. The
|
||||
completion row is matched by the grammar Codex 0.160.0 prints it with
|
||||
(`Worked for <elapsed> • <time>`, a 12- or 24-hour clock, a date before
|
||||
the time on another day, runtime metrics after it). No row anywhere may
|
||||
be a running, streaming, queued, pending-input or paste-hint row (a live
|
||||
status row, `esc to interrupt`, `paste again to expand`, a queued label,
|
||||
Codex's pending-input headers or `↳` rows, a spinner). Any other frame,
|
||||
including every running, streaming, steer-preview, paste-hint or unknown
|
||||
layout, another seat's name, a second rule drawn under the box, a blank,
|
||||
prompt-, menu- or dialog-shaped row between the closing rule and the
|
||||
footer (so a fullscreen renderer that reserves a blank row for the
|
||||
statusLine reads as before), and a background shell in the footer, reads
|
||||
`seat: not-idle` or `unknown`, and the send reads unconfirmed. This
|
||||
holds when a running turn hides its signal (Codex hides its `• Working`
|
||||
row while it streams; Claude Code shows only `paste again to expand` in
|
||||
its footer for 8 s after a paste, and no `esc to interrupt` under a
|
||||
statusLine). So a send to
|
||||
a busy or unknown seat always reads unconfirmed.
|
||||
|
||||
**Versions.** The layouts were recorded from Claude Code 2.1.296 and
|
||||
Codex 0.160.0 (the versioned frames under
|
||||
`packages/daemon/test/fixtures/input-box/`, with their limits in that
|
||||
folder's README). A frame that visibly names another client version reads
|
||||
`unknown` before the paste and `unrecognized` in the box: Claude Code's
|
||||
welcome banner (`Claude Code v2.1.297`), Codex's session header
|
||||
(`>_ OpenAI Codex (v0.161.0)`) or its update notice, whose first version
|
||||
is the running one (`Update available! 0.159.0 -> …`). Most frames
|
||||
show no version (the banner and header scroll away), and there a matching
|
||||
layout is evidence about the layout only, not proof of the client
|
||||
version: another version that draws the same rows reads the same. There is
|
||||
no version discovery and no extra read for it.
|
||||
|
||||
Which Claude seats OpenRig launches can verify: an idle Claude Code
|
||||
seat in any posture OpenRig launches, after any number of turns, whose
|
||||
status slot holds its last turn's completion row as above, or a fresh
|
||||
seat whose slot is blank under the welcome banner. It reads `unknown`
|
||||
when that row is anything else: a blank row under transcript text, a
|
||||
completion row with more after the time (a still-running note, hidden
|
||||
messages, a budget), the weekday or date of a turn that finished on an
|
||||
earlier day, another locale's or a custom time format, an interrupted
|
||||
turn, or a row printed after the turn. And only a text Claude Code shows
|
||||
inline in its box can verify (see the box reading below): a multi-line
|
||||
text, which includes every `rig send` envelope and every queue wake,
|
||||
collapses to a paste label and stays unconfirmed.
|
||||
- **The box held exactly this message before Enter.** One extra capture
|
||||
between the paste and Enter must show exactly the sent text in the input
|
||||
box with its word boundaries kept (`readInputBox`, `boxHoldsText`): runs
|
||||
of whitespace, including Unicode spaces, compare as one space, so a box
|
||||
holding `nowhere` is not the text `now here`. A row break counts as a
|
||||
space only where it must be whitespace of the box's text. Claude Code
|
||||
2.1.296 wraps its input at spaces only (wrap-ansi). Codex 0.160.0 wraps
|
||||
it with textwrap (FirstFit, UAX #14 break opportunities, the hyphen
|
||||
splitter), which also breaks with no space drawn after a hyphen or a
|
||||
slash and between ideographs. So a break counts where the next row's
|
||||
first word would have fit on the row before it (both wrappers fill a row
|
||||
first, so the text has a line break there), or where it falls between
|
||||
two ASCII letters or digits, or before a hyphen, a slash, a closing
|
||||
bracket or `.,:;!?`, and the two words it separates, joined, fit in the
|
||||
box's widest row, but only after a row whose text starts at its first
|
||||
cell with a printable ASCII character and whose last word is ASCII
|
||||
letters, digits, `'` and `.,:;` only. Codex keeps a word that fits in a
|
||||
row whole only where the word starts on a row that already holds text:
|
||||
a row holding only indentation absorbs the next word and splits it
|
||||
where the row fills, and a word after a hyphen or a slash can be split
|
||||
the same way in a row that took the next textwrap line. So on Codex a
|
||||
split between two letters is not always the text's space; after an
|
||||
indented row, or after a last word with a break opportunity inside it,
|
||||
only the first rule reads the break. The first rule has one known
|
||||
residual on Codex, found in a model of the rust-v0.160.0 source, not in
|
||||
a live capture: once an indented row has absorbed and split a word, the
|
||||
re-pass can end a later row early at a hyphen, so a break with no
|
||||
whitespace can pass the first rule. Every such box in that model also
|
||||
holds the earlier split after the indented row, which reads `ambiguous`,
|
||||
so the box never reads `exact`. The widest row is counted in
|
||||
printable ASCII cells only, so combining marks, joiners and zero-width
|
||||
characters never widen it. Any other break, and a break inside a word of
|
||||
the text, reads `ambiguous` and the send stays unconfirmed. A collapsed paste label, Claude's
|
||||
queued-messages label or `Try "…"` suggestion, or another draft is never
|
||||
the message. The default `--verify` matcher above is unchanged. Claude
|
||||
Code's box ends at the last rule on the screen; the rows under it up to
|
||||
the footer are a person's statusLine, read by position with the same
|
||||
rule as the seat check above (#2798), so a box with statusLine rows
|
||||
under it reads as one without them, and a blank, prompt-, menu- or
|
||||
dialog-shaped row there leaves it `unrecognized`. The startup Enter
|
||||
guard reads the same region.
|
||||
- **The box is empty after Enter on the same pane process.** The capture
|
||||
after Enter must show the box empty, with the pane process
|
||||
(`#{pane_pid}`) and the seat's binding unchanged.
|
||||
|
||||
These readings stay internal to the send; the result carries only the
|
||||
verdict (`verified`, `outcome`). An empty box reads `queued` instead of
|
||||
`empty` where the frames say the message may wait behind a turn: Codex's
|
||||
pending-input rows or a row under its running-turn status, or a running
|
||||
turn in the frame read before Enter (Codex's `• Working` row; for Claude
|
||||
Code, `esc to interrupt` or a live status row above the box, including
|
||||
above a box of several rows). The verdict uses these readings: verified
|
||||
needs `empty`, so for a seat that was idle before the paste they are the
|
||||
only check that rejects a message queued behind a turn that started after
|
||||
the pre-paste capture. An audited prompt answer (step 4) reads no box: with
|
||||
the setting on it stays unconfirmed, whatever the default matcher would
|
||||
say.
|
||||
The readings decide the verification verdict, not whether Enter is sent:
|
||||
the seat is read from the capture `--verify` already takes, and none of
|
||||
the extra reads decides whether Enter is sent. Each has
|
||||
a 500 ms deadline, which is a budget, not a guarantee: the pre-Enter read
|
||||
comes after the existing 200 ms wait and can add about 500 ms before
|
||||
Enter, and the post-Enter process read can add about another 500 ms
|
||||
before the reply. A read that misses its deadline reads `unavailable`, the
|
||||
send stays unconfirmed, and Enter is still sent once. With a deliberately
|
||||
slowed screen read, review measured about 718 to 721 ms from paste to
|
||||
Enter, against about 216 to 219 ms on main under the same slowdown.
|
||||
Limits: the seat is read once, before the paste, and Enter
|
||||
follows about 200 ms later. A turn can start in that window, from a person
|
||||
typing in the pane or from the agent itself (Claude Code and Codex start
|
||||
turns on their own when a background task finishes, a scheduled wake-up
|
||||
fires or a teammate's message arrives). Such a turn is caught only when
|
||||
the frame read before Enter shows it; a Codex turn already streaming
|
||||
there hides its `• Working` row and is not seen. An empty box after an
|
||||
exact one means the box was cleared, normally by our Enter; a clear by
|
||||
something else at that moment (a person's C-c) looks the same, and so
|
||||
does a text the TUI runs as a command (a Claude Code `/command`, a Codex
|
||||
`!command`), which clears the box without the model reading a message.
|
||||
`#{pane_pid}` is the pane's root process (the shell for most launches), so
|
||||
an agent restarted under the same shell is not caught by the pane process
|
||||
check, and the same pane and binding are not proof of the same native
|
||||
conversation.
|
||||
An empty box means the TUI took the text, not that the model read it.
|
||||
Consumers see only the report. With the setting on, a queue wake that
|
||||
reads unconfirmed records the existing `delivered-ack-pending` state, as
|
||||
an unconfirmed wake does today. Every queue wake to Claude Code is
|
||||
unconfirmed under the setting, because its multi-line envelope collapses
|
||||
to a paste label, and so is a wake to a busy or not-proven-idle Claude
|
||||
Code or Codex seat. A pending, unclaimed handoff with no pickup evidence
|
||||
can then enter the wake ladder's existing escalation path after
|
||||
`queue.wake_unconfirmed_window_minutes` (default 30). It is not blindly
|
||||
re-sent: the ladder's unconfirmed path skips its retry rung
|
||||
(`queue-wake-ladder.ts`), and nothing in the send path retries or
|
||||
re-sends.
|
||||
6. CLI `--verify` goes further (`packages/cli/src/commands/send.ts:596`): it
|
||||
looks for the text still unsubmitted at the prompt, makes one guarded
|
||||
Enter-only retry through the daemon's `submitOnly` path
|
||||
(`session-transport.ts:1344`, which refuses with `staged_mismatch` unless the
|
||||
pane shows the expected staged text), and ends `staged-not-consumed` with
|
||||
exit code 1 if it is still there.
|
||||
7. Honest result with reason on failure. Reasons missing from the send
|
||||
route's status map (among them the runtime-identity refusals) are returned
|
||||
with HTTP 500 (`routes/transport.ts:118`–`140`).
|
||||
|
||||
The sender comes from the `X-OpenRig-Session` header; an unsigned send gets an
|
||||
unknown-sender notice. The CLI adds a From/To envelope, and the daemon appends
|
||||
a delivered-latency note for delays of 10 s or more.
|
||||
|
||||
Architecture rule 18 (`architecture-rules-and-event-system.md`): tmux is
|
||||
transport, not truth — `send/capture/broadcast` wrap tmux reliably with honest
|
||||
errors.
|
||||
|
||||
The daemon runs tmux without a shell: argv straight to the tmux binary found on
|
||||
`PATH`. `OPENRIG_TMUX_SHELL=1` at daemon start runs every tmux call through
|
||||
`/bin/sh` instead (Linux and macOS; Windows always uses argv). A binary that
|
||||
can't be started at all (a script without `#!`, a missing interpreter) runs
|
||||
through `/bin/sh -c 'exec "$0" "$@"' tmux …`, which finds tmux as the shell
|
||||
path does; nothing ran before, so no command runs twice. The resolved path is
|
||||
cached, so a different tmux installed earlier on `PATH` takes effect after a
|
||||
daemon restart.
|
||||
|
||||
### 3b. Cross-host coordination verbs
|
||||
|
||||
`rig send/capture/transcript/broadcast --host <id>` (and the `agent@rig@host`
|
||||
target sugar on the session-target verbs) cross the host boundary through the
|
||||
remote daemon's ordinary local routes; the cross-host logic is in the CLI.
|
||||
|
||||
- **Sidedness and caller pick the seam.** One-sided remote operations from a
|
||||
caller that can resolve the registry bearer locally (the CLI) go
|
||||
**CLI-direct** through `runRemoteHttpOp`
|
||||
(`packages/cli/src/remote-host-ops.ts:25`) — one hop, no local-daemon
|
||||
involvement, as `rig ps --all-hosts` does. Two-sided operations (a
|
||||
cross-host queue handoff closes the local source and creates the remote
|
||||
successor, so the local daemon owns half the transaction) and callers
|
||||
without the bearer (the browser: Mission Control actions and the `?host=`
|
||||
read-through) go through the local daemon, which forwards to the remote.
|
||||
These four verbs are one-sided remote operations from the CLI.
|
||||
- **The host entry dictates the transport (ssh or http), never a per-call
|
||||
choice.** ssh hosts shell out over ssh for send/capture
|
||||
(`packages/cli/src/cross-host-executor.ts`); http hosts (the kind
|
||||
`rig host pair` registers) go CLI-direct to
|
||||
`POST /api/transport/send|capture|broadcast` / `GET /api/transcripts/*` with
|
||||
the same bodies and paths the local CLI uses. transcript and broadcast are
|
||||
http-only (no ssh path exists for them); on an ssh host they fail with a
|
||||
structured error naming the transport requirement
|
||||
(`remote-host-ops.ts:47`), never a fallback.
|
||||
- **Terminal-bearer posture (`/api/transport/*` only).** The remote's
|
||||
transport routes (send/capture/broadcast) gate on its terminal bearer
|
||||
(`routes/transport.ts:26`; `OPENRIG_TERMINAL_BEARER_TOKEN`,
|
||||
`packages/daemon/src/index.ts:291`). With no token configured the
|
||||
middleware passes every request through
|
||||
(`packages/daemon/src/middleware/auth-bearer-token.ts:99`) — the tailnet is
|
||||
the auth boundary by design — except that a daemon bound to an explicit
|
||||
address that is neither local nor tailnet falls back to
|
||||
`OPENRIG_AUTH_BEARER_TOKEN`
|
||||
(`index.ts:297`). The CLI presents the registry bearer from `hosts.yaml`
|
||||
when one is configured; for a URL-only anonymous host the `Authorization`
|
||||
header is omitted. A remote enforcing a different terminal bearer answers
|
||||
401, which surfaces as the structured `permission-gate` step
|
||||
(`packages/cli/src/host-registry.ts:53`) — never a hang, never silent.
|
||||
Remedy: set the remote terminal bearer equal to the paired registry bearer,
|
||||
or rely on the tailnet boundary. **The transcript read is deliberately
|
||||
outside this class:** `/api/transcripts/*` mounts with no bearer middleware
|
||||
(`packages/daemon/src/server.ts:748`, beside the gated `/api/transport` at
|
||||
`:749`), and credential-shaped text is redacted on the `/full` route only
|
||||
(`routes/transcripts.ts:292`). So a wrong terminal bearer that
|
||||
permission-gates `send --host` does NOT gate `transcript --host` — the read
|
||||
keeps succeeding for a Host name the remote's browser boundary accepts
|
||||
(`server.ts:647`; it applies to every `/api/*` route), and the auth-failure
|
||||
class is not uniform across the four verbs. The route's own comment (`routes/transcripts.ts:250`) leaves a
|
||||
coherent transcript-read auth policy across tail/grep/full for later.
|
||||
- **The 3-part form is CLI-edge sugar only.** `resolveCrossHostTarget`
|
||||
(`packages/cli/src/cross-host-target.ts:58`) strips the suffix only when it
|
||||
resolves to a registered host, or equals this host's own id, which routes
|
||||
home (`:95`; only `rig send` passes the self id,
|
||||
`packages/cli/src/commands/send.ts:398`, so `capture` and `transcript` treat
|
||||
their own id as unknown); otherwise the string passes through unchanged with a loud
|
||||
host hint. Every session string that reaches any daemon stays
|
||||
`member@rig`; the host travels out-of-band. Durable cross-host
|
||||
coordination stays the queue's cross-host routing
|
||||
(`coordination-primitive.md`); these four verbs add no queue surface.
|
||||
|
||||
## 4. Transcript flow
|
||||
|
||||
1. `NodeLauncher` starts transcript rotation right after tmux session
|
||||
creation, before the harness boots
|
||||
(`NodeLauncher`, `startTranscriptRotation`).
|
||||
2. Each rotation tick captures the trailing lines and atomically overwrites
|
||||
(temp file + rename in `startTranscriptRotation`)
|
||||
`transcripts/{rig-name}/{session-name}.log` under the OpenRig home (or
|
||||
the `transcripts.path` setting). It skips the write when the captured bytes
|
||||
are unchanged. Defaults: 1000 trailing lines every 2 s
|
||||
(`DEFAULT_TRANSCRIPT_LINES`, `DEFAULT_TRANSCRIPT_POLL_INTERVAL_MS`); an idle seat is captured less often,
|
||||
within the 10-second freshness window. Both are tunable with
|
||||
`OPENRIG_TRANSCRIPTS_LINES` and `OPENRIG_TRANSCRIPTS_POLL_INTERVAL_SECONDS`
|
||||
or the matching `transcripts.*` settings. This bounded capture replaced the
|
||||
earlier `tmux pipe-pane` stream (see the header comment of `transcript-rotation.ts`), so the file
|
||||
holds a trailing window, not the whole session. Sessions on one adapter
|
||||
tick on one shared timer, and the captures due in a tick share
|
||||
`capturePanesContent`: one chained call per chunk of 24, with a
|
||||
`list-sessions` started beside it on every batch, whose result is consulted
|
||||
only when the chain fails, each chunk on its own (`captureBatch` in
|
||||
`transcript-rotation.ts` → `capturePanesContent` in `adapters/tmux.ts`).
|
||||
A session the batch leaves out, a lone due session, or a pane or window
|
||||
target reads per target with `capturePaneContent` (`adapters/tmux.ts`).
|
||||
3. `TranscriptStore` owns path convention, ANSI stripping for `readTail` and
|
||||
`grep`, boundary
|
||||
markers, `readTail`, `readFull`, `grep`. `readFull` returns the raw bytes
|
||||
(`TranscriptStore.readFull`); the `/full` route redacts them. The transcript
|
||||
routes check capture health first and can start capture lazily for a seat
|
||||
that has none.
|
||||
4. `rig transcript <session> --tail N / --grep "pattern"` provides
|
||||
agent-facing access.
|
||||
5. On restore: a `--- SESSION BOUNDARY: … ---` marker is written before
|
||||
re-launch (`RestoreOrchestrator`); each rotation tick keeps each
|
||||
distinct boundary line once, as a header above the fresh capture
|
||||
(`startTranscriptRotation`, boundary header). (Restore-side detail in
|
||||
`lifecycle-snapshot-restore.md`.)
|
||||
6. `rig ask` gathers rig summary plus transcript excerpts, chat excerpts,
|
||||
insufficiency state, and guidance.
|
||||
|
||||
Architecture rule 19, as the code implements it: transcripts are a bounded
|
||||
`capture-pane` snapshot (not pipe-pane), ANSI-stripped on tail and grep reads; `rg`
|
||||
preferred, `grep -E` fallback. Rule 22: the daemon's `rig ask` is context
|
||||
engineering — it gathers evidence and does NOT call an external LLM; the
|
||||
agent IS the LLM. The one exception is the explicit CLI flag `rig ask --wake`
|
||||
(`packages/cli/src/commands/ask.ts:88`), which runs a headless resume of a
|
||||
Claude or Codex session (`claude -p --resume` / `codex exec resume`,
|
||||
`packages/cli/src/ask-wake.ts:58`–`60`) to answer one question.
|
||||
|
||||
### Current-generation records for `rig walk`
|
||||
|
||||
`rig walk` checks a delivered piece against the current conversation through
|
||||
`GET /api/sessions/:sessionName/generation-record` (`routes/sessions.ts`).
|
||||
Without `sinceBytes`, the route returns record identity and size; with it, the
|
||||
route also reads a bounded suffix from the same open file. This terminal-class
|
||||
endpoint uses the terminal bearer gate.
|
||||
|
||||
For Codex, the current occupant must have a verified identity for its bound
|
||||
pane. The route compares tmux's current root PID with the identity's
|
||||
`observedPanePid`, falling back to `observedPid` for older verdicts that only
|
||||
recorded one PID. Previously it always compared the root with the native Codex
|
||||
PID, refusing a verified Codex running beneath a shell or launcher. Recording
|
||||
both PIDs lets that wrapper pass without accepting a changed pane root or
|
||||
changing the identity schema.
|
||||
|
||||
For dual-PID verdicts, the selected thread must also be the thread resolved for
|
||||
`observedPid`. All candidate threads still participate in the ambiguity check;
|
||||
another process's resolvable thread cannot stand in for a missing or
|
||||
unresolvable verified native process. This reuses the same process observation
|
||||
and thread lookups, without an additional census.
|
||||
|
||||
The existing thread lookup still requires a Codex descendant with one
|
||||
resolvable thread, a thread-table record path and a matching rollout header.
|
||||
The route rechecks occupant generation, pane binding and root PID after the
|
||||
thread lookup. These are bounded observations, not an atomic process snapshot.
|
||||
An unresolved identity still returns 409; `rig walk` reports consumption as
|
||||
unverified instead of claiming it read the record. Route regressions in
|
||||
`packages/daemon/test/walk-consumption-primitives.test.ts` cover shell and
|
||||
launcher wrappers, legacy verdicts, root changes, replaced or unresolved native
|
||||
processes, and changes during thread resolution with synthetic process
|
||||
observations and actual SQLite/JSONL
|
||||
fixtures; they do not exercise a live Codex conversation.
|
||||
|
||||
## 5. Chat flow
|
||||
|
||||
1. `rig chatroom send <rig> "message"` → `POST
|
||||
/api/rigs/:rigId/chat/send` → `ChatRepository.send()`
|
||||
(`routes/chat.ts:43`); the route then emits `chat.message` (`:45`).
|
||||
2. SSE: `GET /api/rigs/:rigId/chat/watch` delivers real-time messages.
|
||||
3. History: `GET /api/rigs/:rigId/chat/history` returns channel history in id
|
||||
order, 100 messages unless `limit` says otherwise
|
||||
(`chat-repository.ts:68`), filterable by `topic`, `after`, `since` and
|
||||
`sender` (a `since` SQLite cannot parse is refused with 400,
|
||||
`routes/chat.ts:73`); `POST /api/rigs/:rigId/chat/topic` persists topic markers.
|
||||
4. UI: a `RigChatPanel` component exists
|
||||
(`packages/ui/src/components/RigChatPanel.tsx`), but no UI view mounts it:
|
||||
**1** file under `packages/ui/src` names it, its own
|
||||
(`git grep -l 'RigChatPanel' -- packages/ui/src | wc -l`).
|
||||
5. MCP: **`rig_chatroom_send` + `rig_chatroom_watch`** (`mcp-server.ts:366`,
|
||||
`:402`).
|
||||
6. Source of truth: daemon-backed SQLite (`chat_messages` table), NOT tmux
|
||||
scrollback.
|
||||
|
||||
**ChatMessage** type (`chat-repository.ts:6`): durable rig-scoped message —
|
||||
`id`, `rigId`, `sender`, `kind`, `body`, `topic`, `createdAt`.
|
||||
|
||||
## 6. Compatibility notes
|
||||
|
||||
The intentional limits in this layer (numbered as in
|
||||
`architecture-rules-and-event-system.md`):
|
||||
|
||||
- Note 4 — the daemon's `rig ask` gathers context only; it does not call an
|
||||
external LLM. The agent reasons about the gathered evidence. Only the
|
||||
explicit `--wake` flag executes a runtime (§4).
|
||||
- Note 5 — transcript search prefers `rg` but falls back to `grep -E`;
|
||||
search quality/performance varies by backend.
|
||||
- Note 6 — chat is rig-scoped only: no cross-rig channels or DMs.
|
||||
- Note 7 — `--verify` on `rig send` reports what the pane shows, not agent
|
||||
acknowledgement. By default it compares the message (whole for a
|
||||
header-carrying send, the first 40 characters for a raw one) in captures
|
||||
before and after the send, leaving out the current input box; with
|
||||
`transport.verify_input_box` on, Claude Code and Codex sends are verified
|
||||
only for a seat proven idle before the paste and on input-box evidence; a
|
||||
busy or unknown seat reads unconfirmed (§3 step 5). When it cannot confirm,
|
||||
it reports `rendered-unconfirmed`, not a failure.
|
||||
|
||||
(The full compatibility-notes list lives in
|
||||
`architecture-rules-and-event-system.md`.)
|
||||
|
||||
## See also
|
||||
|
||||
- `daemon-core.md` — the route-mount surface these routes sit in, and the MCP
|
||||
tool count.
|
||||
- `agent-spec-and-startup.md` — the tmux `@rigged_*` metadata-key axis detail
|
||||
(whoami/adopt).
|
||||
- `lifecycle-snapshot-restore.md` — restore-side transcript boundary
|
||||
markers.
|
||||
- `coordination-primitive.md` — cross-host queue routing (the two-sided
|
||||
daemon-forward twin of §3b's one-sided CLI-direct verbs).
|
||||
- `../cli-reference.md` — each verb's registered `--host <id>` option; run
|
||||
`rig <verb> --help` for its cross-host behaviour.
|
||||
- Source roots: `packages/daemon/src/domain/{session-transport,transcript-store,transcript-rotation,history-query,ask-service,chat-repository}.ts`,
|
||||
`packages/daemon/src/routes/{transport,transcripts,ask,chat}.ts`,
|
||||
`packages/cli/src/mcp-server.ts`,
|
||||
`packages/cli/src/{remote-host-ops,cross-host-target,cross-host-executor,ask-wake}.ts`.
|
||||
@@ -1,326 +0,0 @@
|
||||
---
|
||||
kind: as-built
|
||||
title: Workflow Runtime — Specs, Packets, and Lifecycle Graphs
|
||||
status: active
|
||||
topics: [orchestration, coordination]
|
||||
domains: [engineering-advisor, operating-advisor]
|
||||
applies-when: |
|
||||
Following workflow validation, instantiation, projection, routing, failure
|
||||
recovery, lifecycle graph revisions, or the corresponding CLI/API.
|
||||
last-verified-against-source: e8f0ab340db773392ec8be75b072d1c0f3068a50
|
||||
last-updated: 2026-10-08
|
||||
---
|
||||
|
||||
# Workflow Runtime — Specs, Packets, and Lifecycle Graphs
|
||||
|
||||
Source snapshot: `e8f0ab340db773392ec8be75b072d1c0f3068a50`. This describes the source at that commit;
|
||||
it does not establish the version or behavior of a running daemon.
|
||||
|
||||
A workflow binds a specification to durable queue packets. The daemon
|
||||
records the instance, its current frontier, and the step trail; agents supply
|
||||
step outcomes and evidence. Serial workflows and dependency graphs share
|
||||
this runtime. The engine checks declared fields and recorded state; it does
|
||||
not judge the substance of a referenced proof document.
|
||||
|
||||
## Source map
|
||||
|
||||
All paths in this table are under `packages/daemon/src/`.
|
||||
|
||||
| File / symbol | Responsibility |
|
||||
|---|---|
|
||||
| [`domain/workflow-types.ts`](../../../packages/daemon/src/domain/workflow-types.ts) | Spec, instance, trail, frontier, and failure-occurrence contracts. |
|
||||
| [`WorkflowSpecCache`](../../../packages/daemon/src/domain/workflow-spec-cache.ts) | YAML parsing, source-hash cache, stored specs, diagnostics and retention. |
|
||||
| [`WorkflowValidator`](../../../packages/daemon/src/domain/workflow-validator.ts) | Roles, exits, routing/dependency edges, pins, cycles, and advisories. |
|
||||
| [`WorkflowRuntime`](../../../packages/daemon/src/domain/workflow-runtime.ts) | Instantiation, inspection, routing, resume, abort, and lifecycle facade. |
|
||||
| [`WorkflowProjector`](../../../packages/daemon/src/domain/workflow-projector.ts) | Close the current step and update its successor/frontier state. |
|
||||
| [`WorkflowInstanceStore`](../../../packages/daemon/src/domain/workflow-instance-store.ts) | Instances, guarded updates, frontier bindings, and failure occurrences. |
|
||||
| [`WorkflowStepTrailLog`](../../../packages/daemon/src/domain/workflow-step-trail-log.ts) | Append/read step history. |
|
||||
| [`workflow-reconciliation.ts`](../../../packages/daemon/src/domain/workflow-reconciliation.ts) | Inspect graph changes, apply explicit revisions, recover operation receipts. |
|
||||
| [`workflow-keepalive.ts`](../../../packages/daemon/src/domain/policies/workflow-keepalive.ts) | Watchdog evaluation and reminders. |
|
||||
|
||||
## Specification and cache
|
||||
|
||||
`parseWorkflowSpec` parses the `workflow` document and checks known key sets.
|
||||
`WorkflowValidator.validate` then checks relationships between roles, steps,
|
||||
exits, and targets. The exits are `handoff`, `waiting`, `done`, and `failed`.
|
||||
An exit-specific `next_hop.on` mapping takes precedence over structural
|
||||
routing; without a mapping, `resolveNextStep` considers `forbid`, suggested
|
||||
roles, `require`, and then the next array element.
|
||||
|
||||
The language includes `harness`, `host`, singular `gate`, `depends_on`, typed
|
||||
`acceptance`, and waiting re-presentation settings. At this pin,
|
||||
`WORKFLOW_AGENT_HARNESSES` contains only `claude-code` and `codex`; support for
|
||||
another runtime elsewhere in OpenRig does not add it to this pin's workflow
|
||||
harness grammar. Nonlocal host pins can validate as registered names but
|
||||
`instantiate` still rejects execution with `host_pin_remote_unsupported`.
|
||||
This is a workflow-step restriction, not a claim that all queue traffic is
|
||||
local-only.
|
||||
|
||||
A step whose `allowed_exits` can neither finish it nor route it on is refused as
|
||||
`step_cannot_finish` (`workflow-validator.ts:223-236`): its exits leave out `done`, none is
|
||||
mapped in `next_hop.on`, and it allows no `handoff` that routes on. In a dependency graph,
|
||||
allowing `handoff` is enough, because handoff completes a sink. `instantiate` refuses any spec
|
||||
with a validation error.
|
||||
|
||||
`allowed_exits` and `max_hops` affect validation/projection. In contrast,
|
||||
`continuation_required`, `preserve_lineage`, `closure_required`, closure
|
||||
messages, `skill_refs`, and `spawn_budget` produce
|
||||
`declared_not_enforced_v1` advisories. Legacy `gates: []` and
|
||||
`next_hop.mode: prefer` are rejected by the parser rather than silently
|
||||
accepted. Prerequisite cycles are rejected; a routing cycle requires a
|
||||
`max_hops` declaration and is distinct from a prerequisite cycle.
|
||||
`re_present_after_seconds` compiles to the queue's park timer on an unrouted
|
||||
`waiting` exit, and `re_present_max_seconds` turns it into a repeating
|
||||
reminder with backoff (`workflow-projector.ts:534`–`549`); a step that cannot
|
||||
reach `waiting`, or that maps `waiting` to a branch, fails validation
|
||||
(`waiting_re_presentation_unreachable`).
|
||||
|
||||
`WorkflowSpecCache.readThrough` parses the source file, hashes it, and
|
||||
updates the `(name, version)` cache row. Full `spec_json` preserves fields
|
||||
that the older scalar columns cannot represent. A same-name/same-version
|
||||
source edit can update that cache row; the version label alone is not an
|
||||
immutable content pin. `getByNameVersion` is the runtime lookup.
|
||||
Diagnostic-only rows, moved sources, and versions retained for unfinished
|
||||
work have separate paths in `readThrough`, `writeDiagnostic`, and
|
||||
`isPinnedByUnfinishedWork`.
|
||||
|
||||
Only instantiate (and `rig workflow run`, which posts to the same route) and
|
||||
the workflows-folder scan register specs through `readThrough`.
|
||||
`WorkflowRuntime.validate` (`rig workflow validate`, `POST /validate`) reads
|
||||
the file with `WorkflowSpecCache.readFromDisk`: the same file and parse
|
||||
checks, in memory, with no `workflow_specs` write. A spec that was only
|
||||
validated from outside the workflows folder is not listed by
|
||||
`rig workflow specs` and is not found by its bare name; its path still
|
||||
instantiates (#2726).
|
||||
|
||||
## Durable state
|
||||
|
||||
The migration files record the storage evolution; they are not a count of
|
||||
all database migrations:
|
||||
|
||||
| Migration | Workflow state introduced |
|
||||
|---|---|
|
||||
| [`033`](../../../packages/daemon/src/db/migrations/033_workflow_specs.ts), [`034`](../../../packages/daemon/src/db/migrations/034_workflow_instances.ts), [`035`](../../../packages/daemon/src/db/migrations/035_workflow_step_trails.ts) | Spec cache, instances/frontier/current step, and append-only trail API. |
|
||||
| [`040`](../../../packages/daemon/src/db/migrations/040_workflow_specs_diagnostic.ts), [`050`](../../../packages/daemon/src/db/migrations/050_workflow_spec_json.ts) | Cache diagnostics and complete parsed spec JSON. |
|
||||
| [`049`](../../../packages/daemon/src/db/migrations/049_workflow_instance_version.ts), [`051`](../../../packages/daemon/src/db/migrations/051_workflow_resume.ts), [`052`](../../../packages/daemon/src/db/migrations/052_workflow_instance_bound_rig.ts) | Optimistic version guard, resume/hop baseline, and bound rig. |
|
||||
| [`079`](../../../packages/daemon/src/db/migrations/079_workflow_lifecycle_parallel.ts) | Lifecycle operation key/digest/binding, packet-to-step frontier bindings, and failure occurrences. |
|
||||
|
||||
Current instance states include `active`, `waiting`, `completed`, `failed`,
|
||||
and `aborted`. A multi-packet frontier needs packet-to-step bindings; a
|
||||
single `current_step_id` cannot describe all parallel work.
|
||||
|
||||
## Instantiation and owner selection
|
||||
|
||||
`WorkflowRuntime.instantiate` resolves a cached spec name or explicit path,
|
||||
validates it, resolves the rig and the entry owner, then creates the instance
|
||||
and its single entry packet (for `steps[0]`) through the transactional
|
||||
event/queue path (`workflow-runtime.ts:433`, `:757`). The entry packet gets a
|
||||
best-effort post-commit nudge with no staged wake intent (`:843`–`845`); its
|
||||
keepalive, armed in the same transaction, and the boot sweep cover a lost
|
||||
entry nudge. An explicit
|
||||
unknown `targetRig` fails; an unknown spec-default rig produces an advisory
|
||||
and falls back to unbound operation. A bound rig without a required declared
|
||||
role is a different error.
|
||||
|
||||
`resolveDefaultOwner` uses declared `preferred_targets` first, applying a
|
||||
harness match when pinned. With no preferred targets and a bound rig, it uses
|
||||
[`selectRoleSeat`](../../../packages/daemon/src/domain/workflow-role-resolver.ts):
|
||||
a running managed agent with the declared role and required runtime, ordered
|
||||
by pending-only backlog and then canonical coordinate. Infrastructure and
|
||||
unresolvable/adopted coordinates do not become implicit replacement agents.
|
||||
Explicit owner overrides are reconciled with harness pins. Declared
|
||||
`preferred_targets` are not checked for liveness; validate and instantiate
|
||||
only warn (`role_no_live_preferred_target`) when none of a role's targets has
|
||||
a running session. A structured gate compiles by target kind (`compileGate`):
|
||||
for a human-seat target, the packet goes to the gated step's own owner and is
|
||||
parked `blocked_on` that human seat in the same transaction (summary and
|
||||
`evidence_ref` required), and the instance waits until the park is resolved;
|
||||
for a declared role, an ordinary packet goes to that handler role's seat
|
||||
(`workflow-projector.ts:1659`–`1778`).
|
||||
|
||||
A `preferred_targets` entry may end in `@{rig}`, for example `watcher@{rig}`.
|
||||
When the instance is bound to a rig (`--rig`, or the spec's `target.rig`
|
||||
default), the trailing `{rig}` becomes that rig, so one reusable spec addresses
|
||||
that rig's member, including a `builtin:terminal` member. Only a trailing
|
||||
`@{rig}` is the form: braces in the member part or a partial rig name
|
||||
(`ops-{rig}@rig-a`, `ops-watcher@prefix-{rig}`) stay literal. Literal
|
||||
`member@rig` entries are unchanged. With no bound rig the entry stays literal
|
||||
and is handled like any other destination, at the step that selects it: a
|
||||
selected `member@{rig}` is refused as an unknown destination rig
|
||||
(`unknown_destination_rig`), and `rig workflow validate` reports such an
|
||||
exception target as `unregistered`. A `{rig}` in a role that is never selected,
|
||||
or in a preferred target that is not selected, does not stop an instantiate.
|
||||
The cached spec keeps the literal `{rig}`; the expansion is computed from the
|
||||
instance's stored `bound_rig` each time its spec is read, so later hand-offs,
|
||||
resume and exception routing use the same target
|
||||
(`workflow-rig-placeholder.ts`, `WorkflowSpecCache.getForInstance`). A
|
||||
terminal member cannot act on a nudge, so it must poll its own queue for the
|
||||
packet.
|
||||
|
||||
Instantiation also emits advisories for missing members in otherwise
|
||||
registered target rigs. That member check is advisory and does not imply
|
||||
all preferred targets received a live-readiness test.
|
||||
|
||||
## Projection, waiting, and concurrency
|
||||
|
||||
`WorkflowProjector.project` checks active/waiting status, frontier membership,
|
||||
packet-to-step identity, allowed exits, and applicable evidence requirements.
|
||||
For a successful advance, queue closure, successor creation, trail entries,
|
||||
frontier/version updates, durable events, and wake intent staging share the
|
||||
transactional notification envelope. Terminal delivery happens after the
|
||||
commit, and the runtime awaits it, so project, route, resume and exception
|
||||
responses return after the delivery attempt; a terminal close without its
|
||||
successor's staged intent rolls back. A committed packet or staged wake is
|
||||
not evidence that its recipient consumed the message.
|
||||
|
||||
Each exit closes the packet through the queue's own closure path
|
||||
(`updateWithinTransaction`, hot-potato rules intact): `handoff` records
|
||||
`handed-off` with `handed_off_to` the next owner; `waiting` records `blocked`
|
||||
with `blocked_on` the supplied blocker, or `external-gate` when none is given;
|
||||
`done` records `done` with `no-follow-on`; and `failed` records `done` with
|
||||
`denied` and the result note as target (`workflow-projector.ts:1571`–`1631`).
|
||||
A dependency-graph handoff that fans out to more than one successor, or to
|
||||
none, closes the packet `done` with `no-follow-on`.
|
||||
|
||||
Both executors absorb an identical waiting replay by matching the stored
|
||||
closure intent; changed wait data is a new decision. A consumed terminal
|
||||
packet is no longer on the frontier and is refused. On the serial path a
|
||||
routed close moves the frontier to the new packet and leaves the instance
|
||||
`active`, or `waiting` when the target step is gated; an unrouted `waiting`
|
||||
keeps the closed packet on the frontier so the keepalive can find its owner;
|
||||
`failed` sets `failed`, and `done` with nothing left sets `completed`. A
|
||||
dependency graph is `waiting` while every frontier packet is blocked, and
|
||||
`failed` when the frontier empties with an unresolved occurrence.
|
||||
`max_hops` constrains routing relative to the current drive's baseline: a
|
||||
route that would exceed it becomes a `failed` exit with a `max_hops_exceeded`
|
||||
note and evidence, the packet closes and no successor is created
|
||||
(`workflow-projector.ts:419`–`443`); resume establishes a new bounded drive
|
||||
rather than erasing history.
|
||||
|
||||
Presence of `depends_on` selects `projectDependencyGraph`. It uses durable
|
||||
packet bindings to advance eligible successors and record per-occurrence
|
||||
failures without treating an unrelated live branch as completed. A step's
|
||||
completion counts only while it is later, in trail append order, than its
|
||||
prerequisites' latest completions, so a prerequisite re-run through a routed
|
||||
exit sends its dependents round again (`workflow-projector.ts:1004`–`1019`).
|
||||
Inspection reports unknown bindings rather than inventing a step from trail order.
|
||||
|
||||
The injected [`createWorkflowFrontierPredicate`](../../../packages/daemon/src/domain/workflow-frontier-guard.ts)
|
||||
lets the queue refuse ordinary terminal closure of live workflow packets.
|
||||
Workflow mutations use the explicit workflow path; the queue does not need
|
||||
to import the workflow domain to evaluate that predicate.
|
||||
|
||||
## Recovery operations
|
||||
|
||||
`WorkflowRuntime` distinguishes these operations:
|
||||
|
||||
| Operation | Meaning |
|
||||
|---|---|
|
||||
| `route` | Replace a current packet's owner while retaining the step. This is not step advancement. Multiple frontier packets require an explicit packet selection. |
|
||||
| `resume` | Redrive failed work to a newly resolved owner, recording the optional `--decision` text. A serial instance must be `failed` (`instance_not_failed`). Resume re-resolves the owner rather than copying it, re-bases `hops_baseline`, increments `resume_count`, and closes that occurrence's open exception items. Dependency failures use occurrence identity; multiple unresolved failures require selection. Identical occurrence/decision replay returns the recorded redrive, while changed decision bytes conflict. |
|
||||
| `abort` | Explicitly stop unfinished work with an actor and reason: cancels every frontier packet, records a `failed` trail row per packet, sets `aborted`, and emits `workflow.failed` with reason `aborted: <reason>`. Refused only for `completed` or `aborted` instances. |
|
||||
| `continue` | Read-only inspection of instance, trail, guidance, reconciliation, frontier, failures, unknowns, and boundary obligations. It does not mechanically advance a step. |
|
||||
|
||||
## Deadlines and exceptions
|
||||
|
||||
[`evaluateStepDeadline`](../../../packages/daemon/src/domain/workflow-deadline.ts)
|
||||
derives overdue state from frontier queue rows. Claimed work uses its closure
|
||||
deadline, otherwise its claim time plus **4 hours** (creation time if the
|
||||
claim time is absent). Pending work uses creation time plus **4 hours**.
|
||||
Blocked waits are not overdue under this evaluator. This is a diagnostic,
|
||||
not a substitute for packet existence or binding checks.
|
||||
|
||||
Instantiate and every routed projection arm a `workflow-keepalive` watchdog
|
||||
job in the same transaction (one per live packet for dependency graphs, one
|
||||
per instance for serial specs), evaluated every 15 minutes and quiet until the
|
||||
step is overdue (`workflow-keepalive-arming.ts:38`). At each daemon start the
|
||||
boot sweep re-arms keepalives for live instances, re-nudges pending frontier
|
||||
packets that were never nudged, and surfaces overdue instances
|
||||
(`workflow-boot-sweep.ts`). Neither path advances a step.
|
||||
|
||||
There are three exception classes: `unmapped_failed`, `stuck_overdue` and
|
||||
`human_gate_trip` (`workflow-exception.ts:34`–`38`). An unrouted `failed`
|
||||
exit creates one urgent `workflow-exception` queue item in the same
|
||||
transaction, with evidence `rig workflow trace <instance>` and a body naming
|
||||
the `rig workflow resume` command; a human gate's own parked packet carries the
|
||||
`human_gate_trip` identity, so no second item is created.
|
||||
|
||||
[`resolveExceptionRoute`](../../../packages/daemon/src/domain/workflow-exception-router.ts)
|
||||
selects per-class configuration, then workflow default, host default, and
|
||||
finally orchestrator-first behavior. An unresolved orchestrator route falls
|
||||
back to registered-human selection. A human gate is intrinsically human
|
||||
routed. Agent-routed exceptions use the ordinary workflow tier; human-routed
|
||||
ones use `human-gate`.
|
||||
|
||||
[`workflow-human-destination.ts`](../../../packages/daemon/src/domain/workflow-human-destination.ts)
|
||||
handles human selection: the registered human named by
|
||||
`workspace.operator_seat_name`, or the only registered human when it is unset;
|
||||
any other case fails with `workflow_human_destination_unavailable` (409). The
|
||||
host default dial is the setting `workflow.exception_routing`
|
||||
(`orchestrator` or `human_only`), read per exception. A valid agent route need
|
||||
not have a human fallback configured. If fallback is needed, missing/ambiguous selection is an explicit
|
||||
error, not an invented destination. Failure admission is transactional;
|
||||
overdue detection is handled by boot/keepalive paths. Runtime reconciliation
|
||||
closes recovered overdue occurrences against their own packets, preserving
|
||||
unresolved siblings and unknown provenance.
|
||||
|
||||
## Project lifecycle graphs
|
||||
|
||||
`compileLifecycle` delegates to
|
||||
[`compileProjectLifecycle`](../../../packages/daemon/src/domain/project-lifecycle-compiler.ts).
|
||||
`instantiateLifecycle` requires an operation key and compiled input digest.
|
||||
An exact replay retrieves the recorded instance/entry packet; changed input
|
||||
under the same key conflicts. Compilation and an eligible result do not by
|
||||
themselves launch work.
|
||||
|
||||
`inspectGraph`, `reviseGraph`, and `recoverGraphOperation` in
|
||||
[`workflow-reconciliation.ts`](../../../packages/daemon/src/domain/workflow-reconciliation.ts)
|
||||
separate inspection from explicit graph revision and operation recovery.
|
||||
Inspection reports `current`, `source-only`, `compatible`, `incompatible`,
|
||||
`unavailable` or `unbound`. Only a live, manifest-bound instance whose specs
|
||||
are explicit dependency graphs without `next_hop.on` can be revised; a
|
||||
revision may add steps and change unstarted ones, but may not remove steps,
|
||||
touch a step with completed, live or failed work, drop a required obligation
|
||||
or its order, change workflow-wide fields other than `exception_routing`, or
|
||||
add an unstarted step with no outstanding prerequisite. A changed
|
||||
`exception_routing` applies to future occurrences only; existing obligations
|
||||
keep their owners. Applying needs the inspected version and digest, an
|
||||
operation key, an actor and a reason; the receipt is kept in the instance's
|
||||
`revisionHistory`, and replaying the same key with a different decision is
|
||||
refused with `lifecycle_revision_conflict` (`workflow-reconciliation.ts:12`,
|
||||
`:113`–`223`).
|
||||
`guidance` reads context and current packet information without interpreting
|
||||
an agent's substantive judgment.
|
||||
|
||||
For lifecycle steps that require a receipt, a `done`/`handoff` projection
|
||||
requires an `evidence_ref`. Typed acceptance gates additionally compare the
|
||||
submitted candidate, allowed verdict, and evidence reference against the
|
||||
step declaration. These are mechanical identity/presence checks; the
|
||||
projector does not read the referenced prose to decide whether the work is
|
||||
good or ready to publish.
|
||||
|
||||
## CLI and HTTP surface
|
||||
|
||||
The [CLI inventory](../cli-reference.md#workflow) lists every registered
|
||||
`rig workflow` subcommand and option. The implementation is
|
||||
[`packages/cli/src/commands/workflow.ts`](../../../packages/cli/src/commands/workflow.ts).
|
||||
The [`workflow` routes](../../../packages/daemon/src/routes/workflow.ts)
|
||||
are mounted under `/api/workflow`:
|
||||
|
||||
| Method | Suffixes |
|
||||
|---|---|
|
||||
| `POST` | `/validate`, `/compile`, `/instantiate`, `/instantiate-lifecycle`, `/project` |
|
||||
| `GET` | `/list`, `/specs`, `/operations/:key`, `/:instance_id`, `/:instance_id/trace`, `/:instance_id/guidance`, `/:instance_id/revision` |
|
||||
| `POST` | `/:instance_id/route`, `/:instance_id/resume`, `/:instance_id/abort`, `/:instance_id/continue`, `/:instance_id/revision` |
|
||||
| `GET` | `/sse`, `/watch` (same live event stream handler) |
|
||||
|
||||
The stream carries the 8 `workflow.*` events (`instantiated`, `step_closed`,
|
||||
`next_qitem_projected`, `completed`, `failed`, `resumed`,
|
||||
`routing_table_changed`, `revised`); it is not a general queue stream or a
|
||||
stored-history replay. Route error mapping distinguishes missing
|
||||
records, authoring errors, state/version conflicts, and internal failures.
|
||||
See the source mapping before assigning a meaning to a nonzero CLI result.
|
||||
|
||||
For authoring examples and project-level boundaries, use
|
||||
[product journey SDLC](../../reference/product-journey-sdlc.md) and
|
||||
[release boundaries](../../reference/release-boundary.md).
|
||||
@@ -1,606 +0,0 @@
|
||||
---
|
||||
kind: as-built
|
||||
title: Workspace Primitive — RigSpec.workspace, Migrations 038/039, Missions/Projects/Slices
|
||||
status: active
|
||||
topics: [specification-and-bundles, observability]
|
||||
domains: [engineering-advisor, operating-advisor]
|
||||
applies-when: |
|
||||
Need to know how a rig declares a typed workspace (workspaceRoot / repos /
|
||||
defaultRepo / knowledgeRoot), how that block is persisted and resolved into
|
||||
whoami / node-inventory, how per-item target_repo scope is validated, or how
|
||||
the file-backed missions/slices tree is indexed and projected into the
|
||||
Project UI.
|
||||
siblings: [content-surfaces.md, daemon-core.md, ../ui/project-and-for-you.md]
|
||||
prerequisite-reads: [../README.md, daemon-core.md]
|
||||
last-verified-against-source: e8f0ab340db773392ec8be75b072d1c0f3068a50
|
||||
last-updated: 2026-10-08
|
||||
---
|
||||
|
||||
# Workspace Primitive — RigSpec.workspace, Migrations 038/039, Missions/Projects/Slices
|
||||
|
||||
The **workspace primitive** is the typed declaration that lets a rig name
|
||||
*where its work lives* — a workspace root, a set of named repos with kinds, an
|
||||
optional default repo, and an optional knowledge root — and have that surface
|
||||
through `whoami` / node-inventory and gate per-item repo scope. Alongside it, a
|
||||
**file-backed missions/slices tree** is indexed read-only and projected into
|
||||
the Project UI's mission / slice surfaces.
|
||||
|
||||
> Paths are relative to `packages/daemon/src/` unless prefixed `packages/` or
|
||||
> `docs/`. Verified against source at main `e8f0ab340db773392ec8be75b072d1c0f3068a50`. Each count sits beside
|
||||
> the command that produces it; run the commands from the repository root.
|
||||
> The files / markdown / progress / steering surfaces are in the sibling
|
||||
> `content-surfaces.md`.
|
||||
|
||||
## 0. Release attribution
|
||||
|
||||
Checked with `git cat-file -e <tag>:packages/daemon/src/<path>`. The table
|
||||
records when each file first appeared; most have changed since.
|
||||
|
||||
| Subsystem | First release | Check |
|
||||
|---|---|---|
|
||||
| `domain/workspace/{workspace-resolver,frontmatter-validator,default-workspace-scaffold}.ts` | **≤0.3.0** | present at `v0.3.0` |
|
||||
| migrations `038_workspace_primitive.ts`, `039_queue_target_repo.ts` | **≤0.3.0** | present at `v0.3.0` |
|
||||
| `domain/slices/{slice-indexer,slice-detail-projector}.ts` | **≤0.3.0** | present at `v0.3.0` |
|
||||
| `routes/{workspace,slices,projects}.ts` | **≤0.3.0** | present at `v0.3.0` |
|
||||
| `domain/workspace/getting-started-narrative.ts` | **0.3.1** | absent at `v0.3.0`; present at `v0.3.1` |
|
||||
| `routes/missions.ts` | **0.3.1** | absent at `v0.3.0`; present at `v0.3.1` |
|
||||
| `domain/workspace/workspace-doctor.ts` | **0.3.2** | absent at `v0.3.1`; present at `v0.3.2` |
|
||||
|
||||
The workspace primitive itself (declaration, resolver, validator, migrations
|
||||
038/039) is a **0.3.0** feature — do not back-attribute it to 0.3.1. The
|
||||
`missions` route and the getting-started narrative are **0.3.1** additions;
|
||||
the workspace doctor is **0.3.2**.
|
||||
|
||||
`getting-started-narrative.ts` is still in the tree, but nothing imports it:
|
||||
**0** files reference it (`git grep -l 'workspace/getting-started-narrative' -- packages | wc -l`).
|
||||
Workspace initialization no longer seeds a getting-started mission (§3).
|
||||
|
||||
## 1. The typed workspace declaration — 0.3.0
|
||||
|
||||
`RigSpec.workspace` is an **optional** typed block. Rigs without it stay valid;
|
||||
`whoami` / node-inventory return a null workspace block in that case.
|
||||
|
||||
> Source: `domain/types.ts:1163–1170` (`WorkspaceSpec`), `:1153–1159`
|
||||
> (`WorkspaceRepoSpec`), `:1188` (`RigSpec.workspace?`).
|
||||
|
||||
| Field | Shape | Notes |
|
||||
|---|---|---|
|
||||
| `workspaceRoot` | string | Verbatim from spec |
|
||||
| `repos[]` | `{ name, path, kind }[]` | `path` resolved to absolute at parse time (`domain/rigspec-schema.ts:361–363`); authors may declare it relative to `workspaceRoot` in YAML |
|
||||
| `defaultRepo?` | string | active repo when no env override / cwd match |
|
||||
| `knowledgeRoot?` | string | treated as `kind=knowledge` when surfaced |
|
||||
|
||||
`WorkspaceKind` is a closed union of **5** members
|
||||
(`grep '^export const WORKSPACE_KINDS' packages/daemon/src/domain/types.ts | grep -o '"[a-z]*"' | wc -l`):
|
||||
`user`, `project`, `knowledge`, `lab`, `delivery`.
|
||||
|
||||
> Source: `domain/types.ts:1149–1150` (`WORKSPACE_KINDS` / `WorkspaceKind`).
|
||||
|
||||
In YAML the block is spelled `workspace: { workspace_root, repos: [{ name,
|
||||
path, kind }], default_repo?, knowledge_root? }`. Validation requires a
|
||||
non-empty `workspace_root`, a `repos` array whose names are unique and whose
|
||||
`kind` is one of the five kinds, a `default_repo` that names a declared repo,
|
||||
and a non-empty `knowledge_root` when present.
|
||||
|
||||
> Source: `domain/rigspec-schema.ts:288–347`.
|
||||
|
||||
### 1.1 Persistence — migration 038 + RigRepository
|
||||
|
||||
Migration **038** adds `workspace_json TEXT` to the `rigs` table. It holds the
|
||||
typed `RigSpec.workspace` block as JSON when a rig declares one; NULL for rigs
|
||||
without a workspace block.
|
||||
|
||||
> Source: `db/migrations/038_workspace_primitive.ts:16–21`
|
||||
> (`ALTER TABLE rigs ADD COLUMN workspace_json TEXT` at `:19`); doc comment
|
||||
> `:3–15`.
|
||||
|
||||
`RigRepository.setRigWorkspace(rigId, workspace)` persists it (UPDATE
|
||||
`workspace_json` + `updated_at`); `getRigWorkspace(rigId)` reads it back,
|
||||
JSON-parsing to `WorkspaceSpec` and returning `null` on parse failure. Both
|
||||
are **defensive no-ops** when the column is absent (`hasRigColumn` probe) —
|
||||
older test fixtures that bypass the canonical migration list don't have the
|
||||
column; the setter's contract is "best-effort persistence".
|
||||
|
||||
> Source: `domain/rig-repository.ts:184–189` (setter; column probe `:185`),
|
||||
> `:192–202` (getter; probe `:193`; parse failure → null `:199–201`);
|
||||
> `hasRigColumn` `:347`.
|
||||
|
||||
The block is written only when declared. **3** call sites write it
|
||||
(`git grep -n 'setRigWorkspace(' -- packages/daemon/src | grep -v 'rig-repository.ts' | wc -l`):
|
||||
the instantiator's two rig-persist paths, `materializeValidatedSpec` and
|
||||
`instantiateOnce`, each guarded by `if (rigSpec.workspace)`; and the
|
||||
workspace-only import route `POST /api/rigs/import/workspace`
|
||||
(`rig import --workspace-only --target-rig <rigId>`), which validates and
|
||||
normalizes the block and writes it only when it differs from the stored one.
|
||||
|
||||
> Source: `domain/rigspec-instantiator.ts:619` (`materializeValidatedSpec`),
|
||||
> `:655–657`; `:1260` (`instantiateOnce`), `:1349–1351`;
|
||||
> `routes/rigspec.ts:134–173` (validation `:158`; compare-and-write
|
||||
> `:168–170`), mounted `server.ts:734`; CLI
|
||||
> `packages/cli/src/commands/import.ts:82`.
|
||||
|
||||
### 1.2 Runtime resolution — workspace-resolver
|
||||
|
||||
`resolveWorkspaceContext({ spec, cwd, envOverride })` (consumed by
|
||||
`whoami-service`) returns the `WhoamiWorkspaceBlock` or `null` when no spec.
|
||||
`activeRepo` resolution: a non-blank `envOverride` wins **verbatim** — even an
|
||||
unknown repo name is honored, because operators set `OPENRIG_TARGET_REPO`
|
||||
consciously (the comment at `workspace-resolver.ts:35–36`); otherwise `defaultRepo` is used **only
|
||||
if it names a declared repo**. `knowledgeKind` is `"knowledge"` when
|
||||
`knowledgeRoot` is declared, else `null`. The resolver accepts `cwd` but does
|
||||
not use it (`workspace-resolver.ts:31`), so `rig whoami` reports the env
|
||||
override or the declared `defaultRepo`, never the repo containing the seat's
|
||||
working directory; node-inventory's per-node `activeRepo` (below) is the one
|
||||
that follows cwd.
|
||||
|
||||
> Source: `domain/workspace/workspace-resolver.ts:26–52` (resolver; null
|
||||
> without a spec `:32`; env override `:39–40`; declared `defaultRepo`
|
||||
> `:41–42`; `knowledgeKind` `:50`).
|
||||
|
||||
`whoami-service` reads the persisted spec and resolves with the query's
|
||||
`targetRepoOverride`, falling back to the daemon's own
|
||||
`process.env["OPENRIG_TARGET_REPO"]`. `rig whoami` forwards the caller's
|
||||
`OPENRIG_TARGET_REPO` as `?targetRepo=`, which the route passes on as
|
||||
`targetRepoOverride`.
|
||||
|
||||
> Source: `domain/whoami-service.ts:333–338` (`getRigWorkspace` +
|
||||
> `resolveWorkspaceContext`, env fallback `:337`); `whoami` returns
|
||||
> `workspace` in its payload `:350`; `routes/whoami.ts:29`;
|
||||
> `packages/cli/src/commands/whoami.ts:281–282`.
|
||||
|
||||
`resolveNodeWorkspace({ spec, cwd })` (consumed by `node-inventory`) derives a
|
||||
per-node `NodeWorkspaceInfo` by picking the **longest declared repo path that
|
||||
contains** the node's `cwd`. When no repo contains it, `kind` falls back to
|
||||
`knowledge` if cwd is under `knowledgeRoot`; `activeRepo` then falls back to
|
||||
the rig's `defaultRepo` (taking that repo's kind if `kind` is still unset).
|
||||
Containment uses a `path.relative` boundary check (not a string `startsWith`)
|
||||
so `/foo/bar` does not match `/foo/bar-other`. Node-inventory reads
|
||||
`workspace_json` with its own queries rather than through `RigRepository`.
|
||||
|
||||
> Source: `domain/workspace/workspace-resolver.ts:57–95` (longest match
|
||||
> `:67–73`; knowledge fallback `:77–79`; default-repo fallback `:82–88`),
|
||||
> `isInside` `:97–103`; `domain/node-inventory.ts:705`
|
||||
> (`workspace: resolveNodeWorkspace(...)`), readers `:790–799`
|
||||
> (`readRigWorkspaceJson`) and `:716–726` (`readAllRigWorkspaceJson`,
|
||||
> batched); `NodeWorkspaceInfo` `domain/types.ts:795–799`.
|
||||
|
||||
### 1.3 Per-item repo scope — migration 039 + queue validation
|
||||
|
||||
Migration **039** adds `target_repo TEXT` to `queue_items` plus
|
||||
`idx_queue_items_target_repo`. Per the migration's comment it carries the
|
||||
per-item typed repo scope when an operator passes `--target-repo <name>`; it
|
||||
is NULL when the item is unambiguous against the rig's `default_repo` or no
|
||||
workspace is declared, and Mission Control views surface the field for
|
||||
cross-rig handoff clarity. At M nothing in the daemon's Mission Control code
|
||||
reads it; the UI shows it as "Target repo" in the queue item viewer's full
|
||||
detail (`packages/ui/src/components/drawer-viewers/QueueItemViewer.tsx:157`).
|
||||
|
||||
> Source: `db/migrations/039_queue_target_repo.ts:16–22`
|
||||
> (`ALTER TABLE queue_items ADD COLUMN target_repo TEXT` at `:19`;
|
||||
> `CREATE INDEX … idx_queue_items_target_repo` at `039_queue_target_repo.ts:20`); doc comment
|
||||
> `:3–15`; CLI flag `packages/cli/src/commands/queue.ts:461`.
|
||||
|
||||
`validateTargetRepo` checks an explicit `targetRepo` against the **source
|
||||
rig's** `RigSpec.workspace.repos[]`: it parses the source session with
|
||||
`parseSessionName`, looks the rig up by name, reads `getRigWorkspace`, and
|
||||
rejects an unknown repo with 400 `unknown_target_repo` plus the `knownRepos`
|
||||
list. It **fails open** (`{ ok: true }`) when there is no rigRepo, the session
|
||||
is not a canonical `<member>@<rig>` name, the rig is unknown, or no workspace
|
||||
is declared — validation only bites when a workspace actively declares repos.
|
||||
|
||||
It runs on **3** routes (`grep -c 'validateTargetRepo(c,' packages/daemon/src/routes/queue.ts`):
|
||||
`POST /create` (through the shared `createInputRefusal` check, which also
|
||||
requires `destinationSession` and `body`), `POST /:qitemId/handoff` and
|
||||
`POST /:qitemId/handoff-and-complete`, each before any cross-host forwarding,
|
||||
because the source rig's workspace lives on the source host.
|
||||
`createInputRefusal` also runs in the in-process create that finishes a
|
||||
handoff an earlier self-forward started (a `hostId` equal to this daemon's own
|
||||
id). A handoff that inherits the source row's `targetRepo` without an override
|
||||
is not re-validated.
|
||||
|
||||
`--target-repo` is accepted by `rig queue create`, `handoff` and
|
||||
`handoff-and-complete`. `rig queue list --target-repo <name>`
|
||||
(`GET /api/queue/list?targetRepo=`) filters rows by exact `target_repo`
|
||||
match; the filter is skipped when the column is absent.
|
||||
|
||||
> Source: `routes/queue.ts:96–123` (fail-open guards `:102`, `:106`, `:109`,
|
||||
> `:112`; `getRigWorkspace` `:111`; `unknown_target_repo` + `knownRepos`
|
||||
> `:114–120`); `createInputRefusal` `:128–141` (call `:136`), used by
|
||||
> `/create` `:490` and the self-forward create `:436`; handoff calls `:679`,
|
||||
> `:758` in handlers `:650`, `:732` (`/create` handler `:450`); ordering note
|
||||
> `:482–489`; inherited value `:676–677`, `domain/queue-repository.ts:1691`,
|
||||
> `:1868`; list filter `routes/queue.ts:906`,
|
||||
> `domain/queue-repository.ts:3236–3238`, CLI
|
||||
> `packages/cli/src/commands/queue.ts:1143`; mounted `server.ts:784`.
|
||||
|
||||
## 2. Workspace HTTP route — frontmatter validator and doctor
|
||||
|
||||
`workspaceRoutes()` (mounted `server.ts:785` as `/api/workspace`) exposes **2**
|
||||
endpoints (`grep -c -E 'app\.(get|post)\(' packages/daemon/src/routes/workspace.ts`).
|
||||
**Neither mutates the filesystem.**
|
||||
|
||||
- `POST /api/workspace/validate` — body `{ root, workspaceKind?, recursive?,
|
||||
requireFrontmatter?, maxFiles? }`; returns a `FrontmatterValidationReport`.
|
||||
`root` is required (400 `root_required`); an out-of-vocabulary
|
||||
`workspaceKind` is rejected 400 `invalid_workspace_kind`; validator throws →
|
||||
500 `validate_failed`.
|
||||
- `POST /api/workspace/doctor` — runs `runWorkspaceDoctor` against the
|
||||
daemon's resolved `workspace.root`, or a `workspaceRoot` given in the body,
|
||||
and returns its report. 503 `settings_unavailable` without a settings
|
||||
store; 500 `doctor_failed` when it throws. The doctor runs **8** checks in
|
||||
order (root reachable, missions folder, file allowlist, daemon points at
|
||||
this workspace, config changed since start, optional slice docs, mission
|
||||
notes, SDLC convention sections), each `ok`/`warn`/`fail` with a fix hint,
|
||||
plus a summary count. With a body `workspaceRoot`, missions are checked at
|
||||
`<workspaceRoot>/missions`. (The orchestrator's own doc comment still says
|
||||
7.)
|
||||
|
||||
> Source: `routes/workspace.ts:16` (no mutation), `:37–74` (validate;
|
||||
> root-required `:46–48`; kind check `:50–58`; 500 `:72`), `:85–160`
|
||||
> (doctor; 503 `:87`; missions root from the body `:115–118`;
|
||||
> `runWorkspaceDoctor` call `:145`; 500 `:158`);
|
||||
> `domain/workspace/workspace-doctor.ts:643–685` (the 8 checks; comment
|
||||
> `:636`); mounted `server.ts:785`.
|
||||
|
||||
`validateWorkspaceFrontmatter()` walks a root, parses each `.md` file's YAML
|
||||
frontmatter (delimited `---` on the first line), and emits a structured gap
|
||||
report — **advisory only, never modifies files**. There are **4** gap kinds
|
||||
(`sed -n '/^export type FrontmatterGapKind/,/;$/p' packages/daemon/src/domain/workspace/frontmatter-validator.ts | grep -c '^ | '`):
|
||||
`missing-required-field`, `unrecognized-status-value`, `parse-error`,
|
||||
`missing-frontmatter`. Per-kind required fields (checked only when a kind is
|
||||
given): `user`/`project` → `["doc"]`; `knowledge`/`lab`/`delivery` →
|
||||
`["doc","status","created","owner"]`. The `status` enum has **4** values
|
||||
(`grep '^const VALID_STATUS_VALUES' packages/daemon/src/domain/workspace/frontmatter-validator.ts | grep -o '"[a-z]*"' | wc -l`):
|
||||
`active|draft|archived|superseded`. Default behavior skips files without
|
||||
`---` silently (informal notes) unless `requireFrontmatter`; recurses by
|
||||
default; skips `node_modules` / `.git` / `.worktrees` / `dist` / `build`;
|
||||
hard cap `maxFiles` default 10000.
|
||||
|
||||
> Source: `domain/workspace/frontmatter-validator.ts:12` (advisory), `:23–27`
|
||||
> (gap kinds), `:53` (status enum), `:56–62` (per-kind required), `:81–83`
|
||||
> (defaults), `:90–121` (walk; skip-dirs `:105–111`), `:160–172`
|
||||
> (missing-frontmatter behavior), `:201–226` (required + status check), `:239`
|
||||
> (opening delimiter).
|
||||
|
||||
CLI surface: `rig workspace validate [root]` and `rig workspace doctor`.
|
||||
|
||||
> Source: `packages/cli/src/commands/workspace.ts:86` (`validate`), `:146`
|
||||
> (`doctor`); `docs/as-built/cli-reference.md#workspace`.
|
||||
|
||||
## 3. Default project-workspace scaffold
|
||||
|
||||
`workspaceScaffoldDirs()` / `workspaceScaffoldFiles()` produce the repo-ready
|
||||
default workspace (`~/.openrig/workspace/`, the default `workspace.root`, or
|
||||
`--root`). It emits exactly **2** directories
|
||||
(`grep '^const WORKSPACE_DIRS' packages/daemon/src/domain/workspace/default-workspace-scaffold.ts | grep -o '"[a-z]*"' | wc -l`):
|
||||
`missions/`, `exhaust/`; and **4** files
|
||||
(`sed -n '/^export function workspaceScaffoldFiles/,/^}/p' packages/daemon/src/domain/workspace/default-workspace-scaffold.ts | grep -c 'relPath: "'`):
|
||||
`SPEC.md`, `project.yaml`, `workspace.yaml`, `.gitignore`. The catalog
|
||||
(`workspace.yaml`) points its single `default` project at `.`; `project.yaml`
|
||||
names `SPEC.md` as project intent, roots mission discovery at `missions`, and
|
||||
exposes empty `install.context` / `install.skills` selectors. The ignore file
|
||||
excludes `exhaust/` and local `.openrig/` projection state while keeping
|
||||
authored project context versionable.
|
||||
|
||||
`ensureDefaultWorkspace()` applies the scaffold additively: it creates only
|
||||
missing paths and never deletes or overwrites an existing file. It checks
|
||||
every managed path first; if one exists with the wrong kind (a file where a
|
||||
directory belongs, or the reverse) it writes nothing and returns `ok: false`
|
||||
with the conflicts. Its callers are the shared instance initializer
|
||||
`ensureOpenRigInstance()`, which both CLI daemon start and direct daemon start
|
||||
run (so the scaffold is reconciled on every start); `rig config
|
||||
init-workspace`; and `POST /api/config/init-workspace` (409
|
||||
`init_workspace_conflict`). The retained `--force` spelling is a
|
||||
compatibility no-op. The CLI command re-exports the daemon's owner from
|
||||
`@openrig/daemon/instance-initialization` rather than keeping its own copy,
|
||||
and a parity test pins the CLI and daemon layouts as byte-identical. Mission
|
||||
and slice content is created explicitly through `rig scope`, not seeded by
|
||||
workspace initialization. Instance context, System World, skill source,
|
||||
runtime state, and the retired `artifacts/`, `evidence/`, `progress/`,
|
||||
`field-notes/`, `dogfood-evidence/`, `README.md`, and `STEERING.md` entries
|
||||
are not emitted.
|
||||
|
||||
> Source: `domain/workspace/default-workspace-scaffold.ts:7` (dirs),
|
||||
> `:20–30` (`project.yaml`), `:32–36` (`workspace.yaml`), `:38–41`
|
||||
> (`.gitignore`), `:43–54` (`workspaceScaffoldDirs`,
|
||||
> `workspaceScaffoldFiles`), `:100–153` (`ensureDefaultWorkspace`; conflicts
|
||||
> checked before writes `:117–135`; write-if-missing `:140`);
|
||||
> `domain/instance-initialization.ts:39` (`ensureOpenRigInstance`; default
|
||||
> workspace root `:44`; dry-run plan `:83`; apply `:103`); default
|
||||
> `workspace.root` `domain/user-settings/settings-store.ts:28–31`; daemon
|
||||
> `index.ts:250`; CLI `packages/cli/src/daemon-lifecycle.ts:658`;
|
||||
> `packages/cli/src/commands/config-init-workspace.ts:10` (import), `:13`
|
||||
> (re-export), `:28` (`--force`), `:56–67` (`runInitWorkspace`);
|
||||
> `routes/config.ts:56–65`, mounted `server.ts:772`; `rig scope`
|
||||
> `packages/cli/src/commands/scope.ts:2071`; parity
|
||||
> `packages/daemon/test/getting-started-narrative-parity.test.ts:15–25`,
|
||||
> retired entries `:43–57`.
|
||||
|
||||
### 3.1 The workspace catalog and project selection
|
||||
|
||||
`workspace.yaml` at `workspace.catalog_path` (default
|
||||
`<workspace.root>/workspace.yaml`) lists `projects[]`, each with a string `id`,
|
||||
a `root` relative to the catalog file, and an optional `rigs` list naming the
|
||||
rigs that work in that project. With one entry, that project is selected
|
||||
implicitly. With several, `rig context work-install` and the daemon's
|
||||
operating posture use an explicit `--project` first, then the calling rig's
|
||||
`rigs` association, then the unique deepest project root that contains the
|
||||
working directory, then the only entry no rig claims; otherwise they stop with
|
||||
`project_required` and the candidate ids. Duplicate ids are refused
|
||||
(`project_identity_ambiguous`), and a root that does not exist gives
|
||||
`project_root_missing`.
|
||||
|
||||
Installing a bundle that carries a project places it at
|
||||
`<workspace.projects_root>/<id>/` (default `<workspace.root>/projects`) and
|
||||
records it in the catalog before the rig's seats launch: a new entry is
|
||||
appended as text, or the rig is added to an entry's `rigs` list. Existing
|
||||
catalog bytes are never rewritten; a conflict writes nothing for that part and
|
||||
reports the exact line to add.
|
||||
|
||||
Daemon reads accept `?project=<id>`, resolved by `selectedProject()`: the id
|
||||
must be in the catalog (or be the single uncatalogued workspace project, whose
|
||||
id comes from its `project.yaml` or defaults to `workspace`), the project's
|
||||
`project.yaml` `missions.root` must stay inside the project, and a supplied
|
||||
`?projectRoot` must still match. Every failure answers 409 with its code.
|
||||
`GET /api/scopes/projects` lists the projects with their missions roots.
|
||||
|
||||
> Source: `domain/workspace/project-catalog.ts:16–25` (`readProjectCatalog`),
|
||||
> `:26–37` (`selectCatalogProject`), `:66–79` (`inferCatalogProject`, steps
|
||||
> 3–5); `domain/user-settings/settings-store.ts:489–490`
|
||||
> (`workspace.projects_root`, `workspace.catalog_path`);
|
||||
> `domain/workspace/project-registration.ts:6–29` (rules), `:36–37`
|
||||
> (projects root), `:181` (`registerBundleProject`), called from
|
||||
> `domain/bundle-content-routing.ts:184`; `domain/workspace/project-read.ts:44–60`
|
||||
> (`projectEntry`), `:61–69` (`listProjects`), `:70–81` (`selectedProject`),
|
||||
> `:83–84` (409); `routes/scopes.ts:34`, mounted `server.ts:820`.
|
||||
|
||||
## 4. File-backed missions / slices tree
|
||||
|
||||
### 4.1 SliceIndexer — 0.3.0
|
||||
|
||||
`SliceIndexer` reads slice folders from configured filesystem roots. The
|
||||
**default workspace contract** is `workspace/missions/<mission>/slices/<slice>`;
|
||||
flat roots (`workspace/slices/<slice>`) remain supported for compatibility.
|
||||
A nested `slices/` child folder makes the parent a mission
|
||||
(`missionId = <mission-folder>`); a bare folder is a flat slice
|
||||
(`missionId = null`). Startup builds the indexer on `workspace.slices_root`
|
||||
(default `<workspace.root>/missions`) and adds `<workspace.root>/missions` and
|
||||
`<workspace.root>/slices` as extra roots. NO new SQLite migration, NO new event
|
||||
type: read-only projection over existing tables (`queue_items`,
|
||||
`queue_transitions`, `mission_control_actions`) + dogfood-evidence
|
||||
directories. Listing, detail and mission-status caches are time-bounded (60 s
|
||||
default); `invalidate()` drops all three plus the queue-membership index.
|
||||
Besides the TTL, every unscoped list or detail read first compares a basis
|
||||
built from each mission's `mission.yaml` text and `slices/` listing and drops
|
||||
the caches when it changes. When the indexer is ready, startup also watches the
|
||||
workspace and invalidates the indexer when proof sources change, emitting
|
||||
`proof.sources_changed`.
|
||||
`isReady()` is true when any configured slice root exists on disk. A set
|
||||
`OPENRIG_SLICES_ROOT` still takes precedence over `workspace.slices_root`.
|
||||
|
||||
Slices are keyed by folder name alone: when two missions (or two roots) hold a
|
||||
slice with the same name, only the first one walked is indexed, and
|
||||
`/api/slices/:name` and `/project/slice/$sliceId` address it by that bare
|
||||
name. The project-scoped read (`?project=…&mission=…`, §4.3) is the one path
|
||||
that selects by mission. A slice's `qitemIds` come first from rows tagged
|
||||
`slice:<id>`, then from the legacy match on the rail item or slice name; in a
|
||||
project-scoped indexer only rows tagged `project:<id>` count, so untagged
|
||||
historical rows are not attributed to a selected project.
|
||||
|
||||
> Source: `domain/slices/slice-indexer.ts:2–15` (contract + no new state),
|
||||
> `:238` (TTL), `:297–299` (`isReady`), `:331–336` (`invalidate`),
|
||||
> `:457–472` (`reconcileComposition`), `:476–521` (`readSliceLocations`;
|
||||
> first name wins `:478–483`; nested-vs-flat `:495–517`), `:61–88`
|
||||
> (`SliceRecord`: `missionId` `:65`, `slicePath` `:67`, `qitemIds` `:77`,
|
||||
> `proofPacket` `:81`), `:90–111` (`SliceListEntry`, which carries
|
||||
> `qitemCount` / `hasProofPacket` instead), `:865–875` and `:936–946` (queue
|
||||
> membership; project filter `:870`, `:942`); `domain/workspace/project-catalog.ts:132–138`
|
||||
> (`belongsToProject`); startup `startup.ts:1705`, `:1718` (`OPENRIG_SLICES_ROOT`
|
||||
> first), `:1725–1730`, `:1743–1748`, proof-source watch `:1762–1765`
|
||||
> (`domain/proof/source-watch.ts:16`); default
|
||||
> `domain/user-settings/settings-store.ts:486`.
|
||||
|
||||
### 4.2 SliceDetailProjector — 0.3.0
|
||||
|
||||
Given a `SliceRecord`, the projector assembles the full per-slice payload
|
||||
across six tabs (Story, Acceptance, Decisions, Docs, Tests/Verification,
|
||||
Topology) — **6** tab keys in `SliceDetailPayload`
|
||||
(`sed -n '/^export interface SliceDetailPayload/,/^}/p' packages/daemon/src/domain/slices/slice-detail-projector.ts | grep -c -E '^ (story|acceptance|decisions|docs|tests|topology):'`),
|
||||
plus a `readiness` field. Read-only; composes already-shipped tables +
|
||||
`workflow_specs`/`workflow_instances`/`workflow_step_trails` + slice docs on
|
||||
disk + dogfood-evidence. **v1 removed the v0 hardcoded legacy phase taxonomy**
|
||||
(`discovery`/`product-lab`/`delivery`/…): `StoryEvent.phase` is now an
|
||||
open-ended string-or-null — the spec-defined `step.id` when bound to a
|
||||
`workflow_instance`, else `null` (UI groups under "Untagged"). When no
|
||||
workflow runtime is constructed the projector silently degrades to v0
|
||||
behavior (`workflowBinding`, `specGraph`, `phaseDefinitions` and
|
||||
`currentStep` all `null`).
|
||||
|
||||
> Source: `domain/slices/slice-detail-projector.ts:1–20` (six-tab contract +
|
||||
> v1 enrichment), `:16–20` (v0 phase taxonomy removed), `:64`
|
||||
> (`StoryEvent.phase`), `:177–203` (`SliceDetailPayload`), `:205–213`
|
||||
> (optional `workflowSpecCache`); startup degrade path
|
||||
> `startup.ts:1749–1760` (comment `:1749–1755`;
|
||||
> `workflowSpecCache: workflowRuntime?.specCache` `:1759`).
|
||||
|
||||
### 4.3 Slices routes — 0.3.0
|
||||
|
||||
`slicesRoutes()` (mounted `server.ts:803` as `/api/slices`) registers **5**
|
||||
handlers (`grep -c -E 'app\.(get|post)\(' packages/daemon/src/routes/slices.ts`):
|
||||
|
||||
- `GET /` — filter `all|active|done|blocked` (default `all`; anything else is
|
||||
400 `filter_invalid`); `?refresh=1` (or `true`) invalidates first; optional
|
||||
`?boundToWorkflow=<name>:<version>` lens narrows to slices bound to a
|
||||
workflow instance (400 `boundToWorkflow_invalid` without the colon). Each
|
||||
slice row carries `readiness`, and a `missions` sidecar carries authored
|
||||
mission status and readiness. Rows sort by last activity, newest first.
|
||||
- `POST /refresh` — drops the indexer caches, no daemon restart.
|
||||
- `GET /:name/proof-asset/*` — serves a proof asset from the slice's matched
|
||||
dogfood-evidence directory; rejects `..` (400); `Cache-Control: public,
|
||||
max-age=86400`; byte ranges (206 / 416).
|
||||
- `GET /:name/doc/*` — markdown for the Docs tab; rejects `..`.
|
||||
- `GET /:name` — full per-tab payload. With `?project=<id>` it also requires
|
||||
`?mission=<id>` (400 `exact_mission_and_slice_required`) and reads through
|
||||
an indexer scoped to that project's missions root.
|
||||
|
||||
503 `slices_indexer_unavailable` when unwired; on `GET /`, 503
|
||||
`slices_root_not_configured` + setup hint when not ready. **Route-order
|
||||
discipline:** literal `/` / `/refresh` / `/:name/proof-asset/*` /
|
||||
`/:name/doc/*` are registered BEFORE the dynamic `/:name` so they are not
|
||||
shadowed.
|
||||
|
||||
> Source: `routes/slices.ts:33–198` (handlers; route-order comments
|
||||
> `:36–37`, `:118`, `:126–127`, `:175`; 503s `:40–46`; filters `:31`,
|
||||
> `:47–53`; refresh `:54–57`; `boundToWorkflow` `:67–93`; readiness +
|
||||
> `missions` sidecar `:99–110`; `/refresh` `:119–124`; proof asset
|
||||
> `:131–154`, cache header `:235`, ranges `:237–266`; doc `:158–173`;
|
||||
> project scope `:180–190`); mounted `server.ts:803`.
|
||||
|
||||
### 4.4 Missions route — 0.3.1
|
||||
|
||||
`missionsRoutes()` (mounted `server.ts:814` as `/api/missions`) is the
|
||||
mission scope data layer, with **2** handlers
|
||||
(`grep -c -E 'app\.(get|post)\(' packages/daemon/src/routes/missions.ts`).
|
||||
|
||||
`GET /:missionId` returns
|
||||
`{ missionId, missionPath, readiness, slices, workflow_spec, topology, status }`:
|
||||
|
||||
- `slices` — filtered from the SliceIndexer by `missionId`, each with its
|
||||
`readiness`;
|
||||
- `missionPath` — two levels up from any slice's `slicePath`;
|
||||
- `workflow_spec` — parsed from the frontmatter of the mission's node file
|
||||
(`SPEC.md`, else legacy `README.md`) with the same `parseWorkflowSpecRef`
|
||||
helper the slice-indexer exports;
|
||||
- `topology.specGraph` — projected via `projectSpecGraph(spec, null)` when the
|
||||
spec is cached; `{ specGraph: null }` when declared but not cached (or no
|
||||
cache is wired); `null` when nothing is declared;
|
||||
- `readiness` — the mission's proof readiness;
|
||||
- `status` — `metadata.status` from `mission.yaml` when present, else the
|
||||
node file's `status` frontmatter.
|
||||
|
||||
`POST /:missionId/complete` sets the mission status to `complete`:
|
||||
`metadata.status` in `mission.yaml` when the manifest exists (other fields and
|
||||
comments kept), otherwise the `status` line in the node file's frontmatter
|
||||
(creating `SPEC.md` when the mission has no node file). It is idempotent.
|
||||
After a successful write it drops the indexer's mission-status cache so the
|
||||
next `/api/slices` read sees the new status. The daemon is the audit-trail surface; the UI keeps an
|
||||
optimistic localStorage mirror. 404 `mission_not_found` when no slices match;
|
||||
500 `mission_complete_write_failed` on a write error.
|
||||
|
||||
> Source: `routes/missions.ts:45–88` (GET; 503s `:46–64`; 404 `:68–70`;
|
||||
> readiness + status `:77–78`; response `:79–87`), `:95–125` (complete; 500
|
||||
> `:110–117`; cache drop `:123`), `:132–163` (`writeMissionStatusComplete`;
|
||||
> manifest `:133–141`; node file `:145–162`), `:169–171`
|
||||
> (`computeMissionPath` up two levels), `:179–186` (`readMissionStatus`),
|
||||
> `:192–198` (`readMissionWorkflowSpec`), `:226–236`
|
||||
> (`computeMissionTopology`); node-file precedence
|
||||
> `domain/scope/node-file.ts:17`; readiness
|
||||
> `domain/proof/judgments.ts:288–295`; UI mirror
|
||||
> `packages/ui/src/hooks/useCompletedMissions.ts:15`; mounted `server.ts:814`;
|
||||
> §0: `routes/missions.ts` absent at `v0.3.0` ⇒ **0.3.1**.
|
||||
|
||||
### 4.5 Projects route — stream classifier — 0.3.0
|
||||
|
||||
`projectsRoutes()` (mounted `server.ts:786` as `/api/projects`) backs the
|
||||
`rig project` CLI verb — the **stream-item classifier** (lease lifecycle +
|
||||
idempotent classify + operator-verb reclaim + attempt ledger + SSE), NOT the
|
||||
Project *workspace UI* (that is the slices/missions surface above; the naming
|
||||
overlap is a documented seam). Endpoints:
|
||||
|
||||
- `GET /worker-sources` — requires `?project=<id>`, a workspace catalog
|
||||
project;
|
||||
- `GET /shadow`, `POST /shadow/drain`, `POST /shadow/stop` (the last two take
|
||||
an optional body `{ actor }`);
|
||||
- `POST /lease/acquire` (optional `evaluateDeadnessFirst` clears stale/dead
|
||||
leases first), `POST /lease/heartbeat`, `POST /reclaim-classifier`;
|
||||
- `POST /project` (idempotent on `stream_item_id`);
|
||||
- `POST /attempts/begin`, `POST /attempts/:attemptId/abstain`,
|
||||
`POST /attempts/:attemptId/fail`, `GET /eligible`;
|
||||
- `GET /lease`, `GET /list`, `GET /sse` + `GET /watch` (SSE);
|
||||
- `GET /:projectId`.
|
||||
|
||||
**Route-order discipline:** every literal path is registered BEFORE the bare
|
||||
`/:projectId` catchall, which comes last.
|
||||
|
||||
> Source: `routes/projects.ts:27–375` (handlers; route-order notes `:25`,
|
||||
> `:240`, `:309`, `:322`, `:339–340`, `:366`); worker sources `:98–109`;
|
||||
> shadow `:111–127`; lease / reclaim / project `:129–237`
|
||||
> (`evaluateDeadnessFirst` `:142–144`); attempts and eligible `:239–306`;
|
||||
> lease and list `:308–336`; SSE `:338–364`; `/:projectId` `:366–372`;
|
||||
> error-code → HTTP mapping `:58–95`; CLI
|
||||
> `packages/cli/src/commands/project.ts:46`; mounted `server.ts:786`.
|
||||
|
||||
## 5. UI route reality — source-verified at routes.tsx
|
||||
|
||||
`packages/ui/src/routes.tsx` is **605** lines (`wc -l < packages/ui/src/routes.tsx`)
|
||||
and uses TanStack Router (`createRoute({ path, component })` objects), NOT JSX
|
||||
`<Route>` (**0** matches: `grep -c '<Route' packages/ui/src/routes.tsx`). The
|
||||
mission / project / slice destinations as they exist:
|
||||
|
||||
| Path | routes.tsx reality | routes.tsx:NN |
|
||||
|---|---|---|
|
||||
| `/project` (workspace) | **REAL route** — `WorkspaceScopePage` | `:136–140` |
|
||||
| `/project/mission/$missionId` | **REAL route** — `MissionScopePage` | `:142–146` |
|
||||
| `/project/slice/$sliceId` | **REAL route** — `SliceScopePage` | `:148–152` |
|
||||
| `/files` | **REAL route** — `FilesWorkspace` (see content-surfaces.md) | `:248–252` |
|
||||
| `/mission-control` | **`<Navigate to="/for-you" />`** redirect-stub (the Mission Control *system* lives in the daemon at `/api/mission-control`, not as a UI destination) | `:496–500` |
|
||||
| `/slices` | **`<Navigate to="/project" />`** redirect-stub | `:503–507` |
|
||||
| `/slices/$name` | **`<Navigate to="/project/slice/$sliceId" />`** redirect-stub | `:509–516` |
|
||||
| `/progress` | **`<Navigate to="/project" />`** redirect-stub (folds into Project tabs) | `:519–523` |
|
||||
| `/steering` | **`<Navigate to="/project" />`** redirect-stub (folds into the Project workspace overview tab) | `:526–530` |
|
||||
| `/missions` (top-level) | **NO route** — missions are reached only via `/project/mission/$missionId` | absent: **0** (`grep -c 'path: "/missions"' packages/ui/src/routes.tsx`) |
|
||||
| `/markdown` | **NO route** — markdown is a component in file/drawer surfaces, not a destination (see content-surfaces.md) | absent: **0** (`grep -c 'path: "/markdown"' packages/ui/src/routes.tsx`) |
|
||||
|
||||
> Source: `packages/ui/src/routes.tsx:136–152` (project routes), `:248–252`
|
||||
> (`/files`), `:484–530` (redirect block, which also redirects `/context` to
|
||||
> `/topology`), `:539–597` (route tree); `/api/mission-control` mounted
|
||||
> `packages/daemon/src/server.ts:790–793`.
|
||||
|
||||
Net: the daemon `/api/missions` + `/api/slices` + `/api/projects` routes are
|
||||
real. The **operator-facing UI** consumes the missions and slices routes
|
||||
through the `/project*` destinations; it does not call `/api/projects`
|
||||
(**0** files: `git grep -l '/api/projects' -- packages/ui/src | wc -l`).
|
||||
`/mission-control`, `/slices`, `/progress`, `/steering` are redirect-stubs
|
||||
(the systems exist at the daemon layer; the URLs were collapsed into
|
||||
`/project` / `/for-you`).
|
||||
|
||||
> Source: `packages/ui/src/hooks/useMission.ts:51` (`/api/missions`),
|
||||
> `packages/ui/src/hooks/useSlices.ts:84` (`/api/slices`).
|
||||
|
||||
## 6. Cross-cutting properties
|
||||
|
||||
- **Optional + valid-without:** rigs without a `workspace` block stay valid;
|
||||
whoami / node-inventory return null and queue target-repo validation fails
|
||||
open (`workspace-resolver.ts:32`, `:62`; `rig-repository.ts:185`, `:193`;
|
||||
`routes/queue.ts:112`).
|
||||
- **Defensive column access:** `RigRepository` probes for `workspace_json`
|
||||
(`rig-repository.ts:185`, `:193`), node-inventory's own reads fall back to
|
||||
null on error (`node-inventory.ts:724`, `:796–798`), and the queue
|
||||
repository detects `target_repo` (`queue-repository.ts:758`), so partial
|
||||
test fixtures don't crash. Migrations 038/039 ship separately so a fixture
|
||||
can apply only the half it needs (`038_workspace_primitive.ts:12–14`).
|
||||
- **Read-only projection, no new state for the tree:** SliceIndexer /
|
||||
SliceDetailProjector add NO migration / event type; only the workspace
|
||||
declaration (038) + per-item scope (039) touch SQLite
|
||||
(`slice-indexer.ts:14–15`).
|
||||
- **Advisory, never deletes:** the frontmatter validator never modifies files
|
||||
(`frontmatter-validator.ts:12`), and the workspace scaffold never deletes or
|
||||
overwrites operator content (`default-workspace-scaffold.ts:140`;
|
||||
`packages/cli/src/commands/config-init-workspace.ts:18`).
|
||||
|
||||
## Open items
|
||||
|
||||
- `resolveWorkspaceContext` honors an `envOverride` **verbatim even when it
|
||||
names no declared repo** (`workspace-resolver.ts:39–40`), whereas the queue
|
||||
route **rejects** an unknown `target_repo` against the same `repos[]`
|
||||
(`routes/queue.ts:114–120`). These two surfaces apply opposite policies to
|
||||
an unknown repo name (whoami trusts the operator; queue validates). The
|
||||
resolver's comment at `workspace-resolver.ts:35–36` states the verbatim behavior is intended, but
|
||||
the comment line before it (`workspace-resolver.ts:34`) and `whoami-service.ts:331–332` describe
|
||||
a repo-name check that the code does not perform. Stated as-is; the
|
||||
divergence is not reconciled in source.
|
||||
- The `/api/projects` *classifier* and the `/project*` *workspace UI*
|
||||
(slices/missions) share the word "project" but are different subsystems.
|
||||
Both accept a workspace catalog project as `?project=<id>`
|
||||
(`routes/projects.ts:105`; `routes/slices.ts:181`). Documented as a naming
|
||||
seam; no source defect.
|
||||
@@ -1,118 +0,0 @@
|
||||
---
|
||||
kind: as-built
|
||||
title: Arteries — where a small change has a large effect
|
||||
status: active
|
||||
applies-when: |
|
||||
You are about to change, or review a change to, an area that many other paths depend on:
|
||||
message delivery, launch and resume, the queue, rig identity, skill projection, native
|
||||
process observation, migrations, or restore. Read this to see what depends on the area and
|
||||
what broke there before, then go and read the code.
|
||||
siblings: [README.md, test-layers.md]
|
||||
prerequisite-reads: [../../ARCHITECTURE.md]
|
||||
last-verified-against-source: e8f0ab340db773392ec8be75b072d1c0f3068a50
|
||||
last-updated: 2026-10-08
|
||||
---
|
||||
|
||||
# Arteries: where a small change has a large effect
|
||||
|
||||
## Read this first: this map is not the territory
|
||||
|
||||
- **This page is a zoomed-out view, not a description of OpenRig.** It names a few areas where
|
||||
small changes caused large regressions before. OpenRig is much larger than this page. Code,
|
||||
behaviour and risk that are not listed here exist, and some of it is riskier than anything below.
|
||||
- **It is incomplete by construction.** A pull request is not safe because its files are missing
|
||||
from this list.
|
||||
- **It may be stale.** It was checked against commit `e8f0ab34`. Files and dependencies move. If
|
||||
this page and the code disagree, the code is right; fix the page.
|
||||
- **Use it to change altitude, then go to the ground:** read the code, run the commands, check the
|
||||
behaviour. When you are lost, go and look rather than leaning on this page.
|
||||
|
||||
## The arteries
|
||||
|
||||
Paths are relative to `packages/`. The key files are entry points, not the full extent of each
|
||||
area.
|
||||
|
||||
| Artery | Key files | What depends on it | Past regressions and lessons |
|
||||
|---|---|---|---|
|
||||
| **Message delivery to seats** | `daemon/src/domain/session-transport.ts`, `daemon/src/domain/seat-delivery-guard.ts`, `daemon/src/adapters/tmux.ts` | Every `rig send`, queue wake, nudge, watchdog wake and handoff | #150 (the fix for #142) stopped typing into a bare shell. It then refused managed Codex seats (fixed by #171 before release) and every Claude seat launched with an explicit permission mode, which shipped in 0.6.2 as #197 (fixed by #220). The delivery guard's rule that a name must resolve to exactly one target, added in #109 (0.6.0), was involved in #141 (fixed by #151), the delivery half of #174 (fixed by #181) and #188 (fixed by #189). Since #310/#327, a Claude seat whose runtime can't be observed still receives ordinary messages with a warning; only a plain idle shell or the wrong recipient is refused. |
|
||||
| **Launch, relaunch, resume** | `daemon/src/adapters/claude-code-adapter.ts`, `daemon/src/domain/claude-managed-launch.ts`, `daemon/src/adapters/codex-runtime-adapter.ts`, `daemon/src/domain/codex-daemon-support.ts`, `daemon/src/adapters/pi-runtime-adapter.ts`, `daemon/src/domain/seat-launch-environment.ts`, `daemon/src/domain/native-resume-probe.ts`, `daemon/src/domain/startup-orchestrator.ts` | Every seat start, restore and permission mode, plus the non-interruptive, `permission_policy`, effort, kernel operational-authority and team permission-default settings that each launch and resume command carries | Always setting `CLAUDE_CONFIG_DIR` gave explicit-mode Claude seats a blank config (#154). #225 fixed which config home is selected, and #565 writes the onboarding and trust bootstrap into that home. Codex seats launch with `--no-daemon` only when `codex --help` shows support (#69, #77); when support is unknown, OpenRig refuses to launch, so a failing or slow `codex --help` blocks the seat. The readiness window defaults to 30 seconds and can time out under load (#182); since #643 it is configurable as `runtime.readiness_timeout_seconds` (env `OPENRIG_RUNTIME_READINESS_TIMEOUT_SECONDS`, 1–600). A git worktree's `.git` file broke the Codex sandbox (#121; see #126). #618 moved non-managed Claude, Codex and Pi launches behind the seat launch environment, and #628, #638 and #666 followed within days. Pi launch and resume carry `--thinking` from the seat's resolved effort (OMP does not). |
|
||||
| **Queue claim, pickup, wakes** | `daemon/src/domain/queue-repository.ts`, `daemon/src/domain/queue-pickup.ts`, `daemon/src/domain/queue-wake-ladder.ts`, `daemon/src/routes/require-sender-identity.ts`, `cli/src/client.ts` | All durable work between seats | After a slow `/healthz`, the CLI host-qualified a local sender and claims failed (#131, fixed by #135). Done-and-closed items read as stalled forever (#164, fixed by #176). |
|
||||
| **Rig identity: same names, archived and deleted rigs** | `daemon/src/domain/rig-repository.ts`, `daemon/src/domain/rig-lifecycle-service.ts`, `daemon/src/domain/rig-teardown.ts`, `daemon/src/domain/seat-handover-service.ts`, `daemon/src/domain/running-name-guard.ts` | Seat resolution, `rig remove`, restore, importing a rig from YAML | Archived rigs still matched seat references, and `rig remove` killed another rig's live seat with the same name (#174, fixed by #181). Replacing stopped same-name rig generations on YAML import followed in #196. |
|
||||
| **Projection of skills and files** | `daemon/src/domain/rigspec-instantiator.ts`, `daemon/src/domain/projection-planner.ts`, and each runtime adapter's `project()` (Claude Code, Codex, Pi, OMP, stub) | What every agent is given at start | Codex seats skipped skills that a Claude sibling had already projected (#159, fixed by #162). File-shaped entries failed with ENOTDIR (fixed by #185). |
|
||||
| **Native process observation** | `daemon/src/domain/native-process-lineage.ts` (parses `ps -Ao pid,ppid,pgid,tpgid,ucomm,lstart,command`) | Liveness, identity verdicts, delivery checks | Processes whose command name contains a space were dropped (#134, fixed by #82). Non-English locales broke the date parsing (fixed by #239, which runs `ps` with `LC_ALL=C`). #220 also changed this file to recognise managed Claude seats behind launch wrappers. Claude identity proof (wrappers, launcher shims, conversation rotation, effort, version-named binaries, launch `--settings`) changed ten more times after 0.6.3. |
|
||||
| **Schema and migrations** | `daemon/src/db/migrations/`, `daemon/src/db/all-migrations.ts`, `daemon/src/db/migrate.ts` | Every install, and every upgrade of a long-lived install | Parallel pull requests pick the same next number: #112 adds `086_rig_kernel_variant` while `main` already has `086_classification_fields_and_attempts`, and #155 and #195 landed as 090 and 091. `migrate.ts` applies by migration name in name order, so a duplicate number is not skipped, but its order relative to the other migration changes; 093 is unused at `e8f0ab34`. A long-lived install upgrading across several releases applies many migrations in one start, against real data. |
|
||||
| **Restore and recovery** | `daemon/src/domain/restore-orchestrator.ts`, `daemon/src/adapters/{claude,codex,pi,omp}-resume.ts`, `daemon/src/domain/reconciler.ts`, `daemon/src/domain/restore-check-service.ts`, `daemon/src/domain/startup-orchestrator.ts`, `daemon/src/adapters/tmux.ts` | Reboots, power loss, daemon restarts | Detached seats kept stale tmux pane ids after a reboot (#141, fixed by #151 in `tmux.ts`). `restore-check` read queue files and Claude hooks from a fixed shared-docs path (#130); #139 made it use the configured root, and #328 replaced the queue-file check with a probe of the daemon's queue store and checks the selected Claude hooks. After a fresh relaunch, a stale Codex restore attention flag never clears (#116). Full-restore recovery of OMP seats is open (#41). |
|
||||
|
||||
## How reviews use this page
|
||||
|
||||
- **Find where the effect lands.** A reviewer notes which artery a pull request touches, if any,
|
||||
and what depends on it.
|
||||
- **At an artery, review the effect, not only the diff.** Whatever the diff size, the reviewer
|
||||
states the downstream effects and checks behaviour with a scratch reproduction or a test.
|
||||
- **Away from the arteries,** small pull requests keep a normal review. Scrutiny goes where the
|
||||
effect is.
|
||||
|
||||
## The scenario rule
|
||||
|
||||
**A pull request that touches an artery adds or extends a stub-agent scenario, or says in its
|
||||
description why it cannot.** Reviewers check for one or the other. Scenarios live in
|
||||
`packages/test-system/scenarios/`; [ARCHITECTURE.md](../../ARCHITECTURE.md#a-scenario) says how to
|
||||
write and run one, and [test-layers.md](test-layers.md) says what each test layer can and cannot
|
||||
catch.
|
||||
|
||||
The goal is that every artery has at least one scenario whose seeded regression fails, so a small
|
||||
change to an artery meets a real end-to-end check before it merges, not only a reviewer's reading.
|
||||
|
||||
### Scenario coverage today
|
||||
|
||||
At commit `e8f0ab34`. For why the stub cannot reach some of these yet, see "What a stub can and
|
||||
cannot prove" in [test-layers.md](test-layers.md).
|
||||
|
||||
- **Queue durability across a daemon restart: covered.** CI runs
|
||||
`queue-baton-survives-restart.yaml` healthy, then with a seeded `baton-drop` fault that must
|
||||
fail, then healthy again (#243). It runs `rig queue claim` and checks the destination after the
|
||||
restart, but pickup, wakes and claim-identity edge cases (such as the #131 sender case) have no
|
||||
scenario yet.
|
||||
- **Delivery (the #197 class): not covered.** The stub runtime has no input behaviour, so it cannot
|
||||
exercise delivery to a managed wrapper; `send-verify-means-rendered.yaml` is not admitted to CI
|
||||
for that reason (`packages/test-system/ci/README.md`).
|
||||
- **Addressing and rendered delivery: partial.** CI runs the transcript and capture stub scenarios
|
||||
in passing form, and the capture scenario catches a seat resolved by member-name prefix.
|
||||
`send-renders-in-addressed-pane.yaml` and `down-stops-every-seat.yaml` run with the local runner
|
||||
only. `stub-cli-journeys.integration.test.ts` checks that a verified send lands in the addressed
|
||||
pane and not its sibling. None of these proves a seat consumed a message.
|
||||
- **Restore (the #141 class): not covered.** The scenario runner does not bind the `restore` verb
|
||||
yet; it fails with `UnboundActionError`.
|
||||
- **Rig identity (the #174 class): not covered.** No library scenario sets up a same-name archived
|
||||
or removed rig.
|
||||
- **Projection (the #159 class): not coverable with `runtime: stub`,** which projects skills into its
|
||||
own `.openrig/stub/skills/` directory. Use adapter-level unit tests.
|
||||
- **Native process observation: unit tests only** (for example
|
||||
`packages/daemon/test/codex-process-observation.test.ts`, `native-ps-locale.test.ts` and
|
||||
`send-runtime-not-running.test.ts`).
|
||||
- **Migrations: unit guards only** (`migration-fixture-parity.test.ts`,
|
||||
`startup-migrations-mirror.test.ts`). Every scenario starts from an empty database, so upgrading
|
||||
an existing database is not covered.
|
||||
- **Launch:** every scenario's `up` goes through the real `rig up`, instantiation and startup
|
||||
orchestration with stub seats. The stub adapter types its runner directly; Claude Code, Codex
|
||||
and Pi seats launch through the seat launch environment (`seat-launch-environment.ts`), which
|
||||
no scenario reaches. Real Codex launch and handover run only in two opt-in daemon tests
|
||||
(`OPENRIG_E2E_REAL_CODEX=1`, which needs tmux, `codex` and a Codex login). The CI containers
|
||||
have no provider CLIs or credentials.
|
||||
|
||||
## Regression log
|
||||
|
||||
When a regression reaches a release, add one line: date, issue, artery, and which layer missed it
|
||||
(the layers are named in [test-layers.md](test-layers.md)). Each line should end up as a scenario.
|
||||
|
||||
- 2026-09-30, #197 (0.6.2), delivery: review saw a small diff in isolation; CI had no managed
|
||||
explicit-mode Claude seat; the project's own day-to-day seats use the default launch path.
|
||||
- 2026-09-29/30, #141, #174, #188 (after 0.6.0), delivery and rig identity: reviews of #109 did not
|
||||
see the downstream effect of the exactly-one-target rule.
|
||||
|
||||
- 2026-10-19, #1483, managed compaction: a helper-only command-composition check did not bind the submitted command to the precompact effect. `managed-compaction-effect.test.ts` now drives the real enforcer command into a version-bound stub input model and checks the shipped precompact marker; its 851-character pasted-command control lacks that effect despite a successful submit receipt. `rig compact` rejects stub runtimes, so this is an adapter/helper check, not an installed CLI or native compaction proof. The model uses the recorded Claude Code 2.1.282 distinction; other versions and actual context compaction remain unverified here.
|
||||
- 2026-10-19, #484, refocus orientation: script tests supplied basis strings without composing parked typed queue state through `queue whoami` and the hook. `refocus-parked-baton.yaml` plus the active-baton contrast now assert the shipped hook's own emitted context; installed-scenario runs healthy/seeded/healthy; its scratch trace-child fault moves the parked row from held work into the work candidates as current work, and the hook's emitted context must then fail with that row's slice shown as `WORK FROM QUEUE EVIDENCE`. This proves the composed diagnostic effect, not native hook scheduling or an agent reading and acting on it. With the work packet's bounds and per-runtime default, the parked scenario runs with the packet on and asserts the row as held work. `refocus-parked-baton-packet-off.yaml` keeps the labelled fallback as the packet-off control.
|
||||
- 2026-10-23, F1 (0.7.0 candidate `2fec7382`), launch (a fresh Codex kernel): a fresh Codex kernel stopped at Codex's hook review. Caught at the cut, before publication, by native first-user and daily runs. Missed by the stub layer, which has no real Codex binary. Fixed in #1824.
|
||||
- 2026-10-24, B6 (0.7.1 candidate `7badb557`), launch and resume (Codex hook trust): after an upgrade from 0.7.0, a Codex seat stopped at "Hooks need review: 6", because Codex 0.164 asks to approve a plugin's whole content again. Caught at the cut, before publication, by a native upgrade run with a real Codex. Missed by source tests and stub scenarios (a stub can't see the real Codex binary) and by the installed-upgrade check (no real Codex). Fixed in #1948. Scenario owed: a cheap real-agent Codex upgrade with approval renewal.
|
||||
- 2026-10-24, pane-only attention (#1876 on the 0.7.1 candidate), native observation: a Codex seat waiting at a prompt that only its pane showed read as working, with no attention signal. Caught at the cut, before publication, by real work on a test machine. Missed by fixtures that all carried hook signals. Fixed in #1946. Scenario owed: a stub pane showing a selector with no hook event must read as needing attention.
|
||||
- 2026-10-25, #1955, delivery (send readiness): approval-prompt text that a working Claude seat quoted in its own output read as a waiting prompt, so sends and queue wakes to that seat were refused. This one was present since 0.7.0, so it did reach a release; it was found during the 0.7.1 cut by real work on a test machine. Missed by classifier fixtures that had no working pane quoting prompt text. Fixed in #1955 for 0.7.2.
|
||||
+883
-1083
File diff suppressed because it is too large.
Load diff
+407
-92
@@ -1,110 +1,425 @@
|
||||
---
|
||||
kind: as-built
|
||||
title: OpenRig Codemap — Modules and Source Entry Points
|
||||
status: active
|
||||
topics: [knowledge-and-context, observability]
|
||||
domains: [engineering-advisor, operating-advisor]
|
||||
applies-when: |
|
||||
You know the behavior you need to inspect but not its owning source files.
|
||||
Follow the module and source entry points below, then read the implementation.
|
||||
siblings: [README.md, arteries.md, test-layers.md, cli-reference.md]
|
||||
prerequisite-reads: [README.md]
|
||||
last-verified-against-source: e8f0ab340db773392ec8be75b072d1c0f3068a50
|
||||
last-updated: 2026-10-08
|
||||
---
|
||||
# OpenRig Source Codemap
|
||||
|
||||
# OpenRig codemap
|
||||
Source-driven map of every file under `packages/daemon/src`, `packages/cli/src`, and `packages/ui/src` at current `HEAD`.
|
||||
|
||||
The paths and named entry points below were checked at main
|
||||
`e8f0ab340db773392ec8be75b072d1c0f3068a50`. This is a navigation map, not a replacement for
|
||||
reading the implementation. Linked documents retain their individual verification stamps.
|
||||
[README.md](README.md) lists the complete document tree and commands to derive its inventory.
|
||||
Current footprint:
|
||||
- daemon: 211 source files (126 domain, 31 routes, 11 adapters, 36 migrations, 7 other)
|
||||
- CLI: 78 source files
|
||||
- UI: 87 source files
|
||||
- total: 376 source files
|
||||
|
||||
## Package entry points
|
||||
How to use this doc:
|
||||
- `Exports` lists the public symbols defined by the file.
|
||||
- `Related` lists the file's primary local outgoing dependencies so you can follow the code path quickly.
|
||||
- The package/area groupings are structural, not ownership metadata.
|
||||
|
||||
Paths in backticks are relative to the repository root.
|
||||
Fast lookup:
|
||||
- Daemon app wiring and mounted HTTP surface: `packages/daemon/src/startup.ts`, `packages/daemon/src/server.ts`, `packages/daemon/src/routes/*`
|
||||
- AgentSpec / pod-aware engine: `packages/daemon/src/domain/agent-*`, `profile-resolver.ts`, `projection-planner.ts`, `startup-*`, `rigspec-*`, `pod-repository.ts`
|
||||
- Rig environment / managed-app substrate: `packages/daemon/src/adapters/compose-services-adapter.ts`, `packages/daemon/src/domain/service-orchestrator.ts`, `packages/daemon/src/domain/services-readiness.ts`, `packages/daemon/src/routes/env.ts`
|
||||
- Snapshot / restore / continuity: `packages/daemon/src/domain/snapshot-*`, `checkpoint-store.ts`, `restore-orchestrator.ts`
|
||||
- Transport / communication: `packages/daemon/src/domain/session-transport.ts`, `packages/daemon/src/routes/transport.ts`
|
||||
- Transcripts / persistence: `packages/daemon/src/domain/transcript-store.ts`, `packages/daemon/src/domain/history-query.ts`, `packages/daemon/src/routes/transcripts.ts`
|
||||
- Ask / context packs: `packages/daemon/src/domain/ask-service.ts`, `packages/daemon/src/routes/ask.ts`
|
||||
- Chat: `packages/daemon/src/domain/chat-repository.ts`, `packages/daemon/src/routes/chat.ts`
|
||||
- Spec review / library / identity: `packages/daemon/src/domain/spec-review-service.ts`, `packages/daemon/src/domain/spec-library-service.ts`, `packages/daemon/src/domain/whoami-service.ts`, `packages/daemon/src/routes/spec-review.ts`, `packages/daemon/src/routes/spec-library.ts`, `packages/daemon/src/routes/whoami.ts`
|
||||
- Node inventory: `packages/daemon/src/domain/node-inventory.ts`
|
||||
- Context usage: `packages/daemon/src/domain/context-usage-store.ts`, `packages/daemon/src/domain/context-monitor.ts`
|
||||
- Resume honesty: `packages/daemon/src/domain/native-resume-probe.ts`, `resume-metadata-refresher.ts`, `codex-thread-id.ts`
|
||||
- Package / bootstrap / bundle flows: `packages/daemon/src/domain/package-*`, `install-*`, `bootstrap-*`, `bundle-*`
|
||||
- CLI command surface: `packages/cli/src/index.ts`, `packages/cli/src/commands/*` (53 groups)
|
||||
- CLI infrastructure: `packages/cli/src/config-store.ts`, `packages/cli/src/system-preflight.ts`
|
||||
- UI workspace and navigation: `packages/ui/src/components/Explorer.tsx`, `packages/ui/src/components/WorkspaceHome.tsx`, `packages/ui/src/components/AppShell.tsx`
|
||||
- UI shared drawer: `packages/ui/src/components/SharedDetailDrawer.tsx`, `RigDetailPanel.tsx`, `NodeDetailPanel.tsx`, `DiscoveryPanel.tsx`, `SpecsPanel.tsx`, `RigChatPanel.tsx`, `SystemPanel.tsx`
|
||||
- UI spec/library surfaces: `packages/ui/src/components/RigSpecDisplay.tsx`, `packages/ui/src/components/AgentSpecDisplay.tsx`, `packages/ui/src/components/LibraryReview.tsx`, `packages/ui/src/components/SpecsWorkspace.tsx`
|
||||
- UI managed-app runtime surfaces: `packages/ui/src/components/RigEnvPanel.tsx`, `packages/ui/src/hooks/useRigEnv.ts`, `packages/ui/src/lib/build-setup-prompt.ts`
|
||||
- UI live identity surfaces: `packages/ui/src/components/LiveIdentityDisplay.tsx`, `packages/ui/src/components/LiveNodeDetails.tsx`
|
||||
- UI data layer: `packages/ui/src/hooks/*`
|
||||
|
||||
| Surface | Start here |
|
||||
|---|---|
|
||||
| Daemon process | `packages/daemon/src/index.ts`; `createDaemon()` in `packages/daemon/src/startup.ts`. |
|
||||
| HTTP application | `createApp()` in `packages/daemon/src/server.ts`; route modules under `packages/daemon/src/routes/`. |
|
||||
| State and migrations | `packages/daemon/src/db/connection.ts`, `packages/daemon/src/db/migrate.ts`, `packages/daemon/src/db/all-migrations.ts`. |
|
||||
| CLI | `createProgram()` in `packages/cli/src/index.ts`; commands under `packages/cli/src/commands/`, HTTP in `packages/cli/src/client.ts`. |
|
||||
| MCP | `createMcpServer()` in `packages/cli/src/mcp-server.ts`; stdio command in `packages/cli/src/commands/mcp.ts`. |
|
||||
| Terminal UI | `packages/tui/src/main.ts`, `packages/tui/src/state.ts`, `packages/tui/src/render.ts`; HTTP in `packages/tui/src/daemon-client.ts`. |
|
||||
| Web UI | `packages/ui/src/main.tsx`, `packages/ui/src/routes.tsx`. |
|
||||
| npm assembly | `scripts/build-package.sh`, `scripts/rewrite-daemon-imports.mjs`, `packages/cli/package.json`. |
|
||||
## Daemon
|
||||
|
||||
The daemon's route handlers obtain domain services from request context. `startup.ts` constructs
|
||||
those services, repositories and runtime adapters; `server.ts` wires them into the application.
|
||||
CLI and TUI clients use the daemon's HTTP projections. Package boundaries and the development
|
||||
versus bundled paths are described in [ARCHITECTURE.md](../../ARCHITECTURE.md).
|
||||
### Entrypoints And Wiring
|
||||
- `packages/daemon/src/index.ts`: CLI entrypoint for starting the daemon HTTP server. Exports: startServer. Related: `packages/daemon/src/startup.js`.
|
||||
- `packages/daemon/src/seed.ts`: Development/test seed entrypoint for populating sample daemon state. Exports: none. Related: `packages/daemon/src/db/connection.js`, `packages/daemon/src/db/migrate.js`, `packages/daemon/src/db/migrations/001_core_schema.js`, `packages/daemon/src/db/migrations/002_bindings_sessions.js`, +3 more.
|
||||
- `packages/daemon/src/server.ts`: Defines `AppDeps`, enforces shared-DB invariants, and mounts the full public Hono route tree including dual-format rigspec import/export/materialize, bundles, transport/transcripts/ask/chat, env status/logs/down, specs review/library, and `whoami`. Exports: AppDeps, createApp. Related: `packages/daemon/src/adapters/cmux.js`, `packages/daemon/src/adapters/tmux.js`, `packages/daemon/src/domain/bootstrap-orchestrator.js`, `packages/daemon/src/domain/bootstrap-repository.js`, +36 more.
|
||||
- `packages/daemon/src/startup.ts`: Constructs the daemon dependency graph, runs all 36 migrations, wires legacy, reboot, North Star, Post-North-Star, Phase 5 RigEnv, context-usage, Final Stretch 3 managed-app services, and PL-004 coordination services together, and returns `{ app, db, deps }`. Exports: createDaemon. Related: `packages/daemon/src/adapters/claude-resume.js`, `packages/daemon/src/adapters/cmux-transport.js`, `packages/daemon/src/adapters/cmux.js`, `packages/daemon/src/adapters/codex-resume.js`, +58 more.
|
||||
|
||||
## Runtime and state modules
|
||||
### Adapters
|
||||
- `packages/daemon/src/adapters/claude-code-adapter.ts`: Adapter implementation for claude code integration. Exports: ClaudeAdapterFsOps, ClaudeCodeAdapter. Related: `packages/daemon/src/adapters/tmux.js`, `packages/daemon/src/domain/projection-planner.js`, `packages/daemon/src/domain/runtime-adapter.js`.
|
||||
- `packages/daemon/src/adapters/claude-resume.ts`: Resume adapter for claude sessions. Exports: ClaudeResumeAdapter, ResumeResult. Related: `packages/daemon/src/adapters/shell-quote.js`, `packages/daemon/src/adapters/tmux.js`.
|
||||
- `packages/daemon/src/adapters/cmux-transport.ts`: Adapter/util module for cmux transport integration. Exports: createCmuxCliTransport. Related: `packages/daemon/src/adapters/cmux.js`, `packages/daemon/src/adapters/tmux.js`.
|
||||
- `packages/daemon/src/adapters/cmux.ts`: Adapter/util module for cmux integration. Exports: CmuxAdapter, CmuxResult, CmuxStatus, CmuxSurface, CmuxTransport, CmuxTransportFactory, CmuxWorkspace. Related: none.
|
||||
- `packages/daemon/src/adapters/compose-services-adapter.ts`: Compose-backed rig-environment adapter for `up`, `ps`, `logs`, and teardown. Exports: ComposePsService, ComposeServicesAdapter, ComposeServicesUpParams, ComposeServicesUpResult, ComposeServicesLogsParams, ComposeServicesLogsResult, ComposeServicesDownParams, ComposeServicesDownResult. Related: `packages/daemon/src/adapters/tmux-exec.js`.
|
||||
- `packages/daemon/src/adapters/codex-resume.ts`: Resume adapter for codex sessions. Exports: CodexResumeAdapter, ResumeResult. Related: `packages/daemon/src/adapters/claude-resume.js`, `packages/daemon/src/adapters/shell-quote.js`, `packages/daemon/src/adapters/tmux.js`.
|
||||
- `packages/daemon/src/adapters/codex-runtime-adapter.ts`: Adapter implementation for codex runtime integration. Exports: CodexAdapterFsOps, CodexRuntimeAdapter. Related: `packages/daemon/src/adapters/tmux.js`, `packages/daemon/src/domain/projection-planner.js`, `packages/daemon/src/domain/runtime-adapter.js`.
|
||||
- `packages/daemon/src/adapters/terminal-adapter.ts`: Infrastructure runtime adapter for terminal/shell nodes. No-op project/deliver/launch, immediate readiness. Exports: TerminalAdapter. Related: `packages/daemon/src/domain/runtime-adapter.js`.
|
||||
- `packages/daemon/src/adapters/shell-quote.ts`: Adapter/util module for shell quote integration. Exports: shellQuote. Related: none.
|
||||
- `packages/daemon/src/adapters/tmux-exec.ts`: Adapter/util module for tmux exec integration. Exports: execCommand. Related: `packages/daemon/src/adapters/tmux.js`.
|
||||
- `packages/daemon/src/adapters/tmux.ts`: Adapter/util module for tmux integration. Exports: ExecFn, TmuxAdapter, TmuxPane, TmuxResult, TmuxSession, TmuxWindow. Related: none.
|
||||
|
||||
Each row names an existing module and a small set of source starting points. Directories group
|
||||
related code; they are not an exhaustive dependency list.
|
||||
### DB Core
|
||||
- `packages/daemon/src/db/connection.ts`: Creates the SQLite connection with daemon-wide pragmas. Exports: createDb. Related: none.
|
||||
- `packages/daemon/src/db/migrate.ts`: Migration runner and `Migration` type shared by all schema files. Exports: Migration, migrate. Related: none.
|
||||
|
||||
| Question | Module | Source entry points |
|
||||
|---|---|---|
|
||||
| How are the daemon, database and routes assembled? | [Daemon core](architecture/daemon-core.md) | `packages/daemon/src/startup.ts`, `packages/daemon/src/server.ts`, `packages/daemon/src/db/`. |
|
||||
| How do runtime launch, projection and resume work? | [Adapters and runtimes](architecture/adapters-and-runtimes.md) | `packages/daemon/src/domain/runtime-adapter.ts`, `packages/daemon/src/adapters/`, `packages/daemon/src/domain/native-resume-probe.ts`. |
|
||||
| How do authored specs become launched agents with identity? | [Agent spec and startup](architecture/agent-spec-and-startup.md) | `packages/daemon/src/domain/rigspec-schema.ts`, `packages/daemon/src/domain/startup-orchestrator.ts`, `packages/daemon/src/domain/whoami-service.ts`. |
|
||||
| Where are durable work, delivery and handoff handled? | [Coordination](architecture/coordination-primitive.md) | `packages/daemon/src/domain/queue-repository.ts`, `packages/daemon/src/domain/stream-store.ts`, `packages/daemon/src/domain/inbox-handler.ts`, `packages/daemon/src/domain/outbox-handler.ts`. |
|
||||
| How does a workflow retain progress and resume? | [Workflow runtime](architecture/workflow-runtime.md) | `packages/daemon/src/domain/workflow-runtime.ts`, `packages/daemon/src/domain/workflow-projector.ts`, `packages/daemon/src/domain/policies/`, `packages/daemon/src/routes/workflow.ts`. |
|
||||
| How are queue observations and actions exposed? | [Mission control](architecture/mission-control.md) | `packages/daemon/src/domain/mission-control/`, `packages/daemon/src/routes/mission-control.ts`. |
|
||||
| What is captured and checked during restore? | [Lifecycle, snapshot and restore](architecture/lifecycle-snapshot-restore.md) | `packages/daemon/src/domain/snapshot-capture.ts`, `packages/daemon/src/domain/restore-orchestrator.ts`, `packages/daemon/src/routes/restore-check.ts`. |
|
||||
| How do messages and transcript reads reach sessions? | [Transport and transcripts](architecture/transport-and-transcripts.md) | `packages/daemon/src/domain/session-transport.ts`, `packages/daemon/src/domain/transcript-store.ts`, `packages/daemon/src/routes/transport.ts`. |
|
||||
| Where do workspaces, projects and repository scope resolve? | [Workspace](architecture/workspace-primitive.md) | `packages/daemon/src/domain/workspace/`, `packages/daemon/src/domain/current-work.ts`, `packages/daemon/src/routes/projects.ts`. |
|
||||
| How are files, progress and steering read or written? | [Content surfaces](architecture/content-surfaces.md) | `packages/daemon/src/domain/files/`, `packages/daemon/src/domain/progress/`, `packages/daemon/src/domain/steering/`. |
|
||||
| How are review inputs, proof and approvals composed? | [Living notes and review](architecture/living-notes-review.md) | `packages/daemon/src/domain/review/`, `packages/daemon/src/domain/scope/`, `packages/daemon/src/routes/review.ts`. |
|
||||
| Where are plugins, agent images and context packs discovered? | [Plugins, images and context packs](architecture/plugin-agent-image-context-pack.md) | `packages/daemon/src/domain/plugin-discovery-service.ts`, `packages/daemon/src/domain/agent-images/`, `packages/daemon/src/domain/context-packs/`. |
|
||||
| How are reusable bundles assembled and installed? | [Packaging, bootstrap and bundles](architecture/packaging-bootstrap-bundles.md) | `packages/daemon/src/domain/pod-bundle-assembler.ts`, `packages/daemon/src/domain/bootstrap-orchestrator.ts`, `packages/daemon/src/routes/bundles.ts`, `packages/daemon/src/routes/up.ts`. |
|
||||
| Where are event types, publication and SSE handled? | [Architecture rules and events](architecture/architecture-rules-and-event-system.md) | `packages/daemon/src/domain/types.ts`, `packages/daemon/src/domain/event-bus.ts`, `packages/daemon/src/routes/events.ts`. |
|
||||
### DB Migrations
|
||||
- `packages/daemon/src/db/migrations/001_core_schema.ts`: Database migration for core schema. Exports: coreSchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/002_bindings_sessions.ts`: Database migration for bindings sessions. Exports: bindingsSessionsSchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/003_events.ts`: Database migration for events. Exports: eventsSchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/004_snapshots.ts`: Database migration for snapshots. Exports: snapshotsSchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/005_checkpoints.ts`: Database migration for checkpoints. Exports: checkpointsSchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/006_resume_metadata.ts`: Database migration for resume metadata. Exports: resumeMetadataSchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/007_node_spec_fields.ts`: Database migration for node spec fields. Exports: nodeSpecFieldsSchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/008_packages.ts`: Database migration for packages. Exports: packagesSchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/009_install_journal.ts`: Database migration for install journal. Exports: installJournalSchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/010_journal_seq.ts`: Database migration for journal seq. Exports: journalSeqSchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/011_bootstrap.ts`: Database migration for bootstrap. Exports: bootstrapSchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/012_discovery.ts`: Database migration for discovery. Exports: discoverySchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/013_discovery_fk_fix.ts`: Database migration for discovery fk fix. Exports: discoveryFkFix. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/014_agentspec_reboot.ts`: Database migration for agentspec reboot. Exports: agentspecRebootSchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/015_startup_context.ts`: Database migration for startup context. Exports: startupContextSchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/016_chat_messages.ts`: Database migration for durable rig-scoped chat messages. Exports: chatMessagesSchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/017_pod_namespace.ts`: Database migration that persists authored pod namespace separately from pod DB id for export, adoption, and identity parity. Exports: podNamespaceSchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/018_context_usage.ts`: Database migration for per-node context-usage persistence. Exports: contextUsageSchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/019_external_cli_attachment.ts`: Database migration extending bindings for external CLI attachment metadata. Exports: externalCliAttachmentSchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/020_rig_services.ts`: Database migration for rig-scoped services records and persisted env receipts. Exports: rigServicesSchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/021_seat_handover_observability.ts`: Database migration for seat-handover observability fields. Exports: seatHandoverObservabilitySchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/022_node_codex_config_profile.ts`: Database migration adding Codex config-profile field on nodes. Exports: nodeCodexConfigProfileSchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/023_stream_items.ts`: PL-004 Phase A — L1 stream items table (append-only intake/audit root). Exports: streamItemsSchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/024_queue_items.ts`: PL-004 Phase A — L3 queue items table (owned-work queue with closure obligation). Exports: queueItemsSchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/025_queue_transitions.ts`: PL-004 Phase A — append-only queue state transition log. Exports: queueTransitionsSchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/026_inbox_entries.ts`: PL-004 Phase A — mailbox-style inbox for asynchronous deposit. Exports: inboxEntriesSchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/027_outbox_entries.ts`: PL-004 Phase A — sender-side outbox audit. Exports: outboxEntriesSchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/028_project_classifications.ts`: PL-004 Phase B — L2 project classifications table (idempotent on stream_item_id; FK to stream_items). Exports: projectClassificationsSchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/029_classifier_leases.ts`: PL-004 Phase B — single-writer classifier lease table (partial UNIQUE INDEX on state='active'). Exports: classifierLeasesSchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/030_views_custom.ts`: PL-004 Phase B — custom view registry. Exports: viewsCustomSchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/031_watchdog_jobs.ts`: PL-004 Phase C — watchdog jobs (canonical scheduler state; policy enum + intervals + lifecycle). Exports: watchdogJobsSchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/032_watchdog_history.ts`: PL-004 Phase C — append-only watchdog evaluation history (sent/skipped/terminal; not_due polls not recorded). Exports: watchdogHistorySchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/033_workflow_specs.ts`: PL-004 Phase D — workflow specs (cached read-through from markdown/YAML; UNIQUE on name+version; source_hash invalidation). Exports: workflowSpecsSchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/034_workflow_instances.ts`: PL-004 Phase D — workflow instances (live state; status enum; current_frontier_json; hop_count). Exports: workflowInstancesSchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/035_workflow_step_trails.ts`: PL-004 Phase D — append-only workflow step trail history (FK to workflow_instances and queue_items). Exports: workflowStepTrailsSchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/036_watchdog_policy_enum_extension.ts`: PL-004 Phase D — documenting no-op migration; Phase C uses application-layer enforcement so the enum extension to include workflow-keepalive is implemented in PHASE_D_POLICIES (watchdog-jobs-repository.ts), not in DDL. Exports: watchdogPolicyEnumExtensionSchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
- `packages/daemon/src/db/migrations/037_mission_control_actions.ts`: PL-005 Phase A — Mission Control action audit table (append-only; 7-verb enum at app layer; before/after qitem snapshots; FK to queue_items). Exports: missionControlActionsSchema. Related: `packages/daemon/src/db/migrate.js`.
|
||||
|
||||
For an execution projection, also read `packages/daemon/src/domain/execution-view.ts` and
|
||||
`packages/daemon/src/domain/view-projector.ts`. The terminal-side consumer starts in
|
||||
`packages/tui/src/execution/`. These sit alongside the queue and workflow sources above.
|
||||
### Domain: Mission Control / Queue Observability (PL-005 Phase A)
|
||||
- `packages/daemon/src/domain/mission-control/mission-control-action-log.ts`: Append-only writer for mission_control_actions. API surface lacks update/delete/remove (contract enforcement). Validates verb-specific required fields (annotate→annotation, hold/drop→reason). Exports: MissionControlActionLog, MissionControlActionLogError, MissionControlVerb, MISSION_CONTROL_VERBS. Related: `packages/daemon/src/db/migrations/037_mission_control_actions.js`.
|
||||
- `packages/daemon/src/domain/mission-control/mission-control-write-contract.ts`: LOAD-BEARING. Atomic 7-verb action coordinator. Each verb is one db.transaction: queue mutation via Phase D's QueueRepository.updateWithinTransaction (preserves Phase A hot-potato closure validation; emits queue.updated) + audit row + persisted mission_control.action_executed event. The 4-step handoff shape (source-update + destination-create + opt-in best-effort notify + audit append) is verified atomic; notify failure does NOT roll back durable state. Exports: MissionControlWriteContract, MissionControlWriteContractError, MissionControlActionInput, MissionControlActionResult. Related: `packages/daemon/src/domain/queue-repository.js`, `packages/daemon/src/domain/mission-control/mission-control-action-log.js`.
|
||||
- `packages/daemon/src/domain/mission-control/mission-control-read-layer.ts`: Maps 7 Mission Control views to source-of-truth paths. my-queue/human-gate/active-work/recent-ships query queue_items via QueueRepository; fleet uses fleetCliCapability roll-up; recently-active delegates to PL-004 Phase B ViewProjector.show("recently-active"); recent-observations reads stream_items via StreamStore. All rows return the 9-field phone-friendly content model (PRD non-negotiable). Exports: MissionControlReadLayer, MissionControlViewName, CompactStatusRow, MISSION_CONTROL_VIEWS. Related: `packages/daemon/src/domain/queue-repository.js`, `packages/daemon/src/domain/view-projector.js`, `packages/daemon/src/domain/stream-store.js`, `packages/daemon/src/domain/mission-control/mission-control-fleet-cli-capability.js`.
|
||||
- `packages/daemon/src/domain/mission-control/mission-control-fleet-cli-capability.ts`: Per-rig CLI capability cache + fleet roll-up. Implements 4 sub-clauses of PRD § Runtime/Source Drift Acceptance: per-field availability check, per-rig honesty, once-per-session-per-rig logging, "rigs running stale CLI" indicator. Synthesizes per-rig activity state (active|idle|attention|blocked|degraded) from queue_items aggregation. Exports: MissionControlFleetCliCapability, FleetRollupRow, FleetRollup, MISSION_CONTROL_DESIRED_FIELDS. Related: `packages/daemon/src/domain/rig-repository.js`, `packages/daemon/src/db/migrations/024_queue_items.js`.
|
||||
- `packages/daemon/src/routes/mission-control.ts`: Hono route module for `/api/mission-control` (`GET /views` literal list; `GET /views/:view-name` view fetch; `POST /action` 7-verb atomic execute; `GET /sse` + `GET /watch` SSE; `GET /cli-capabilities` fleet drift cache). SSE/literal paths mounted before /views/:view-name catchall per PL-004 Phase A R1 route-order discipline. Exports: missionControlRoutes. Related: `packages/daemon/src/domain/mission-control/*`, `packages/daemon/src/domain/event-bus.js`.
|
||||
|
||||
### Newer entry points
|
||||
### Product UI: Mission Control / Queue Observability (PL-005 Phase A)
|
||||
- `packages/ui/src/components/mission-control/MissionControlSurface.tsx`: Top-level Mission Control surface inside the existing product UI shell. Tab navigation across the 7 view names. Mounted at the `/mission-control` route. Exports: MissionControlSurface.
|
||||
- `packages/ui/src/components/mission-control/views/{MyQueueView,HumanGateView,FleetView,ActiveWorkView,RecentShipsView,RecentlyActiveView,RecentObservationsView}.tsx`: 7 view components. Most wrap a shared GenericListView; FleetView combines CliDriftIndicator + per-row MissingFieldPlaceholder; MyQueueView combines HumanSeatCard + GenericListView with verb actions enabled.
|
||||
- `packages/ui/src/components/mission-control/components/CompactStatusRow.tsx`: 9-field phone-friendly content-model atom (PRD non-negotiable). Renders compact / expanded density modes; UI may collapse but JSON preserves all 9 fields.
|
||||
- `packages/ui/src/components/mission-control/components/VerbActions.tsx`: Shared 7-verb action sub-component. Renders verb buttons + per-verb required-field input (destination for route/handoff, annotation for annotate, reason for hold/drop). Submits to `POST /api/mission-control/action`. Invalidates Mission Control view queries on success.
|
||||
- `packages/ui/src/components/mission-control/components/HumanSeatCard.tsx`: First-class human-seat rendering. Shows session identity, pending load, blocked count, verb capabilities. NOT invisible config-layer convention.
|
||||
- `packages/ui/src/components/mission-control/components/CliDriftIndicator.tsx`: Cross-CLI-version drift indicator (PRD sub-clause 4). Shows fleet-level "rigs running stale CLI" badge + per-row "field unavailable on this rig's daemon version" placeholders via the MissingFieldPlaceholder export.
|
||||
- `packages/ui/src/components/mission-control/hooks/{useMissionControlView,useMissionControlAction,useMissionControlCliCapabilities}.ts`: TanStack Query hooks fetching from `/api/mission-control/{views/:name,action,cli-capabilities}`.
|
||||
|
||||
These features were added after the module pages above were last verified. Each module's own
|
||||
stamp says whether it already covers them; until then, start from the source named here.
|
||||
### Domain: Coordination Watchdog (PL-004 Phase C; extended in Phase D)
|
||||
- `packages/daemon/src/domain/watchdog-jobs-repository.ts`: Persistence for watchdog_jobs. Phase D extension: enforces four-policy enum (periodic-reminder, artifact-pool-ready, edge-artifact-required, workflow-keepalive) at register-time. Phase C's policy_deferred_to_phase_d rejection has been replaced with positive registration-accept. Exports: WatchdogJobsRepository, WatchdogJobsError, WatchdogJob, PHASE_D_POLICIES, PHASE_C_POLICIES (alias). Related: `packages/daemon/src/db/migrations/031_watchdog_jobs.js`.
|
||||
- `packages/daemon/src/domain/watchdog-history-log.ts`: Append-only writer for watchdog_history. Records meaningful evaluations only (sent/skipped/terminal). API surface intentionally lacks update/delete. Exports: WatchdogHistoryLog, WatchdogHistoryEntry, WatchdogOutcome. Related: `packages/daemon/src/db/migrations/032_watchdog_history.js`.
|
||||
- `packages/daemon/src/domain/watchdog-policy-engine.ts`: Resolves policy by name, parses spec_yaml context, dispatches policy.evaluate(), records meaningful outcomes via history-log, emits watchdog.* events, calls injected delivery callback for action=send. Exports: WatchdogPolicyEngine, DeliveryFn, DeliveryRequest, DeliveryOutcome. Related: `packages/daemon/src/domain/watchdog-jobs-repository.js`, `packages/daemon/src/domain/watchdog-history-log.js`, `packages/daemon/src/domain/event-bus.js`.
|
||||
- `packages/daemon/src/domain/watchdog-scheduler.ts`: Tick-driven daemon-native scheduler that joins the supervision tree. Filters listActive() through isDue(), evaluates due jobs sequentially, supports start/stop with graceful in-flight await. Exports: WatchdogScheduler, isDue. Related: `packages/daemon/src/domain/watchdog-policy-engine.js`, `packages/daemon/src/domain/watchdog-jobs-repository.js`.
|
||||
- `packages/daemon/src/domain/policies/types.ts`: Policy contract (Policy, PolicyJob, PolicyEvaluation). Exports: Policy, PolicyJob, PolicyEvaluation. Related: none.
|
||||
- `packages/daemon/src/domain/policies/periodic-reminder.ts`: Policy that returns send {target, message} unconditionally; throws policy_spec_invalid if context.target.session or context.message missing. Translated from POC `lib/policies/periodic-reminder.mjs`. Exports: periodicReminderPolicy. Related: `packages/daemon/src/domain/policies/types.js`.
|
||||
- `packages/daemon/src/domain/policies/artifact-pool-ready.ts`: Policy that scans context.pools and skips with no_actionable_artifacts when empty; otherwise sends a formatted message capped at max_items. Translated from POC `lib/policies/artifact-pool-ready.mjs`. Exports: artifactPoolReadyPolicy. Related: `packages/daemon/src/domain/policies/artifact-pool-helpers.js`, `packages/daemon/src/domain/policies/types.js`.
|
||||
- `packages/daemon/src/domain/policies/edge-artifact-required.ts`: Policy that scans source_pools and target_pools, computes source-key per artifact, and sends an edge-repair message for sources lacking matching downstream artifacts. Skips with no_missing_edge_artifacts when all sources matched. Translated from POC `lib/policies/edge-artifact-required.mjs`. Exports: edgeArtifactRequiredPolicy. Related: `packages/daemon/src/domain/policies/artifact-pool-helpers.js`, `packages/daemon/src/domain/policies/types.js`.
|
||||
- `packages/daemon/src/domain/policies/artifact-pool-helpers.ts`: Shared filesystem scanner + formatter for artifact-pool-ready and edge-artifact-required. Translated from POC `lib/policies/artifact-pool.js`. Exports: scanArtifactPools, formatArtifactList, sourceKeyFor, ArtifactPoolSpec, ScannedArtifact. Related: none.
|
||||
- `packages/daemon/src/routes/watchdog.ts`: Hono route module for `/api/watchdog` (`POST /register`, `GET /list`, `GET /sse`, `GET /watch`, `GET /:job_id`, `GET /:job_id/status`, `POST /:job_id/stop`). SSE/literal paths mounted before /:job_id catchall per Phase A R1 route-order discipline. Exports: watchdogRoutes. Related: `packages/daemon/src/domain/watchdog-jobs-repository.js`, `packages/daemon/src/domain/watchdog-history-log.js`, `packages/daemon/src/domain/event-bus.js`.
|
||||
- `packages/daemon/src/domain/policies/workflow-keepalive.ts`: PL-004 Phase D — workflow-keepalive policy (carry-forward from Phase C deferral). Reads workflow_instances directly via SQLite (audit row 18 LOAD-BEARING); requires status active|waiting; resolves frontier qitem owners via queue_items. Translated from POC `lib/policies/workflow-keepalive.mjs`; source-of-truth switched from markdown to SQLite. Exports: makeWorkflowKeepalivePolicy. Related: `packages/daemon/src/domain/policies/types.js`, `packages/daemon/src/db/migrations/034_workflow_instances.js`.
|
||||
|
||||
| Question | Source entry points | Nearest module |
|
||||
|---|---|---|
|
||||
| How does a GitHub folder link become a bundle source? | `isGitHubBundleLink()` and the bundle source in `packages/cli/src/lib/bundle-source.ts`, used by `packages/cli/src/commands/up.ts` and `packages/cli/src/commands/bundle.ts`. | [Packaging and bundles](architecture/packaging-bootstrap-bundles.md) |
|
||||
| What does `rig bundle check` read? | `checkBundleFolder()` in `packages/cli/src/lib/bundle-check.ts`: advisory file reads against `openrig.bundle-standard/v1`, with no daemon, launch or provider call. | [Packaging and bundles](architecture/packaging-bootstrap-bundles.md) |
|
||||
| How does a member's `permission_policy` become a launch posture, including `builtin:auto`? | `builtinLaunchPosture()` and `resolvePermissionPolicyAttachment()` in `packages/daemon/src/domain/permission-policy/policy-ref.ts` (`builtin:yolo` is full bypass, `builtin:auto` is auto, the other built-ins are the floor), bound per seat in `packages/daemon/src/domain/rigspec-instantiator.ts`. `packages/daemon/src/domain/bundle-behaviour.ts` is the read-only preview of the same mapping for an unopened bundle. | [Adapters and runtimes](architecture/adapters-and-runtimes.md) |
|
||||
| What does a non-interruptive launch add? | `nonInterruptiveArgs()` in `packages/daemon/src/adapters/non-interruptive.ts` (per-launch arguments only; it never changes permissions or native settings files). The separate instance-wide YOLO setting, off by default, is in `packages/daemon/src/adapters/yolo-mode.ts`. | [Adapters and runtimes](architecture/adapters-and-runtimes.md) |
|
||||
| What launch arguments does a kernel seat get? | `operationalLaunchArgs()` and `KERNEL_CLAUDE_ALLOW` in `packages/daemon/src/adapters/kernel-authority.ts`; `NativePermissionStore.launchOverride()` in `packages/daemon/src/domain/native-permission-store.ts` decides it per launch (only the rig named `kernel`, with no explicit seat choice, permission policy or Codex profile). | [Adapters and runtimes](architecture/adapters-and-runtimes.md) |
|
||||
| What launch default does a seat in any other rig get? | Under the same conditions `launchOverride()` sets `teamPermissionDefault`: a Claude seat at the floor posture gets `TEAM_CLAUDE_ALLOW` and `TEAM_CLAUDE_ASK` from `operationalLaunchArgs()` in `packages/daemon/src/adapters/kernel-authority.ts`, whose `claudeCommandSettings()` hands the kernel or team lists to the session-only Bash hook `packages/daemon/assets/claude-team-permissions.cjs`, which returns the allow or ask decision; a workspace-write Codex seat gets `--add-dir` roots from `prepareCodexTeamWorkspace()` in `packages/daemon/src/domain/codex-team-workspace.ts`. | [Adapters and runtimes](architecture/adapters-and-runtimes.md) |
|
||||
| How does `rig seat continue` finish a fresh or Codex-fork startup that stopped before its context was delivered? | `continueStartup()` in `packages/daemon/src/domain/seat-lifecycle-service.ts`, `canContinueStartup()` and `readPendingStartupContext()` in `packages/daemon/src/domain/startup-orchestrator.ts`, `captureForkIdentity()` in `packages/daemon/src/adapters/codex-runtime-adapter.ts`, the route in `packages/daemon/src/routes/seat.ts`, and the command in `packages/cli/src/commands/seat.ts`. | [Agent spec and startup](architecture/agent-spec-and-startup.md) |
|
||||
| How does `rig context work-install` choose a project and position? | `resolveWorkPosition()` in `packages/cli/src/lib/work-install.ts`, `packages/cli/src/commands/context.ts`, and `packages/daemon/src/domain/workspace/project-catalog.ts`. | [Workspace](architecture/workspace-primitive.md) |
|
||||
| Where do rosters come from? | `packages/cli/src/commands/roster.ts`: `rig roster list`, `show` and `find` read JSON roster files from `<workspace.root>/rosters` (or `--folder`) and are read-only. | [Agent spec and startup](architecture/agent-spec-and-startup.md), "Specialist rosters" |
|
||||
| What telemetry can be read? | `packages/daemon/src/domain/finite-telemetry.ts` (the `events`, `queue-transitions` and `tenures` streams), `/api/telemetry` in `packages/daemon/src/routes/telemetry.ts`, and `packages/cli/src/commands/telemetry.ts`. | [Architecture rules and events](architecture/architecture-rules-and-event-system.md), "Telemetry read surface" |
|
||||
### Domain: Workflow Runtime (PL-004 Phase D)
|
||||
- `packages/daemon/src/domain/workflow-types.ts`: Shared types (WorkflowSpec, WorkflowInstance, WorkflowStepTrailEntry, WorkflowExitKind). Reverse-engineered from POC `workflow-runtime-v0/lib/workflow-runtime.rb` + fixture YAMLs. Exports: WorkflowSpec, WorkflowStepSpec, WorkflowRoleSpec, WorkflowInvariants, WorkflowClosureMessages, WorkflowLoopGuards, WorkflowInstance, WorkflowInstanceStatus, WorkflowStepTrailEntry, WorkflowExitKind, WorkflowSpecRow. Related: none.
|
||||
- `packages/daemon/src/domain/workflow-spec-cache.ts`: Read-through cache from markdown/YAML spec files into workflow_specs table. Workspace-surface reconciliation: source_hash invalidates the cache on next read; valid operator edits to spec files win. Uses the `yaml` package for parsing. Exports: WorkflowSpecCache, WorkflowSpecError, parseWorkflowSpec. Related: `packages/daemon/src/db/migrations/033_workflow_specs.js`.
|
||||
- `packages/daemon/src/domain/workflow-validator.ts`: Validates a workflow spec against role resolution, step uniqueness, allowed-exits consistency, and (optional via injected callback) seat liveness. Returns structured ValidationResult with severity-tagged issues. Exports: WorkflowValidator, ValidationIssue, ValidationResult, SeatLivenessCheckFn. Related: `packages/daemon/src/domain/workflow-types.js`.
|
||||
- `packages/daemon/src/domain/workflow-instance-store.ts`: CRUD on workflow_instances + frontier tracking. Survives daemon restart (queries SQLite directly). Exports: WorkflowInstanceStore, WorkflowInstanceError, CreateWorkflowInstanceInput. Related: `packages/daemon/src/db/migrations/034_workflow_instances.js`.
|
||||
- `packages/daemon/src/domain/workflow-step-trail-log.ts`: Append-only writer/reader for workflow_step_trails. API surface lacks update/delete (contract enforcement). Mirrors Phase A's queue-transition-log + Phase C's watchdog-history-log shape. Exports: WorkflowStepTrailLog, WorkflowStepTrailRecordInput. Related: `packages/daemon/src/db/migrations/035_workflow_step_trails.js`.
|
||||
- `packages/daemon/src/domain/workflow-projector.ts`: LOAD-BEARING. Implements the transactional-scribe contract: closing a workflow_instances frontier qitem AND creating the next-step queue_items row AND appending the workflow_step_trails entry AND updating instance frontier+status all happen in the SAME daemon transaction (db.transaction). If any side throws, all roll back. Lost handoffs are impossible by design. Post-commit, fans out persisted events + nudges next owner. Uses Phase A QueueRepository.createWithinTransaction() (Phase D extension point). Exports: WorkflowProjector, WorkflowProjectorError, ProjectStepInput, ProjectStepResult. Related: `packages/daemon/src/domain/queue-repository.js`, `packages/daemon/src/domain/workflow-instance-store.js`, `packages/daemon/src/domain/workflow-step-trail-log.js`.
|
||||
- `packages/daemon/src/domain/workflow-runtime.ts`: Facade over spec-cache + validator + instance-store + projector + step-trail-log. Operations: validate, instantiate (creates instance + entry qitem in same transaction), project (delegates to projector), continue (idempotent inspector). Exports: WorkflowRuntime, WorkflowRuntimeDeps, InstantiateInput, InstantiateResult, WorkflowInstanceError, WorkflowProjectorError, WorkflowSpecError. Related: `packages/daemon/src/domain/workflow-projector.js`, `packages/daemon/src/domain/workflow-spec-cache.js`.
|
||||
- `packages/daemon/src/routes/workflow.ts`: Hono route module for `/api/workflow` (`POST /validate`, `POST /instantiate`, `POST /project`, `GET /list`, `GET /sse`, `GET /watch`, `GET /:instance_id`, `GET /:instance_id/trace`, `POST /:instance_id/continue`). SSE/literal paths mounted before /:instance_id catchall per Phase A R1 route-order discipline. Exports: workflowRoutes. Related: `packages/daemon/src/domain/workflow-runtime.js`, `packages/daemon/src/domain/event-bus.js`.
|
||||
|
||||
## Change and verification guides
|
||||
### Domain: Core Runtime
|
||||
- `packages/daemon/src/domain/errors.ts`: Domain-specific error types, currently including `RigNotFoundError`. Exports: RigNotFoundError. Related: none.
|
||||
- `packages/daemon/src/domain/event-bus.ts`: Persistent event log plus in-process pub/sub for routes, SSE, and domain emitters. Exports: EventBus. Related: `packages/daemon/src/domain/types.js`.
|
||||
- `packages/daemon/src/domain/graph-projection.ts`: Pure projection from persisted rig/session state to React Flow graph nodes and edges. Exports: ReactFlowGraph, RigGraphInput, projectRigToGraph. Related: `packages/daemon/src/domain/types.js`.
|
||||
- `packages/daemon/src/domain/context-monitor.ts`: Daemon-owned context-usage recorder that samples live runtime state and updates persistent usage summaries. Exports: ContextMonitor. Related: `packages/daemon/src/domain/context-usage-store.js`.
|
||||
- `packages/daemon/src/domain/context-usage-store.ts`: SQLite-backed store for per-node context usage snapshots used by whoami/node inventory surfaces. Exports: ContextUsageStore, ContextUsageSnapshot, ContextUsageStoreOpts. Related: none.
|
||||
- `packages/daemon/src/domain/node-launcher.ts`: Atomic node launch service that creates tmux, session rows, bindings, and launch events together. Exports: LaunchResult, NodeLauncher. Related: `packages/daemon/src/adapters/tmux.js`, `packages/daemon/src/domain/event-bus.js`, `packages/daemon/src/domain/rig-repository.js`, `packages/daemon/src/domain/session-name.js`, +2 more.
|
||||
- `packages/daemon/src/domain/ps-projection.ts`: Builds the `/api/ps` summary view over rigs, nodes, sessions, and latest snapshots. Exports: PsEntry, PsProjectionService. Related: none.
|
||||
- `packages/daemon/src/domain/reconciler.ts`: Marks missing tmux sessions as detached and keeps persisted session state aligned with reality. Exports: ReconcileResult, Reconciler. Related: `packages/daemon/src/adapters/tmux.js`, `packages/daemon/src/domain/event-bus.js`, `packages/daemon/src/domain/session-registry.js`.
|
||||
- `packages/daemon/src/domain/rig-repository.ts`: SQLite-backed repository for rig, node, edge, and rig-services state. Also projects `hasServices` into rig summaries for runtime/UI gating. Exports: RigRepository. Related: `packages/daemon/src/domain/types.js`.
|
||||
- `packages/daemon/src/domain/rig-teardown.ts`: Graceful rig shutdown/delete orchestrator used by `/api/down`. Exports: RigTeardownOrchestrator, TeardownResult. Related: `packages/daemon/src/adapters/tmux.js`, `packages/daemon/src/domain/errors.js`, `packages/daemon/src/domain/event-bus.js`, `packages/daemon/src/domain/rig-repository.js`, +2 more.
|
||||
- `packages/daemon/src/domain/service-orchestrator.ts`: Rig-scoped services orchestrator for env boot, readiness gating, receipt capture, and teardown. Exports: ServiceBootResult, ServiceOrchestrator, ServiceTeardownResult. Related: `packages/daemon/src/adapters/compose-services-adapter.js`, `packages/daemon/src/domain/rig-repository.js`, `packages/daemon/src/domain/services-readiness.js`.
|
||||
- `packages/daemon/src/domain/session-name.ts`: Tmux session-name derivation and validation helpers. Exports: deriveSessionName, validateSessionName. Related: none.
|
||||
- `packages/daemon/src/domain/session-registry.ts`: Session and binding lifecycle repository, including claimed-session registration and startup-status updates. Exports: SessionRegistry. Related: `packages/daemon/src/domain/session-name.js`, `packages/daemon/src/domain/types.js`.
|
||||
- `packages/daemon/src/domain/services-readiness.ts`: Shared readiness evaluator for RigEnv wait targets (`url`, `tcp`, `service + condition`). Exports: WaitTargetFailure, WaitTargetSuccess, evaluateWaitTargets. Related: none.
|
||||
- `packages/daemon/src/domain/types.ts`: Canonical daemon type layer for entities, events, legacy rig specs, rebooted RigSpec/AgentSpec topology, RigEnv/service contracts, startup, snapshots, and restore. Exports: AgentResources, AgentSpec, Binding, Checkpoint, ContinuityPolicySpec, ContinuityState, Edge, EnvCheckpoint, EnvReceipt, GuidanceResource, HookResource, ImportSpec, InstantiateOutcome, InstantiateResult, LegacyRigSpec, LegacyRigSpecEdge, LegacyRigSpecNode, LifecycleDefaults, Node, NodeStartupSnapshot, NodeWithBinding, PersistedEvent, PersistedProjectionEntry, Pod, PreflightResult, ProfileSpec, RestoreNodeResult, RestoreOutcome, RestoreResult, Rig, RigEvent, RigServicesRecord, RigServicesSpec, RigSpec, RigSpecCrossPodEdge, RigSpecPod, RigSpecPodEdge, RigSpecPodMember, RigWithRelations, RuntimeResource, Session, SkillResource, Snapshot, SnapshotData, StartupAction, StartupBlock, StartupFile, SubagentResource, ValidationResult. Related: none.
|
||||
- `packages/daemon/src/domain/up-command-router.ts`: Source router that classifies a path as a rig spec or rig bundle before bootstrap. Exports: RouteResult, SourceKind, UpCommandRouter. Related: `packages/daemon/src/domain/rigspec-codec.js`, `packages/daemon/src/domain/rigspec-schema.js`.
|
||||
|
||||
| Need | Read |
|
||||
|---|---|
|
||||
| Contributor setup and a small change recipe | [CONTRIBUTING.md](../../CONTRIBUTING.md), [ARCHITECTURE.md](../../ARCHITECTURE.md). |
|
||||
| What depends on a sensitive source path | [arteries.md](arteries.md). |
|
||||
| Unit, route, scenario, package and CI coverage | [test-layers.md](test-layers.md), `packages/test-system/README.md`, `.github/workflows/tests.yml`. |
|
||||
| Exact CLI syntax | [cli-reference.md](cli-reference.md), then `packages/cli/src/index.ts`, its command module and current command help. |
|
||||
| As-built metadata | [frontmatter-schema.md](frontmatter-schema.md). |
|
||||
| An older architecture link | [architecture.md](architecture.md), which redirects into this module tree. |
|
||||
| Visual specification | [DESIGN.md](../DESIGN.md). |
|
||||
### Domain: Specs, Resolution, Startup
|
||||
- `packages/daemon/src/domain/agent-manifest.ts`: Canonical `agent.yaml` parser, validator, and normalizer for AgentSpec files. Exports: normalizeAgentSpec, parseAgentSpec, validateAgentSpec. Related: `packages/daemon/src/domain/path-safety.js`, `packages/daemon/src/domain/startup-validation.js`, `packages/daemon/src/domain/types.js`.
|
||||
- `packages/daemon/src/domain/agent-preflight.ts`: Lightweight preflight for a single `agent_ref`, used before full rig instantiation. Exports: agentPreflight. Related: `packages/daemon/src/domain/agent-resolver.js`, `packages/daemon/src/domain/types.js`.
|
||||
- `packages/daemon/src/domain/agent-resolver.ts`: Resolves `agent_ref` roots, imported AgentSpecs, and cross-spec resource-collision metadata. Exports: AgentResolverFsOps, ResolveResult, ResolvedAgentSpec, ResourceCollision, resolveAgentRef, resolveImports. Related: `packages/daemon/src/domain/agent-manifest.js`, `packages/daemon/src/domain/types.js`.
|
||||
- `packages/daemon/src/domain/pod-repository.ts`: SQLite-backed repository for pods and live continuity-state rows. Exports: PodRepository. Related: `packages/daemon/src/domain/types.js`.
|
||||
- `packages/daemon/src/domain/profile-resolver.ts`: Applies AgentSpec defaults, profile uses, startup layering, and restore-policy narrowing to produce a `ResolvedNodeConfig`. Exports: QualifiedResource, ResolutionContext, ResolutionResult, ResolvedNodeConfig, ResolvedResources, resolveNodeConfig. Related: `packages/daemon/src/domain/agent-resolver.js`, `packages/daemon/src/domain/startup-resolver.js`, `packages/daemon/src/domain/types.js`.
|
||||
- `packages/daemon/src/domain/projection-planner.ts`: Converts resolved node config plus collision data into runtime projection entries, diagnostics, conflicts, and no-ops. Exports: PlanResult, ProjectionClassification, ProjectionEntry, ProjectionFsOps, ProjectionInput, ProjectionPlan, planProjection. Related: `packages/daemon/src/domain/agent-resolver.js`, `packages/daemon/src/domain/conflict-detector.js`, `packages/daemon/src/domain/profile-resolver.js`, `packages/daemon/src/domain/types.js`.
|
||||
- `packages/daemon/src/domain/rigspec-codec.ts`: YAML codec layer for both legacy flat-node RigSpec and rebooted pod-aware RigSpec shapes. Exports: LegacyRigSpecCodec, RigSpecCodec. Related: `packages/daemon/src/domain/types.js`.
|
||||
- `packages/daemon/src/domain/rigspec-exporter.ts`: Exports live rigs back into RigSpec YAML/JSON. Dual-format: detects pod-aware rigs (podId non-null) and exports canonical v0.2 RigSpec with pods/members/edges; legacy rigs export as flat-node v1. Exports: RigSpecExporter. Related: `packages/daemon/src/domain/errors.js`, `packages/daemon/src/domain/pod-repository.js`, `packages/daemon/src/domain/rig-repository.js`, `packages/daemon/src/domain/session-registry.js`, `packages/daemon/src/domain/types.js`.
|
||||
- `packages/daemon/src/domain/rigspec-instantiator.ts`: Dual-stack instantiation layer containing both legacy `RigInstantiator` and rebooted `PodRigInstantiator`. Exports: PodRigInstantiator, RigInstantiator. Related: `packages/daemon/src/domain/agent-resolver.js`, `packages/daemon/src/domain/event-bus.js`, `packages/daemon/src/domain/node-launcher.js`, `packages/daemon/src/domain/pod-repository.js`, +10 more.
|
||||
- `packages/daemon/src/domain/rigspec-preflight.ts`: Dual-stack preflight layer: legacy `RigSpecPreflight` plus rebooted pure `rigPreflight`. Exports: RigPreflightInput, RigSpecPreflight, rigPreflight. Related: `packages/daemon/src/adapters/tmux.js`, `packages/daemon/src/domain/agent-resolver.js`, `packages/daemon/src/domain/profile-resolver.js`, `packages/daemon/src/domain/rig-repository.js`, +4 more.
|
||||
- `packages/daemon/src/domain/rigspec-schema.ts`: Validation and normalization for both legacy and rebooted RigSpec schemas. Exports: LegacyRigSpecSchema, RigSpecSchema. Related: `packages/daemon/src/domain/path-safety.js`, `packages/daemon/src/domain/startup-validation.js`, `packages/daemon/src/domain/types.js`.
|
||||
- `packages/daemon/src/domain/runtime-adapter.ts`: Runtime-adapter contract plus bridge types used by startup orchestration and runtime-specific adapters. Exports: InstalledResource, NodeBinding, ProjectionResult, ReadinessResult, ResolvedStartupFile, RuntimeAdapter, StartupDeliveryResult. Related: `packages/daemon/src/domain/projection-planner.js`, `packages/daemon/src/domain/types.js`.
|
||||
- `packages/daemon/src/domain/spec-validation-service.ts`: Pure validation helpers for raw AgentSpec and RigSpec YAML strings. Exports: validateAgentSpecFromYaml, validateRigSpecFromYaml. Related: `packages/daemon/src/domain/agent-manifest.js`, `packages/daemon/src/domain/rigspec-codec.js`, `packages/daemon/src/domain/rigspec-schema.js`.
|
||||
- `packages/daemon/src/domain/spec-review-service.ts`: Structured review-model builder for RigSpec and AgentSpec YAML. Produces topology preview data, normalized summaries, provenance/source-state, and managed-app services metadata used by preview, library, and live display surfaces. Exports: AgentSpecReview, LegacyRigSpecReview, PodAwareRigSpecReview, RigSpecReview, RigSpecServicesReview, SourceState, SpecGraphData, SpecReviewError, SpecReviewService. Related: `packages/daemon/src/domain/agent-manifest.js`, `packages/daemon/src/domain/rigspec-codec.js`, `packages/daemon/src/domain/rigspec-schema.js`.
|
||||
- `packages/daemon/src/domain/spec-library-service.ts`: Filesystem-backed spec library index over builtin and user roots. Classifies YAML through `SpecReviewService`, assigns stable ids, marks service-backed rigs with `hasServices`, and serves metadata/YAML lookup. Exports: SpecLibraryEntry, SpecLibraryOpts, SpecLibraryService. Related: `packages/daemon/src/domain/spec-review-service.js`.
|
||||
- `packages/daemon/src/domain/whoami-service.ts`: Daemon-side runtime identity projector used by `/api/whoami`. Resolves by node or session, expands peers/edges/transcript helpers, and surfaces ambiguity honestly. Exports: WhoamiResult, WhoamiAmbiguousError, WhoamiService. Related: `packages/daemon/src/domain/rig-repository.js`, `packages/daemon/src/domain/session-registry.js`, `packages/daemon/src/domain/transcript-store.js`.
|
||||
- `packages/daemon/src/domain/startup-orchestrator.ts`: Startup execution engine for projection, startup-file delivery, action execution, readiness, and persisted replay context. Exports: StartupInput, StartupOrchestrator, StartupResult. Related: `packages/daemon/src/adapters/tmux.js`, `packages/daemon/src/domain/event-bus.js`, `packages/daemon/src/domain/projection-planner.js`, `packages/daemon/src/domain/runtime-adapter.js`, +2 more.
|
||||
- `packages/daemon/src/domain/startup-resolver.ts`: Pure additive startup-layer merge function across agent, profile, rig, pod, member, and operator layers. Exports: StartupLayerInputs, resolveStartup. Related: `packages/daemon/src/domain/types.js`.
|
||||
- `packages/daemon/src/domain/startup-validation.ts`: Shared validation and normalization for startup files and startup actions. Exports: normalizeStartupBlock, validateStartupAction, validateStartupBlock, validateStartupFile. Related: `packages/daemon/src/domain/path-safety.js`, `packages/daemon/src/domain/types.js`.
|
||||
|
||||
## Existing web UI references
|
||||
### Domain: Snapshots And Restore
|
||||
- `packages/daemon/src/domain/checkpoint-store.ts`: Checkpoint persistence, lookup, and rig-wide checkpoint aggregation. Exports: CheckpointStore. Related: `packages/daemon/src/domain/types.js`.
|
||||
- `packages/daemon/src/domain/restore-orchestrator.ts`: Restore pipeline for relaunch, resume, checkpoint delivery, startup replay, topology ordering, and RigEnv boot gating before agent restore. Exports: RestoreOrchestrator. Related: `packages/daemon/src/adapters/claude-resume.js`, `packages/daemon/src/adapters/codex-resume.js`, `packages/daemon/src/adapters/tmux.js`, `packages/daemon/src/domain/checkpoint-store.js`, +7 more.
|
||||
- `packages/daemon/src/domain/snapshot-capture.ts`: Captures a consistent snapshot of rig topology, sessions, checkpoints, pods, continuity state, startup context, and the latest rig-env receipt. Exports: SnapshotCapture. Related: `packages/daemon/src/domain/checkpoint-store.js`, `packages/daemon/src/domain/errors.js`, `packages/daemon/src/domain/event-bus.js`, `packages/daemon/src/domain/rig-repository.js`, +3 more.
|
||||
- `packages/daemon/src/domain/snapshot-repository.ts`: SQLite-backed repository for snapshot CRUD and listing. Exports: SnapshotRepository. Related: `packages/daemon/src/domain/types.js`.
|
||||
|
||||
These historical documents are still indexed; this pass checks their locations, not their
|
||||
current implementation claims. Consult each page's stamp.
|
||||
### Domain: Packages And Install Engine
|
||||
- `packages/daemon/src/domain/conflict-detector.ts`: Content-aware conflict classification shared by package installs and rebooted projection planning. Exports: GuidanceConflictMeta, RefinedInstallPlan, classifyResourceProjection, detectConflicts. Related: `packages/daemon/src/domain/install-planner.js`, `packages/daemon/src/domain/package-resolver.js`, `packages/daemon/src/domain/projection-planner.js`.
|
||||
- `packages/daemon/src/domain/install-engine.ts`: Journaled apply/rollback engine for package installs. Exports: EngineFsOps, InstallEngine, InstallResult, RollbackResult. Related: `packages/daemon/src/domain/conflict-detector.js`, `packages/daemon/src/domain/install-planner.js`, `packages/daemon/src/domain/install-policy.js`, `packages/daemon/src/domain/install-repository.js`.
|
||||
- `packages/daemon/src/domain/install-planner.ts`: Computes classified package install entries and target paths. Exports: ActionClassification, ConflictInfo, InstallPlan, InstallPlanEntry, InstallPlanner, PlanOptions. Related: `packages/daemon/src/domain/package-resolver.js`, `packages/daemon/src/domain/role-resolver.js`.
|
||||
- `packages/daemon/src/domain/install-policy.ts`: Approval-policy gate for package install plans. Exports: PolicyRejection, PolicyResult, applyPolicy. Related: `packages/daemon/src/domain/conflict-detector.js`, `packages/daemon/src/domain/install-planner.js`.
|
||||
- `packages/daemon/src/domain/install-repository.ts`: SQLite-backed repository for package install records and journal entries. Exports: Install, InstallRepository, InstallSummary, JournalEntry. Related: none.
|
||||
- `packages/daemon/src/domain/install-verifier.ts`: Post-apply verification for install records and filesystem targets. Exports: Check, EntryVerification, InstallVerifier, VerificationResult, VerifierFsOps. Related: `packages/daemon/src/domain/install-repository.js`, `packages/daemon/src/domain/package-repository.js`.
|
||||
- `packages/daemon/src/domain/package-install-service.ts`: Top-level package validate/plan/install/rollback service used by routes and bootstrap. Exports: PackageInstallOutcome, PackageInstallService. Related: `packages/daemon/src/domain/conflict-detector.js`, `packages/daemon/src/domain/install-engine.js`, `packages/daemon/src/domain/install-planner.js`, `packages/daemon/src/domain/install-policy.js`, +4 more.
|
||||
- `packages/daemon/src/domain/package-manifest.ts`: Parser and validator for `package.yaml` manifests. Exports: AgentExport, GuidanceExport, HookExport, InstallPolicy, McpExport, PackageExports, PackageManifest, PackageRequirements, RoleDefinition, SkillExport, ValidationResult, VerificationCheck, VerificationConfig, normalizeManifest, parseManifest, serializeManifest, validateManifest. Related: none.
|
||||
- `packages/daemon/src/domain/package-repository.ts`: SQLite-backed repository for package metadata and install summaries. Exports: Package, PackageRepository, PackageSummary. Related: none.
|
||||
- `packages/daemon/src/domain/package-resolve-helper.ts`: Shared helper that wraps package resolution for routes and orchestrators. Exports: ResolveResult, resolvePackage. Related: `packages/daemon/src/domain/package-manifest.js`, `packages/daemon/src/domain/package-resolver.js`.
|
||||
- `packages/daemon/src/domain/package-resolver.ts`: Resolves local package sources and computes manifest hashes. Exports: FsOps, PackageResolver, ResolvedPackage. Related: `packages/daemon/src/domain/package-manifest.js`.
|
||||
- `packages/daemon/src/domain/path-safety.ts`: Shared safe-path validator used by manifests and startup config. Exports: validateSafePath. Related: none.
|
||||
- `packages/daemon/src/domain/role-resolver.ts`: Filters package exports according to selected roles/runtime scope. Exports: DeferredExport, ResolvedExports, resolveExports. Related: `packages/daemon/src/domain/package-manifest.js`.
|
||||
|
||||
| Reference | Source area |
|
||||
|---|---|
|
||||
| [UI compatibility pointer](ui.md) | `packages/ui/src/`. |
|
||||
| [Shell and routing](ui/shell-and-routing.md) | `packages/ui/src/routes.tsx`, `packages/ui/src/components/AppShell.tsx`. |
|
||||
| [Topology](ui/topology.md) | `packages/ui/src/components/topology/`. |
|
||||
| [Project and For You](ui/project-and-for-you.md) | `packages/ui/src/routes.tsx`. |
|
||||
| [Library/specs and design system](ui/library-specs-and-design-system.md) | `packages/ui/src/components/specs/`. |
|
||||
### Domain: Bootstrap, Discovery, Bundles
|
||||
- `packages/daemon/src/domain/bootstrap-orchestrator.ts`: Staged bootstrap workflow that composes runtime verification, requirement probes, external installs, package installs, legacy rig instantiation, direct pod-aware rig handling, and schema-version-2 pod-bundle delegation. Exports: BootstrapMode, BootstrapOptions, BootstrapOrchestrator, BootstrapResult, BootstrapStageResult. Related: `packages/daemon/src/domain/bootstrap-repository.js`, `packages/daemon/src/domain/bootstrap-types.js`, `packages/daemon/src/domain/bundle-source-resolver.js`, `packages/daemon/src/domain/external-install-executor.js`, +10 more.
|
||||
- `packages/daemon/src/domain/bootstrap-repository.ts`: SQLite-backed repository for bootstrap runs and action journals. Exports: BootstrapRepository. Related: `packages/daemon/src/domain/bootstrap-types.js`.
|
||||
- `packages/daemon/src/domain/bootstrap-types.ts`: Shared type definitions for bootstrap data. Exports: ActionKind, ActionStatus, BootstrapAction, BootstrapRun, BootstrapStatus, RuntimeStatus, RuntimeVerification. Related: none.
|
||||
- `packages/daemon/src/domain/bundle-archive.ts`: Archive pack/unpack and digest verification for `.rigbundle` files. Exports: pack, unpack, verifyArchiveDigest. Related: `packages/daemon/src/domain/bundle-integrity.js`, `packages/daemon/src/domain/bundle-types.js`.
|
||||
- `packages/daemon/src/domain/bundle-assembler.ts`: Legacy rig-bundle assembler for package-based bundles. Exports: AssembleOptions, AssemblerFsOps, LegacyBundleAssembler, PackageInput. Related: `packages/daemon/src/domain/bundle-types.js`, `packages/daemon/src/domain/rigspec-codec.js`, `packages/daemon/src/domain/rigspec-schema.js`.
|
||||
- `packages/daemon/src/domain/bundle-integrity.ts`: Per-file integrity manifest generation and verification for bundles. Exports: IntegrityFsOps, VerifyResult, computeIntegrity, verifyIntegrity, writeIntegrity. Related: `packages/daemon/src/domain/bundle-types.js`.
|
||||
- `packages/daemon/src/domain/bundle-source-resolver.ts`: Dual-stack bundle resolver containing legacy package-bundle and rebooted pod-bundle extraction flows. Exports: BundleResolvedSource, LegacyBundleSourceResolver, PodBundleResolvedSource, PodBundleSourceResolver. Related: `packages/daemon/src/domain/bundle-archive.js`, `packages/daemon/src/domain/bundle-types.js`, `packages/daemon/src/domain/package-resolve-helper.js`, `packages/daemon/src/domain/package-resolver.js`.
|
||||
- `packages/daemon/src/domain/bundle-types.ts`: Bundle manifest types plus parse/validate/serialize helpers for schema v1 and schema v2 bundles. Exports: BundleIntegrity, LegacyBundleManifest, LegacyBundlePackageEntry, PodBundleAgentEntry, PodBundleAgentImportEntry, PodBundleManifest, isRelativeSafePath, normalizeLegacyBundleManifest, parseLegacyBundleManifest, parsePodBundleManifest, serializeLegacyBundleManifest, serializePodBundleManifest, validateLegacyBundleManifest, validatePodBundleManifest. Related: none.
|
||||
- `packages/daemon/src/domain/claim-service.ts`: Claims discovered sessions into managed rig/node state. Exports: ClaimResult, ClaimService. Related: `packages/daemon/src/domain/discovery-repository.js`, `packages/daemon/src/domain/event-bus.js`, `packages/daemon/src/domain/rig-repository.js`, `packages/daemon/src/domain/session-registry.js`.
|
||||
- `packages/daemon/src/domain/discovery-coordinator.ts`: Coordinates tmux scans, fingerprinting, enrichment, persistence, and event emission for discovery. Exports: DiscoveryCoordinator. Related: `packages/daemon/src/domain/discovery-repository.js`, `packages/daemon/src/domain/discovery-types.js`, `packages/daemon/src/domain/event-bus.js`, `packages/daemon/src/domain/session-enricher.js`, +3 more.
|
||||
- `packages/daemon/src/domain/discovery-repository.ts`: SQLite-backed repository for discovered session records. Exports: DiscoveryRepository, UpsertData. Related: `packages/daemon/src/domain/discovery-types.js`.
|
||||
- `packages/daemon/src/domain/discovery-types.ts`: Shared type definitions for discovery data. Exports: Confidence, DiscoveredSession, DiscoveryStatus, RuntimeHint, SessionOrigin. Related: none.
|
||||
- `packages/daemon/src/domain/external-install-executor.ts`: Runs approved external install actions and journals execution results. Exports: ExecutionResult, ExecutionSummary, ExternalInstallExecutor, TaggedAction. Related: `packages/daemon/src/adapters/tmux.js`, `packages/daemon/src/domain/external-install-planner.js`.
|
||||
- `packages/daemon/src/domain/external-install-planner.ts`: Turns requirement-probe results into auto-approvable or manual external install actions. Exports: ApprovalClassification, ExternalInstallAction, ExternalInstallPlan, ExternalInstallPlanner. Related: `packages/daemon/src/adapters/shell-quote.js`, `packages/daemon/src/domain/requirements-probe.js`.
|
||||
- `packages/daemon/src/domain/pod-bundle-assembler.ts`: Rebooted bundle assembler that vendors AgentSpecs, imports, rig startup files, and rewritten `agent_ref`s. Exports: PodAssembleOptions, PodAssembleResult, PodAssemblerFsOps, PodBundleAssembler. Related: `packages/daemon/src/domain/agent-resolver.js`, `packages/daemon/src/domain/bundle-types.js`, `packages/daemon/src/domain/rigspec-codec.js`, `packages/daemon/src/domain/rigspec-schema.js`, +1 more.
|
||||
- `packages/daemon/src/domain/requirements-probe.ts`: Requirement-probe registry and probe result types used during bootstrap. Exports: ProbeResult, ProbeStatus, RequirementSpec, RequirementsProbeRegistry. Related: `packages/daemon/src/adapters/shell-quote.js`, `packages/daemon/src/adapters/tmux.js`.
|
||||
- `packages/daemon/src/domain/runtime-verifier.ts`: Verifies runtime/tool availability and persists verification results. Exports: RuntimeVerifier. Related: `packages/daemon/src/adapters/tmux.js`, `packages/daemon/src/domain/bootstrap-types.js`.
|
||||
- `packages/daemon/src/domain/session-enricher.ts`: Adds repository/config hints to discovered sessions based on filesystem inspection. Exports: EnrichmentResult, SessionEnricher. Related: none.
|
||||
- `packages/daemon/src/domain/session-fingerprinter.ts`: Infers runtime/session identity from tmux, cmux, process, and pane evidence. Exports: FingerprintEvidence, FingerprintResult, SessionFingerprinter. Related: `packages/daemon/src/adapters/cmux.js`, `packages/daemon/src/adapters/tmux.js`, `packages/daemon/src/domain/discovery-types.js`, `packages/daemon/src/domain/tmux-discovery-scanner.js`.
|
||||
- `packages/daemon/src/domain/tmux-discovery-scanner.ts`: Low-level tmux scanner that enumerates panes/sessions for discovery. Exports: ScanResult, ScannedPane, TmuxDiscoveryScanner. Related: `packages/daemon/src/adapters/tmux.js`.
|
||||
|
||||
### Domain: North Star Operator Layer
|
||||
- `packages/daemon/src/domain/node-inventory.ts`: Universal node-level projection consumed by CLI, UI, and MCP. Core fields (session name, status, startup status, restore outcome, attach command) plus extended fields (model, agent ref, startup files, installed resources). Exports: NodeInventoryService. Related: `packages/daemon/src/domain/types.js`, `packages/daemon/src/domain/runtime-adapter.js`.
|
||||
- `packages/daemon/src/domain/demo-rig-selector.ts`: Helper for existing-rig power-on — finds the right rig by name from ps summary entries. Exports: selectCurrentRigSummary. Related: none.
|
||||
- `packages/daemon/src/domain/draft-rig-generator.ts`: Synthesizes a candidate RigSpec YAML from discovered sessions — groups by CWD, suggests pods, sanitizes IDs. Exports: generateDraftRig. Related: `packages/daemon/src/domain/discovery-types.js`, `packages/daemon/src/domain/rigspec-codec.js`, `packages/daemon/src/domain/types.js`.
|
||||
- `packages/daemon/src/domain/native-resume-probe.ts`: Honest assessment of whether a harness actually resumed vs fresh-launched — probes pane command and content for runtime-specific indicators. Exports: NativeResumeProbeResult, assessNativeResumeProbe, buildNativeResumeCommand, isProbeShellReady. Related: `packages/daemon/src/adapters/shell-quote.js`.
|
||||
- `packages/daemon/src/domain/resume-metadata-refresher.ts`: Post-launch resume token capture — reads Claude conversation IDs from state files and Codex thread IDs from SQLite. Exports: ResumeMetadataRefresher. Related: `packages/daemon/src/domain/codex-thread-id.js`, `packages/daemon/src/domain/native-resume-probe.js`, `packages/daemon/src/domain/session-registry.js`, `packages/daemon/src/adapters/tmux.js`.
|
||||
- `packages/daemon/src/domain/codex-thread-id.ts`: Codex-specific thread ID extraction from the Codex SQLite state database. Exports: defaultResolveHomeDirByPid, readCodexThreadIdFromCandidateHomes, ResolveHomeDirByPid. Related: none.
|
||||
|
||||
### Domain: Post-North-Star Transport and History
|
||||
- `packages/daemon/src/domain/session-transport.ts`: Communication primitives — send/capture/broadcast with session resolution (canonical + legacy names), mid-work detection via pane content heuristics, honest error reporting, pod/rig/global targeting. Exports: SessionTransport, TargetSpec, SendResult, CaptureResult, BroadcastResult, SendOpts. Related: `packages/daemon/src/domain/rig-repository.js`, `packages/daemon/src/domain/session-registry.js`, `packages/daemon/src/adapters/tmux.js`.
|
||||
- `packages/daemon/src/domain/transcript-store.ts`: Pipe-pane transcript management — ANSI stripping on read (preserves raw on disk), boundary markers for session lifecycle events, readTail, grep. Filesystem-backed, not SQLite. Exports: TranscriptStore, TranscriptStoreOpts. Related: none (filesystem-only).
|
||||
- `packages/daemon/src/domain/history-query.ts`: Unified transcript + chat search — prefers `rg` when available, falls back to `grep -E`, surfaces which backend was used. Exports: HistoryQuery, SearchResult, ChatSearchResult, ExecDep. Related: `packages/daemon/src/domain/transcript-store.js`.
|
||||
- `packages/daemon/src/domain/ask-service.ts`: Context engineering evidence pack — gathers topology snapshot, transcript excerpts, chat history excerpts, restore metadata. Returns structured context for the agent to reason about. Does NOT call an external LLM. Exports: AskService, AskDeps. Related: `packages/daemon/src/domain/ps-projection.js`, `packages/daemon/src/domain/rig-repository.js`, `packages/daemon/src/domain/history-query.js`.
|
||||
- `packages/daemon/src/domain/chat-repository.ts`: Durable rig-scoped chat messages — CRUD for chat_messages table, monotonic ULID IDs, rig-scoped queries. Exports: ChatRepository, ChatMessage. Related: none (SQLite-only).
|
||||
|
||||
### Domain: Coordination Primitive (PL-004 Phase A)
|
||||
- `packages/daemon/src/domain/stream-store.ts`: L1 coordination stream — append-only intake/audit root. Idempotent emit on stream_item_id, chronological list with cursor pagination, soft archive. Exports: StreamStore, StreamItem, StreamEmitInput, StreamListOptions. Related: `packages/daemon/src/domain/event-bus.js`.
|
||||
- `packages/daemon/src/domain/queue-repository.ts`: L3 coordination queue — create, claim/unclaim, update with hot-potato strict-rejection, transactional handoff (close source + create new in one transaction), pod-fallback rerouting, overdue lookup. POC qitem_id format preserved. Exports: QueueRepository, QueueRepositoryError, QueueItem, QueueState, QueuePriority, QueueCreateInput, QueueUpdateInput, QueueHandoffInput. Related: `packages/daemon/src/domain/event-bus.js`, `packages/daemon/src/domain/queue-transition-log.js`, `packages/daemon/src/domain/hot-potato-enforcer.js`.
|
||||
- `packages/daemon/src/domain/queue-transition-log.ts`: Append-only state transition log for queue_items. No UPDATE / DELETE from domain code. Exports: QueueTransitionLog, QueueTransition, QueueTransitionInput. Related: `packages/daemon/src/domain/hot-potato-enforcer.js`.
|
||||
- `packages/daemon/src/domain/hot-potato-enforcer.ts`: Pure validator for the load-bearing hot-potato API contract — state=done requires closure_reason from the 6-value enum. Tier→SLA mapping for closure_required_at. Exports: CLOSURE_REASONS, ClosureReason, validateClosure, isClosureReason, computeClosureRequiredAt. Related: none.
|
||||
- `packages/daemon/src/domain/inbox-handler.ts`: Mailbox handler — authenticated drop (idempotent on inbox_id), absorb (promote pending entry to a queue_item), deny (record reason). Authentication is a pluggable constructor hook. Exports: InboxHandler, InboxHandlerError, InboxEntry, InboxState, InboxDropInput. Related: `packages/daemon/src/domain/event-bus.js`, `packages/daemon/src/domain/queue-repository.js`.
|
||||
- `packages/daemon/src/domain/outbox-handler.ts`: Sender-side outbox — symmetric to inbox; idempotent record, mark delivered/failed, list. No event-bus events emitted. Exports: OutboxHandler, OutboxHandlerError, OutboxEntry, OutboxDeliveryState, OutboxRecordInput. Related: none (SQLite-only).
|
||||
|
||||
### Routes
|
||||
- `packages/daemon/src/routes/adapters.ts`: Hono route module for adapters API endpoints. Exports: adaptersRoutes. Related: `packages/daemon/src/adapters/cmux.js`, `packages/daemon/src/adapters/tmux.js`.
|
||||
- `packages/daemon/src/routes/agents.ts`: Hono route module for AgentSpec validation. POST `/api/agents/validate` using `validateAgentSpecFromYaml`. Exports: agentsRoutes. Related: `packages/daemon/src/domain/spec-validation-service.js`.
|
||||
- `packages/daemon/src/routes/bootstrap.ts`: Hono route module for bootstrap API endpoints. Exports: bootstrapRoutes. Related: `packages/daemon/src/domain/bootstrap-orchestrator.js`, `packages/daemon/src/domain/bootstrap-repository.js`, `packages/daemon/src/domain/event-bus.js`.
|
||||
- `packages/daemon/src/routes/bundles.ts`: Hono route module for dual-format bundle create/inspect/install. Supports legacy v1 package bundles and schema-version-2 pod bundles, including v2 inspect responses with `agents[]` and integrity data. Exports: bundleRoutes. Related: `packages/daemon/src/domain/bootstrap-orchestrator.js`, `packages/daemon/src/domain/bootstrap-repository.js`, `packages/daemon/src/domain/bundle-archive.js`, `packages/daemon/src/domain/bundle-assembler.js`, +7 more.
|
||||
- `packages/daemon/src/routes/discovery.ts`: Hono route module for discovery scan/list/detail plus Final Stretch flows: `draft-rig`, `bind`, and composite `adopt`. Exports: discoveryRoutes. Related: `packages/daemon/src/domain/claim-service.js`, `packages/daemon/src/domain/discovery-coordinator.js`, `packages/daemon/src/domain/discovery-repository.js`, `packages/daemon/src/domain/draft-rig-generator.js`.
|
||||
- `packages/daemon/src/routes/down.ts`: Hono route module for down API endpoints. Exports: downRoutes. Related: `packages/daemon/src/domain/errors.js`, `packages/daemon/src/domain/rig-teardown.js`.
|
||||
- `packages/daemon/src/routes/env.ts`: Hono route module for rig-environment status, logs, and teardown. Refreshes env receipts best-effort and returns additive managed-app/runtime metadata (`hasServices`, `surfaces`, `receipt`). Exports: envRoutes. Related: `packages/daemon/src/adapters/compose-services-adapter.js`, `packages/daemon/src/domain/rig-repository.js`, `packages/daemon/src/domain/service-orchestrator.js`.
|
||||
- `packages/daemon/src/routes/events.ts`: Hono route module for events API endpoints. Exports: eventsRoute. Related: `packages/daemon/src/domain/event-bus.js`, `packages/daemon/src/domain/types.js`.
|
||||
- `packages/daemon/src/routes/packages.ts`: Hono route module for the legacy package-install surface (`/validate`, `/plan`, `/install`, history, rollback). AgentSpec validation no longer lives here. Exports: packagesRoutes. Related: `packages/daemon/src/domain/conflict-detector.js`, `packages/daemon/src/domain/event-bus.js`, `packages/daemon/src/domain/install-engine.js`, `packages/daemon/src/domain/install-planner.js`, +6 more.
|
||||
- `packages/daemon/src/routes/ps.ts`: Hono route module for ps API endpoints. Exports: psRoutes. Related: `packages/daemon/src/domain/ps-projection.js`.
|
||||
- `packages/daemon/src/routes/rigs.ts`: Hono route module for rig CRUD, graph projection, summary, delete, and existing-rig power-on by rig id. Exports: rigsRoutes. Related: `packages/daemon/src/domain/event-bus.js`, `packages/daemon/src/domain/graph-projection.js`, `packages/daemon/src/domain/node-inventory.js`, `packages/daemon/src/domain/rig-repository.js`, `packages/daemon/src/domain/session-registry.js`.
|
||||
- `packages/daemon/src/routes/rigspec.ts`: Hono route module for dual-format RigSpec import, validate, preflight, materialize, and export. Pod-aware import/preflight/materialize require `X-Rig-Root`; export emits v0.2 pod-aware YAML/JSON when the live rig has pods. Exports: handleExportJson, handleExportYaml, rigspecImportRoutes. Related: `packages/daemon/src/domain/errors.js`, `packages/daemon/src/domain/rigspec-codec.js`, `packages/daemon/src/domain/rigspec-exporter.js`, `packages/daemon/src/domain/rigspec-instantiator.js`, +2 more.
|
||||
- `packages/daemon/src/routes/sessions.ts`: Hono route module for rig sessions and node inventory/detail/launch/focus. `GET /api/rigs/:rigId/nodes/:logicalId` is the backend contract for the live node drawer and full-details workspace. Exports: nodesRoutes, sessionsRoutes. Related: `packages/daemon/src/adapters/cmux.js`, `packages/daemon/src/domain/node-inventory.js`, `packages/daemon/src/domain/node-launcher.js`, `packages/daemon/src/domain/rig-repository.js`, `packages/daemon/src/domain/session-registry.js`.
|
||||
- `packages/daemon/src/routes/snapshots.ts`: Hono route module for snapshots API endpoints. Exports: restoreRoutes, snapshotsRoutes. Related: `packages/daemon/src/domain/errors.js`, `packages/daemon/src/domain/restore-orchestrator.js`, `packages/daemon/src/domain/snapshot-capture.js`, `packages/daemon/src/domain/snapshot-repository.js`.
|
||||
- `packages/daemon/src/routes/transport.ts`: Hono route module for communication primitives (`POST /send`, `/capture`, `/broadcast`). Resolves session targets, delegates to SessionTransport. Exports: transportRoutes. Related: `packages/daemon/src/domain/session-transport.js`.
|
||||
- `packages/daemon/src/routes/transcripts.ts`: Hono route module for transcript access (`GET /:session/tail`, `/:session/grep`). Resolves session to rig/node, delegates to TranscriptStore. Exports: transcriptRoutes. Related: `packages/daemon/src/domain/transcript-store.js`, `packages/daemon/src/domain/rig-repository.js`.
|
||||
- `packages/daemon/src/routes/ask.ts`: Hono route module for context evidence packs (`POST /`). Delegates to AskService. Exports: askRoutes. Related: `packages/daemon/src/domain/ask-service.js`.
|
||||
- `packages/daemon/src/routes/chat.ts`: Hono route module for durable rig chat (`GET /watch` SSE stream, `POST /send`, `GET /history`, `POST /topic`). Delegates to ChatRepository, streams via SSE. Exports: chatRoutes. Related: `packages/daemon/src/domain/chat-repository.js`, `packages/daemon/src/domain/event-bus.js`.
|
||||
- `packages/daemon/src/routes/stream.ts`: Hono route module for the PL-004 Phase A coordination L1 stream (`POST /emit`, `GET /list`, `GET /:id`, `POST /:id/archive`, `GET /watch` SSE). Host-scoped (no rigId). Exports: streamRoutes. Related: `packages/daemon/src/domain/stream-store.js`, `packages/daemon/src/domain/event-bus.js`.
|
||||
- `packages/daemon/src/routes/queue.ts`: Hono route module for the PL-004 Phase A coordination L3 queue plus inbox/outbox (`POST /create`, `POST /:id/{claim,unclaim,update,handoff,fallback}`, `GET /list`, `GET /overdue`, `GET /:id`, `GET /:id/transitions`, inbox/outbox subroutes, `GET /watch` SSE). Surfaces hot-potato strict-rejection structured errors with validReasons enum. Exports: queueRoutes. Related: `packages/daemon/src/domain/queue-repository.js`, `packages/daemon/src/domain/inbox-handler.js`, `packages/daemon/src/domain/outbox-handler.js`, `packages/daemon/src/domain/event-bus.js`.
|
||||
- `packages/daemon/src/routes/spec-review.ts`: Hono route module for structured review over raw RigSpec / AgentSpec YAML. Accepts `sourceState` and returns review models or validation errors. Exports: specReviewRoutes. Related: `packages/daemon/src/domain/spec-review-service.js`.
|
||||
- `packages/daemon/src/routes/spec-library.ts`: Hono route module for the filesystem-backed spec library (`list`, `get`, `review`, `sync`). Enriches service-backed reviews with best-effort compose preview data for managed-app UI surfaces. Exports: specLibraryRoutes. Related: `packages/daemon/src/domain/spec-library-service.js`, `packages/daemon/src/domain/spec-review-service.js`.
|
||||
- `packages/daemon/src/routes/whoami.ts`: Hono route module for daemon-backed runtime identity lookup by `nodeId` or `sessionName`. Exports: whoamiRoutes. Related: `packages/daemon/src/domain/whoami-service.js`.
|
||||
- `packages/daemon/src/routes/up.ts`: Hono route module for `/api/up`, routing direct rig specs, bundles, and rig-name restore into the staged bootstrap flow. Exports: upRoutes. Related: `packages/daemon/src/domain/bootstrap-orchestrator.js`, `packages/daemon/src/domain/bootstrap-repository.js`, `packages/daemon/src/domain/event-bus.js`, `packages/daemon/src/domain/up-command-router.js`.
|
||||
|
||||
## CLI
|
||||
|
||||
### Entrypoints And Infrastructure
|
||||
- `packages/cli/src/client.ts`: Shared HTTP client for talking to the daemon API, including raw JSON posts and text-body posts with optional extra headers such as `X-Rig-Root`. Exports: DaemonClient, DaemonConnectionError, DaemonResponse. Related: none.
|
||||
- `packages/cli/src/daemon-lifecycle.ts`: Starts, stops, inspects, and tails the local daemon process. Spawns the daemon with `process.execPath` and bypasses local `daemon.json` gating when `OPENRIG_URL` is set by probing remote `/healthz` directly. Exports: DaemonState, DaemonStatus, LOG_FILE, LifecycleDeps, OPENRIG_DIR, STATE_FILE, StartOptions, getDaemonPath, getDaemonStatus, readLogs, startDaemon, stopDaemon, tailLogs. Related: none.
|
||||
- `packages/cli/src/index.ts`: Commander entrypoint that assembles the full `rig` CLI program (40 mounted command groups, including env status/logs/down, `destroy`, and Final Stretch 1 `specs`, `whoami`, `bind`, and `adopt`). Exports: ProgramDeps, createProgram. Related: `packages/cli/src/commands/bootstrap.js`, `packages/cli/src/commands/bundle.js`, `packages/cli/src/commands/claim.js`, `packages/cli/src/commands/daemon.js`, +19 more.
|
||||
- `packages/cli/src/mcp-server.ts`: Implements the CLI-hosted MCP server over stdio, wrapping 17 daemon-backed tools including node inventory, send/capture, bundle/spec validation, and chatroom. Exports: createMcpServer. Related: `packages/cli/src/client.js`.
|
||||
|
||||
### Commands
|
||||
- `packages/cli/src/commands/agent.ts`: Commander command module for `agent`. Subcommands: `validate` (POST `/api/agents/validate`). Supports `--json`. Exports: AgentDeps, agentCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/daemon-lifecycle.js`.
|
||||
- `packages/cli/src/commands/bootstrap.ts`: Commander command module for `bootstrap`. Supports library-name resolution before falling back to raw source refs. Exports: bootstrapCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/commands/daemon.js`, `packages/cli/src/commands/specs.js`, `packages/cli/src/commands/status.js`, `packages/cli/src/daemon-lifecycle.js`.
|
||||
- `packages/cli/src/commands/bind.ts`: Commander command module for `bind` — bind a discovered session to an existing logical node. Exports: bindCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/commands/daemon.js`, `packages/cli/src/commands/status.js`, `packages/cli/src/daemon-lifecycle.js`.
|
||||
- `packages/cli/src/commands/bundle.ts`: Commander command module for `bundle`. Supports dual-format bundle create/inspect/install, `--json`, and pod-aware `--rig-root` on create. Exports: bundleCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/commands/daemon.js`, `packages/cli/src/commands/status.js`, `packages/cli/src/daemon-lifecycle.js`.
|
||||
- `packages/cli/src/commands/claim.ts`: Commander command module for `claim`. Exports: claimCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/commands/daemon.js`, `packages/cli/src/commands/status.js`, `packages/cli/src/daemon-lifecycle.js`.
|
||||
- `packages/cli/src/commands/daemon.ts`: Commander command module for `daemon`. Exports: daemonCommand, realDeps. Related: `packages/cli/src/daemon-lifecycle.js`.
|
||||
- `packages/cli/src/commands/discover.ts`: Commander command module for `discover`. Exports: discoverCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/commands/daemon.js`, `packages/cli/src/commands/status.js`, `packages/cli/src/daemon-lifecycle.js`.
|
||||
- `packages/cli/src/commands/down.ts`: Commander command module for `down`. Exports: downCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/commands/daemon.js`, `packages/cli/src/commands/status.js`, `packages/cli/src/daemon-lifecycle.js`.
|
||||
- `packages/cli/src/commands/env.ts`: Commander command module for `env` — rig environment status/logs/down for service-backed rigs and managed apps. Exports: envCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/commands/daemon.js`, `packages/cli/src/daemon-lifecycle.js`, `packages/cli/src/commands/status.js`.
|
||||
- `packages/cli/src/commands/export.ts`: Commander command module for `export`. Exports: ExportDeps, exportCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/commands/daemon.js`, `packages/cli/src/commands/status.js`, `packages/cli/src/daemon-lifecycle.js`.
|
||||
- `packages/cli/src/commands/import.ts`: Commander command module for `import`. Locally parses YAML to detect pod-aware rigs, sends `X-Rig-Root` for pod-aware preflight/instantiate flows, and supports `--rig-root`. Exports: ImportDeps, importCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/commands/daemon.js`, `packages/cli/src/commands/status.js`, `packages/cli/src/daemon-lifecycle.js`.
|
||||
- `packages/cli/src/commands/mcp.ts`: Commander command module for `mcp`. Exports: mcpCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/commands/daemon.js`, `packages/cli/src/commands/status.js`, `packages/cli/src/daemon-lifecycle.js`, +1 more.
|
||||
- `packages/cli/src/commands/package.ts`: Commander command module for the legacy package-management surface. Exports: packageCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/commands/daemon.js`, `packages/cli/src/commands/status.js`, `packages/cli/src/daemon-lifecycle.js`.
|
||||
- `packages/cli/src/commands/ps.ts`: Commander command module for `ps`. Exports: psCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/commands/daemon.js`, `packages/cli/src/commands/status.js`, `packages/cli/src/daemon-lifecycle.js`.
|
||||
- `packages/cli/src/commands/requirements.ts`: Commander command module for `requirements`. Exports: requirementsCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/commands/daemon.js`, `packages/cli/src/commands/status.js`, `packages/cli/src/daemon-lifecycle.js`.
|
||||
- `packages/cli/src/commands/restore.ts`: Commander command module for `restore`. Exports: restoreCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/commands/daemon.js`, `packages/cli/src/commands/status.js`, `packages/cli/src/daemon-lifecycle.js`.
|
||||
- `packages/cli/src/commands/rig.ts`: Commander command module for `rig`. Subcommands: `validate` (POST `/api/rigs/import/validate`), `preflight` (POST `/api/rigs/import/preflight` with `X-Rig-Root` header). Both support `--json`. Exports: RigDeps, rigCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/daemon-lifecycle.js`.
|
||||
- `packages/cli/src/commands/snapshot.ts`: Commander command module for `snapshot`. Exports: snapshotCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/commands/daemon.js`, `packages/cli/src/commands/status.js`, `packages/cli/src/daemon-lifecycle.js`.
|
||||
- `packages/cli/src/commands/status.ts`: Commander command module for `status`. Exports: StatusDeps, statusCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/commands/daemon.js`, `packages/cli/src/daemon-lifecycle.js`.
|
||||
- `packages/cli/src/commands/specs.ts`: Commander command module for the agent-facing spec library workflow. Subcommands: `ls`, `show`, `preview`, `add`, `sync`, plus shared name resolution used by `up` and `bootstrap`. `add` accepts single YAML files and full spec directories containing `rig.yaml` or `agent.yaml`. Help/examples and not-found guidance explicitly call out managed apps. Exports: resolveLibrarySpec, specsCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/daemon-lifecycle.js`.
|
||||
- `packages/cli/src/commands/ui.ts`: Commander command module for `ui`. Exports: UiDeps, uiCommand. Related: `packages/cli/src/commands/daemon.js`, `packages/cli/src/daemon-lifecycle.js`.
|
||||
- `packages/cli/src/commands/up.ts`: Commander command module for `up`. Supports spec paths, bundle paths, rig-name restore, and library-name resolution; fails loudly on rig-name vs library-name ambiguity. Help/examples now treat managed apps like `secrets-manager` as first-class launch targets. Auto-preflight, auto-daemon-start, post-command handoff with attach commands. Exports: upCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/commands/daemon.js`, `packages/cli/src/commands/specs.js`, `packages/cli/src/daemon-lifecycle.js`, `packages/cli/src/system-preflight.js`.
|
||||
- `packages/cli/src/commands/whoami.ts`: Commander command module for `whoami`. Resolves identity from explicit args, env vars, tmux metadata, and session-name fallback, then delegates to daemon `/api/whoami`. Exports: resolveIdentitySource, whoamiCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/daemon-lifecycle.js`.
|
||||
- `packages/cli/src/commands/adopt.ts`: Commander command module for `adopt` — materialize topology from a pod-aware RigSpec, then bind discovered live sessions according to `--bind` mappings. Exports: AdoptDeps, adoptCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/commands/import.js`, `packages/cli/src/daemon-lifecycle.js`.
|
||||
|
||||
### Commands: Post-North-Star Additions
|
||||
- `packages/cli/src/commands/send.ts`: Commander command module for `send` — send message to agent terminal via reliable tmux two-step. Supports `--verify` and `--force`. Exports: sendCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/daemon-lifecycle.js`.
|
||||
- `packages/cli/src/commands/capture.ts`: Commander command module for `capture` — capture agent pane content. Supports `--lines N` and `--json`. Exports: captureCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/daemon-lifecycle.js`.
|
||||
- `packages/cli/src/commands/broadcast.ts`: Commander command module for `broadcast` — send to all agents in a rig or pod. Reports per-session delivery status. Exports: broadcastCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/daemon-lifecycle.js`.
|
||||
- `packages/cli/src/commands/transcript.ts`: Commander command module for `transcript` — composable transcript access via `--tail N` and `--grep "pattern"`. Exports: transcriptCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/daemon-lifecycle.js`.
|
||||
- `packages/cli/src/commands/stream.ts`: Commander command module for `rig stream` (PL-004 Phase A) — coordination L1 intake stream. Subcommands: `emit`, `list`, `show`, `archive`. Speaks only to the daemon HTTP API; does not touch POC filesystem state. Exports: streamCommand, StreamDeps. Related: `packages/cli/src/client.js`, `packages/cli/src/daemon-lifecycle.js`.
|
||||
- `packages/cli/src/commands/queue.ts`: Commander command module for `rig queue` (PL-004 Phase A) — coordination L3 queue plus inbox/outbox. Subcommands: `create`, `claim`, `unclaim`, `update`, `handoff`, `fallback`, `show`, `transitions`, `list`, `overdue`, `inbox-drop`, `inbox-absorb`, `inbox-deny`, `inbox-pending`, `outbox-record`, `outbox-list`. Hot-potato strict-rejection enforced at daemon. Exports: queueCommand, QueueDeps. Related: `packages/cli/src/client.js`, `packages/cli/src/daemon-lifecycle.js`.
|
||||
- `packages/cli/src/commands/config.ts`: Commander command module for `config` — read/write/reset config keys. Supports `--json`. Exports: configCommand. Related: `packages/cli/src/config-store.js`.
|
||||
- `packages/cli/src/commands/preflight.ts`: Commander command module for `preflight` — system readiness checks. Supports `--json`. Exports: preflightCommand. Related: `packages/cli/src/system-preflight.js`, `packages/cli/src/config-store.js`.
|
||||
- `packages/cli/src/commands/ask.ts`: Commander command module for `ask` — context evidence pack over transcripts + topology + chat. Supports `--json`. Exports: askCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/daemon-lifecycle.js`.
|
||||
- `packages/cli/src/commands/chatroom.ts`: Commander command module for `chatroom` — durable group chat with subcommands `send`, `watch`, `history`, and `topic`. Exports: chatroomCommand. Related: `packages/cli/src/client.js`, `packages/cli/src/daemon-lifecycle.js`.
|
||||
|
||||
### CLI Infrastructure: Post-North-Star Additions
|
||||
- `packages/cli/src/config-store.ts`: Config file management at `~/.openrig/config.json` with legacy fallback from `~/.rigged/config.json`. 5 locked keys with env var overrides. Precedence: CLI flag > env var > config file > default. Exports: ConfigStore and config helpers. Related: none (filesystem-only).
|
||||
- `packages/cli/src/system-preflight.ts`: System readiness checks: Node.js version, tmux availability, writable directories, port availability. Every error follows the 3-part pattern (what + why + fix). Exports: SystemPreflight, PreflightCheck, PreflightResult. Related: `packages/cli/src/config-store.js`, `packages/cli/src/daemon-lifecycle.js`.
|
||||
|
||||
## UI
|
||||
|
||||
### App Entry And Routing
|
||||
- `packages/ui/src/App.tsx`: Thin app wrapper that renders the TanStack Router provider. Exports: App. Related: `packages/ui/src/routes.js`.
|
||||
- `packages/ui/src/globals.css`: Tailwind/theme tokens and global UI styling. Exports: none. Related: none.
|
||||
- `packages/ui/src/main.tsx`: Bootstraps the React app, fonts, and global styles. Exports: none. Related: `packages/ui/src/App.js`, `packages/ui/src/globals.css`.
|
||||
- `packages/ui/src/routes.tsx`: Declares the explorer-first route tree and root shell composition, including specs review/library routes, discovery/specs drawer bridges, and live-node full-details route `/rigs/$rigId/nodes/$logicalId`. Exports: router. Related: `packages/ui/src/components/AppShell.js`, `packages/ui/src/components/BootstrapWizard.js`, `packages/ui/src/components/BundleInspector.js`, `packages/ui/src/components/BundleInstallFlow.js`, +13 more.
|
||||
|
||||
### Components: Application Surfaces
|
||||
- `packages/ui/src/components/ActivityFeed.tsx`: Rolling global activity/event overlay driven by SSE. Exports: ActivityFeed. Related: `packages/ui/src/hooks/useActivityFeed.js`.
|
||||
- `packages/ui/src/components/AppShell.tsx`: Persistent explorer/workspace/drawer shell. Owns drawer selection context, discovery placement state, specs workspace provider, explorer visibility, and top-level discovery/specs/system toggles. Exports: DrawerSelectionContext, DrawerSelectionContextValue, DiscoveryPlacementContext, DiscoveryPlacementContextValue, ExplorerVisibilityContext, ExplorerVisibilityContextValue, NodeSelectionContext, AppShell, useDiscoveryPlacement, useDrawerSelection, useExplorerVisibility, useNodeSelection. Related: `packages/ui/src/components/ActivityFeed.js`, `packages/ui/src/components/Explorer.js`, `packages/ui/src/components/SharedDetailDrawer.js`, `packages/ui/src/components/SpecsWorkspace.js`, `packages/ui/src/hooks/useActivityFeed.js`, `packages/ui/src/hooks/useGlobalEvents.js`, `packages/ui/src/hooks/useRigSummary.js`.
|
||||
- `packages/ui/src/components/BootstrapWizard.tsx`: Multi-step bootstrap planning/apply flow UI for both rig specs and `.rigbundle` archives. Includes the spec-or-bundle entry field and bundle-inspector handoff link. Exports: BootstrapWizard. Related: `packages/ui/src/components/RequirementsPanel.js`, `packages/ui/src/hooks/useBootstrap.js`.
|
||||
- `packages/ui/src/components/BundleInspector.tsx`: Bundle inspection screen for manifest and integrity results. Renders `agents[]` plus schema-version badge for v2 pod bundles and legacy package content for v1 bundles. Exports: BundleInspector. Related: `packages/ui/src/hooks/useBundles.js`.
|
||||
- `packages/ui/src/components/BundleInstallFlow.tsx`: Bundle install plan/apply screen. Labels the install as pod-aware or legacy from bootstrap `resolve_spec.detail.source`. Exports: BundleInstallFlow. Related: `packages/ui/src/hooks/useBundles.js`.
|
||||
- `packages/ui/src/components/Dashboard.tsx`: Home dashboard that renders rig cards and high-level rig actions. Exports: Dashboard. Related: `packages/ui/src/components/RigCard.js`, `packages/ui/src/hooks/mutations.js`, `packages/ui/src/hooks/usePsEntries.js`, `packages/ui/src/hooks/useRigSummary.js`.
|
||||
- `packages/ui/src/components/DiscoveryOverlay.tsx`: Session discovery and claim surface. Exports: DiscoveryOverlay. Related: `packages/ui/src/hooks/useDiscovery.js`, `packages/ui/src/hooks/useRigSummary.js`.
|
||||
- `packages/ui/src/components/ImportFlow.tsx`: RigSpec validate/preflight/instantiate flow UI. Supports pod-aware `X-Rig-Root`, structured preflight diagnostics, and rebooted RigSpec terminology. Exports: ImportFlow. Related: `packages/ui/src/hooks/mutations.js`.
|
||||
- `packages/ui/src/components/AgentSpecValidateFlow.tsx`: Standalone validate surface for one AgentSpec YAML, backed by `/api/agents/validate`. Exports: AgentSpecValidateFlow. Related: none.
|
||||
- `packages/ui/src/components/WorkflowScaffold.tsx`: Shared page-shell primitives for multi-step/spec-review flows (`WorkflowHeader`, `WorkflowStepIndicator`, `WorkflowSection`, summary cards, code preview). Exports: WorkflowHeader, WorkflowStepIndicator, WorkflowSection, WorkflowSummaryGrid, WorkflowSummaryCard, WorkflowCodePreview. Related: none.
|
||||
- `packages/ui/src/components/WorkspacePage.tsx`: Shared center-workspace page shell used by specs/library/live-details surfaces. Exports: WorkspacePage. Related: none.
|
||||
- `packages/ui/src/components/SpecsWorkspace.tsx`: Local persisted workspace state for spec authoring/review tasks, drafts, and active task list. Exports: SpecsDraft, SpecsTask, SPECS_WORKSPACE_STORAGE_KEYS, SpecsWorkspaceProvider, useSpecsWorkspace. Related: none.
|
||||
- `packages/ui/src/components/PackageDetail.tsx`: Package detail screen with install history, journal, and rollback actions. Exports: PackageDetail. Related: `packages/ui/src/hooks/usePackageDetail.js`.
|
||||
- `packages/ui/src/components/PackageInstallFlow.tsx`: Legacy package validate/configure/plan/apply flow UI, explicitly labeled `(Legacy)`. Exports: PackageInstallFlow. Related: none.
|
||||
- `packages/ui/src/components/PackageList.tsx`: Legacy package summary list/grid screen presented under the rebooted `SPECS` navigation, with an empty-state handoff into RigSpec import. Exports: PackageList. Related: `packages/ui/src/hooks/usePackages.js`.
|
||||
- `packages/ui/src/components/RequirementsPanel.tsx`: Visualizes requirement probe results inside bootstrap flows. Exports: RequirementResult, RequirementsPanel. Related: none.
|
||||
- `packages/ui/src/components/RigCard.tsx`: Dashboard card for one rig with counts, status, and primary actions. Exports: RigCard, RigSummary. Related: `packages/ui/src/hooks/useCountUp.js`, `packages/ui/src/hooks/usePsEntries.js`.
|
||||
- `packages/ui/src/components/RigGraph.tsx`: Rig graph view that renders topology, discovery overlays, and focus interactions. Exports: RigGraph. Related: `packages/ui/src/components/RigNode.js`, `packages/ui/src/hooks/useDiscovery.js`, `packages/ui/src/hooks/useRigEvents.js`, `packages/ui/src/hooks/useRigGraph.js`.
|
||||
- `packages/ui/src/components/RigNode.tsx`: Custom graph-node renderer for React Flow topology cards. Exports: RigNode. Related: none.
|
||||
- `packages/ui/src/components/Sidebar.tsx`: App navigation sidebar. Exports: Sidebar. Related: none.
|
||||
- `packages/ui/src/components/SnapshotPanel.tsx`: Snapshot list/create/restore panel shown beside rig graphs. Exports: SnapshotPanel. Related: `packages/ui/src/hooks/mutations.js`, `packages/ui/src/hooks/useSnapshots.js`.
|
||||
|
||||
### Components: North Star + Post-North-Star Additions
|
||||
- `packages/ui/src/components/Explorer.tsx`: Hierarchical tree navigator — rigs → pods → agents with status indicators. Primary navigation surface. Click rig loads graph, click node opens detail drawer. Exports: Explorer. Related: `packages/ui/src/hooks/useRigSummary.js`, `packages/ui/src/hooks/usePsEntries.js`, `packages/ui/src/hooks/useNodeInventory.js`.
|
||||
- `packages/ui/src/components/SharedDetailDrawer.tsx`: Unified drawer surface keyed by `DrawerSelection`. Routes to rig, node, system, discovery, or specs panels without leaving the current workspace. Exports: DrawerSelection, SharedDetailDrawer. Related: `packages/ui/src/components/DiscoveryPanel.js`, `packages/ui/src/components/NodeDetailPanel.js`, `packages/ui/src/components/RigDetailPanel.js`, `packages/ui/src/components/SpecsPanel.js`, `packages/ui/src/components/SystemPanel.js`.
|
||||
- `packages/ui/src/components/RigDetailPanel.tsx`: Rig detail: identity, node summary, snapshots/actions, and runtime drawer tabs (`Info`, conditional `Env`, `Chat Room`). Exports: RigDetailPanel. Related: `packages/ui/src/hooks/useRigSummary.js`, `packages/ui/src/hooks/useSnapshots.js`, `packages/ui/src/hooks/useNodeInventory.js`.
|
||||
- `packages/ui/src/components/RigEnvPanel.tsx`: Runtime env surface for service-backed rigs. Shows overall env state, per-service health, health gates, surfaces, logs, and teardown action. Exports: RigEnvPanel. Related: `packages/ui/src/hooks/useRigEnv.js`.
|
||||
- `packages/ui/src/components/NodeDetailPanel.tsx`: Runtime-first node drawer. Shows rig/session/runtime identity, peers, directional edges, transcript helpers, compact spec summary, status, actions (tmux/cmux/resume/full-details), startup files, and recent events. Exports: NodeDetailPanel. Related: `packages/ui/src/components/LiveIdentityDisplay.js`, `packages/ui/src/hooks/useNodeDetail.js`, `packages/ui/src/lib/copy-text.js`, `packages/ui/src/lib/display-name.js`.
|
||||
- `packages/ui/src/components/NodeDetailPlaceholder.tsx`: Empty state for node detail when no node is selected. Exports: NodeDetailPlaceholder. Related: none.
|
||||
- `packages/ui/src/components/DiscoveryPanel.tsx`: Drawer-native discovery/adoption surface. Supports scan/list, draft-rig generation, placement-first bind/adopt, and pod/member targeting inside the shared drawer. Exports: DiscoveryPlacementTarget, DiscoveryPanel. Related: `packages/ui/src/hooks/useDiscovery.js`.
|
||||
- `packages/ui/src/components/SpecsPanel.tsx`: Drawer-native spec library and authoring launcher. Presents a unified library list with `All / Apps / Rigs / Agents`, type/stability badges, richer managed-app rows, and review-route handoff. Exports: SpecsPanel. Related: `packages/ui/src/components/SpecsWorkspace.js`, `packages/ui/src/hooks/useSpecLibrary.js`.
|
||||
- `packages/ui/src/components/RigChatPanel.tsx`: Chat room tab in rig drawer — send messages, watch live stream, view history. Exports: RigChatPanel. Related: `packages/ui/src/hooks/useRigChat.js`.
|
||||
- `packages/ui/src/components/LiveIdentityDisplay.tsx`: Reusable runtime-identity subsection for peers, edges, transcript helpers, and compact spec summary. Used by the node drawer and full-details route. Exports: LiveIdentityDisplay. Related: none.
|
||||
- `packages/ui/src/components/LiveNodeDetails.tsx`: Explicit center-workspace route for deep inspection of one live node. Composes runtime identity plus reusable spec display components. Exports: LiveNodeDetails. Related: `packages/ui/src/components/AgentSpecDisplay.js`, `packages/ui/src/components/LiveIdentityDisplay.js`, `packages/ui/src/hooks/useNodeDetail.js`.
|
||||
- `packages/ui/src/components/RigSpecDisplay.tsx`: Canonical RigSpec display primitive used by draft review, library review, and live/workspace inspection flows. Supports a library-only managed-app Environment tab with compose preview, health gates, and surfaces. Exports: RigSpecDisplay. Related: `packages/ui/src/components/SpecTopologyPreview.js`.
|
||||
- `packages/ui/src/components/AgentSpecDisplay.tsx`: Canonical AgentSpec display primitive used by preview, library, and live-details flows. Exports: AgentSpecDisplay. Related: none.
|
||||
- `packages/ui/src/components/RigSpecReview.tsx`: Route-level rig review page that consumes `RigSpecDisplay` and specs workspace state. Exports: RigSpecReview. Related: `packages/ui/src/components/RigSpecDisplay.js`, `packages/ui/src/components/SpecsWorkspace.js`, `packages/ui/src/hooks/useSpecReview.js`.
|
||||
- `packages/ui/src/components/AgentSpecReview.tsx`: Route-level agent review page that consumes `AgentSpecDisplay` and specs workspace state. Exports: AgentSpecReview. Related: `packages/ui/src/components/AgentSpecDisplay.js`, `packages/ui/src/components/SpecsWorkspace.js`, `packages/ui/src/hooks/useSpecReview.js`.
|
||||
- `packages/ui/src/components/LibraryReview.tsx`: Library-entry review page that renders reusable display components plus provenance/source-state. Service-backed rigs surface managed-app copy, specialist identity, setup-prompt copy, and env-aware review details. Exports: LibraryReview. Related: `packages/ui/src/components/AgentSpecDisplay.js`, `packages/ui/src/components/RigSpecDisplay.js`, `packages/ui/src/hooks/useSpecLibrary.js`.
|
||||
- `packages/ui/src/components/SpecTopologyPreview.tsx`: Lightweight topology preview for structured rig review models. Exports: SpecTopologyPreview. Related: none.
|
||||
- `packages/ui/src/components/SystemPanel.tsx`: Drawer surface for system-wide event log/status tabs. Replaces the old status-bar role with a richer right-drawer system surface. Exports: SystemPanel. Related: `packages/ui/src/hooks/useActivityFeed.js`.
|
||||
- `packages/ui/src/components/WorkspaceHome.tsx`: Landing page when no rig is selected. Entry point for explorer-first workflow. Exports: WorkspaceHome. Related: `packages/ui/src/components/AppShell.js`.
|
||||
|
||||
### Components: UI Primitives
|
||||
- `packages/ui/src/components/ui/alert.tsx`: Wrapped/customized shadcn-style UI primitive for alert. Exports: Alert, AlertDescription, AlertTitle. Related: none.
|
||||
- `packages/ui/src/components/ui/badge.tsx`: Wrapped/customized shadcn-style UI primitive for badge. Exports: Badge, BadgeProps, badgeVariants. Related: none.
|
||||
- `packages/ui/src/components/ui/button.tsx`: Wrapped/customized shadcn-style UI primitive for button. Exports: Button, ButtonProps, buttonVariants. Related: none.
|
||||
- `packages/ui/src/components/ui/card.tsx`: Wrapped/customized shadcn-style UI primitive for card. Exports: Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle. Related: none.
|
||||
- `packages/ui/src/components/ui/dialog.tsx`: Wrapped/customized shadcn-style UI primitive for dialog. Exports: Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger. Related: none.
|
||||
- `packages/ui/src/components/ui/input.tsx`: Wrapped/customized shadcn-style UI primitive for input. Exports: Input. Related: none.
|
||||
- `packages/ui/src/components/ui/separator.tsx`: Wrapped/customized shadcn-style UI primitive for separator. Exports: Separator. Related: none.
|
||||
- `packages/ui/src/components/ui/table.tsx`: Wrapped/customized shadcn-style UI primitive for table. Exports: Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow. Related: none.
|
||||
- `packages/ui/src/components/ui/tabs.tsx`: Wrapped/customized shadcn-style UI primitive for tabs. Exports: Tabs, TabsContent, TabsList, TabsTrigger. Related: none.
|
||||
- `packages/ui/src/components/ui/textarea.tsx`: Wrapped/customized shadcn-style UI primitive for textarea. Exports: Textarea. Related: none.
|
||||
- `packages/ui/src/components/ui/tooltip.tsx`: Wrapped/customized shadcn-style UI primitive for tooltip. Exports: Tooltip, TooltipContent, TooltipProvider, TooltipTrigger. Related: none.
|
||||
|
||||
### Hooks
|
||||
- `packages/ui/src/hooks/mutations.ts`: Shared mutation hooks for rig import, teardown, snapshot creation, and restore. `useImportRig` supports optional `rigRoot` and sends `X-Rig-Root` for pod-aware import. Exports: ImportError, useCreateSnapshot, useImportRig, useRestoreSnapshot, useTeardownRig. Related: none.
|
||||
- `packages/ui/src/hooks/useActivityFeed.ts`: SSE-backed activity-feed model and event-formatting helpers. Exports: ActivityEvent, UseActivityFeedResult, eventColor, eventRoute, eventSummary, formatRelativeTime, useActivityFeed. Related: none.
|
||||
- `packages/ui/src/hooks/useBootstrap.ts`: Bootstrap plan/apply hooks. Exports: BootstrapApplyResult, BootstrapPlanResult, useBootstrapApply, useBootstrapPlan. Related: none.
|
||||
- `packages/ui/src/hooks/useBundles.ts`: Bundle inspect/install hooks. Typed for v2 inspect responses (`schemaVersion`, `agents`, integrity) and install-stage `detail.source` labeling. Exports: BundleInstallResult, InspectResult, useBundleInspect, useBundleInstall. Related: none.
|
||||
- `packages/ui/src/hooks/useCountUp.ts`: Small animation helper for count-up telemetry. Exports: useCountUp. Related: none.
|
||||
- `packages/ui/src/hooks/useDiscovery.ts`: Discovery polling plus claim/bind/adopt mutations and draft-rig generation helpers. Exports: DiscoveredSession, useAdoptSession, useBindSession, useClaimSession, useDiscoveredSessions, useDiscoveredSessionsConditional, useDiscoveryPoll, useDiscoveryScan. Related: none.
|
||||
- `packages/ui/src/hooks/usePackageDetail.ts`: Package detail, journal, history, and rollback hooks. Exports: InstallSummary, JournalEntry, PackageInfo, useInstallHistory, useJournalEntries, usePackageInfo, useRollbackInstall. Related: none.
|
||||
- `packages/ui/src/hooks/usePackages.ts`: Package summary list hook. Exports: PackageSummary, usePackages. Related: none.
|
||||
- `packages/ui/src/hooks/usePsEntries.ts`: `/api/ps` summary hook. Exports: PsEntry, usePsEntries. Related: none.
|
||||
- `packages/ui/src/hooks/useRigEvents.ts`: Rig-scoped SSE subscription and invalidation hook. Exports: UseRigEventsResult, useRigEvents. Related: none.
|
||||
- `packages/ui/src/hooks/useRigGraph.ts`: Rig graph fetch/transformation hook. Exports: useRigGraph. Related: none.
|
||||
- `packages/ui/src/hooks/useRigSummary.ts`: Rig summary list hook used across dashboard and shell surfaces. Includes `hasServices` so runtime/UI can gate managed-app env surfaces. Exports: RigSummary, useRigSummary. Related: none.
|
||||
- `packages/ui/src/hooks/useSnapshots.ts`: Snapshot list hook for one rig. Exports: Snapshot, useSnapshots. Related: none.
|
||||
|
||||
### Hooks: North Star + Post-North-Star Additions
|
||||
- `packages/ui/src/hooks/useNodeInventory.ts`: Fetches node inventory for a rig from the canonical `GET /api/rigs/:rigId/nodes` endpoint. Exports: useNodeInventory. Related: none.
|
||||
- `packages/ui/src/hooks/useNodeDetail.ts`: Fetches extended node detail from `GET /api/rigs/:rigId/nodes/:logicalId`, including peers, edges, transcript helpers, and compact spec summary for the adopted-parity/live-identity drawer. Exports: NodeDetailData, useNodeDetail. Related: none.
|
||||
- `packages/ui/src/hooks/useRigEnv.ts`: Fetches `GET /api/rigs/:rigId/env` for service-backed rig status, surfaces, and receipt data, with runtime-friendly refetch behavior. Exports: RigEnvData, useRigEnv. Related: none.
|
||||
- `packages/ui/src/hooks/useRigChat.ts`: Chat send/watch/history hooks for the rig chat panel. Exports: useRigChat. Related: none.
|
||||
- `packages/ui/src/hooks/useGlobalEvents.ts`: Global SSE subscription for explorer status updates. Exports: useGlobalEvents. Related: none.
|
||||
- `packages/ui/src/hooks/useSpecLibrary.ts`: Spec library list and library-review fetch hooks. Typed for `sourceState: "library_item"` review results and `hasServices`-aware managed-app classification. Exports: SpecLibraryEntry, LibraryReview, LibraryRigReview, LibraryAgentReview, useSpecLibrary, useLibraryReview. Related: none.
|
||||
- `packages/ui/src/hooks/useSpecReview.ts`: Structured spec-review hooks for raw YAML preview (`rig` and `agent`) over `/api/specs/review/*`. The rig review type now carries managed-app services metadata and compose preview. Exports: SpecGraphData, RigSpecReview, AgentSpecReview, useRigSpecReview, useAgentSpecReview. Related: none.
|
||||
|
||||
### Lib Utilities
|
||||
- `packages/ui/src/lib/edge-styles.ts`: Edge-style resolver for graph links. Exports: EdgeStyleResult, getEdgeStyle. Related: none.
|
||||
- `packages/ui/src/lib/graph-layout.ts`: Tree-layout helper for rig graph positioning. Exports: applyTreeLayout. Related: none.
|
||||
- `packages/ui/src/lib/instantiate-status-colors.ts`: Color-class lookup for instantiation states. Exports: getInstantiateStatusColorClass. Related: none.
|
||||
- `packages/ui/src/lib/query-client.ts`: Shared TanStack Query client singleton. Exports: queryClient. Related: none.
|
||||
- `packages/ui/src/lib/restore-status-colors.ts`: Color-class lookup for restore states. Exports: getRestoreStatusColorClass. Related: none.
|
||||
- `packages/ui/src/lib/status-colors.ts`: Generic rig/session status color mapping. Exports: getStatusColorClass. Related: none.
|
||||
- `packages/ui/src/lib/utils.ts`: Shared UI utility helpers, currently `cn` class merging. Exports: cn. Related: none.
|
||||
|
||||
### Lib Utilities: North Star + Post-North-Star Additions
|
||||
- `packages/ui/src/lib/display-id.ts`: Short ULID display utility — uses ULID tail (not head) for better uniqueness in same-view contexts. UI-only, no backend changes. Exports: shortId. Related: none.
|
||||
- `packages/ui/src/lib/display-name.ts`: Pod name extraction from qualified logical IDs (e.g., "dev1" from "dev1.impl"). Exports: displayPodName, inferPodName. Related: none.
|
||||
- `packages/ui/src/lib/build-setup-prompt.ts`: Managed-app setup-prompt generator used by library review copy surfaces. Exports: buildSetupPrompt. Related: none.
|
||||
- `packages/ui/src/lib/copy-text.ts`: Clipboard copy utility for the "Copy tmux attach" and "Copy resume command" buttons. Exports: copyText. Related: none.
|
||||
@@ -1,73 +0,0 @@
|
||||
---
|
||||
kind: as-built
|
||||
title: As-Built Frontmatter
|
||||
status: active
|
||||
topics: [knowledge-and-context, frontmatter]
|
||||
domains: [engineering-advisor, operating-advisor]
|
||||
applies-when: |
|
||||
Writing or refreshing a source-grounded document under docs/as-built/.
|
||||
last-verified-against-source: e8f0ab340db773392ec8be75b072d1c0f3068a50
|
||||
last-updated: 2026-10-08
|
||||
---
|
||||
|
||||
# As-Built Frontmatter
|
||||
|
||||
Source snapshot: `e8f0ab340db773392ec8be75b072d1c0f3068a50`. This describes the source at that commit;
|
||||
it does not establish the version or behavior of a running daemon.
|
||||
|
||||
Frontmatter makes an as-built page discoverable and records the source it
|
||||
was checked against. This page describes the convention visible in this
|
||||
repository. It does not require an unpublished schema or an internal
|
||||
workspace to interpret the fields.
|
||||
|
||||
## Fields
|
||||
|
||||
The corpus uses these fields; see the [module index](README.md) and
|
||||
[CLI reference](cli-reference.md) for examples.
|
||||
|
||||
| Field | Meaning |
|
||||
|---|---|
|
||||
| `kind: as-built` | Identifies a description of implemented behavior. |
|
||||
| `title` | Human-readable subject of the page. |
|
||||
| `status` | Document status, such as `active`; not a daemon or feature status. |
|
||||
| `applies-when` | Short retrieval cue explaining when to open the page. |
|
||||
| `topics`, `domains` | Lists of indexing labels used in this corpus. They do not select permissions or runtime behavior. |
|
||||
| `siblings`, `prerequisite-reads` | Related pages and reading order, when useful. Paths are relative to the containing page. |
|
||||
| `last-verified-against-source` | A resolvable Git commit whose source was used to check the page. |
|
||||
| `last-updated` | Date of the document update, in `YYYY-MM-DD` form. |
|
||||
|
||||
## Verification stamp
|
||||
|
||||
Use a commit ID, preferably full length, rather than a branch name or a
|
||||
moving `HEAD` label. A source stamp identifies what was read; it is not proof
|
||||
that an installed package or a running system uses that source. Keep runtime
|
||||
observations separately qualified.
|
||||
|
||||
```yaml
|
||||
last-verified-against-source: e8f0ab340db773392ec8be75b072d1c0f3068a50
|
||||
last-updated: 2026-10-08
|
||||
```
|
||||
|
||||
Check that the commit resolves with `git cat-file -t <commit>` in a checkout
|
||||
containing that history. Verify the paths, symbols, and numeric claims cited
|
||||
by the page at that commit before changing its stamp. A newer date alone
|
||||
does not refresh the content. Missing history in a shallow checkout and an
|
||||
invalid commit ID are different conditions.
|
||||
|
||||
Use repository links and named symbols for source references. For numeric
|
||||
inventories, state what is counted and how it was obtained; for example, the
|
||||
CLI reference distinguishes registered command objects from aliases and
|
||||
Commander-generated help.
|
||||
|
||||
## What the repository guard checks
|
||||
|
||||
[`scripts/check-docs-guard.mjs`](../../scripts/check-docs-guard.mjs)
|
||||
checks allowed tracked documentation paths. Its `findBlockedDocsPaths`
|
||||
function does **not** validate this frontmatter or prove the prose matches
|
||||
source. Passing that guard should not be reported as a schema or factual
|
||||
accuracy check.
|
||||
|
||||
[As-built pages](README.md) and [operator reference pages](../reference/help.md)
|
||||
serve different purposes. `docs/reference/` is staged by
|
||||
[`scripts/build-package.sh`](../../scripts/build-package.sh); placing content
|
||||
there changes what is distributed in the package.
|
||||
@@ -1,628 +0,0 @@
|
||||
---
|
||||
kind: as-built
|
||||
title: Test layers — what to run before you push, and what each layer proves
|
||||
status: active
|
||||
applies-when: |
|
||||
You changed OpenRig and want to know which checks to run before opening a pull
|
||||
request, what each check actually proves, and what CI will run for you. Also
|
||||
read it before claiming a stub-agent scenario covers a behaviour.
|
||||
siblings: [arteries.md, README.md, codemap.md]
|
||||
last-verified-against-source: e8f0ab340db773392ec8be75b072d1c0f3068a50
|
||||
last-updated: 2026-11-09
|
||||
---
|
||||
|
||||
# Test layers: what to run before you push, and what each layer proves
|
||||
|
||||
> **This page is a map, not the territory.** It was checked against source at `e8f0ab34`
|
||||
> and it will drift. It is also incomplete on purpose. Before you rely on a command or a
|
||||
> claim here, read the script it names or run it. If a behaviour is missing from this page
|
||||
> (or from [arteries.md](arteries.md)), that tells you nothing about whether a change to it
|
||||
> is safe.
|
||||
|
||||
## Short version
|
||||
|
||||
For every change, run what the pull-request template asks for:
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
npm run lint # typecheck only; there is no formatter or style linter
|
||||
npm test # = npm run test:repo && npm run test:workspaces
|
||||
```
|
||||
|
||||
Then add the layers your change calls for, using the ladder below. CI also runs a
|
||||
network-denied macOS test pass and a containerised installed-package scenario on every PR
|
||||
to `main`. In the PR description, say which layers you ran and which you could not run.
|
||||
|
||||
## The ladder
|
||||
|
||||
| Layer | Command | What it proves | What it cannot prove | When to run it |
|
||||
|---|---|---|---|---|
|
||||
| Typecheck | `npm run lint` | Builds the daemon, then `tsc --noEmit` passes for daemon, ui, cli and tui | Any runtime behaviour. Formatting. | Every change |
|
||||
| Build | `npm run build` | All four workspaces compile (the UI also runs `vite build`) | That the *published* package is complete (see Packaging) | Every change |
|
||||
| Repo checks | `npm run test:repo` | Builds the daemon, then runs `node --test --test-concurrency=1 scripts/*.test.mjs`, `scripts/check-first-run-references.mjs`, `scripts/check-docs-guard.mjs`, `mirror-skills --check` (skill homes, generated copies, layout and digests) and `generate-context-packs --check` | Package behaviour | Every change. It matters most when you touch `scripts/`, `docs/` or skills. |
|
||||
| Package suites | `npm run test:workspaces`. One package: `npm run test -w packages/cli`. One file, from the repo root: `npx vitest run packages/cli/test/<file>.test.ts` | Vitest unit and integration tests for daemon, cli and tui. Some of them start real scenario daemons, tmux sessions and stub runners. | The installed package. Real Claude Code or Codex. CI's sandbox conditions (see CI). | Every change (`npm test` covers it) |
|
||||
| Web UI suite | `npm run test:ui` | jsdom unit tests for `packages/ui` | Real browser rendering | When you touch `packages/ui`, or a daemon API the UI reads. The web UI is in maintenance mode, so this suite is advisory locally and is not part of `npm test`. |
|
||||
| Generated files | `npm run mirror-skills:check`, `npm run generate-context-packs:check` (both part of `npm test`) | Shipped skill homes are publishable, generated copies match their homes, all edges match their recorded hashes, and every static context pack passes the daemon's own manifest parser and the leak scan | Whether the skill content is any good | Edit a shipped skill's one in-repo home, run `npm run mirror-skills` to regenerate its other copies and refresh digests, then run the checks and commit the result. The apply reads nothing outside the repository. See "A shipped skill" in [ARCHITECTURE.md](../../ARCHITECTURE.md#a-shipped-skill). |
|
||||
| Local gate | `npm run gate` | Lint plus `npm test` on a clean tree, tied to `HEAD`, one run per machine at a time, with a JSON receipt | Anything that lint and `npm test` don't already prove | Optional. Useful when several agents share one machine, or when you want a receipt tied to a commit. |
|
||||
| Packaging | `npm run build:package`, `npm run test:tui-package`, `bash scripts/smoke-fresh-install.sh` | The publishable `@openrig/cli` assembles, the packed TUI launches, and a fresh install starts a daemon | Behaviour beyond startup | When you touch dependencies, bin entries, build scripts, or files a package ships |
|
||||
| Stub scenarios, host mode | `node --import tsx packages/daemon/scripts/run-scenarios.mjs [scenario.yaml …]` | A scenario's steps and assertions pass against a scratch daemon, real tmux and `runtime: stub` seats | The installed package. Seeded-regression pairing. Everything under [What a stub can and cannot prove](#what-a-stub-can-and-cannot-prove). | While you write or debug a scenario |
|
||||
| Stub scenarios, installed package in a container | CI job `installed-scenario`. Before pushing: `DOCKER_HOST=ssh://<your-host> bash scripts/run-pr-scenarios.sh --remote …` | A clean Linux install starts. A claimed queue item survives a daemon restart. A planted fault is caught at the intended assertion. Two stub seats' scripted output reads back through `rig transcript` and `rig capture` (passing form only). | Native runtimes. Most command families (see [Help wanted](#help-wanted-command-families-without-a-behavioural-scenario)). | Changes that can affect daemon startup, packaging, queue durability or the scenario harness |
|
||||
| Live-model evals | `npm run eval -w packages/daemon -- --provider rig …` | Whether a real Claude Code seat runs the expected `rig context get <entry>` for natural prompts | Determinism. Codex. Anything without a case. | Changes to shipped skills or context entries meant to steer agents. **Uses provider credits.** |
|
||||
| Testbed runbooks | `docker/testbed/runbooks/` (manual) | The testbed image itself works as a test environment | Product behaviour | Only when you change the testbed image or the container plumbing |
|
||||
|
||||
Some package tests are gated. `OPENRIG_E2E_REAL_CODEX=1` runs two daemon e2e tests against a
|
||||
real Codex (they also need tmux, `codex` and `~/.codex/auth.json`, and they use provider
|
||||
credits). `OPENRIG_REAL_CLAUDE_INTEGRATION=1` enables only a placeholder: it requires
|
||||
`OPENRIG_PARENT_NATIVE_ID` and checks its format, and no real-Claude fork test exists yet.
|
||||
Without tmux, some tmux tests skip and others (such as `transcript-seat-env-native.test.ts`)
|
||||
fail. The CLI's release-tag tests skip without local tags unless `CI=true`, so a green local
|
||||
run without tags has skipped them. CI installs tmux and fetches full history.
|
||||
|
||||
The TUI has its own vitest suite (`npm run test -w packages/tui`, which builds the TUI first)
|
||||
and runs in the `tui` leg of `package-tests`; `npm run test:tui-package` launches the packed
|
||||
TUI in demo mode. The `tui_socket` scenario surface exists, but the only scenarios that use it
|
||||
are not yet runnable checks.
|
||||
|
||||
### Gotchas that cost people time
|
||||
|
||||
- **Running inside a seat.** The root `vitest.config.ts` makes a root-level file run use its
|
||||
package's config. The cli, daemon and TUI configs all run the shared root
|
||||
`test/hermetic-env.setup.ts` before any test file. It clears the inherited `OPENRIG_*` and `RIGGED_*` instance selectors (connection,
|
||||
database, workspace and topology roots, seat identity, bearer tokens), forces a fixture
|
||||
`OPENRIG_HOME` and fails if the home is not fixture-scoped. It also wraps `fetch`, so a real
|
||||
request to anything other than a loopback ephemeral-port fixture or a registered target fails.
|
||||
It points `HOME` and the XDG directories at a fixture user home and clears `CODEX_HOME` and
|
||||
`CLAUDE_CONFIG_DIR`. `OPENRIG_E2E_REAL_CODEX=1` keeps native homes and binaries only for
|
||||
`seat-handover-model-fidelity-e2e.test.ts` and `seat-lifecycle-set-model-resume-e2e.test.ts`;
|
||||
all other files remain isolated. The scenario scaffold always uses fixture homes and runtime stubs.
|
||||
The two native files rely on Vitest's default per-file isolation; runs without it are unsupported.
|
||||
Scenario scaffolds scrub inherited provider roots but preserve those passed in an explicit `baseEnv`.
|
||||
A test can still set its own
|
||||
overrides after setup. The UI suite (jsdom) uses its own `packages/ui/test/setup.ts`.
|
||||
|
||||
- **Stale vendored daemon.** `npm run build:package` leaves an assembled copy of the daemon at
|
||||
`packages/cli/daemon/` (gitignored). If you change daemon source afterwards,
|
||||
`scripts/check-cli-daemon-freshness.test.mjs` inside `test:repo` can fail on the stale copy.
|
||||
Re-run `npm run build:package` or delete `packages/cli/daemon/`. `npm run gate` deletes it for you.
|
||||
- **Local passes, CI fails.** CI runs the package suites with a scratch `HOME`, no
|
||||
credentials, and outbound network blocked except to localhost (see below). A test that reads
|
||||
your real home directory, a running daemon, the network or a credential can pass on your
|
||||
machine and fail in CI.
|
||||
- **Mixed fixture clocks.** `compaction-command-submission.test.ts` uses one clock for the
|
||||
enforcer, the transport's injected `now` and sleeps, and its `Date.now` idle deadline. Previously,
|
||||
real-time transport waits could exhaust the fixture's 20 ms budget during a scheduler pause
|
||||
even though the modeled seat was idle. Restore the `Date.now` spy after each test; keep real
|
||||
deadline behavior covered by the transport tests. This changes the fixture, not production timeouts.
|
||||
- **Docs location.** `check-docs-guard` fails if a tracked file under `docs/` sits outside
|
||||
`docs/as-built/`, `docs/reference/` or `docs/releases/`. `docs/DESIGN.md` is the one allowed
|
||||
exception.
|
||||
|
||||
## What CI runs on your pull request
|
||||
|
||||
`.github/workflows/tests.yml` runs on pull requests to `main` and on manual dispatch, all on
|
||||
Node 22. Each suite is its own job, so one failure doesn't hide the others. No provider
|
||||
credentials are supplied.
|
||||
|
||||
| Job | Runner | What it does |
|
||||
|---|---|---|
|
||||
| `build-and-package` | ubuntu | `npm ci` and `npm run build`. Then `npm pack --dry-run` for cli, daemon and tui, with each file list uploaded as the `pack-lists` artifact. The job only records the lists; reviewers read them. |
|
||||
| `typecheck` | ubuntu | `npm run lint` |
|
||||
| `repo-checks` | ubuntu | `npm run test:repo` |
|
||||
| `package-tests` | macOS, matrix `daemon`, `cli`, `tui`, `ui` (fail-fast off) | Installs tmux, runs `npm ci` and `npm run build`. Then runs `npm run test -w packages/<pkg>` under `env -i` with a scratch `HOME`/`TMPDIR`/`TMUX_TMPDIR` and a `sandbox-exec` profile that blocks outbound network except localhost and the job's own socket directory. `scripts/ci-macos-process-probe.mjs` runs first. |
|
||||
| `linux-seat-environment` | ubuntu | Builds all packages, then runs `seat-env-parent-session.integration.test.ts` with real CLI, daemon, tmux and stub seats. A network namespace has loopback only; tests run as the unprivileged runner user under `env -i` with scratch `HOME`/`TMPDIR`/`TMUX_TMPDIR`. Runs on pull requests and manual dispatch, like `package-tests`. |
|
||||
| `installed-scenario` | ubuntu | `bash scripts/run-pr-scenarios.sh` (fifteen container runs, described below). Uploads `dist/pr-scenarios/` as `installed-scenario-evidence`, including on failure. |
|
||||
|
||||
The Linux seat-environment job covers a gap in the macOS matrix: the integration test
|
||||
reads `/proc/<pid>/environ`, so macOS skips it. It checks both a new tmux server and an
|
||||
existing server carrying a parent's session identity. It verifies the scoped environment,
|
||||
the runtime directory and disabled session-bus default, and tmux's terminal identity in
|
||||
the launched panes; the existing owner session stays unchanged. This adds one focused
|
||||
Linux job to the existing suite. Stub seats prove the environment passed to the pane;
|
||||
native Claude image paste and Shift+Enter handling still need separate verification
|
||||
(see [#2041](https://github.com/mvschwarz/openrig/issues/2041)).
|
||||
|
||||
A second workflow, `.github/workflows/portability-report.yml` (job `portability-report`),
|
||||
lists lines your PR adds that contain machine-specific values: credentials, home or temp
|
||||
paths, network addresses, email addresses. It never fails because of a finding. To see the
|
||||
matched text locally (credentials show only their first characters), run
|
||||
`node scripts/portability-report.mjs`. By default it compares the
|
||||
merge-base with `origin/main` against `HEAD`; pass `--staged` to check staged changes instead.
|
||||
|
||||
The UI suite is a CI matrix leg even though it is advisory locally. Check your PR's status to
|
||||
see whether a red UI leg is treated as blocking. Branch-protection settings are not in the
|
||||
repository.
|
||||
|
||||
## The local gate: `npm run gate`
|
||||
|
||||
`scripts/gate-lane.mjs` runs two legs: `npm run lint` and `npm run test`. It does not run
|
||||
`test:ui`, the scenarios or the packaging checks. On top of those legs it:
|
||||
|
||||
- holds a machine-wide lock on localhost port 40404 (override with `OPENRIG_GATE_LANE_PORT`).
|
||||
A second gate exits 2 straight away instead of waiting.
|
||||
- refuses a dirty worktree. Refuses a `node_modules` that is a symlink, or `@openrig/*`
|
||||
workspace links that resolve outside this worktree. Both are common with shared git worktrees.
|
||||
- deletes the stale vendored daemon bundle at `packages/cli/daemon/` before the legs run.
|
||||
- writes `gate-lane-verdict.json` at the repo root, or at `OPENRIG_GATE_VERDICT`, which must
|
||||
point inside the current worktree (a path outside it exits 3 before the legs run). The file holds the `HEAD` SHA, per-leg results and durations, and a note of other
|
||||
`node`/`vitest`/`tsc` processes running at the time. It fails if `HEAD` changes during the
|
||||
run. The file is not gitignored, so don't commit it.
|
||||
|
||||
Exit codes: 0 pass, 1 a leg failed, 2 lane busy, 3 the runner itself errored.
|
||||
`OPENRIG_GATE_LANE_SMOKE=1` skips the legs to test the wiring only, and the verdict records
|
||||
`smoke: true`.
|
||||
|
||||
## Stub-agent scenarios
|
||||
|
||||
### What's in the box
|
||||
|
||||
| Piece | Where |
|
||||
|---|---|
|
||||
| `runtime: stub` adapter | `packages/daemon/src/adapters/stub-runtime-adapter.ts`, with `stub-runner.ts` (the process that runs in the pane), `stub-script.ts` (the launch-script format) and `stub-compaction.ts` / `stub-restore.ts` |
|
||||
| Scenario format and runner | `packages/daemon/test/helpers/scenario-*.ts`. Step verbs: `up`, `down`, `send`, `restart`, `daemon`, `seed_regression` are bound. `restore`, `mutate`, `policy` are rejected at load time (`UNBOUND_STEP_VERB`) because the runner has no shipped binding for them; `emit` parses but only `refocus` is bound. Assertion surfaces: `ps`, `queue`, `stream`, `scope`, `pane`, `transcript`, `tui_socket`, `policy_provenance`. |
|
||||
| Scenario library | `packages/test-system/scenarios/`: 20 scenarios, 7 stub topologies, `agents/`, `culture.md`, per-seat stub scripts in `scripts/` |
|
||||
| Runner fixtures | `packages/daemon/test/fixtures/scenarios/` (`scenario-01-per-seat-scripts`, `scenario-02-baton`, `scenario-10-one-view-state`) |
|
||||
| Host runner | `packages/daemon/scripts/run-scenarios.mjs` |
|
||||
| In-container entry and result check | `packages/test-system/ci/run.mjs`, `packages/test-system/ci/result.mjs` |
|
||||
| PR orchestrator | `scripts/run-pr-scenarios.sh` |
|
||||
| Image | `docker/testbed/Dockerfile`, built by `scripts/build-testbed-image.sh`; `docker/testbed/Dockerfile.scenarios` is layered on top by `scripts/run-pr-scenarios.sh` |
|
||||
|
||||
### Host mode: while you're writing a scenario
|
||||
|
||||
The tmux server selection regression also uses these scaffold and lifecycle helpers
|
||||
in `packages/daemon/test/tmux-server-isolation-daemon.test.ts`. It runs a real stub
|
||||
seat through config changes, daemon restarts, capture, self-attachment and cleanup,
|
||||
while native socket observations verify that a personal namesake survives. The
|
||||
test also removes the effective-server record and verifies that `destroy --all`
|
||||
leaves the daemon, database and both servers' sessions intact before restoring
|
||||
the record and checking normal cleanup. The
|
||||
YAML grammar has no config-write or socket-inventory surface, so these assertions
|
||||
live in the Vitest integration test. It does not exercise a real provider model,
|
||||
SSH connection or graphical terminal window.
|
||||
|
||||
`packages/daemon/test/tmux-server-two-instances.test.ts` adds two real daemons
|
||||
sharing a scratch OS home and inherited personal tmux server, with separate
|
||||
OpenRig homes, databases and ports. Both enable isolation and control mode. It
|
||||
checks same-named seats through inventory, capture and verified writes, observes
|
||||
the marked control session on each selected socket, then stops one instance and
|
||||
checks that the other instance's pane identity, control session and delivery still
|
||||
work. The personal namesake remains untouched. This test requires a tmux version
|
||||
that supports control mode and the built CLI and daemon. Like CI's package suites,
|
||||
it should run with scratch homes and outbound network restricted to localhost;
|
||||
scripted seats prove terminal routing and rendered delivery, not model receipt.
|
||||
|
||||
```sh
|
||||
npm run build # the runner drives packages/cli/dist/bin-wrapper.js
|
||||
node --import tsx packages/daemon/scripts/run-scenarios.mjs # the fixture scenario-*.yaml files
|
||||
node --import tsx packages/daemon/scripts/run-scenarios.mjs packages/daemon/test/fixtures/scenarios/scenario-02-baton.yaml
|
||||
```
|
||||
|
||||
- The runner creates a scratch `HOME`/`OPENRIG_HOME`, its own tmux server and its own
|
||||
daemon. It builds the scenario environment from `HOME`, `PATH` and `TERM` only, so your
|
||||
shell's daemon-target variables (`OPENRIG_URL`, `OPENRIG_PORT` and similar), `TMUX` and
|
||||
`OPENRIG_TEST_CLOCK_NOW` are dropped rather than inherited. It does not touch the daemon or
|
||||
tmux server your own session uses.
|
||||
- It accepts YAML paths only. Any flag, including `--container`, is refused before anything runs.
|
||||
- It supplies no fault controller, so any scenario with a `seed_regression` step fails loudly
|
||||
at that step. That includes the library's `queue-baton-survives-restart`. Use the container
|
||||
path to run the seeded pair.
|
||||
- `scenario-10-one-view-state` declares its own normaliser for its cross-surface `equals`
|
||||
check. Whether it passes in host mode has not been verified.
|
||||
|
||||
### Container mode: the scenario pack CI runs
|
||||
|
||||
In CI the `installed-scenario` job runs `scripts/run-pr-scenarios.sh` with no arguments:
|
||||
both baton cases (`fixture` = `scenario-02-baton.yaml`, `library` = `queue-baton-survives-restart.yaml`),
|
||||
each run three times as healthy, then lost-baton, then healthy (six containers); the
|
||||
`refocus-parked` effect case runs healthy, seeded, then healthy (three containers); and the
|
||||
six passing-only cases (`transcript` = `transcript-reads-addressed-seat.yaml`, `capture` =
|
||||
`capture-returns-addressed-seat.yaml`, `daemon-restart`, `refocus-active`,
|
||||
`refocus-parked-off`, `prompt-state`) run once each, healthy only (six containers). That is
|
||||
fifteen fresh containers.
|
||||
|
||||
To run a case before you push, point `DOCKER_HOST` at a Docker engine you control over SSH:
|
||||
|
||||
```sh
|
||||
npm ci
|
||||
DOCKER_HOST=ssh://<your-host> bash scripts/run-pr-scenarios.sh \
|
||||
--remote --case library --mode healthy --out dist/scenario-check
|
||||
```
|
||||
|
||||
- Options: `--case fixture|library|transcript|capture|daemon-restart|refocus-parked|
|
||||
refocus-active|refocus-parked-off|prompt-state` (default: all nine), `--mode
|
||||
healthy|lost-baton|seeded` (default: healthy, lost-baton, healthy) and `--out <dir>` (default
|
||||
`dist/pr-scenarios`). `--mode` applies to the baton cases (`fixture` and `library`) and to
|
||||
the `refocus-parked` effect case; the six passing-only cases (`transcript`, `capture`,
|
||||
`daemon-restart`, `refocus-active`, `refocus-parked-off`, `prompt-state`) always run once
|
||||
each, healthy. `--mode lost-baton` leaves them out of a full run, and
|
||||
`--case transcript|capture --mode lost-baton` is refused with exit 2. Use a fresh `--out`
|
||||
directory for each run you want to keep.
|
||||
- `--remote` accepts only `ssh://` values for `DOCKER_HOST`, and it refuses if
|
||||
`DOCKER_CONTEXT` is set. The engine must be Linux on amd64 or arm64. The script reads the
|
||||
*server's* platform, so a macOS/arm64 laptop driving a Linux/amd64 engine works.
|
||||
- Without `--remote`, the script refuses to run anywhere except GitHub Actions on Linux.
|
||||
- The source build and pack run on your machine. The image build and the containers run on
|
||||
the engine. Building the image needs network access for the pinned base image, Node and
|
||||
dependencies. The scenario containers have no network.
|
||||
- The image is built from your working tree but tagged with `git rev-parse HEAD`. Commit
|
||||
first, so the tag names what you actually tested.
|
||||
- It never pushes images. It removes only the containers it named. If the SSH connection
|
||||
drops, it prints the name of any container it could not remove.
|
||||
|
||||
What happens, in order:
|
||||
|
||||
1. `scripts/build-testbed-image.sh` packs `@openrig/cli` from your tree. It installs the
|
||||
package into a digest-pinned Debian image with Node, checks `rig --version`, and checks
|
||||
that the installed daemon loads. The result is tagged `openrig-testbed:<sha>`.
|
||||
2. `docker/testbed/Dockerfile.scenarios` adds a bundled copy of
|
||||
`packages/test-system/ci/run.mjs` plus the fixture and library scenarios.
|
||||
3. Each run uses a fresh container: `--network none`, read-only root plus a 512 MB `/tmp`
|
||||
tmpfs, `--cap-drop ALL`, `no-new-privileges`, non-root, 2 CPUs, 2 GiB, 256 PIDs and a
|
||||
300-second deadline.
|
||||
4. In lost-baton mode, the runner stops the daemon, changes exactly one queue row in the
|
||||
scratch database from `in-progress` to `pending`, and restarts the daemon.
|
||||
`packages/test-system/ci/result.mjs` then accepts only one outcome: a failure at the
|
||||
post-restart queue assertion, with `baton-1` observed as `pending` at the expected
|
||||
destination. A startup error, a timeout or any other failure does not count as a caught
|
||||
regression. Healthy runs must pass with no fault armed.
|
||||
5. The `transcript` and `capture` runs are passing form only: `result.mjs` accepts only a
|
||||
clean PASS of the named scenario, with no seed or fault. Their planted failures are
|
||||
product-code mutations, so they stay per-PR evidence and are not CI runs. Stub scripts
|
||||
work here because the runner executes the pipeline in host mode inside the image.
|
||||
|
||||
A single healthy run proves that one leg only. The paired fault run is what shows the
|
||||
assertion can catch the failure.
|
||||
|
||||
Adding a new case to this pack today means changing three files:
|
||||
`scripts/run-pr-scenarios.sh` (the `--case` list), `CASES` in
|
||||
`packages/test-system/ci/result.mjs`, and the fault controller in
|
||||
`packages/test-system/ci/run.mjs`, which is currently specific to the baton row. A
|
||||
passing-only case needs no fault controller: add it to `PASSING_CASES` and the `--case` list
|
||||
in `scripts/run-pr-scenarios.sh`, and to `PASSING_CASES` in `result.mjs`.
|
||||
|
||||
### What a green stub scenario proves
|
||||
|
||||
- The packaged CLI installs into a clean Linux container and starts a daemon.
|
||||
- `rig up` and `rig down` of a stub topology go through the real daemon, tmux, SQLite and CLI.
|
||||
- Queue state, read through the shipped `rig queue list` path, survives a daemon restart.
|
||||
- With the paired seeded run: the assertion fails when that state is broken.
|
||||
|
||||
### What a stub can and cannot prove
|
||||
|
||||
The stub does not fabricate product outputs. It runs the real daemon, tmux and CLI. But it is
|
||||
not Claude Code or Codex, and it differs from them in ways that decide what a stub green
|
||||
means:
|
||||
|
||||
| Area | What the stub does at `e8f0ab34` | What that means for your test |
|
||||
|---|---|---|
|
||||
| Consuming a message | `stub-runner.ts` runs its launch script once and then idles. It has no stdin reader, socket or other input channel. The default script prints `[stub] scripted reply: acknowledged` at boot, before anything has been sent. | A pane showing a reply, or your echoed text, does not prove the message was consumed. No stub scenario can currently prove "delivered and answered". Separately, `rig send --verify` means "appeared in the pane" (with transport.verify_input_box on, a Claude Code or Codex send means "the seat was idle before the paste and its input box took the exact text"), not acknowledgement (see `rig send --help`), and the scenario `send` step doesn't pass `--verify` at all. |
|
||||
| Launch path | `StubRuntimeAdapter` types `node <stub-runner> …` into the pane (`tmux.sendText`, then Enter). Claude Code with an explicit permission mode launches through a managed launch (`ClaudeManagedLaunch.prepare`, `tmux.sendShellCommand`). Otherwise Claude Code, Codex and Pi launch through `SeatLaunchEnvironment.command` and `tmux.sendShellCommand`; the stub adapter does not get that environment. The shell-foreground check in `session-transport.ts` (`unverifiedShellForeground`) runs for every runtime except `terminal` and `claude-code`, and only `codex` has a native-process proof; Claude Code ordinary delivery applies its own uncertainty policy at the input boundary. | A stub seat doesn't exercise the seat launch environment, managed launch, wrappers or native-process identity (the class behind #197). Wrapping the stub in a shell wouldn't change that. |
|
||||
| Permissions | `validateNativePermissionSelection` accepts only `codex` and `claude-code`. The stub carries the resolved `floor` / `full_bypass` posture, including one set by a declarative `permission_policy`, into its runner's `--posture` argument and READY line. It ignores non-interruptive launches, and it doesn't exercise Claude or Codex native permission behaviour. | Testing Claude permission modes or per-seat permission selection needs a real runtime. |
|
||||
| Queue pickup and wake | No stub worker reacts to a nudge by claiming an item, working on it and handing it back. | The baton scenarios prove the claim *survives a restart*. They don't prove delivery, pickup or wake. |
|
||||
| Reboot / tmux reset | `daemon: {op: restart}` restarts only the scenario daemon, and the tmux server keeps running. No step resets tmux. | Recycled pane IDs and stale bindings after a real reboot (the class behind #141) are not covered by a daemon restart. |
|
||||
| Multi-rig and same-name identities | The `up` step always uses the scenario's top-level `topology` and ignores a per-step override. No step archives or removes a rig. | Cross-rig scope, same-name generations and archive/remove routing (the class behind #174) need runner work before they can be tested. |
|
||||
| Step-time restore, compaction, mutation, policy | `restore`, `mutate` and `policy` steps are refused at load time (`UNBOUND_STEP_VERB`); `emit` parses but a non-`refocus` behavior throws `UnboundActionError` when the runner reaches it. Behaviours run only from a per-seat launch script (`env.stub_scripts`). A seat `restart` step maps to `rig launch`, which is not native resume or fork. | Step-time compaction and restore, and native resume and fork, are not covered. |
|
||||
| Skill projection | The stub projects skills into `<cwd>/.openrig/stub/skills/`. Claude Code projects a team seat's skills into the seat's generated plugin in OpenRig's state, and into `<cwd>/.claude/skills/` only for a seat without one. | A stub green says nothing about provider-specific projection (the class behind #159). |
|
||||
| Model and provider | No model, no tokens. Stub scripts refuse `usage_limit` because it only exists on real runtimes. | Model behaviour, provider usage limits and real context usage need live evals or a real runtime. |
|
||||
| Platform | CI's scenario job runs in a Linux container. | macOS process behaviour is covered by the macOS `package-tests` jobs, not by scenarios. |
|
||||
|
||||
The most useful next pieces of harness work, roughly in dependency order:
|
||||
|
||||
1. A stub that consumes input and answers a fresh nonce.
|
||||
2. A model-free test entry point shaped like a provider, that goes through the real native
|
||||
adapter and managed launch. Package tests such as `claude-interactive-launch.test.ts` and
|
||||
the `*-native.test.ts` files already drive the real adapters through tmux against an
|
||||
offline recorder; they prove command resolution and arguments.
|
||||
`claude-executable-foreground.test.ts` holds that recorder open in private Bash, zsh, dash and ksh panes
|
||||
to check parent PID, process-group ownership, tmux's pane command and parent-shell
|
||||
restoration for unmanaged fresh, fork and resume launches. These fixtures do not prove
|
||||
native readiness, history or answers.
|
||||
3. Lifecycle and identity steps (tmux reset, archive/remove).
|
||||
4. Then pickup, wake and restore scenarios.
|
||||
|
||||
### Library status
|
||||
|
||||
Seven library scenarios run in CI: `queue-baton-survives-restart` (since #243) and
|
||||
`refocus-parked-baton` with their seeded pairs, and `transcript-reads-addressed-seat`,
|
||||
`capture-returns-addressed-seat`, `daemon-restart-preserves-team-identity`,
|
||||
`refocus-active-baton` and `refocus-parked-baton-packet-off` in passing form only. The
|
||||
`fixture` case runs the `scenario-02-baton` test fixture through the same seeded pair.
|
||||
Three more, `down-stops-every-seat`, `send-renders-in-addressed-pane` and
|
||||
`up-relaunch-restores-a-two-seat-team`, run with the local runner but not in CI, and
|
||||
`compaction-restore-resumes-role` runs the compaction round-trip's success path the same
|
||||
way. Those ten later scenarios are not in this table; "Status today" under
|
||||
[Help wanted](#help-wanted-command-families-without-a-behavioural-scenario) covers seven
|
||||
of them. The other nine scenarios are authored but not yet runnable as meaningful checks:
|
||||
|
||||
| Scenario | Why it doesn't run as a check yet | Smallest useful next step |
|
||||
|---|---|---|
|
||||
| `queue-baton-survives-restart` | Runs in CI: healthy / seeded `baton-drop` / healthy | Extend it to handoff, block, resolve and unclaim |
|
||||
| `clean-lifecycle-no-residue` | The post-`down` "nothing left over" assertion is commented out, because the format has no way to say "empty" | Add a real absence assertion |
|
||||
| `ps-scope-honesty` | The second `up` reuses the first topology, and the expected match tolerates the other rig | Make `up` honour the per-step topology, then assert the exclusion |
|
||||
| `send-verify-means-rendered` | The `send` step drops `verify`, and the stub has no input loop | Build the input-consuming stub and assert a response derived from the nonce |
|
||||
| `home-divergence-preseed-visible` | The pipeline ignores `env.pre_existing_tmux`, and the default script never prints the expected `trust reached` | Recreate the real HOME split and whatever consumes it |
|
||||
| `stream-emit-durable-replay-live` | Step-time `emit` is unbound | Assert the actual stream contract |
|
||||
| `policy-posture-survives-structural-edit` | The validator refuses its `policy` and `mutate` steps at load time (`UNBOUND_STEP_VERB` — no shipped runner binding) | Rewrite around current per-seat permission selection and an observed launch |
|
||||
| `locked-restore-never-permissive` | The validator refuses its `policy` and `restore` steps at load time (`UNBOUND_STEP_VERB` — no shipped runner binding) | Keep the invariant (the selection survives a restore) and use current modes |
|
||||
| `one-view-state-after-mutation-storm` | The validator rejects its bare-list `equals` (`EQUALS_NOT_DECLARATIVE`) and refuses its `mutate` steps at load time (`UNBOUND_STEP_VERB`) | Declare the comparable fields and real mutations |
|
||||
| `kill-daemon-mid-handoff` | A pane `send` doesn't create the queue item the next assertion expects, and later assertions use a shape the queue reader doesn't return | Rebuild the setup before treating a red result as meaningful |
|
||||
|
||||
In most of these files, `seed_regression` steps that come *after* the assertions are
|
||||
historical markers, not executable fault injections.
|
||||
|
||||
### Ordinary CLI journeys with stub seats
|
||||
|
||||
`packages/daemon/test/stub-cli-journeys.integration.test.ts` reuses the scenario
|
||||
helpers with one private daemon and two stub rigs. It checks three contracts
|
||||
through the real CLI: verified send appears in the addressed pane but not its
|
||||
sibling; current-rig and explicit rig listings contain exactly the expected seats;
|
||||
and a filtered stream item survives daemon restart and an idempotent emit retry.
|
||||
|
||||
After building, run the focused file with:
|
||||
|
||||
```sh
|
||||
npm test -w @openrig/daemon -- test/stub-cli-journeys.integration.test.ts
|
||||
```
|
||||
|
||||
The file also runs in the daemon package-test job. It covers these command
|
||||
contracts without extending the YAML grammar. The corresponding library scenarios
|
||||
above remain pending: pane rendering does not prove input consumption, durable
|
||||
stream replay does not prove live subscription, and this test does not exercise
|
||||
native providers, restore or policy changes.
|
||||
|
||||
`packages/daemon/test/rig-edge-cli.integration.test.ts` uses the same private
|
||||
scenario daemon and tmux helpers for edge repair. It removes a stub seat, re-adds
|
||||
its logical ID with the terminal runtime, then restores its cross-pod edges with one
|
||||
`rig edge add` per edge. It checks repeat IDs and edge counts, refusal without a
|
||||
write, unchanged sibling node/binding identities and pane PIDs, export,
|
||||
snapshot capture and exact rollback. It models the cross-runtime replacement
|
||||
workaround without invoking Claude Code or Codex. The YAML runner has no bound
|
||||
edge/remove/add verbs, so this journey drives the real CLI directly:
|
||||
|
||||
```sh
|
||||
npm test -w @openrig/daemon -- test/rig-edge-cli.integration.test.ts
|
||||
```
|
||||
|
||||
## If your change touches an artery, add or extend a scenario (or say why not)
|
||||
|
||||
[arteries.md](arteries.md) lists the behaviours that everything else depends on. If your
|
||||
diff can affect one, your PR description should do one of these:
|
||||
|
||||
- link the scenario that covers it, and say which layer you ran it in;
|
||||
- add or extend a scenario. For a regression scenario, show that the fixed code passes and
|
||||
that the planted old defect fails *at the intended assertion*. A setup crash or a timeout
|
||||
doesn't count;
|
||||
- or say precisely why the stub can't exercise it. Use the table above, which covers
|
||||
message consumption, native launch and permissions, tmux reset and so on. Then say what
|
||||
you ran instead, for example a manual run with the real runtime.
|
||||
|
||||
This is part of normal review, not an extra approval step. It also isn't a claim that
|
||||
changes outside the arteries map are safe.
|
||||
|
||||
## Live-model evals
|
||||
|
||||
`packages/test-system/evals/` is a separate check from scenarios. A real seat receives a
|
||||
natural prompt and decides what to do. A case passes when the seat's transcript shows the
|
||||
expected `rig context get <entry>` command, in the expected order. Cases live in
|
||||
`cases/selection.yaml` (10) and `cases/loading.yaml` (4).
|
||||
|
||||
```sh
|
||||
# Harness check: a fake provider fed a JSON map of prompt -> transcript.
|
||||
# Any prompt without a transcript counts as an error, never as a pass.
|
||||
npm run eval -w packages/daemon -- --transcripts <transcripts.json> --out <result.json>
|
||||
|
||||
# Live run: drives a real Claude Code seat
|
||||
npm run eval -w packages/daemon -- --provider rig --seat <session> --out <result.json>
|
||||
npm run eval -w packages/daemon -- --provider rig --seat-spec "$PWD/<one-seat-rig.yaml>" --out <result.json>
|
||||
```
|
||||
|
||||
- **Live runs use provider credits, and the results vary from run to run.** CI never runs them.
|
||||
- `--provider rig` needs `OPENRIG_HOME` set, and it supports Claude Code seats only. It
|
||||
refuses a Codex seat.
|
||||
- `--seat-spec` runs `rig up` of a scratch rig against **your current daemon**, then
|
||||
`rig down` at the end. `--seat` attaches to an existing seat and leaves it running.
|
||||
- `evals/rig-seat/example-scratch-rig.yaml` sets `permission_policy: builtin:yolo` and
|
||||
`cwd: "."`. It also references `local:agents/blank`, and no such agent directory ships
|
||||
beside it. Read the file and expect to adapt it before you use it.
|
||||
- Before any run, the runner builds the production context package in a temp directory and
|
||||
refuses if any case's entry doesn't resolve in it.
|
||||
- `npm test` covers the harness without a model: the case schema, the grader, the fixtures
|
||||
and production entry resolution (`packages/daemon/test/eval-*.test.ts`).
|
||||
|
||||
## Docker testbed runbooks
|
||||
|
||||
`docker/testbed/runbooks/` holds manual procedures, L0 to L6, that you run on a machine with
|
||||
Docker. They check the testbed image itself:
|
||||
|
||||
- L0: resolve the pinned base image and the stub asset list
|
||||
- L1: PTY allocation
|
||||
- L2: tmux server lifecycle
|
||||
- L3: the daemon inside a container, settling a stub rig
|
||||
- L4: the scenario environment still refuses a foreign daemon target inside a container
|
||||
- L5: several containers acting as named hosts on one Docker network
|
||||
- L6: the scenario runner driving a scenario through a container
|
||||
|
||||
Use them when you change `docker/testbed/`, `scripts/build-testbed-image.sh` or the
|
||||
container staging helpers. Ordinary PRs don't need them. They were written before the CI
|
||||
scenario job and contain internal planning references, so treat them as procedures to adapt,
|
||||
not as current status.
|
||||
|
||||
## Help wanted: command families without a behavioural scenario
|
||||
|
||||
> This list may be stale. The tables below cover the families that have a proposed first
|
||||
> check: at `b29a70f14`, the Commander tree returned by `createProgram()`
|
||||
> (`packages/cli/src/index.ts`) registers 92 top-level families, 87 of which have a row
|
||||
> below (7 of them marked partial); `roster` and `telemetry` now have rows too. These 5
|
||||
> families still have no row: `acp`, `community-harness`, `edge`, `environment`, `exec`.
|
||||
> Before picking one up, check `packages/test-system/scenarios/`, the cases in
|
||||
> `scripts/run-pr-scenarios.sh`, and the current `rig --help`.
|
||||
> One scenario doesn't cover a family's every option, sequence or platform. A `--help`
|
||||
> check only proves the command is discoverable. Existing unit tests are not counted here.
|
||||
> They exist, but this list is about behavioural scenarios.
|
||||
|
||||
Status today: `queue` has partial coverage, namely create, claim and list, plus the claim
|
||||
surviving a restart. `down` has partial coverage in
|
||||
`packages/test-system/scenarios/down-stops-every-seat.yaml`: after `rig down`, `rig ps` reports
|
||||
every seat stopped and the rig still listed. `send` has partial coverage in
|
||||
`send-renders-in-addressed-pane.yaml` beside it: the envelope and the message body render in
|
||||
the addressed seat's pane. `transcript` has partial coverage in
|
||||
`transcript-reads-addressed-seat.yaml`: each of two seats' scripted output is readable through
|
||||
that seat's `rig transcript --tail 200 --json`. `capture` has partial coverage in
|
||||
`capture-returns-addressed-seat.yaml`: in a rig whose member names overlap by prefix (`impl`,
|
||||
`impl2`; `dev-impl`, `dev-impl2` with the pod), `rig capture <seat>` returns each seat's own
|
||||
pane. The scenario catches a capture that resolves the seat by a prefix match on its member
|
||||
name (so `dev-impl` also matches `dev-impl2`) and takes the first hit, or any resolver that
|
||||
returns the same pane for both seats; it does not exercise a prefix match on the full seat
|
||||
name. All six run with the local runner (`run-scenarios.mjs`). The transcript, capture and daemon
|
||||
scenarios also run in the CI job, in passing form only; the down, send and up scenarios don't.
|
||||
They do not prove that panes are gone or that a seat consumed a message. The transcript
|
||||
case does not establish ordering, uniqueness, sibling-token absence, tail limits or restart
|
||||
persistence. `up` has partial coverage in `up-relaunch-restores-a-two-seat-team.yaml`: a
|
||||
two-seat stub team comes up, goes down, and the same spec starts a new team in place of the
|
||||
stopped one with the same name, exactly two seats, both running, not archived, so a
|
||||
relaunch that re-records the old seats into the team's row fails on the final `ps` counts.
|
||||
It does not prove that conversations survive the round trip (that is the `--existing`
|
||||
restore path), that a seat consumed a message, or that the panes are gone after `down`.
|
||||
`daemon` has partial coverage in `daemon-restart-preserves-team-identity.yaml`: a two-seat
|
||||
stub team is up, the runner restarts the scratch daemon, and `rig ps --json` must report the
|
||||
same name still running with exactly two seats, both running, not archived, so a restart
|
||||
that loses the team's row, or changes its seat counts or status, fails on the post-restart `ps`;
|
||||
`rig down` then records it stopped, so the restart left a governable team. It does not prove
|
||||
that queue items survive the restart (that is `queue-baton-survives-restart.yaml`), that a
|
||||
kill inside a handoff window is reconciled (that is `kill-daemon-mid-handoff.yaml`), the
|
||||
port-and-process half of the row, or that the panes are gone after `down`.
|
||||
`compaction-restore-resumes-role` covers the compaction round-trip's success path: the
|
||||
seat's launch script (delivered through `env.stub_scripts`) fires the real precompact and
|
||||
restore seams in order, and impl's own pane carries the injected directive. It runs with
|
||||
the local runner, not in CI (container mode refuses `env.stub_scripts` loud and named), and
|
||||
its seeded wrong-seat-restore negative case is not armed yet.
|
||||
|
||||
Before you start, two honest constraints:
|
||||
|
||||
- Today's step verbs can't run arbitrary `rig` commands. Many rows below need a runner
|
||||
binding first. One proposal (not implemented) is a single structured-argv step that runs
|
||||
the installed CLI through the existing `rigBin`, instead of one new verb per command.
|
||||
- Rows that need a seat to *answer* (anything with "nonce") depend on the input-consuming
|
||||
stub described above.
|
||||
- `expect` has no negative, absence or count form. `match` is a structural subset and
|
||||
`contains` is a substring, so "the sibling received nothing", "the panes are gone" or "no
|
||||
duplicates" can't be asserted yet. An exact field value, such as `runningCount: 0` on the
|
||||
`ps` surface, is the closest available form.
|
||||
- The `restart` step runs `rig launch <rig> <node> --json` and passes no other flag; the node
|
||||
is the logical ID (for example `dev.qa`). A stub seat has no resume token. `rig launch` has
|
||||
no `--fresh` option: a deliberate fresh start of a stub seat after `down` goes through
|
||||
`rig up --existing <rig> --fresh <seat>` or `rig seat launch <seat> --fresh --reason "<why>"`, so the
|
||||
`launch` row needs a runner binding to one of those first.
|
||||
|
||||
| Group | Family | Proposed first check (observable result) |
|
||||
|---|---|---|
|
||||
| lifecycle | `start` | Restore two stopped stub seats. Both answer distinct fresh nonces, and no second daemon starts. |
|
||||
| lifecycle | `daemon` (partial) | Restart-preserving-the-persisted-team-row is covered by `daemon-restart-preserves-team-identity.yaml`. Next: port and process agreement through public commands, and `stop`, which needs a runner binding. |
|
||||
| lifecycle | `bootstrap` | Bootstrap a minimal stub spec. Seats come up ready and the selected spec is the one used. |
|
||||
| lifecycle | `up` (partial) | Relaunch-not-duplicating-identities is covered by `up-relaunch-restores-a-two-seat-team.yaml`. Next: each seat consumes its own send, which needs the input-consuming stub. |
|
||||
| lifecycle | `down` | (partial) Next: its panes are gone, a sibling rig still answers, retained data follows the docs. |
|
||||
| lifecycle | `create` | Create a one-seat rig through the public path. It appears and answers without a hand-written topology. |
|
||||
| lifecycle | `launch` | Fresh, relaunch, resume and fork on provider-shaped fixtures. Correct restored token, one live process. |
|
||||
| reads | `status` | Two rigs in different states. Status matches their exact identities and transitions. |
|
||||
| reads | `ps` | Two rigs with overlapping seat names. Rig and node filters return exactly the intended members. |
|
||||
| reads | `usage` | Deterministic stub telemetry. top and series respect seat and time bounds. Missing data isn't reported as zero. |
|
||||
| reads | `roster` | A scratch workspace with two rigs and a rosters folder: `rig roster find` returns the expected specialists, and an empty folder reports none without an error. |
|
||||
| reads | `telemetry` | Deterministic retained-metadata fixtures: one bounded page returns exactly the recorded rows, and a history gap is named rather than skipped silently. |
|
||||
| reads | `health` | Known fresh, stale and cleared findings. Scope, explain and diagnosis values match the fixture. |
|
||||
| continuity | `snapshot` | Snapshot two seats. Rig, membership and snapshot bytes survive a daemon restart. |
|
||||
| continuity | `restore` | Restore after a daemon and tmux reset. Seat identity and nonce answers survive. |
|
||||
| continuity | `agent-image` | create/show/pin/unpin/delete for a provider-shaped fixture. References and identity match. |
|
||||
| continuity | `fork` | Fork at a known history token. The child keeps the prefix with its own identity, and the parent still answers. |
|
||||
| continuity | `restore-check` | One restorable and one missing transcript. Readiness tells them apart without relaunching. |
|
||||
| continuity | `restore-packet` | write/read/validate round trip. A malformed packet fails clearly. |
|
||||
| continuity | `compact-plan` | Scripted history and usage fixtures. Candidates and exclusions match thresholds, and nothing is compacted. |
|
||||
| continuity | `compact` | Input-driven compact and restore keep the role and token. Messages before and after reach the same seat. |
|
||||
| continuity | `handover` | Scripted predecessor and successor. Context is delivered before the rebind, and the successor answers. |
|
||||
| recovery | `crash-cart` | Stop only the scratch daemon. The output names paths and state, and the command changes nothing. |
|
||||
| recovery | `preflight` | Ready and missing-dependency fixtures produce named checks without launching seats. |
|
||||
| recovery | `doctor` | Healthy and damaged scratch installs. Exact diagnosis, no automatic repair. |
|
||||
| recovery | `destroy` | In a disposable scenario HOME only. Removes the intended state and keeps explicitly retained markers. |
|
||||
| registry | `gateway` | Add, show, update and remove a synthetic human. Reads match exact fields, and removal sticks. |
|
||||
| registry | `host` | add/list/select/rename isolated entries that resolve to fixture endpoints. Pairing needs two container daemons. |
|
||||
| coordination | `parked` | An owed item with a stopped worker is reported. A held worker with a live wake is distinguished. |
|
||||
| coordination | `stream` | emit/list/show/archive an exact item. A restart and a bounded watch keep the order with no duplicates. |
|
||||
| coordination | `queue` | (partial) handoff → successor owned by `--to` and source asserted closed (`handed-off`), block → parked blocked, resolve → unpark to in-progress (owner nudge suppressed), unclaim → pending with destination kept — each with exact items and destinations asserted via `expect queue` (`rig queue list --json --full --all`) in `queue-transitions-reach-exact-states.yaml`; baton restart coverage stays in `queue-baton-survives-restart.yaml`. |
|
||||
| coordination | `project` | Acquire a lease, classify one item twice, read one projection. The wrong holder cannot act. |
|
||||
| coordination | `view` | Register a view over known items. list/show return the exact filtered result after a mutation. |
|
||||
| coordination | `watchdog` | Register one short wake, see one response, stop it, prove no further deliveries. |
|
||||
| coordination | `workflow` | validate/compile/instantiate a tiny flow. It transitions and resumes after a restart with no duplicate queue work. |
|
||||
| coordination | `chatroom` | send/history/topic/clear in a scratch room. A bounded watcher sees exactly the ordered messages. |
|
||||
| coordination | `heartbeat` | Known execution-proof and missing-closure fixtures report the right status. Empty output isn't completion. |
|
||||
| authoring | `export` | Export a stub rig, then validate and re-import it. Membership and configuration are equivalent. |
|
||||
| authoring | `import` | Import a minimal spec, and the export matches it. Malformed input leaves state unchanged. |
|
||||
| authoring | `package` | validate/plan/install/rollback in scratch. Installed files and prior state match. |
|
||||
| authoring | `requirements` | Complete and missing capability fixtures produce the right named requirements. |
|
||||
| authoring | `bundle` | create/inspect/install a local bundle and round-trip it. A missing asset gives a named failure. |
|
||||
| authoring | `agent` | Validate a good and a malformed stub AgentSpec. The diagnostic names the bad field. |
|
||||
| authoring | `spec` | validate/audit/preflight/show a minimal spec, with predictable diagnostics for a malformed field. |
|
||||
| authoring | `specs` | add/show/preview/rename/remove a library fixture. A missing reference fails explicitly. |
|
||||
| authoring | `workspace` | validate/doctor known roots plus one missing-frontmatter case. Reports the exact affected path. |
|
||||
| authoring | `config` | set/get/reset a scratch key and init a workspace. Readback and files match the requested root. |
|
||||
| presentation | `ui` | Open the URL through a fake browser executable and read a local UI route. Rendering is out of scope. |
|
||||
| presentation | `tui` | Drive the TUI command bar and socket over two stub rigs. select, mutate and back show exact identities. |
|
||||
| presentation | `terminal` | open/status/views against a fake terminal-app endpoint. Correct target and layout arguments. |
|
||||
| adoption | `discover` | One unmanaged and one owned scratch tmux session. Only the intended candidate is discovered. |
|
||||
| adoption | `attach` | Attach to the intended seat. `whoami` reports it without duplicating the process. |
|
||||
| adoption | `bind` | Bind one discovered pane. A different, already-occupied identity is refused. |
|
||||
| adoption | `adopt` | Adopt two scratch sessions. PIDs and history are preserved, and the identities become addressable. |
|
||||
| adoption | `reconcile-session` | Rebind a hand-resumed provider-shaped fixture without starting or typing into another process. |
|
||||
| adoption | `unclaim` | Release an adopted seat. The tmux process and its history stay alive. |
|
||||
| adoption | `release` | Release one rig's claimed sessions. Processes survive, and a sibling rig is unchanged. |
|
||||
| identity | `archive` | Archive an old same-name generation. The current one stays routable, and the old data is kept. |
|
||||
| identity | `unarchive` | Unarchive a retained generation without taking over another live seat. |
|
||||
| identity | `remove` | Remove an archived generation or one member. Only that identity goes, and a reused name still routes. |
|
||||
| identity | `whoami` | Inside each stub seat, it returns that seat, not a sibling or the caller. |
|
||||
| identity | `seat` | Status, held-message guard, stop/launch and resume token keep the right process and permissions. |
|
||||
| integration | `mcp` | Serve against a scratch daemon. One listed tool's value matches CLI state. |
|
||||
| integration | `slack` | Manifest and config round trip, plus deterministic inbound and outbound fixture requests. No real Slack account. |
|
||||
| integration | `ask` | Search fixture transcripts via a fake model endpoint. Citations resolve to the stored lines. |
|
||||
| integration | `file` | Copy a fixture payload through isolated transport endpoints. Verify bytes and paths. |
|
||||
| integration | `env` | A fixture service-backed rig. status/logs/down reflect that service and stop only it. |
|
||||
| delivery | `transcript` (partial) | Own scripted output from two addressed seats is covered by `transcript-reads-addressed-seat.yaml`. Remaining: ordering, uniqueness, no other seat's token, tail limits and restart persistence; absence/count assertions need a runner binding. |
|
||||
| delivery | `send` | (partial) Next: send a fresh nonce and require an answer derived from it. Boot text or echo can't pass. |
|
||||
| delivery | `capture` | (partial) Each seat's own token, under prefix-overlapping member names. Next: the not-found, tail (`--lines`), sibling-absence and `--rig`/`--pod` cases, which need an error form, a tail option, an absence form and a multi-target surface in the runner. |
|
||||
| delivery | `broadcast` | Broadcast a nonce to a rig or pod. Each intended seat answers once, and an excluded seat gets nothing. |
|
||||
| delivery | `walk` | Walk two pieces through an input-consuming stub. Order is kept, and a bad piece isn't reported as delivered. |
|
||||
| context | `context` | add/get/compose/trace/profile over a fixture project. Output bytes, order and roots match. |
|
||||
| context | `plugin` | list/show/used-by/validate a fixture plugin. A missing resource is not the same as a valid empty selection. |
|
||||
| context | `skill` | Resolve and audit a fixture skill, then inspect the projection the consumer actually gets. |
|
||||
| context | `mode` | set/effective/cite/unset at nested scopes. Precedence holds, and the inherited value returns after unset. |
|
||||
| context | `startup-proof` | Submit a proof from a real stub identity. Check the stored attribution and the duplicate behaviour. |
|
||||
| permissions | `policy` | Apply and read an explicit permission selection using a provider-shaped fixture. It survives a restore, and the default is unchanged. |
|
||||
| accounts | `auth` | Synthetic auth files in scratch: save, list, validate, switch. No provider contact, no real credentials. |
|
||||
| accounts | `provider` | Synthetic account, binding and signal state. The precheck refuses where it should, and a switch keeps seat history. |
|
||||
| topology | `expand` | Add a pod to a live stub rig. Old seats survive, and new members answer nonces. |
|
||||
| topology | `add` | Add one seat. Exact binding, siblings preserved, and the duplicate-name behaviour is explicit. |
|
||||
| topology | `grow` | Grow two seats. Identities are unique, config is inherited, and existing history is kept. |
|
||||
| topology | `shrink` | Remove a pod. Unrelated seats still answer, and the removed membership is gone. |
|
||||
| onboarding | `setup` | Scripted newcomer answers against synthetic providers. No install or login for an unused provider. |
|
||||
| work | `scope` | Create a mission and a slice. move/progress/notes/graph readback keep identity and paths. |
|
||||
| work | `proof` | add/judge/show evidence in a fixture scope. Readiness updates without overwriting earlier proof. |
|
||||
|
||||
How to make a case useful:
|
||||
|
||||
- Assert actual state or content, not just exit code 0. Where ordinary use includes bad
|
||||
input, add an invalid or missing input case too.
|
||||
- Reuse the existing runner and the installed CLI. Don't add a second scenario engine.
|
||||
- If a scenario exposes an existing product bug, keep the failing case and open an issue.
|
||||
Include the exact source revision, the shortest failing sequence, and the intended versus
|
||||
observed result. Don't weaken the assertion to make it pass. Keep "the harness setup
|
||||
failed" separate from "the product misbehaved".
|
||||
- A check that only covers the success path is still useful. Just don't call it a
|
||||
regression detector until you have seen its negative case fail.
|
||||
|
||||
## Existing docs in this area that are out of date
|
||||
|
||||
Read these with care. This page notes them and proposes no changes.
|
||||
|
||||
- The headers of `transcript-reads-addressed-seat.yaml` and `capture-returns-addressed-seat.yaml`
|
||||
say "host mode only". Both run in the CI container job, which executes the host-mode pipeline
|
||||
inside the image.
|
||||
- `packages/test-system/scripts/README.md` and `packages/test-system/ROUTING.md` are
|
||||
dependency-routing notes from when the scenarios were first written. Several of the items
|
||||
they list have since landed. Treat them as history.
|
||||
- `docker/testbed/runbooks/`: see above. They are manual procedures that predate the CI job.
|
||||
+230
-36
@@ -1,44 +1,238 @@
|
||||
---
|
||||
kind: as-built
|
||||
title: ui.md — Reorganized into the Modular As-Built Tree (Redirect Stub)
|
||||
status: superseded
|
||||
topics: [knowledge-and-context]
|
||||
domains: [engineering-advisor, operating-advisor, product-advisor]
|
||||
applies-when: |
|
||||
You followed an old reference to docs/as-built/ui.md. The monolith was
|
||||
reorganized (slice-08, context-architecture-v1) into the ui/ module folder.
|
||||
Go to README.md (map of territory) or codemap.md (use-case lookup), then the
|
||||
named ui/ module.
|
||||
siblings: [README.md, codemap.md, architecture.md]
|
||||
prerequisite-reads: []
|
||||
last-verified-against-source: e8f0ab340db773392ec8be75b072d1c0f3068a50
|
||||
last-updated: 2026-10-08
|
||||
---
|
||||
# OpenRig UI As-Built
|
||||
|
||||
# ui.md was reorganized into the modular as-built tree
|
||||
This document describes the current UI architecture and component system. The
|
||||
brand/design rules live in `DESIGN.md`; this file focuses on how the shipped UI
|
||||
is assembled.
|
||||
|
||||
This single-file UI monolith was split into a folder of independently-loadable,
|
||||
frontmatter-tagged thematic modules (slice-08, `context-architecture-v1`). The
|
||||
canonical as-built description of the shipped operator UI now lives in `ui/`.
|
||||
This stub is a forwarding pointer so old references still resolve.
|
||||
## Package Shape
|
||||
|
||||
**Start here:**
|
||||
UI package: `packages/ui`
|
||||
|
||||
- [`./README.md`](./README.md) — the map of territory and full module index.
|
||||
- [`./codemap.md`](./codemap.md) — navigation index: use-case lookup table and
|
||||
source-root pointers. Use this when you know *what* you need but not *which*
|
||||
module.
|
||||
Primary implementation areas:
|
||||
|
||||
## Where the major content went
|
||||
- `src/routes.tsx` - TanStack Router route tree
|
||||
- `src/components/AppShell.tsx` - global shell, rail, Explorer, drawer, preview stack
|
||||
- `src/components/ui/` - reusable vellum and base UI primitives
|
||||
- `src/components/graphics/` - runtime, tool, and actor marks
|
||||
- `src/components/topology/` - host/rig/pod/seat graph, table, terminal, activity
|
||||
- `src/components/project/` - workspace/mission/slice observability and project metadata primitives
|
||||
- `src/components/for-you/` - attention feed and actionable queue cards
|
||||
- `src/components/specs/` - Library and skill file viewing
|
||||
- `src/components/preview/` - terminal/session preview panes
|
||||
- `src/hooks/` - route data hooks and event consumption
|
||||
- `src/lib/` - classifiers, formatters, layout helpers, brand helpers, activity logic
|
||||
|
||||
The UI content is now the **4 modules under `ui/`**:
|
||||
## Shell Model
|
||||
|
||||
| Module | What moved here |
|
||||
|---|---|
|
||||
| [`ui/shell-and-routing.md`](ui/shell-and-routing.md) | Package shape, the `AppShell` shell model (rail / Explorer / center workspace / drawer / preview stack), the route tree, design primitives, the shared detail-drawer/viewer system, event/activity consumption. |
|
||||
| [`ui/topology.md`](ui/topology.md) | The topology surface — host hybrid graph, table/terminal views, the activity-ring / hot-potato visual language, terminal-preview popovers, navigation/overlay contracts. |
|
||||
| [`ui/project-and-for-you.md`](ui/project-and-for-you.md) | The operator destination surfaces — Project observability (workspace/mission/slice scope tabs), the For-You attention feed (5-card classifier + queue actions), the Dashboard landing on the vellum brand system. |
|
||||
| [`ui/library-specs-and-design-system.md`](ui/library-specs-and-design-system.md) | The Library (`/specs`) UI — specs/applications/skills surfaces, the graphics layer, the current design constraints, and the design-system pointer to `../DESIGN.md`. |
|
||||
`AppShell` wraps every route with:
|
||||
|
||||
The brand/design rules still live at `docs/DESIGN.md` (repo `docs/` root, by
|
||||
design — see `ui/library-specs-and-design-system.md`).
|
||||
- Desktop rail
|
||||
- Route-aware Explorer sidebar
|
||||
- Center workspace
|
||||
- Shared detail drawer
|
||||
- Preview stack
|
||||
- Topology overlay state provider
|
||||
- Shared drawer selection context
|
||||
- Shared discovery placement context
|
||||
|
||||
The rail destinations are:
|
||||
|
||||
- Dashboard: `/`
|
||||
- Topology: `/topology`
|
||||
- For You: `/for-you`
|
||||
- Project: `/project`
|
||||
- Library: `/specs`
|
||||
- Settings: `/settings`
|
||||
- Advisor and Operator placeholders/settings links
|
||||
|
||||
The Explorer is contextual. It changes its tree based on the current destination
|
||||
rather than behaving as a generic file browser.
|
||||
|
||||
## Route Model
|
||||
|
||||
Main routes:
|
||||
|
||||
- `/` - Dashboard
|
||||
- `/topology` - host topology scope
|
||||
- `/topology/rig/$rigId` - rig scope
|
||||
- `/topology/pod/$rigId/$podName` - pod scope
|
||||
- `/topology/seat/$rigId/$logicalId` - seat scope with live node details
|
||||
- `/for-you` - attention feed
|
||||
- `/project` - workspace project scope
|
||||
- `/project/mission/$missionId` - mission scope
|
||||
- `/project/slice/$sliceId` - slice scope
|
||||
- `/specs` - Library
|
||||
- `/specs/applications` - application section
|
||||
- `/specs/skills/$skillToken` - skill viewer defaulting to `skill.md`
|
||||
- `/specs/skills/$skillToken/file/$fileToken` - specific skill file viewer
|
||||
- `/settings` - settings center
|
||||
- `/search` - audit/history view
|
||||
- `/lab/project-graphics-preview` - graphics preview lab route
|
||||
|
||||
Legacy routes remain for compatibility where needed, including package flows,
|
||||
bootstrap flows, validation flows, and `/rigs/$rigId`.
|
||||
|
||||
## Design Primitives
|
||||
|
||||
The UI moved from route-specific one-off panels toward reusable primitives.
|
||||
|
||||
Base primitives:
|
||||
|
||||
- `VellumCard` - paper card with optional dark header, hard shadow, registration marks
|
||||
- `VellumSheet` - side sheet/drawer with vellum-heavy surface and registration marks
|
||||
- `RegistrationMarks` - corner registration marks
|
||||
- `StatusPip` - semantic status only
|
||||
- `SectionHeader`, `EmptyState`, `Button`, `Tabs`, `Table`, and form controls
|
||||
|
||||
Graphics primitives:
|
||||
|
||||
- `RuntimeMark` and `RuntimeBadge`
|
||||
- `ToolMark` and `ToolBadge`
|
||||
- `ActorMark` and `OperatorMoodMark`
|
||||
- Runtime and tool normalization in `runtime-brand.ts` and `tool-brand.ts`
|
||||
|
||||
Project metadata primitives:
|
||||
|
||||
- `EventBadge`
|
||||
- `QueueStateBadge`
|
||||
- `TagPill`
|
||||
- `ActorChip`
|
||||
- `DateChip`
|
||||
- `FlowChips`
|
||||
- `ProofThumbnailGrid`
|
||||
- `ProofPacketHeader`
|
||||
|
||||
These primitives centralize visual language for runtime identity, queue state,
|
||||
actor flow, event labels, dates, tags, and proof images.
|
||||
|
||||
## Topology
|
||||
|
||||
Topology is a scoped workspace with graph, table, and terminal views.
|
||||
|
||||
Current topology pieces:
|
||||
|
||||
- `HostMultiRigGraph` renders the host-level hybrid React Flow graph.
|
||||
- `HybridAgentNode` renders compact agent cards with runtime badges, context
|
||||
percentage, token totals, activity state, terminal preview, and CMUX actions.
|
||||
- `HybridPodGroupNode` renders soft dashed pod frames.
|
||||
- `RigGroupNode` renders soft rig frames with registration marks and aggregate
|
||||
activity.
|
||||
- `ActivityRing` and card activity classes show active, needs-input, and blocked
|
||||
states.
|
||||
- `HotPotatoEdge` visualizes directional queue movement through the graph.
|
||||
- `TopologyTableView` mirrors topology data in a dense table.
|
||||
- `TopologyTerminalView` shows terminal-oriented topology state.
|
||||
- `TerminalPreviewPopover` provides black-glass quick terminal preview above the
|
||||
graph via a portal.
|
||||
|
||||
Important topology contracts:
|
||||
|
||||
- Graph/table/tree navigation resolves to seat URLs.
|
||||
- Host-level graph uses cross-rig node ID prefixing.
|
||||
- Expanded rig state is managed by `TopologyOverlayProvider`.
|
||||
- Graph data is lazy-fetched for expanded rigs.
|
||||
- Terminal preview actions are hover/focus visible.
|
||||
- The terminal popover must escape React Flow stacking contexts and stay within
|
||||
the viewport.
|
||||
|
||||
## Project Observability
|
||||
|
||||
Project is organized as a hierarchy:
|
||||
|
||||
- Workspace
|
||||
- Mission
|
||||
- Slice
|
||||
|
||||
The current route scope acts as a filter. Higher scopes show broader rollups;
|
||||
lower scopes show more specific story, queue, proof, artifact, test, and topology
|
||||
data.
|
||||
|
||||
Current project tabs:
|
||||
|
||||
- Story - narrative progress and queue body content
|
||||
- Progress - status and activity rollup
|
||||
- Artifacts - files, commits, proof, and outputs
|
||||
- Tests - proof material, screenshots, and verification detail
|
||||
- Queue - operational qitems with actions and drawer detail
|
||||
- Topology - workflow/agent diagram where available
|
||||
|
||||
Project primitives translate raw queue and event data into readable badges,
|
||||
chips, dates, flows, proof thumbnails, and action outcome panels.
|
||||
|
||||
## For You
|
||||
|
||||
For You is the attention-routing surface. It uses live event feed data plus queue
|
||||
details to classify cards into:
|
||||
|
||||
- Action Required
|
||||
- Approval
|
||||
- Shipped
|
||||
- Progress
|
||||
- Observation
|
||||
|
||||
The card renderer prioritizes readable body content, proof thumbnails, actor
|
||||
flow, friendly dates, and action affordances. Queue actions reuse existing
|
||||
mission-control action endpoints.
|
||||
|
||||
## Library And Skills
|
||||
|
||||
`/specs` is the Library route. It contains specs, applications, context packs,
|
||||
agent specs, agent images, and skills.
|
||||
|
||||
Skills are folder-based. The skill tree in Explorer treats each skill as a
|
||||
folder. Clicking a skill opens a skill detail route that defaults to `skill.md`
|
||||
case-insensitively. The skill detail page uses the existing `FileViewer` and
|
||||
does not add a second in-page file browser; file navigation belongs in the
|
||||
Explorer tree.
|
||||
|
||||
## Drawer And Viewer System
|
||||
|
||||
The shared detail drawer is used for transient focused detail:
|
||||
|
||||
- Queue item viewer
|
||||
- File viewer
|
||||
- Subspec preview
|
||||
- Other detail selections
|
||||
|
||||
Drawer triggers carry root/path provenance so the viewer can fetch and render the
|
||||
correct content. The drawer supports outside-click dismissal.
|
||||
|
||||
Image proof viewing is handled by `ProofImageViewer`, which respects the shell
|
||||
layout and uses black-glass styling.
|
||||
|
||||
## Event And Activity Consumption
|
||||
|
||||
UI event consumption is centralized through shared event hooks so the app avoids
|
||||
spawning duplicate SSE connections for related surfaces.
|
||||
|
||||
Current activity consumers include:
|
||||
|
||||
- For You feed hydration
|
||||
- Topology activity rings and hot-potato movement
|
||||
- Activity feed/system surfaces
|
||||
- Rig event surfaces where appropriate
|
||||
|
||||
Reduced-motion preference is honored for pulse and packet animations.
|
||||
|
||||
## Graphics Layer
|
||||
|
||||
The graphics package is intentionally centralized:
|
||||
|
||||
- Runtime identity: Claude, Codex, Terminal, Unknown
|
||||
- Tool identity: CMUX, tmux, VS Code, Terminal, file, markdown, config, code,
|
||||
screenshot, proof, transcript, commit, folder, skill, video, trace
|
||||
- Actor identity: human/operator, runtime agents, terminal fallback
|
||||
|
||||
The goal is to make dense operator surfaces scannable without adding more raw
|
||||
metadata text.
|
||||
|
||||
## Current Design Constraints
|
||||
|
||||
- Use the shared primitives before creating new card styles.
|
||||
- Keep vellum and black-glass separate.
|
||||
- Keep status semantics separate from activity animation.
|
||||
- Do not duplicate runtime/tool brand detection outside the central helpers.
|
||||
- Do not add new daemon shape for UI polish unless the slice explicitly declares
|
||||
and verifies that boundary.
|
||||
- Do not make page-level content look like a raw terminal log unless the content
|
||||
is actually terminal output.
|
||||
@@ -1,151 +0,0 @@
|
||||
---
|
||||
kind: as-built
|
||||
title: UI Library/Specs Surfaces + Design-System Pointer
|
||||
status: active
|
||||
topics: [specification-and-bundles, observability]
|
||||
domains: [engineering-advisor, product-advisor]
|
||||
applies-when: |
|
||||
Need to know how the Library (`/specs`) UI is assembled — the spec/skills/
|
||||
plugins surfaces, the spec-review + spec-library flows that feed it, and
|
||||
how seat details open — or where the canonical visual/design-system spec
|
||||
lives.
|
||||
siblings: [shell-and-routing.md]
|
||||
prerequisite-reads: [../README.md, shell-and-routing.md]
|
||||
last-verified-against-source: e8f0ab340db773392ec8be75b072d1c0f3068a50
|
||||
last-updated: 2026-10-08
|
||||
---
|
||||
|
||||
# UI Library/Specs Surfaces + Design-System Pointer
|
||||
|
||||
The Library destination keeps `/specs` as the route and presents the product
|
||||
label "Library": specs, applications, context packs, agent specs, agent
|
||||
images, plugins, and skills.
|
||||
|
||||
> Verified against source at `e8f0ab34`; package version 0.6.7. Component and
|
||||
> primitive names below are reconciled against `packages/ui/src/components/`,
|
||||
> not taken on trust from `DESIGN.md`'s lists. The web UI is in maintenance
|
||||
> mode (`docs/reference/developing.md`).
|
||||
|
||||
## 1. Library / specs surfaces
|
||||
|
||||
Components in `packages/ui/src/components/specs/`:
|
||||
|
||||
- `SpecsLibraryPage.tsx` — the `/specs` Library page (also mounted at
|
||||
`/specs/applications`): a sectioned grid of Rig Specs, Workspace Specs
|
||||
(placeholder), Workflow Specs, Context Packs, Agent Specs, Agent Images
|
||||
and Applications, then full-width Plugins and Skills sections.
|
||||
Service-backed rigs (`hasServices`) appear under Applications, not Rig
|
||||
Specs.
|
||||
- `SpecsTreeView.tsx` — the Explorer tree. (`SpecsTable.tsx` is still in
|
||||
the folder but nothing imports it.)
|
||||
- `SkillsIndexPage.tsx`, `SkillDetailPage.tsx` — skills. The Explorer
|
||||
groups skills into category folders; each skill is a single row, not
|
||||
expandable into files. Opening a skill goes to a detail route that
|
||||
defaults to `SKILL.md` (case-insensitive) and renders its own docs
|
||||
browser: a file tree with breadcrumbs beside a viewer, `MarkdownViewer`
|
||||
for `.md`/`.mdx`, `SyntaxHighlight` for other text, preformatted text
|
||||
otherwise. Files come from `/api/skills/:id/files/list` and `/files/read`
|
||||
(`hooks/useSkillFiles.ts`).
|
||||
- `PluginsIndexPage.tsx`, `PluginDetailPage.tsx`, `AgentPluginsList.tsx` —
|
||||
the plugin surfaces.
|
||||
|
||||
Library routes: `/specs`, `/specs/applications`, `/specs/skills`,
|
||||
`/specs/skills/$skillToken`, `/specs/skills/$skillToken/file/$fileToken`,
|
||||
`/specs/plugins`, `/plugins/$pluginId`, `/specs/library/$entryId` (see
|
||||
[`shell-and-routing.md`](shell-and-routing.md) §3).
|
||||
|
||||
> The plugin primitive is a **0.3.1** feature (absent at `v0.3.0`, present
|
||||
> at `v0.3.1`). Do NOT back-attribute the `/specs/plugins`,
|
||||
> `/plugins/$pluginId` routes or these plugin components to 0.3.0.
|
||||
|
||||
## 2. Spec review / library flows and seat details
|
||||
|
||||
These daemon-backed flows feed the Library UI:
|
||||
|
||||
- **Raw YAML preview:** UI/CLI posts YAML to `/api/specs/review/rig` or
|
||||
`/api/specs/review/agent`; `SpecReviewService` parses/validates/returns
|
||||
structured review models; UI renders them through `RigSpecDisplay` /
|
||||
`AgentSpecDisplay` (the same primitives reused for draft preview, library
|
||||
review, and live details).
|
||||
- **Filesystem-backed library:** `SpecLibraryService` scans builtin + user
|
||||
roots (`packages/daemon/specs`, `~/.openrig/specs`, legacy fallback
|
||||
`~/.rigged/specs`); each YAML is classified via structured review;
|
||||
service-backed rigs are marked `hasServices`. Each list request also
|
||||
merges workflow entries from the workflow-spec cache.
|
||||
`/api/specs/library` serves list, get, review and sync, plus
|
||||
`DELETE /:id`, `POST /:id/rename` and GET/POST/DELETE `/active-lens`.
|
||||
On a non-local host, list and review reads carry `?host=<id>`; the
|
||||
active lens stays local.
|
||||
- **CLI mirror:** `rig specs ls/show/preview/add/sync/remove/rename`;
|
||||
`rig specs add` installs a single YAML spec or a full spec directory;
|
||||
`rig up` / `rig bootstrap` resolve name-shaped sources through the
|
||||
library.
|
||||
- **Seat details:** graph, tree and table clicks navigate to
|
||||
`/topology/seat/$rigId/$logicalId`, which renders `LiveNodeDetails`
|
||||
(Overview and Details tabs). The shared drawer no longer has a
|
||||
seat-detail kind; its kinds are system, log, status, discovery, qitem,
|
||||
file and sub-spec. The legacy `/rigs/$rigId/nodes/$logicalId` route also
|
||||
renders `LiveNodeDetails`, but nothing in the UI links to it.
|
||||
|
||||
> Source: `packages/daemon/src/routes/spec-library.ts`;
|
||||
> `packages/ui/src/hooks/useSpecLibrary.ts`; `packages/cli/src/commands/specs.ts`
|
||||
> (`remove`, `rename`); `packages/ui/src/components/SharedDetailDrawer.tsx`
|
||||
> (viewer kinds; seat-detail retired).
|
||||
|
||||
## 3. UI primitive inventory (reconciled against source)
|
||||
|
||||
- **Vellum/base** (`components/ui/`): `VellumCard`, `VellumSheet`,
|
||||
`RegistrationMarks`, `StatusPip`, `SectionHeader`, `EmptyState`,
|
||||
`Button`, `Tabs`, `Table`, form controls, plus `rig-stamp.tsx`
|
||||
(`RigStamp`).
|
||||
- **Graphics** (`components/graphics/RuntimeMark.tsx`): `RuntimeMark`,
|
||||
`RuntimeBadge`, `ToolMark`, `ToolBadge`, `ActorMark`,
|
||||
`OperatorMoodMark`; normalization in `lib/runtime-brand.ts` +
|
||||
`lib/tool-brand.ts`. Runtime brands are Claude Code, Codex, Pi ("Pi"),
|
||||
OMP ("Oh My Pi", short "OMP"), terminal and unknown; `ActorMark` reuses
|
||||
the runtime marks for recognized runtimes.
|
||||
- **Project metadata** (`components/project/ProjectMetaPrimitives.tsx`):
|
||||
`ProjectPill` (`:199`), `EventBadge` (`:227`), `QueueStateBadge` (`:231`),
|
||||
`TagPill` (`:277`), `ActorChip` (`:286`), `DateChip` (`:306`),
|
||||
`FlowChips` (`:318`), `ProofThumbnailGrid` (`:337`), `ProofPacketHeader`
|
||||
(`:383`) — plus `QueueCountIcon` / `StatusDot`.
|
||||
- **Viewers**: `FileViewer` (`components/drawer-viewers/FileViewer.tsx`),
|
||||
`MarkdownViewer` (`components/markdown/MarkdownViewer.tsx`),
|
||||
`ProofImageViewer` and `Lightbox` (`components/project/`),
|
||||
`SessionPreviewPane` (`components/preview/`).
|
||||
- **Themes** (`lib/theme.ts`): Vellum Light, Vellum Dark and System
|
||||
(default `system`, following the OS colour scheme), stored under the
|
||||
`openrig.theme` localStorage key. `applyTheme` toggles a `.dark` class
|
||||
on `<html>`, which switches to the `.dark` token block in `globals.css`.
|
||||
`ThemeProvider` is mounted in `main.tsx`, `ThemeSelector` in the
|
||||
AppShell top bar, and a pre-paint script in `index.html` mirrors the
|
||||
storage key.
|
||||
|
||||
## 4. Design-system pointer (NOT a copy — Q1)
|
||||
|
||||
The canonical OpenRig visual/design system is **`docs/DESIGN.md`** (at the
|
||||
repo `docs/` root, NOT under `docs/as-built/`). Q1 is ratified: DESIGN.md
|
||||
stays at root (the as-built reorganization left it byte-identical), and this
|
||||
module **points to it rather than copying it**. DESIGN.md is the source for: Vellum-paper vs
|
||||
Black-glass surface languages, colour tokens (`packages/ui/src/globals.css`
|
||||
+ `tailwind.config.ts`), typography (`font-body` Inter / `font-headline`
|
||||
Space Grotesk / `font-mono` JetBrains Mono), layout (48px rail + Explorer +
|
||||
center + drawer + preview stack), the core-primitive list, the graphics
|
||||
system, and the interaction/motion/accessibility/do-and-do-not rules.
|
||||
|
||||
> Reconciliation note: every primitive DESIGN.md names (Vellum, Graphics,
|
||||
> Project-metadata, Topology, Preview) resolves to a real export at the
|
||||
> stamped commit. Do not duplicate its content here; read `docs/DESIGN.md`
|
||||
> directly for the visual spec.
|
||||
|
||||
## See also
|
||||
|
||||
- `docs/DESIGN.md` — the canonical visual/design-system spec (pointer; do
|
||||
not copy).
|
||||
- `shell-and-routing.md` — the `/specs` route family + shell + drawer.
|
||||
- `../architecture/agent-spec-and-startup.md` — the spec parsing/resolution
|
||||
contract behind the review flows.
|
||||
- `../architecture/plugin-agent-image-context-pack.md` — the 0.3.1 content
|
||||
layer behind the plugins/agent-image/context-pack Library surfaces.
|
||||
- Source roots: `packages/ui/src/components/specs/`,
|
||||
`packages/daemon/src/domain/{spec-review-service,spec-library-service}*`.
|
||||
@@ -1,380 +0,0 @@
|
||||
---
|
||||
kind: as-built
|
||||
title: UI Project Observability, For You, Dashboard
|
||||
status: active
|
||||
topics: [observability, coordination]
|
||||
domains: [engineering-advisor, product-advisor, operating-advisor]
|
||||
applies-when: |
|
||||
Need to know how the operator-facing destination surfaces are built —
|
||||
the For-You attention feed (5-card classifier, attention merge, verb
|
||||
actions), the Project workspace/mission/slice scope pages (tabbed
|
||||
rollups), and the Dashboard launcher. The web UI is in maintenance mode;
|
||||
claims are sourced to file and symbol (and line where verified) at the
|
||||
stamped commit.
|
||||
siblings: [shell-and-routing.md, ../architecture/mission-control.md]
|
||||
prerequisite-reads: [../README.md, shell-and-routing.md]
|
||||
last-verified-against-source: e8f0ab340db773392ec8be75b072d1c0f3068a50
|
||||
last-updated: 2026-10-08
|
||||
---
|
||||
|
||||
# UI Project Observability, For You, Dashboard
|
||||
|
||||
The three operator-facing destination surfaces: **For You** (`/for-you`,
|
||||
the attention feed), **Project** (`/project*`, the workspace/mission/slice
|
||||
scope pages), and the **Dashboard** (`/`, the launcher). All read live
|
||||
daemon state. For You persists two dismissal sets in localStorage; shared
|
||||
shell preferences include the theme choice (see
|
||||
[`library-specs-and-design-system.md`](library-specs-and-design-system.md))
|
||||
and the maintenance-notice dismissal (see
|
||||
[`shell-and-routing.md`](shell-and-routing.md)).
|
||||
|
||||
> Paths are relative to `packages/ui/src/` unless prefixed `docs/` or
|
||||
> `packages/`. Verified at `e8f0ab34` (package version 0.6.7). The web UI
|
||||
> is in maintenance mode (`docs/reference/developing.md`); this page
|
||||
> records what ships, not a roadmap.
|
||||
|
||||
## 0. Release attribution (forensic seam)
|
||||
|
||||
Checked via `git cat-file -e <tag>:<path>`:
|
||||
|
||||
| Layer | Release | Forensic proof |
|
||||
|---|---|---|
|
||||
| Project-observability foundation (For-You feed, feed-classifier, project scope pages, the Mission Control 7-verb vocabulary) | **0.3.0** | `v0.3.0:components/for-you/Feed.tsx`, `v0.3.0:lib/feed-classifier.ts`, `v0.3.0:components/project/ScopePages.tsx`, `v0.3.0:components/mission-control/components/VerbActions.tsx` all resolve at `v0.3.0` |
|
||||
| Vellum **brand-identity system** (`dashboard/vellum/*`: CornerBracket, VellumDestinationCard, marks, graphics, barrel) | **0.3.1, NOT 0.3.0** | `git cat-file -e v0.3.0:packages/ui/src/components/dashboard/vellum/index.ts` fails; `v0.3.1:` resolves |
|
||||
|
||||
Vellum surface *primitives* (`components/ui/vellum-*.tsx`) shipped 0.3.0;
|
||||
the brand system under `dashboard/vellum/` is 0.3.1. Do not back-attribute
|
||||
the brand system to 0.3.0.
|
||||
|
||||
## 1. Route reality
|
||||
|
||||
Verified at `routes.tsx` (605 lines):
|
||||
|
||||
| Path | Reality | Component | Source |
|
||||
|---|---|---|---|
|
||||
| `/` (index) | destination route | `Dashboard` | `routes.tsx:96-100` |
|
||||
| `/for-you` | destination route | `Feed` | `routes.tsx:130-134` |
|
||||
| `/project` | destination route | `WorkspaceScopePage` | `routes.tsx:136-140` |
|
||||
| `/project/mission/$missionId` | destination route | `MissionScopePage` | `routes.tsx:142-146` |
|
||||
| `/project/slice/$sliceId` | destination route | `SliceScopePage` | `routes.tsx:148-152` |
|
||||
| `/mission-control` | redirect stub to `/for-you` | `() => <Navigate to="/for-you">` | `routes.tsx:496-500` |
|
||||
| `/slices` | redirect stub to `/project` | `() => <Navigate to="/project">` | `routes.tsx:503-507` |
|
||||
| `/slices/$name` | redirect stub to `/project/slice/$sliceId` | `useParams` → `<Navigate>` | `routes.tsx:509-516` |
|
||||
| `/progress` | redirect stub to `/project` | `() => <Navigate to="/project">` | `routes.tsx:519-523` |
|
||||
| `/steering` | redirect stub to `/project` | `() => <Navigate to="/project">` | `routes.tsx:526-530` |
|
||||
| `/markdown` | **not a route** — `MarkdownViewer` is a component | (no route) | no `path: "/markdown"` in `routes.tsx` |
|
||||
|
||||
> Source: component imports `routes.tsx:42-43` (`Dashboard`, `Feed`),
|
||||
> `:61-65` (`WorkspaceScopePage`/`MissionScopePage`/`SliceScopePage` from
|
||||
> `components/project/ScopePages.js`).
|
||||
|
||||
The review altitudes `/agents` (`RigAgentsPage`) and `/fleet` (`FleetPage`)
|
||||
are listed in [`shell-and-routing.md`](shell-and-routing.md).
|
||||
|
||||
> **Route-reality contract:** the **Mission Control *system*** lives in the
|
||||
> daemon (see `../architecture/mission-control.md`); `/mission-control` is
|
||||
> a redirect stub, not a UI destination. The 7-verb *action vocabulary*
|
||||
> surfaces here via `VerbActions` in For-You cards. `/progress`, `/slices`
|
||||
> and `/steering` fold into `/project` tabs.
|
||||
|
||||
## 2. For You — the attention feed (`/for-you` → `Feed`)
|
||||
|
||||
`Feed` is the operator's attention surface: decision cards first, then the
|
||||
rest newest-first, subscription- and lens-filtered and soft-dismissable.
|
||||
Header reads `Attention` / `For You`; max width 720px. A plain-language
|
||||
`LevelControl` (All activity / Highlights / Needs you) sits at the top of
|
||||
the feed over the same subscription toggles.
|
||||
|
||||
> Source: `components/for-you/Feed.tsx` (`data-testid="for-you-feed"`,
|
||||
> max-w-720 header; `LevelControl` in `feed-level-control`; design note
|
||||
> `:3-12`); `lib/feed-levels.ts` `FEED_LEVEL_LABELS`.
|
||||
|
||||
### 2.1 The 5-card classifier (0.3.0 spine)
|
||||
|
||||
`classifyFeed(events)` maps every `ActivityEvent` to one of five
|
||||
`FeedCardKind` values — `action-required`, `approval`, `shipped`,
|
||||
`progress`, `observation` — sorted by `receivedAt` descending. **Nothing is
|
||||
silently dropped**: an unmatched event type falls through to
|
||||
`observation`. Queue events are sub-classified by
|
||||
`queueKind(type, state, tier)`: `*.closed` → shipped;
|
||||
`qitem.closure_overdue` / `inbox.denied` → action-required; tier
|
||||
`human-gate` → **approval**; state `pending-approval` → action-required;
|
||||
`closeout-pending-ratify` → approval; `done|closed|completed|shipped` →
|
||||
shipped; otherwise progress. A human-seat destination forces
|
||||
`action-required` unless the card is already an approval.
|
||||
|
||||
> Source: `lib/feed-classifier.ts` — `FeedCardKind` `:10-15`, `queueKind`
|
||||
> `:105-129`, `isHumanSeat`, `classifyEvent` (approval-preserving
|
||||
> human-seat override; default-observation fallthrough), `classifyFeed`,
|
||||
> `sortFeedByDecisionBand`.
|
||||
|
||||
### 2.2 The feed pipeline
|
||||
|
||||
1. **Event cards:** `classifyFeed(events)` capped at `HISTORY_LIMIT = 50`.
|
||||
2. **Attention merge:** daemon attention rows (`useAttentionItems(50, …)`:
|
||||
`GET /api/queue/list?attention=1…`, or `GET /api/queue/attention-aggregate`
|
||||
when a remote host subscription is on) and needs-input seats
|
||||
(`useNeedsInputSeats`) become cards, and `mergeAttentionIntoFeed` lets a
|
||||
queue-derived card supersede a matching event-derived **action-required
|
||||
or approval** card; shipped, progress and observation event cards for the
|
||||
same qitem remain.
|
||||
3. **Decision-band sort:** `sortFeedByDecisionBand` lifts every
|
||||
action-required and approval card above the rest, newest-first within
|
||||
each band.
|
||||
4. **Hydration:** `hydratedCardKind` re-reads each card against the live
|
||||
queue item and the action audit: a recorded outcome on an
|
||||
action/approval card → `approval`; tier `human-gate` → `approval`; a
|
||||
`done|closed|completed` item → `shipped`; a human-seat destination →
|
||||
`action-required`.
|
||||
5. **Filters, in order:** subscription (`isCardKindSubscribed`;
|
||||
`action-required` always visible, `observation` only with the audit
|
||||
subscription), the transient lens chips (All / Action req / Approvals /
|
||||
Shipped / Progress / Audit), a per-host filter, then dismissal.
|
||||
Event cards dismiss by event seq (`forYou.dismissedSeqs`); queue-derived
|
||||
cards dismiss by card id (`forYou.dismissedCardIds`); both are
|
||||
localStorage-backed.
|
||||
|
||||
> Source: `components/for-you/Feed.tsx` — `LENS_CHIPS` `:60-67`,
|
||||
> `HISTORY_LIMIT` `:69`, `hydratedCardKind` `:167-186`, attention merge and
|
||||
> sort `:244-276`, dismiss hooks `:285`/`:294`, filter pipeline `:334-356`;
|
||||
> `hooks/useAttentionItems.ts`, `hooks/useNeedsInputSeats.ts`,
|
||||
> `lib/attention-feed.ts` (`mergeAttentionIntoFeed`, `attentionKindFor`),
|
||||
> `hooks/useDismissedSeqs.ts`, `hooks/useDismissedCardIds.ts`.
|
||||
|
||||
With one or more remote host subscriptions enabled, the feed shows host
|
||||
chips and status rows, cards carry a host chip, and verbs forward the
|
||||
card's `hostId` (`Feed.tsx` host filter; `hooks/useFeedSubscriptions.ts`
|
||||
key `feed.subscriptions.<hostId>.enabled`; `VerbActions.tsx`).
|
||||
|
||||
### 2.3 Queue-item hydration + proof previews
|
||||
|
||||
For cards carrying a `qitemId`, `useQueueItemMap` hydrates the queue item;
|
||||
for `shipped` cards `sliceForCard` matches a slice by tag/text and
|
||||
`proofPreviewForSlice` pulls the first proof packet with screenshots, so
|
||||
the card renders an inline `ProofThumbnailGrid` → `ProofImageViewer`.
|
||||
|
||||
> Source: `components/for-you/Feed.tsx` (`useQueueItemMap`,
|
||||
> `sliceForCard`, `proofPreviewForSlice`); `FeedCard.tsx` proof block and
|
||||
> `<ProofImageViewer>`.
|
||||
|
||||
### 2.4 Verb actions on action/approval cards
|
||||
|
||||
The **canonical Mission Control action vocabulary is the 7-verb system** —
|
||||
`MISSION_CONTROL_VERBS = [approve, deny, route, annotate, hold, drop,
|
||||
handoff]`. `VerbActions` defaults to all seven
|
||||
(`enabledVerbs = [...MISSION_CONTROL_VERBS]`); route/handoff need a
|
||||
destination, annotate an annotation, hold/drop a reason.
|
||||
|
||||
> Source: `components/mission-control/hooks/useMissionControlAction.ts:5-15`;
|
||||
> `components/mission-control/components/VerbActions.tsx` (`enabledVerbs`
|
||||
> default `:108`).
|
||||
|
||||
**The For-You actionable-card surface is bare one-tap APPROVE + CHAT.**
|
||||
`FeedCard` passes `bare`, `enabledVerbs={["approve"]}` and
|
||||
`oneClickVerbs={["approve"]}`, beside a CHAT button that opens the shared
|
||||
`ProgressiveTerminal` seeded via `buildChatPreamble` (human-action cards
|
||||
chat with the sender). Deny/route are not offered here, and the
|
||||
action-required empty state reads "one-tap approve and chat with the
|
||||
owning agent". The 7-verb vocabulary remains the Mission Control
|
||||
system-level vocabulary.
|
||||
|
||||
> Source: `components/for-you/FeedCard.tsx:558-570` (`bare` `:561`,
|
||||
> `enabledVerbs` `:563`, `oneClickVerbs` `:564`),
|
||||
> `resolveCardTerminalSession`; `Feed.tsx:76-82` (`EMPTY_COPY`);
|
||||
> `review/chat.ts:21` (`buildChatPreamble`); test
|
||||
> `test/foryou-bare-approve-chat.test.tsx`.
|
||||
|
||||
On mutation success `VerbActions` fires `onOptimisticOutcome`; `Feed`
|
||||
keeps an optimistic-outcome map keyed by `qitemId` so the
|
||||
`ActionOutcomePanel` receipt renders before the audit re-fetch. A terminal
|
||||
qitem with no recorded outcome derives a fallback receipt from its closure
|
||||
reason. Card footers also carry a "show context" queue-item trigger, a
|
||||
terminal drill, the evidence reference, and a "review →" link to the
|
||||
slice's Review tab.
|
||||
|
||||
> Source: `Feed.tsx` (optimistic map; optimistic-first lookup);
|
||||
> `FeedCard.tsx` (`isActionableCard`, `fallbackOutcomeFromQueueItem`,
|
||||
> `ActionOutcomePanel`, `QueueItemTrigger`, `FeedCardTerminalDrill`).
|
||||
|
||||
### 2.5 Card surface (0.3.1 brand layer)
|
||||
|
||||
`FeedCard` renders a `bg-surface-low/45 backdrop-blur-[10px]` surface with
|
||||
a 3-stop ambient box-shadow (no border), four `CornerBracket` marks, a
|
||||
mono-uppercase kind tag with a tone-coloured icon (a dot only when a kind
|
||||
has no icon), and `ActorMark` runtime marks. Cards dismiss by keyboard
|
||||
(Backspace/Delete) or swipe, with an `UndoToast`.
|
||||
|
||||
> Source: `components/for-you/FeedCard.tsx` — `CARD_SURFACE_CLASS`
|
||||
> `:108-109`, `CARD_SHADOW_STYLE`, `KIND_TOKEN`, corner brackets
|
||||
> `:452-455`, `ActorMark` `:265`; `CornerBracket` from
|
||||
> `dashboard/vellum/index.ts`.
|
||||
|
||||
The 0.3.1 storytelling preview band is **gone**: `Feed.tsx:375-380` records
|
||||
its removal. The `components/feed/cards/storytelling-cards.tsx` primitives
|
||||
remain for the `/lab/card-previews` gallery. No UI code calls
|
||||
`POST /api/missions/:id/complete` any more; the daemon route still exists
|
||||
(`packages/daemon/src/routes/missions.ts`).
|
||||
|
||||
## 3. Project — workspace / mission / slice scope pages (0.3.0 spine)
|
||||
|
||||
`/project*` mounts three scope pages on a shared `ScopeShell` +
|
||||
`TabNav` tabbed pattern. Tab sets:
|
||||
|
||||
| Page | Tabs | Default |
|
||||
|---|---|---|
|
||||
| `WorkspaceScopePage` | `SHARED_TABS`: Overview, Story, Progress, Artifacts, Proof, Queue, Workflow (`topology`) | `overview` |
|
||||
| `MissionScopePage` | `MISSION_TABS`: Overview, Steering, Review, Story, Progress, Artifacts, Proof, Queue, Workflow | `steering` |
|
||||
| `SliceScopePage` | `SLICE_TABS`: Review, Story, Overview, Progress, Artifacts, Proof, Queue, Workflow | `review` |
|
||||
|
||||
> Source: `components/project/ScopePages.tsx` — `SharedTab`/`SliceTab`
|
||||
> `:83-84`, `SHARED_TABS` `:86-94`, `MISSION_TABS` `:98-110`, `SLICE_TABS`
|
||||
> `:112-123`, `TabNav`, `ScopeShell`; defaults `useState` at `:698`,
|
||||
> `:798`, `:1226`.
|
||||
|
||||
Off the local host, the workspace and mission headers show an `ON <host>`
|
||||
chip (the slice header does not), the Review tab is local-only, and
|
||||
file-backed sections explain why local files are not shown.
|
||||
|
||||
### 3.1 WorkspaceScopePage (`/project`)
|
||||
|
||||
Reads the live workspace name; honest empty state (`NO WORKSPACE
|
||||
CONNECTED` with an Open-settings action) when unset. Tabs: Overview →
|
||||
`WorkspacePortfolioPanel`; Story/Progress/Artifacts/Proof/Queue → the
|
||||
scope rollups (§3.4); Workflow → `HostMultiRigGraph`.
|
||||
(`WorkspaceOverviewPanel`, the older Current Work / Archive grid, is still
|
||||
defined but not rendered.)
|
||||
|
||||
> Source: `ScopePages.tsx` `WorkspaceScopePage` (no-workspace guard;
|
||||
> `WorkspacePortfolioPanel` `:745`; `HostMultiRigGraph` `:788`).
|
||||
|
||||
### 3.2 MissionScopePage (`/project/mission/$missionId`)
|
||||
|
||||
Lands on **Steering** (`SteeringTab`). Review → `MissionReviewTab` (§3.5).
|
||||
Overview renders the mission README (`MarkdownViewer`) above a slice rail.
|
||||
Progress renders `MissionProgressHeatmap` plus the mission `PROGRESS.md`
|
||||
(the per-slice rollup cards were removed). Artifacts → `ArtifactsNavigator`;
|
||||
Proof → `ScopeProofRollup`. Workflow renders the **projected workflow
|
||||
spec graph** via `TopologyTab` when the mission README declares a
|
||||
`workflow_spec` that is in the workflow-spec cache (the daemon returns
|
||||
`topology.specGraph`), otherwise `ScopeTopologyRollup`.
|
||||
|
||||
> Source: `ScopePages.tsx` `MissionScopePage` (`SteeringTab` `:834`,
|
||||
> `MissionReviewTab` `:843`, `MissionProgressHeatmap` `:906`,
|
||||
> rollup-cards removal note `:979-983`, `ArtifactsNavigator` `:990`,
|
||||
> spec-graph branch `:1010-1036`).
|
||||
|
||||
### 3.3 SliceScopePage (`/project/slice/$sliceId`)
|
||||
|
||||
Lands on **Review** (`SliceReviewTab`, §3.5). Overview →
|
||||
`SliceOverviewTab` (a 4-metric summary — status, progress, qitems, last
|
||||
activity — then the README). Story → `ScopeStoryRollup` (the queue-lineage
|
||||
story graph). Progress → `AcceptanceTab`. Artifacts → `ArtifactsNavigator`;
|
||||
Proof → `SliceProofTab`; Queue → `SliceQueueTab`; Workflow → `TopologyTab`.
|
||||
Loading and error states are honest (the error names
|
||||
`rig config get workspace.slices_root` as the likely misconfiguration).
|
||||
|
||||
> Source: `ScopePages.tsx` `SliceScopePage` (`SliceReviewTab` `:1307`,
|
||||
> `ScopeStoryRollup` `:1317`, `SliceOverviewTab` `:1325`, `AcceptanceTab`
|
||||
> `:1354`, `ArtifactsNavigator` `:1363`, `SliceProofTab` `:1366`,
|
||||
> `TopologyTab` `:1379`, slices_root hint `:1276`), `SliceOverviewTab`
|
||||
> summary grid `:1157-1162`.
|
||||
|
||||
### 3.4 Shared scope rollups
|
||||
|
||||
`ScopeStoryRollup`, `ScopeProgressRollup`, `ScopeArtifactsRollup`,
|
||||
`ScopeQueueRollup` and `ScopeTopologyRollup` read from one
|
||||
`useProjectScopeRollup(missionId, loadDetails)` hook so workspace and
|
||||
mission scopes share rollup behavior; `ScopeProofRollup`
|
||||
(`project/ProofTab.tsx`) covers Proof. Detail fetching is gated on the
|
||||
active tab (`active !== "overview"` for the workspace;
|
||||
`active !== "overview" && active !== "steering"` for a mission).
|
||||
|
||||
> Source: `ScopePages.tsx` `useProjectScopeRollup`, rollup components,
|
||||
> gates `:700`, `:803`.
|
||||
|
||||
### 3.5 Review tabs (the Living Notes surface)
|
||||
|
||||
The slice tab (`review/SliceReviewTab.tsx`) renders one reviewable
|
||||
structure per slice — bands in order **NEEDS YOU → AGENTS → INTENT / PLAN
|
||||
/ DELIVERED → verify-lineage → SETTLED** — from `GET /api/review/slice/:name`
|
||||
(`hooks/useReview.ts`), with a proof-readiness status line and a defects
|
||||
box above NEEDS YOU:
|
||||
|
||||
- **DELIVERED** pairs each planned deliverable with its curated proof,
|
||||
media inline and expanding one at a time (`?item=` deep link). Without a
|
||||
configured readiness policy, `verified` renders as `✓ legacy QA-verified
|
||||
(item revision unbound)` · `◇ unverified — no PASSING QA comparison` ·
|
||||
`✗ missing — promised, nothing delivered`; with one, as `✓ accepted under
|
||||
selected policy` / `◇ current acceptance absent`. A note renders as
|
||||
"Judgment note:". Visible, never blocking. "See all proof" drills into
|
||||
`proof/`.
|
||||
- **Media plays**: video inline with native controls (`?seek`/`?play` deep
|
||||
links); images open `Lightbox`; evidence and the "full PRD →" door open
|
||||
`FileViewer` in the shared right-edge drawer.
|
||||
- **Two locks** render as stamps: plan-lock in PLAN, both stamps in
|
||||
SETTLED; an unaudited stamp renders as UNVERIFIED.
|
||||
- Review cards share one vellum recipe (`review/vellum.ts`); DELIVERED rows
|
||||
stack below the `sm` breakpoint.
|
||||
- The mission tab (`review/MissionReviewTab.tsx`) is board-first: stage
|
||||
cells, the completion ledger, cut-complete, and a row expansion reading
|
||||
the same contract. The rig altitude (`review/RigAgentsPage.tsx`, route
|
||||
`/agents`) reads `GET /api/review/rig`.
|
||||
|
||||
Backend contract: see
|
||||
[`../architecture/living-notes-review.md`](../architecture/living-notes-review.md).
|
||||
|
||||
> Source: `components/review/{SliceReviewTab,MissionReviewTab,
|
||||
> NeedsYouAccordion,AgentsBandView,VerifyLineageCard,EvidenceOpener,
|
||||
> RigAgentsPage}.tsx`, `review/vellum.ts`, `hooks/useReview.ts`,
|
||||
> `SharedDetailDrawer.tsx`; verified labels `SliceReviewTab.tsx:131-135`,
|
||||
> `:164`, `:194`.
|
||||
|
||||
## 4. Dashboard — the launcher (`/` → `Dashboard`)
|
||||
|
||||
`Dashboard` is the paper-draft launcher (OPR.0.4.1.14). It renders, in
|
||||
order: a header with the station online state; `FieldEnvironment` (live
|
||||
rows: RIGS from `useRigSummary`, AGENTS as the sum of `nodeCount` from
|
||||
`usePsEntries`, STATION from `window.location.hostname`, OPERATOR ID from
|
||||
the `agents.operator_session` setting with an `OPERATOR` fallback, and
|
||||
VERSION from `useDaemonVersion` via `/api/health-summary/version`);
|
||||
`KernelStatusCard` (from `/api/kernel/status`); `HostConfigCard`; a grid of
|
||||
six destinations (Topology, Project, For You, Library, Search & Audit,
|
||||
Settings); and `DashboardFooter`. Its glyphs come from
|
||||
`dashboard/vellum/fidelity-glyphs.js`.
|
||||
|
||||
The kernel card renders `kernel_state: "skipped"` as a neutral **boot not
|
||||
tracked** state. Built-in boot can be skipped because a kernel rig is already
|
||||
managed or because automatic boot was disabled; the served reason remains
|
||||
visible. Ready-agent counts do not promote this state to healthy. An unavailable
|
||||
tracker or a malformed status envelope remains **unknown**. Recovery still uses
|
||||
the existing launch-plan and daemon restore guards.
|
||||
|
||||
The vellum barrel `dashboard/vellum/index.ts` still exports
|
||||
`BackLayerContent` and `BackVellumSheet`; `/lab/vellum-lab`
|
||||
(`lab/VellumLab.tsx`) imports from it.
|
||||
|
||||
> Source: `components/dashboard/Dashboard.tsx` (header comment `:1-24`,
|
||||
> glyph import, `DESTINATIONS`, data wiring, render order);
|
||||
> `hooks/useDaemonVersion.ts`; `components/dashboard/vellum/index.ts:5-6`;
|
||||
> `components/KernelStatusCard.tsx`; `components/RigStatusCard.tsx`.
|
||||
|
||||
## 5. Cross-cutting properties
|
||||
|
||||
- **Live daemon state** — every surface reads daemon hooks. UI-local state
|
||||
on these surfaces is the two For You dismissal sets (localStorage), the
|
||||
lens, host filter and optimistic-outcome map (transient), and each scope
|
||||
page's tab state.
|
||||
- **Honest empty/error states** — `WorkspaceScopePage` no-workspace and
|
||||
`SliceScopePage` not-available surface the cause and a remediation
|
||||
pointer, never a blank screen.
|
||||
- **0.3.0 observability spine on the 0.3.1 vellum brand layer** — the
|
||||
classifier / verb-action / scope-rollup logic is the 0.3.0 foundation
|
||||
(§0); the visual surface is the 0.3.1 brand layer and later refreshes.
|
||||
|
||||
## OPEN items (carried, not smoothed)
|
||||
|
||||
- **OPEN — `/project` redirect-stub consolidation.** `/progress`,
|
||||
`/slices`, `/steering` are `<Navigate to="/project">` stubs and
|
||||
`/slices/$name` redirects into `/project/slice/$sliceId` (§1). Whether
|
||||
the stubs are permanent or transitional is a product decision not
|
||||
resolvable from source.
|
||||
@@ -1,202 +0,0 @@
|
||||
---
|
||||
kind: as-built
|
||||
title: UI Shell, Routing, Drawer System
|
||||
status: active
|
||||
topics: [observability]
|
||||
domains: [engineering-advisor, product-advisor]
|
||||
applies-when: |
|
||||
Need to know how the UI shell (AppShell rail / Explorer / center workspace /
|
||||
drawer / preview stack) is assembled, the actual route tree the shipped UI
|
||||
mounts, or how the shared detail drawer and event consumption work.
|
||||
siblings: [topology.md, project-and-for-you.md, library-specs-and-design-system.md]
|
||||
prerequisite-reads: [../README.md]
|
||||
last-verified-against-source: e8f0ab340db773392ec8be75b072d1c0f3068a50
|
||||
last-updated: 2026-10-08
|
||||
---
|
||||
|
||||
# UI Shell, Routing, Drawer System
|
||||
|
||||
The `@openrig/ui` package is the shell-first, route-first, primitive-driven
|
||||
operator surface. Brand/visual rules live in `docs/DESIGN.md` (pointer from
|
||||
`library-specs-and-design-system.md`); this module is how the shell is
|
||||
assembled and what routes it mounts.
|
||||
|
||||
> Verified against source at `e8f0ab34`; package version 0.6.7. UI
|
||||
> footprint is **304** source files (`packages/ui/src`, `.ts`+`.tsx`,
|
||||
> non-test; 235 at the previous stamp `7eaf524c`). The web UI is in
|
||||
> maintenance mode (`docs/reference/developing.md`).
|
||||
|
||||
## 1. Package shape
|
||||
|
||||
UI package: `packages/ui`. Primary implementation areas:
|
||||
|
||||
- `src/routes.tsx` — TanStack Router route tree.
|
||||
- `src/App.tsx`, `src/main.tsx` — app root (`ThemeProvider`, the
|
||||
maintenance notice, the router).
|
||||
- `src/components/AppShell.tsx` — global shell: rail, Explorer, drawer,
|
||||
preview stack.
|
||||
- `src/components/ui/` — reusable vellum + base UI primitives.
|
||||
- `src/components/graphics/RuntimeMark.tsx` — runtime/tool/actor marks.
|
||||
- `src/components/topology/` — graph/table/terminal topology surfaces.
|
||||
- `src/components/project/`, `for-you/`, `dashboard/`, `feed/` —
|
||||
observability surfaces.
|
||||
- `src/components/review/`, `workflow/` — review altitudes (`/agents`,
|
||||
`/fleet`) and workflow pages.
|
||||
- `src/components/specs/` — Library / skills / plugins surfaces.
|
||||
- `src/components/{preview,markdown,drawer-viewers}/` — viewers.
|
||||
- `src/hooks/`, `src/lib/` — route data hooks, classifiers, formatters,
|
||||
layout/brand helpers.
|
||||
|
||||
## 2. Shell model
|
||||
|
||||
The root route wraps `AppShell` in `QueryClientProvider` and
|
||||
`DaemonHealthProvider` (`routes.tsx:80-90`). Above the router, `App.tsx`
|
||||
renders a dismissible `UiMaintenanceNotice` (localStorage key
|
||||
`openrig.uiMaintenanceNoticeDismissed`); `main.tsx` mounts `ThemeProvider`.
|
||||
|
||||
`AppShell.tsx` lays a 48px icon rail + a route-aware Explorer sidebar +
|
||||
center workspace + shared right drawer + preview stack + topology overlay
|
||||
provider + shared drawer-selection / discovery-placement / specs-workspace
|
||||
contexts. The top bar's right slot holds `HostIndicator` and
|
||||
`ThemeSelector`. When the shared event connection is down, a "Live updates
|
||||
unavailable…" status line shows. Switching the selected host clears the
|
||||
discovery placement (`useClearPlacementOnHostSwitch`). Below the `lg`
|
||||
breakpoint a 3-slot `MobileBottomNav` (For You / Project / Topology) also
|
||||
shows.
|
||||
|
||||
The rail destinations (`AppShell.tsx` `RAIL_ICONS`) are exactly six
|
||||
destination icons plus Advisor/Operator entries:
|
||||
|
||||
- Dashboard `/` (`rail-dashboard`)
|
||||
- Topology `/topology` (`rail-topology`)
|
||||
- For You `/for-you` (`rail-for-you`)
|
||||
- Project `/project` (`rail-project`)
|
||||
- Library `/specs` (`rail-specs`)
|
||||
- Settings `/settings` (`rail-settings`)
|
||||
|
||||
The Explorer is contextual: its tree changes with the current destination
|
||||
rather than acting as a generic file browser.
|
||||
|
||||
## 3. Route model
|
||||
|
||||
> The route table below is derived from `packages/ui/src/routes.tsx` at
|
||||
> `e8f0ab34` (605 lines). Each line number is the route's
|
||||
> `createRoute(` line. The exported route tree is assembled at
|
||||
> `routes.tsx:539` (`export const routeTree = rootRoute.addChildren([`);
|
||||
> it holds 54 routes.
|
||||
>
|
||||
> `/mission-control` and `/progress` are **redirect stubs**, not
|
||||
> destination routes, and there is **no `/markdown` route** (markdown is
|
||||
> rendered by the `MarkdownViewer` component). There is a real `/files`
|
||||
> route.
|
||||
|
||||
### Primary destination routes
|
||||
|
||||
| Path | Component | Notes |
|
||||
|---|---|---|
|
||||
| `/` | Dashboard (`indexRoute` `routes.tsx:96`) | rail destination |
|
||||
| `/topology` | host topology (`:106`) | + rig/pod/seat scopes (see `topology.md`) |
|
||||
| `/topology/rig/$rigId` | rig scope (`:112`) | |
|
||||
| `/topology/pod/$rigId/$podName` | pod scope (`:118`) | |
|
||||
| `/topology/seat/$rigId/$logicalId` | seat scope (`:124`) | live node details |
|
||||
| `/for-you` | attention feed (`:130`) | rail destination |
|
||||
| `/project` | workspace project scope (`:136`) | rail destination |
|
||||
| `/project/mission/$missionId` | mission scope (`:142`) | |
|
||||
| `/project/slice/$sliceId` | slice scope (`:148`) | |
|
||||
| `/specs` | Library (`:202`) | rail destination |
|
||||
| `/specs/applications` | applications section (`:208`) | |
|
||||
| `/specs/skills` | skills index (`:216`) | |
|
||||
| `/specs/skills/$skillToken` | skill viewer (`:222`) | defaults to `SKILL.md` |
|
||||
| `/specs/skills/$skillToken/file/$fileToken` | skill file viewer (`:231`) | |
|
||||
| `/specs/plugins` | plugins index (`:242`) | 0.3.1 (see `library-specs…`) |
|
||||
| `/plugins/$pluginId` | plugin detail (`:257`) | 0.3.1 |
|
||||
| `/specs/$specKind/$specName` | generic spec → library redirect (`:269`) | |
|
||||
| `/files` | Files workspace (`:248`) | |
|
||||
| `/settings` | settings center (`:278`) | rail destination |
|
||||
| `/settings/policies` | Policies (`:288`) | Claude-compaction form |
|
||||
| `/settings/log` | Log (`:293`) | 4-item Settings explorer |
|
||||
| `/settings/status` | Status (`:298`) | |
|
||||
| `/search` | audit/history view (`:304`) | |
|
||||
|
||||
### Zoom-addressed routes (no rail entry)
|
||||
|
||||
Reached from other views and deep links, deliberately not on the rail:
|
||||
|
||||
| Path | Component | Notes |
|
||||
|---|---|---|
|
||||
| `/agents` | `RigAgentsPage` (`:159`) | rig review altitude; separate from legacy `/agents/validate` |
|
||||
| `/fleet` | `FleetPage` (`:171`) | fleet attention altitude above host |
|
||||
| `/workflows` | `WorkflowsPage` (`:181`) | |
|
||||
| `/workflow/instance/$instanceId` | `WorkflowInstancePage` (`:187`) | optional `?step=<id>` anchor |
|
||||
|
||||
### Lab routes (design experiments)
|
||||
|
||||
`/lab/project-graphics-preview` (`:310`), `/lab/card-previews` (`:318`),
|
||||
`/lab/vellum-lab` (`:328`), `/lab/vellum-bg/{a-large,b-small,c-allover}`
|
||||
(`:339`–`:349`). The vellum-lab + vellum-bg routes are the 0.3.1 vellum
|
||||
brand-system experiment surface (the `dashboard/vellum/*` system is 0.3.1).
|
||||
|
||||
### Legacy / compatibility routes
|
||||
|
||||
`/rigs/$rigId` (`:374`), `/rigs/$rigId/nodes/$logicalId` (`:380`),
|
||||
`/import` (`:389`), `/packages`, `/packages/install`, `/packages/$packageId`
|
||||
(`:395`–`:407`), `/bootstrap` (`:413`), `/agents/validate` (`:419`),
|
||||
`/specs/rig` + `/specs/agent` review (`:428`/`:434`),
|
||||
`/specs/library/$entryId` (`:440`), `/discovery` + `/discovery/inventory`
|
||||
(`:451`/`:466`), `/bundles/inspect` + `/bundles/install` (`:472`/`:478`).
|
||||
|
||||
### Redirect stubs (deleted routes)
|
||||
|
||||
`routes.tsx:488`–`:530` mount `<Navigate>` redirects for removed routes —
|
||||
NOT destination pages: `/context` → `/topology`; `/mission-control` →
|
||||
`/for-you` (For-You replaces it); `/slices` → `/project`; `/slices/$name` →
|
||||
`/project/slice/$sliceId` (with `sliceId` set to the name); `/progress` →
|
||||
`/project`; `/steering` → `/project`. The Mission Control *system* still
|
||||
exists in the daemon (`../architecture/mission-control.md`); only the old
|
||||
UI *route* was retired in favour of For-You.
|
||||
|
||||
## 4. Drawer and viewer system
|
||||
|
||||
`SharedDetailDrawer` is the shared transient detail surface for queue-item
|
||||
viewer, file viewer (`FileViewer`), subspec preview, and other detail
|
||||
selections. Drawer triggers carry root/path provenance so the viewer
|
||||
fetches and renders the correct content; the drawer supports outside-click
|
||||
dismissal.
|
||||
|
||||
Image proof viewing: For-You cards use `ProofImageViewer`
|
||||
(`components/project/ProofImageViewer.tsx`), which stays inside the shell
|
||||
layout. The Project Proof tab and the Review surfaces use the full-viewport
|
||||
`Lightbox` (`components/project/Lightbox.tsx`; mounted in
|
||||
`project/ProofTab.tsx`, `review/EvidenceOpener.tsx`,
|
||||
`review/SliceReviewTab.tsx`).
|
||||
|
||||
Skill detail (`components/specs/SkillDetailPage.tsx`) renders its own
|
||||
two-pane docs browser — a file tree beside a viewer — and draws content
|
||||
with `MarkdownViewer` (`.md`/`.mdx`) or `SyntaxHighlight` (other text). It
|
||||
does not use `FileViewer`, which only the shared drawer renders.
|
||||
|
||||
## 5. Event and activity consumption
|
||||
|
||||
UI event consumption is centralized through one shared `/api/events`
|
||||
connection (`lib/topology-events.ts`) so related surfaces avoid duplicate
|
||||
SSE connections. Current consumers: For You feed hydration, topology
|
||||
activity rings + hot-potato movement, the activity feed/system surfaces,
|
||||
and rig event surfaces. That connection carries stored events only:
|
||||
`view.changed` is delivered on the live view streams (`/api/views/sse`) and
|
||||
is not stored, so it no longer appears in the activity feed, as a For You
|
||||
observation card or in the telemetry export (entries stored by older
|
||||
versions still replay until an events retention, a separate change, removes them). `useGlobalEvents` reports whether that connection
|
||||
is up and refetches review, slice and mission data on reconnect and on
|
||||
`proof.*` events. Reduced-motion preference is honoured for pulse/packet
|
||||
animation. (Daemon SSE surfaces: `/api/events`, `/api/stream/watch`,
|
||||
`/api/queue/watch` —
|
||||
[`../architecture/architecture-rules-and-event-system.md`](../architecture/architecture-rules-and-event-system.md)
|
||||
§2, "Emission and delivery".)
|
||||
|
||||
## See also
|
||||
|
||||
- `topology.md` — graph/table/terminal topology surface.
|
||||
- `project-and-for-you.md` — project observability + For-You feed.
|
||||
- `library-specs-and-design-system.md` — Library/specs + DESIGN.md pointer.
|
||||
- Source roots: `packages/ui/src/routes.tsx`,
|
||||
`packages/ui/src/components/AppShell.tsx`.
|
||||
@@ -1,144 +0,0 @@
|
||||
---
|
||||
kind: as-built
|
||||
title: UI Topology — Graph/Table/Terminal, HotPotato, ActivityRing
|
||||
status: active
|
||||
topics: [observability, coordination]
|
||||
domains: [engineering-advisor, operating-advisor]
|
||||
applies-when: |
|
||||
Need to know how the topology surface is built — the host hybrid graph,
|
||||
the rig/pod graphs, the table/terminal views, the activity-ring /
|
||||
hot-potato visual language, terminal-preview popovers and launchers, and
|
||||
the topology navigation/overlay contracts.
|
||||
siblings: [shell-and-routing.md, project-and-for-you.md]
|
||||
prerequisite-reads: [../README.md, shell-and-routing.md]
|
||||
last-verified-against-source: e8f0ab340db773392ec8be75b072d1c0f3068a50
|
||||
last-updated: 2026-10-08
|
||||
---
|
||||
|
||||
# UI Topology — Graph/Table/Terminal, HotPotato, ActivityRing
|
||||
|
||||
Topology is a scoped workspace with graph, table, and terminal views at the
|
||||
`/topology` route family (`shell-and-routing.md` §3). It is the operator's
|
||||
live picture of host → rig → pod → seat.
|
||||
|
||||
> Verified against source at `e8f0ab34` (package version 0.6.7). The web UI
|
||||
> is in maintenance mode (`docs/reference/developing.md`). The source file
|
||||
> that defines the hybrid nodes is `HybridTopologyNodes.tsx` (DESIGN.md's
|
||||
> implementation reference).
|
||||
|
||||
## 1. Topology pieces
|
||||
|
||||
> `HybridAgentNode` and `HybridPodGroupNode` are exports inside
|
||||
> `packages/ui/src/components/topology/HybridTopologyNodes.tsx`, not
|
||||
> top-level files (`HybridPodGroupNode = memo(...)` at
|
||||
> `HybridTopologyNodes.tsx:101`; `HybridAgentNode = memo(HybridAgentNodeInner,
|
||||
> ...)` at `:321`).
|
||||
|
||||
Components in `packages/ui/src/components/topology/`:
|
||||
|
||||
- `HostMultiRigGraph.tsx` — host-level hybrid React Flow graph (multi-rig
|
||||
single canvas). Also mounted on the `/project` workspace Workflow tab.
|
||||
- `HybridTopologyNodes.tsx` — exports `HybridAgentNode` (compact agent cards:
|
||||
runtime badges, context %, token totals, activity state, terminal preview,
|
||||
per-card CMUX open via `useCmuxLaunch`) and `HybridPodGroupNode` (soft
|
||||
dashed pod frames).
|
||||
- `RigGroupNode.tsx` — soft rig frames with registration marks + aggregate
|
||||
activity.
|
||||
- `ActivityRing.tsx` — active / needs-input / blocked activity ring; card
|
||||
activity classes in `activity-card-visuals.ts`.
|
||||
- `HotPotatoEdge.tsx` — directional queue-movement edge animation.
|
||||
- `TopologyTableView.tsx` — dense table mirror of topology data (with
|
||||
`topology-table-shimmer.css`).
|
||||
- `TopologyTerminalView.tsx` — pinned-card terminal grid for host, rig or
|
||||
pod scope; host scope shows 12 cards by default (`SAFE_N`) with a "show
|
||||
all" toggle.
|
||||
- `TopologyTreeView.tsx` — the topology tree, rendered in the Explorer
|
||||
sidebar (`components/Explorer.tsx`), not as a center view mode.
|
||||
- `TopologyViewModeTabs.tsx` — host tabs Graph / Table / Terminal; rig and
|
||||
pod tabs add Overview. Tabs switch in place (React state), not separate
|
||||
routes. (`SEAT_SCOPE_TABS` is defined but unused; the seat scope renders
|
||||
`LiveNodeDetails`.)
|
||||
- `TerminalPreviewPopover.tsx` — smoked-glass terminal preview, portaled
|
||||
above the graph. On the graph and table it is `progressive`: a static
|
||||
mirror first, click to go live, under a page-wide live-terminal cap.
|
||||
- `TerminalLauncher.tsx` — the rig-scope tab-bar launcher: choose a
|
||||
provider (herdr or cmux) and a view (this rig, a pod, a mission's or
|
||||
slice's agents, a saved view), then open it via
|
||||
`POST /api/terminal/open`. Shown only when the selected host is local.
|
||||
- `LaunchCmuxButton.tsx` — the older rig-scope "Launch in CMUX" button.
|
||||
`TerminalLauncher` replaced it in the tab bar, and no production
|
||||
component imports it now (tests still do).
|
||||
- `ScopePages.tsx` — host/rig/pod/seat scope page wrappers.
|
||||
- `topology-overlay-context.tsx` — `TopologyOverlayProvider`: expanded-rig
|
||||
state, the rig named in the URL auto-expanded, and the Explorer mode
|
||||
(`overlay` on the graph tab, `opaque` on the others).
|
||||
|
||||
The rig and pod graphs are not the hybrid graph: rig and pod scopes render
|
||||
`components/RigGraph.tsx` (`RigNode` cards, `graph-layout.ts` layout),
|
||||
sharing `HotPotatoEdge` and `useTopologyActivity`. Rig scope's Overview tab
|
||||
renders `RigOverviewTab` (`RigSpecDisplay`); pod scope's is a placeholder.
|
||||
On narrow viewports the graph tab falls back to the table.
|
||||
|
||||
> Source: `topology/TopologyViewModeTabs.tsx` (`HOST_SCOPE_TABS`,
|
||||
> `RIG_POD_SCOPE_TABS`, `SEAT_SCOPE_TABS`); `topology/ScopePages.tsx`
|
||||
> (`TerminalLauncher` `:270`, `RigGraph` `:322`/`:437`, `RigOverviewTab`
|
||||
> `:342`, `useOverlayForActiveTab`); `topology/TerminalPreviewPopover.tsx`
|
||||
> (`progressive`); `topology/TopologyTerminalView.tsx` (`SAFE_N` `:35`).
|
||||
|
||||
> Note (vs `DESIGN.md` "Topology"): DESIGN.md lists the exported brand
|
||||
> primitives (`HostMultiRigGraph`, `HybridAgentNode`, `HybridPodGroupNode`,
|
||||
> `RigGroupNode`, `ActivityRing`, `HotPotatoEdge`, `TerminalPreviewPopover`,
|
||||
> `TopologyTableView`, `TopologyTerminalView`); all are present in source.
|
||||
> `TopologyTreeView`, `TopologyViewModeTabs`, `TerminalLauncher` and
|
||||
> `LaunchCmuxButton` exist in source but are not in DESIGN.md's primitive
|
||||
> list (a scope difference, not drift).
|
||||
|
||||
## 2. Topology contracts
|
||||
|
||||
Re-confirmed against source:
|
||||
|
||||
- Graph / table / tree navigation all resolve to seat detail URLs
|
||||
(`/topology/seat/$rigId/$logicalId`).
|
||||
- The host-level graph prefixes node IDs per rig for the multi-rig single
|
||||
canvas (`lib/hybrid-layout.ts`: `PREFIX_DELIMITER = "::"`,
|
||||
`prefixedHybridNodeId`).
|
||||
- Expanded-rig state is owned by `TopologyOverlayProvider`. Rigs default to
|
||||
**expanded** (`DEFAULT_RIG_EXPANDED = true` in `HostMultiRigGraph.tsx`),
|
||||
with "Expand all" / "Collapse all" controls; graph data is fetched for
|
||||
expanded rigs only.
|
||||
- Compact agent cards show context percentage, token totals, and activity
|
||||
card tint; `ActivityRing` + activity classes surface active / needs-input /
|
||||
blocked.
|
||||
- `HotPotatoEdge` animates directional queue movement with non-scaling
|
||||
strokes; reduced-motion preference removes pulse/travel animation and
|
||||
keeps static state signals.
|
||||
- Terminal-preview actions are hover/focus-visible; the
|
||||
`TerminalPreviewPopover` escapes React Flow stacking contexts (portaled
|
||||
to `document.body`) and stays within the viewport.
|
||||
- With a non-local host selected, the host graph fetch carries the host
|
||||
parameter; terminal-preview and CMUX buttons are hidden on cards and in
|
||||
the table; and the rig page shows a read-only marker instead of the
|
||||
launcher.
|
||||
|
||||
## 3. Layout and activity helpers
|
||||
|
||||
In `src/lib/`: `hybrid-layout.ts` lays out the host graph
|
||||
(`HostMultiRigGraph`); `graph-layout.ts` (`applyTreeLayout`) lays out the
|
||||
rig/pod graph (`RigGraph`); `multi-rig-layout.ts` has no production
|
||||
importer (tests only). `topology-activity.ts` computes ring states and
|
||||
hot-potato edges, and `activity-visuals.ts` the rollups and activity dots.
|
||||
Runtime/tool identity on the cards comes from the central
|
||||
`lib/runtime-brand.ts` / `lib/tool-brand.ts` and
|
||||
`components/graphics/RuntimeMark.tsx` (runtimes: Claude Code, Codex, Pi,
|
||||
OMP, terminal, unknown). Do not duplicate brand logic (DESIGN.md "Do
|
||||
not").
|
||||
|
||||
## See also
|
||||
|
||||
- `shell-and-routing.md` — the `/topology` route family and shell.
|
||||
- `project-and-for-you.md` — sibling observability surface.
|
||||
- `../architecture/coordination-primitive.md` — the queue movement the
|
||||
hot-potato edge visualizes.
|
||||
- Source root: `packages/ui/src/components/topology/`,
|
||||
`packages/ui/src/components/RigGraph.tsx`,
|
||||
`packages/ui/src/lib/{hybrid,graph}-layout.ts`.
|
||||
@@ -1,355 +0,0 @@
|
||||
# Handing work between OpenRig and A2A agents
|
||||
|
||||
An OpenRig seat can give a queue row's task to an outside A2A agent. In the other direction, an A2A server can
|
||||
front a seat's queue and return its answer as an artifact. These are compositions of the
|
||||
[official Python SDK](https://github.com/a2aproject/a2a-python) and existing `rig queue` commands, not a
|
||||
`rig a2a` command or a daemon endpoint. The scripts below are examples you run and own.
|
||||
|
||||
OpenRig does not configure the outside agent's permissions, model or credentials. Choose a peer and task content
|
||||
you are authorized to share. The peer's result is its report, not acceptance of the work by your team.
|
||||
|
||||
## Before either recipe
|
||||
|
||||
Use a working OpenRig installation and an existing seat. `rig whoami --json` identifies your seat;
|
||||
`rig ps --nodes --rig <rig>` shows the receiving seat's canonical session name. If you need a team first, use
|
||||
[getting started](getting-started.md). Keep the daemon and provider environment you already use.
|
||||
|
||||
In a scratch folder, make a Python environment (Python 3.10 or newer, with `venv` and `pip` available):
|
||||
|
||||
```sh
|
||||
python3 -m venv .venv
|
||||
.venv/bin/python -m pip install 'a2a-sdk[http-server]==1.1.0' httpx uvicorn
|
||||
```
|
||||
|
||||
The examples pin the SDK version used in the pipe trials; they are not a recommendation to downgrade another
|
||||
application. The official SDK 1.1.0 paths were exercised in both directions. Other 1.x versions were not tested.
|
||||
Recheck the API and cancellation behavior when changing versions.
|
||||
|
||||
Pydantic AI 2.60.0 with FastA2A 2.1.1 did not interoperate with these SDK 1.1.0 recipes in the tested setup:
|
||||
FastA2A's inbound typed client returned `result={}` even though the SDK front, with 0.3 compatibility enabled,
|
||||
sent task, artifact and completed records in its raw SSE stream. Outbound, the official SDK raised a protobuf
|
||||
parse error at the FastA2A task's `status.timestamp`; the full wire timestamp was not retained. When mixing
|
||||
frameworks, compare the raw stream's task IDs, artifact and final status with the typed client's output.
|
||||
An HTTP 200 or an empty typed result is not proof that the task succeeded. Retain the evidence before retrying.
|
||||
|
||||
Save this helper as `queue_io.py`:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
async def rig(*args):
|
||||
child = await asyncio.create_subprocess_exec(
|
||||
'rig', *args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
||||
out, err = await child.communicate()
|
||||
if child.returncode:
|
||||
raise RuntimeError(f'rig {args[:2]} exited {child.returncode}: {err.decode()} {out.decode()}')
|
||||
return json.loads(out)
|
||||
```
|
||||
|
||||
It inherits the caller's OpenRig environment. Run queue operations in the intended seat's environment; set
|
||||
`OPENRIG_URL` only when deliberately selecting another daemon. A timed-out write is uncertain: read its row and
|
||||
transitions before trying again. Do not silently send the task a second time.
|
||||
|
||||
## Outbound: a queue task goes to an A2A peer
|
||||
|
||||
You need the peer's base URL and a row assigned to your seat. For a no-model peer, the official
|
||||
[Hello World sample](https://github.com/a2aproject/a2a-samples/tree/6603ba3f2c31a7ef33e70b9d8b5b5f8be42ac9a3/samples/python/agents/helloworld)
|
||||
returns an echo artifact. Fetch that revision:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/a2aproject/a2a-samples.git
|
||||
git -C a2a-samples checkout --detach 6603ba3f2c31a7ef33e70b9d8b5b5f8be42ac9a3
|
||||
```
|
||||
|
||||
Start the peer yourself in a separate terminal, following `a2a-samples/samples/python/agents/helloworld/README.md`.
|
||||
Its default URL is `http://127.0.0.1:9999`, used below; it is available only while you run the sample.
|
||||
The sample does not implement the input-required and cancellation controls described later. Use a peer that
|
||||
supports those operations when testing them.
|
||||
|
||||
Save as `a2a_client.py`. This client also works without an OpenRig row, for the inbound recipe below.
|
||||
Streaming reads have no deadline so quiet worker waits keep their stream; connection, write and pool waits are bounded to 10 seconds.
|
||||
Stop the local wait with Ctrl-C or a caller-owned overall deadline; this does not confirm cancellation, so keep the task/context IDs and cancel through the peer when it is reachable.
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
import httpx
|
||||
from a2a.client import A2ACardResolver, ClientConfig, create_client
|
||||
from a2a.helpers import new_text_message
|
||||
from a2a.types import Role, SendMessageRequest
|
||||
from google.protobuf.json_format import MessageToDict
|
||||
from queue_io import rig
|
||||
|
||||
async def main():
|
||||
row_id = os.environ.get('A2A_ROW')
|
||||
text = os.environ.get('A2A_TEXT')
|
||||
if row_id:
|
||||
row = await rig('queue', 'show', row_id, '--full', '--json')
|
||||
identity = await rig('whoami', '--json')
|
||||
if row['destinationSession'] != identity['identity']['sessionName']:
|
||||
raise RuntimeError('Choose a row assigned to this seat.')
|
||||
if row['state'] == 'pending':
|
||||
await rig('queue', 'claim', row_id, '--json')
|
||||
row = await rig('queue', 'show', row_id, '--full', '--json')
|
||||
if row['state'] != 'in-progress':
|
||||
raise RuntimeError('The row is not in progress; inspect it before sending.')
|
||||
if text is None:
|
||||
text = row['body']
|
||||
if text is None:
|
||||
raise RuntimeError('Set A2A_TEXT or A2A_ROW.')
|
||||
message = new_text_message(text, role=Role.ROLE_USER)
|
||||
if os.environ.get('A2A_CONTEXT'):
|
||||
message.context_id = os.environ['A2A_CONTEXT']
|
||||
if os.environ.get('A2A_TASK'):
|
||||
message.task_id = os.environ['A2A_TASK']
|
||||
log_path = Path(f'a2a-{uuid.uuid4()}.jsonl').resolve()
|
||||
state, bound = 'UNKNOWN', False
|
||||
with log_path.open('x') as log:
|
||||
async with httpx.AsyncClient() as http:
|
||||
card = await A2ACardResolver(httpx_client=http, base_url=os.environ['A2A_URL']).get_agent_card()
|
||||
log.write(json.dumps({'card': MessageToDict(card), 'message': MessageToDict(message)}) + '\n')
|
||||
log.flush()
|
||||
client = await create_client(agent=card, client_config=ClientConfig(
|
||||
streaming=True, httpx_client=httpx.AsyncClient(timeout=httpx.Timeout(10.0, read=None))))
|
||||
try:
|
||||
async for chunk in client.send_message(SendMessageRequest(message=message)):
|
||||
event = MessageToDict(chunk) # SDK 1.1.0 yields StreamResponse, not a tuple.
|
||||
log.write(json.dumps(event) + '\n')
|
||||
log.flush()
|
||||
print(json.dumps(event), flush=True)
|
||||
task = event.get('task')
|
||||
if task and row_id and not bound:
|
||||
await rig('queue', 'update', row_id, '--note',
|
||||
f"A2A peer={card.name} task={task['id']} context={task['contextId']} log={log_path}", '--json')
|
||||
bound = True
|
||||
status = (task or event.get('statusUpdate') or {}).get('status', {})
|
||||
state = status.get('state', state)
|
||||
finally:
|
||||
await client.close()
|
||||
if row_id:
|
||||
await rig('queue', 'update', row_id, '--note', f'A2A reported {state}; full result/events: {log_path}', '--json')
|
||||
print(f'Reported state: {state}; saved events: {log_path}')
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
When `A2A_TEXT` is unset, the row body becomes the peer's message verbatim. Write the body as the peer's input,
|
||||
or set `A2A_TEXT` to the exact text you want to send. Run it from your seat, replacing the row and peer URL:
|
||||
|
||||
```sh
|
||||
export A2A_URL=http://127.0.0.1:9999
|
||||
export A2A_ROW=<your-row-id>
|
||||
.venv/bin/python a2a_client.py
|
||||
```
|
||||
|
||||
Read the saved `artifactUpdate` parts and final status. Put the useful result on the **same** row, using a note
|
||||
while work continues or a resolution when you deliberately close it. The script leaves the row owned and open:
|
||||
|
||||
```sh
|
||||
rig queue update "$A2A_ROW" --note 'A2A result: <answer, artifact reference, and any limits>' --json
|
||||
# Only when your work on the row is actually finished:
|
||||
rig queue update "$A2A_ROW" --state done --closure-reason no-follow-on --resolution '<your assessed outcome>' --json
|
||||
```
|
||||
|
||||
If the stream fails, retain its exception and log. The peer may still be working; inspect the known task before
|
||||
resending. When the task asks for input, set `A2A_TASK` and `A2A_CONTEXT` to its recorded IDs and `A2A_TEXT` to the
|
||||
answer, then run the client again. For a new follow-up, unset `A2A_TASK` and keep `A2A_CONTEXT`: a completed task
|
||||
is not reopened. Unset `A2A_TEXT` when the next outbound task should again use its row body.
|
||||
|
||||
## Inbound: an A2A client reaches an OpenRig seat
|
||||
|
||||
Run the following server in a custodian seat's environment, with `A2A_SEAT` set to the existing worker seat's
|
||||
canonical session name. The worker is a separate participant: it watches its queue, claims the row, does the work
|
||||
and writes a resolution. A deterministic shell worker is enough to test this path; a model is optional.
|
||||
|
||||
Save as `a2a_front.py` beside `queue_io.py`:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import uvicorn
|
||||
from starlette.applications import Starlette
|
||||
from a2a.helpers import get_message_text, new_task_from_user_message, new_text_message, new_text_part
|
||||
from a2a.server.agent_execution import AgentExecutor
|
||||
from a2a.server.request_handlers import DefaultRequestHandler
|
||||
from a2a.server.routes import create_agent_card_routes, create_jsonrpc_routes
|
||||
from a2a.server.tasks import InMemoryTaskStore, TaskUpdater
|
||||
from a2a.types import AgentCapabilities, AgentCard, AgentInterface, AgentSkill
|
||||
from queue_io import rig
|
||||
|
||||
seat = os.environ['A2A_SEAT']
|
||||
rows, canceling, published = {}, set(), {}
|
||||
|
||||
class Front(AgentExecutor):
|
||||
async def execute(self, context, events):
|
||||
task = context.current_task or new_task_from_user_message(context.message)
|
||||
if not context.current_task:
|
||||
await events.enqueue_event(task)
|
||||
update = TaskUpdater(events, task.id, task.context_id)
|
||||
published[task.id] = asyncio.Event()
|
||||
try:
|
||||
text = get_message_text(context.message)
|
||||
if task.id not in rows:
|
||||
row_id = 'qitem-a2a-' + task.id
|
||||
body = json.dumps({'taskId': task.id, 'contextId': task.context_id, 'request': text})
|
||||
await rig('queue', 'create', '--id', row_id, '--destination', seat, '--body', body,
|
||||
'--summary', 'Inbound A2A task', '--no-nudge', '--full', '--json')
|
||||
rows[task.id] = row_id
|
||||
else:
|
||||
row_id = rows[task.id]
|
||||
await rig('queue', 'update', row_id, '--note', 'A2A answer: ' + text, '--json')
|
||||
working = False
|
||||
while True:
|
||||
row = await rig('queue', 'show', row_id, '--full', '--json')
|
||||
if task.id in canceling:
|
||||
await asyncio.Future() # cancel() owns the terminal event.
|
||||
if row['state'] == 'done':
|
||||
if not row.get('resolution'):
|
||||
await update.failed(new_text_message('Row closed without a result resolution.'))
|
||||
else:
|
||||
await update.add_artifact([new_text_part(row['resolution'])], name='seat-result')
|
||||
await update.complete(new_text_message('Seat closed row ' + row_id))
|
||||
return
|
||||
if row['state'] in ('failed', 'denied', 'handed-off'):
|
||||
await update.failed(new_text_message(row.get('resolution') or ('Row ' + row['state'])))
|
||||
return
|
||||
if row['state'] == 'canceled':
|
||||
await update.cancel(new_text_message('Queue row canceled.'))
|
||||
return
|
||||
if row['state'] == 'in-progress' and not working:
|
||||
await update.start_work(new_text_message('Seat claimed ' + row_id))
|
||||
working = True
|
||||
history = await rig('queue', 'transitions', row_id, '--full', '--json')
|
||||
notes = [t.get('transitionNote') or '' for t in history]
|
||||
conversation = [n for n in notes if n.startswith(('A2A question: ', 'A2A answer: '))]
|
||||
if conversation and conversation[-1].startswith('A2A question: '):
|
||||
await update.requires_input(new_text_message(conversation[-1][14:]))
|
||||
return
|
||||
await asyncio.sleep(1)
|
||||
except asyncio.CancelledError:
|
||||
if task.id in canceling:
|
||||
await published[task.id].wait() # See SDK 1.1.0 cancellation note below.
|
||||
raise
|
||||
except Exception as error:
|
||||
await update.failed(new_text_message(str(error)))
|
||||
|
||||
async def cancel(self, context, events):
|
||||
canceling.add(context.task_id)
|
||||
try:
|
||||
row_id = rows[context.task_id]
|
||||
await rig('queue', 'update', row_id, '--state', 'canceled', '--resolution',
|
||||
'A2A client requested cancellation; worker stop not yet confirmed.', '--json')
|
||||
await TaskUpdater(events, context.task_id, context.context_id).cancel(
|
||||
new_text_message('Queue cancellation committed; worker stop is a separate observation.'))
|
||||
finally:
|
||||
published[context.task_id].set()
|
||||
|
||||
card = AgentCard(name='OpenRig queue: ' + seat, version='example-1',
|
||||
description='A worker seat returns its queue resolution.',
|
||||
default_input_modes=['text/plain'], default_output_modes=['text/plain'],
|
||||
capabilities=AgentCapabilities(streaming=True),
|
||||
supported_interfaces=[AgentInterface(protocol_binding='JSONRPC', url='http://127.0.0.1:19092', protocol_version='1.0')],
|
||||
skills=[AgentSkill(id='queue-work', name='Queue work', description='Work assigned to ' + seat, tags=['queue'])])
|
||||
handler = DefaultRequestHandler(agent_executor=Front(), task_store=InMemoryTaskStore(), agent_card=card)
|
||||
app = Starlette(routes=[*create_agent_card_routes(card), *create_jsonrpc_routes(handler, '/')])
|
||||
uvicorn.run(app, host='127.0.0.1', port=19092)
|
||||
```
|
||||
|
||||
Start the front, then use a second terminal for the outside client:
|
||||
|
||||
```sh
|
||||
# Custodian terminal:
|
||||
export A2A_SEAT=<worker-session-from-rig-ps>
|
||||
.venv/bin/python a2a_front.py
|
||||
|
||||
# Client terminal, in the folder with the example scripts and venv:
|
||||
unset A2A_ROW A2A_TASK A2A_CONTEXT
|
||||
export A2A_URL=http://127.0.0.1:19092
|
||||
export A2A_TEXT='Return exactly these words: "hello from the receiving seat".'
|
||||
.venv/bin/python a2a_client.py
|
||||
```
|
||||
|
||||
The client waits while the worker does its part. In the **worker's own environment**, inspect and claim the row:
|
||||
|
||||
```sh
|
||||
rig queue list --owned --full --json
|
||||
export ROW=<the-new-qitem-a2a-id>
|
||||
rig queue show "$ROW" --full --json
|
||||
rig queue claim "$ROW" --json
|
||||
rig queue update "$ROW" --state done --closure-reason no-follow-on --resolution 'hello from the receiving seat' --json
|
||||
```
|
||||
|
||||
The client should receive that exact resolution as an artifact, followed by completed. Compare the artifact with
|
||||
`rig queue show "$ROW" --full --json`, including the task/context IDs in the row body. The sample uses `--no-nudge`:
|
||||
the worker must be watching its queue; merely starting the front does not start or wake an agent.
|
||||
To wake a model worker after the row appears, run `rig send <worker-session> 'Check your queue and process the new A2A task.'`
|
||||
from the custodian's environment, or remove `--no-nudge` from the front's queue-create call to request the normal queue wake.
|
||||
|
||||
For input-required, the worker instead records `--note 'A2A question: <question>'` and leaves the row in progress.
|
||||
The client reruns with the same `A2A_TASK`, `A2A_CONTEXT` and its answer as `A2A_TEXT`. The worker reads the
|
||||
`A2A answer: ` transition with `rig queue transitions "$ROW" --full --json`, then finishes that row. These prefixes
|
||||
are this example's small convention, not built-in OpenRig fields. A new follow-up omits the completed task ID and
|
||||
keeps the context ID; it produces a new row.
|
||||
|
||||
## Cancel and read the outcome honestly
|
||||
|
||||
For a protocol-1.0 JSON-RPC peer, a direct control request includes `A2A-Version: 1.0`. For example, after the task
|
||||
is bound and working (substitute its ID; use `GetTask` with the same parameters to inspect it):
|
||||
|
||||
```sh
|
||||
curl -sS "$A2A_URL" -H 'Content-Type: application/json' -H 'A2A-Version: 1.0' \
|
||||
--data '{"jsonrpc":"2.0","id":"cancel-1","method":"CancelTask","params":{"id":"<task-id>"}}'
|
||||
```
|
||||
|
||||
Read the JSON body and task state, not just HTTP status. A pipe trial received HTTP 200 carrying JSON-RPC
|
||||
`-32009 VERSION_NOT_SUPPORTED` when the version header was omitted. That was an error, not a completed task.
|
||||
The SDK handles versioned calls; raw requests must follow the peer's advertised interface and
|
||||
[protocol version rules](https://a2a-protocol.org/v1.0.0/specification/).
|
||||
|
||||
SDK 1.1.0's default V2 handler cancels the running producer **before** awaiting the executor's `cancel` callback.
|
||||
If that callback awaits a queue write, the producer can close the event channel first. The inbound trial then
|
||||
returned stale working state even though the row was canceled. The event in the example keeps that producer
|
||||
alive until the callback publishes its outcome. This is specific to the exercised ordering; see the
|
||||
[pinned SDK implementation](https://github.com/a2aproject/a2a-python/blob/v1.1.0/src/a2a/server/agent_execution/active_task.py).
|
||||
Test it again when changing SDK versions, including an awaited external write rather than an immediate callback.
|
||||
|
||||
Keep these facts separate:
|
||||
|
||||
| A2A observation | What it means for the queue or worker |
|
||||
|---|---|
|
||||
| Submitted | The A2A front accepted a task; a row may not yet be created. |
|
||||
| Working | This front observed the receiving seat's claim. |
|
||||
| Input-required | A question needs answering; this example leaves the row in-progress. |
|
||||
| Completed + artifact | The worker's stored result was returned; your assessment still belongs to you. |
|
||||
| Canceled | This inbound front committed queue cancellation; an outside peer's cancellation says nothing about your outbound row. Verify separately that the worker noticed and stopped. |
|
||||
| Failed / JSON-RPC error | Retain the actual error and any row effect; do not report success. |
|
||||
| Disconnect / timeout | The last known state remains all you know. Inspect before retrying. |
|
||||
|
||||
There is no one-to-one A2A/queue state mapping. The example does not follow handoffs to another row, so it reports
|
||||
that limit instead of pretending the successor's work completed. Stopping the front only detaches clients; it does
|
||||
not cancel its queue rows or stop their workers. Cancel or hand off outstanding work deliberately before stopping
|
||||
your server. Acknowledged cancellation does not establish that a model provider stopped spending.
|
||||
|
||||
## What was exercised, and what remains open
|
||||
|
||||
The initial trials used an OpenRig 0.7.0 candidate and SDK 1.1.0 on Linux. Outbound discovery, streamed output and
|
||||
artifacts used the official Hello World sample. Deterministic peers covered input-required, cancellation, failure
|
||||
and concurrency. Inbound used a private daemon and a separate no-model queue worker: five artifacts matched the
|
||||
stored resolutions, two tasks overlapped without crossing IDs, and completed-task reuse left the old result
|
||||
unchanged. The successful cancellation check also observed the worker stop, beyond receiving the protocol reply.
|
||||
|
||||
A later inbound check used an official SDK 1.1.0 client and front, an OpenRig queue row, and Pi 1.4.0 running
|
||||
`openai/gpt-4o-mini`. One marker answer reached the row's resolution and the client's completed artifact. A
|
||||
custodian performed the queue writes; model tools were disabled. That run used a 120-second streaming read
|
||||
timeout and prompt RPC event draining, changed together. It did not separately test the unbounded read used above.
|
||||
|
||||
These results do not establish model quality or provider billing. Other framework versions, model tool use,
|
||||
public deployment and authentication, stream reconnect, durable bridge restart, and cancellation before row
|
||||
binding were not exercised. The inbound example holds A2A tasks and their row mapping in memory: keep it running for the conversation;
|
||||
after a crash, inspect retained OpenRig rows before any replacement run. It binds loopback and supplies no public
|
||||
authentication layer. Use it on your own machine or a trusted private path, not as an unauthenticated public gateway.
|
||||
@@ -1,251 +0,0 @@
|
||||
# Handing work to an ACP agent
|
||||
|
||||
`rig acp run` gives one queue row's task to an agent that speaks the
|
||||
[Agent Client Protocol](https://agentclientprotocol.com) (ACP), such as OpenClaw's `openclaw acp`, and records what
|
||||
happened on that same row. The seat that runs it is the row's custodian; the agent is recorded as the executor.
|
||||
|
||||
```sh
|
||||
rig acp run <qitemId> --cwd <work folder> -- <the agent's ACP command> [args...]
|
||||
```
|
||||
|
||||
The row's notes get, in order:
|
||||
- **the binding:** the agent's name and version, the ACP protocol version, whether it can load sessions, and the session;
|
||||
- **progress,** about once a minute (tool calls so far, output so far); the full transcript is in the run's log file, named on the row;
|
||||
- **each permission request and its answer** (below);
|
||||
- **the outcome:** `completed`, `stopped` (for example `max_tokens`), `cancelled`, `failed` or `unknown`, with the
|
||||
agent's output and the tokens and cost it reported, each labelled with where it came from (unknown when it reported
|
||||
none);
|
||||
- **what happened to the agent's processes** after the run ended.
|
||||
|
||||
The outcome is the agent's report, not a judgment of the work. The row stays open and yours: read the result, then
|
||||
close it or hand it on with your own resolution, as you would any other.
|
||||
|
||||
## When the agent asks permission
|
||||
|
||||
OpenRig holds no permission policy of its own. The agent's own settings decide what it asks, and an owner decides
|
||||
the answer. Each request becomes a queue row to `--permissions-to` (this seat by default), listing the action, its
|
||||
target, the options the agent offered and the run, session and request it belongs to. Answer on that row with the
|
||||
option's id:
|
||||
|
||||
```sh
|
||||
rig queue update <permission row> --state done --closure-reason no-follow-on --resolution allow-once
|
||||
```
|
||||
|
||||
Answer on the permission row itself; don't hand it off, because the run waits on that row. The answer goes back to
|
||||
that one request, once. The turn waits for it; there's no deadline. An answer that isn't one
|
||||
of the offered options is noted and the question is asked again. If the turn is cancelled first, the permission row
|
||||
is closed as stale and nothing is sent. Whoever answers can follow their own written policy; that choice is theirs.
|
||||
|
||||
## Cancel, detach and stop
|
||||
|
||||
These are different things, and the row keeps them apart:
|
||||
- **Cancel** (Ctrl-C, or `rig acp cancel <runId>` from another terminal) asks the agent to stop its turn. The row
|
||||
records when it was requested and when the agent acknowledged it. Whether the model provider stopped work, and what
|
||||
it cost, stays **unknown** unless you check the provider's own usage.
|
||||
- **Detach** (a second Ctrl-C, or the run ending normally) closes the agent's connection. That doesn't stop
|
||||
the agent's runtime.
|
||||
- **Stop:** when the run ends, it reports whether any process carrying the run's environment marker remains:
|
||||
`settled` (none), `incomplete` (with their process ids) or `unknown` (the process table couldn't be read). Only
|
||||
processes that inherited the marker are seen, so one the agent started with a cleared environment, or through a
|
||||
service manager, isn't. It never touches a process it didn't launch. To stop what's left, use the runtime's
|
||||
own stop command, through `rig acp stop <runId> -- <that command>` so the result lands on the row.
|
||||
|
||||
A gateway the agent connects to but didn't start, such as OpenClaw's Gateway, isn't a process of the run. Leave a
|
||||
shared one running; stop your own with its own command.
|
||||
|
||||
## Reading the outcome
|
||||
|
||||
- `completed`: the agent ended its turn with output.
|
||||
- `unknown`: the turn ended, but nothing shows the work happened. That covers an `end_turn` with no output and no or
|
||||
zero usage (an agent can end a turn this way after its provider refused the request), an `end_turn` whose text
|
||||
reports an interruption (OpenClaw's `[OpenClaw interruption]`), and a connection that closed before the turn ended.
|
||||
Check the agent's own log before you treat it as done.
|
||||
- `failed`: the agent returned an error. The row keeps its original code, message and data.
|
||||
- `cancelled`, `stopped`: as above.
|
||||
|
||||
## Claude Code and Codex, through their ACP adapters
|
||||
|
||||
Both have maintained ACP adapters. What follows was checked on 2026-11-06 with OpenRig 0.7.8: one small file task per
|
||||
case, with `claude-haiku-5-5` and with `gpt-6.1-sol` at low effort. Other models weren't checked.
|
||||
|
||||
Install the adapters once; running them through `npx` adds about 10 seconds a turn:
|
||||
|
||||
```sh
|
||||
npm install --prefix <dir> @agentclientprotocol/claude-agent-acp@0.101.0 @agentclientprotocol/codex-acp@2.5.0
|
||||
export PATH="<dir>/node_modules/.bin:$PATH"
|
||||
|
||||
rig acp run <row> --cwd <work folder> -- env CLAUDE_CODE_EXECUTABLE=<your claude> ANTHROPIC_MODEL=<model> claude-agent-acp
|
||||
rig acp run <row> --cwd <work folder> -- codex-acp
|
||||
```
|
||||
|
||||
**Which versions run.** The binding note on the row names the adapter and its version.
|
||||
- `claude-agent-acp` 0.101.0 runs Claude Code through the Claude Agent SDK 0.3.318. With `CLAUDE_CODE_EXECUTABLE` set,
|
||||
it runs that `claude`, with that `claude`'s own login; the check ran Claude Code 2.1.310 this way. Without it, an npm
|
||||
install runs the Claude binary from the SDK's platform package (Claude Code 2.1.318, by the SDK's package metadata),
|
||||
which wasn't checked. Installing with `--omit=optional` leaves that binary out, and then, without
|
||||
`CLAUDE_CODE_EXECUTABLE`, the adapter refuses to start; it never falls back to a `claude` on PATH. The adapter also
|
||||
loads your Claude configuration, skills included.
|
||||
- `codex-acp` 2.5.0 depends on `@openai/codex` `^0.165.0`, and 0.165.0 was the only release in that range when this
|
||||
was checked. A later install can resolve a newer 0.165 release; `npm ls @openai/codex` in `<dir>` shows yours. It uses
|
||||
the machine's existing Codex login.
|
||||
- `@zed-industries/claude-code-acp` and `@zed-industries/codex-acp` are deprecated on npm in favour of these. The zed
|
||||
`codex-acp` 0.16.0's Codex was too old for the model in the check.
|
||||
|
||||
**Permissions.** Each agent keeps its own permission mode, and OpenRig adds no restriction. In the check, neither
|
||||
asked anything for file writes or shell commands: Claude started in `auto` mode and Codex in `workspace-write`. To
|
||||
have the agent ask, so that each request becomes a permission row:
|
||||
- **Claude:** give it a dedicated `CLAUDE_CONFIG_DIR` whose settings set `permissions.defaultMode` to `"default"`.
|
||||
- **Codex:** set `INITIAL_AGENT_MODE=read-only` in its environment. Approval and sandbox keys in `CODEX_CONFIG` didn't
|
||||
make it ask.
|
||||
|
||||
These are the settings the check used to make the agents ask. Whether to use them is the caller's choice; they're not a
|
||||
recommended default. Send permission rows to a seat that can be woken: a row to a `terminal` seat sets off the stuck
|
||||
sweep.
|
||||
|
||||
**Answers differ between the two.**
|
||||
- **Claude** offers `allow-once`, `allow-with-updates` (allow always) and `reject`. `reject` refuses that one action,
|
||||
and the turn carries on.
|
||||
- **Codex** offers `allow_once`, `accept_execpolicy_amendment` (allow always) and `cancel`. Its only refusal, `cancel`,
|
||||
ends the whole turn, and the outcome is `cancelled`.
|
||||
|
||||
**Continuing, and a session that doesn't exist.**
|
||||
- **Continuing:** `--load-session` continued an earlier session on a second row with both adapters.
|
||||
- **A made-up session id:** both refused it and started nothing, but they report it differently. Claude returns
|
||||
`-32002 Resource not found`, and Codex returns `-32603 Internal error` with "no rollout found" in its data. The row
|
||||
shows `failed` with the agent's own error. Neither code by itself means a missing session, so read the message.
|
||||
|
||||
**A completed turn isn't finished work.** In the check, Claude was asked to run a command and wait for it. It moved the
|
||||
command into a background task, said it would report when it finished, and ended the turn `completed`. The task died
|
||||
with the adapter, and its file never appeared. Check the work itself before you close the row. Beyond this one case,
|
||||
long and background work weren't checked.
|
||||
|
||||
**Cancel.** `rig acp cancel` was acknowledged in 20-30 ms for both, sent while the agent waited on a permission
|
||||
answer; a cancel mid-generation or mid-tool wasn't timed. Straight after, Claude's run reported `incomplete`
|
||||
with one process still running, which was gone a few seconds later; Codex's reported `settled`. An acknowledgement
|
||||
isn't a finished shutdown, and it says nothing about further spend.
|
||||
|
||||
**Usage and cost.** Claude's adapter reports token usage and a cost in USD; Codex's reports tokens only. Both are in
|
||||
the run's log file, not on the row.
|
||||
|
||||
**Not checked:**
|
||||
- long or multi-tool tasks, and background work;
|
||||
- concurrent runs;
|
||||
- `rig acp stop` with a real stop command;
|
||||
- an adapter that crashes mid-turn;
|
||||
- continuing after a daemon restart;
|
||||
- MCP servers.
|
||||
|
||||
## Notes for specific agents
|
||||
|
||||
- **OpenClaw:** `openclaw acp` is a bridge to a running OpenClaw Gateway, so start the Gateway first, then run, for
|
||||
example, `rig acp run <row> -- openclaw acp --url ws://127.0.0.1:18789 --token-file <file>`. Its permission requests
|
||||
come from the Gateway's exec approvals.
|
||||
- **Continuing a session:** `--load-session <id>` continues an earlier session when the agent advertises loading. If it
|
||||
doesn't, or the load fails, the run reports that and starts nothing new.
|
||||
|
||||
## Running it in the background: `rig exec`
|
||||
|
||||
`rig exec` runs the same single turn on a named harness, `claude` or `codex`, through that harness's ACP adapter, in a
|
||||
connector that outlives the command:
|
||||
|
||||
```sh
|
||||
rig exec harnesses # the adapter and native versions installed here, and how each differs
|
||||
rig exec run <row> --harness claude --plan # what would run, with versions; starts nothing
|
||||
rig exec run <row> --harness claude [--wait] # start it; --wait waits here for the outcome
|
||||
rig exec status <runId|row> # state, outcome, usage and cost, and what still runs
|
||||
rig exec cancel <runId>
|
||||
```
|
||||
|
||||
- **The connector owns the run.** Closing the terminal, or leaving `--wait` with Ctrl-C, doesn't stop it. Its record
|
||||
in `$OPENRIG_HOME/acp/runs/` holds its process and its state: `requested`, `started`, `finished` or `interrupted`.
|
||||
The row gets the same notes as with `rig acp run`.
|
||||
- **One run per row.** Running it again returns the row's run, still going or ended, instead of sending the task
|
||||
twice. `--again` starts another turn once that run has ended.
|
||||
- **Interrupted.** A run whose connector died without recording an outcome reads `interrupted`, outcome unknown, with
|
||||
one note on the row, the next time `status`, `run` or `--wait` looks at it. Whether any of the agent's processes
|
||||
remain is said there too.
|
||||
- **Missing.** With `--load-session`, a session reads `missing` only on the adapter's own evidence: for Claude,
|
||||
`-32002` naming that session; for Codex, its "no rollout found for thread id" error for that session. Any other error
|
||||
reads `failed`, with the agent's code and message, and no new session is started either way.
|
||||
- **The outcome is the agent's report.** `finished` with `completed` means the turn ended; accepting the work stays
|
||||
with you, on the row. `--wait` exits 0 for `completed`, 2 for any other outcome, and 3 when you stop waiting.
|
||||
Without `--wait`, a run that has already ended exits the same way, and one still going exits 0. Either way the code
|
||||
follows the row's run, including one another caller started.
|
||||
- **Permissions:** the agent keeps its own mode, and `rig exec` adds no policy; your environment passes through.
|
||||
`rig exec harnesses` lists what each harness reads.
|
||||
|
||||
`rig acp run` is unchanged: it stays in your terminal for the length of the turn.
|
||||
|
||||
## Limits
|
||||
|
||||
- One run is one turn for one row.
|
||||
- The client offers the agent no file-system or terminal methods; the agent works in its own environment.
|
||||
- Spend is whatever the agent's provider charges. OpenRig doesn't measure it; it records only what the agent reports.
|
||||
|
||||
## OpenRig as an agent in ACP apps
|
||||
|
||||
The other direction: `rig acp serve`. An app that speaks ACP and can launch a custom agent command can offer your OpenRig team as its agent. The app starts
|
||||
`rig acp serve` and talks to it on stdin and stdout:
|
||||
|
||||
```sh
|
||||
rig acp serve --to <member@rig>
|
||||
```
|
||||
|
||||
Without `--to`, the receiver is this machine's `acp.to` setting: `rig config set acp.to <member@rig>`, set once. An
|
||||
app that starts the agent from the ACP registry passes no arguments, so that's how it reaches your team.
|
||||
|
||||
- **A prompt** becomes a queue row from you to `--to`, the seat that takes your team's new work. The row's body is your
|
||||
prompt, plus a line saying which app sent it and that its closing resolution is your answer.
|
||||
- **Progress:** each row the work reaches shows in the app as a step (`Sent to …`, then `Handed to …` at each handoff),
|
||||
with the row's notes as they're written, each with the seat that wrote it. The step's title also shows its latest
|
||||
note, for apps that show steps collapsed.
|
||||
- **The reply** is the closing resolution, with who closed the row and how (`done`, `done (canceled)`, `denied`, …). The
|
||||
turn ending says only that the turn ended: read the reply for how the work ended.
|
||||
- **A follow-up** in the same session goes to `--to` as a new row whose body names the earlier rows and their states,
|
||||
and the app first shows any earlier row that's still open.
|
||||
- **A question the team asks you** while a turn is open comes to the app with the ask's own choices, when its row is
|
||||
tagged for that chat (`acp-session:<id>`; the task's row tells the team how). An app that takes forms (it
|
||||
advertises ACP elicitation's form mode, as T3 Code does) gets a one-choice form with each choice by its label; any
|
||||
other app gets a permission request with one option per choice, as Zed shows them. An untagged
|
||||
question from a seat the work reached is named in the chat with where to answer it, because it could be another
|
||||
chat's. Your choice is recorded as your answer, as a click in Slack would be, and the seat that asked gets it.
|
||||
Dismissing it, or cancelling the turn, records nothing: the question stays open in the TUI and Slack, and no further
|
||||
question opens after a cancel. A question that ends unanswered here (dismissed, cancelled, closed with the work, or
|
||||
one the app couldn't show) is named in the chat as still open, as well as on its card. A question that wants an
|
||||
answer in words is shown with where to answer it.
|
||||
- **Sessions are kept in the queue:** the app can list your earlier sessions (`session/list`) and reopen one
|
||||
(`session/load`). Reopening replays each prompt, its work, the questions the team asked you during it with your
|
||||
answers, and the reply, as they stand now. Nothing is asked again on reopening: a question still open comes up in
|
||||
your next prompt's turn, and a new prompt continues the session.
|
||||
- **Cancel** stops `rig acp serve` waiting. The team's work carries on, and the reply says so.
|
||||
- **Work handed to a seat on another machine** can't be followed from here; the reply says where it went.
|
||||
|
||||
**Who you are:** `--as <address>`, else `OPENRIG_SESSION_NAME`, else this machine's default person: the only
|
||||
registered person, or the one chosen with `rig gateway default <id>` when there are several (the address is
|
||||
`<id>@external`). The app's sign-in step, "OpenRig on this machine", checks
|
||||
that the daemon answers and that you resolve to an address; until then, signing in and new sessions fail with the
|
||||
reason and the fix. The agent talks to the same daemon as `rig` on this computer: the local one, so start it first
|
||||
(`rig daemon start`), unless `OPENRIG_URL` names another.
|
||||
The team's questions reach you only as a registered person: an `<id>@external` address needs
|
||||
`rig gateway human add <id> --display-name "<your name>" --delivery-class <A|B|C|D>` first. Until then, the app says
|
||||
so at your first prompt, and your work is still sent.
|
||||
|
||||
**In Zed**, add the agent to `settings.json`:
|
||||
|
||||
```json
|
||||
"agent_servers": {
|
||||
"OpenRig": { "type": "custom", "command": "rig", "args": ["acp", "serve", "--to", "lead@my-team"] }
|
||||
}
|
||||
```
|
||||
|
||||
Other apps take the same command and arguments wherever they configure a custom ACP agent.
|
||||
|
||||
**Limits:**
|
||||
- One receiver per agent command (or the `acp.to` setting); the app has no team picker.
|
||||
- Questions show only while a turn is open; one asked while no turn is open waits in your usual places (the TUI, Slack).
|
||||
- An app that shows a permission request's options only as fixed buttons, such as Approve and Decline, and doesn't
|
||||
take forms, can't show the choices: answer those questions in the TUI or Slack.
|
||||
- Prompts are text. Links and embedded text files are passed on as written; images and audio aren't accepted.
|
||||
- Progress is read every 2 seconds while a turn is open (`--poll-seconds`).
|
||||
- Sessions are found among the 2,000 newest rows you've sent, and a reopened session's questions among the 2,000 newest
|
||||
sent to you, so a caller with a lot of traffic, such as a busy seat, can't list or fully reopen its oldest sessions.
|
||||
+69
-258
@@ -1,8 +1,8 @@
|
||||
# AgentSpec Reference
|
||||
|
||||
Version: 1.0
|
||||
Last validated against code: 2026-10-07, at the 0.6.6 cut `2620dea8` (whole document at `fcaf1f8e`; changes to its sources and the shipped agent specs since then checked)
|
||||
Source of truth: `packages/daemon/src/domain/agent-manifest.ts`, `packages/daemon/src/domain/types.ts`, `packages/daemon/src/domain/agent-resolver.ts`, `packages/daemon/src/domain/profile-resolver.ts`, `packages/daemon/src/domain/skill-catalog.ts`
|
||||
Last validated against code: 2026-04-11
|
||||
Source of truth: `packages/daemon/src/domain/agent-manifest.ts`, `packages/daemon/src/domain/types.ts`
|
||||
|
||||
This is the canonical reference for the AgentSpec YAML format (`agent.yaml`). Every field, validation rule, and default documented here was traced from the actual parser and validator code.
|
||||
|
||||
@@ -20,7 +20,7 @@ profiles:
|
||||
skills: []
|
||||
guidance: []
|
||||
subagents: []
|
||||
plugins: []
|
||||
hooks: []
|
||||
runtime_resources: []
|
||||
|
||||
resources: {}
|
||||
@@ -35,7 +35,7 @@ startup:
|
||||
```yaml
|
||||
name: implementer
|
||||
version: "1.0"
|
||||
description: Implementation agent — implements and verifies coherent changes
|
||||
description: Implementation agent — writes code following TDD discipline
|
||||
|
||||
defaults:
|
||||
runtime: claude-code
|
||||
@@ -46,11 +46,11 @@ imports:
|
||||
profiles:
|
||||
default:
|
||||
uses:
|
||||
skills: [development-team, systematic-debugging, verification-before-completion]
|
||||
skills: [using-superpowers, openrig-user, development-team, test-driven-development, systematic-debugging]
|
||||
guidance: []
|
||||
subagents: []
|
||||
plugins: [shared:openrig-core]
|
||||
runtime_resources: [shared:claude-default-settings, shared:claude-default-mcp, shared:codex-default-config, shared:claude-activity-hooks]
|
||||
hooks: []
|
||||
runtime_resources: []
|
||||
|
||||
resources:
|
||||
guidance:
|
||||
@@ -75,10 +75,9 @@ description: Vault specialist agent — manages HashiCorp Vault for this managed
|
||||
defaults:
|
||||
runtime: claude-code
|
||||
model: claude-opus-4-6
|
||||
effort: high
|
||||
lifecycle:
|
||||
execution_mode: interactive_resident
|
||||
compaction_strategy: default-compaction
|
||||
compaction_strategy: harness_native
|
||||
restore_policy: resume_if_possible
|
||||
|
||||
imports:
|
||||
@@ -92,11 +91,11 @@ profiles:
|
||||
preferences:
|
||||
runtime: claude-code
|
||||
uses:
|
||||
skills: [systematic-debugging, vault-user]
|
||||
skills: [openrig-user, systematic-debugging, vault-user]
|
||||
guidance: []
|
||||
subagents: []
|
||||
plugins: [shared:openrig-core, vault-tools]
|
||||
runtime_resources: [claude-settings]
|
||||
hooks: []
|
||||
runtime_resources: []
|
||||
startup:
|
||||
files:
|
||||
- path: guidance/profile-specific.md
|
||||
@@ -112,11 +111,10 @@ resources:
|
||||
guidance:
|
||||
- id: role
|
||||
path: guidance/role.md
|
||||
plugins:
|
||||
- id: vault-tools
|
||||
source:
|
||||
kind: local
|
||||
path: plugins/vault-tools
|
||||
hooks:
|
||||
- id: pre-commit
|
||||
path: hooks/pre-commit.sh
|
||||
runtimes: [claude-code]
|
||||
runtime_resources:
|
||||
- id: claude-settings
|
||||
path: runtime/claude-settings.fragment.json
|
||||
@@ -149,12 +147,12 @@ startup:
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `name` | string | yes | — | Agent name. Used in spec library identification and validation messages. |
|
||||
| `version` | string | yes | — | Spec version. Quote it (`"1.0"`): an unquoted number fails as a non-string. An importing spec that pins `imports[].version` must match it exactly. |
|
||||
| `version` | string | yes | — | Spec version. Informational — not used for compatibility gating. |
|
||||
| `description` | string | no | — | Human-readable description. Shown in spec library and review surfaces. |
|
||||
| `defaults` | Defaults | no | — | Default runtime, model, and lifecycle settings. Applied when not overridden by the rig spec or profile. |
|
||||
| `imports` | Import[] | no | `[]` | Other AgentSpecs to import. Resources from imported specs become available for profile `uses` references. |
|
||||
| `profiles` | map<string, Profile> | no | `{}` | Named profiles. Each profile selects resources and can override startup/lifecycle. The rig spec member's `profile` field selects which profile to use. |
|
||||
| `resources` | Resources | no | all empty | Declared resources (skills, guidance, subagents, plugins, runtime resources). These are the available pool that profiles select from via `uses`. |
|
||||
| `resources` | Resources | no | all empty | Declared resources (skills, guidance, subagents, hooks, runtime resources). These are the available pool that profiles select from via `uses`. |
|
||||
| `startup` | StartupBlock | no | `{ files: [], actions: [] }` | Agent-level startup files and actions. Applied to all profiles via the startup layering model. |
|
||||
|
||||
---
|
||||
@@ -165,19 +163,16 @@ startup:
|
||||
defaults:
|
||||
runtime: claude-code
|
||||
model: claude-opus-4-6
|
||||
effort: high
|
||||
lifecycle:
|
||||
execution_mode: interactive_resident
|
||||
compaction_strategy: default-compaction
|
||||
compaction_strategy: harness_native
|
||||
restore_policy: resume_if_possible
|
||||
```
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `runtime` | string | no | — | Default runtime for this agent. Precedence: the rig spec member's `runtime`, then the profile's `preferences.runtime`, then this, then `claude-code`. |
|
||||
| `model` | string | no | — | Default model, with the same precedence. |
|
||||
| `effort` | string | no | — | Default reasoning effort, with the same precedence. Claude gets `--effort`, Codex `-c model_reasoning_effort=…`, Pi `--thinking <level>` (Pi's levels: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`; passed as written). A blank or non-string value is ignored with an advisory. |
|
||||
| `advisor` | string | no | — | Default Claude Advisor model, with the same precedence. Passed as the session-scoped `--advisor` flag, independently of the primary model; never written to shared Claude settings. Blank or non-string values are ignored with an advisory. Other runtimes ignore it. |
|
||||
| `runtime` | string | no | — | Default runtime for this agent. Can be overridden by the rig spec member's `runtime` field. |
|
||||
| `model` | string | no | — | Default model. Can be overridden by the rig spec member's `model` field. |
|
||||
| `lifecycle` | Lifecycle | no | see below | Lifecycle behavior defaults. |
|
||||
|
||||
### Lifecycle Defaults
|
||||
@@ -185,9 +180,8 @@ defaults:
|
||||
| Field | Type | Default | Allowed Values |
|
||||
|-------|------|---------|----------------|
|
||||
| `execution_mode` | string | `interactive_resident` | `interactive_resident` (only value in v1; `wake_on_demand` is explicitly rejected) |
|
||||
| `compaction_strategy` | string | `default-compaction` | `default-compaction`, `managed-compaction`, `handover`, `apprentice-handover`; deprecated aliases accepted with a validation advisory: `harness_native` → `default-compaction`, `pod_continuity` → `handover` (`custom_prompt` is explicitly rejected in v1). `managed-compaction` and `apprentice-handover` create their jobs only for `claude-code` seats; see [Codex compaction and sizing](agent-startup-guide.md#codex-compaction-and-sizing) |
|
||||
| `compaction_strategy` | string | `harness_native` | `harness_native`, `pod_continuity` (`custom_prompt` is explicitly rejected in v1) |
|
||||
| `restore_policy` | string | `resume_if_possible` | `resume_if_possible`, `relaunch_fresh`, `checkpoint_only` |
|
||||
| `mechanic` | string | — | A canonical `seat@rig` session address; needed by `apprentice-handover` |
|
||||
|
||||
---
|
||||
|
||||
@@ -203,18 +197,15 @@ imports:
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `ref` | string | yes | Reference to another AgentSpec directory. Must start with `local:` (relative) or `path:` (absolute). The referenced directory must contain an `agent.yaml`. |
|
||||
| `version` | string | no | Optional version constraint. Must be an exact version — no ranges (`~`, `^`, `>=`, etc.). When set, the imported spec's `version` must equal it, or resolution fails with `version_mismatch`. |
|
||||
| `version` | string | no | Optional version constraint. Must be an exact version — no ranges (`~`, `^`, `>=`, etc.). |
|
||||
|
||||
### Import Resolution
|
||||
|
||||
- `local:` paths resolve relative to the importing spec's directory
|
||||
- `path:` paths are absolute filesystem paths
|
||||
- Imported resources become available for `uses` references in profiles
|
||||
- A qualified reference `namespace:id` (e.g., `shared:openrig-core`) names the imported spec by its `name` field
|
||||
- An unqualified reference (just `id`) resolves against the spec's own resources first, then against the one import that
|
||||
declares it. If two imports declare it, the reference is ambiguous and fails: qualify it
|
||||
- An imported spec may not have imports of its own, and its `name` may not contain `:`. Two imports that resolve to the
|
||||
same spec name are refused, and import cycles are detected
|
||||
- When referencing imported resources in `uses`, use qualified `namespace:id` format (e.g., `shared:openrig-user`)
|
||||
- Unqualified references (just `id`) resolve against the spec's own local resources first
|
||||
|
||||
### The Shared Import Pattern
|
||||
|
||||
@@ -225,14 +216,7 @@ imports:
|
||||
- ref: local:../../shared
|
||||
```
|
||||
|
||||
This gives access to the shared pool: skills such as `systematic-debugging`, `verification-before-completion`,
|
||||
`development-team` and `test-driven-development`; the `openrig-core` plugin, which carries `openrig-user` and OpenRig's
|
||||
other skills; and runtime resources such as `claude-default-settings`, `claude-default-mcp`, `codex-default-config` and
|
||||
`claude-activity-hooks`. Agents select the ones they need via profile `uses`, for example
|
||||
`plugins: [shared:openrig-core]`. For a Claude seat selecting the vendored `openrig-core`, the daemon loads the plugin
|
||||
itself through `CLAUDE_CODE_PLUGIN_DIRS`, including its skills and hooks, without projecting a second copy of its skills
|
||||
(see `refocus-channel.md`). Other selected plugin skills follow the projection rules below. A seat that didn't select
|
||||
`openrig-core` does not get its hooks through this selection.
|
||||
This gives access to the full pool of shared skills (openrig-user, systematic-debugging, development-team, etc.) and agents select the ones they need via profile `uses`.
|
||||
|
||||
---
|
||||
|
||||
@@ -246,10 +230,10 @@ profiles:
|
||||
runtime: claude-code
|
||||
model: claude-opus-4-6
|
||||
uses:
|
||||
skills: [systematic-debugging, vault-user]
|
||||
skills: [openrig-user, systematic-debugging, vault-user]
|
||||
guidance: [role]
|
||||
subagents: []
|
||||
plugins: [shared:openrig-core]
|
||||
hooks: []
|
||||
runtime_resources: []
|
||||
startup:
|
||||
files: []
|
||||
@@ -260,15 +244,13 @@ profiles:
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `summary` | string | no | — | Profile description. Spec review surfaces read a profile `description` instead. |
|
||||
| `preferences` | object | no | — | Runtime/model/effort/Advisor preferences for this profile. |
|
||||
| `summary` | string | no | — | Profile description. |
|
||||
| `preferences` | object | no | — | Runtime/model preferences for this profile. |
|
||||
| `preferences.runtime` | string | no | — | Preferred runtime. |
|
||||
| `preferences.model` | string | no | — | Preferred model. |
|
||||
| `preferences.effort` | string | no | — | Preferred reasoning effort. |
|
||||
| `preferences.advisor` | string | no | — | Preferred Claude Advisor model; overrides `defaults.advisor` and can be overridden by the member. |
|
||||
| `uses` | Uses | no | all empty | Selects which declared resources are active for this profile. |
|
||||
| `startup` | StartupBlock | no | — | Profile-level startup files and actions. Merged with agent-level startup via layering. |
|
||||
| `lifecycle` | Lifecycle | no | — | Profile-level lifecycle overrides. `restore_policy` can only narrow (`resume_if_possible` → `relaunch_fresh` → `checkpoint_only`); `compaction_strategy` and `mechanic` take the most specific value (defaults, then profile, then member). |
|
||||
| `lifecycle` | Lifecycle | no | — | Profile-level lifecycle overrides. |
|
||||
|
||||
### Uses
|
||||
|
||||
@@ -276,32 +258,18 @@ The `uses` block selects which resources from the `resources` pool (including im
|
||||
|
||||
```yaml
|
||||
uses:
|
||||
skills: [systematic-debugging, vault-user]
|
||||
skills: [openrig-user, systematic-debugging, vault-user]
|
||||
guidance: [role]
|
||||
subagents: []
|
||||
plugins: [shared:openrig-core]
|
||||
hooks: [pre-commit]
|
||||
runtime_resources: [claude-settings]
|
||||
```
|
||||
|
||||
Each array contains resource IDs. These can be:
|
||||
- **Unqualified** (`vault-user`) — resolves against the spec's own `resources` first, then imported specs
|
||||
- **Qualified** (`shared:systematic-debugging`) — resolves against a specific imported spec's resources
|
||||
- **Qualified** (`shared:openrig-user`) — resolves against a specific imported spec's resources
|
||||
|
||||
The `uses` categories are: `skills`, `guidance`, `subagents`, `plugins`, `runtime_resources`. A `uses.hooks` key, even an
|
||||
empty list, is refused: hooks ship inside plugins. Any other key selects nothing and gets a validation advisory, shown at
|
||||
launch and by `rig agent validate`, so a typo such as `skils` doesn't go unnoticed. A single value written in place of a
|
||||
category's list (`skills: vault-user`) is read as a one-item list, with an advisory suggesting list syntax, and resolves
|
||||
exactly like that list: when the profile is selected, a resource that isn't available fails that member's resolution with
|
||||
"not found in resource pool".
|
||||
|
||||
**Skills resolve more widely than other resources.** For Claude Code and Codex seats, a skill ID that no declared
|
||||
resource provides is looked up at launch in the project's runtime skill folder in the seat's cwd (`<cwd>/.claude/skills` or
|
||||
`<cwd>/.agents/skills`), then the `skills/` folder beside the RigSpec, then the home skill folder, then the managed
|
||||
skills root. Pi looks only in the `skills/` folder beside the RigSpec. OMP does no filesystem discovery, so its
|
||||
`uses.skills` references need declared resources. A discovered skill's ID is its `SKILL.md` frontmatter `name`, and a declared resource wins over a discovered skill with the
|
||||
same ID. When a profile selects a skill from inside the spec's own folder and the managed catalog holds a different
|
||||
copy, the spec's copy is used and launch warns `skill_bundle_precedence`. Any other differing copy fails launch with
|
||||
`skill_identity_conflict`.
|
||||
The `uses` categories are: `skills`, `guidance`, `subagents`, `hooks`, `runtime_resources`.
|
||||
|
||||
---
|
||||
|
||||
@@ -318,12 +286,10 @@ resources:
|
||||
subagents:
|
||||
- id: helper
|
||||
path: subagents/helper
|
||||
plugins:
|
||||
- id: vault-tools
|
||||
source:
|
||||
kind: local
|
||||
path: plugins/vault-tools
|
||||
plugin_type: auto
|
||||
hooks:
|
||||
- id: pre-commit
|
||||
path: hooks/pre-commit.sh
|
||||
runtimes: [claude-code]
|
||||
runtime_resources:
|
||||
- id: claude-settings
|
||||
path: runtime/claude-settings.fragment.json
|
||||
@@ -331,181 +297,50 @@ resources:
|
||||
type: claude_settings_fragment
|
||||
```
|
||||
|
||||
Resources are the available pool. They are NOT automatically delivered to agents — profiles select them via `uses`.
|
||||
The only declared resources delivered are those that the active profile's `uses` block references. Two sources add
|
||||
skills outside `uses`: the managed skill catalog's system selection and a project's `install.skills`.
|
||||
|
||||
`resources.hooks` is refused at validation: hooks now ship inside plugins, declared under `resources.plugins`.
|
||||
|
||||
### Plugin paths
|
||||
|
||||
A local plugin's `source.path` supports these forms:
|
||||
|
||||
| Form | Resolution |
|
||||
|------|------------|
|
||||
| `/absolute/plugin` | The exact absolute path. |
|
||||
| `~/plugins/example` or `~` | The operating-system user home. |
|
||||
| `openrig-home:plugins/openrig-core` | Relative to the daemon's configured OpenRig home, including a non-default `OPENRIG_HOME`. |
|
||||
| `plugins/example` or `./plugins/example` | Relative to the agent spec directory. |
|
||||
|
||||
For a plugin seeded by OpenRig, use the home-relative form so the spec selects
|
||||
that daemon's copy:
|
||||
|
||||
```yaml
|
||||
resources:
|
||||
plugins:
|
||||
- id: openrig-core
|
||||
source:
|
||||
kind: local
|
||||
path: "openrig-home:plugins/openrig-core"
|
||||
profiles:
|
||||
default:
|
||||
uses:
|
||||
plugins: [openrig-core]
|
||||
```
|
||||
|
||||
`openrig-home:` is resolved by the daemon from its configured state root. It is
|
||||
not shell interpolation and does not consult a seat's shell startup files. Its
|
||||
suffix must be relative. Existing absolute, tilde and spec-relative paths keep
|
||||
their meaning; `~user` is still a literal relative segment, not a user lookup.
|
||||
This form needs OpenRig 0.6.5 or later; keep bundles using it pinned to such a
|
||||
version.
|
||||
Resources are the available pool. They are NOT automatically delivered to agents — profiles select them via `uses`. The only resources delivered are those that the active profile's `uses` block references.
|
||||
|
||||
### Resource Categories
|
||||
|
||||
| Category | Fields | Description |
|
||||
|----------|--------|-------------|
|
||||
| `skills` | `id`, `path`, `on_restore`* | Skill directories containing a SKILL.md (see "Skill Resources"). Projected by the runtime adapter. Claude and Codex seats get them in the seat's own storage, `<OpenRig home>/state/seats/<rig id>/<logical seat>/<runtime>/`, outside the project: Claude to `plugin/skills/<id>` in the seat's generated plugin, where they show as `openrig:<id>`, and Codex to `skills/<id>` beside the seat's skill catalog. A Claude or Codex seat without that storage gets `<cwd>/.claude/skills/<id>` or `<cwd>/.agents/skills/<id>`. Pi and OMP project to the seat's own agent directory under OpenRig's state. |
|
||||
| `guidance` | `id`, `path`, `target`*, `merge`* | Guidance files, merged as a managed block (see "Guidance Resources"). |
|
||||
| `subagents` | `id`, `path` | Subagent definitions. Claude copies them to `<cwd>/.claude/agents/`, Codex to `<cwd>/.agents/`; Pi and OMP don't project them. |
|
||||
| `plugins` | `id`, `source`, `plugin_type`* | Local plugin directories; selected with `uses.plugins`. `source.kind` must be `local`. `plugin_type` is `claude`, `codex` or `auto` (default): Claude projects a plugin to `<cwd>/.claude/plugins/<id>` when it is `claude`, or `auto` with a `.claude-plugin/plugin.json`; Codex projects to `<cwd>/.codex/plugins/<id>` when it is `codex`, or `auto` with a `.codex-plugin/plugin.json`. Pi and OMP don't project plugins. Claude Code and Codex do not read skills from that projected plugin folder, so selected plugin skills are also projected into the seat's skill folder under their plain names: the seat's own storage (see `skills`), or `.claude/skills/<name>` or `.agents/skills/<name>` for a seat without it. The exception is the vendored `openrig-core` selected by a Claude seat: Claude loads that plugin directly, skills included, so its skills are not projected a second time. A folder already there with the same name, ignoring letter case, that OpenRig doesn't own is left alone; unless it already holds the same bytes, launch warns `plugin_skill_kept`. A skill the seat already selects by that name (from its profile, the System World or the project) keeps that source, an edited OpenRig-owned copy is kept with `plugin_skill_kept`, a retained plugin copy whose remembered source can no longer be projected is also kept with `plugin_skill_kept` (a seat selecting that plugin refreshes it at its next launch), and if the plugin skills can't be projected the seat launches without them and warns `plugin_skills_not_projected`. Hooks ship inside plugins. |
|
||||
| `runtime_resources` | `id`, `path`, `runtime`, `type` | Runtime-specific resources. `runtime` and `type` are required. An entry whose `runtime` isn't the seat's runtime is skipped. Pi and OMP project none. |
|
||||
| `skills` | `id`, `path` | Skill directories containing a SKILL.md. Delivered via `skill_install`. |
|
||||
| `guidance` | `id`, `path`, `target`*, `merge`* | Guidance files. Delivered via `guidance_merge` into CLAUDE.md/AGENTS.md. |
|
||||
| `subagents` | `id`, `path` | Subagent definitions. |
|
||||
| `hooks` | `id`, `path`, `runtimes`* | Hook scripts. Optional `runtimes` array restricts to specific runtimes. |
|
||||
| `runtime_resources` | `id`, `path`, `runtime`, `type` | Runtime-specific resources. `runtime` and `type` are required. |
|
||||
|
||||
*Fields marked with `*` are optional.
|
||||
|
||||
Recognized runtime resource types:
|
||||
- `claude_settings_fragment` — merge a JSON object into `<cwd>/.claude/settings.local.json`.
|
||||
- `claude_mcp_fragment` — merge a JSON object into `<cwd>/.mcp.json`.
|
||||
- `claude_activity_hooks` — reconcile OpenRig's activity hooks into `<cwd>/.claude/settings.local.json`.
|
||||
- `codex_config_fragment` — upsert a TOML fragment into `$CODEX_HOME/config.toml` (default `~/.codex/config.toml`) inside an OpenRig-managed block.
|
||||
- `codex_config_fragment` — upsert a TOML fragment into `~/.codex/config.toml` inside an OpenRig-managed block.
|
||||
|
||||
Unknown runtime resource types are still copied to the Claude or Codex runtime extension directory for agent-visible context.
|
||||
|
||||
#### Writing a `codex_config_fragment`
|
||||
|
||||
**Begin the fragment with a table header.** A fragment must be a valid TOML
|
||||
document on its own, and every key it sets must sit under a table it declares.
|
||||
A fragment that puts keys ahead of its first table header is refused at
|
||||
projection time, and nothing is written.
|
||||
|
||||
The reason is TOML's grammar rather than a policy choice. The managed block is
|
||||
appended to the end of the user's `config.toml`, and TOML has no syntax for
|
||||
returning to document root once a table has been opened. So if the user's file
|
||||
ends inside any table, an appended root-level key does not land at root — it
|
||||
silently becomes a member of *their* table. Refusal is deterministic and never
|
||||
inspects the user's file: a fragment author cannot see user state, and a rule
|
||||
that passed or failed depending on it would be impossible to reproduce.
|
||||
|
||||
```toml
|
||||
# refused — `model` would bind into whatever table the user's file ends inside
|
||||
model = "gpt-5"
|
||||
|
||||
[mcp_servers.exa]
|
||||
url = "https://mcp.exa.ai/mcp"
|
||||
```
|
||||
|
||||
```toml
|
||||
# accepted — every key sits under a table this fragment declares
|
||||
[mcp_servers.exa]
|
||||
url = "https://mcp.exa.ai/mcp"
|
||||
```
|
||||
|
||||
Where a fragment's table is one the user already declares, the user's table
|
||||
wins: the managed table is dropped, their values are never merged, rewritten or
|
||||
overwritten, and the rest of the fragment still applies.
|
||||
|
||||
**A fragment can't install a named Codex profile.** Codex versions with named
|
||||
profiles read each one from its own file, `<CODEX_HOME>/<name>.config.toml`,
|
||||
with top-level keys, and won't load a profile while `config.toml` still holds a
|
||||
legacy `[profiles.<name>]` table. A fragment only adds tables to `config.toml`,
|
||||
so neither form works there. Create the profile file yourself and select it with
|
||||
the member's `codex_config_profile`; see "Codex: select sandbox and approvals
|
||||
together" in [getting-started.md](getting-started.md#codex-select-sandbox-and-approvals-together).
|
||||
Before a launch or restore, OpenRig checks that a selected profile loads, and for
|
||||
a legacy table it names the file to move the settings into.
|
||||
Unknown runtime resource types are still copied to the runtime extension directory for agent-visible context.
|
||||
|
||||
### Resource Path Rules
|
||||
|
||||
- All resource paths must be safe relative paths (no `..` traversal, no absolute paths), except a plugin's
|
||||
`source.path`, which takes the forms under "Plugin paths"
|
||||
- All resource paths must be safe relative paths (no `..` traversal, no absolute paths)
|
||||
- Paths resolve relative to the agent spec's directory
|
||||
- Resource IDs must be unique within their category
|
||||
- Resource IDs are the identifiers used in `uses` references
|
||||
|
||||
### Skill Resources
|
||||
|
||||
```yaml
|
||||
skills:
|
||||
- id: house-style
|
||||
path: skills/house-style
|
||||
on_restore: follow_source # optional — keep_saved by default
|
||||
```
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `id` | string | yes | — | Resource identifier, used in `uses.skills`. |
|
||||
| `path` | string | yes | — | Relative path to the skill's folder, which holds its `SKILL.md`. |
|
||||
| `on_restore` | string | no | `keep_saved` | What a restore (`rig up <rig>`) does with a Claude or Codex seat's copy of this skill in the seat's own storage: `keep_saved` or `follow_source`. |
|
||||
|
||||
An unknown `on_restore` value produces an advisory and uses `keep_saved`; it does not invalidate the spec.
|
||||
|
||||
A restore keeps the skills a seat was launched with. With `keep_saved`, it puts back the saved copy and never takes
|
||||
today's bytes from the source. With `follow_source`, before the seat's session starts, the restore reads the seat's agent
|
||||
spec as it is now, using the saved launch recipe and rig folder, and:
|
||||
|
||||
- replaces the seat's copy with the source folder as it is now, and records it, so `rig down` removes it and the next
|
||||
restore puts it back as usual;
|
||||
- copies in a `follow_source` skill the profile now selects that the seat didn't have;
|
||||
- removes a skill that followed its source and that the profile no longer selects.
|
||||
|
||||
A copy edited in the seat (changed bytes or recorded file permissions, or an added file), or a folder with the same id that OpenRig didn't write, is kept as
|
||||
it is and named in the restore's warnings. If the agent spec can't be read or resolved, the seat keeps its saved skills
|
||||
and the restore says why; a source folder without a `SKILL.md` keeps that skill's saved copy. A skill changed to
|
||||
`keep_saved` stops following at the next restore, and its copy stays. Older seat records without file permissions
|
||||
retain their byte-only ownership checks until a new projection or refresh records permissions.
|
||||
|
||||
Source file symlinks are dereferenced, including links to files outside the skill folder, just as during Claude and
|
||||
Codex launch projection. The seat receives an ordinary file containing the target's bytes. A broken link or a link to
|
||||
a directory makes the source unreadable and keeps the saved copy; links in the seat's destination remain protected.
|
||||
|
||||
Only a restore does this; a running seat isn't changed, and neither is a Codex seat restarted in place. A Claude seat
|
||||
loads its seat plugin when its session starts, so it sees the new copies. A Codex seat with following skills receives
|
||||
its rebuilt catalog after its resumed session is ready. The catalog lists the skills actually present, replaces its
|
||||
earlier OpenRig skill list, and asks it to read current skill files before using them. This includes newly selected
|
||||
skills and the removal of the last following skill. An edited or linked catalog is kept and not sent, with a warning;
|
||||
a delivery failure is reported instead of claiming a complete restore. Neither runtime erases skill instructions
|
||||
already read into the retained conversation.
|
||||
|
||||
Following covers the whole selected skill folder, including scripts and references. It does not install external
|
||||
tools, change MCP configuration, or update bundled plugins. The saved launch recipe supplies the rig directory,
|
||||
member and profile; today's agent spec supplies the skill selection. Today's rig YAML and other agent settings are
|
||||
not applied by this refresh.
|
||||
|
||||
### Guidance Resources
|
||||
|
||||
```yaml
|
||||
guidance:
|
||||
- id: role
|
||||
path: guidance/role.md
|
||||
merge: managed_block # optional — the only strategy applied
|
||||
target: CLAUDE.md # optional — where to merge
|
||||
merge: managed_block # optional — how to merge (default: managed_block)
|
||||
```
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `id` | string | yes | — | Resource identifier. A `rig-role` guidance resource is never merged; deliver a role with a `send_text` startup file. |
|
||||
| `id` | string | yes | — | Resource identifier. |
|
||||
| `path` | string | yes | — | Relative path to the guidance file. |
|
||||
| `target` | string | no | — | Parsed but not used: the runtime decides the file. Claude merges into the rig's `managed_blocks` file (`CLAUDE.md` by default, or `CLAUDE.local.md`); Codex, Pi and OMP merge into `<cwd>/AGENTS.md`, except that a fresh Pi seat normally uses its own agent folder's `AGENTS.md` and a fresh Codex 0.164.0+ seat normally gets launch developer instructions; the conditions are in [Codex guidance at launch](agent-startup-guide.md#codex-guidance-at-launch) and [Runtime Differences](agent-startup-guide.md#runtime-differences). |
|
||||
| `merge` | string | no | `managed_block` | Only `managed_block` is applied. Any other value, such as `append`, merges nothing. |
|
||||
| `target` | string | no | — | Target file for merging (e.g., `CLAUDE.md`). |
|
||||
| `merge` | string | no | `managed_block` | Merge strategy. One of: `managed_block`, `append`. |
|
||||
|
||||
---
|
||||
|
||||
@@ -515,17 +350,11 @@ The startup block follows the same format as in the RigSpec (see `docs/reference
|
||||
|
||||
Agent-level startup is applied to all profiles. Profile-level startup is applied only when that profile is active. Both merge additively via the startup layering model.
|
||||
|
||||
An additional orientation challenge requires an explicit `startup_proof` action
|
||||
with `value: authenticated` and `idempotent: true`. Omission adds no exercise;
|
||||
a later applicable `value: none` overrides an earlier selection. See
|
||||
[startup proof selection](rig-spec.md#startup-proof-selection) for precedence,
|
||||
restore behavior, and the distinction between readiness and verified proof.
|
||||
|
||||
### Delivery Hint Quick Reference
|
||||
|
||||
| Hint | When Delivered | Mechanism |
|
||||
|------|---------------|-----------|
|
||||
| `auto` | Depends on the file | A seat's own role (a file marked `orientation: role`, or at a `guidance/role.md` path) is `send_text`, so pod-mates never overwrite each other's role in the shared file. A seat launched before this rule keeps the hint saved at its launch, which restore (`rig up <rig>`), `rig seat launch --fresh` and handover replay, until the rig is instantiated from its spec again: `rig down <rig>`, then `rig up <spec file>`, which starts a new generation with fresh conversations. Otherwise a path ending in `SKILL.md`, or content beginning with `# SKILL`, is `skill_install`; a `.md` file is `guidance_merge`; and any other file is `send_text` |
|
||||
| `auto` | Before harness boot | System chooses based on file type |
|
||||
| `guidance_merge` | Before harness boot | Merged into CLAUDE.md/AGENTS.md as managed block |
|
||||
| `skill_install` | Before harness boot | Installed to runtime skill directory |
|
||||
| `send_text` | After harness is ready | Sent as text to agent terminal via tmux |
|
||||
@@ -539,27 +368,17 @@ restore behavior, and the distinction between readiness and verified proof.
|
||||
3. Import `ref` must start with `local:` (relative) or `path:` (absolute).
|
||||
4. Import `version` must be an exact version (no ranges).
|
||||
5. `profiles` must be a map (object), not an array.
|
||||
6. Profile `uses` references other than skills must resolve to declared resources (local or imported). Skill
|
||||
references are checked at launch, not at validation (see "Uses").
|
||||
6. Profile `uses` references must resolve to declared resources (local or imported).
|
||||
7. Unqualified `uses` references that don't resolve to local resources require imports to be present.
|
||||
8. Qualified `uses` references must be in `namespace:id` format.
|
||||
9. All resource paths must be safe relative paths (a plugin's `source.path` excepted).
|
||||
10. Resource IDs must be unique within their category, and each `resources.<category>` must be an array.
|
||||
11. `runtime_resources` entries require `runtime` and `type` fields.
|
||||
11a. An unknown skill `on_restore` value produces an advisory naming the known values and falls back to `keep_saved`.
|
||||
12a. `resources.hooks` and `profiles.<name>.uses.hooks` are refused: "removed in plugin-primitive (Phase 3a)". Declare
|
||||
a plugin instead.
|
||||
12b. A plugin needs a unique non-empty `id`, a `source` object with `kind: local` and a non-empty `path`, and a
|
||||
`plugin_type` of `claude`, `codex` or `auto` when given.
|
||||
12c. Lifecycle `mechanic` must be a canonical `seat@rig` session address.
|
||||
9. All resource paths must be safe relative paths.
|
||||
10. Resource IDs must be unique within their category.
|
||||
11. `runtime_resources` entries require `runtime` field.
|
||||
12. Lifecycle `execution_mode` must be `interactive_resident`.
|
||||
13. Lifecycle `compaction_strategy` must be one of `default-compaction`, `managed-compaction`, `handover`, `apprentice-handover` — or a deprecated alias (`harness_native`, `pod_continuity`), which validates with a deprecation advisory and normalizes to its canonical value (OPR.0.5.6.20).
|
||||
13. Lifecycle `compaction_strategy` must be `harness_native` or `pod_continuity`.
|
||||
14. Lifecycle `restore_policy` must be `resume_if_possible`, `relaunch_fresh`, or `checkpoint_only`.
|
||||
15. Startup files and actions follow the same validation rules as in RigSpec.
|
||||
|
||||
Unknown keys are ignored, not refused. `rig agent validate <path> [--json]` runs these syntax checks through the running
|
||||
daemon; it doesn't resolve imports, find skills or check that files exist, so a missing skill shows up only at launch.
|
||||
|
||||
---
|
||||
|
||||
## File System Layout
|
||||
@@ -576,8 +395,8 @@ my-agent/
|
||||
skills/
|
||||
my-skill/
|
||||
SKILL.md # skill content
|
||||
plugins/
|
||||
my-plugin/ # plugin (skills, hooks) with .claude-plugin/ and/or .codex-plugin/
|
||||
hooks/
|
||||
pre-commit.sh # hook script
|
||||
runtime/
|
||||
claude-settings.fragment.json # runtime-specific resource
|
||||
```
|
||||
@@ -588,20 +407,12 @@ The only required file is `agent.yaml`. Everything else is referenced by paths i
|
||||
|
||||
## Shipped Examples
|
||||
|
||||
Every agent below except `shared` imports `shared` and selects `plugins: [shared:openrig-core]`, which carries
|
||||
`openrig-user`. Paths are under `packages/daemon/specs/agents/`; the kernel's own agents are under
|
||||
`packages/daemon/specs/rigs/launch/kernel/agents/`.
|
||||
|
||||
| Agent | Location | Default runtime | Profile skills | Purpose |
|
||||
|-------|----------|-----------------|----------------|---------|
|
||||
| `shared` | `shared/` | — | — (resource pool only) | Shared skills, the `openrig-core` plugin and runtime resources |
|
||||
| `implementer` | `development/implementer/` | claude-code | development-team, systematic-debugging, verification-before-completion | Implementation agent |
|
||||
| `qa` | `development/qa/` | codex | development-team, systematic-debugging, verification-before-completion, agent-browser, dogfood | Quality assurance agent |
|
||||
| `orchestrator` | `orchestration/orchestrator/` | claude-code | orchestration-team, systematic-debugging, verification-before-completion | Rig orchestration lead |
|
||||
| `independent-reviewer` | `review/independent-reviewer/` | claude-code | review-team, systematic-debugging, verification-before-completion | Independent code reviewer |
|
||||
| `vault-specialist` | `apps/vault-specialist/` | claude-code | systematic-debugging, verification-before-completion, vault-user | Vault domain specialist |
|
||||
| `product-designer` | `design/product-designer/` | claude-code | development-team, frontend-design, verification-before-completion | Product designer |
|
||||
| `pm` | `product-management/pm/` | claude-code | office-hours, context-builder, requirements-writer, ui-mockup, plan-review, exec-summary, backlog-capture; `advisor` profile: orchestration-team | Product manager; its `advisor` profile is the factory's advisor |
|
||||
| `analyst`, `synthesizer` | `research/analyst/`, `research/synthesizer/` | claude-code, codex | — | Research team |
|
||||
| `conveyor-lead`, `-planner`, `-builder`, `-reviewer` | `conveyor/` | claude-code, codex, claude-code, codex | per role | Kept for the `factory-rsi` world bundle (its planner); no built-in team uses them |
|
||||
| `factory-rsi-release-manager`, `factory-rsi-dogfood` | `factory-rsi/` | claude-code | per role | Used by the `factory-rsi` world bundle (not a built-in team) |
|
||||
| Agent | Location | Imports | Profile Skills | Purpose |
|
||||
|-------|----------|---------|---------------|---------|
|
||||
| `shared` | `specs/agents/shared/` | none | — (resource pool only) | Shared skill pool for all built-in agents |
|
||||
| `implementer` | `specs/agents/development/implementer/` | `shared` | openrig-user, development-team, test-driven-development, systematic-debugging, etc. | TDD implementation agent |
|
||||
| `qa` | `specs/agents/development/qa/` | `shared` | openrig-user, development-team, etc. | Quality assurance agent |
|
||||
| `orchestrator` | `specs/agents/orchestration/orchestrator/` | `shared` | openrig-user, orchestration-team, etc. | Rig orchestration lead |
|
||||
| `reviewer` | `specs/agents/review/reviewer/` | `shared` | openrig-user, review-team, etc. | Independent code reviewer |
|
||||
| `vault-specialist` | `specs/agents/apps/vault-specialist/` | `shared` | openrig-user, systematic-debugging, vault-user | Vault domain specialist |
|
||||
| `design` | `specs/agents/design/` | none | — | Product designer |
|
||||
@@ -1,95 +1,13 @@
|
||||
# Agent Startup Guide
|
||||
|
||||
Last validated: 2026-10-05, against main `9b88b118`
|
||||
Applies to: OpenRig 0.6.8
|
||||
Version: 0.2.0
|
||||
Last validated: 2026-04-11
|
||||
Applies to: OpenRig 0.1.x
|
||||
|
||||
This guide teaches you how to think about what goes into an agent's startup experience — what files to write, where to put them, and how the layering model delivers them. It is an authoring guide, not a schema reference. For field-level details, see `rig-spec.md` and `agent-spec.md`.
|
||||
|
||||
---
|
||||
|
||||
## Continue after a native consent prompt
|
||||
|
||||
If a fresh Claude seat is waiting at its bypass-permissions warning, review and
|
||||
answer that warning in the seat's native pane. Unless the rig was launched with
|
||||
`--non-interruptive` (see [non-interruptive mode](non-interruptive-mode.md)),
|
||||
OpenRig does not accept it for you, and it never sends configured startup context
|
||||
into the active dialog. The seat reports `attention_required` and retains that
|
||||
context for the same occupant.
|
||||
|
||||
After accepting, run `rig seat continue <seat>` (or press `c` on the seat in the
|
||||
TUI, shown while its context is pending). This delivers the pending context
|
||||
without relaunching Claude, and its first prompt reminds the seat that this is a
|
||||
fresh conversation and how to find its work. It does not replay a delivery that
|
||||
already started. A timeout, or a refusal because nothing is pending, reports an
|
||||
unknown outcome; inspect `rig seat status <seat>` before taking another action.
|
||||
It is also refused while the prompt is still showing (`attention_required`), or
|
||||
when the seat's binding or runtime has changed since launch.
|
||||
Runtime readiness, startup delivery, and orientation proof remain separate;
|
||||
a missing proof does not block this continuation.
|
||||
|
||||
Other fresh-launch prerequisites, such as login or workspace trust, use the same
|
||||
continuation: resolve the prerequisite in the native pane, then run the displayed
|
||||
command. `rig ps --nodes` shows it under "Startup details (<seat>)", and
|
||||
`rig up` and `rig bundle install` print it as "Startup attention (<seat>)":
|
||||
"After resolving it in <session>, run: rig seat continue <session>".
|
||||
|
||||
The attention codes that stop startup this way are `trust_gate`,
|
||||
`hook_trust_gate`, `update_gate`, `login_required`, `mcp_gate`,
|
||||
`bypass_consent_gate`, `codex_auth_refusal` and `codex_client_incompatible`.
|
||||
They are recognized from each harness's English prompt text.
|
||||
|
||||
## Continue a Codex fork whose child ID arrived late
|
||||
|
||||
For a member with `session_source.mode: fork`, OpenRig waits for Codex's new
|
||||
child thread ID before sending its role and other startup prompts. It saves the
|
||||
ID and sends the role only for a verified child: the bound pane's foreground
|
||||
process forks the requested parent, and its own native record, matched to its
|
||||
start time, supplies the ID. This capture uses `runtime.readiness_timeout_seconds`
|
||||
(30 seconds by default, configurable from 1 to 600 seconds), as does the later
|
||||
readiness check. The two waits have separate budgets. A seat handover with
|
||||
`--source fork:<seat>` is different: its fork carries the conversation itself,
|
||||
and a missed ID capture leaves the ID pending rather than stopping the handover.
|
||||
|
||||
If the ID is still unavailable, the seat reports `attention_required` and keeps
|
||||
its startup context pending. Wait for the child to finish starting, resolve any
|
||||
native prompt, then run `rig seat continue <seat>`. The continuation verifies
|
||||
that the bound pane is running a fork of the requested parent and reads the
|
||||
child's own native identity before delivering the saved context. It does not
|
||||
launch another fork or resume the parent. The parent ID is never saved as the
|
||||
child's resume token.
|
||||
|
||||
An unavailable or conflicting identity leaves the context pending. Once prompt
|
||||
delivery has been attempted, it cannot be replayed by `continue`, including
|
||||
after a daemon restart: inspect `rig seat status <seat>` and the native pane
|
||||
before deciding how to recover an uncertain delivery.
|
||||
|
||||
## After a restored seat's prompt
|
||||
|
||||
A restore that stops at a prompt, such as Claude's resume selection, records the
|
||||
seat's restore outcome as `attention_required`. `rig seat continue` doesn't change
|
||||
that record: it only delivers pending startup context. While the record stands, it
|
||||
alone keeps the seat's lifecycle at `attention_required`, even with startup `ready`:
|
||||
`rig ps --nodes` shows LIFECYCLE `att`, `needsAttention` is true, and the seat counts
|
||||
in the rig's `attentionCount` and the TUI's attention totals.
|
||||
|
||||
After a full restore of a Claude or Codex seat, answering the prompt is enough. When
|
||||
the seat's activity next turns to running or idle, or when the whole restore completes
|
||||
after the seat already reads running or idle, the daemon checks the live seat: its tmux
|
||||
session is alive, and the pane runs the agent resuming the conversation the seat
|
||||
records. When the check passes, the restore outcome becomes `operator_recovered`, so
|
||||
the restore no longer makes the seat need attention. When it fails, the restore outcome
|
||||
stays and the seat still needs attention, which is true when the pane runs another
|
||||
conversation. A check that started before a newer restore of the seat doesn't settle
|
||||
the newer outcome.
|
||||
|
||||
`rig seat clear-attention <seat>` runs the same check on demand. When it fails, the
|
||||
command says which check failed, and `--reason` doesn't bypass it. If
|
||||
the seat records the wrong conversation and you know the right one, set it with
|
||||
`rig seat set-resume-token <seat> --token-stdin --reason <text>`, then run
|
||||
`clear-attention` again. After a restore of only some seats, or a community
|
||||
harness's assisted startup, `--reason "<what you saw>"` can acknowledge the
|
||||
outcome instead.
|
||||
|
||||
## Two Categories of Startup
|
||||
|
||||
Everything an agent receives at boot time falls into one of two categories:
|
||||
@@ -108,14 +26,14 @@ Context loading shapes what the agent knows, believes, and is capable of:
|
||||
### Category 2: Deterministic Configuration
|
||||
|
||||
The rig spec declaratively installs things into the agent's runtime environment:
|
||||
- Hooks (shipped inside plugins)
|
||||
- Hooks (git hooks, pre-commit scripts)
|
||||
- Permissions (`.claude/settings.json` allowlists, approval modes)
|
||||
- MCPs (Model Context Protocol servers)
|
||||
- System dependencies (tools, packages)
|
||||
|
||||
See the **Current Support Matrix** section at the end of this guide for what is reliable today vs experimental.
|
||||
|
||||
**The recommendation:** Put as much of your setup logic as possible into Category 1 (context loading via markdown files). Describe the desired end state in startup files and let the agent handle the configuration. The deterministic path exists in the spec and will become more reliable over time, but for now the context-loading path is the one that works consistently across runtimes.
|
||||
**The v0.2.0 recommendation:** Put as much of your setup logic as possible into Category 1 (context loading via markdown files). Describe the desired end state in startup files and let the agent handle the configuration. The deterministic path exists in the spec and will become more reliable over time, but for now the context-loading path is the one that works consistently across runtimes.
|
||||
|
||||
---
|
||||
|
||||
@@ -200,35 +118,30 @@ Startup content is merged additively through layers. Each layer adds to what the
|
||||
```
|
||||
1. Agent layer — from the AgentSpec's top-level startup block
|
||||
2. Profile layer — from the active profile's startup block
|
||||
3. Culture layer — OpenRig's default culture, then the RigSpec's culture_file
|
||||
4. Rig layer — from the RigSpec's top-level startup block
|
||||
3. Rig layer — from the RigSpec's top-level startup block
|
||||
4. Culture layer — from the RigSpec's culture_file
|
||||
5. Pod layer — from the pod's startup block
|
||||
6. Member layer — from the member's startup block in the RigSpec
|
||||
7. Operator layer — added by OpenRig: openrig-start.md and, on a fresh start, the onboarding pack
|
||||
7. Operator layer — injected at runtime (openrig-start overlay, context collector, etc.)
|
||||
```
|
||||
|
||||
A member with a `starter_ref` gets that agent starter's files in front of the agent layer. Terminal seats
|
||||
(`builtin:terminal`) skip the agent and profile layers.
|
||||
|
||||
### What Each Layer Is For
|
||||
|
||||
| Layer | Authored by | Purpose | Example content |
|
||||
|-------|-------------|---------|-----------------|
|
||||
| Agent | Agent spec author | Core identity and capabilities that travel with this agent type | Role guidance, default skills |
|
||||
| Profile | Agent spec author | Profile-specific variations | Different skill sets for "default" vs "minimal" profiles |
|
||||
| Culture | OpenRig, then the rig spec author | Team norms | OpenRig's `CULTURE-default.md` for every seat, then the rig's own `CULTURE.md` |
|
||||
| Rig | Rig spec author | Rig-wide context for all agents | Project documentation |
|
||||
| Rig | Rig spec author | Rig-wide context for all agents | Team norms, project documentation |
|
||||
| Culture | Rig spec author | Rig constitution | `CULTURE.md` — communication, quality, operating philosophy |
|
||||
| Pod | Rig spec author | Pod-specific coordination context | Pod SOP, intra-pod workflow |
|
||||
| Member | Rig spec author | Individual member overrides | Member-specific instructions, cwd-specific context |
|
||||
| Operator | OpenRig system | System-injected runtime content | `openrig-start.md` (optional delivery); on a fresh start, `openrig-onboarding-01.md` and `-02.md` unless `onboarding.default_pack.enabled` is false |
|
||||
|
||||
After the files, OpenRig sends a session-identity message (rig, pod, member, logical ID) once the seat is ready.
|
||||
| Operator | OpenRig system | System-injected runtime content | `openrig-start.md`, context collector |
|
||||
|
||||
### Practical Guidance
|
||||
|
||||
**Most rigs only need three layers:** agent (role.md), culture, and the built-in operator layer. Start simple. Add pod and member layers only when agents in the same pod need different startup content.
|
||||
**Most rigs only need three layers:** agent (role.md), rig (culture), and operator (openrig-start). Start simple. Add pod and member layers only when agents in the same pod need different startup content.
|
||||
|
||||
**The rig's own culture file is high-value and often skipped.** Every seat gets OpenRig's default culture, but that says nothing about how this team communicates and coordinates. Write a `CULTURE.md`. Even a short one dramatically improves team coherence.
|
||||
**The culture layer is high-value and often skipped.** A rig without a `CULTURE.md` relies on agents to guess how the team communicates and coordinates. Write one. Even a short culture file dramatically improves team coherence.
|
||||
|
||||
**Member-level startup is for exceptions, not the rule.** If every member has its own startup block, the layering model is being used as a configuration dump. Refactor shared content up to the pod or rig level.
|
||||
|
||||
@@ -240,8 +153,8 @@ After the files, OpenRig sends a session-identity message (rig, pod, member, log
|
||||
|
||||
| Delivery Hint | When | How | Use For |
|
||||
|---------------|------|-----|---------|
|
||||
| `auto` | Depends on the file | Resolved per file: a `SKILL.md` (or content starting `# SKILL`) is `skill_install`, any other `.md` is `guidance_merge`, and any other file is `send_text` | Default — let OpenRig decide |
|
||||
| `guidance_merge` | Before harness boot | Merged as a managed block into the repository `AGENTS.md` (Codex, OMP), a fresh Pi seat's own agent-directory `AGENTS.md` (see [Runtime Differences](#runtime-differences)), or `CLAUDE.md` (Claude; `CLAUDE.local.md` with the rig's `managed_blocks: { claude-code: CLAUDE.local.md }`). A fresh launch of Codex 0.164.0 or later can carry it as launch developer instructions instead (see [Codex guidance at launch](#codex-guidance-at-launch)) | Role guidance, culture, project context |
|
||||
| `auto` | Before harness boot | System chooses | Default — let OpenRig decide |
|
||||
| `guidance_merge` | Before harness boot | Merged into `CLAUDE.md` / `AGENTS.md` as a managed block | Role guidance, culture, project context |
|
||||
| `skill_install` | Before harness boot | Copied to runtime's skill directory | Skills |
|
||||
| `send_text` | After harness is ready | Sent as text to agent's terminal via tmux | Boot-time grounding, identity hints, instructions to read skills |
|
||||
|
||||
@@ -254,183 +167,14 @@ Files delivered via `send_text` happen AFTER the harness is ready. The agent rec
|
||||
- Instructions to load skills (the files are already projected, the message tells the agent to read them)
|
||||
- Context that should feel like an operator briefing, not pre-loaded content
|
||||
|
||||
On a fresh launch the first message is one turn: the session identity, the first `send_text` file and, when a startup
|
||||
proof is selected, its challenge (below).
|
||||
|
||||
A file's `required` field defaults to `true`: a required file that can't be delivered fails startup, and an optional
|
||||
one is dropped without a message.
|
||||
|
||||
### Readiness, startup proof and failure
|
||||
|
||||
**Readiness.** After launch OpenRig polls the pane until the harness is interactive (backing off 1, 2, 4, 8 and 16
|
||||
seconds), for up to 30 seconds by default. Change the window with
|
||||
`rig config set runtime.readiness_timeout_seconds <1-600>` (or `OPENRIG_RUNTIME_READINESS_TIMEOUT_SECONDS`); it
|
||||
applies to the next launch. A recognized prompt (the attention codes above) ends the wait at once as
|
||||
`attention_required`. A timeout ends startup as `failed` ("Readiness timeout after Ns — harness did not become
|
||||
interactive"), and the post-launch files aren't sent. A Codex fork that the rig launches (a member with
|
||||
`session_source.mode: fork`, or an agent image's fork) is the exception: its timeout ends as `attention_required`,
|
||||
with its startup context kept for `rig seat continue` (see below). A seat handover's fork has its own readiness check.
|
||||
|
||||
**Fresh Claude handovers.** `rig handover --source fresh` keeps the previous agent's scrollback. Before sending the
|
||||
handover context, OpenRig waits for the latest prompt inside Claude's complete input frame, and confirms that the
|
||||
process in the pane belongs to the new launch. A shell's `❯` prompt or echoed launch command is not enough, even
|
||||
with an old Claude header or input frame above it and Claude already running. A header alone or a partially drawn
|
||||
input frame also keeps the check pending. If the current screen or process cannot be read, or the process identity is unconfirmed, the handover keeps
|
||||
waiting within the readiness window and fails if that window expires. Old screen text or the `claude` process label
|
||||
alone cannot complete this check. A healthy agent can therefore remain unconfirmed while those observations are
|
||||
unavailable; inspect its pane before deciding how to recover.
|
||||
|
||||
**Claude submission check.** For each startup message to a Claude seat, OpenRig makes a bounded check that the prompt
|
||||
was submitted. When the capture can't tell, it records the submission as unverified and carries on. When it sees the
|
||||
same prompt still sitting in the input, it presses Enter once more; if it's still there, it warns "Startup prompt still
|
||||
staged in <session>; press Enter in that pane." The seat can still end `ready` with its submission `unverified` or
|
||||
`staged`, so `ready` doesn't prove the prompt was submitted. After all files and actions are delivered, readiness is
|
||||
checked once more, so a prompt that appears by then gives `attention_required`.
|
||||
|
||||
**Startup proof.** A `startup_proof` action selects `authenticated` or `none` (the default is none; the last applicable
|
||||
one wins, and it needs `idempotent: true`). With `authenticated`, a fresh launch of an agent seat includes a challenge,
|
||||
and the seat answers it with `rig startup-proof submit --challenge-id <id> --answer <answer>`. `rig ps --nodes --full`
|
||||
shows the result in the ORIENTED column: `verified`, `missing`, `rejected` or `n-a`. A missing proof doesn't block startup.
|
||||
|
||||
**After a failure.** Delivery, launch and action failures, and readiness timeouts other than a Codex fork's, give
|
||||
`failed`. Every 30 seconds the context monitor marks a `failed` or `attention_required` seat `ready` once its pane reads
|
||||
ready, unless its fresh or Codex fork context is still pending for `rig seat continue`. A seat that timed out can therefore read `ready` without having received its
|
||||
post-launch files. If it needs them, first look at what that occupant has done; then
|
||||
`rig seat launch <seat> --fresh --reason <text>` starts a blank conversation for the seat. If the occupant is still
|
||||
live, the command refuses with `session_live` unless you also pass `--stop`, which replaces that occupant deliberately.
|
||||
|
||||
**Preview before replacing a seat.** `rig seat launch <seat> --fresh --plan` inspects the selected
|
||||
seat without starting tmux, stopping or launching a process, projecting startup files, or changing
|
||||
lifecycle/queue records. Add `--json` for structured output. `--fresh` is still required;
|
||||
`--reason` is optional for the preview and required when executing. Pass `--stop` to preview the
|
||||
intended replacement of a live managed occupant. Adopted and unmanaged occupants remain blocked.
|
||||
|
||||
The plan identifies the target, current occupant, stop requirements, blockers, startup counts,
|
||||
and each sibling seat's unchanged effect. Siblings stay attached and untouched. Launch composes
|
||||
a new command through the runtime adapter using current settings; it does not replay a stored
|
||||
wrapper command. The seat's launch environment and managed executable selection still apply.
|
||||
The plan does not construct or display the launch command.
|
||||
|
||||
A preview is a snapshot, not a reservation or a native-readiness check. Unreadable tmux produces
|
||||
`complete: false` and `launchable: null` (unknown), unless a known blocker already proves
|
||||
`launchable: false`. A successful preview exits 0, including blocked or incomplete plans; scripts
|
||||
must inspect `launchable === true` before deciding to execute. Request failures still exit nonzero.
|
||||
Execution always resolves and checks the current state again. An older daemon without the preview
|
||||
endpoint refuses the request; the CLI never falls back to executing a launch.
|
||||
|
||||
### Change an existing seat's working directory
|
||||
|
||||
Editing `cwd` in an already applied RigSpec doesn't update an existing seat's saved launch directory. Use `set-cwd` to
|
||||
save a new one for the seat's next launch:
|
||||
|
||||
```sh
|
||||
rig seat set-cwd planning-lead@project \
|
||||
--cwd /work/project/seats/planning-lead --reason "isolate planning"
|
||||
rig seat status planning-lead@project
|
||||
```
|
||||
|
||||
**The path:** it must be absolute **on the daemon's host**. OpenRig creates a missing directory, resolves symlinks to
|
||||
its real path, and refuses a filesystem root.
|
||||
|
||||
**What it saves:** the directory and a `node.cwd_changed` audit event (from, to, reason, operator), together, in one
|
||||
transaction. The seat's node, canonical address, queue, session history and skill selection stay as they are.
|
||||
`rig seat status` shows **Working directory for future launches** (`launch_cwd` in JSON). That's saved configuration,
|
||||
not where the running process is.
|
||||
|
||||
**What it writes:** it creates the folder if it's missing, and writes nothing else there. The seat's next fresh launch or
|
||||
rebuild handover writes its startup files there, as launch does today.
|
||||
|
||||
**`set-cwd`'s preflight policy:** before saving, it reads, without writing, the enumerated resource copies (including
|
||||
nested plugin files) and startup skills that launch would place in the new directory, at their actual destinations,
|
||||
as they are at that moment. It's a point-in-time check, not a global overwrite guarantee or protection against later
|
||||
changes; at adoption, launch applies its own policy. (The commit's re-check covers the saved startup configuration,
|
||||
not the destination's file contents.)
|
||||
- **Refused, with the protected paths named (`cwd_destination_conflict`):** a file there that an operator edited, an
|
||||
unrecorded file with different content, or two sources for one destination. That's stricter than launch, which
|
||||
overwrites an unrecorded differing file with a warning; the refusal is deliberate, while the occupant still runs.
|
||||
- **What to do:** use a clean destination, or move or merge those files, then retry.
|
||||
- **Not checked:** managed guidance blocks and native configuration merges keep the adapter's policy at launch, and
|
||||
launch-owned files such as `.openrig/context-collector.cjs` are written by launch as always.
|
||||
- **A community harness** takes no startup files, so none are checked for it.
|
||||
|
||||
**The running occupant keeps its current directory.** Adopt the new one with:
|
||||
- `rig seat launch <seat> --fresh --stop --reason <text>`, which starts a blank conversation; or
|
||||
- `rig handover <seat> --source rebuild --reason <text>`, which carries durable context.
|
||||
|
||||
For Codex rebuilds, OpenRig also carries a complete supported native permission policy read from the live
|
||||
predecessor's own conversation and process, without adding fresh-launch grants. A later recorded seat permission
|
||||
or policy choice, or a differing declared successor profile, takes precedence. Missing, stale or unsupported observations keep the existing launch
|
||||
behaviour and are reported as unverified in the handover result; requested modes and profiles alone don't prove
|
||||
preservation. The result reports what was emitted, not native successor equivalence.
|
||||
|
||||
**Adopt it promptly.** The saved directory doesn't move the running process, but background work can use it before
|
||||
adoption. Saved configuration isn't the live process's directory, and these examples aren't the only consumers. For a
|
||||
running Claude seat:
|
||||
- **Collector provisioning** writes `.openrig/context-collector.cjs` and merges `.claude/settings.local.json` in the
|
||||
saved directory, under its existing preservation rules.
|
||||
- **Managed compaction,** manual (`POST /api/compaction/trigger`) or automatic, asks the seat to write its restore map
|
||||
under it. That may be outside the seat's current workspace, so a seat without bypass permissions may meet a write
|
||||
prompt.
|
||||
|
||||
Adopting promptly is guidance. It doesn't protect against a poll or a compaction that runs before your next command.
|
||||
|
||||
**Before adopting,** don't `rig down`/`rig up` or hand over by fork. Those resume the existing conversation in the
|
||||
saved directory, and Claude keeps conversations per project directory, so a resume there may not find it.
|
||||
Cross-directory resume hasn't been run natively; this is the order to follow. `rig seat restart --keep-conversation`
|
||||
(Codex) isn't adoption: it respawns in the directory it observes from the running process.
|
||||
|
||||
**Failures:** a refusal, missing or malformed saved startup context, or an audit-write failure leaves the prior
|
||||
directory saved, and changes no seat, custody or startup file (a refusal may leave the new folder created). A concurrent configuration change is reported, not overwritten. A CLI timeout means the outcome is
|
||||
unknown; check `rig seat status` before retrying. Choosing the same directory again records no second event.
|
||||
|
||||
**The old directory:** files there are left as they are, since other seats may still use them. The source RigSpec
|
||||
declaration isn't changed either; update it separately if a future recreation should use the new directory.
|
||||
|
||||
### The `applies_on` Field
|
||||
|
||||
Each startup file and action specifies when it applies:
|
||||
- `fresh_start` — delivered when a new conversation starts: the first launch, `rig seat launch --fresh`,
|
||||
`rig seat continue`, and seats restored with `--fresh`
|
||||
- `restore` — delivered when a seat is restored without its earlier conversation: a restore whose resume falls back to a fresh conversation, or a retried start
|
||||
- `fresh_start` — delivered on first launch only
|
||||
- `restore` — delivered on restore from snapshot
|
||||
- Default: `[fresh_start, restore]` (both)
|
||||
|
||||
**An exact resume delivers neither.** When a restore reopens the seat's saved conversation (`rig up <rig> --existing` with a resume token), OpenRig types no startup actions and delivers no startup files, `restore` ones included: the conversation already has them, and typing them again would add turns to it. The one exception: the guidance file's managed blocks are brought up to date before the harness starts. A missing block is put back, and a stale one is refreshed (see below).
|
||||
|
||||
**A seat handover does the same put-back.** Before a fresh, fork or rebuild successor starts, the handover puts the seat's missing managed guidance blocks back in its guidance file from the saved startup context. That's the rig's Claude file (`CLAUDE.md`, or `CLAUDE.local.md` when the rig uses it) for Claude, and `AGENTS.md` for Codex, Pi and omp. So a seat rebuilt into a new working directory keeps its guidance.
|
||||
|
||||
**A block you edited by hand is never overwritten.** OpenRig records what it last wrote in each managed block, so a launch, an exact resume, a handover or a runtime switch can refresh a block whose source file changed since. The record follows the file itself, so two links to one file share it. A block you've edited since OpenRig wrote it is left as you wrote it. The start's result names it (`guidance_block_edited`), and the seat is told in one line once it's ready, or in a handover's packet. A block OpenRig has no record of writing (any block written before these records were kept) that differs from its source works like this:
|
||||
- a launch replaces it, as launches always did, and names it (`guidance_block_refreshed_unrecorded`);
|
||||
- an exact resume or a handover leaves it, and names it as possibly stale (`guidance_block_may_be_stale`).
|
||||
|
||||
An exact resume doesn't type anything into the resumed conversation. So there, the restore result names the block (`guidanceNotes`), but the seat isn't told.
|
||||
|
||||
Use this to avoid re-sending context that a fresh conversation doesn't need twice. For example, a one-time project briefing might only apply on `fresh_start`, while identity grounding should apply on both, so a seat that comes back fresh gets it again.
|
||||
|
||||
---
|
||||
|
||||
## What a seat is set to, and what it's running
|
||||
|
||||
A seat's settings can disagree with its running agent: what's saved for the next launch, what the running process
|
||||
actually uses, and what its conversation has already read are three separate things. OpenRig reports each one
|
||||
separately. Without live evidence it says unknown (or not verified), never the saved value. A saved change reaches the
|
||||
running seat only through the action in the last column.
|
||||
|
||||
| What | Saved for the next launch | What the running seat has | How the running seat gets it |
|
||||
|---|---|---|---|
|
||||
| Model | `rig seat set-model` | `rig ps --nodes` shows the configured model beside the last observed one, or `UNKNOWN (<reason>)` | The next managed resume or successor launch |
|
||||
| Reasoning effort | `rig seat set-effort` | `rig seat effort <seat>`: requested, saved, sent at launch, and what the session recorded, which is not verified until it takes a turn | Its next managed continuation; nothing restarts |
|
||||
| Permissions | `rig seat set-permissions` (`--mode`, `--rules`, or for Codex `--codex-permissions` or `--codex-profile`), shown by `rig seat status` | What it launched with; a status read doesn't prove the running process enforces the selection | Its next managed launch or resume ([per-seat permission mode](getting-started.md#per-seat-permission-mode)) |
|
||||
| Working directory | `rig seat set-cwd`, shown by `rig seat status` as **Working directory for future launches** | The directory it started in | `rig seat launch --fresh --stop` (a blank conversation) or `rig handover --source rebuild` (durable context); [details](#change-an-existing-seats-working-directory) |
|
||||
| Launch wrapper (`launch_wrapper`) | Member `launch_wrapper` in `rig.yaml` (`pi`, `codex` and `claude-code` only; `launch_wrapper: []` clears, omitting retains on existing-seat apply; surfaced as `launchWrapper` in `rig ps --nodes --json` when set; no live `rig seat` edit verb) | The literal wrapper argv that prefixes the resolved Codex entry or Claude executable (preserving TTY stdio) or the resolved Pi child executable inside OpenRig's host runner (preserving RPC pipes and runner lifecycle) when that session launched; a configured prefix is not proof of isolation, tooling, memory or governance | Supported spec create/recreate/apply flow, then the seat's next managed launch, resume, restore or handover (fails closed when wrapper support is unavailable; never falls back to bare child launch; [details](rig-spec.md#launch-wrapper-runtime-pi-and-codex)) |
|
||||
| Startup files and skills | The spec's startup files, skills and actions | What its conversation read when it started; editing a file afterwards doesn't reach it | A fresh launch or a handover delivers them again; for the running seat, send or walk the change (`rig walk <seat> --through <files>`) |
|
||||
| Tools (MCP servers, plugins) | Selected fragments, merged into the harness's own config (`.mcp.json` for Claude, `config.toml` for Codex) | Unknown to OpenRig: registered isn't active, Claude asks before it uses a new server, and no OpenRig command reports which tools a running harness loaded | Check in the harness itself (Claude Code's `/mcp`); a running session may need a restart to pick up newly written config ([runtime config disclosure](#runtime-config-disclosure)) |
|
||||
|
||||
Per harness: Claude Code's and Codex's permission choices are in the setup reference
|
||||
([Claude Code](getting-started.md#claude-code-a-different-launch-flag),
|
||||
[Codex](getting-started.md#codex-select-sandbox-and-approvals-together)), Codex homes are
|
||||
[below](#separate-codex-homes-for-separate-installations), community harnesses' limits are in
|
||||
[their presets](community-harness-presets.md), and moving a live seat to the other runtime is in the
|
||||
[setup reference](getting-started.md#move-a-seat-to-the-other-runtime).
|
||||
Use this to avoid re-sending context that the agent already has from its resumed conversation. For example, a one-time project briefing might only apply on `fresh_start`, while identity grounding should apply on both.
|
||||
|
||||
---
|
||||
|
||||
@@ -440,10 +184,15 @@ The AgentSpec and RigSpec allow declaring deterministic environment configuratio
|
||||
|
||||
### What the Spec Supports
|
||||
|
||||
**Hooks** ship inside plugins: declare the plugin under `resources.plugins[]`. AgentSpec validation refuses
|
||||
`resources.hooks`. OpenRig loads one plugin's hooks into a seat itself: the shipped `openrig-core` plugin, for a Claude seat
|
||||
that selects it (see [the refocus channel](refocus-channel.md#the-mechanism)). The folder a declared plugin is projected into
|
||||
is not one Claude Code or Codex loads, so a plugin of your own does not get its hooks run by that projection.
|
||||
**Hooks** (in `resources.hooks`):
|
||||
```yaml
|
||||
resources:
|
||||
hooks:
|
||||
- id: pre-commit
|
||||
path: hooks/pre-commit.sh
|
||||
runtimes: [claude-code]
|
||||
```
|
||||
Hooks are scripts copied to the agent's workspace. They can be git hooks, automation scripts, or environment setup.
|
||||
|
||||
**Runtime resources** (in `resources.runtime_resources`):
|
||||
```yaml
|
||||
@@ -465,31 +214,23 @@ startup:
|
||||
phase: after_ready
|
||||
idempotent: true
|
||||
```
|
||||
Each value is typed into the harness's input and submitted, so it is a prompt or a slash command, not a shell command.
|
||||
Types are `send_text`, `slash_command` and `startup_proof` (see "Startup proof" above); `shell` is refused.
|
||||
`idempotent` is required, and an action that isn't idempotent must not list `restore` in `applies_on`. Both phases
|
||||
run after the harness is ready: `after_files` (the default) after the post-launch files, then `after_ready`.
|
||||
Commands sent to the agent's terminal after it's ready. Can install MCPs, run setup commands, etc.
|
||||
|
||||
**Don't rename a managed Claude seat.** OpenRig starts every Claude seat with `--name <session>`, on a fresh start and on
|
||||
every resume, and its own reads of the seat use that name: context usage and managed compaction read the status-line
|
||||
record Claude saves under it, and the input box is recognized by the name on its border. A startup `/rename` to another
|
||||
name breaks those reads until the next resume puts the session name back.
|
||||
|
||||
### Current Support Matrix
|
||||
### Current Support Matrix (OpenRig 0.1.x)
|
||||
|
||||
| Capability | Status | Notes |
|
||||
|------------|--------|-------|
|
||||
| Guidance file projection (`guidance_merge`) | **Supported** | Reliable. Primary delivery mechanism. |
|
||||
| Skill projection (`skill_install`) | **Supported** | Reliable. Skills are copied to workspace. |
|
||||
| `send_text` delivery after ready | **Supported** | Reliable. Requires harness to be ready. |
|
||||
| Hooks | **Through plugins, limited** | `resources.hooks` is refused. OpenRig loads the shipped `openrig-core` plugin's hooks into a Claude seat that selects it (Claude Code v2.1.280 or later); a plugin you declare under `resources.plugins[]` is projected, but its hooks are not loaded. |
|
||||
| Hook projection | **Experimental** | Files are copied but execution/integration varies by runtime. |
|
||||
| Runtime resource projection | **Supported for recognized fragments** | `claude_settings_fragment`, `claude_mcp_fragment`, and `codex_config_fragment` are applied to provider config. Unknown types are copied to runtime extension directories. |
|
||||
| Permission configuration | **Native settings plus managed launch flags** | OpenRig launches Claude with `acceptEdits` and Codex with `workspace-write` unless an explicit supported selection changes them; a seat with no policy, selection or named Codex profile also gets the per-launch [team default](rig-spec.md#team-launch-defaults), and the `kernel` rig's seats get a wider operational default ([rig spec, "At launch"](rig-spec.md)). Neither is written to a settings file. Use [the first-user permission guide](getting-started.md#opt-in-permissive-operation) for opt-in and custom choices. |
|
||||
| MCP installation | **Supported for Claude fragments** | A selected `claude_mcp_fragment` is merged into the project's `.mcp.json`. Claude asks to approve new servers found there; that prompt stops startup as `mcp_gate` until someone answers it and runs `rig seat continue`. Otherwise use `/mcp` or `claude mcp add`, or describe the servers in startup files for the agent to configure. |
|
||||
| Permission allowlisting | **Supported via harness config** | Core OpenRig boot adds the minimal Claude `Bash(rig:*)` baseline. Additional policy should be supplied through spec-selected runtime resource fragments or native Codex config profiles, not adapter hard-code. |
|
||||
| MCP installation | **Experimental** | Claude Code: `/mcp` interactive command or `claude mcp add` from CLI. Can also be described in startup files for agent self-configuration. Reliability depends on runtime TUI state. |
|
||||
| System dependency installation | **Not deterministic** | Describe in startup files; agent handles via shell commands. |
|
||||
| Recurring tasks / wake timers | **Runtime-dependent** | Claude Code supports recurring tasks via the `/loop` command. Codex does not have a confirmed equivalent. Orchestrators should include `/loop` instructions in startup for Claude Code agents. |
|
||||
|
||||
### The Approach: Describe, Then Let the Agent Handle It
|
||||
### The v0.2.0 Approach: Describe, Then Let the Agent Handle It
|
||||
|
||||
For anything beyond `guidance_merge`, `skill_install`, and `send_text`, the recommended approach is:
|
||||
|
||||
@@ -505,117 +246,20 @@ This way, when deterministic support becomes fully reliable, the agent will boot
|
||||
OpenRig performs best-effort deterministic runtime configuration for managed sessions. Core bootstrap stays minimal; user/custom policy belongs in spec-selected runtime resources. These writes are intentionally invasive and should be disclosed plainly:
|
||||
|
||||
- Claude global config: `~/.claude/settings.json`
|
||||
OpenRig no longer writes a core `Bash(rig:*)` permission allowance here. Older
|
||||
installations may retain one; existing user settings are not removed.
|
||||
Purpose: allow core `rig` commands without repeated Claude permission prompts.
|
||||
- Claude global state: `~/.claude.json`
|
||||
Purpose: pre-trust managed workspaces and mark onboarding complete for fresh managed sessions.
|
||||
Explicit permission modes use the launch-selected `HOME/.claude.json`, or
|
||||
`<CLAUDE_CONFIG_DIR>/.claude.json` when that variable is set. Classic startup
|
||||
retains the daemon-home path.
|
||||
- Claude project-local config: `.claude/settings.local.json`
|
||||
Purpose: apply context collector/activity hooks and selected `claude_settings_fragment` resources inside the project without committing them to git.
|
||||
- Claude project-local MCP config: `.mcp.json`
|
||||
Purpose: apply selected `claude_mcp_fragment` resources for Claude in that project.
|
||||
- Codex global config: `$CODEX_HOME/config.toml`, or `~/.codex/config.toml` when unset
|
||||
- Codex global config: `~/.codex/config.toml`
|
||||
Purpose: pre-trust managed workspaces and apply selected `codex_config_fragment` resources. Codex currently has no equivalent project-local MCP config path for global profile settings.
|
||||
- Global skill folders: `~/.claude/skills` and `~/.agents/skills`
|
||||
Purpose: daemon startup seeds the `openrig-skills` and `refocusing` skills and the person-facing `rigs` skill. Each
|
||||
upgrade refreshes the `rigs` skill; a copy installed another way is left as it is.
|
||||
|
||||
Two important caveats:
|
||||
- these writes are best-effort and should still be paired with startup guidance so the local agent can verify and repair them if needed
|
||||
- already-running adopted sessions may need restart before they pick up newly written config
|
||||
|
||||
Permission mode and runtime resource projection are separate. A selected fragment
|
||||
can affect native configuration, but OpenRig's launch flags can override those
|
||||
values. Recording a config-surface `permission_policy` is not proof that its
|
||||
rules were translated or applied. See [permission precedence and limits](getting-started.md#custom-settings-and-precedence).
|
||||
|
||||
### Separate Codex homes for separate installations
|
||||
|
||||
Set an **absolute `CODEX_HOME` in the environment that starts the daemon** when
|
||||
you want a separate Codex configuration and state root. OpenRig uses that root
|
||||
for its startup hook setup/removal and trust records, feature flag, workspace
|
||||
trust and selected config fragments. Startup can write those files even when
|
||||
the daemon has no seats.
|
||||
|
||||
The explicit selection also applies to future managed Codex fresh, resume and
|
||||
fork launches, their capability/profile/configuration probes, native thread and
|
||||
context reads, and plugin-cache discovery. Launch commands reassert it after
|
||||
pane shell startup, so an rc file cannot silently switch that launch to another
|
||||
Codex home. Executable and PATH selection stay the same. Existing running
|
||||
sessions are not moved or restarted; a thread in another home is not searched
|
||||
as a fallback for an explicitly selected home.
|
||||
|
||||
**With `CODEX_HOME` unset, a second install still shares `~/.codex`.** Setting only
|
||||
`OPENRIG_HOME` separates OpenRig state, not provider configuration. The default
|
||||
launch behavior is unchanged, including existing pane-shell overrides.
|
||||
|
||||
OpenRig does not copy or migrate authentication, history, rules, trust or
|
||||
settings into the selected home. A separate file-backed home needs its intended
|
||||
authentication provisioned if absent; OS credential stores and other config
|
||||
layers can behave differently, so a new login is not universally required.
|
||||
Explicit Codex auth commands resolve their **caller's** `CODEX_HOME`, otherwise
|
||||
`$HOME/.codex`; contacting a daemon with `OPENRIG_URL` does not retarget auth
|
||||
save/switch/registry writes to that daemon's selection. Set the intended root
|
||||
on those commands as well.
|
||||
|
||||
This separates provider files, not OS users or accounts. Project-local resources
|
||||
still use the selected project directory; shared project and system settings
|
||||
remain shared.
|
||||
|
||||
### Codex guidance at launch
|
||||
|
||||
On a fresh launch of Codex 0.164.0 or later, OpenRig passes a seat's managed guidance (OpenRig's start file and the
|
||||
rig's and project's context, culture and SOP files) with the launch, as `-c developer_instructions=…`, instead of
|
||||
merging it into the repository `AGENTS.md`. Neither the tracked `AGENTS.md` nor the Codex home is written for it, and
|
||||
both still load as usual. The text opens: "OpenRig seat guidance for this session follows. Where it conflicts with
|
||||
AGENTS.md guidance in the repository or the Codex home, follow AGENTS.md."
|
||||
|
||||
A launch value replaces a configured one, so this happens only when no Codex config layer OpenRig reads sets
|
||||
`developer_instructions`. OpenRig reads:
|
||||
|
||||
- `/etc/codex/config.toml` and `/etc/codex/managed_config.toml`;
|
||||
- the Codex home's `config.toml`, profiles included, and the selected profile's `<profile>.config.toml`;
|
||||
- each `.codex/config.toml` from the seat's working directory up.
|
||||
|
||||
The Codex home it reads is the seat's own (a `CODEX_HOME` in its `env:`, or a recorded account switch), else the
|
||||
daemon's `CODEX_HOME`, else `~/.codex`.
|
||||
|
||||
The guidance still goes to the repository `AGENTS.md`, as before, when:
|
||||
|
||||
- a layer OpenRig reads sets `developer_instructions`, or can't be read. A launch warning names the file;
|
||||
- the launch resumes or forks a conversation. Developer instructions on a resume haven't been tested, and an exact
|
||||
resume puts the guidance back in the repository `AGENTS.md`;
|
||||
- the seat is restored, even when the restore starts a new conversation. A restore keeps the guidance file its launch
|
||||
recorded, which for Codex is the repository `AGENTS.md`;
|
||||
- a handover successor starts (fresh, fork or rebuild). Its launch carries no guidance of its own, so the handover
|
||||
first puts the seat's missing blocks back in the repository `AGENTS.md` from its saved startup context;
|
||||
- Codex is older than 0.164.0, or its version is unknown.
|
||||
|
||||
A seat's role still arrives as startup text, and the seat-skills line, after the person's yes, still goes to
|
||||
`AGENTS.md`.
|
||||
|
||||
A launch that carries developer instructions writes no new blocks to the repository `AGENTS.md`. But blocks an earlier
|
||||
handover or resume left there are refreshed from their sources, by the same rule as every other managed block. That
|
||||
matters because the launch text asks Codex to follow `AGENTS.md` where the two differ.
|
||||
|
||||
**If your shell sets `CODEX_HOME`.** When neither the seat nor the daemon declares a `CODEX_HOME`, a seat's pane keeps
|
||||
its own, and OpenRig reads `~/.codex`. A `CODEX_HOME` that only your shell startup sets (in `~/.zshrc`, for example),
|
||||
with the daemon started without it, isn't seen. A `developer_instructions` set in that home is then replaced by the
|
||||
launch value for the session. To have it checked, declare that same absolute path as `CODEX_HOME` in the member's or
|
||||
rig's `env:` (see [Launch environment](rig-spec.md#launch-environment), "A seat's own account"). OpenRig then reads
|
||||
that home at the seat's next fresh launch, and a `developer_instructions` set there keeps the repository `AGENTS.md`.
|
||||
The seat keeps the same home and login.
|
||||
|
||||
**What it doesn't cover.** OpenRig doesn't read Codex's packaged defaults, or MDM- or enterprise-managed settings. A
|
||||
`developer_instructions` set there may be replaced for the session. Legacy MDM-managed config ranks above launch flags,
|
||||
so it can override OpenRig's guidance, and the seat then gets none of it.
|
||||
|
||||
**Not established.** The order Codex assembles developer instructions and `AGENTS.md` in, and how it settles every
|
||||
conflict, aren't established; the opening sentence asks it to follow `AGENTS.md`. The native behaviour was observed on
|
||||
Codex 0.164.0 alone, on fresh launches without tools.
|
||||
|
||||
### Runtime Differences
|
||||
|
||||
**Claude Code:**
|
||||
@@ -630,162 +274,13 @@ Codex 0.164.0 alone, on fresh launches without tools.
|
||||
- Reads from `AGENTS.md` and `.agents/` directory
|
||||
- Recurring task support is limited — no confirmed equivalent of Claude Code's `/loop` command
|
||||
- MCP configuration mechanism differs from Claude Code
|
||||
- Approval policy and sandbox access are separate controls. OpenRig's default
|
||||
`-s workspace-write` selects the sandbox; it does not force `-a`. A member's
|
||||
`codex_config_profile` selects native `-p`, distinct from the AgentSpec `profile`.
|
||||
- On that plain `-s workspace-write` launch (fresh, resume, fork or restore), a
|
||||
short-lived `codex app-server` first reads Codex's own configuration with the
|
||||
seat's executable, home and working directory. OpenRig adds
|
||||
`-c sandbox_workspace_write.network_access=true` only when no configuration
|
||||
layer sets network access and no managed requirement could restrict it. A
|
||||
timeout, error or unrecognized answer adds nothing. Named profiles and full
|
||||
bypass are not read. The read may write Codex's own state files in
|
||||
`CODEX_HOME`, read its login and fetch managed policy, as a Codex start does.
|
||||
- Managed launches pass `-c check_for_update_on_startup=false`, so Codex skips its startup update check; your Codex
|
||||
config is not changed.
|
||||
- Approval mode controlled via launch flags (`-a`, `-s`, `--full-auto`)
|
||||
- Can self-install dependencies from instructions but timer/recurring behavior is not reliably available
|
||||
|
||||
**Pi and OMP:**
|
||||
- Read `AGENTS.md` in the working directory, added to Pi's own system prompt. Pi also reads the one in its agent
|
||||
directory: on a fresh launch, a Pi seat's guidance is merged there, under OpenRig's state, not into the
|
||||
repository's. When that file or its directory is a link, the repository `AGENTS.md` is used instead. A restore keeps
|
||||
the file the seat launched with. OMP keeps its guidance in the repository `AGENTS.md`
|
||||
- Selected skills are projected into the seat's own agent directory under OpenRig's state, not into a shared
|
||||
workspace folder
|
||||
|
||||
For Pi tool-selection diagnosis and deliberate in-place recovery, see
|
||||
[Pi tool-selection recovery](pi-tool-selection-recovery.md). It distinguishes
|
||||
configured, currently intended, registered and active tools, including the TUI/RPC boundary.
|
||||
|
||||
**Terminal seats** (`builtin:terminal`) get no agent or profile layers and no startup proof.
|
||||
|
||||
### Claude compaction preparation directory
|
||||
|
||||
Managed Claude compaction asks the seat to write a restore map before `/compact`. By default,
|
||||
OpenRig uses `<cwd>/.openrig/compaction/preparation/`, falling back to its instance home when
|
||||
that directory cannot be prepared. A filesystem write check cannot establish whether Claude's
|
||||
file-edit permissions allow the seat to write there.
|
||||
|
||||
If the launch workspace is denied by Claude's permissions, select an absolute preparation root:
|
||||
|
||||
```sh
|
||||
rig config set policies.claude_compaction.preparation_root /absolute/path/to/restore-maps
|
||||
```
|
||||
|
||||
Grant the seat file-edit access to that root in its Claude configuration. OpenRig does not change
|
||||
Claude permissions; choose a directory outside any matching deny rule. The corresponding environment
|
||||
variable is `OPENRIG_POLICIES_CLAUDE_COMPACTION_PREPARATION_ROOT`.
|
||||
|
||||
New manual and automatic attempts use `<root>/<session>/<attempt-id>/RESTORE-MAP.md`; an in-flight
|
||||
attempt keeps its original path through the post-compaction restore request. The root gets an
|
||||
exclusive `.gitignore` containing `*`, and the daemon publishes the marked `.tmp` file with an
|
||||
atomic rename. Use a dedicated root: a conflicting
|
||||
ignore file or an unwritable configured directory stops preparation with
|
||||
`preparation_directory_unavailable`, visible in `rig compact <session> --state`, without choosing
|
||||
another location. Fix the directory and explicitly retry. `--skip-map` still skips the map once.
|
||||
|
||||
Reset with `rig config reset policies.claude_compaction.preparation_root` to restore the default.
|
||||
The setting applies to new attempts without restarting the daemon.
|
||||
|
||||
The managed `/compact` command keeps its 600-character budget. When the selected map path or
|
||||
the attempt's compaction instruction does not fit, the existing restore request carries it;
|
||||
it never substitutes a newer preparation root or instruction. An explicitly selected root's
|
||||
published map is also named in that request when the path fit in `/compact`. The default root
|
||||
keeps the existing command/restore handoff. Under `--skip-map`, restore still recognizes an
|
||||
optional map completed at that attempt's path, or says that no map was required.
|
||||
|
||||
The compaction preparation and command-submission tests cover manual and automatic attempts,
|
||||
long paths and instructions, settings changed during preparation, directory failures and
|
||||
late maps after cancellation or expiry. They use synthetic delivery and filesystem fixtures;
|
||||
they do not prove a live Claude session's permission choices or compaction behavior.
|
||||
|
||||
### Codex compaction and sizing
|
||||
|
||||
OpenRig's managed compaction is for Claude Code seats only, by design. That covers `rig compact`, the
|
||||
`policies.claude_compaction.*` automatic policy, and the watchdog jobs that the `managed-compaction` and
|
||||
`apprentice-handover` strategies create. A Codex seat compacts with Codex's own automatic compaction, and OpenRig
|
||||
leaves that to Codex.
|
||||
|
||||
- `rig compact <session>` refuses a Codex seat: "not a Claude (claude-code) seat".
|
||||
- `rig compact-plan` lists a Codex seat as skipped (`codex_not_managed_by_claude_compact_in_place`).
|
||||
- The automatic policy never acts on a Codex seat.
|
||||
- A Codex member with `compaction_strategy: managed-compaction` or `apprentice-handover` gets no watchdog jobs.
|
||||
|
||||
**What Codex compacts at.** Codex describes `model_auto_compact_token_limit` as the "token threshold that triggers
|
||||
automatic history compaction (unset uses model defaults)". Its configuration reference does not say what those
|
||||
defaults are, and the model catalog's `auto_compact_token_limit` is null for the models we checked in Codex 0.162.1.
|
||||
Measure a seat's own compactions before relying on an unset limit.
|
||||
|
||||
**What window the model has.** `codex debug models` prints the catalog. Each model carries `context_window`,
|
||||
`max_context_window` and `effective_context_window_percent`. In Codex 0.162.1, `gpt-5.6-sol` has a `context_window`
|
||||
of 272000 at 95%, which is 258400. That is the `model_context_window` its rollout `token_count` events report.
|
||||
A limit only matters when it is below that effective window. Leave `model_context_window` alone: don't raise it toward
|
||||
`max_context_window` to avoid compaction. Set the limit instead.
|
||||
|
||||
**Setting the limit for seats only.** Put `model_auto_compact_token_limit` in a Codex profile file,
|
||||
`$CODEX_HOME/<name>.config.toml` (`~/.codex/<name>.config.toml` when `CODEX_HOME` is unset), and name it on each
|
||||
Codex member that should use it:
|
||||
|
||||
```toml
|
||||
# ~/.codex/seat-compaction.config.toml
|
||||
model_auto_compact_token_limit = 150000 # choose your own value
|
||||
```
|
||||
|
||||
```yaml
|
||||
codex_config_profile: seat-compaction
|
||||
```
|
||||
|
||||
OpenRig passes `-p seat-compaction` on that seat's fresh, fork and resume launches. Codex layers the profile file on
|
||||
top of the base user config, so only seats that name the profile see the lower limit. A Codex session started without
|
||||
that profile keeps the base `config.toml` in the same Codex home (`$CODEX_HOME/config.toml`, or `~/.codex/config.toml`
|
||||
when `CODEX_HOME` is unset). The `150000` is an example, not a recommendation.
|
||||
|
||||
With the normal launch mode, `codex_config_profile` replaces OpenRig's explicit workspace-write sandbox flag, so the
|
||||
profile must set the sandbox and approvals you want. See
|
||||
[permission precedence and limits](getting-started.md#custom-settings-and-precedence), which also gives Codex's order
|
||||
of precedence between a profile, project settings and CLI overrides.
|
||||
|
||||
**The trade-off.** Every model call carries the whole context so far, so a smaller context makes each call cheaper. The
|
||||
cost is that Codex compacts sooner and more often, so more of the conversation is carried as a summary instead of
|
||||
verbatim. A higher limit keeps more detail and makes each call larger.
|
||||
|
||||
**Related keys.** These also shape compaction. This guide doesn't recommend values for them. See Codex's
|
||||
[configuration reference](https://learn.chatgpt.com/docs/config-file/config-reference):
|
||||
|
||||
| Key | What Codex says it does |
|
||||
|---|---|
|
||||
| `model_auto_compact_token_limit_scope` | `total` (the default) counts the full active context; `body_after_prefix` counts only growth after the carried compaction-window prefix. |
|
||||
| `compact_prompt` | Inline override for the history compaction prompt. |
|
||||
| `experimental_compact_prompt_file` | Loads the compaction prompt override from a file (experimental). |
|
||||
| `tool_output_token_limit` | Token budget for storing individual tool or function outputs in history. |
|
||||
|
||||
When authoring startup content, note which instructions are runtime-specific. For example, an orchestrator that needs a monitoring loop should include instructions like: "If running Claude Code, use `/loop 3m` to periodically check rig health. If running Codex, check rig health at the start of each task cycle instead."
|
||||
|
||||
---
|
||||
|
||||
## Files OpenRig writes into a project
|
||||
|
||||
These are the files OpenRig writes into a seat's working directory, and what `rig down` does with each. Paths are
|
||||
relative to that directory.
|
||||
|
||||
| Path | When OpenRig writes it | What `rig down` does |
|
||||
| --- | --- | --- |
|
||||
| Managed blocks in `CLAUDE.md` (or the rig's `managed_blocks` file, such as `CLAUDE.local.md`) or `AGENTS.md` | Each launch merges the seat's guidance blocks. A fresh Codex launch that takes developer instructions puts them in its launch text instead: in `AGENTS.md` it adds only the seat-skills line, after your yes, and refreshes blocks already there. A fresh Pi seat writes its own agent-directory `AGENTS.md` outside the project, except when that folder or file is a link: then its guidance stays in the repository `AGENTS.md`. | Removes OpenRig's blocks, current or legacy, and keeps everything else. When nothing else is left, it deletes the file if the file began with an OpenRig block, or held only blocks in an older format. A block-only file isn't proof that OpenRig created it. The seat-skills line is kept if you edited it. |
|
||||
| `.openrig/context-collector.cjs` | Claude seats, at launch. | Removes it while its bytes are still the shipped collector's; otherwise keeps it and says so. |
|
||||
| `.claude/settings.local.json` | Claude seats, at launch: OpenRig's status line and its activity hooks are merged in, separately. Your own status line is never replaced, but the activity hooks are still merged beside it. A file that doesn't parse gets neither. | Removes OpenRig's status line and hooks and keeps the file, even if `{}` is all that's left. |
|
||||
| `.claude/settings.local.json` fragments and `.mcp.json` | When a profile or spec declares `claude_settings_fragment` or `claude_mcp_fragment`. | Leaves the merged content in place. |
|
||||
| `.claude/agents/`, `.claude/plugins/<id>/`, `.claude/extensions/<id>/`; `.agents/<id>.yaml`, `.codex/plugins/<id>/`, `.agents/extensions/<id>/` | When the seat's projection includes subagents, plugins or runtime resources (Claude, then Codex). | Leaves them in place. |
|
||||
| `.claude/skills/<id>/`, `.agents/skills/<id>/` | Only for a seat without seat storage. New Claude and Codex seats keep their skills under `$OPENRIG_HOME/state/seats/`, outside the project. | Leaves them in place. |
|
||||
| The managed compaction preparation folder described in [Claude compaction preparation directory](#claude-compaction-preparation-directory), with its `.gitignore` of `*` and each attempt's `RESTORE-MAP.md` | Managed Claude compaction, unless a preparation root is configured elsewhere. | Leaves it in place. |
|
||||
| `.rigged-checkpoint.md` | A restore that didn't resume a seat's native conversation (by choice or not) and had a checkpoint to hand it. | Leaves it in place. |
|
||||
|
||||
`rig down` leaves a file alone while another seat still uses it: a running seat of the same rig, or any seat of
|
||||
another rig that isn't archived. That is decided file by file. A guidance file is kept while another seat's
|
||||
guidance targets the same file. The Claude settings and collector are kept while another Claude seat works in that
|
||||
directory. So another rig's Codex seat keeps `AGENTS.md` without keeping `CLAUDE.md` or the Claude settings. It
|
||||
removes `.openrig/` and `.claude/` only when they are empty.
|
||||
`rig down --delete` does nothing more to project files. OpenRig doesn't edit your project's own `.gitignore`.
|
||||
|
||||
## Patterns and Anti-Patterns
|
||||
|
||||
### Good Patterns
|
||||
@@ -798,89 +293,6 @@ profile → uses.skills: [openrig-user]
|
||||
```
|
||||
This is the minimum effective startup. The agent knows who it is, how the team works, and how to use the rig.
|
||||
|
||||
For seats sharing a working directory, keep each role in its own AgentSpec startup entry:
|
||||
|
||||
```yaml
|
||||
startup:
|
||||
files:
|
||||
- path: guidance/role.md
|
||||
orientation: role
|
||||
delivery_hint: send_text
|
||||
required: true
|
||||
```
|
||||
|
||||
`rig queue whoami --json` reports only the calling seat's explicitly marked role
|
||||
bindings, independently of its current work. The `role.state` is `no-record`,
|
||||
`not-declared`, `missing`, `present`, or `unknown` when identity, storage or the
|
||||
observation is unavailable. Files retain their original `path`, `absolutePath`
|
||||
and `ownerRoot` plus the currently resolved `resolvedPath` and
|
||||
`resolvedOwnerRoot`. Built-in packaged paths follow the running installation;
|
||||
existing development-checkout paths stay where they were recorded.
|
||||
|
||||
`recordedAt` is the startup-context record write/attempt, which happens before
|
||||
launch succeeds. Exact resume can retain an older record. `present` means the
|
||||
resolved file currently exists, not that its bytes still match the spec or that
|
||||
the agent read it. Unmarked startup files and startup actions are not exposed.
|
||||
|
||||
Refocus includes the role path and a re-read reminder with topology or both trees,
|
||||
even if the trace fails. Work-only mode omits it; disabling refocus still disables
|
||||
the entire hook. Custom refocus prose keeps its existing precedence. The hook
|
||||
reuses its work lookup, or makes one bounded lookup when the work node is explicit;
|
||||
a failed or skipped lookup reports `unknown`.
|
||||
|
||||
The shipped kernel seats no longer project their role into shared guidance.
|
||||
Existing user files and legacy role blocks are left intact; this is not a cleanup
|
||||
of already-installed shared blocks. General user-authored guidance projection is
|
||||
unchanged.
|
||||
|
||||
The other shipped agent specs that start a seat with `guidance/role.md` mark that
|
||||
file `orientation: role` too, so `rig queue whoami --json` reports their role and
|
||||
refocus names it. They never projected a role into shared guidance; their startup
|
||||
delivery is unchanged.
|
||||
|
||||
**Removing a leftover shared role block by hand.** A kernel installed before 0.6.5
|
||||
may still hold one seat's old role in its working directory's `CLAUDE.md` (or the
|
||||
rig's configured `managed_blocks` file) or `AGENTS.md`. Upgrading doesn't remove it.
|
||||
`rig down` removes OpenRig's managed blocks from that file when no other running
|
||||
rig uses it, so it stays until then. To remove it sooner, by hand:
|
||||
|
||||
1. Find the exact block: the lines from `<!-- BEGIN OpenRig MANAGED BLOCK: role -->`
|
||||
through `<!-- END OpenRig MANAGED BLOCK: role -->` (in older files, `RIGGED`
|
||||
instead of `OpenRig`). Only the block whose id is exactly `role`; leave
|
||||
`rig-role`, other ids and everything outside the markers alone.
|
||||
2. Confirm each seat that shares that directory has its per-seat replacement:
|
||||
`rig queue whoami --json` run in the seat reports `role.state` as `present`.
|
||||
A seat started before it was upgraded still has the old content in its
|
||||
conversation; restart it fresh if you want it to start from the per-seat role.
|
||||
3. Keep a copy of the file, remove only those lines, and compare the result with
|
||||
the copy: nothing else in the file should differ.
|
||||
|
||||
**The kernel's own folder.** A newly created default kernel works in
|
||||
`<workspace.root>/kernel`, beside your projects (`<workspace.root>/projects` by
|
||||
default) rather than above them. Claude reads `CLAUDE.md` from a seat's folder
|
||||
and from every folder above it, so a kernel working in the workspace root put its
|
||||
guidance into every Claude project seat below it. If that folder can't be made,
|
||||
the new kernel works in the workspace root, as before.
|
||||
|
||||
A kernel that already exists keeps its folders, usually `workspace.root`.
|
||||
Upgrading moves no seat, changes no restored conversation and removes no file.
|
||||
To move an existing kernel yourself, if you want to:
|
||||
|
||||
1. Save the new folder for each kernel seat with `rig seat set-cwd <seat> --cwd
|
||||
<workspace.root>/kernel --reason <text>`, using the absolute path. It creates
|
||||
the folder ([details](#change-an-existing-seats-working-directory)).
|
||||
2. Choose when each seat adopts it. A seat keeps its current folder until it
|
||||
starts again, and the new start doesn't continue its current native
|
||||
conversation: `rig seat launch <seat> --fresh --stop --reason <text>` starts a
|
||||
blank one, and `rig handover <seat> --source rebuild --reason <text>` carries
|
||||
durable context into a new one. Nothing does this for you.
|
||||
3. Tidy the old `<workspace.root>/CLAUDE.md` (or `AGENTS.md`) by hand, once no
|
||||
running rig's seat still works in that folder: a seat there still reads those
|
||||
blocks, and `rig down` checks the same before it removes them. Remove only
|
||||
OpenRig's managed blocks, each from its `<!-- BEGIN OpenRig MANAGED BLOCK: … -->`
|
||||
line through its `END` line, as in the steps above, and keep everything else,
|
||||
including your own text. Don't move or delete the whole file.
|
||||
|
||||
**Separate project context from role**
|
||||
|
||||
Don't put project documentation inside the role guidance. The role is about the agent's function; project context is about what the agent is working on. Use rig-level or pod-level startup files for project context.
|
||||
@@ -899,8 +311,8 @@ This prompts the agent to actually invoke the skills, not just have them as pass
|
||||
## System Check
|
||||
|
||||
After identity recovery, verify:
|
||||
1. `rig ps --nodes` shows your rig running (scoped to your session's rig by default; outside a managed session name it explicitly: `rig ps --nodes --rig <name>`)
|
||||
2. `rig env status <rig>` shows services healthy (if applicable)
|
||||
1. `rig ps --nodes` shows your rig running
|
||||
2. `rig env status` shows services healthy (if applicable)
|
||||
3. Your working directory is correct
|
||||
4. Required tools are available (node, npm, git, etc.)
|
||||
|
||||
@@ -936,12 +348,11 @@ If your rig REQUIRES a hook to function and the hook installation fails silently
|
||||
When creating a new agent's startup experience:
|
||||
|
||||
- [ ] Write a `guidance/role.md` — who is this agent?
|
||||
- [ ] Reference it in `startup.files` with `orientation: role` and `delivery_hint: send_text`. Avoid projecting distinct seat roles into one shared `AGENTS.md` or `CLAUDE.md`.
|
||||
- [ ] Reference it in the agent spec's `resources.guidance` AND `startup.files`
|
||||
- [ ] Write a rig `CULTURE.md` if the rig doesn't have one
|
||||
- [ ] Choose skills from the shared pool via profile `uses`
|
||||
- [ ] Write a `startup/context.md` if the agent needs environment grounding
|
||||
- [ ] Include a system check in the startup context
|
||||
- [ ] Verify the agent spec validates: `rig agent validate agent.yaml`
|
||||
- [ ] Verify the rig spec validates: `rig spec validate rig.yaml`
|
||||
- [ ] Run the advisory authoring check: `rig spec audit rig.yaml`
|
||||
- [ ] Test by launching the rig and checking that the agent received the expected content
|
||||
@@ -1,227 +0,0 @@
|
||||
# Agent state taxonomy
|
||||
|
||||
The formal activity language OpenRig's surfaces render from — the TUI and the `rig ps`
|
||||
ACTIVITY column. One oracle computes it; surfaces render it. Two exceptions remain: the web
|
||||
UI's graph node still derives its own running/needs-input/idle/unknown from the legacy
|
||||
`agentActivity`, and `rig ps --active` / `--filter agentActivity.state` filter on that legacy
|
||||
field. Startup, orientation, restore and attention states are separate vocabularies, listed
|
||||
under "Other state on the same surfaces" below.
|
||||
|
||||
**This document is the canonical text for the shipped taxonomy** and adds the
|
||||
engineering reconciliation; it never forks a second vocabulary. Its typed source of
|
||||
truth is `packages/daemon/src/domain/activity-taxonomy.ts`.
|
||||
|
||||
## The three orthogonal axes
|
||||
|
||||
| axis | values | question it answers |
|
||||
|---|---|---|
|
||||
| session | `present` · `detached` · `exited` · `absent` | does a process exist |
|
||||
| activity | `working` · `idle-at-prompt` · `unknown` (+ needs-input as count+reason, below) | what a present agent is doing |
|
||||
| resumability | `live` · `resumable` · `context-walled` | what a non-present agent can come back as |
|
||||
|
||||
Only the activity axis is served today. The session and resumability values are declared in
|
||||
`activity-taxonomy.ts` but no surface reads them: `rig ps --nodes --full` shows the raw session
|
||||
STATUS, both node views show a LIFECYCLE of `running`, `detached`, `recoverable` or `attention_required` (`run`, `det`, `rec`,
|
||||
`att`), and node inventory serves `occupantLifecycle` (`active`, `retiring`, `retired`,
|
||||
`context_walled`, `compacted`, `crashed`, `unknown`). Node inventory's `sessionStatus` is the
|
||||
recorded session status, with two observed corrections: a running session whose pane is verified
|
||||
to have exited (identity reason `pane_exited`) reads `exited`, with `startupStatus:
|
||||
"attention_required"`, and a running session confirmed missing reads `detached`.
|
||||
|
||||
The axes never blend. Reachability is strict; revival optimism is a different field
|
||||
(omnigent's strict-liveness split). `unknown` is a first-class honest value — the oracle
|
||||
saying it cannot tell. Unknown beats a confident wrong answer.
|
||||
|
||||
### needs-input is a count + reason, never a status value
|
||||
|
||||
Blocked-on-input rides as `{ count, reason }` — the number of outstanding prompts plus a
|
||||
short phrase naming why nothing is moving ("permission prompt", "usage limit",
|
||||
"classifier hold" — provider chrome and usage limits are explicitly in-axis: they are
|
||||
the second cause of production-observed parks). Human surfaces may *display* "needs-input"
|
||||
(the four-value human list) via the one bridge `deriveDisplayActivity`, which
|
||||
renders needs-input whenever `count > 0`; no store or transition ever holds it as a
|
||||
state. Attention/alarm values likewise stay in their own machinery (`attentionCount`).
|
||||
Both herdr and omnigent converged on this exclusion, with warning comments on the
|
||||
collision in both codebases.
|
||||
|
||||
The legacy `agentActivity` field distinguishes a typed provider interruption from
|
||||
a request for a person: fresh Codex `at_limit` and Claude `StopFailure/rate_limit`
|
||||
events carry `state: unknown` and `reason: usage_limit`, never a `needs_input`
|
||||
hook. A permission prompt can independently supply human-input evidence. See
|
||||
[Claude usage-limit detection](claude-status-line.md#claude-usage-limit-detection)
|
||||
for the separate provider signal and queue timer.
|
||||
|
||||
### Per-seat attention in the node inventory
|
||||
|
||||
`rig ps --nodes` carries the daemon's `needsAttention` result, using the same predicate
|
||||
as the rig's `attentionCount`. It also serves `arbitratedNeedsInput`: `true` for observed
|
||||
input, `false` for an observed clear, and `null` when no authoritative needs-input source
|
||||
answered. `needsInputEvidence` retains that source's rung and observation time, or `null`.
|
||||
A zero prompt count alone is not an observed clear. Absent arbitration evidence leaves
|
||||
the existing live-hook fallback in effect; lifecycle, startup, held and error signals
|
||||
can independently raise attention. A false attention flag means no known trigger, not
|
||||
proof that an unobserved input box is clear.
|
||||
|
||||
A handover whose successor started without a required file also raises attention. That's
|
||||
a projected guidance file or a guidance startup file marked `required` still missing from
|
||||
the guidance file, or, for a fresh or rebuild successor, a required `send_text` startup
|
||||
file that's missing. The seat serves `startedWithoutRequired` with each file's path, the
|
||||
table's reason column names each file, and the handover's own output says it's degraded.
|
||||
The mark belongs to that successor's session, so the seat's next launch or handover
|
||||
clears it. Time alone never does. Repairing the file in place doesn't clear it either,
|
||||
because no route delivers guidance into a running conversation.
|
||||
|
||||
A fresh or rebuild successor's first turn opens with the seat's `send_text` startup files,
|
||||
the ones a fresh start delivers, as absolute paths with the role first. A pointer asks the
|
||||
successor to read the file; it isn't the pasted text a start sends. A fork successor
|
||||
carries its parent's conversation and gets no list. `send_text` startup actions, such as
|
||||
an `after_ready` preload, aren't run for a successor.
|
||||
|
||||
The compact human table's ATTN column renders `yes`, `no`, or `?` when the flag is
|
||||
unavailable. Compact JSON retains `needsAttention`; use `--full` or
|
||||
`--fields needsAttention,arbitratedNeedsInput,needsInputEvidence` with `--json` for the
|
||||
provenance. The TUI uses the served flag for attention glyphs and totals, and shows the
|
||||
needs-input evidence in seat detail, including a hook-only prompt. With an older daemon,
|
||||
attention stays unknown while its existing prompt, startup and error hints remain visible.
|
||||
|
||||
## Derived diagnoses — computed at read time, never stored
|
||||
|
||||
- **PARKED** — the disease: `(activity = idle-at-prompt OR needs-input pending) ×
|
||||
(open obligations OR unhealthy HELD rows)`. A dropped baton or an unanswered block. When
|
||||
activity is `unknown` the verdict is `indeterminate`, never parked or not-parked. Queryable
|
||||
at rig and seat level with `rig parked [seat] [--rig <name>] [--json]`; the join lives in the
|
||||
parked query, never inside the oracle (the oracle's non-inference contract: it never reads
|
||||
queue state).
|
||||
- **HELD** — the deliberate counterpart: a queue-level hold with a named owner and an
|
||||
armed wake (`rig view show held`). Stopping on purpose is HELD; stopping by accident is
|
||||
PARKED. A HELD row is healthy only while its wake is live, and not fired-but-unconsumed
|
||||
unless the stuck sweep owns its recovery; an unhealthy HELD row on a stopped seat makes
|
||||
the seat parked.
|
||||
- **DONE-UNSEEN** — finished work whose completion nobody consumed (herdr derives it as
|
||||
idle ∧ unseen). Declared as a concept only; no surface computes it yet.
|
||||
|
||||
## Reconciliation with prior art
|
||||
|
||||
| ours | herdr (`src/detect/mod.rs:11`) | omnigent (`schemas.py:2767`) |
|
||||
|---|---|---|
|
||||
| `working` | `Working` | `running` |
|
||||
| `idle-at-prompt` | `Idle` | `idle` |
|
||||
| needs-input count+reason | `Blocked` + `visible_blocker` override | `pending_elicitations_count` + `blocked_on` phrase — **not** a status |
|
||||
| `unknown` | `Unknown` ("plain shell or unrecognized program") | — |
|
||||
| derived DONE-UNSEEN | derived `Done` = idle ∧ unseen (`src/ui/sidebar.rs:186`) | — |
|
||||
| session axis | server owns the PTY (presence direct) | `runner_online` × `host_online` × `host_resumable` |
|
||||
| — (rejected) | — | `waiting` (turn parked on async drain — collides with Claude's dialog `waiting`; both codebases carry warning comments) |
|
||||
|
||||
## The evidence ladder (rung inventory and retirement conditions)
|
||||
|
||||
State is computed by ONE arbitration point (`SeatActivityService`) from ranked,
|
||||
time-bounded evidence. Rungs, top down, with each rung's retirement condition:
|
||||
|
||||
- **r3 — Claude `sessions/<pid>.json` self-report** (busy/shell/idle/waiting, since
|
||||
v2.1.139): standing, above hooks for working/idle. Undocumented internal — unreadable
|
||||
means fall down the ladder, never error.
|
||||
- **r2 — lifecycle hooks** (Claude SessionStart/UserPromptSubmit/PostToolBatch/Stop/StopFailure/Notification;
|
||||
Codex SessionStart/UserPromptSubmit/Stop/PermissionRequest): standing for Claude; Codex's
|
||||
hook rung starts at trial and is promoted after 50 agreeing observations over at least an
|
||||
hour. Exactly-once turn boundaries; SubagentStop filtered (it
|
||||
can fire post-turn and must never revive an idle seat). Hook-rung authority is
|
||||
TIME-BOUNDED: the hook rung is the only rung that does not self-date its evidence, so
|
||||
persistent cross-rung contradiction degrades it to identity-only, visibly, with a
|
||||
rung-health event.
|
||||
- **r4 — visible needs-input chrome**: standing; outranks self-report for the
|
||||
needs-input signal only (a dialog you can see beats a hook that said working).
|
||||
- **r1 — window-activity sampling** (today's oracle): the fallback tier. Retires
|
||||
PER-HARNESS when that harness's hook rung earns trust through a measured agreement
|
||||
window in production — never by fixture pass alone (symmetric admission: a fixture
|
||||
pass admits a rung to TRIAL, where it is measured but not consulted; production agreement
|
||||
promotes it). For Codex, this fallback also accepts a fresh working status adjacent to
|
||||
the composer, or an advancing counter across two fresh captures when output separates
|
||||
them. This positive evidence expires after five seconds; unrecognized, frozen, old or
|
||||
pre-occupant frames add nothing. It uses the existing pane capture. Undeclared and partial-coverage rungs are identity-only, which is also where
|
||||
a degraded rung goes, with one declared exception: for a pi or omp seat or a community
|
||||
harness, sampling is positive-only: a changing pane reads working, and a quiet one never reads idle on its
|
||||
own, since the pane can be quiet mid-turn.
|
||||
- **pi and omp runner hooks**: OpenRig's runner reads the child's RPC event stream and posts
|
||||
each turn boundary (`agent_start`, `turn_start`, tool calls, `agent_end`; attention). These
|
||||
decide working, idle and needs-input. Unlike Claude and Codex hooks, an idle or attention
|
||||
boundary holds until the next one instead of expiring after 15 s; a working boundary still
|
||||
expires, so a turn whose end was lost reads unknown, not working forever. Posts are taken
|
||||
in the runner's own order (time, then its sequence), so an older boundary arriving late
|
||||
doesn't win. The runner's report that its child
|
||||
exited reads unknown, not idle. A boundary the runner didn't see (if a runtime ended a turn
|
||||
without `agent_end`) leaves the seat working up to the 15 s window, then unknown, never idle. After a
|
||||
daemon restart, a runner seat takes its last recorded boundary from the hook store, for its
|
||||
current session and occupant generation only, so an idle seat still reads idle before it
|
||||
posts again, provided that report is fresh: a pi runner's idle boundary states an interval
|
||||
(below) and lapses to unknown once it is older than two of them.
|
||||
A pi runner also posts an idle `Stop` (`idle_heartbeat`) every three minutes while it waits, and states that
|
||||
interval as `livenessSeconds` on every `Stop`: a newly ready runner reads idle at once, and an idle boundary that
|
||||
carries an interval holds for two of them, then lapses to unknown, so a runner killed without saying so stops reading
|
||||
idle. Silence never clears a pending attention prompt, and a runner that states no interval (omp, an older pi
|
||||
runner) keeps the hold-until-the-next-boundary behaviour.
|
||||
|
||||
Claude permission notifications raise needs-input immediately. A visible permission can clear
|
||||
after it has been observed and then disappears. A notification whose dialog was never observed
|
||||
can also clear when Claude reports `PostToolBatch`: every tool in that actor's batch has resolved,
|
||||
so its earlier permission requests are no longer pending. The relay carries the native actor
|
||||
identity; a subagent or in-process teammate finishing its batch cannot clear the main session's
|
||||
request, and overlapping actors remain separate. An individual `PostToolUse` is insufficient
|
||||
because another tool in the batch may still be waiting for permission.
|
||||
|
||||
Batch completion must come from the current occupant and have a later source timestamp than
|
||||
the matching notification. Missing actor attribution from an older relay, equal or older
|
||||
timestamps, and another actor's completion retain attention. An unrecognized or missing screen
|
||||
does not expire attention by age. `Stop` and `UserPromptSubmit` remain the turn-boundary fallback.
|
||||
The oracle remembers each actor's latest completed batch for the current occupant, so a strictly
|
||||
earlier notification arriving late cannot restore the mark, even if the completion arrived first.
|
||||
A notification with an equal or later timestamp remains eligible; swapping or retiring the
|
||||
occupant discards its remembered completions.
|
||||
Completing a batch changes the needs-input signal without inferring that the seat is idle or
|
||||
overwriting a later elicitation. The hook contract is documented in
|
||||
[Claude's hooks reference](https://code.claude.com/docs/en/hooks#posttoolbatch).
|
||||
|
||||
Partial-coverage honesty: a source with partial lifecycle coverage gets identity-only
|
||||
trust, unless its inventory declares it positive-only. A positive-only source can report
|
||||
working and nothing else; its silence reads unknown, never idle. The ladder is also the migration path — new oracles slot in above existing
|
||||
heuristics, lower rungs retire a release at a time, consumers never see a flag day.
|
||||
|
||||
Seat-keyed: state attaches to the durable seat nodeId, never the occupant. A handover or
|
||||
generation swap is its own visible event — never an activity transition — and triggers
|
||||
rung-inventory re-declaration: a successor never inherits its predecessor's rung
|
||||
authority (hooks live in occupant config while the pane is seat-persistent).
|
||||
|
||||
## Other state on the same surfaces
|
||||
|
||||
These are separate vocabularies, not activity values. `rig ps --nodes --full` shows them in
|
||||
its STARTUP, ORIENTED, LIFECYCLE and RESTORE columns (the default compact view shows only
|
||||
LIFECYCLE); see the
|
||||
[startup guide](agent-startup-guide.md) for how each arises.
|
||||
|
||||
- **Startup status:** `pending`, `ready`, `attention_required`, `failed`. `ready` means
|
||||
startup finished, never that the seat is oriented, and a seat isn't marked `ready` while
|
||||
its startup context is still pending for `rig seat continue`.
|
||||
- **Oriented:** `verified`, `missing`, `rejected`, `n-a`, from the startup proof.
|
||||
- **Attention codes:** `trust_gate`, `hook_trust_gate`, `update_gate`, `login_required`,
|
||||
`mcp_gate`, `bypass_consent_gate`, `codex_auth_refusal`, `codex_client_incompatible`.
|
||||
- **Restore outcome:** `resumed`, `fresh-primed`, `awaiting-decision`, `attention_required`,
|
||||
`failed` (plus `rebuilt`, `operator_recovered` and the legacy `fresh`). A seat joined with
|
||||
`rig attach` as an external CLI reads `left_attached`: restore leaves it as it is.
|
||||
An `attention_required` outcome, or `failed` with a running session, makes the seat's
|
||||
lifecycle `attention_required`, whatever its startup status, so it raises `needsAttention`.
|
||||
After a full restore, the daemon reconciles it to `operator_recovered` when the seat's
|
||||
activity next turns to running or idle (or reads so when the restore completes) and the
|
||||
live seat passes its check, and `rig seat clear-attention <seat>` runs that check on
|
||||
demand; `rig seat continue` doesn't change it
|
||||
([startup guide](agent-startup-guide.md#after-a-restored-seats-prompt)).
|
||||
|
||||
## Rejected oracles — dated receipts (2026-08-26), so nobody re-derives them expensively
|
||||
|
||||
- **transcript-quiescence as an activity oracle: REJECTED.** omnigent's 1-second version
|
||||
oscillated on mid-turn lulls and idempotently locked out real completions; its
|
||||
surviving 5-second variant is annotated a stopgap awaiting a hook replacement.
|
||||
Distinction held: transcript growth remains FINE as the refocus hook's cadence
|
||||
trigger — a different purpose, and the one permitted consumer.
|
||||
- **pane-scraping as primary truth: REJECTED.** herdr — whose TOML screen manifests are
|
||||
the state of that art — subordinates them to hooks wherever hooks cover the lifecycle,
|
||||
and marks viewer screens `skip_state_update` because scrollback lies. Pane reading
|
||||
stays fallback-tier only.
|
||||
@@ -1,74 +0,0 @@
|
||||
# Integration author contracts — DRAFT, first published with 0.7.6
|
||||
|
||||
OpenRig composes agents into a team. An integration author supplies the pieces that let a new harness participate, or let an existing harness run in a new environment. This guide divides that work into two contracts:
|
||||
|
||||
1. **[Harness and runner](harness-runner-contract.md):** launch the agent, report its conversation identity and activity, deliver input, handle permission requests and cancellation, and reopen the requested conversation.
|
||||
2. **[Execution and platform](execution-platform-contract.md):** choose placement, deliver files and credential references, distinguish owned from attached resources, preserve native state, return artifacts and reconcile shutdown.
|
||||
|
||||
These are draft author responsibilities. Some use implemented OpenRig interfaces; others describe a proposed integration whose code remains with its author. Including the guide in release documentation does not make every described capability available.
|
||||
|
||||
## Release and support status
|
||||
|
||||
This draft was published with **OpenRig 0.7.6** (tag [`v0.7.6`](https://github.com/mvschwarz/openrig/tree/v0.7.6), source `c00c6321`). Its implemented-interface baseline is commit [`b6bd4633`](https://github.com/mvschwarz/openrig/tree/b6bd4633fbd94634a2b4bdef2574a6880f97be63). That base includes the runner declaration and `passes_model` changes. The published 0.7.6 adds release repairs and documentation on top of that base. **OpenRig 0.7.8** adds seat activity grants, OpenRig's side of the answered permission path and the maintained OpenShell reference driver (`rig environment`); the rows below that say 0.7.8 describe it. References below use the immutable base instead of a moving branch, except the community preset reference, which is the in-tree copy.
|
||||
|
||||
| Area | Status | What the status means |
|
||||
|---|---|---|
|
||||
| Community launcher presets, RigSpec members and AgentSpec role/startup declarations | Implemented interfaces | They compose configuration. A launcher must implement its declared effects; successful parsing does not prove delivery or native behavior. |
|
||||
| `runner: true` with `launch_result: true` | Released in 0.7.6 | The declaration is retained at launch, and Core uses the runner identity/activity path. It does not supply a runner executable. |
|
||||
| `passes_model: true` | Released in 0.7.6 | Passes the declared model through `OPENRIG_MODEL`. It does not observe or verify the effective native model. Cursor and Hermes examples leave it off pending native qualification. |
|
||||
| The dsh/ACP input, cancellation and answered permission recipe (a person answers, and the default is permit) | OpenRig's side released in 0.7.8; runner side shown on two runners, not qualified in general | OpenRig asks a person for a runner's permission requests and returns the answer. In 0.7.8's release testing on a Claude Haiku model, dsh-seat 0.3.0 (dsh 0.2.1-rc.2 over ACP) showed input, activity, cancel, exact resume and shutdown, and the dsh example runner showed a person's allow-once and reject-once; see the [runner contract](harness-runner-contract.md)'s status for what wasn't shown. Native reopen capabilities and the tools-off case remain implementer questions. |
|
||||
| Exact native resume via an author launcher | Existing interface; integration-specific evidence | A requested conversation must reopen exactly or fail honestly. One provider/harness result does not establish portability to another. |
|
||||
| Environment preparation, projection, artifact export and owned-resource cleanup | OpenShell reference driver released in 0.7.8; generic seam proposed | The platform chapter makes the responsibilities reusable. Core ships one maintained reference driver, OpenShell 0.1.6, as `rig environment`. It doesn't provide a generic platform driver or `DeploySpec` loader. |
|
||||
| `HarnessSpec`, `DeploySpec`, generic lifecycle fields and wake-on-demand declarations | Proposed vocabulary or unsupported configuration | They are not new accepted YAML keys. Use existing composition and an explicit author implementation. |
|
||||
| Cross-host native resume, provider-wide cancellation or permission support | Unresolved unless qualified by an integration | Unsupported and untested are distinct from observed failure. Publish the actual boundary and version. |
|
||||
|
||||
## How the pieces compose
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
R["RigSpec: members, relationships, work arrangement"] --> A["AgentSpec: role, context and startup declarations"]
|
||||
R --> H["Built-in runtime or community preset"]
|
||||
H --> N["Harness / runner: native conversation and control"]
|
||||
H --> P["Author platform integration: placement and resources"]
|
||||
P --> N
|
||||
P --> F["Workspace, retained state and returned artifacts"]
|
||||
R --> S["Optional application services"]
|
||||
```
|
||||
|
||||
**RigSpec remains the composer.** AgentSpec describes reusable agent intent; the runtime or preset selects how the native agent starts. An integration can surround that launch with an environment-specific wrapper. A bundled application's database or Compose service is software the team operates; it is distinct from the environment in which an agent executes.
|
||||
|
||||
The terminal provider, native harness protocol and network connection also have different jobs. A terminal provider carries input and output; a runner understands native turn boundaries; a granted MCP or other connection exposes coordination operations. An HTTPS connection does not by itself provision compute, awaken a suspended application or establish native resume. Each integration states which of those effects it implements.
|
||||
|
||||
| Owner | Responsibility |
|
||||
|---|---|
|
||||
| OpenRig Core | Team and work identity, queue records, existing launcher contracts, activity interpretation and declared configuration. |
|
||||
| Harness/runner author | Native launch and protocol, exact native reference, prompt framing, truthful activity, permission/cancel behavior and native resume. |
|
||||
| Platform integration author | Placement binding, destination projection, resource ownership, state transfer, artifact delivery and resource reconciliation. |
|
||||
| Operator / credential owner | Selected environments, grants, provider credentials and spending authority. |
|
||||
|
||||
The queue records who owns work. It does not prove that an old provider process has stopped, that a new runtime consumed its history, or that guest bytes reached the consumer. Those observations belong in the launch, resume and export receipts alongside the work record. This division reuses the queue rather than creating another task system.
|
||||
|
||||
## Start from an existing example
|
||||
|
||||
For a terminal harness, begin with the [community preset reference](community-harness-presets.md) and the pinned [Cursor](https://github.com/mvschwarz/openrig/tree/b6bd4633fbd94634a2b4bdef2574a6880f97be63/docs/reference/community-harness-examples/cursor) or [Hermes](https://github.com/mvschwarz/openrig/tree/b6bd4633fbd94634a2b4bdef2574a6880f97be63/docs/reference/community-harness-examples/hermes) directory. Their presence is source guidance, not an effective-model or complete integration guarantee.
|
||||
|
||||
For a structured runner, the [runner chapter](harness-runner-contract.md#2-declare-it-what-the-preset-says-fixed-at-launch) supplies a minimal preset, launch record and activity message. The executable named by the preset must still be implemented and installed by its author.
|
||||
|
||||
For a contained worker, the [platform composition](execution-platform-contract.md#a-minimal-composition-using-existing-surfaces) pairs an ordinary coordinator with a worker preset. It uses existing member and preset fields. The wrapper must prepare or attach to the selected environment explicitly; declaring a runtime does not provision it. Its illustrated member list is a fragment, and its launcher is a skeleton, not a ready-to-run provider package.
|
||||
|
||||
A rig bundle can carry declarations, roles and supporting scripts. Do not confuse those files with hydrated image layers, installed binaries, registered presets, credentials or live sessions. State what the recipient must supply.
|
||||
|
||||
## Lifetime and recovery
|
||||
|
||||
**Persistent** and **Non-persistent** describe execution lifetime. A non-persistent invocation has an explicit completion condition tied to its assigned work. **Durable** describes interruption recovery and is independent of that lifetime. An agent can run briefly and still resume its retained conversation later.
|
||||
|
||||
Retaining a workspace and reconstruction material can restore useful competence without restoring the same native conversation. Label that as reconstruction. Native resume additionally requires the correct harness-qualified reference and supported native state. The platform chapter's measured example restored copied history into recreated containers on the same host; it does not establish new-host migration or RAM restoration.
|
||||
|
||||
## Questions contributors can help resolve
|
||||
|
||||
- At a pinned dsh version, which advertised method reopens a session, and can a useful first case run without permission requests?
|
||||
- Which additional harness would justify a shared protocol runner rather than a separate wrapper?
|
||||
- Which provider effects can use the existing projection, native-reference and lifecycle dispatch, and which need a new maintained interface?
|
||||
- What exact state and ownership must travel when compute is recreated or moved?
|
||||
|
||||
These questions remain part of the draft. An unanswered contributor thread does not prevent authors from using the implemented interfaces or reviewing the proposed responsibilities. Compatibility claims should name versions, observed behavior and unsupported operations; they should not imply a support guarantee from a manifest alone.
|
||||
@@ -1,49 +0,0 @@
|
||||
|
||||
## Browser access and allowed addresses
|
||||
|
||||
The OpenRig daemon answers its API only when a request is addressed to this machine and, if it
|
||||
comes from a web page, only when that page is the OpenRig UI or a page you allowed.
|
||||
|
||||
### What works with no setup
|
||||
|
||||
- The CLI, the TUI and agents on this machine, addressing the daemon by `localhost` or an IP
|
||||
address.
|
||||
- Other OpenRig hosts and clients that reach this daemon by its IP address, its own hostname, or
|
||||
its own Tailscale name (for example `my-machine.your-tailnet.ts.net`, or the short `my-machine`).
|
||||
The daemon learns its own Tailscale name automatically from Tailscale on this machine.
|
||||
- The web UI, when it is turned on (`rig config set ui.enabled true`, then restart the daemon) and
|
||||
opened over `http` on the daemon's own port at any address the daemon accepts, including its
|
||||
Tailscale name. A UI page served any other way, such as over `https` through your own domain,
|
||||
also needs its exact origin in `OPENRIG_ALLOWED_ORIGINS`, then a daemon restart.
|
||||
|
||||
### When you see a refusal
|
||||
|
||||
The daemon answers `403` with a sentence that says what to do. There are two kinds:
|
||||
|
||||
- **The address is not recognized** (`untrusted_host`). You reached the daemon by a name it does not
|
||||
know as its own, such as a custom DNS name, an alias in `/etc/hosts`, or a reverse proxy domain.
|
||||
Use `localhost`, an IP address or the machine's own name instead, or list the name you use:
|
||||
`OPENRIG_ALLOWED_HOSTS=rig.example.com` in the daemon's environment, then restart the daemon.
|
||||
A client that presents the daemon's configured bearer token is not refused for its address
|
||||
(it still needs whatever access each route requires).
|
||||
- **The web page is not allowed** (`browser_origin_refused`). A browser page that is not the
|
||||
OpenRig UI at the daemon's own address called the API. This includes another local app or
|
||||
development server on a different port. To allow a page, list its exact origin:
|
||||
`OPENRIG_ALLOWED_ORIGINS=http://localhost:5173` in the daemon's environment, then restart the
|
||||
daemon. A bearer token does not override this.
|
||||
|
||||
`OPENRIG_ALLOWED_HOSTS` lists names you use to reach the daemon. `OPENRIG_ALLOWED_ORIGINS` lists
|
||||
web pages allowed to call it. They are separate: a UI served through your own domain usually needs
|
||||
both. Separate several entries with commas. Pages on other origins cannot read the refusal; the
|
||||
message is for you, the CLI and agents.
|
||||
|
||||
### Limits
|
||||
|
||||
- If you rename the machine in Tailscale, the daemon keeps accepting its previous name until it
|
||||
restarts or until it next looks its name up after seeing an unrecognized one.
|
||||
- If the daemon has not reached Tailscale's resolver since it started, its Tailscale name is
|
||||
refused until a lookup succeeds. A later failed lookup keeps the name it already learned.
|
||||
`localhost` and IP addresses keep working.
|
||||
- This protects against unknown web pages and unknown names. It is not complete browser isolation:
|
||||
a page on another site can still cause a simple read request to an address the daemon accepts,
|
||||
although it cannot read the answer. Keep the daemon on loopback or your tailnet.
|
||||
@@ -1,209 +0,0 @@
|
||||
# Bundle formats (v1)
|
||||
|
||||
The shared data formats for sharing rig bundles: the configurations a bundle offers, the before-install view of what it
|
||||
will do, run records and the public status file, and registry entries. The schemas are in [`schemas/`](schemas/), with
|
||||
fixtures that pass and fail them in [`schemas/fixtures/`](schemas/fixtures/). Tools outside this repository read them
|
||||
at a pinned commit.
|
||||
|
||||
**A v1 format only grows:** new optional properties may appear, and consumers ignore properties they don't know. Any
|
||||
other change is a v2. The schemas are open to new properties, with three closed on purpose: the package digest (an exact
|
||||
value), a run record's subject (a harness check can never carry a team's identity), and the public status file, where
|
||||
nothing outside the schema may appear, so a private field can't leak in. A new property in a closed part is a v2. The
|
||||
status schema also rejects labels that contradict their evidence: a tested label needs records and a package, tested
|
||||
with help needs its count, tested can't carry a count above zero, and an unreadable listing carries only Status
|
||||
unavailable.
|
||||
|
||||
## Three identities, kept apart
|
||||
|
||||
| What | Name | Defined by |
|
||||
|---|---|---|
|
||||
| What someone chose | configuration ID | the mapping of every seat to a runtime |
|
||||
| What was packaged | package digest | the built archive's files |
|
||||
| What actually ran | execution binding | a run record (resolved resources, settings, environment, unknowns) |
|
||||
|
||||
None stands in for another. A package digest doesn't describe what ran, and a configuration ID doesn't describe what was
|
||||
packaged.
|
||||
|
||||
### Source identity
|
||||
|
||||
A bundle on GitHub is named by `repository` (`https://github.com/<owner>/<repo>`, no credentials), `folder` (the
|
||||
repo-relative path of the folder holding `rig.yaml`, or `.` for the repository root) and `resolvedCommit` (40 lowercase
|
||||
hex).
|
||||
- `requestedRef` (the branch, tag or commit someone gave, or `HEAD` for a link without `/tree/…`) and `canonicalUrl`
|
||||
(`…/tree/<resolvedCommit>/<folder>`, with each folder segment percent-encoded and no folder part for the root)
|
||||
explain it, and aren't part of it.
|
||||
- A built bundle records its source in its manifest's provenance, which is outside the package digest.
|
||||
|
||||
### Configuration ID
|
||||
|
||||
Every member's `pod.member=runtime`, sorted by `pod.member` in UTF-16 code-unit order, joined with `,`. There's no
|
||||
whitespace, and runtimes are spelled as in `rig.yaml`. For example:
|
||||
|
||||
```text
|
||||
dev.build=claude-code,dev.qa=claude-code,dev.review=codex,orch.lead=codex
|
||||
```
|
||||
|
||||
Preset names such as `recommended` or `all-claude` are aliases shown beside the ID, never part of it.
|
||||
|
||||
### Package digest
|
||||
|
||||
`{ algorithm: "sha256", value, coverage: "openrig.package-digest/v1" }`:
|
||||
- `value` is SHA-256 over UTF-8 lines `<quoted path>\t<sha256>\n`, one for each entry of the built archive's
|
||||
`integrity.files`, sorted by path in UTF-16 code-unit order;
|
||||
- `<quoted path>` is the path as a JSON string, exactly as JavaScript's `JSON.stringify(path)` writes it, so a tab or
|
||||
newline in a file name can't make two different file lists hash the same. In words: wrapped in `"`; `"` and `\`
|
||||
escaped with a backslash; U+0008, U+0009, U+000A, U+000C and U+000D written `\b`, `\t`, `\n`, `\f` and `\r`; any
|
||||
other character below U+0020, and any lone surrogate, written `\uXXXX` in lowercase hex; everything else as itself;
|
||||
- it's the same when the same folder is rebuilt by the same OpenRig, while the archive's own hash changes with every
|
||||
build (`createdAt`).
|
||||
|
||||
**Coverage, shown wherever the digest is:** the bytes of every packaged file. It doesn't cover:
|
||||
- `bundle.yaml`, so no manifest field, provenance or `createdAt`;
|
||||
- the names the integrity walk skips (`.DS_Store`, `Thumbs.db`, `.gitkeep`);
|
||||
- file modes;
|
||||
- anything resolved on the installing machine at launch, for example an `openrig-home:` plugin, catalog skills, or a
|
||||
model or effort nothing declares.
|
||||
|
||||
**A build result** carries `source` (`null` unless the bundle was built from a GitHub link), `configurationId`,
|
||||
`packageDigest`, `archiveHash` and `assembler`. `rig bundle inspect` reports the same fields for an archive, and any the
|
||||
archive doesn't state are `null`. `assembler` is `{ openrigVersion }`: the format also allows a `commit`, which this
|
||||
OpenRig doesn't write. `configurationId()` and `packageDigest()` are exported from `@openrig/daemon/bundle-identity`.
|
||||
Shared test vectors are in `schemas/fixtures/identity-vectors.json`.
|
||||
|
||||
## The formats
|
||||
|
||||
| Format | Schema | Lives | Written by |
|
||||
|---|---|---|---|
|
||||
| Declared configurations | `bundle-configurations.v1` | `configurations.yaml` beside `rig.yaml` | the bundle's author |
|
||||
| Before-install view | `bundle-behaviour.v1` (`openrig.bundle-behaviour/v1`) | the registry, one file per configuration and assembler version; never inside the archive | the daemon's inspect: `rig bundle inspect`, under `behaviour` with `--json` |
|
||||
| Run record | `run-record.v1` | beside its receipt, private | Fleet, Dev QA, and maintainers for community reports |
|
||||
| Public status | `bundle-status.v1` | `openrig-registry` status, generated | the status generator only |
|
||||
| Registry entry | `registry-entry.v1` | `openrig-registry/registry/<slug>.yaml` | maintainers, through review |
|
||||
|
||||
### Declared configurations
|
||||
|
||||
`seats` gives a `pod.member` the runtimes it may use, and the profile each runtime uses. A member it doesn't list keeps
|
||||
the runtime `rig.yaml` gives it. `presets` names full mappings, and `recommended` names one of them, which must be
|
||||
`rig.yaml` as written. `rig bundle create`, `rig bundle configurations` and `rig bundle check` refuse a file that breaks
|
||||
these rules.
|
||||
|
||||
`--preset` and `--seat` choose a configuration (see the [rig bundle reference](rig-bundle.md)). Any mix of each seat's
|
||||
declared runtimes can be built, not only the presets; a runtime a seat doesn't declare is refused, and nothing is built.
|
||||
Without either flag, create builds `rig.yaml` as written. The built archive's manifest records
|
||||
`configuration: { id, preset }` outside the package digest; `preset` is there only when `--preset` or `--seat` chose a
|
||||
mapping that matches a declared preset.
|
||||
|
||||
### Before-install view
|
||||
|
||||
It's derived from the archive's files alone. Nothing is launched, probed or fetched. It's a view, never a gate, and
|
||||
carries no tested status.
|
||||
- **`identity`:** source, configuration ID, package digest, the archive's assembler, the generator that made the view,
|
||||
stated compatibility, stated provenance (not verified), and integrity (self-consistency, not authorship). A source,
|
||||
configuration ID, package digest or assembler the archive doesn't state is `null`; unstated compatibility and
|
||||
provenance are empty objects. None of it is filled from the inspecting OpenRig.
|
||||
- **`team`, `posture`, `toldFiles`, `alsoRuns`, `writes`, `outsideAddresses`, `needs` and `unknownBeforeLaunch`**, in
|
||||
that order. Facts cite the archive file they come from (`sourceRefs`), except `team` entries and the bundle-wide
|
||||
unknowns. `toldFiles` and `alsoRuns` also say how each resolves: `archive`, `host_at_launch` or `unresolved`.
|
||||
- Posture separates what the archive declares from the product's default. Its effect on the host is `unknown`,
|
||||
because host settings decide it.
|
||||
Optional `posture[].permissionPrompts` is `off` for declared `full_bypass` (including rig- or member-level
|
||||
`builtin:yolo`) on any runtime; for Claude/Codex, `on` for a declared flag-surface floor policy or `default` when
|
||||
no policy is declared (prompts on unless the installing machine's settings turn them off); for Codex, `on` for
|
||||
declared `auto`. Omit it for Claude under auto (naming auto mode in `selection`), config-surface policies
|
||||
(`builtin:locked`, `builtin:standard`, `builtin:open` or an archived `surface: config` policy), host-resolved
|
||||
Codex profiles, `permission_policy: none`, a policy the archive can't resolve, and every runtime other than Claude
|
||||
and Codex (Pi, terminal) except declared full bypass. `selection` names the declared policy.
|
||||
An absent field means not stated, never `on`. For `off`, Claude
|
||||
bypasses permissions, Codex runs with full access and never asks, and Pi gets full resource trust (`--approve`,
|
||||
not a permission mode). These are archive facts; `nativeEffect` remains `unknown`.
|
||||
Optional `posture[].firstRunWarnings.claudeBypass: "harness_asks_once"` names the first-run bypass warning for a
|
||||
declared full-bypass Claude seat, conditional on whether the harness has remembered acceptance.
|
||||
`posture[].nonInterruptive: "available"` says a full-bypass Claude/Codex seat can use
|
||||
[non-interruptive launch flags](non-interruptive-mode.md). It does not select the option, read host acceptance
|
||||
state or prove a prompt-free launch. Both fields are omitted where inapplicable or unresolved; older v1 views
|
||||
remain valid without them. Optional `posture[].nonInterruptiveDefault` (boolean) carries the rig's authored
|
||||
`non_interruptive` for those seats; an explicit launch choice overrides it. When it is `true`, `firstRunWarnings`
|
||||
is omitted. Pi has no additional warning-suppression flag. `nativeEffect` stays `unknown`.
|
||||
- An empty list means none are known. Something unknown goes in `unknownBeforeLaunch`.
|
||||
- Per-seat facts carry `seat` (`pod.member`) and human output labels them with that identity. It's required on
|
||||
`team` and `posture`. Elsewhere it's optional: bundle-wide facts (services, library and project writes, setup and
|
||||
version needs) omit it, and older generators may not have stated it.
|
||||
- **`not_generated`:** a combination without a generated view, or an archive the generator can't describe: schema 1, no
|
||||
readable pod-aware rig spec, a member missing its pod, id or agent reference, a runtime that depends on an AgentSpec
|
||||
the archive can't read, or an error while reading. It carries a reason and the local command that shows it, and no
|
||||
sections.
|
||||
|
||||
### Declared setup preconditions
|
||||
|
||||
The author's `bundle.yaml`, beside `rig.yaml`, may declare setup that the user must do before installing:
|
||||
|
||||
```yaml
|
||||
preconditions:
|
||||
- name: Run inside an OpenRig source clone with dependencies installed and packages built.
|
||||
commands:
|
||||
- git clone https://github.com/mvschwarz/openrig.git
|
||||
- cd openrig
|
||||
- npm ci
|
||||
- npm run build
|
||||
- name: Sign in to the selected coding runtimes before starting the team.
|
||||
```
|
||||
|
||||
Local-folder creation and GitHub-link imports carry these declarations into the archive manifest. A generated
|
||||
before-install view lists each as `needs[]` with `kind: precondition`, the author's `name`, optional ordered `commands`,
|
||||
`status: not_checked`, and a `sourceRefs` entry naming `bundle.yaml` and `preconditions[index]`. Inspect, plan and apply
|
||||
previews show the same declaration. A legacy schema-1 archive retains it in the manifest, but its view remains
|
||||
`not_generated`. Missing preconditions mean none were declared, not that no setup is needed.
|
||||
|
||||
Each command is one plain command line, without shell operators (`;`, `&&`, pipes, command substitution, backticks or
|
||||
redirection). Run them in order in **one shell**, then run the install command in that same shell: it inherits the
|
||||
working directory left by the setup commands. There is no separate `workdir` field. Commands are untrusted author data;
|
||||
a site building a copyable block must validate every line and bound the number of entries, omitting all setup lines
|
||||
if any fails its plain-command rule. OpenRig displays these declarations; it does not execute them, verify that setup
|
||||
was done, or add a prerequisite gate. The archive hash covers the manifest; `packageDigest` excludes `bundle.yaml`
|
||||
and therefore does not cover these declarations.
|
||||
|
||||
At creation, malformed preconditions produce a warning and the whole block is omitted; other author-field validation
|
||||
is unchanged. Shell operators in otherwise valid command lines produce a warning, but the declarations are retained
|
||||
as data. An already-built archive with malformed preconditions reports them as unknown before launch.
|
||||
|
||||
### Run records and status
|
||||
|
||||
**A run record's subject** is either a team (source, configuration ID, package digest, assembler) or a harness check (a
|
||||
runtime). A harness check never upgrades a team label.
|
||||
|
||||
**A run record's `environment.platform` and `arch`** are Node's `process.platform` (`linux`, `darwin` or `win32`) and
|
||||
`process.arch` (`x64` or `arm64`): the values a status platform key can carry, so every valid record can be labelled.
|
||||
|
||||
**Records are private.** Only `outcome.publicNote` (a short public cause) reaches the status file.
|
||||
|
||||
**Relations:** `supersedes`, `withdraws` and `resolves`. Reusing earlier evidence for a changed package is a reviewer's
|
||||
decision, recorded in the registry entry's `evidenceReuse`.
|
||||
|
||||
**The status file** is generated from records by rule `openrig.status-rule/v1`:
|
||||
- **Labels:** `known_problem`, `tested`, `tested_with_help`, `partly_tested`, `not_tested` and `status_unavailable`.
|
||||
They're shown as Known problem, Tested by OpenRig, Tested with help (N), Partly tested, Not tested by OpenRig
|
||||
and Status unavailable.
|
||||
- **A label belongs to one configuration ID.** No configuration is ever shown another configuration's label.
|
||||
- **Empty `platforms`** means Not tested by OpenRig. So does a custom configuration, one the listing doesn't offer.
|
||||
- **Status unavailable is only for a listed configuration** whose records can't be read, or that's missing from the
|
||||
status file; never Not tested by OpenRig. A listing whose records can't be read is `status_unavailable` as a whole.
|
||||
- **Platform keys** are `<os>-<arch>` (`linux`, `darwin` or `win32`; `x64` or `arm64`).
|
||||
- **`tested` means no help was needed:** it can't carry an assistance count above zero.
|
||||
- **An assist** is one action a person or the test runner took to get past a problem during the run, such as restarting a seat or re-running a command; following the bundle's own documented steps doesn't count.
|
||||
- **The file holds no private paths, host names, row IDs, account names or receipt text,** and regenerating it gives
|
||||
identical bytes.
|
||||
- **`bodyDigest`** is SHA-256, lowercase hex, over the RFC 8785 (JCS) canonical JSON, as UTF-8, of an object holding
|
||||
every other top-level field. The site import recomputes it and rejects a hand-edited file. `harnessChecks` has its own
|
||||
`state`, so an unreadable harness-check record reads Status unavailable too.
|
||||
|
||||
### Registry entries
|
||||
|
||||
Each entry records:
|
||||
- the source at its reviewed commit;
|
||||
- each offered configuration with its package digest, assembler and behaviour-view file;
|
||||
- optional `evidenceReuse`;
|
||||
- the review date, and `listed` or `withdrawn` (with `withdrawnOn`).
|
||||
|
||||
v1 has no author field: a listing shows the owner of `source.repository`.
|
||||
|
||||
A listing shows "Reviewed for listing on `<date>` at commit `<short>`. Review is not a security audit."
|
||||
@@ -1,129 +0,0 @@
|
||||
# The Chain-File Convention (CE-v2)
|
||||
|
||||
**This document is the SSOT for chain files**: what they are named, what each
|
||||
altitude carries, and how the leaf-to-root trace resolves. Both trees' walkers
|
||||
consume the paths defined here. If a walker and this document disagree, one of
|
||||
them has a defect — report it; do not fork the convention.
|
||||
|
||||
## The one rule
|
||||
|
||||
**One filename, identical at every altitude of a tree.** A reader orients by
|
||||
walking from where they stand toward the root, reading the same-named file at
|
||||
each level. No pointers to follow, no branching, no per-level names. Inventing
|
||||
a level-specific name breaks every walker.
|
||||
|
||||
Chain files sit on **nodes** (an instance, a rig, a pod, a seat; a mission, a slice)
|
||||
— never on shelves (`rigs/`, `seats/`, `missions/` are containment, not
|
||||
nodes, and carry no chain file).
|
||||
|
||||
## Two trees, two questions
|
||||
|
||||
| tree | path shape | carries | ships? |
|
||||
|---|---|---|---|
|
||||
| **Topology** | instance → rig → pod → seat | how work is done here | **yes** — generally applicable defaults ship in source |
|
||||
| **Project** | project → mission → slice | what is being built | **no** — depends on what the user builds; see below |
|
||||
|
||||
### The topology tree
|
||||
|
||||
Rooted at the typed config key **`topology.root`** (resolution
|
||||
`env OPENRIG_TOPOLOGY_ROOT > config file > derived default
|
||||
$OPENRIG_HOME/topology`). Never hardcode the path — `rig config get
|
||||
topology.root` is the answer on any machine. The instance altitude **is the
|
||||
top of the root** (there is no instance directory to find; the root is the
|
||||
instance):
|
||||
|
||||
```
|
||||
<topology.root>/<NAME>.md # instance altitude
|
||||
<topology.root>/rigs/<rig>/<NAME>.md # rig altitude
|
||||
<topology.root>/rigs/<rig>/pods/<pod>/<NAME>.md # pod altitude
|
||||
<topology.root>/rigs/<rig>/seats/<seat>/<NAME>.md # seat altitude
|
||||
```
|
||||
|
||||
The engine creates the instance root, rig directory, and each declared pod
|
||||
directory when it materializes topology, even when no authored chain default
|
||||
exists. Pod context remains optional: use that altitude only for knowledge
|
||||
shared within one context domain. Cross-pod guidance still belongs at the rig
|
||||
altitude.
|
||||
|
||||
Established chain names on the topology tree:
|
||||
|
||||
- `LEARNED.md` — what a POSITION has learned; never shared or shipped as-is.
|
||||
A seat's LEARNED is identified by its PATH: the same seat name in two rigs
|
||||
is two different files.
|
||||
- `CRAFT.md` — how work is done at that altitude (shipped at instance, rig and
|
||||
seat), with `ORCHESTRATION-CRAFT.md` at rig level for orchestration practice.
|
||||
- `CULTURE.md` — team norms. Note that startup delivers the rig spec's own
|
||||
`culture_file` (a path beside `rig.yaml`), not a `CULTURE.md` under
|
||||
`topology.root`.
|
||||
|
||||
### The project tree
|
||||
|
||||
Rooted at the workspace keys (`workspace.root`, `workspace.slices_root`).
|
||||
Chain names there: `SPEC.md` (intent in frontmatter composes up the chain;
|
||||
specification is a leaf property), `PROOF.md`, `PROGRESS.md`. The walkers also
|
||||
read `NOTES.md` (or `MISSION_NOTES.md`), and fall back to `README.md` for a node
|
||||
without a `SPEC.md`.
|
||||
|
||||
**Project-tree context cannot ship.** It describes what *you* are building —
|
||||
mission intent, slice specifications, proof contracts. No vendor can author it
|
||||
in advance, and shipping a default would install someone else's project as
|
||||
your context. Real projects author their own; `rig scope mission create` and
|
||||
`rig scope slice create` start the tree. (Topology-tree context is the opposite: how a team
|
||||
of agents runs well is largely general, which is why included rigs ship
|
||||
defaults.)
|
||||
|
||||
## Legacy location and the advisory
|
||||
|
||||
Before this convention the topology tree lived at `~/.openrig/shared-docs/rigs/`
|
||||
(design decision 2026-08-14: an arbitrary folder on one machine, not a product
|
||||
path). That location **stays readable** as a per-level fallback for
|
||||
`rig context trace`, at the rig and seat levels, so existing rigs migrate instead
|
||||
of breaking. The refocus trace, `seat:` profile atoms and handover priming read
|
||||
only `topology.root`, so until migration they report those files as missing. Every
|
||||
read that resolves to the legacy location emits a
|
||||
named advisory (`legacy-topology-read: …`) stating the legacy source, the
|
||||
canonical destination under `topology.root`, and the config key. A silent
|
||||
legacy read is a defect. The legacy literal lives in exactly one helper per
|
||||
settings twin (`resolveLegacyTopologyRigsRoot`); walkers import it and carry
|
||||
no path literal.
|
||||
|
||||
## The trace
|
||||
|
||||
`rig context trace --rig <rig> [--pod <pod>] [--seat <seat>] --name <NAME>.md`
|
||||
performs the walk: it prints each selected altitude root-first (general →
|
||||
specific), marks whether content came from `topology.root`, the legacy tree
|
||||
(with the advisory on stderr), or is absent, and needs no running daemon —
|
||||
orientation is exactly when the daemon may be down. Existing rig/seat traces
|
||||
remain valid; selecting `--pod` adds that context domain between them. `--json`
|
||||
returns the structured result.
|
||||
|
||||
Your reads ARE the walk: a trace assembled from memory is a recitation that
|
||||
reproduces the drift it was meant to catch. Run it; do not recall it.
|
||||
|
||||
## Shipped defaults and the curation path
|
||||
|
||||
Included rigs ship sensible-default chain files under a `topology/` folder,
|
||||
installed under `topology.root` at rig-up. Today only `factory` ships one,
|
||||
with instance, rig and seat defaults; the installer also supports pod defaults.
|
||||
A shipped default is a **starting point** the occupying team appends to — it
|
||||
is never overwritten by a later rig-up (existing files win).
|
||||
|
||||
Adding a discovered best practice to the shipped defaults is deliberately
|
||||
lightweight:
|
||||
|
||||
1. **Discovery** — the practice appears where it was earned: a seat's
|
||||
`LEARNED.md`, a field note, a review observation.
|
||||
2. **Curation** — someone judges it *generally applicable* (would it hold on a
|
||||
stranger's machine, in a different project?). If it is project-specific it
|
||||
stays at the altitude that earned it.
|
||||
3. **Ship** — add it to the rig's default chain files in source
|
||||
(`packages/daemon/specs/rigs/…/topology/`), with the motivating incident in
|
||||
one line. It reaches new installs at the next release; running rigs pick it
|
||||
up only via explicit delivery (see the refocus channel) — **editing a file
|
||||
is not delivery to a running seat.**
|
||||
|
||||
## What this file does not cover
|
||||
|
||||
How chain files reach a RUNNING seat (the refocus channel) is specified in the
|
||||
refocus documentation; quality bars for the work itself live in the rig's
|
||||
operating-model skill and culture file.
|
||||
@@ -1,22 +0,0 @@
|
||||
# Checked seat stop (HTTP)
|
||||
|
||||
Read `GET /api/seat/stop-precondition/:seatRef` immediately before an operator-confirmed stop. A successful response has this shape:
|
||||
|
||||
```json
|
||||
{"contract":"seat-stop.v1","expected":{"nodeId":"<node ID>","sessionId":"<latest session ID or null>"}}
|
||||
```
|
||||
|
||||
Pass `expected` unchanged in the body of the existing `POST /api/seat/stop/:seatRef`, alongside `reason` and optional `operator`. The contract marker lets clients detect support. `expected` is optional; a request without it retains the existing stop behavior. A malformed explicit value returns HTTP 400. A null session ID can describe an empty seat, but stopping it returns `no_session`.
|
||||
|
||||
Checked stop requires the delivery guard. It refuses a changed node, latest session, binding or occupant before a terminal effect with HTTP 409 `selection_changed`. A kill failure or a failure to save stop state after a kill reports `stop_uncertain`; inspect the seat before retrying. Success retains the usual `seat`, `sessionName` and `sessionId` fields.
|
||||
|
||||
A `selection_changed` or `stop_uncertain` response from a checked stop says how far the stop got in `stopPhase`. A refusal before the stop starts carries neither `stopPhase` nor `partialEffect`, and nothing was sent. The stop detaches the seat's clients, checks the selection again, then kills the session:
|
||||
|
||||
| `stopPhase` | What was sent to the seat |
|
||||
| --- | --- |
|
||||
| `none` | Nothing. |
|
||||
| `detach_attempted` | The client detach, which may not have finished. No kill. |
|
||||
| `detached` | The client detach. No kill. |
|
||||
| `kill_attempted` | The kill. |
|
||||
|
||||
`partialEffect` is `true` whenever `stopPhase` isn't `none`, so on its own it doesn't say whether the kill went out. `selection_changed` with `stopPhase: "kill_attempted"` is a change seen after the kill, when the stop state is saved: the session was killed, so re-read the seat rather than offering the stop again.
|
||||
@@ -1,111 +0,0 @@
|
||||
# Custom Claude status line
|
||||
|
||||
OpenRig installs a silent context collector as `statusLine.command` in the seat
|
||||
working directory's `.claude/settings.local.json`. Claude's project-local setting
|
||||
overrides your user-level status line, so a renderer configured only in
|
||||
`~/.claude/settings.json` can disappear in that directory. Other Claude sessions
|
||||
using the same directory also read that setting.
|
||||
|
||||
Since 0.6.5, OpenRig preserves a custom, nonempty project-local status line command,
|
||||
including a wrapper or a command composed with shell operators. It still refreshes
|
||||
its own collector script. If your command does not run the collector, context and
|
||||
provider usage stop updating, and Claude resume-token capture at adoption or
|
||||
handover loses its current session evidence. An earlier sidecar can remain and
|
||||
contain an older session ID; without one, context and capture report
|
||||
`missing_sidecar`, and provider usage reports `no_statusline_cache_yet` unless an
|
||||
earlier cache remains.
|
||||
|
||||
## Feed both commands the same input
|
||||
|
||||
On macOS, Linux or WSL2, save a wrapper such as this, replacing the three
|
||||
placeholder paths and `my-statusline` with your actual commands:
|
||||
|
||||
```sh
|
||||
#!/bin/sh
|
||||
input=$(cat)
|
||||
printf '%s' "$input" |
|
||||
node '/seat/cwd/.openrig/context-collector.cjs' \
|
||||
'/instance/home/state/context-usage' \
|
||||
'/instance/home/state/provider-usage' >/dev/null 2>&1
|
||||
printf '%s' "$input" | my-statusline
|
||||
```
|
||||
|
||||
Take the collector path and both directory arguments from the existing generated
|
||||
`statusLine.command` in that seat's `.claude/settings.local.json`; the instance
|
||||
home is the selected `OPENRIG_HOME`. Keep paths quoted, including paths with
|
||||
spaces. Each command needs the same status line JSON on stdin: simply running
|
||||
the collector and renderer in sequence against a single stdin lets the first
|
||||
consume the input.
|
||||
|
||||
In that same settings file, merge this field while preserving your other settings:
|
||||
|
||||
```json
|
||||
{
|
||||
"statusLine": {
|
||||
"type": "command",
|
||||
"command": "sh '/absolute/path/to/openrig-status-line.sh'"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use the renderer command from your user-level configuration in the wrapper. The
|
||||
collector stays silent and the renderer supplies the visible status line. This
|
||||
POSIX-shell example applies to WSL2's Linux installation; it does not establish
|
||||
native Windows support.
|
||||
|
||||
## Check after an upgrade or instance change
|
||||
|
||||
The collector command's paths and arguments are implementation details. OpenRig
|
||||
preserves your wrapper, so it cannot refresh those arguments for you. Recheck
|
||||
them after an upgrade, a seat-directory move or an `OPENRIG_HOME` change.
|
||||
|
||||
After Claude next updates its status line, inspect context usage in `rig ps` or
|
||||
`rig whoami --full` from the seat. Unknown or stale context is a reason to check
|
||||
the wrapper; it does not prove the collector is running. For a direct check,
|
||||
inspect the current session's JSON under the configured `state/context-usage`
|
||||
directory: its timestamp should advance and its `session_id` should match the
|
||||
current Claude session. A visible custom status line alone does not verify
|
||||
collection or end-to-end adoption/handover recovery. The wrapper above suppresses
|
||||
collector errors; remove its stderr redirection temporarily when diagnosing it.
|
||||
|
||||
## Claude usage-limit detection
|
||||
|
||||
The status line supplies subscription usage windows and their reset timestamps.
|
||||
OpenRig also registers Claude's [`StopFailure` hook](https://code.claude.com/docs/en/hooks#stopfailure),
|
||||
available since Claude Code 2.1.78. Its exact `error: "rate_limit"` value reports a
|
||||
provider interruption even when no status line window is available. This path
|
||||
requires the updated OpenRig hook configuration and the current seat's runtime
|
||||
and occupant generation. Unknown identity or an expired event cannot trigger a
|
||||
usage-limit pause; a hook with a newer source timestamp supersedes the event,
|
||||
regardless of arrival order. The event expires after five minutes in any case.
|
||||
|
||||
A helper Claude started from a seat's shell inherits that seat's environment,
|
||||
so its rate-limit hook is attributed to that seat too. This is appropriate when
|
||||
the helper uses the same login, since the quota belongs to the account. A
|
||||
helper on a different login is attributed the same way, so its rate limit can
|
||||
hold pending handoffs to every seat in that seat's Claude pool for one retry
|
||||
interval, even when the seat's own login still has room. This attribution does
|
||||
not bind the hook to Claude's own native session.
|
||||
|
||||
The event carries no usage percentage, account identity or reset timestamp.
|
||||
It marks the Claude provider pool as limited and preserves a current
|
||||
`agentActivity.reason` of `usage_limit` over a cached idle reading. It does not
|
||||
count as a request for human input; a newer permission prompt still takes
|
||||
precedence in the seat's activity display. Conflicting seat windows retain the
|
||||
existing `explicit_unknown` host diagnosis.
|
||||
|
||||
Pending work handled by the wake ladder uses the existing local Claude pool
|
||||
(the one-account-per-host assumption), with separate pools for seats that
|
||||
declare different Claude config directories. If reset windows conflict within
|
||||
a proposed shared pool, only the seats reporting exhaustion are held; another
|
||||
seat's pending handoffs can still retry. An exhausted status line window with
|
||||
a known reset supplies the timer. Otherwise a typed failure uses
|
||||
`queue.wake_retry_interval_seconds` (default 300) from the event time, plus the
|
||||
existing wake jitter, and records `config-fallback`. That is a retry time, not a
|
||||
claim that the provider's quota will reset then. The timer releases the shared
|
||||
blocker once and retains the existing durable wake delivery behavior.
|
||||
|
||||
Authentication, billing and overloaded errors are not classified as usage
|
||||
limits. Claude limit banners are not scraped, and older runtimes or failures
|
||||
that do not emit this hook still depend on status line data. Without either
|
||||
source, provider usage remains unknown.
|
||||
@@ -1,14 +0,0 @@
|
||||
# Install as $OPENRIG_HOME/community-harnesses/cursor.yaml, with openrig-cursor-launch on the seat's PATH.
|
||||
format: 1
|
||||
name: cursor
|
||||
command: openrig-cursor-launch
|
||||
maintainer: Your name or team
|
||||
limits:
|
||||
- resume reads a chat as missing unless its store is in the seat's Cursor config folder, under the seat's working directory
|
||||
- no guidance, skills or startup files are placed
|
||||
- needs-input and approval prompts are read from the screen only
|
||||
launch_result: true
|
||||
resume: true
|
||||
startup_assisted: false
|
||||
first_input: true
|
||||
passes_model: false # the launcher maps OPENRIG_MODEL for a new session; set true once checked with the real harness
|
||||
@@ -1,74 +0,0 @@
|
||||
#!/bin/sh
|
||||
# A reference launcher for Cursor's CLI as an OpenRig community harness. Copy it, own it, and change it: OpenRig
|
||||
# doesn't ship or run it. It follows docs/reference/community-harness-presets.md:
|
||||
# - a new launch: `cursor-agent create-chat` prints a new chat id; the launcher reports it, then reopens that chat;
|
||||
# - a resume (OPENRIG_RESUME_REF set): it reopens the chat it was asked for and reports that id, or, when Cursor holds
|
||||
# no such chat, reports it missing and starts nothing.
|
||||
# Cursor's chat-id facts come from the probe in OpenRig issue #352, and the chat store's location from Cursor's own CLI
|
||||
# (2026.10.22).
|
||||
set -eu
|
||||
|
||||
producer="openrig-cursor-launch 2"
|
||||
cursor="${OPENRIG_CURSOR_AGENT:-cursor-agent}"
|
||||
|
||||
# Each seat gets its own Cursor config folder unless CURSOR_CONFIG_DIR is already set. Cursor keeps cli-config.json
|
||||
# (`--model`, approval settings) and the chats there, so a seat doesn't rewrite the person's own ~/.cursor. Cursor's
|
||||
# login isn't in it: Cursor keeps that in ~/.config/cursor/auth.json on Linux and in the Keychain on macOS.
|
||||
CURSOR_CONFIG_DIR="${CURSOR_CONFIG_DIR:-${OPENRIG_HOME:-$HOME/.openrig}/state/cursor/${OPENRIG_NODE_ID:-${OPENRIG_SESSION_NAME:-default}}}"
|
||||
export CURSOR_CONFIG_DIR
|
||||
mkdir -p "$CURSOR_CONFIG_DIR"
|
||||
|
||||
# Write this attempt's record, all at once, where OpenRig asked for it (nothing when it didn't ask).
|
||||
report() {
|
||||
[ -n "${OPENRIG_LAUNCH_RESULT:-}" ] || return 0
|
||||
tmp="$OPENRIG_LAUNCH_RESULT.$$.tmp"
|
||||
printf '%s\n' "$1" > "$tmp"
|
||||
mv "$tmp" "$OPENRIG_LAUNCH_RESULT"
|
||||
}
|
||||
plain_id() { case "$1" in ''|*[!A-Za-z0-9._:-]*) return 1 ;; *) return 0 ;; esac; }
|
||||
# Where Cursor keeps a chat: <config folder>/chats/<md5 of the working directory>/<chat id>/store.db. Cursor hashes
|
||||
# its process's working directory, which is the physical path, so this hashes `pwd -P`.
|
||||
chat_store() {
|
||||
if command -v md5sum >/dev/null 2>&1; then sum=$(pwd -P | tr -d '\n' | md5sum | cut -d' ' -f1)
|
||||
else sum=$(pwd -P | tr -d '\n' | md5 -q); fi
|
||||
printf '%s/chats/%s/%s/store.db' "$CURSOR_CONFIG_DIR" "$sum" "$1"
|
||||
}
|
||||
|
||||
# The seat's declared model (OPENRIG_MODEL, when the preset sets passes_model) goes to a new chat only. A resumed chat isn't
|
||||
# passed one: whether Cursor's --resume keeps the chat's earlier model or takes a new one hasn't been checked.
|
||||
if [ -n "${OPENRIG_MODEL:-}" ] && [ -z "${OPENRIG_RESUME_REF:-}" ]; then
|
||||
set -- --model "$OPENRIG_MODEL" "$@"
|
||||
fi
|
||||
|
||||
# Preserve the file's exact bytes, including trailing newlines. A read error stops before the harness starts.
|
||||
if [ -n "${OPENRIG_FIRST_INPUT:-}" ]; then
|
||||
first_input=$(cat "$OPENRIG_FIRST_INPUT" && printf '.')
|
||||
first_input=${first_input%.}
|
||||
set -- "$@" "$first_input"
|
||||
fi
|
||||
|
||||
if [ -n "${OPENRIG_RESUME_REF:-}" ]; then
|
||||
id="$OPENRIG_RESUME_REF"
|
||||
plain_id "$id" || { echo "openrig-cursor-launch: refusing an unusual chat id" >&2; exit 2; }
|
||||
# Cursor's `--resume` with an id it doesn't hold exits 0 on a new, empty chat, so check first. Gone: say so, start nothing.
|
||||
if [ ! -f "$(chat_store "$id")" ]; then
|
||||
report "{\"format\":1,\"attempt\":\"${OPENRIG_LAUNCH_ATTEMPT:-}\",\"harness\":\"cursor\",\"error\":\"missing\",\"producer\":\"$producer\"}"
|
||||
echo "openrig-cursor-launch: Cursor has no chat $id in $CURSOR_CONFIG_DIR for $(pwd -P); starting nothing" >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
# create-chat prints the id but doesn't exit (see #352): read the first line, then stop it.
|
||||
out=$(mktemp)
|
||||
"$cursor" create-chat > "$out" 2>/dev/null &
|
||||
pid=$!
|
||||
i=0
|
||||
while [ "$i" -lt 100 ] && [ ! -s "$out" ]; do sleep 0.1; i=$((i + 1)); done
|
||||
kill "$pid" 2>/dev/null || true
|
||||
id=$(head -n 1 "$out" | tr -d '\r\n\t ')
|
||||
rm -f "$out"
|
||||
# No usable id: report nothing, so OpenRig shows the identity as unavailable, and start Cursor on its own chat.
|
||||
plain_id "$id" || exec "$cursor" --trust "$@"
|
||||
fi
|
||||
|
||||
report "{\"format\":1,\"attempt\":\"${OPENRIG_LAUNCH_ATTEMPT:-}\",\"harness\":\"cursor\",\"nativeRef\":\"$id\",\"producer\":\"$producer\"}"
|
||||
exec "$cursor" --resume "$id" --trust "$@"
|
||||
@@ -1,96 +0,0 @@
|
||||
# An example runner for DeepSeek Harness (dsh)
|
||||
|
||||
`openrig-dsh-runner.mjs` is a small runner for the draft [harness and runner contract](../../harness-runner-contract.md).
|
||||
It runs in the seat's pane, hosts `dsh --profile acp` over ACP on stdio, and does the runner's part of each contract
|
||||
section:
|
||||
- **identity:** the launch record, written once the conversation is open;
|
||||
- **one prompt per block:** each submitted block becomes one `session/prompt`, using bracketed paste in a TTY;
|
||||
- **turn boundaries:** reported as activity hooks, with a working beat every 10 s through a long, quiet turn;
|
||||
- **cancel:** `/abort` or Ctrl-C sends `session/cancel`, and a turn reads cancelled only when dsh says so. Type `/abort`
|
||||
in the seat's pane: one sent with `rig send` arrives inside a From/To envelope, so it's an ordinary prompt;
|
||||
- **permissions:** each request goes to a person through OpenRig;
|
||||
- **resume:** it reopens the exact conversation, or says honestly that it couldn't.
|
||||
|
||||
It has no dependencies, and it isn't installed by OpenRig. Copy it with [`dsh.yaml`](dsh.yaml) and maintain both as your
|
||||
own. [bpamiri/dsh-seat](https://github.com/bpamiri/dsh-seat) is a fuller dsh runner by its own maintainer.
|
||||
|
||||
## Set it up
|
||||
|
||||
1. **Install dsh and Node.** Install Node 22 or later, then `npm i -g @deepseek-ai/[email protected]`. This is the version
|
||||
it was written for, and it has been checked only against a stand-in agent.
|
||||
2. **Install the runner and preset.** Make the runner executable and put it on the seat's `PATH`. Copy `dsh.yaml` to
|
||||
`$OPENRIG_HOME/community-harnesses/`. Then declare a member with `runtime: dsh`.
|
||||
3. **Give the seat credentials.** Each seat gets its own dsh home, at `$OPENRIG_HOME/state/dsh/<node>/home`, because dsh
|
||||
rewrites its profile there on every start. Put a `.credentials.yaml` there, or put a provider key in the seat's
|
||||
`env:`. To use a home you prepared yourself, set `DSH_SEAT_HOME` in the member's `env:`; `DSH_SEAT_BIN` there names
|
||||
another `dsh` binary. The runner doesn't take dsh's own `DSH_HOME` from its environment, because your shell's startup
|
||||
files run after the seat's `env:` and could set it. A member's `env:` can't set `OPENRIG_*` names, which OpenRig
|
||||
reserves, but the runner still reads `OPENRIG_DSH_HOME` and `OPENRIG_DSH` when something exports them to it, such as
|
||||
a preset `command` wrapper.
|
||||
|
||||
The runner's own defaults for dsh:
|
||||
- **Uploads:** the seat home gets a `cordis.patch.yml` that turns off dsh's session-log and plugin-list uploads to
|
||||
DeepSeek, and the runner starts dsh with `DSH_TELEMETRY_DISABLED=1`.
|
||||
- **Permission mode:** `DSH_PERMISSION_MODE` is `workspace-write` unless you set it.
|
||||
- **The hook token:** OpenRig's activity token isn't passed to dsh, whose shell tool inherits the runner's environment.
|
||||
- **Its version:** the launch record names dsh's version from `dsh --version`. If that fails or takes over 5 seconds, it
|
||||
names the version dsh's ACP handshake reports, marked as such: dsh 0.2.1-rc.2 reports 0.0.1 there.
|
||||
|
||||
## Permissions
|
||||
|
||||
The runner submits each request to OpenRig, which asks one person:
|
||||
- the default person;
|
||||
- or the person chosen for the seat with `rig seat set-permissions-to <seat> --to <person> --reason "…"`.
|
||||
|
||||
That person answers one of the options dsh offered, and the runner gives that answer to the same live request, once.
|
||||
|
||||
**When the person answers in their own words** rather than with a choice, a yes or a no, OpenRig permits by default and
|
||||
passes their words back as `said`. The runner gives dsh its option as usual, shows the words in the pane, and queues
|
||||
them like any other message, so dsh gets them after the current turn and after any message already waiting. The words
|
||||
are never the answer.
|
||||
|
||||
**When no one can be asked, the request is permitted** with dsh's own allow option: `allow_once` when dsh offers it,
|
||||
else `allow_always`. That's OpenRig's default. The runner does the same when OpenRig can't answer: an older OpenRig
|
||||
without this path, an OpenRig it can't reach, or a refusal other than `--deny`.
|
||||
|
||||
**`allow_always` creates a standing grant in dsh.** OpenRig doesn't record or revoke it, which is why the runner prefers
|
||||
`allow_once`. Where dsh keeps the grant, and for how long, isn't checked.
|
||||
|
||||
**The runner refuses** only when the seat's operator opted in to refusal, with `rig seat set-permissions-to <seat>
|
||||
--deny`, or when dsh offered no allow option. A refused request is shown in the pane, and the seat reads needs input
|
||||
until the next prompt.
|
||||
|
||||
**The runner answers cancelled, holding nothing,** when the person says no and dsh offered no reject option, or when
|
||||
OpenRig says the request isn't the seat's current occupant's (for example, after a relaunch).
|
||||
|
||||
## Before you declare resume
|
||||
|
||||
The preset leaves `resume` out, so a restore stops and asks, and a fresh launch starts a new conversation.
|
||||
`rig seat restart --keep-conversation` is for Codex seats only. The runner reopens a conversation by the method dsh
|
||||
advertises: `session/resume`, or `session/load`. When dsh advertises neither, it tries `session/resume`. It never starts
|
||||
a new conversation on a resume.
|
||||
|
||||
**Telling a missing conversation from one that won't reopen.** dsh `0.2.1-rc.2` answers `-32602` both for a
|
||||
conversation it doesn't have and for one already active elsewhere, so the runner never reads that error as missing.
|
||||
- **Only dsh's own not-found error (`-32002`) counts as missing.** Any other failure leaves the resume unconfirmed.
|
||||
- **When dsh advertises `session/list`** (it does), the runner first asks the list, following its pages. A conversation
|
||||
the list names but that won't reopen stays unconfirmed, even on a not-found.
|
||||
- **An unlisted conversation is still reopened.** dsh's list can leave out a conversation another running dsh holds
|
||||
unsaved, so it can't show a conversation is gone.
|
||||
|
||||
**Limit:** dsh answers `-32602`, not `-32002`, for a conversation it doesn't have, so the runner can't report a missing
|
||||
dsh conversation as missing. It reads unconfirmed.
|
||||
|
||||
Add `resume: true` after a real dsh has reopened a conversation and recalled what was said in it.
|
||||
|
||||
## Check it without a model
|
||||
|
||||
Point dsh's `DEEPSEEK_BASE_URL` at a local server that speaks DeepSeek's Anthropic-compatible Messages API. Then run the
|
||||
contract's real check:
|
||||
- a seat reads ready only after the handshake;
|
||||
- one multi-line message becomes one prompt;
|
||||
- the seat reads working, then idle;
|
||||
- a refused permission reads needs input;
|
||||
- `/abort` ends a turn as cancelled.
|
||||
|
||||
`packages/daemon/test/community-harness-dsh-example.test.ts` runs the same cases against a stand-in agent.
|
||||
@@ -1,20 +0,0 @@
|
||||
# Install as $OPENRIG_HOME/community-harnesses/dsh.yaml, with openrig-dsh-runner.mjs (executable) on the seat's PATH.
|
||||
format: 1
|
||||
name: dsh
|
||||
command: openrig-dsh-runner.mjs
|
||||
maintainer: Your name or team
|
||||
runner: true
|
||||
launch_result: true
|
||||
first_input: true
|
||||
limits:
|
||||
- written for dsh 0.2.1-rc.2 (npm @deepseek-ai/dsh) over ACP protocol 1, with Node 22 or later; run with a real dsh on two stacks only, dsh's Claude route with a Claude Haiku model in 0.7.8's release testing (with that release's copy of this runner, for an allow-once and a reject-once), and dsh's pi-ai OpenRouter chat-completions profile with the model deepseek/deepseek-v4-flash on OpenRig 0.7.8 (for queue work, activity through a quiet tool call, an allow-once and a reject-once, and cancel); in both, the permission answers came from a test answerer registered as a person, not a live person; dsh's DeepSeek Messages adapter pointed at OpenRouter failed with HTTP 400 in the second, cause not established; no other dsh route or model is checked
|
||||
- each seat gets its own dsh home under $OPENRIG_HOME/state/dsh/<node>/home, holding a patch that turns off dsh's session-log and plugin-list uploads to DeepSeek; give it credentials (.credentials.yaml there) or a provider key in the seat's env; set DSH_SEAT_HOME in the member's env to use a home you prepared, and DSH_SEAT_BIN for another dsh binary (a member's env can't set OPENRIG_* names, and the runner doesn't read dsh's own DSH_HOME)
|
||||
- DSH_PERMISSION_MODE defaults to workspace-write; a permission request goes to the default person or the seat's override (rig seat set-permissions-to), is permitted with dsh's allow_once (else allow_always) when no one can be asked or OpenRig can't answer, and is refused and held as needs input only after --deny
|
||||
- allow_always creates a standing grant in dsh, which OpenRig doesn't record or revoke; the runner prefers allow_once, and where dsh keeps the grant and for how long isn't checked
|
||||
- resume isn't declared; a real dsh has reopened a conversation after down, snapshot and up and recalled it, with resume added to a copy of this preset, but a missing conversation's refusal hasn't been run; the runner reopens by the method dsh advertises and reports missing only on dsh's own not-found error, not when dsh's session list names the conversation; dsh answers -32602 for a missing conversation (and for one active elsewhere), so a missing dsh conversation reads unconfirmed
|
||||
- a prompt whose whole text is /abort can't be sent; a Ctrl-C byte inside a paste also aborts; without a TTY, input is read line by line; this isn't a binary-safe channel
|
||||
- one prompt at a time; a message that arrives mid-turn waits for the next turn
|
||||
- the seat's model isn't passed; dsh uses its own configured model
|
||||
- no guidance, skills or startup files are placed; dsh reads its own AGENTS.md and skill folders
|
||||
- when the pane closes, the runner cancels a running turn and stops dsh; anything dsh's tools started may keep running
|
||||
- fork unsupported
|
||||
@@ -1,626 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
// Example OpenRig runner for DeepSeek Harness (dsh), not installed by OpenRig. Checked against a stand-in agent only;
|
||||
// written for dsh 0.2.1-rc.2 (npm @deepseek-ai/dsh), ACP protocol 1 and Node 22 or later. No dependencies.
|
||||
//
|
||||
// It follows docs/reference/harness-runner-contract.md: it hosts `dsh --profile acp` over ACP on stdio, writes the
|
||||
// launch record once the conversation is open, turns each submitted block into one prompt, reports turn boundaries as
|
||||
// activity hooks with a working beat through a long, quiet turn, cancels on /abort or Ctrl-C, and sends permission
|
||||
// requests to OpenRig: a person answers, or OpenRig permits when no one can be asked, and so does the runner when OpenRig
|
||||
// can't answer (refusing and holding needs input only when the seat opted in to refusal). Copy it and maintain it as your
|
||||
// own.
|
||||
|
||||
import { execFileSync, spawn } from "node:child_process";
|
||||
import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { createInterface } from "node:readline";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const RUNNER = "openrig-dsh-runner 1";
|
||||
const NAME = process.env.OPENRIG_RUNTIME || "dsh"; // the preset's name: OpenRig passes it, and checks the record against it
|
||||
const LIVENESS_SECONDS = 60;
|
||||
const BEAT_MS = 30_000;
|
||||
// The working beat (contract section 4): every 10 s, more often than half its 30 s lifetime.
|
||||
const WORK_LIVENESS_SECONDS = 30;
|
||||
const WORK_BEAT_MS = 10_000;
|
||||
const ACTIVE_EVERY_MS = 15_000;
|
||||
const POST_TIMEOUT_MS = 2_000;
|
||||
const WAIT_SECONDS = 25;
|
||||
const READ_TRIES = 3; // reads in a row that get no answer before the runner permits
|
||||
// Refusals that say the request certainly isn't the seat's current occupant's live one: cancel it, and hold nothing.
|
||||
const NOT_LIVE = ["stale_generation", "activity_grant_wrong_seat", "activity_grant_stale"];
|
||||
// A read's states that end the request, answered cancelled (an `answered` that reaches there names an option dsh didn't
|
||||
// offer). Any other state, like an unlisted status, permits.
|
||||
const ENDED = ["refused", "withdrawn", "stale", "answered"];
|
||||
const CANCELLED = { outcome: { outcome: "cancelled" } };
|
||||
const ACP_RESOURCE_NOT_FOUND = -32002;
|
||||
// The most pages of dsh's session list read before a resume; a longer list decides nothing.
|
||||
const LIST_PAGES = 50;
|
||||
|
||||
const PASTE_START = "\x1b[200~";
|
||||
const PASTE_END = "\x1b[201~";
|
||||
|
||||
/** Turns terminal bytes into submitted blocks. Everything up to an Enter outside a paste is one block, line breaks
|
||||
* included; a block that is exactly /abort, or a Ctrl-C byte, is the only control. */
|
||||
export class InputFramer {
|
||||
#buffer = "";
|
||||
#pasting = false;
|
||||
#partial = "";
|
||||
|
||||
push(chunk) {
|
||||
const events = [];
|
||||
const data = this.#partial + chunk;
|
||||
this.#partial = "";
|
||||
let i = 0;
|
||||
while (i < data.length) {
|
||||
if (data[i] === "\x1b") {
|
||||
const rest = data.slice(i);
|
||||
if (rest.startsWith(PASTE_START) || rest.startsWith(PASTE_END)) {
|
||||
this.#pasting = rest.startsWith(PASTE_START);
|
||||
i += PASTE_START.length;
|
||||
continue;
|
||||
}
|
||||
if (PASTE_START.startsWith(rest) || PASTE_END.startsWith(rest)) { this.#partial = rest; break; }
|
||||
if (!this.#pasting) { i += escapeLength(rest); continue; } // arrow keys and the like
|
||||
}
|
||||
const ch = data[i];
|
||||
i += 1;
|
||||
if (ch === "\x03") { this.#buffer = ""; this.#pasting = false; events.push({ type: "abort" }); continue; }
|
||||
if (this.#pasting) {
|
||||
if (ch === "\r") { if (data[i] === "\n") i += 1; this.#buffer += "\n"; } else this.#buffer += ch;
|
||||
continue;
|
||||
}
|
||||
if (ch === "\r" || ch === "\n") {
|
||||
const text = this.#buffer;
|
||||
this.#buffer = "";
|
||||
if (text.trim() === "/abort") events.push({ type: "abort" });
|
||||
else if (text.trim()) events.push({ type: "block", text });
|
||||
continue;
|
||||
}
|
||||
if (ch === "\x7f" || ch === "\b") {
|
||||
if (this.#buffer) { this.#buffer = this.#buffer.slice(0, -1); events.push({ type: "erase" }); }
|
||||
continue;
|
||||
}
|
||||
if (ch >= " ") { this.#buffer += ch; events.push({ type: "echo", text: ch }); }
|
||||
}
|
||||
return events;
|
||||
}
|
||||
}
|
||||
|
||||
function escapeLength(rest) {
|
||||
if (rest[1] === "[") {
|
||||
let j = 2;
|
||||
while (j < rest.length && !(rest.charCodeAt(j) >= 0x40 && rest.charCodeAt(j) <= 0x7e)) j += 1;
|
||||
return Math.min(j + 1, rest.length);
|
||||
}
|
||||
return rest[1] === "O" ? Math.min(3, rest.length) : 1;
|
||||
}
|
||||
|
||||
/** JSON-RPC 2.0 with the agent, one message per line on its stdio. */
|
||||
class AgentPeer {
|
||||
#child;
|
||||
#next = 1;
|
||||
#waiting = new Map();
|
||||
#requests = new Map();
|
||||
#notifications = new Map();
|
||||
|
||||
constructor(child, onNoise) {
|
||||
this.#child = child;
|
||||
createInterface({ input: child.stdout }).on("line", (line) => {
|
||||
let message;
|
||||
try { message = JSON.parse(line); } catch { message = null; }
|
||||
if (!message || typeof message !== "object") { if (line.trim()) onNoise(line); return; }
|
||||
if (message.method === undefined) {
|
||||
const waiter = this.#waiting.get(message.id);
|
||||
if (!waiter) return;
|
||||
this.#waiting.delete(message.id);
|
||||
if (message.error) waiter.reject(Object.assign(new Error(message.error.message ?? "error"), { code: message.error.code }));
|
||||
else waiter.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (message.id === undefined) { this.#notifications.get(message.method)?.(message.params ?? {}); return; }
|
||||
const handler = this.#requests.get(message.method);
|
||||
if (!handler) { this.#write({ id: message.id, error: { code: -32601, message: `Method not found: ${message.method}` } }); return; }
|
||||
Promise.resolve().then(() => handler(message.params ?? {}, message.id)).then(
|
||||
(result) => this.#write({ id: message.id, result }),
|
||||
(error) => this.#write({ id: message.id, error: { code: error.code ?? -32603, message: error.message } }));
|
||||
});
|
||||
}
|
||||
|
||||
request(method, params) {
|
||||
const id = this.#next++;
|
||||
return new Promise((resolve, reject) => { this.#waiting.set(id, { resolve, reject }); this.#write({ id, method, params }); });
|
||||
}
|
||||
|
||||
notify(method, params) { this.#write({ method, params }); }
|
||||
onRequest(method, handler) { this.#requests.set(method, handler); }
|
||||
onNotification(method, handler) { this.#notifications.set(method, handler); }
|
||||
|
||||
failAll(reason) {
|
||||
for (const waiter of this.#waiting.values()) waiter.reject(new Error(reason));
|
||||
this.#waiting.clear();
|
||||
}
|
||||
|
||||
#write(message) {
|
||||
if (this.#child.stdin.writable) this.#child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", ...message })}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
const openrigHome = () => process.env.OPENRIG_HOME || join(homedir(), ".openrig");
|
||||
|
||||
/** The daemon's base URL and the seat's token: from the environment, else from activity-endpoint.json. */
|
||||
function openrigEndpoint() {
|
||||
const env = process.env;
|
||||
let base = env.OPENRIG_URL || (env.OPENRIG_PORT ? `http://${env.OPENRIG_HOST || "127.0.0.1"}:${env.OPENRIG_PORT}` : "");
|
||||
let token = env.OPENRIG_ACTIVITY_HOOK_TOKEN || "";
|
||||
if (!base || !token) {
|
||||
try {
|
||||
const file = JSON.parse(readFileSync(join(openrigHome(), "activity-endpoint.json"), "utf8"));
|
||||
base ||= file.baseUrl ?? "";
|
||||
token ||= file.token ?? "";
|
||||
} catch { /* no daemon has written one yet */ }
|
||||
}
|
||||
return base && token ? { base: base.replace(/\/+$/, ""), token } : null;
|
||||
}
|
||||
|
||||
async function callOpenRig(method, path, body, timeoutMs) {
|
||||
const endpoint = openrigEndpoint();
|
||||
if (!endpoint) return { status: 0, body: null };
|
||||
try {
|
||||
const response = await fetch(`${endpoint.base}${path}`, {
|
||||
method, signal: AbortSignal.timeout(timeoutMs),
|
||||
headers: { authorization: `Bearer ${endpoint.token}`, ...(body ? { "content-type": "application/json" } : {}) },
|
||||
...(body ? { body: JSON.stringify(body) } : {}),
|
||||
});
|
||||
let parsed = null;
|
||||
try { parsed = await response.json(); } catch { /* not JSON */ }
|
||||
return { status: response.status, body: parsed };
|
||||
} catch {
|
||||
return { status: 0, body: null };
|
||||
}
|
||||
}
|
||||
|
||||
/** Activity posts go one at a time, in order, and never hold up the agent. */
|
||||
class Activity {
|
||||
#sequence = 0;
|
||||
#chain = Promise.resolve();
|
||||
|
||||
constructor(identity) { this.identity = identity; }
|
||||
|
||||
post(hookEvent, subtype, extra = {}) {
|
||||
const body = { ...this.identity, sequence: ++this.#sequence, occurredAt: new Date().toISOString(), hookEvent, subtype, ...extra };
|
||||
this.#chain = this.#chain.then(() => callOpenRig("POST", "/api/activity/hooks", body, POST_TIMEOUT_MS)).then(() => {}, () => {});
|
||||
}
|
||||
|
||||
flushed() { return this.#chain; }
|
||||
}
|
||||
|
||||
const UPLOADS_OFF = [
|
||||
"# Written by openrig-dsh-runner for this seat; safe to edit, and an existing file is left alone.",
|
||||
"# Don't attach this seat's session log, or its plugin package names and versions, to DeepSeek API requests.",
|
||||
"- id: session-log-deepseek",
|
||||
" config:",
|
||||
" enabled: false",
|
||||
"- id: plugin-package-inventory-deepseek",
|
||||
" config:",
|
||||
" enabled: false",
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
/** Each seat gets its own writable dsh home (dsh rewrites its profile there on every start). DSH_SEAT_HOME, which a rig
|
||||
* spec's env: can set, or OPENRIG_DSH_HOME names one you prepared yourself, which is used as it is. Not dsh's own
|
||||
* DSH_HOME: a person's shell startup files can export it, and they run after the seat's env:. */
|
||||
function seatHome() {
|
||||
const prepared = process.env.OPENRIG_DSH_HOME || process.env.DSH_SEAT_HOME;
|
||||
if (prepared) return prepared;
|
||||
const seat = process.env.OPENRIG_NODE_ID || process.env.OPENRIG_SESSION_NAME || "default";
|
||||
const home = join(openrigHome(), "state", "dsh", seat, "home");
|
||||
mkdirSync(home, { recursive: true, mode: 0o700 });
|
||||
const patch = join(home, "cordis.patch.yml");
|
||||
if (!existsSync(patch)) writeFileSync(patch, UPLOADS_OFF, { mode: 0o600 });
|
||||
return home;
|
||||
}
|
||||
|
||||
/** dsh's version as its CLI reports it, or null when it can't say. dsh 0.2.1-rc.2's ACP handshake reports 0.0.1.
|
||||
* SIGKILL at the timeout, because a synchronous call waits for the child to exit, and a child can ignore SIGTERM. */
|
||||
function cliVersion(bin, env) {
|
||||
try {
|
||||
const out = execFileSync(bin, ["--version"], { encoding: "utf8", env, timeout: 5_000, killSignal: "SIGKILL", stdio: ["ignore", "pipe", "ignore"] });
|
||||
return out.match(/\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?/)?.[0] ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeLaunchRecord(fields) {
|
||||
const path = process.env.OPENRIG_LAUNCH_RESULT;
|
||||
if (!path) return;
|
||||
const record = { format: 1, attempt: process.env.OPENRIG_LAUNCH_ATTEMPT, harness: NAME, ...fields };
|
||||
const temp = `${path}.${process.pid}.tmp`;
|
||||
writeFileSync(temp, `${JSON.stringify(record)}\n`, { mode: 0o600 });
|
||||
renameSync(temp, path);
|
||||
}
|
||||
|
||||
const sleep = (ms) => new Promise((done) => setTimeout(done, ms));
|
||||
const say = (line) => process.stdout.write(`${line}\n`);
|
||||
const firstLine = (text) => {
|
||||
const lines = text.split("\n");
|
||||
return lines.length > 1 ? `${lines[0]} … (+${lines.length - 1} lines)` : lines[0];
|
||||
};
|
||||
|
||||
async function main() {
|
||||
const env = process.env;
|
||||
const cwd = process.cwd();
|
||||
let firstInput = null;
|
||||
if (env.OPENRIG_FIRST_INPUT) {
|
||||
try { firstInput = readFileSync(env.OPENRIG_FIRST_INPUT, "utf8"); } catch (error) {
|
||||
console.error(`openrig-dsh-runner: the first startup turn couldn't be read (${error.message}); not starting`);
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
const home = seatHome();
|
||||
const activity = new Activity({ sessionName: env.OPENRIG_SESSION_NAME, nodeId: env.OPENRIG_NODE_ID,
|
||||
runtime: NAME, generation: env.OPENRIG_OCCUPANT_GENERATION });
|
||||
const agentEnv = { ...env, DSH_HOME: home };
|
||||
delete agentEnv.OPENRIG_ACTIVITY_HOOK_TOKEN; // dsh's shell tool inherits this environment
|
||||
agentEnv.DSH_PERMISSION_MODE ??= "workspace-write";
|
||||
if (agentEnv.DSH_TELEMETRY_DISABLED === undefined && agentEnv.DSH_TELEMETRY_MODE === undefined) agentEnv.DSH_TELEMETRY_DISABLED = "1";
|
||||
|
||||
// DSH_SEAT_BIN, which a rig spec's env: can set, or OPENRIG_DSH names another dsh binary.
|
||||
const bin = env.OPENRIG_DSH || env.DSH_SEAT_BIN || "dsh";
|
||||
const version = cliVersion(bin, agentEnv);
|
||||
const child = spawn(bin, ["--profile", "acp"], { cwd, env: agentEnv, stdio: ["pipe", "pipe", "inherit"] });
|
||||
child.stdin.on("error", () => {});
|
||||
const peer = new AgentPeer(child, (line) => say(`[dsh] ${line}`));
|
||||
const state = { sessionId: null, turn: 0, running: false, cancelRequestedAt: null, holding: false, refused: false, queue: [], asks: new Set(), lastActive: 0, asking: 0, waiting: 0, resolvedAsk: null };
|
||||
const inflight = new Set();
|
||||
const track = (promise) => { inflight.add(promise); promise.finally(() => inflight.delete(promise)); return promise; };
|
||||
let agentExited = false;
|
||||
let stopping = false;
|
||||
let heartbeat;
|
||||
let workBeat;
|
||||
let tty = false;
|
||||
// The turn may read working: it runs, nothing holds needs input, no permission request is being submitted or waits for its
|
||||
// permission_resolved, and dsh is alive. The beat and the agent's own reports both go by it.
|
||||
const working = () => state.running && !state.holding && state.asking === 0 && !state.resolvedAsk && !agentExited;
|
||||
|
||||
const restoreTty = () => {
|
||||
if (!tty) return;
|
||||
tty = false;
|
||||
process.stdout.write("\x1b[?2004l");
|
||||
try { process.stdin.setRawMode(false); } catch { /* already closed */ }
|
||||
};
|
||||
const finish = async (code) => {
|
||||
clearInterval(heartbeat);
|
||||
clearInterval(workBeat);
|
||||
restoreTty();
|
||||
await Promise.race([Promise.allSettled([...inflight, activity.flushed()]), sleep(2_000)]);
|
||||
process.exit(code);
|
||||
};
|
||||
const fatal = async (message, code) => {
|
||||
stopping = true;
|
||||
say(`[openrig-dsh] ${message}`);
|
||||
if (!agentExited) {
|
||||
child.kill("SIGTERM");
|
||||
await Promise.race([new Promise((done) => child.once("exit", done)), sleep(3_000)]);
|
||||
}
|
||||
await finish(code);
|
||||
};
|
||||
const postIdle = (subtype) => activity.post("Stop", subtype, { livenessSeconds: LIVENESS_SECONDS });
|
||||
const withdraw = (askId, reason) => track(callOpenRig("POST", `/api/activity/permission-requests/${encodeURIComponent(askId)}/withdraw`, { reason }, POST_TIMEOUT_MS));
|
||||
|
||||
child.once("error", (error) => { agentExited = true; void fatal(`dsh couldn't be started (${error.message}); is it on PATH?`, 1); });
|
||||
child.on("exit", (code, signal) => {
|
||||
agentExited = true;
|
||||
peer.failAll("dsh exited");
|
||||
if (stopping) return;
|
||||
for (const ask of state.asks) ask.end("exited");
|
||||
say(`\n[openrig-dsh] dsh exited (code ${code ?? "none"}, signal ${signal ?? "none"})`);
|
||||
activity.post("Stop", `${NAME}_exited`);
|
||||
void finish(code === 0 ? 0 : 1);
|
||||
});
|
||||
|
||||
peer.onNotification("session/update", (params) => {
|
||||
if (params.sessionId !== state.sessionId || !params.update) return;
|
||||
const update = params.update;
|
||||
const reporting = working();
|
||||
if (update.sessionUpdate === "agent_message_chunk") {
|
||||
if (update.content?.type === "text") process.stdout.write(update.content.text);
|
||||
if (reporting && Date.now() - state.lastActive >= ACTIVE_EVERY_MS) { state.lastActive = Date.now(); activity.post("active", "agent_message"); }
|
||||
} else if (update.sessionUpdate === "tool_call") {
|
||||
say(`\n[tool] ${update.title ?? "tool"} (${update.kind ?? "other"})`);
|
||||
if (reporting) activity.post("PreToolUse", String(update.kind ?? "other"));
|
||||
}
|
||||
});
|
||||
|
||||
const refuse = (why) => {
|
||||
state.holding = true;
|
||||
state.refused = true; // the seat's own refusal holds needs input until the next prompt
|
||||
say(`[permission] refused: ${why}. The seat reads needs input until the next prompt.`);
|
||||
activity.post("Notification", "permission_prompt", { turnId: String(state.turn) });
|
||||
return CANCELLED;
|
||||
};
|
||||
|
||||
// The answered path: OpenRig opens one ask to the default person (or the seat's override, `rig seat
|
||||
// set-permissions-to`), and the runner reads the answer back for this same live request. With no one to ask, OpenRig
|
||||
// permits at once, and so does the runner when OpenRig can't answer. Only the seat's opt-in (`--deny`) refuses; a
|
||||
// request OpenRig says isn't the current occupant's, or that a person said no to with no reject option, is cancelled.
|
||||
const askOwner = async (requestId, toolCall, options) => {
|
||||
const turn = state.turn;
|
||||
const live = () => state.running && state.turn === turn && !state.cancelRequestedAt && !agentExited;
|
||||
const submitted = await callOpenRig("POST", "/api/activity/permission-requests", {
|
||||
sessionName: env.OPENRIG_SESSION_NAME, nodeId: env.OPENRIG_NODE_ID, generation: env.OPENRIG_OCCUPANT_GENERATION,
|
||||
nativeRef: state.sessionId, turnId: String(turn), requestId: String(requestId), toolCall,
|
||||
options: options.map((option) => ({ optionId: option.optionId, name: option.name ?? option.optionId, kind: option.kind })),
|
||||
occurredAt: new Date().toISOString(),
|
||||
}, 10_000);
|
||||
const askId = submitted.body?.ok ? submitted.body.askId : undefined;
|
||||
if (!live()) {
|
||||
if (askId) withdraw(askId, state.cancelRequestedAt ? "turn_cancelled" : agentExited ? "exited" : "turn_ended");
|
||||
return CANCELLED;
|
||||
}
|
||||
const offered = (optionId) => options.some((option) => option.optionId === optionId);
|
||||
// No one can be asked: OpenRig's default is permit, with the agent's own allow option.
|
||||
if (submitted.body?.ok && submitted.body.state === "permitted" && offered(submitted.body.optionId)) {
|
||||
say(`[permission] permitted: ${submitted.body.reason ?? "no one can be asked"} (${submitted.body.optionId})`);
|
||||
return { outcome: { outcome: "selected", optionId: submitted.body.optionId } };
|
||||
}
|
||||
// When OpenRig can't answer, nobody can be asked either, so the same default applies. allow_once is preferred, so
|
||||
// dsh doesn't remember a permission nobody chose.
|
||||
const allow = options.find((option) => option.kind === "allow_once") ?? options.find((option) => option.kind === "allow_always");
|
||||
const permit = (why) => {
|
||||
if (!allow) return refuse(`${why}, and the agent offered no allow option`);
|
||||
state.holding = state.refused || state.waiting > 0; // another ask may still wait on a person
|
||||
say(`[permission] permitted: ${why} (${allow.optionId})`);
|
||||
return { outcome: { outcome: "selected", optionId: allow.optionId } };
|
||||
};
|
||||
// A person who answered in their own words: OpenRig passes them back as `said`. They join the prompt queue like any
|
||||
// message, so dsh gets them after this turn and after any message already waiting. They're never the answer.
|
||||
const passOn = (said) => {
|
||||
if (typeof said !== "string" || !said.trim()) return;
|
||||
say(`[permission] the person also wrote: ${firstLine(said)}`);
|
||||
submit(`The person who answered your permission request (${toolCall.title}) also wrote:\n\n${said}`);
|
||||
};
|
||||
if (!askId) {
|
||||
if (submitted.body?.code === "denied_by_seat") return refuse("the seat's operator chose to refuse its permission requests");
|
||||
if (NOT_LIVE.includes(submitted.body?.code)) {
|
||||
say(`[permission] cancelled: OpenRig says this isn't the seat's current request (${submitted.body.code})`);
|
||||
return CANCELLED;
|
||||
}
|
||||
return permit(submitted.status === 404 ? "this OpenRig has no answered path"
|
||||
: submitted.status === 0 ? "OpenRig couldn't be reached" : `OpenRig couldn't ask anyone (${submitted.body?.code ?? submitted.status})`);
|
||||
}
|
||||
state.holding = true; // OpenRig recorded the seat's permission prompt when it opened the ask
|
||||
state.waiting += 1;
|
||||
say(`[permission] OpenRig asked a person (${askId}); waiting for their answer`);
|
||||
// This ask no longer waits on a person; the seat still needs input while another one does, or after its own refusal.
|
||||
const stopWaiting = () => { state.waiting -= 1; state.holding = state.refused || state.waiting > 0; };
|
||||
// The agent got an answer or a permit for this ask: once nothing else waits, permission_resolved says the turn goes on.
|
||||
const resolved = () => { state.resolvedAsk = { turn, askId }; };
|
||||
return new Promise((resolve) => {
|
||||
let done = false; // the agent has its answer
|
||||
let ended = false; // OpenRig was told the request is over
|
||||
const ask = { end: (reason) => {
|
||||
if (ended) return;
|
||||
ended = true;
|
||||
state.asks.delete(ask);
|
||||
withdraw(askId, reason);
|
||||
if (done) return;
|
||||
done = true;
|
||||
stopWaiting(); // the turn ended the person's ask
|
||||
resolve(CANCELLED);
|
||||
} };
|
||||
state.asks.add(ask);
|
||||
void (async () => {
|
||||
let unanswered = 0;
|
||||
while (!done) {
|
||||
const read = await callOpenRig("GET", `/api/activity/permission-requests/${encodeURIComponent(askId)}?waitSeconds=${WAIT_SECONDS}`, null, (WAIT_SECONDS + 10) * 1_000);
|
||||
if (done) return;
|
||||
if (read.status === 0 && ++unanswered < READ_TRIES) { await sleep(2_000); continue; }
|
||||
if (read.status === 200 && read.body?.state === "pending") { unanswered = 0; continue; }
|
||||
done = true;
|
||||
const answer = read.body ?? {};
|
||||
// A person's option, or OpenRig's permit when they closed it without one.
|
||||
if ((answer.state === "answered" || answer.state === "permitted") && offered(answer.optionId) && live()) {
|
||||
state.asks.delete(ask);
|
||||
stopWaiting();
|
||||
say(`[permission] ${answer.state}: ${answer.optionId}`);
|
||||
resolve({ outcome: { outcome: "selected", optionId: answer.optionId } });
|
||||
resolved();
|
||||
passOn(answer.said);
|
||||
} else if (live() && (read.status === 200 ? !ENDED.includes(answer.state) : !NOT_LIVE.includes(answer.code))) {
|
||||
// OpenRig stopped answering, or answered in a way the contract doesn't list: nobody can be asked. The ask stays
|
||||
// tracked, so the turn's end still withdraws it.
|
||||
stopWaiting();
|
||||
resolve(permit(read.status === 0 ? "OpenRig couldn't be reached" : `OpenRig couldn't answer (${answer.code ?? read.status})`));
|
||||
if (!state.refused) resolved(); // permitted, not refused for want of an allow option
|
||||
} else {
|
||||
state.asks.delete(ask);
|
||||
stopWaiting(); // a person said no, or the request is over
|
||||
say(`[permission] not answered (${answer.reason ?? answer.state ?? answer.code ?? read.status}); answered cancelled`);
|
||||
resolve(CANCELLED);
|
||||
passOn(answer.said);
|
||||
}
|
||||
}
|
||||
})();
|
||||
});
|
||||
};
|
||||
|
||||
peer.onRequest("session/request_permission", (params, requestId) => {
|
||||
if (params.sessionId !== state.sessionId) throw Object.assign(new Error(`no turn of conversation ${params.sessionId} runs here`), { code: -32602 });
|
||||
const toolCall = { title: String(params.toolCall?.title ?? "a tool call"), kind: String(params.toolCall?.kind ?? "other") };
|
||||
const options = Array.isArray(params.options) ? params.options : [];
|
||||
say(`\n[permission] dsh asks: ${toolCall.title} (${toolCall.kind}); options ${options.map((option) => option.optionId).join(", ")}`);
|
||||
if (!state.running || state.cancelRequestedAt) return CANCELLED;
|
||||
state.asking += 1; // no beat while a permission request is open: it may be waiting on a person
|
||||
return askOwner(requestId, toolCall, options).finally(() => { state.asking -= 1; setImmediate(settle); });
|
||||
});
|
||||
|
||||
// Once nothing waits on a person or is being submitted, the seat holds no refusal of its own and dsh is alive, say the turn
|
||||
// goes on. It goes out after the agent has its answer, and before the next beat.
|
||||
const settle = () => {
|
||||
const done = state.resolvedAsk;
|
||||
if (!done || state.holding || state.asking > 0) return;
|
||||
state.resolvedAsk = null;
|
||||
if (state.running && state.turn === done.turn && !agentExited) activity.post("active", "permission_resolved", { turnId: String(done.turn), askId: done.askId });
|
||||
};
|
||||
|
||||
const abort = () => {
|
||||
if (!state.running) { say("[openrig-dsh] no turn is running"); return; }
|
||||
if (state.cancelRequestedAt) return;
|
||||
state.cancelRequestedAt = new Date().toISOString();
|
||||
peer.notify("session/cancel", { sessionId: state.sessionId });
|
||||
say(`\n[openrig-dsh] cancel requested at ${state.cancelRequestedAt}; the turn ends when dsh says so`);
|
||||
if (!state.refused) state.holding = false; // the seat's own refusal holds until the next accepted prompt
|
||||
for (const ask of state.asks) ask.end("turn_cancelled");
|
||||
};
|
||||
|
||||
const runTurn = async (text) => {
|
||||
state.running = true;
|
||||
state.turn += 1;
|
||||
state.holding = false;
|
||||
state.refused = false;
|
||||
state.waiting = 0;
|
||||
state.resolvedAsk = null;
|
||||
state.cancelRequestedAt = null;
|
||||
state.lastActive = 0;
|
||||
const turnId = String(state.turn);
|
||||
say(`\nyou ▸ ${firstLine(text)}`);
|
||||
activity.post("UserPromptSubmit", "accepted", { turnId });
|
||||
let stopReason;
|
||||
try {
|
||||
const result = await peer.request("session/prompt", { sessionId: state.sessionId, prompt: [{ type: "text", text }] });
|
||||
stopReason = String(result?.stopReason ?? "unreported");
|
||||
} catch (error) {
|
||||
if (agentExited) return;
|
||||
say(`\n[openrig-dsh] the turn failed: ${error.message}`);
|
||||
activity.post("Notification", "runtime_error", { turnId });
|
||||
stopReason = "error";
|
||||
}
|
||||
state.running = false;
|
||||
for (const ask of state.asks) ask.end("turn_ended");
|
||||
say(`\n── turn end (${stopReason}) ──`);
|
||||
if (state.holding) say("[openrig-dsh] a permission request was refused, so the seat reads needs input until the next prompt");
|
||||
else postIdle(stopReason);
|
||||
pump();
|
||||
};
|
||||
|
||||
const pump = () => {
|
||||
if (state.running || agentExited || state.queue.length === 0) return;
|
||||
void runTurn(state.queue.shift());
|
||||
};
|
||||
|
||||
const submit = (text) => {
|
||||
if (agentExited) return;
|
||||
state.queue.push(text);
|
||||
if (state.running) say(`[queued: ${state.queue.length}] this goes to dsh after the current turn`);
|
||||
pump();
|
||||
};
|
||||
|
||||
// Handshake, then open or reopen the conversation, then report it. Nothing is read as ready before this.
|
||||
let init;
|
||||
try {
|
||||
init = await peer.request("initialize", { protocolVersion: 1, clientCapabilities: {}, clientInfo: { name: "openrig-dsh-runner", version: "1" } });
|
||||
} catch (error) {
|
||||
return fatal(`dsh didn't complete the ACP handshake (${error.message})`, 1);
|
||||
}
|
||||
const capabilities = init?.agentCapabilities ?? {};
|
||||
const reported = init?.agentInfo?.version ? `${init.agentInfo.version} (as its ACP handshake reports it)` : "(version not reported)";
|
||||
const producer = `${RUNNER}; dsh ${version ?? reported}; ACP ${init?.protocolVersion ?? "?"}`;
|
||||
const resumeRef = env.OPENRIG_RESUME_REF;
|
||||
// Whether dsh's own session list names a conversation, following its pages: true, false when every page was read and
|
||||
// none names it, or undefined when the list fails, answers in another shape, or runs past LIST_PAGES pages. An ACP
|
||||
// cursor is an opaque string, possibly empty, passed back unchanged; an absent or null one ends the list.
|
||||
const listsConversation = async (id) => {
|
||||
let cursor;
|
||||
for (let page = 0; page < LIST_PAGES; page += 1) {
|
||||
let listing;
|
||||
try { listing = await peer.request("session/list", cursor === undefined ? {} : { cursor }); } catch { return undefined; }
|
||||
if (!Array.isArray(listing?.sessions) || listing.sessions.some((session) => typeof session?.sessionId !== "string")) return undefined;
|
||||
if (listing.sessions.some((session) => session.sessionId === id)) return true;
|
||||
if (listing.nextCursor === undefined || listing.nextCursor === null) return false;
|
||||
if (typeof listing.nextCursor !== "string") return undefined;
|
||||
cursor = listing.nextCursor;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
if (resumeRef) {
|
||||
// The method dsh advertises; with neither advertised, session/resume, the method dsh 0.2.1-rc.2 answered in its
|
||||
// maintainer's checks. dsh 0.2.1-rc.2 answers -32602 both for a conversation it doesn't have and for one active
|
||||
// elsewhere, so that error can't mean missing. Only dsh's own "not found" counts as missing, and not when dsh's list
|
||||
// names the conversation. The list can't show a conversation is gone: one another running dsh holds unsaved may be
|
||||
// left out, so an unlisted conversation is still reopened. Anything else leaves the resume unconfirmed.
|
||||
const method = capabilities.sessionCapabilities?.resume ? "session/resume" : capabilities.loadSession ? "session/load" : "session/resume";
|
||||
const listed = capabilities.sessionCapabilities?.list ? await listsConversation(resumeRef) === true : false;
|
||||
try {
|
||||
await peer.request(method, { sessionId: resumeRef, cwd, mcpServers: [] });
|
||||
} catch (error) {
|
||||
if (error.code === ACP_RESOURCE_NOT_FOUND && !listed) {
|
||||
writeLaunchRecord({ error: "missing", producer });
|
||||
return fatal(`dsh says conversation ${resumeRef} doesn't exist (${error.message}); no new one was started`, 3);
|
||||
}
|
||||
return fatal(`dsh couldn't reopen conversation ${resumeRef} (${method}: ${error.message}); the resume is unconfirmed and no new conversation was started`, 4);
|
||||
}
|
||||
state.sessionId = resumeRef;
|
||||
} else {
|
||||
try {
|
||||
state.sessionId = (await peer.request("session/new", { cwd, mcpServers: [] }))?.sessionId ?? null;
|
||||
} catch (error) {
|
||||
return fatal(`dsh couldn't open a conversation (${error.message})`, 1);
|
||||
}
|
||||
if (!state.sessionId) return fatal("dsh opened a conversation without naming it", 1);
|
||||
}
|
||||
writeLaunchRecord({ nativeRef: state.sessionId, producer });
|
||||
say(`[openrig-dsh] ready · ${producer} · conversation ${state.sessionId}`);
|
||||
say(`[openrig-dsh] DSH_HOME ${home} · DSH_PERMISSION_MODE ${agentEnv.DSH_PERMISSION_MODE}`);
|
||||
say("[openrig-dsh] each submitted message is one prompt; /abort or Ctrl-C cancels the running turn");
|
||||
|
||||
heartbeat = setInterval(() => { if (!state.running && !state.holding && !agentExited) postIdle("idle_heartbeat"); }, BEAT_MS);
|
||||
// While a turn runs and nothing waits on a person: the turn is still outstanding and dsh is alive. Not progress.
|
||||
workBeat = setInterval(() => {
|
||||
if (working()) {
|
||||
activity.post("active", "liveness", { turnId: String(state.turn), livenessSeconds: WORK_LIVENESS_SECONDS });
|
||||
}
|
||||
}, WORK_BEAT_MS);
|
||||
for (const signal of ["SIGHUP", "SIGTERM"]) {
|
||||
process.on(signal, () => {
|
||||
if (state.running && !state.cancelRequestedAt) peer.notify("session/cancel", { sessionId: state.sessionId });
|
||||
for (const ask of state.asks) ask.end("exited");
|
||||
child.kill("SIGTERM");
|
||||
setTimeout(() => child.kill("SIGKILL"), 3_000).unref();
|
||||
});
|
||||
}
|
||||
process.on("SIGINT", abort);
|
||||
|
||||
const framer = new InputFramer();
|
||||
if (process.stdin.isTTY) {
|
||||
process.stdin.setRawMode(true);
|
||||
process.stdout.write("\x1b[?2004h");
|
||||
tty = true;
|
||||
process.stdin.setEncoding("utf8");
|
||||
process.stdin.on("data", (chunk) => {
|
||||
for (const event of framer.push(chunk)) {
|
||||
if (event.type === "echo") process.stdout.write(event.text);
|
||||
else if (event.type === "erase") process.stdout.write("\b \b");
|
||||
else if (event.type === "abort") { process.stdout.write("\n"); abort(); }
|
||||
else if (event.type === "block") { process.stdout.write("\n"); submit(event.text); }
|
||||
}
|
||||
});
|
||||
} else {
|
||||
createInterface({ input: process.stdin }).on("line", (line) => {
|
||||
if (line.trim() === "/abort") abort();
|
||||
else if (line.trim()) submit(line);
|
||||
});
|
||||
}
|
||||
|
||||
if (firstInput !== null) submit(firstInput);
|
||||
else postIdle("idle_heartbeat");
|
||||
}
|
||||
|
||||
const invoked = (() => {
|
||||
try { return process.argv[1] ? pathToFileURL(realpathSync(process.argv[1])).href : null; } catch { return null; }
|
||||
})();
|
||||
if (invoked === import.meta.url) {
|
||||
main().catch((error) => { console.error(`openrig-dsh-runner: ${error.stack ?? error}`); process.exit(1); });
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
# Merge this into each Hermes seat's config.yaml, in that seat's Hermes home ($OPENRIG_HOME/state/hermes/<node id> with
|
||||
# the example launcher), before the seat's first start.
|
||||
#
|
||||
# Hermes runs every tool command in its gateway (one per Hermes home, so one per seat with the example launcher), with
|
||||
# the gateway's environment. For each session it swaps
|
||||
# in the client's own values only for its toolchain names and the names listed here. Without this list, `rig` inside
|
||||
# Hermes runs as whichever seat started the gateway, or as no seat. With it, each seat runs `rig` as itself.
|
||||
# These are the seat's own OpenRig variables; none is a secret, and Hermes refuses credential-like names anyway.
|
||||
terminal:
|
||||
client_env_passthrough:
|
||||
- OPENRIG_SESSION_NAME
|
||||
- OPENRIG_NODE_ID
|
||||
- OPENRIG_HOME
|
||||
- OPENRIG_URL
|
||||
- OPENRIG_HOST
|
||||
- OPENRIG_PORT
|
||||
- OPENRIG_DB
|
||||
|
||||
# Your own settings go beside it (the guide says which): the model and provider, and agent.max_turns if you want a cap.
|
||||
@@ -1,13 +0,0 @@
|
||||
# Install as $OPENRIG_HOME/community-harnesses/hermes.yaml, with openrig-hermes-launch on the seat's PATH.
|
||||
format: 1
|
||||
name: hermes
|
||||
command: openrig-hermes-launch # no model, provider or --max-turns here: they go in the seat's Hermes config.yaml
|
||||
maintainer: Your name or team
|
||||
limits:
|
||||
- resume not declared until reopening a named session, and a missing one, have been checked with a real Hermes
|
||||
- closing a seat's pane detaches it; Hermes finishes any accepted turn; whether its gateway exits when idle is unchecked
|
||||
- creation settings (model, provider, max turns) come from the seat's Hermes config.yaml, not from flags
|
||||
- no guidance, skills or startup files are placed
|
||||
- needs-input and approval prompts are read from the screen only
|
||||
names_conversation: true
|
||||
passes_model: false # the launcher maps OPENRIG_MODEL for a new session; set true once checked with the real harness
|
||||
@@ -1,49 +0,0 @@
|
||||
#!/bin/sh
|
||||
# A reference launcher for Hermes Agent's CLI as an OpenRig community harness. Copy it, own it, and change it: OpenRig
|
||||
# doesn't ship or run it. It follows docs/reference/community-harness-presets.md (`names_conversation`):
|
||||
# - a new launch (OPENRIG_CONVERSATION set): Hermes creates the session titled with that name;
|
||||
# - a resume (OPENRIG_RESUME_REF set): Hermes reopens that session, and never creates one. A name that matches nothing
|
||||
# ends with Hermes's own "No session found" error, so a gone conversation isn't replaced by a new one.
|
||||
# Put the model, provider and max turns in the seat's Hermes config.yaml, not on the command line. Hermes v0.22.4's CLI
|
||||
# creates a `-c <name> --create-if-missing` session with no launch flags, then reopens it and refuses any creation flag
|
||||
# that differs, so such flags fail even on a new name. Other arguments go only to a new session, never to a resume.
|
||||
set -eu
|
||||
|
||||
hermes="${OPENRIG_HERMES:-hermes}"
|
||||
|
||||
# Each seat gets its own Hermes home, so seats don't share sessions or a gateway, and a seat's stop concerns only its own.
|
||||
# Its settings (model, provider, agent.max_turns) come from that home's config.yaml, and its key from the environment
|
||||
# (an OpenRouter key, say), not from the person's ~/.hermes.
|
||||
HERMES_HOME="${OPENRIG_HOME:-$HOME/.openrig}/state/hermes/${OPENRIG_NODE_ID:-${OPENRIG_SESSION_NAME:-default}}"
|
||||
export HERMES_HOME
|
||||
mkdir -p "$HERMES_HOME"
|
||||
|
||||
# Hermes's tools see its shared gateway's environment except for the names a profile passes through, so a seat whose
|
||||
# config.yaml doesn't pass OPENRIG_SESSION_NAME would run `rig` as another seat, or as none (see config.yaml.example).
|
||||
if ! grep -qs OPENRIG_SESSION_NAME "$HERMES_HOME/config.yaml"; then
|
||||
echo "openrig-hermes-launch: $HERMES_HOME/config.yaml doesn't pass OPENRIG_SESSION_NAME to Hermes's tools, so rig run from Hermes won't act as this seat; see config.yaml.example" >&2
|
||||
fi
|
||||
|
||||
# The seat's declared model (OPENRIG_MODEL, when the preset sets passes_model): Hermes takes it only from config.yaml, and
|
||||
# Hermes v0.22.4 fixes a session's launch settings when it creates it. So it's written for a new session only, and only
|
||||
# into a config.yaml that is absent or empty: a seat's own config is left byte-for-byte as it is, whatever it says. On a
|
||||
# resume this launcher applies no OpenRig model.
|
||||
if [ -n "${OPENRIG_MODEL:-}" ] && [ -z "${OPENRIG_RESUME_REF:-}" ]; then
|
||||
case "$OPENRIG_MODEL" in
|
||||
''|*[!A-Za-z0-9._:/@+-]*) echo "openrig-hermes-launch: not writing unusual model id \"$OPENRIG_MODEL\" into $HERMES_HOME/config.yaml" >&2 ;;
|
||||
*) if [ -s "$HERMES_HOME/config.yaml" ]; then
|
||||
echo "openrig-hermes-launch: the seat has its own $HERMES_HOME/config.yaml; OPENRIG_MODEL was not applied; set the model there" >&2
|
||||
else
|
||||
printf 'model: "%s"\n' "$OPENRIG_MODEL" >> "$HERMES_HOME/config.yaml"
|
||||
fi ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if [ -n "${OPENRIG_RESUME_REF:-}" ]; then
|
||||
exec "$hermes" chat -c "$OPENRIG_RESUME_REF"
|
||||
fi
|
||||
if [ -n "${OPENRIG_CONVERSATION:-}" ]; then
|
||||
exec "$hermes" chat -c "$OPENRIG_CONVERSATION" --create-if-missing "$@"
|
||||
fi
|
||||
echo "openrig-hermes-launch: OpenRig passed no conversation name; is names_conversation set in the preset?" >&2
|
||||
exit 2
|
||||
@@ -1,15 +0,0 @@
|
||||
# The Gateway must already run. Configure its route and authentication in this seat's OPENCLAW_CONFIG_PATH.
|
||||
format: 1
|
||||
name: openclaw-gateway
|
||||
command: openrig-openclaw-launch --gateway
|
||||
maintainer: Your name or team
|
||||
limits:
|
||||
- connects to an existing Gateway; this launcher never starts or stops that service
|
||||
- stopping the TUI closes its client; an accepted turn may keep running on the Gateway
|
||||
- uses only agent main; automatic session resets must stay off
|
||||
- typing /new changes the native key while the seat keeps the name OpenRig gave it
|
||||
- no guidance, skills or startup files are placed
|
||||
- needs-input and approval prompts are read from the screen only
|
||||
names_conversation: true
|
||||
resume: true
|
||||
first_input: true
|
||||
@@ -1,16 +0,0 @@
|
||||
# Install as $OPENRIG_HOME/community-harnesses/openclaw.yaml, with openrig-openclaw-launch on the seat's PATH.
|
||||
format: 1
|
||||
name: openclaw
|
||||
command: openrig-openclaw-launch
|
||||
maintainer: Your name or team
|
||||
limits:
|
||||
- runs locally in the pane (openclaw tui --local), using only agent main, with no Gateway; Gateway-attached use isn't covered
|
||||
- Finish the current local turn before stopping; an interrupted local turn may retain native recovery work and refuse the next request. This launcher does not repair or retry it.
|
||||
- local resume may run a native database integrity check; check that the TUI has finished startup before sending work
|
||||
- automatic session resets (session.reset) must stay off, or a key can start a new conversation
|
||||
- typing /new in the pane moves the chat to a fresh session key; the seat still holds the name OpenRig gave it
|
||||
- no guidance, skills or startup files are placed
|
||||
- needs-input and approval prompts are read from the screen only
|
||||
names_conversation: true
|
||||
resume: true
|
||||
first_input: true
|
||||
@@ -1,70 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Reference launcher for OpenClaw 2026.10.6 (fe06181), not installed by OpenRig.
|
||||
# Each seat owns its client state and uses the main agent. OpenRig's opaque name maps to its full native key.
|
||||
# --gateway selects an existing Gateway configured for this client; the default is an embedded local agent.
|
||||
# Resume checks that exact key at its owner; an unavailable lookup is not a missing conversation.
|
||||
set -eu
|
||||
|
||||
openclaw="${OPENRIG_OPENCLAW:-openclaw}"
|
||||
mode=local
|
||||
if [ "${1:-}" = --gateway ]; then mode=gateway; shift; fi
|
||||
OPENCLAW_STATE_DIR="${OPENRIG_HOME:-$HOME/.openrig}/state/openclaw/${OPENRIG_NODE_ID:-${OPENRIG_SESSION_NAME:-default}}"
|
||||
export OPENCLAW_STATE_DIR
|
||||
mkdir -p "$OPENCLAW_STATE_DIR"
|
||||
|
||||
name="${OPENRIG_RESUME_REF:-${OPENRIG_CONVERSATION:-}}"
|
||||
if [ -z "$name" ]; then
|
||||
echo "openrig-openclaw-launch: OpenRig passed no conversation name; is names_conversation set in the preset?" >&2
|
||||
exit 2
|
||||
fi
|
||||
case "$name" in
|
||||
agent:main:*) key="$name" ;;
|
||||
agent:*) echo "openrig-openclaw-launch: this example supports only the main OpenClaw agent" >&2; exit 2 ;;
|
||||
*) key="agent:main:$name" ;;
|
||||
esac
|
||||
|
||||
# Preserve the file's exact bytes, including trailing newlines. A read error stops before the harness starts.
|
||||
if [ -n "${OPENRIG_FIRST_INPUT:-}" ]; then
|
||||
first_input=$(cat "$OPENRIG_FIRST_INPUT" && printf '.')
|
||||
first_input=${first_input%.}
|
||||
set -- "$@" --message "$first_input"
|
||||
fi
|
||||
|
||||
if [ -n "${OPENRIG_RESUME_REF:-}" ]; then
|
||||
if [ "$mode" = gateway ]; then
|
||||
params=$(KEY="$key" node -e 'process.stdout.write(JSON.stringify({ key: process.env.KEY, agentId: "main" }))')
|
||||
if listing=$("$openclaw" gateway call sessions.describe --params "$params" --json); then listed=true; else listed=false; fi
|
||||
else
|
||||
if listing=$("$openclaw" sessions --agent main --json --limit all); then listed=true; else listed=false; fi
|
||||
fi
|
||||
if [ "$listed" = false ]; then
|
||||
echo "openrig-openclaw-launch: this seat's OpenClaw sessions couldn't be listed; not starting" >&2
|
||||
exit 4
|
||||
fi
|
||||
if printf '%s' "$listing" | KEY="$key" MODE="$mode" node -e '
|
||||
let raw = ""; process.stdin.on("data", (chunk) => { raw += chunk; });
|
||||
process.stdin.on("end", () => {
|
||||
try {
|
||||
const data = JSON.parse(raw);
|
||||
if (process.env.MODE === "gateway") {
|
||||
if (data.session === null) process.exit(3);
|
||||
process.exit(data.session?.key === process.env.KEY ? 0 : 4);
|
||||
}
|
||||
const { sessions } = data;
|
||||
if (!Array.isArray(sessions)) process.exit(4);
|
||||
process.exit(sessions.some((s) => s?.agentId === "main" && s.key === process.env.KEY) ? 0 : 3);
|
||||
} catch { process.exit(4); }
|
||||
});'; then
|
||||
:
|
||||
else
|
||||
result=$?
|
||||
if [ "$result" -eq 3 ]; then
|
||||
echo "openrig-openclaw-launch: this seat's OpenClaw sessions don't list \"$key\"; not starting a new one under that key" >&2
|
||||
else
|
||||
echo "openrig-openclaw-launch: this seat's OpenClaw sessions couldn't be listed; not starting" >&2
|
||||
fi
|
||||
exit "$result"
|
||||
fi
|
||||
fi
|
||||
if [ "$mode" = gateway ]; then exec "$openclaw" tui --session "$key" "$@"; fi
|
||||
exec "$openclaw" tui --local --session "$key" "$@"
|
||||
@@ -1,5 +0,0 @@
|
||||
# OpenShell reference driver
|
||||
|
||||
Run `rig environment setup --help` to install the maintained OpenShell 0.1.6 / Claude Code reference from the packaged daemon assets. Its README, image recipe, endpoint-specific policy, example RigSpec/AgentSpecs and versioned driver schema are installed together. Setup prepares files and an ordinary scoped work grant; it does not create compute or launch a provider.
|
||||
|
||||
This first cut supports explicit owned create or exact attached binding, required destination projection and verified JSON artifact return through authenticated MCP work tools. Explicit owned lifecycle operations retain separate observed receipts. Exact same-host recreate/restore requires verified private history and the selected checkpoint; attached native, storage and compute are preserved. A native process receipt is not a successful model-work claim. Existing unbound host runtimes retain their launch/projection paths.
|
||||
@@ -1,100 +0,0 @@
|
||||
#!/bin/sh
|
||||
# A reference launcher for Pi's own terminal UI (interactive `pi`) as an OpenRig community harness. Copy it, own it, and
|
||||
# change it: OpenRig doesn't ship or run it. It's written for Pi 1.4.0 from its source and not yet checked with a real Pi.
|
||||
# It follows docs/reference/community-harness-presets.md (`names_conversation`, `first_input`):
|
||||
# - a new launch (OPENRIG_CONVERSATION set): Pi creates the session whose id is that name, in the seat's own session
|
||||
# folder, and takes the first input as its first prompt;
|
||||
# - a resume (OPENRIG_RESUME_REF set): the launcher opens the one session file whose id is that name, and starts nothing
|
||||
# when there isn't exactly one. Pi's --session-id creates a new session for an id it doesn't find, and --session with
|
||||
# an id can open another session whose id starts the same way, so neither is used to resume.
|
||||
set -eu
|
||||
|
||||
pi="${OPENRIG_PI:-pi}"
|
||||
state="${OPENRIG_HOME:-$HOME/.openrig}/state/pi-tui/${OPENRIG_NODE_ID:-${OPENRIG_SESSION_NAME:-default}}"
|
||||
sessions="$state/sessions"
|
||||
|
||||
# Each seat gets its own Pi agent folder (settings, logins, skills) unless PI_CODING_AGENT_DIR is already set, so seats
|
||||
# don't share settings and a person's ~/.pi/agent isn't touched. Give the seat its provider key in its environment, or its
|
||||
# login and settings in this folder, before its first start.
|
||||
PI_CODING_AGENT_DIR="${PI_CODING_AGENT_DIR:-$state/agent}"
|
||||
# Pi expands a leading ~ in this folder; do the same, so the skills land where Pi looks.
|
||||
case "$PI_CODING_AGENT_DIR" in
|
||||
"~") PI_CODING_AGENT_DIR="$HOME" ;;
|
||||
"~/"*) PI_CODING_AGENT_DIR="$HOME/${PI_CODING_AGENT_DIR#"~/"}" ;;
|
||||
esac
|
||||
export PI_CODING_AGENT_DIR
|
||||
mkdir -p "$PI_CODING_AGENT_DIR/skills" "$sessions"
|
||||
|
||||
# Copy the installed OpenRig's coordination skills in at each launch, so an upgrade reaches the seat. Pi lists the skills
|
||||
# in <agent folder>/skills/ by itself; the seat's first input should still name queue-handoff (see the recipe).
|
||||
skills_from="${OPENRIG_PI_SKILLS_FROM:-${OPENRIG_HOME:-$HOME/.openrig}/plugins/openrig-core/skills}"
|
||||
set -f
|
||||
for skill in ${OPENRIG_PI_SKILLS:-queue-handoff}; do
|
||||
case "$skill" in
|
||||
*[!a-z0-9-]*|-*) echo "openrig-pi-tui-launch: skipping '$skill': not a skill name" >&2; continue ;;
|
||||
esac
|
||||
if [ -d "$skills_from/$skill" ]; then
|
||||
rm -rf "$PI_CODING_AGENT_DIR/skills/$skill"
|
||||
cp -Rp "$skills_from/$skill" "$PI_CODING_AGENT_DIR/skills/"
|
||||
else
|
||||
echo "openrig-pi-tui-launch: no $skill skill in $skills_from; starting without it" >&2
|
||||
fi
|
||||
done
|
||||
set +f
|
||||
|
||||
# Pi's session ids: letters, digits, '.', '_' and '-', starting and ending with a letter or digit.
|
||||
valid_id() {
|
||||
case "$1" in
|
||||
"" | *[!A-Za-z0-9._-]* | [!A-Za-z0-9]* | *[!A-Za-z0-9]) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# The first input goes over as Pi's first prompt, byte for byte, after `--`. OpenRig's starts with its identity-context
|
||||
# line; Pi would read one starting with @ as a file even after `--`, and may run one starting with / as a command.
|
||||
set --
|
||||
if [ -n "${OPENRIG_FIRST_INPUT:-}" ]; then
|
||||
first_input=$(cat "$OPENRIG_FIRST_INPUT" && printf '.')
|
||||
first_input="${first_input%.}"
|
||||
if [ -n "$first_input" ]; then set -- -- "$first_input"; fi
|
||||
fi
|
||||
|
||||
if [ -n "${OPENRIG_RESUME_REF:-}" ]; then
|
||||
id="$OPENRIG_RESUME_REF"
|
||||
if ! valid_id "$id"; then
|
||||
echo "openrig-pi-tui-launch: '$id' isn't a Pi session id; starting nothing" >&2
|
||||
exit 2
|
||||
fi
|
||||
# Pi names a session file <time>_<id>.jsonl, but the file's first line, its header, is what says which session it is:
|
||||
# its top-level type and id, read as JSON as Pi reads it. Pi writes the file only once the conversation has a message, so
|
||||
# a session that never got one can't be reopened. Pi runs on Node, so node reads the header.
|
||||
node="${OPENRIG_PI_NODE:-node}"
|
||||
if ! command -v "$node" >/dev/null 2>&1; then
|
||||
echo "openrig-pi-tui-launch: $node isn't on PATH to read Pi's session headers; starting nothing" >&2
|
||||
exit 1
|
||||
fi
|
||||
found="" count=0
|
||||
for file in "$sessions"/*_"$id".jsonl; do
|
||||
[ -s "$file" ] || continue
|
||||
if head -n 1 "$file" | "$node" -e '
|
||||
let text = ""; process.stdin.on("data", c => { text += c; }).on("end", () => {
|
||||
try { const h = JSON.parse(text); process.exit(h && h.type === "session" && h.id === process.argv[1] ? 0 : 1); }
|
||||
catch { process.exit(1); }
|
||||
});' "$id"; then
|
||||
found="$file" count=$((count + 1))
|
||||
fi
|
||||
done
|
||||
if [ "$count" -ne 1 ]; then
|
||||
echo "openrig-pi-tui-launch: expected one Pi session $id in $sessions, found $count; starting nothing" >&2
|
||||
exit 1
|
||||
fi
|
||||
exec "$pi" --session-dir "$sessions" --session "$found" "$@"
|
||||
fi
|
||||
if [ -n "${OPENRIG_CONVERSATION:-}" ]; then
|
||||
if ! valid_id "$OPENRIG_CONVERSATION"; then
|
||||
echo "openrig-pi-tui-launch: '$OPENRIG_CONVERSATION' isn't a Pi session id; starting nothing" >&2
|
||||
exit 2
|
||||
fi
|
||||
exec "$pi" --session-dir "$sessions" --session-id "$OPENRIG_CONVERSATION" "$@"
|
||||
fi
|
||||
echo "openrig-pi-tui-launch: OpenRig passed no conversation name; is names_conversation set in the preset?" >&2
|
||||
exit 2
|
||||
@@ -1,15 +0,0 @@
|
||||
# Install as $OPENRIG_HOME/community-harnesses/pi-tui.yaml, with openrig-pi-tui-launch on the seat's PATH.
|
||||
# "pi" itself is the built-in managed Pi runtime, so this preset for Pi's own terminal UI takes another name.
|
||||
format: 1
|
||||
name: pi-tui
|
||||
command: openrig-pi-tui-launch # no model or provider here: they go in the seat's Pi settings or environment
|
||||
maintainer: Your name or team
|
||||
limits:
|
||||
- checked once with Pi 1.4.0 and OpenRig 0.7.4 (one run, one model); other Pi versions aren't checked
|
||||
- resume not declared until reopening a named session, and a missing one, have been checked with a real Pi
|
||||
- with the default typing guard, a wake submits a person's half-typed draft in Pi's editor together with the wake; run the seat unattended, not where a person also types
|
||||
- a send while a Pi dialog OpenRig doesn't recognize is open, such as /settings, types into the dialog
|
||||
- how Pi takes a wake that arrives while it's busy isn't shown
|
||||
- only the listed openrig-core skills are copied into the seat's Pi agent folder; no guidance or startup files are placed
|
||||
names_conversation: true
|
||||
first_input: true
|
||||
@@ -1,455 +0,0 @@
|
||||
# Community harness presets
|
||||
|
||||
A community harness is a coding harness an outside contributor maintains. You install it as one small data file, and a
|
||||
rig member runs it by naming it as its `runtime`. OpenRig doesn't test these harnesses. It shows them as community
|
||||
harnesses, and it reads their activity only from what the screen shows.
|
||||
|
||||
For what an integration author owns end to end, including a runner's duties and running a harness somewhere else, see
|
||||
the draft [integration author contracts](author-contracts.md).
|
||||
|
||||
## What a community harness gets
|
||||
|
||||
What OpenRig does for a community-harness seat, capability by capability. **Partly** and **no** mean OpenRig shows that it doesn't know;
|
||||
it never reads an unsupported capability as idle, working, ready or verified.
|
||||
|
||||
| Capability | Supported | How it works, or what you see instead |
|
||||
|---|---|---|
|
||||
| Launch | yes | OpenRig types the preset's `command` into the seat's pane, with its variables passed through `/usr/bin/env`. The seat reads as ready once something other than a shell runs in the pane; that's a process check, not proof of which conversation it runs. With `startup_assisted`, the seat reports attention required until a person checks it. |
|
||||
| Delivery | yes | `rig send` and queue wakes paste the text into the pane and press Enter, as for any seat. A pane that's back at a shell is refused (`target_runtime_unverified`) and nothing is typed. OpenRig doesn't confirm the harness took the message; `rig send --verify` checks the pane afterwards. |
|
||||
| Identity | when declared | With `launch_result`, the conversation is shown as reported by its launcher; with `names_conversation`, as named by OpenRig. Neither is observed by OpenRig. Without either, the identity is unavailable. |
|
||||
| Resume | when declared | `resume: true` with one of the two above (see [Reopening a conversation](#reopening-a-conversation-resume-true)). Without it, a resume says `Resume is unsupported: community harness "<name>" is launch-only`, and a restore stops and asks. |
|
||||
| Activity | partly | Read from the screen only; there are no activity hooks. Work and attention the screen shows are kept, but a screen that looks idle reads `unknown`, never idle, because a quiet prompt can be an approval or a pause. A [runner preset](#runner-presets-runner-true)'s own reports decide it instead. |
|
||||
| Attention | partly | Read from the screen: a permission question or numbered choice that OpenRig recognizes reads as needing attention, and `rig send` won't type into it. A prompt it doesn't recognize reads `unknown`. |
|
||||
| Approval | no | Approvals stay in the harness's own pane. OpenRig relays none, so a person answers them there. A [runner preset](#runner-presets-runner-true) is the exception: its runner submits the agent's permission requests to OpenRig, which asks a person, and the default is permit (section 7 of the [harness and runner contract](harness-runner-contract.md)). |
|
||||
| Cancel | no | OpenRig has no verb that cancels a running turn. `rig seat stop` and `rig down` end the seat's tmux session; anything the harness runs outside its pane, such as a gateway, is its own, and the preset's `limits` should say what keeps running. Restarting in place and switching harness are refused. |
|
||||
| Transport | yes | The seat is a tmux pane like any other, so `rig capture`, `rig send` and `--host` work as they do for any seat. |
|
||||
|
||||
**Not placed:** guidance, skills and startup files (a launch warns that they weren't delivered), and the model: set it in the harness's own
|
||||
configuration, or let a launcher that opts in take the seat's declared model (`passes_model`, below).
|
||||
|
||||
**Check that the harness answers on its own first.** Before writing a preset, start the harness in a terminal outside OpenRig, with the
|
||||
same configuration and credentials your launcher will give it, and send it one prompt. Use the folder your launcher gives the seat, not
|
||||
the harness's default one: the example launchers keep one per seat under `$OPENRIG_HOME/state/<harness>/` (`HERMES_HOME`,
|
||||
`OPENCLAW_STATE_DIR` or `CURSOR_CONFIG_DIR`), so a check with your own defaults can pass while the seat's setup fails. If no answer comes
|
||||
back, the problem is in the harness's own setup, such as its model, provider or login, and relaunching an OpenRig seat won't fix it. Fix
|
||||
it in the harness, then connect it.
|
||||
|
||||
## The preset
|
||||
|
||||
One YAML file per harness: `$OPENRIG_HOME/community-harnesses/<name>.yaml`. `rig community-harness list` shows what's
|
||||
installed.
|
||||
|
||||
```yaml
|
||||
format: 1
|
||||
name: example-harness # must match the file name: lowercase letters, digits and hyphens
|
||||
command: example-launch # one program and its arguments, typed into the seat's pane
|
||||
maintainer: Example Maintainer
|
||||
limits: # what the maintainer says doesn't work
|
||||
- fork unsupported
|
||||
launch_result: true # optional: the launcher reports the conversation it created (below)
|
||||
# names_conversation: true # optional, instead of launch_result: OpenRig names the conversation (below)
|
||||
resume: true # optional, needs one of those two; declare it only once your harness's resume is checked
|
||||
startup_assisted: true # optional: startup submission needs a person's check (below)
|
||||
first_input: true # optional: launcher takes the first turn from OPENRIG_FIRST_INPUT (below)
|
||||
# runner: true # optional, needs launch_result: the launcher reports the seat's activity (below)
|
||||
# passes_model: true # optional: the launcher takes the seat's declared model from OPENRIG_MODEL (below)
|
||||
# launch_wrapper: true # optional: a runner applies OPENRIG_LAUNCH_WRAPPER to its native child (below)
|
||||
```
|
||||
|
||||
- **`command`** is a single program and its arguments. Shell syntax such as `|`, `&&`, `;`, redirections or `$(…)`
|
||||
is refused: put it in a script and name the script.
|
||||
- **Built-in runtime names are reserved.** A preset can't replace `claude-code`, `codex` or the other built-ins.
|
||||
- **Unknown fields are refused,** so a preset written for a newer OpenRig says so instead of half-working.
|
||||
- **Fork isn't supported, and resume only as described below.** Neither is placing skills, guidance or startup files. Except
|
||||
for assisted startup below, the seat is called ready while something other than a shell runs in its pane, which says
|
||||
nothing about which conversation it is.
|
||||
|
||||
### Assisted startup
|
||||
|
||||
Set `startup_assisted: true` when startup submission needs a person's check. The seat may start its first turn
|
||||
automatically, including from input passed at launch with `first_input: true`. This declaration requires a check after
|
||||
the submission attempt; it does not hold the agent back. Without `first_input`, OpenRig pastes the startup text and
|
||||
presses Enter once.
|
||||
|
||||
After attempting startup input, OpenRig reports **attention required**: check the pane and submit any text still in the
|
||||
composer. OpenRig does not resend the identity prompt. Once the seat responds, use the reported
|
||||
`rig seat clear-attention <session> --reason "Startup submission checked; seat responded"` command to acknowledge the
|
||||
check. A launch with no startup text needs no such check. Other presets keep their existing behavior.
|
||||
|
||||
## Reporting the conversation a launch created (`launch_result: true`)
|
||||
|
||||
Some harnesses create their own conversation id before they start. Cursor's `cursor-agent create-chat` is one. With
|
||||
`launch_result: true`, the harness's launcher tells OpenRig that id for each launch.
|
||||
|
||||
1. **OpenRig names one file per launch attempt.** It starts `command` with two variables:
|
||||
- `OPENRIG_LAUNCH_RESULT`, a new file path inside `$OPENRIG_HOME/state/launch-results/`;
|
||||
- `OPENRIG_LAUNCH_ATTEMPT`, this attempt's id.
|
||||
2. **The launcher writes one JSON record to that path,** then starts the harness:
|
||||
```json
|
||||
{ "format": 1, "attempt": "<OPENRIG_LAUNCH_ATTEMPT>", "harness": "cursor", "nativeRef": "<the conversation id>", "producer": "openrig-cursor-launch 1.0" }
|
||||
```
|
||||
3. **OpenRig reads that attempt's record once,** waiting up to 20 seconds for it. It records `nativeRef` on the session,
|
||||
and the seat shows its identity as **reported by its launcher, not observed**. That's what the launcher says, not
|
||||
proof that the harness used it.
|
||||
|
||||
The seat still starts, with its identity **unavailable** and the reason named, when the record:
|
||||
- wasn't written;
|
||||
- belongs to another attempt or another harness;
|
||||
- isn't JSON, isn't format 1, or is larger than 64 KiB;
|
||||
- has an unusable `nativeRef` (empty, too long, or containing spaces or control characters);
|
||||
- names no producer;
|
||||
- or is reached through a link.
|
||||
|
||||
None of these means the conversation is gone.
|
||||
|
||||
A reported id doesn't make the harness resumable on its own. Without `resume: true`, nothing resumes it: a restore
|
||||
stops and asks, as it did before (re-run it with `--fresh <seat>` only if you want a new conversation), and elsewhere a
|
||||
resume says it's unsupported, and a new conversation starts only where the caller allows a fresh start.
|
||||
|
||||
**Good to know:**
|
||||
- **The variables are passed with `/usr/bin/env`,** so `launch_result` needs a POSIX system.
|
||||
- **A launcher that never writes its record** adds up to 20 seconds to each launch, while OpenRig waits for it.
|
||||
- **Records are kept,** one small file per attempt, under `$OPENRIG_HOME/state/launch-results/<session>/`. A record
|
||||
larger than 64 KiB isn't read.
|
||||
|
||||
## Letting OpenRig name the conversation (`names_conversation: true`)
|
||||
|
||||
Some harnesses take a conversation name from the caller and create that conversation, or reopen it. Hermes's
|
||||
`hermes chat -c <name> --create-if-missing` is one. With `names_conversation: true`, OpenRig names each new conversation
|
||||
and the harness uses that name, so no record is needed.
|
||||
- **A new launch:** OpenRig starts `command` with `OPENRIG_CONVERSATION` set to a new name, `openrig-<seat>-<8 hex digits>`
|
||||
(at most 100 characters). Each launch gets its own name, so two seats never share a conversation.
|
||||
- **What the seat shows:** OpenRig records that name on the session, and the seat shows its identity as **named by
|
||||
OpenRig, not observed**. That's the name OpenRig passed, not proof that the harness used it.
|
||||
- **Not with `launch_result`:** a preset declares one or the other. Either the launcher reports the conversation, or
|
||||
OpenRig names it.
|
||||
|
||||
## Reopening a conversation (`resume: true`)
|
||||
|
||||
**A named conversation:** with `names_conversation`, a resume starts `command` with `OPENRIG_RESUME_REF` set to the saved
|
||||
name. The harness reopens it and must never create one under that name. Nothing reports back, so:
|
||||
- **readiness is process-only:** the seat reads as ready while something other than a shell runs in its pane. That
|
||||
doesn't show the named conversation exists: a harness (or a launcher's own check) that hasn't yet found it gone reads
|
||||
as ready, and the saved name stays unverified until something observes it;
|
||||
- **never creating one is the launcher's job:** a resume must not create a conversation under the saved name. When it's
|
||||
gone, the harness or launcher ends with its error and the pane is back at a shell; the seat keeps its saved name, and
|
||||
OpenRig starts nothing new, though the seat may already have read as ready;
|
||||
- **a former runtime's id** isn't passed to the harness, as below.
|
||||
|
||||
**A reported conversation:** with `launch_result`, a resume (a restore included) starts the launcher with `OPENRIG_RESUME_REF` set to the
|
||||
conversation to reopen. The launcher reports what happened in the same attempt's file:
|
||||
- **Reopened:** the usual record, with that conversation as `nativeRef`.
|
||||
- **Gone:** `{ "format": 1, "attempt": "…", "harness": "cursor", "error": "missing", "producer": "…" }`. The launcher
|
||||
starts nothing. OpenRig reports the conversation missing and starts nothing either: it never replaces it with a new one.
|
||||
|
||||
For a reported conversation, OpenRig also says so plainly when:
|
||||
- **no usable record is written:** the resume isn't confirmed. The seat needs attention and keeps the conversation it
|
||||
asked for; nothing is restarted;
|
||||
- **the record names a different conversation:** the seat needs attention and keeps the conversation it asked for.
|
||||
OpenRig doesn't switch it to the reported one;
|
||||
- **the saved conversation isn't this harness's own** (a former runtime's id, say): it isn't passed to the launcher. A restore stops and asks, and elsewhere the resume says it isn't possible;
|
||||
- **the record's folder can't be made:** the launcher couldn't be told what to reopen, so nothing starts.
|
||||
|
||||
## Runner presets (`runner: true`)
|
||||
|
||||
A plain preset's seat reads its activity from the screen. A **runner** preset is for a launcher that hosts the agent itself
|
||||
and reports what it's doing, as OpenRig's own Pi runner does. The seat's activity then follows those reports.
|
||||
|
||||
- **It needs `launch_result: true`.** A runner reports the conversation it opened in its launch record, so `runner: true`
|
||||
without `launch_result` is refused.
|
||||
- **The declaration is fixed at launch.** Each launch, a restore's included, records on the session what the preset
|
||||
declares at that moment. Editing the preset changes the next launch, not a running seat. A session recorded before this
|
||||
field existed reads as plain.
|
||||
- **No record means attention.** When a runner's launch record doesn't arrive within the wait, or its folder can't be
|
||||
made, the seat needs attention instead of reading ready. A plain `launch_result` preset still starts, with its identity
|
||||
unavailable.
|
||||
- **Input is as for any seat.** A send pastes the text and presses Enter, so a runner should submit everything up to that
|
||||
Enter as one prompt.
|
||||
- **`launch_wrapper: true` declares wrapper support and requires `runner: true`.** A member with a wrapper and a runner preset without this flag is refused at preflight and launch (not schema validation):
|
||||
`launch_wrapper unsupported: the community preset "<runtime>" runs a runner without launch_wrapper: true; no agent started. Add launch_wrapper: true to the preset if its runner applies OPENRIG_LAUNCH_WRAPPER, or remove launch_wrapper from this member.` See the
|
||||
[harness and runner contract](harness-runner-contract.md#2-declare-it-what-the-preset-says-fixed-at-launch)
|
||||
for the environment handoff, native-child execution and refusal requirements.
|
||||
|
||||
**What the runner posts.** The runner posts each event to `POST /api/activity/hooks` with the local activity hook token,
|
||||
one at a time and in order, without making the agent wait. It finds the address and token in `OPENRIG_URL` (or
|
||||
`OPENRIG_PORT`) and `OPENRIG_ACTIVITY_HOOK_TOKEN`, or else in `$OPENRIG_HOME/activity-endpoint.json`. Every post names:
|
||||
- the seat, as `sessionName` and `nodeId`;
|
||||
- `runtime`, the preset's name;
|
||||
- `generation`, from `OPENRIG_OCCUPANT_GENERATION`;
|
||||
- an increasing `sequence`, and `occurredAt`;
|
||||
- `hookEvent` and `subtype`, as in the table.
|
||||
|
||||
| The runner posts | When | The seat reads |
|
||||
|---|---|---|
|
||||
| `UserPromptSubmit` | it gave the agent a prompt | working |
|
||||
| `active` or `PreToolUse` | the agent is working | working |
|
||||
| `Notification` / `permission_prompt` | the agent asked for a permission | needs input |
|
||||
| `Stop` / the turn's stop reason | the turn ended | idle |
|
||||
| `Stop` / `<preset name>_exited` | the agent's process ended | unknown, never idle |
|
||||
|
||||
- **The screen still counts,** but only as evidence of work: on its own it never reads the seat idle.
|
||||
- **The generation attributes a post; it isn't authentication.** A post with an earlier occupant's generation doesn't
|
||||
count for the live seat. The hook token is one local token, so any process that holds it can post for any seat.
|
||||
- **A runner's full duties** (liveness, input, cancellation and permissions) are in the draft
|
||||
[harness and runner contract](harness-runner-contract.md).
|
||||
|
||||
## An example: Hermes
|
||||
|
||||
[`community-harness-examples/hermes/`](community-harness-examples/hermes/) has a preset and a small launcher for Hermes
|
||||
Agent's CLI.
|
||||
- **The launcher:** it creates the session OpenRig named (`-c <name> --create-if-missing`), and on a resume reopens it
|
||||
without `--create-if-missing`, with its own Hermes home per seat. A name that matches nothing ends with Hermes's own
|
||||
"No session found" error.
|
||||
- **Settings go in the seat's Hermes `config.yaml`, not in flags:** Hermes's CLI creates a named session with no launch
|
||||
flags and then reopens it, refusing any creation flag (model, provider, `--max-turns`) that differs. So the preset's
|
||||
command carries none, and each seat's `config.yaml`, in its Hermes home, sets them before the seat's first start:
|
||||
the model and provider, and `agent.max_turns` if you want a cap (nothing else sets it once the flag is gone).
|
||||
- **Let Hermes run `rig` as the seat:** Hermes runs every tool command in its gateway, one per Hermes home, so with
|
||||
this launcher one per seat (a two-seat run showed separate gateways, homes and sockets, and stopping one seat left
|
||||
the other's running). It runs them with that gateway's
|
||||
environment, and swaps in a session's own values only for its toolchain names and the names its profile lists in
|
||||
`terminal.client_env_passthrough`. So put the seat's OpenRig variables in that list, in each seat's `config.yaml`
|
||||
([`config.yaml.example`](community-harness-examples/hermes/config.yaml.example)). Without it, `rig` inside Hermes
|
||||
runs as whichever seat started the gateway, or as no seat; with it, each seat runs `rig` as itself. To check, ask the
|
||||
seat's Hermes to run `rig whoami --json`. The launcher warns when a seat's `config.yaml` doesn't pass
|
||||
`OPENRIG_SESSION_NAME`. This covers Hermes's CLI and TUI, whose clients pass their environment; its ACP clients pass
|
||||
none.
|
||||
- **The preset:** it declares `names_conversation` but not `resume`, until reopening a named session, and a missing one,
|
||||
have been checked with a real Hermes.
|
||||
- **Stopping:** Hermes's CLI is a client of a gateway, so closing a seat's pane detaches it. Hermes finishes any turn it
|
||||
accepted. Whether the seat's gateway then exits once idle hasn't been checked: check with `ps` that a stopped seat's
|
||||
gateway has gone before relying on it. The preset says so in its limits.
|
||||
|
||||
Copy them and maintain them as your own.
|
||||
|
||||
## An example: OpenClaw
|
||||
|
||||
[`community-harness-examples/openclaw/`](community-harness-examples/openclaw/) has a preset and a small launcher for
|
||||
OpenClaw's CLI.
|
||||
- **Choose the mode:** `openclaw.yaml` runs an embedded agent with `tui --local`. `openclaw-gateway.yaml` runs a TUI client of an existing Gateway. Configure that client's Gateway route and authentication through `OPENCLAW_CONFIG_PATH`; the launcher never starts or stops the Gateway.
|
||||
- **The conversation:** both examples use only agent `main`, mapping OpenRig's name to the full native key `agent:main:<OpenRig name>`. Each seat has its own client state folder.
|
||||
- **Its resume check:** local mode checks `sessions --agent main --json --limit all`; Gateway mode calls the selected Gateway's `sessions.describe`. Both require the exact canonical key. An unavailable lookup stops the launch; a confirmed missing key is never recreated. A client's local cache cannot authorize Gateway resume.
|
||||
- **Stopping:** a Gateway client's pane closing detaches it. The Gateway and an accepted turn can keep running; stopping that service is separate. Local mode runs its embedded agent in the pane.
|
||||
- **Unattended seats:** prefer Gateway mode within the capabilities qualified for your installed version. Local interrupted-turn recovery is not supported by these launchers.
|
||||
- **Local interruption:** Finish the current local turn before stopping; an interrupted local turn may retain native recovery work and refuse the next request. This launcher does not repair or retry it. OpenRig recognizes the recorded local “changed while starting work” error frame as needing attention: “OpenClaw turn not recovered; inspect the pane before retrying”. Use `rig capture <session>` to inspect the native conversation. An ordinary send still submits a new turn, with an advisory that the last local turn was not recovered; it is not an automatic replay of that turn. It does not clear native recovery records, repeat the request or start a fresh conversation; a successful process launch is not proof that the turn recovered.
|
||||
- **Local startup after resume:** OpenClaw may run its database integrity check during startup. A reported resumed process does not confirm readiness for the next request; check that the TUI has finished starting before sending work.
|
||||
- **Resets must stay off:** automatic session resets can create a new conversation under the same key. Typing `/new` also moves to a fresh key while the seat keeps the name OpenRig gave it.
|
||||
- **The presets:** both declare `names_conversation`, `resume` and `first_input`. The launcher passes the generated first startup text through OpenClaw's `--message` option. OpenRig labels the saved name as passed, not observed by its adapter.
|
||||
|
||||
Copy the launcher and the preset for your mode, and maintain them as your own.
|
||||
|
||||
**Stopping a community seat stops its pane.** `rig down` names those seats ("Pane stopped: …"), since a harness's own
|
||||
processes, a gateway say, may keep running after it. OpenRig doesn't stop them, and whether they survive isn't exercised
|
||||
by OpenRig's tests, so check the harness's own processes after `rig down`. Each preset's limits say what to expect.
|
||||
|
||||
**Not supported for community harnesses:** restarting a seat in place (`rig seat restart` is for Codex seats) and
|
||||
switching harness on a running seat. Each says so before anything changes.
|
||||
|
||||
## An example: Cursor
|
||||
|
||||
[`community-harness-examples/cursor/`](community-harness-examples/cursor/) has a preset and a small launcher for Cursor's
|
||||
CLI.
|
||||
- **The launcher:** it reports the chat `cursor-agent create-chat` makes, then reopens it. Each seat gets its own Cursor
|
||||
config folder unless `CURSOR_CONFIG_DIR` is already set. Cursor keeps its chats and `cli-config.json` there, and its
|
||||
login elsewhere.
|
||||
- **The preset:** it declares `launch_result` and `resume`. Cursor's `--resume` with an id it doesn't hold starts a new,
|
||||
empty chat without saying so, so on a resume the launcher first checks that the chat's store exists. When it doesn't, the
|
||||
launcher reports `error: "missing"` and starts nothing.
|
||||
- **First input at launch:** the preset declares `first_input: true` and `startup_assisted: false`. The launcher passes
|
||||
the generated startup text as Cursor's positional first prompt. This route and same-chat resume were checked with
|
||||
Cursor Agent 2026.10.22-27dcf5f. Later tool approvals use Cursor's native prompts; startup submission does not answer them.
|
||||
- **Sender names come from the sending session.** A command run from an unattached operator shell can arrive as
|
||||
`<unknown sender>`; that doesn't mean the receiving Cursor seat lost its identity.
|
||||
|
||||
Copy both and maintain them as your own.
|
||||
|
||||
## An example: Pi's terminal UI
|
||||
|
||||
[`community-harness-examples/pi-tui/`](community-harness-examples/pi-tui/) has a preset and a small launcher that run Pi's
|
||||
own terminal UI as a community harness. `pi` is the built-in managed Pi runtime, so the preset is named `pi-tui`. OpenRig
|
||||
delivers to this seat as to any community seat (the Delivery row above), not as to a plain `runtime: terminal` seat, which
|
||||
queue wakes don't reach. Checked once, on Pi 1.4.0 with OpenRig 0.7.4 and one model: a parked row's wake reached Pi in
|
||||
the same session (the same session file) and Pi resumed the work, and a handoff reached a second `pi-tui` seat, which
|
||||
picked it up unattended. The first seat's task named the commands to use, so this shows a wake reaching Pi and Pi acting
|
||||
on it, not Pi choosing by itself to park or hand work on.
|
||||
- **The launcher:** each seat gets its own Pi agent folder (`$OPENRIG_HOME/state/pi-tui/<node>/agent`, unless
|
||||
`PI_CODING_AGENT_DIR` is set) and its own session folder. At each launch it copies the installed `openrig-core` skills
|
||||
it's given (`queue-handoff` by default) into that agent folder. A new launch starts `pi --session-id <the name OpenRig
|
||||
gave>` and passes the first input as Pi's first prompt, byte for byte.
|
||||
- **Resume isn't declared.** The launcher can reopen a named session: it opens the one session file whose header has that
|
||||
id, and starts nothing when there isn't exactly one, because Pi's own `--session-id` creates a session for an id it
|
||||
doesn't find. Declare `resume: true` only after a real Pi has reopened a named session and refused a missing one.
|
||||
- **Its limits:** run a `pi-tui` seat unattended, not where a person also types into it.
|
||||
- With the seat's [typing guard](seat-delivery.md) at its default, a wake submits a person's half-typed draft in Pi's
|
||||
editor together with the wake. The guard's draft-aware mode isn't a known fix for this.
|
||||
- A send while a Pi dialog OpenRig doesn't recognize is open, such as `/settings`, types into the dialog.
|
||||
- Not shown: how Pi takes a wake that arrives while it's busy (a message sent during a busy turn was answered after
|
||||
it), and other Pi versions.
|
||||
- **Give the seat a model** before its first start: its provider key in its environment, or its login and settings in its
|
||||
agent folder.
|
||||
|
||||
Copy both and maintain them as your own.
|
||||
|
||||
## An example runner: dsh
|
||||
|
||||
[`community-harness-examples/dsh/`](community-harness-examples/dsh/) has a runner preset (`runner: true`) and a small runner
|
||||
for DeepSeek Harness, which speaks ACP on stdio and has no terminal UI of its own. It's written for dsh 0.2.1-rc.2 and
|
||||
checked only against a stand-in agent.
|
||||
- **The runner:**
|
||||
- it opens the conversation and writes the launch record;
|
||||
- it turns each submitted block into one prompt;
|
||||
- it reports turn boundaries as activity hooks;
|
||||
- it cancels on `/abort` or Ctrl-C, as the [runner contract](harness-runner-contract.md) describes.
|
||||
- **Permissions:** it sends each request to OpenRig, which asks the default person or the seat's override.
|
||||
- **When no one can be asked, or OpenRig can't answer,** it permits with dsh's own allow option (`allow_once`, else
|
||||
`allow_always`), which is OpenRig's default. `allow_always` creates a standing grant in dsh.
|
||||
- **It refuses and holds needs input** only after `--deny`. A person's no when dsh offered no reject option, or a
|
||||
request OpenRig says isn't the current occupant's, is answered cancelled.
|
||||
- **Each seat gets its own dsh home,** with dsh's session-log and plugin-list uploads to DeepSeek turned off. Give that
|
||||
home credentials, or put a provider key in the seat's environment.
|
||||
- **Resume isn't declared.** The runner reopens a conversation by the method dsh advertises, and reports one missing only
|
||||
on dsh's own not-found error. Declare `resume: true` only after a real dsh has reopened a conversation and refused a
|
||||
missing one.
|
||||
|
||||
Its [README](community-harness-examples/dsh/README.md) has the setup and a check you can run without a model. Copy both
|
||||
and maintain them as your own.
|
||||
|
||||
The launcher is the contributor's code, in the contributor's package. OpenRig runs no code from a preset and loads no
|
||||
module into its daemon. A preset that sets `launch_result`, `names_conversation`, `startup_assisted`, `first_input`, `runner` or `passes_model` is refused by an older
|
||||
OpenRig that doesn't know the field.
|
||||
|
||||
## Pass the first startup input at launch
|
||||
|
||||
A format-1 preset may set `first_input: true` (a boolean, default false). OpenRig composes the same initial identity turn it would otherwise paste, including the first merged text file, durable-obligation guidance and any selected orientation challenge. Before launch it writes that text to a per-attempt 0600 file under its own `state/first-input/` folder and passes the path as `OPENRIG_FIRST_INPUT`. The typed launch command carries the path, not the text; an argument-based launcher still puts the text in the native harness’s argv.
|
||||
|
||||
The contributor-owned launcher reads the file and uses its harness’s supported initial-prompt route: the Cursor example passes one positional prompt; OpenClaw uses `--message`. Core knows neither flag. If no initial identity turn is due (an ordinary resume, for example), the variable is absent and the launcher starts without a first message. Later startup files and actions use their existing delivery paths; they are not folded into the launch file.
|
||||
|
||||
OpenRig records `node.startup_first_input_passed` with the path and `submission: unconfirmed`. This says the input was passed at launch, not that the native harness submitted or read it. An uncertain launch is not permission to replay it. Readiness remains process-only. `startup_assisted` is independent: when declared, startup still requires a person’s acknowledgement, including when the first input was passed at launch.
|
||||
|
||||
Presets without `first_input` and built-in runtimes keep their existing behavior. Older OpenRig versions reject this new field as unknown. The private input file remains for diagnostics; it can contain private startup context. A launcher that turns its contents into one argument is subject to the platform’s argument-size limit; the launcher must report a launch error rather than silently truncate the turn. The examples’ argument transport is covered with stand-ins; native unattended startup still requires qualification with the installed harness.
|
||||
|
||||
## Pass the seat's declared model at launch
|
||||
|
||||
A format-1 preset may set `passes_model: true` (a boolean, default false). Then a model declared for the seat (in the rig
|
||||
spec, or set later with `rig seat set-model`) reaches its launcher as `OPENRIG_MODEL`, on new and resumed launches alike. A seat that declares no model gets none,
|
||||
and an inherited `OPENRIG_MODEL` is cleared. The launcher maps it into its harness's own setting; Core knows no harness's
|
||||
model flag or config key.
|
||||
|
||||
OpenRig keeps three things apart:
|
||||
- **requested:** the declared model, as `rig ps --nodes` shows it ("configured", or "not-declared");
|
||||
- **passed:** the launcher was handed the model. OpenRig records `node.startup_model_passed` with the model and whether the
|
||||
launch was a resume only when the launch command's send succeeded, and a later failure of the launch doesn't undo that.
|
||||
It never says passed from the declaration alone or for a launch that failed before anything was typed. When typing
|
||||
began and the send then failed, the model's dispatch is uncertain: it isn't recorded as passed, and it isn't retyped;
|
||||
- **effective:** the model that actually runs. OpenRig doesn't observe it for a community harness, so it reads UNKNOWN.
|
||||
|
||||
Passing a model never makes OpenRig pick a default, substitute another model, or retry with one. A model the harness
|
||||
refuses is the harness's own error.
|
||||
|
||||
The examples apply the request for a new session only; on a resume, the example launcher doesn't apply it, and which
|
||||
model a resumed session runs stays unknown unless the harness's own evidence says otherwise:
|
||||
- **Cursor:** `--model "$OPENRIG_MODEL"` for a new chat. What a resumed chat runs hasn't been checked: the launcher leaving
|
||||
out `--model` doesn't show that it keeps its earlier model.
|
||||
- **Hermes:** Hermes takes its model only from the seat's `config.yaml`, so for a new session the launcher writes
|
||||
`model: "<id>"` there only when that file is absent or empty. A seat that has its own `config.yaml` keeps it
|
||||
byte-for-byte, and the launcher says the model wasn't applied: set it in that file. A seat set up from
|
||||
`config.yaml.example` already has a config, so set its model there. Hermes v0.22.4 fixes a session's
|
||||
launch settings when it creates it (its own source), so a resumed Hermes session runs with what it was created with.
|
||||
|
||||
Both example presets ship with `passes_model: false` until that mapping has been checked with the real harness. Presets
|
||||
without the field and built-in runtimes are unchanged, and older OpenRig versions reject it as unknown.
|
||||
|
||||
## Interactive Pi in a terminal seat: the coordination skills
|
||||
|
||||
This is for a seat that runs interactive `pi` from a `runtime: terminal` member's startup, or from a community preset's
|
||||
launcher. It isn't for a managed `runtime: pi` seat, which already copies its selected skill directories into the seat's own
|
||||
Pi agent directory (`$OPENRIG_HOME/state/pi/<session>/agent/skills/`), or for Workshop, whose Pi profiles carry pinned skill
|
||||
copies (openrig-world's `PI-SKILLS.md`). The [`pi-tui` example](#an-example-pis-terminal-ui) above does step 1 for its
|
||||
seats in its launcher.
|
||||
|
||||
OpenRig installs no skills for a terminal or community seat: it skips their resources and startup files, and `rig skill
|
||||
loadout` covers Claude Code and Codex only. Without the coordination skills, the agent there doesn't know the queue's habits,
|
||||
such as parking a row that waits on CI instead of leaving it in progress, or handing work on so the next row names its
|
||||
source. The seat's setup does two things.
|
||||
|
||||
1. **Make the released skills readable.** The installed OpenRig's own plugin carries them, matching the installed release:
|
||||
`rig plugin list` shows `openrig-core` and its path (usually `$OPENRIG_HOME/plugins/openrig-core`), and the skills are in
|
||||
its `skills/` folder. Copy whole skill directories, keeping their helper files and modes, into the `skills/` folder of the
|
||||
Pi agent directory that seat uses (`PI_CODING_AGENT_DIR`, or `~/.pi/agent` by default). At least `queue-handoff`:
|
||||
|
||||
```bash
|
||||
CORE="$(rig plugin list | awk '$1 == "openrig-core" {print $NF}')"
|
||||
PI_AGENT_DIR="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
|
||||
mkdir -p "$PI_AGENT_DIR/skills"
|
||||
cp -Rp "$CORE/skills/queue-handoff" "$PI_AGENT_DIR/skills/"
|
||||
```
|
||||
|
||||
Copy again after each OpenRig upgrade: a copy doesn't follow the plugin.
|
||||
2. **Point the agent at it when it starts.** Don't count on Pi finding the skill by itself; name it in the seat's first turn.
|
||||
For example:
|
||||
|
||||
> Before you work your queue, read `<Pi agent directory>/skills/queue-handoff/SKILL.md` in full: it's how this team hands
|
||||
> work on, parks it and waits. When you wait on CI or a long job, park the row with `rig queue block <id> --on
|
||||
> external:<what> --wake-after <duration> --continuation "<what resumes>"`.
|
||||
|
||||
- A community preset with `first_input: true` carries it in the seat's first input when the file is declared with
|
||||
`delivery_hint: send_text` (see "Pass the first startup input at launch" above). A `.md` file left at the `auto` hint
|
||||
counts as guidance, which a community seat doesn't take.
|
||||
- A plain terminal seat can pass it as Pi's initial message in the command its startup runs. Check `pi --help` for that
|
||||
argument in your Pi version.
|
||||
- `first_input` carries text, not files: it doesn't install the skills.
|
||||
|
||||
**In a plain terminal seat, a queue wake doesn't reach Pi.** OpenRig doesn't treat a `runtime: terminal` seat as an agent
|
||||
seat. When a parked row's wake fires, OpenRig doesn't type it into Pi, and a handoff to that seat records a failed wake
|
||||
(`failed:terminal-seat`). Parking still records the wait, but `--wake-after` won't bring Pi back. Check the seat's parked
|
||||
rows yourself (`rig queue whoami --session <seat>`, or `rig queue show <id>`), or tell Pi to continue the named row.
|
||||
|
||||
**What this shows, and what it doesn't.** A copied folder and a startup line put the text in front of the agent. They don't
|
||||
show that Pi discovered the skill by itself, or that the agent follows it. Check those separately: that the agent read the
|
||||
whole file (its tool trace), and what its rows record, such as a CI wait parked on `external:` with a wake, and a handoff
|
||||
made with `handoff-and-complete`, whose next row names it as its source.
|
||||
|
||||
## Write your own
|
||||
|
||||
Start from the example closest to your harness, and change it. First check that the harness answers a prompt on its own
|
||||
([above](#what-a-community-harness-gets)).
|
||||
|
||||
1. **Choose how the conversation is named.**
|
||||
- **Your harness creates an id you can read before it starts,** as Cursor's `create-chat` does: declare `launch_result: true`, and the
|
||||
launcher writes the record.
|
||||
- **Your harness takes a name from its caller and creates or reopens it,** as Hermes's `-c <name>` and OpenClaw's session key do: declare
|
||||
`names_conversation: true`, and the launcher passes `OPENRIG_CONVERSATION` on.
|
||||
- **Neither:** leave both out. The seat starts and works, its identity shows as unavailable, and it can't be resumed.
|
||||
2. **Write the launcher:** a small script your package installs on the seat's `PATH`. It reads OpenRig's variables, starts the harness with
|
||||
`exec`, and does nothing else.
|
||||
- `OPENRIG_CONVERSATION` with `names_conversation`, or `OPENRIG_LAUNCH_RESULT` and `OPENRIG_LAUNCH_ATTEMPT` with `launch_result`.
|
||||
- **On a resume,** `OPENRIG_RESUME_REF`: reopen exactly that conversation. Check that it exists first, and when it doesn't, end with an
|
||||
error (or report `error: "missing"`) instead of creating one.
|
||||
- **With `first_input: true`,** `OPENRIG_FIRST_INPUT`: pass the file's exact bytes through your harness's own first-prompt route.
|
||||
- **Keep each seat's harness state apart** (Cursor's config folder, Hermes's home, OpenClaw's state folder), so two seats never share a
|
||||
conversation.
|
||||
3. **Write the preset** at `$OPENRIG_HOME/community-harnesses/<name>.yaml`, and check it with `rig community-harness list`.
|
||||
- Declare `resume` only after checking, with the real harness, that a reopen works and that a missing conversation is refused.
|
||||
- List what doesn't work under `limits`.
|
||||
4. **Try it in a rig:** give a member `runtime: <name>`, then go through the checks in the promotion test below.
|
||||
5. **Publish the preset and the launcher in your own package,** and maintain them. OpenRig runs no code from a preset, so the launcher is
|
||||
yours.
|
||||
|
||||
## The promotion test
|
||||
|
||||
A community harness stays a community harness: you maintain it, and OpenRig shows it as one. **Promotion** means OpenRig takes a harness on
|
||||
as a built-in runtime that it maintains and tests itself. **Promotion is decided by the OpenRig maintainers, on evidence from OpenRig's own
|
||||
testing.** Passing these checks isn't automatic promotion, and it isn't a promise of upstream support.
|
||||
|
||||
What the maintainers look at:
|
||||
1. **Everything the preset declares works on a real install,** end to end through OpenRig:
|
||||
- a seat starts and takes its first turn (`rig up`);
|
||||
- a message arrives (`rig send`), and the reply can be read (`rig capture`);
|
||||
- the seat stops (`rig down`) without leaving work running that the person can't see, or the preset's `limits` say what keeps running;
|
||||
- with `resume`, `rig up <rig> --existing` reopens the same conversation, and a missing one is refused, never replaced.
|
||||
2. **What it doesn't support reads as not known,** never as idle, working, ready or verified.
|
||||
3. **Its `limits` are true,** and name anything a person would otherwise be surprised by.
|
||||
4. **The built-in runtimes are unchanged.**
|
||||
5. **It's maintained:** a named maintainer, and a launcher that keeps working with the harness's current release.
|
||||
|
||||
There's no score or threshold. The maintainers weigh the evidence.
|
||||
@@ -1,210 +0,0 @@
|
||||
# Writing a daemon client
|
||||
|
||||
What a program that talks to the OpenRig daemon over HTTP needs and can't read from `rig --help`: how to find the
|
||||
daemon, its health route, which bearer token each route family checks, the event streams, and some route
|
||||
details. It describes the daemon as it is. It adds no promise beyond what each section says, and route shapes not
|
||||
listed here can change.
|
||||
|
||||
## Finding the daemon
|
||||
|
||||
`rig status --json` is the discovery surface. Its `daemon` object gives `state` and `reachable`, and `pid` and
|
||||
`port` when they are known: a stopped daemon's response has no `port`, and a running daemon found through
|
||||
`OPENRIG_URL` has no `pid`. `reachable` is `true` when the daemon answered its health probe, `false` when it is
|
||||
positively down, and `null` when that isn't confirmed (see `rig status` in
|
||||
[the command reference](../as-built/cli-reference.md#status)). It reads the recorded state and probes; it never
|
||||
starts a daemon.
|
||||
|
||||
The CLI itself finds the daemon in these orders:
|
||||
|
||||
- **Data commands** (everything that sends a request): `OPENRIG_URL` (or the legacy `RIGGED_URL`); else
|
||||
`$OPENRIG_HOME/daemon.json` when it holds a valid `pid` and `port`; else the config keys `daemon.host` and
|
||||
`daemon.port` (environment `OPENRIG_HOST` and `OPENRIG_PORT`), which default to `127.0.0.1:7433`.
|
||||
- **Status** (`rig status`, `rig daemon status`): `OPENRIG_URL` (or `RIGGED_URL`) first, probed directly; else
|
||||
the state file below.
|
||||
- **Start and stop** (`rig daemon start`, `stop`): the state file, `$OPENRIG_HOME/daemon.json`, then the legacy
|
||||
`~/.rigged/daemon.json`.
|
||||
|
||||
`daemon.json` holds `pid`, `port`, `host`, `db` and `startedAt`. `rig daemon start` writes a new daemon's record
|
||||
only after its health check passes, so while a start is pending, the file holds no record of the new daemon: it
|
||||
may be missing, or hold an earlier daemon's stale record. It is the CLI's own state file, not a published
|
||||
contract: prefer `rig status --json`.
|
||||
|
||||
## `GET /healthz`
|
||||
|
||||
The liveness and identity read. It always returns `status: "ok"` and `pid`. Every other field is present only
|
||||
in the case named:
|
||||
|
||||
| Field | When it is present |
|
||||
| --- | --- |
|
||||
| `euid` | On platforms with effective user ids. A self-report for the CLI's owner check, not authentication. |
|
||||
| `databaseWarnings` | When the daemon opened its database with warnings. |
|
||||
| `semver`, `commit`, `dirty`, `builtAt` | On a stamped (released) build; a development run has none. |
|
||||
| `selfHostId`, `selfHostIdSource` | After the boot reconcile has set this host's id. |
|
||||
| `eventLoop` | When the event-loop monitor runs: `lagMeanMs`, `lagP99Ms`, `utilization` (0 to 1), `lastTickAgeMs` and `healthy`. |
|
||||
| `routeTimings` | With `eventLoop`. |
|
||||
| `slowOperations` | When the slow-operation recorder runs. |
|
||||
| `bind` | The bind plan: `mode` (`explicit` or `default`), `hosts` (the addresses bound), `tailscaleDetected`, and `ignoredRoutingHost` when a routing value was ignored. |
|
||||
| `stuckSweep`, `wakeLadder` | The heartbeats of the stuck sweep and the wake ladder, when they run. |
|
||||
|
||||
## Bearer tokens
|
||||
|
||||
The daemon reads two tokens from its environment at start:
|
||||
|
||||
- **`OPENRIG_AUTH_BEARER_TOKEN`** checks Mission Control's write routes (`POST /api/mission-control/action` and
|
||||
`/notifications/test`), the `/api/hosts` writes (`add`, `update`, `remove`, `pair`), and the `/api/rig-mode`
|
||||
binding writes. `rig host pair` hands this token to the paired machine, so a daemon that will be paired with
|
||||
needs it set (see `rig host pair` in [the command reference](../as-built/cli-reference.md)).
|
||||
- **`OPENRIG_TERMINAL_BEARER_TOKEN`** checks every `/api/transport` and `/api/compaction` route, `POST
|
||||
/api/terminal/follow` and `/viewer/ensure`, and every `GET /api/terminal/<name>` route, the terminal websocket
|
||||
included. When the daemon is bound to an explicit address (`rig daemon start --host`) that isn't loopback or a
|
||||
tailnet address, and this token isn't set, these routes check `OPENRIG_AUTH_BEARER_TOKEN` instead.
|
||||
|
||||
A route whose token isn't set lets every request through. Send a token as `Authorization: Bearer <token>`; the
|
||||
`GET /api/terminal/<name>` routes, the websocket included, also take `?token=<token>`. The CLI sends the terminal token from `OPENRIG_TERMINAL_BEARER_TOKEN`,
|
||||
or else from the file `$OPENRIG_HOME/terminal-token`, which the person writes; the daemon doesn't create it.
|
||||
|
||||
A wrong or missing token gets HTTP 401. The routes that check `OPENRIG_AUTH_BEARER_TOKEN`, and `/api/transport`
|
||||
and `/api/compaction`, answer `{ "error": "unauthorized", "message", "what_failed", "why_it_matters", "what_to_do" }`.
|
||||
The `GET /api/terminal/<name>` routes, the websocket included, answer `{ "error": "unauthorized", "hint" }`. A valid configured bearer token in the Authorization header can pass the browser boundary's target-name check
|
||||
for a well-formed but unaccepted Host. It cannot pass a malformed Host or a refused Origin; see
|
||||
[Browser access](browser-access.md).
|
||||
|
||||
## Event streams
|
||||
|
||||
All of them are server-sent events (`text/event-stream`):
|
||||
|
||||
- **`GET /api/events`**: the daemon's event log, stored events only. `?rigId=` limits it to one rig. Each event
|
||||
carries `id: <seq>`. `view.changed` is not stored, so it does not appear here, live or replayed; `view.changed`
|
||||
rows stored by older versions still replay until an events retention (a separate change) removes them.
|
||||
Without `Last-Event-ID`, `?replay=<n>` first sends the newest `n` stored events (`0`: none, live events only);
|
||||
`?replay=all`, no `replay`, or any other value sends the whole log first. A `Last-Event-ID` header (what
|
||||
`EventSource` sends when it reconnects) sends the events after that id instead, whatever `replay` says.
|
||||
Live events that arrive during the replay follow it, without duplicates.
|
||||
- **`GET /api/activity/events`**: live only, no replay or ids. Each message is named after its event type:
|
||||
`seat.activity_changed`, `seat.rung_health`, `proof.judged` or `proof.sources_changed`. It tells a view to read
|
||||
again; it carries no derived state.
|
||||
- **`GET /api/views/sse`** (alias `/api/views/watch`) and **`GET /api/views/<viewName>/sse`**: `view.changed`, live
|
||||
only. Its `id:` is the `seq` of the stored event that caused it, so it can repeat across views and is not a replay
|
||||
cursor.
|
||||
- **`GET /api/queue/sse`** (alias `/api/queue/watch`): live only, with `id: <seq>`, for `queue.created`,
|
||||
`queue.handed_off`, `queue.claimed`, `queue.unclaimed`, `qitem.fallback_routed`, `qitem.closure_overdue`,
|
||||
`inbox.absorbed` and `inbox.denied`.
|
||||
|
||||
## People, asks and updates without Slack
|
||||
|
||||
`GET /api/gateway/participants` returns the same JSON projection as `rig gateway participants --json`:
|
||||
`{ ok, participants, default, organizations, groups, warnings, participantFile }`. Pick a person by `kind: "person"`
|
||||
and use their `id` for a Feed `view`, or their `address` for the queue's `destinationSession`. The effective
|
||||
`default` is `{ id, source }`, or `{ id: null, reason }`; it does not establish who is using the client.
|
||||
Participant records omit connector credential pointers. Check `warnings` and `participantFile.error` for a partial
|
||||
registry read; an unreadable human registry returns HTTP 503. See [Gateway participants](gateway-participants.md).
|
||||
|
||||
| Read | Meaning |
|
||||
| --- | --- |
|
||||
| `GET /api/queue/list?destinationSession=<address>&state=pending,in-progress,blocked&limit=100` | Open rows for exactly that address. `humanIntent: "update"` identifies an FYI; other values are asks. Each row has `qitemId`, `state`, `summary` and `body`. |
|
||||
| `GET /api/queue/open-human-updates?view=<person-id>&limit=20` | Open human FYIs, even when no Slack delivery receipt exists. Returns `{ items, limit, truncated, perspective? }`. |
|
||||
| `GET /api/queue/<qitemId>` and `/<qitemId>/transitions` | The current row and its history, including closed state and recorded outcome. Closing or acknowledging a row is different from delivering it. |
|
||||
| `GET /api/queue/human-updates?view=<person-id>&limit=20` | Existing delivered-update history: explicit FYIs with a recorded Slack posting receipt, including closed rows. Items add `deliveredAt` and `deliveryReceipt`. It is not the open-update inbox. |
|
||||
|
||||
Both update lists accept integer `limit` values from 1 to 100 (default 20); invalid values return HTTP 400.
|
||||
Open FYIs are ordered by priority, then newest first. `view=all` (also the default) shows all perspectives;
|
||||
`view=default` selects the effective default person, or all when no default is available. A named view that cannot
|
||||
resolve a person has `perspective.basis: "unknown"` and no items when a registry is available. With no registered
|
||||
people there is no perspective filter: inspect `perspective` rather than assuming the requested view was applied.
|
||||
A person's view also includes instance or unassigned items when that person is the default. These are views on a
|
||||
trusted daemon, not per-person authentication or access control; existing daemon and browser access rules still apply.
|
||||
|
||||
Always display `truncated: true` as incomplete, even if fewer than `limit` items arrive. A person-view scan examines
|
||||
at most 1,001 open updates (101 for delivered history) before filtering; older matching items may be outside that
|
||||
window. These endpoints have no pagination cursor. Reading an open update never posts it to Slack, acknowledges it,
|
||||
or manufactures a delivery receipt.
|
||||
|
||||
For example, this Node client takes a daemon URL, a registered person id and a rig name. It uses no Slack API and
|
||||
makes only reads; use the daemon discovery and access rules above. In an ES module, `process.argv.slice(1)` is the
|
||||
argument list for `node --input-type=module -e '<code>' <url> <person-id> <rig>`:
|
||||
|
||||
```js
|
||||
// client-people-example
|
||||
const [base, personId, rig] = process.argv.slice(1);
|
||||
async function get(path) {
|
||||
const response = await fetch(new URL(path, base));
|
||||
if (!response.ok) throw new Error(`${path}: HTTP ${response.status}`);
|
||||
return response.json();
|
||||
}
|
||||
const registry = await get("/api/gateway/participants");
|
||||
const person = registry.participants.find(p => p.kind === "person" && p.id === personId);
|
||||
if (!person) throw new Error("Choose a registered person id");
|
||||
const rows = await get(`/api/queue/list?destinationSession=${encodeURIComponent(person.address)}&state=pending,in-progress,blocked&limit=100`);
|
||||
const updates = await get(`/api/queue/open-human-updates?view=${encodeURIComponent(person.id)}&limit=20`);
|
||||
const receipts = await get(`/api/queue/human-updates?view=${encodeURIComponent(person.id)}&limit=20`);
|
||||
const rigHistory = await get(`/api/queue/list?rig=${encodeURIComponent(rig)}&limit=100`);
|
||||
const messages = await get(`/api/queue/outbox/list?rig=${encodeURIComponent(rig)}&limit=100`);
|
||||
console.log(JSON.stringify({ participants: registry.participants, default: registry.default,
|
||||
warnings: registry.warnings, participantFile: registry.participantFile,
|
||||
asks: rows.filter(row => row.humanIntent !== "update"), updates, receipts, rigHistory, messages }, null, 2));
|
||||
```
|
||||
|
||||
The queue and outbox lists are bounded snapshots, not subscriptions. A full queue list may need a larger `limit`.
|
||||
An outbox entry's `deliveryState` reports the send attempt; only `delivered` confirms delivery. A queue transition,
|
||||
an open FYI, or a closed ask is not that receipt. The rig-wide reads and their limits are described below; the existing
|
||||
`outbox/list?senderSession=<session>` read remains available.
|
||||
|
||||
## Route details
|
||||
|
||||
- **A rig's queue history:** `GET /api/queue/list?rig=<rig>&updatedSince=<ISO time>&sort=updated` returns the
|
||||
rig's queue items (any whose source or destination seat is in the rig) updated at or after that time: open items
|
||||
(`pending`, `in-progress`, `blocked`) first, then the rest by latest update. `limit` defaults to 100 and has no
|
||||
upper cap. The list is cut after `limit` rows in that order, so when it comes back full, raise `limit` to read
|
||||
them all. Each item's own history is `GET /api/queue/<qitemId>/transitions`. This read scans the queue table, so
|
||||
read it occasionally rather than polling it. For live changes, treat `GET /api/queue/sse` events as hints to read
|
||||
`queue/list?rig=` again. Not every event names both seats (`queue.claimed` and `queue.unclaimed` carry only the
|
||||
destination), and not every state change is on that stream, so the events alone don't track a rig's items.
|
||||
- **A rig's messages:** `GET /api/queue/outbox/list?rig=<rig>` returns the messages sent from or to the rig's
|
||||
seats, newest first; `?senderSession=<session>` still returns one sender's. One of the two is required; with
|
||||
both, `senderSession` is used and `rig` is ignored. `limit` defaults to 100. No event is sent when a message is delivered, so read this list to see delivery
|
||||
states.
|
||||
- **`GET /api/queue/recent-transitions`** returns at most 20 rows. A positive `limit` above 20 is capped at 20;
|
||||
a missing, zero, negative or unparseable `limit` means 20.
|
||||
- **`POST /api/transport/send`** takes `urgent: true`, the same as `rig send --urgent`. The daemon records who
|
||||
marked it from the sender identity, not from the body.
|
||||
- **`POST /api/scope/approve`** is what `rig scope slice approve` and `rig scope mission approve` call. The body
|
||||
takes `scopeTier` (`slice` or `mission`), `scopePath`, and optionally `approvalScope` (`spec` or `delivery`, the
|
||||
default), `missionsRoot`, `onBehalfOf`, `reApprove`, `reason` and `lockedArtifacts`. The approver is the
|
||||
request's sender identity (`actorSession` in the body is recorded beside it, and a different value is
|
||||
superseded). Success is 201 with `scopeTier`, `scopeId`, `scopePath`, `approvalScope`, `approvedBy`,
|
||||
`approvedAt`, `onBehalfOf`, `actionId`, `freezeFired` and `reApproved`. Refusals include 400 for an invalid
|
||||
`scopeTier`, `scopePath` or `approvalScope`, or a missing actor (`actor_required`); 503 `action_log_unavailable`;
|
||||
and, from the approval itself, 404 `scope_not_found`, 409 `already_approved` or `nothing_to_reapprove`, 503
|
||||
`workspace_not_configured`, 500 `audit_write_failed`, and 400 for its other refusals. An unexpected error is 500
|
||||
`internal`.
|
||||
- **`POST /api/proof/judge`** is what `rig proof judge` calls. The body takes `scope`, `item`, `verdict`
|
||||
(`accept`, `reject` or `withdraw`), `reason`, `expectedRevision` and `expectedPrevious` (a judgment id or
|
||||
`null`), and optionally `evidence`, `expectedEvidence`, `subject`, `operationId`, `replace`, `project` and
|
||||
`projectRoot`. The judge is the request's sender identity. The response is `judgment`, the slice's `readiness`,
|
||||
`replayed` and `notification`: 201 for a new judgment, 200 when an `operationId` replays an earlier one.
|
||||
- **`POST /api/workflow/instantiate`** requires `specPath`, `rootObjective` and `createdBySession`, and takes
|
||||
`entryOwnerSession`, `targetRig` and `scope` (see [Project workspace](project-workspace.md) for `scope`).
|
||||
Success is 201 with `instance`, `spec`, `entryQitemId` and `entryOwnerSession`, any `advisories`, and
|
||||
`scopeIgnored` when a scope wasn't applied. The instance keeps `rootObjective`, which every instance read returns
|
||||
(`/api/workflow/list`, `/api/workflow/<id>` and its `trace`). An instance started before it was kept reads `null`.
|
||||
- **`GET /api/provider/usage`** returns `{ hostUsage }`, this host's usage rows (`host: "local"`, a `provider`
|
||||
and a `state`). A Claude row's `windows` are the seats' status-line readings, one entry per reading, each with
|
||||
its `window`, `usedPercent`, `resetsAt`, `asOf` and `seatSession`. A Codex row has `windows: []`. Treating the
|
||||
readings as one account rests on the deployment assumption the row's `provenance` names (one account per
|
||||
host), not on a measured identity. Readings whose reset times disagree are reported as `anomalies`, and the
|
||||
row's `state` can then be `explicit_unknown`. Show the row's `state`, `anomalies` and each reading's seat and
|
||||
`asOf` as reported, rather than merging readings that happen to match.
|
||||
- **Slack channels and people:** `GET /api/gateway/slack/channels` and `GET /api/gateway/slack/users` each return
|
||||
one page from Slack, read with the daemon's configured bot token, which never leaves the daemon. Channels are
|
||||
public and unarchived, as `{ id, name, isMember }`; people are `{ id, name, realName, displayName, isBot,
|
||||
deleted }`, with no email. Pass `cursor` (the previous page's `nextCursor`, `null` on the last page) and
|
||||
`limit` (default 100, at most 200). Inbound delivery needn't be enabled. Failures: 503 `slack_not_configured`
|
||||
when no bot token resolves, which is different from an empty list; 403 `slack_missing_scope` with the scope
|
||||
Slack names in `needed` (an older app may lack `users:read`); 429 `slack_rate_limited` with Slack's
|
||||
`Retry-After`; otherwise 502. Each request is a live Slack call, so page through a list when someone opens a
|
||||
chooser rather than polling it. Private channels and direct messages aren't listed.
|
||||
- **Slices are found by folder name alone.** `GET /api/slices/:name`, `/:name/doc/*`, `/:name/proof-asset/*` and
|
||||
`GET /api/review/slice/:name` take only the name; the detail route reads `?mission` only together with
|
||||
`?project`. When two missions have a slice folder of the same name, the daemon keeps the first one it finds:
|
||||
that one is listed and served, and the other doesn't appear in `GET /api/slices` at all. Keep slice names unique
|
||||
across missions, as [Project workspace](project-workspace.md) says.
|
||||
@@ -1,54 +0,0 @@
|
||||
# Developing OpenRig — gates and lanes
|
||||
|
||||
This is the contributor-facing statement of which checks BLOCK a change and which are
|
||||
advisory. Every pull request to `main` runs `.github/workflows/tests.yml` on a clean GitHub
|
||||
runner: build and packaging, typecheck, the repository scripts (`npm run test:repo`), each
|
||||
workspace's test suite (daemon, cli, tui and ui) on macOS without ambient credentials or
|
||||
external network, and an installed-package scenario (`scripts/run-pr-scenarios.sh`). The root
|
||||
`package.json` scripts below run the same build, typecheck, repository and test checks locally,
|
||||
with one difference: `npm test` does not include the UI suite, which CI runs as
|
||||
`package-tests (ui)`. Run `npm run test:ui` for it.
|
||||
|
||||
**In a fresh clone or worktree, run `npm ci`, then `npm run build`,** as CI's build-and-package and
|
||||
package-test jobs do (typecheck and repo-checks build only the daemon, through their scripts; see
|
||||
`CONTRIBUTING.md` and [worktree builds](worktree-builds.md)).
|
||||
Locally, `npm test` builds only the daemon, and `test:workspaces` and `test:ui` build nothing.
|
||||
|
||||
## Blocking gates (must pass before a candidate moves)
|
||||
|
||||
| Gate | Command | Covers |
|
||||
|---|---|---|
|
||||
| Typecheck | `npm run lint` | daemon + **ui** + cli + tui tsconfigs — UI typecheck STAYS blocking |
|
||||
| Build | `npm run build` | all workspaces — the UI dist ships in the package, so its build STAYS blocking |
|
||||
| Repo scripts | `npm run test:repo` | daemon build, script self-tests, first-run reference check, docs guard, skill mirror check, context-pack generation check |
|
||||
| Unit tests | `npm run test:workspaces` | `packages/daemon` + `packages/cli` + `packages/tui` |
|
||||
| UI unit tests | `npm run test:ui` | `packages/ui` vitest. NOT part of `npm test`, but every pull request runs it as `package-tests (ui)` (the `package-tests` matrix in `.github/workflows/tests.yml`) |
|
||||
| Installed-package scenario | CI only: `scripts/run-pr-scenarios.sh` | the installed package, a daemon restart and a seeded durability failure. The script refuses to run outside GitHub Actions unless given `--remote` with `DOCKER_HOST=ssh://…` |
|
||||
| Package file lists | CI only | records each package's published file list (`npm pack --dry-run`) |
|
||||
|
||||
`npm test` runs `test:repo` and `test:workspaces`; `npm run test:ui` is the separate UI
|
||||
suite. Both sets are readable in the root `package.json` scripts.
|
||||
|
||||
## Advisory lane
|
||||
|
||||
| Lane | Command | Meaning |
|
||||
|---|---|---|
|
||||
| Portability report | `node scripts/portability-report.mjs` | lists added lines with machine-specific values; findings never fail it (`.github/workflows/portability-report.yml`) |
|
||||
|
||||
## The norm (web-UI freeze at 0.5.0)
|
||||
|
||||
Daemon API changes no longer require browser or interaction verification of the UI, and
|
||||
the contract mirrors under `packages/ui/src/hooks/` are no longer proactively maintained. A
|
||||
new `test:ui` failure usually signals a moved API contract. Because pull-request CI runs the
|
||||
UI suite as `package-tests (ui)`, the change that moves the contract has to update the affected
|
||||
UI test or mirror to pass that check.
|
||||
|
||||
Browser/interaction testing of the web UI is not a contributor gate. (The packaged
|
||||
starter-rig agent skills that exercise the UI are product content for user rigs, not
|
||||
part of this repo's gates.)
|
||||
|
||||
## Wording rule
|
||||
|
||||
The web UI is **experimental**, in **maintenance mode**, supported **best-effort**;
|
||||
**the CLI is primary**. There is no scheduled removal and PRs are welcome. Do not
|
||||
describe the UI with stronger end-of-life language than this section uses.
|
||||
@@ -1,8 +1,8 @@
|
||||
# Edge Types Reference
|
||||
|
||||
Historical whole-document validation: 2026-10-07, at the 0.6.6 cut `2620dea8` (whole document at `fcaf1f8e`).
|
||||
The fallback escalation section and deferred-target exceptions below describe #605.
|
||||
Source of truth: `packages/daemon/src/domain/rigspec-schema.ts`, `packages/daemon/src/domain/rigspec-instantiator.ts`, `packages/daemon/src/domain/queue-owner.ts`, `packages/daemon/src/domain/escalation-owner.ts`
|
||||
Version: 0.2.0
|
||||
Last validated against code: 2026-04-11
|
||||
Source of truth: `packages/daemon/src/domain/rigspec-schema.ts`, `packages/daemon/src/domain/rigspec-instantiator.ts`
|
||||
|
||||
---
|
||||
|
||||
@@ -17,83 +17,46 @@ See `docs/reference/rig-spec.md` for the YAML syntax.
|
||||
|
||||
## Edge Kinds
|
||||
|
||||
Five edge kinds are accepted by both the pod-aware and legacy flat-node validators:
|
||||
Five edge kinds are accepted by the validator:
|
||||
|
||||
| Kind | Accepted | Has Runtime Behavior | Description |
|
||||
|------|----------|---------------------|-------------|
|
||||
| `delegates_to` | yes | **yes — launch order and queue escalation** | Source delegates work to target |
|
||||
| `spawned_by` | yes | **yes — affects launch order** | Source was spawned by target |
|
||||
| `delegates_to` | yes | **yes — affects launch order** | Source delegates work to target |
|
||||
| `spawned_by` | yes | **yes — affects launch order** | Target was spawned by source |
|
||||
| `can_observe` | yes | no | Source can observe target's output |
|
||||
| `collaborates_with` | yes | no | Peer collaboration |
|
||||
| `escalates_to` | yes | **yes — fallback queue escalation** | Source escalates to target |
|
||||
| `escalates_to` | yes | no | Source escalates to target |
|
||||
|
||||
## What Edges Actually Do Today
|
||||
## What Edges Actually Do Today (OpenRig 0.1.x)
|
||||
|
||||
### Launch ordering
|
||||
|
||||
Only `delegates_to` and `spawned_by` constrain the order in which nodes are launched:
|
||||
Only `delegates_to` and `spawned_by` affect runtime behavior. They constrain the order in which nodes are launched:
|
||||
|
||||
- **`delegates_to`**: source launches BEFORE target. The delegator must be up before the delegate.
|
||||
- **`spawned_by`**: target (parent) launches BEFORE source (child). The parent must be up before the child it spawned.
|
||||
|
||||
This ordering is enforced in both `PodRigInstantiator` (initial launch) and `RestoreOrchestrator` (restore from snapshot). The code uses topological sort over the dependency graph — if there's a cycle, instantiation fails with `cycle_error` before the rig is created. A `delegates_to` or `spawned_by` edge from a member to itself counts as a cycle. Restore uses the same sort but has no cycle check of its own.
|
||||
This ordering is enforced in both `PodRigInstantiator` (initial launch) and `RestoreOrchestrator` (restore from snapshot). The code uses topological sort over the dependency graph — if there's a cycle, instantiation fails.
|
||||
|
||||
All other edge kinds (`can_observe`, `collaborates_with`, `escalates_to`) do NOT constrain launch order.
|
||||
|
||||
### Queue escalation
|
||||
|
||||
`delegates_to` also names a seat's orchestrator for the queue: the source of the `delegates_to` edge into a seat. The
|
||||
wake ladder escalates an unacknowledged row to that orchestrator, and stuck-row findings and recovery go to it. A seat
|
||||
with no incoming `delegates_to` edge has no default orchestrator. `spawned_by` plays no part here.
|
||||
|
||||
### Fallback escalation across rigs
|
||||
|
||||
A seat with **no declared `delegates_to` parent** may declare one `escalates_to` destination. The wake ladder and
|
||||
stuck sweep use it for new recovery rows; they do not move the original obligation or reassign an already-open recovery. A stopped or unbound declared
|
||||
parent still takes precedence, preserving the existing parent route and its fallback.
|
||||
|
||||
```yaml
|
||||
edges:
|
||||
- kind: escalates_to
|
||||
from: work.lead
|
||||
to: review-advisor@coord
|
||||
```
|
||||
|
||||
The target is a canonical same-daemon seat address (`pod-member@rig`), or an existing local member using the usual
|
||||
edge syntax. Named targets are stored without requiring the other rig to exist and resolved when needed. A matching
|
||||
physical session name takes precedence over a derived canonical address. Deferred declarations survive export and
|
||||
snapshots; they don't constrain launch order or make the rigs depend on each other's lifecycle.
|
||||
|
||||
Delivery is one hop. Recovery rows carry `escalation-route:one-hop`, so a destination's own `escalates_to` declaration
|
||||
does not forward the recovery again. A missing or stopped destination, multiple matches or declarations, self route,
|
||||
or declared cycle preserves the existing fallback: the wake ladder proceeds to its operator rung; the sweep retains
|
||||
its existing owner fallback. The recovery body/transition explains the result. Cycle inspection includes stopped
|
||||
seats and delegation parents; an unresolved downstream endpoint is reported as unknown, never as proof of no cycle.
|
||||
Queue address admission is unchanged. Cross-host and synthetic destinations are not supported.
|
||||
|
||||
On an existing rig, use `rig edge add <rig> work.lead review-advisor@coord --kind escalates_to`; `--plan` writes nothing.
|
||||
The receipt's target node ID is null for a deferred address. Remove it with the exact edge ID returned by add.
|
||||
A rig-local graph omits deferred targets, since those seats are outside that graph; the raw topology and export retain them.
|
||||
|
||||
### Graph visualization
|
||||
|
||||
Local-node edges are rendered in the UI topology graph; deferred cross-rig targets are omitted. They create visual connections between nodes, helping the operator understand the team structure. Pod-local edges appear within the pod group; cross-pod edges connect across groups.
|
||||
All edges are rendered in the UI topology graph. They create visual connections between nodes, helping the operator understand the team structure. Pod-local edges appear within the pod group; cross-pod edges connect across groups.
|
||||
|
||||
### Identity projection
|
||||
|
||||
Local-node edges appear in `rig whoami --json` output under `edges.outgoing` and `edges.incoming`, with their kind and the connected peer's identity. This lets an agent know its relationship to other members — but the agent interprets this information, it's not enforced by the runtime.
|
||||
All edges appear in `rig whoami --json` output under `edges.outgoing` and `edges.incoming`, with their kind and the connected peer's identity. This lets an agent know its relationship to other members — but the agent interprets this information, it's not enforced by the runtime.
|
||||
|
||||
### Attach hint heuristic
|
||||
|
||||
The post-command handoff (the "Attach:" line after `rig up` and `rig restore`) doesn't use edges. It names the first
|
||||
node, in creation order, whose session is running, or else the first node with a session name.
|
||||
The post-command handoff (the "Attach:" line after `rig up` and `rig restore`) uses edges to prefer the orchestrator as the default attach target. The heuristic looks for the first node with `delegates_to` outgoing edges.
|
||||
|
||||
## What Edges Do NOT Do Today
|
||||
|
||||
Edges do NOT currently:
|
||||
|
||||
- **Route messages** — `rig send` can target any session, regardless of edges (queue escalation, above, is the one
|
||||
daemon path that follows `delegates_to` or fallback `escalates_to`)
|
||||
- **Route messages** — `rig send` can target any session, regardless of edges
|
||||
- **Enforce delegation** — an agent can communicate with any peer, not just its edge targets
|
||||
- **Control permissions** — there's no edge-based access control
|
||||
- **Affect transport** — `rig capture`, `rig broadcast`, etc. work based on session identity, not edge topology
|
||||
@@ -157,7 +120,7 @@ If none fit, `can_observe` is the safest default — it documents the relationsh
|
||||
## Validation Rules
|
||||
|
||||
1. Edge kinds must be one of: `delegates_to`, `spawned_by`, `can_observe`, `collaborates_with`, `escalates_to`
|
||||
2. Pod-local edges use unqualified member IDs — both endpoints must exist in the same pod, except a named `escalates_to` target as above.
|
||||
3. Cross-pod edges use `pod.member` format — both endpoints must resolve to actual members, except a named `escalates_to` target as above.
|
||||
2. Pod-local edges use unqualified member IDs — both `from` and `to` must exist in the same pod
|
||||
3. Cross-pod edges use `pod.member` format — both endpoints must resolve to actual members
|
||||
4. Cross-pod edges must reference different pods (same-pod edges should use the pod-local syntax)
|
||||
5. Self-edges are not validated at the schema level but are not meaningful
|
||||
@@ -1,138 +0,0 @@
|
||||
# Execution and platform integration contract — draft
|
||||
|
||||
This chapter describes the placement and resource responsibilities of an integration author. The harness/runner chapter owns native protocol, model input, activity, attention, permission exchange and turn cancellation. This chapter owns where that harness runs, which files and credentials it can reach, how native state survives an environment change, and how results and resources return to their owners.
|
||||
|
||||
**Status:** a design contract for authors, with an existing-compatible composition and one measured private implementation. Since 0.7.8, OpenRig also ships one maintained reference driver, OpenShell 0.1.6, as `rig environment` (see the [OpenShell example](community-harness-examples/openshell/README.md)). The measured baseline used OpenRig 0.7.4, OpenShell 0.1.6 and Claude Code 2.1.310. 0.7.6 published this draft; neither it nor the reference driver implements a generic environment driver, a `DeploySpec` loader or a cross-host native-resume guarantee. Proposed operations below are not commands or accepted configuration keys. Capability support must be stated for the actual integration and version.
|
||||
|
||||
## Keep the identities and responsibilities separate
|
||||
|
||||
RigSpec composes members, their AgentSpecs/profiles, runtimes, working directories and relationships. AgentSpec supplies reusable role, startup and context declarations. A built-in runtime or registered community preset supplies a launch path. An environment integration supplies placement and provider-specific resource handling around that path. Existing declarations do not automatically provision a remote filesystem, credentials or compute.
|
||||
|
||||
| Identity or handle | Meaning and responsibility |
|
||||
|---|---|
|
||||
| Stable seat and work identity | The member's canonical address/node and owned work records. OpenRig retains this identity across executions; an environment IP or display name does not replace it. |
|
||||
| Native session identity | The harness's conversation token/UUID, qualified by harness and resume-reference type. The harness owns its format and resume semantics. |
|
||||
| Environment binding | The integration's durable record linking placement, immutable provider resource IDs, native-state location, projection and ownership. It is separate from the native session token. |
|
||||
| Execution generation | A particular launch/process instance, with observable process/start identity when available. A new process can resume an old conversation. |
|
||||
| Viewer/client | A terminal attachment or connection to an execution. Closing it need not stop either the native process or compute. |
|
||||
|
||||
An application service bundled with a rig is a separate responsibility. An artifact browser or database in `RigSpec.services` serves the team's application; its presence does not place an agent inside that service. Bundling service definitions also does not bundle image bytes, native binaries, authenticated homes or live runtime state.
|
||||
|
||||
## A minimal composition using existing surfaces
|
||||
|
||||
The following is a **fragment of one pod** (`pods[].members`), using existing RigSpec fields, not a complete standalone RigSpec:
|
||||
|
||||
```yaml
|
||||
pods:
|
||||
- id: dev
|
||||
label: Development
|
||||
members:
|
||||
- id: coordinator
|
||||
agent_ref: local:agents/coordinator
|
||||
profile: default
|
||||
runtime: claude-code
|
||||
- id: worker
|
||||
agent_ref: local:agents/worker
|
||||
profile: default
|
||||
runtime: example-contained-claude
|
||||
```
|
||||
|
||||
The worker runtime names an author-installed community preset. This preset shape uses existing fields exercised on the 0.7.4 baseline:
|
||||
|
||||
```yaml
|
||||
format: 1
|
||||
name: example-contained-claude
|
||||
maintainer: Integration author
|
||||
command: ./scripts/launch-contained-worker
|
||||
launch_result: true
|
||||
resume: true
|
||||
startup_assisted: true
|
||||
limits:
|
||||
- Requires an explicitly prepared environment binding and guest projection.
|
||||
- Native resume is qualified only for the documented retained-state recipe.
|
||||
- Viewer closure does not implement turn cancellation or resource deletion.
|
||||
```
|
||||
|
||||
This is an integration skeleton, not an installed or qualified preset. The author must supply the executable, resolve its path against the actual launch working directory, register the preset in the existing OpenRig home, and prepare the member's host working directory. The launcher implements the preset's declared launch-result and typed-resume behavior; the runner chapter defines those mechanics. `startup_assisted` requires a startup-submission check; it does not hold back submission or establish successful context delivery or ingestion. A member's model setting is not automatically honored by a custom launcher: its resolved native model/effort must be explicit in that launcher's supported configuration. With `passes_model: true`, Core passes the declared model through `OPENRIG_MODEL`; the launcher must map and apply it. The effective model remains unknown to Core.
|
||||
|
||||
For a contained worker, the launcher may resolve a private binding, create or attach to the selected resource, project explicit inputs, then connect to the native process inside it. In the measured recipe, OpenRig ran on the host, the plain coordinator used the built-in native runtime, and the worker used a private community launcher. The wrapper performed projection, provider-reference delivery, resume validation and artifact export explicitly. Those effects were not supplied by the preset declarations themselves. The integration must not assume host guidance/skills, hook paths or environment variables automatically reach a community runtime's guest.
|
||||
|
||||
A downstream author can implement this composition today around a qualified provider API and native harness. It remains an author-maintained integration until maintained platform support is separately implemented and documented. Validate against the target OpenRig schema and effective launch configuration; a parsed field or successful process start alone does not establish its effect.
|
||||
|
||||
## Placement and launch receipts
|
||||
|
||||
Before mutation, resolve the selected host/provider, workspace, image/native versions, reachability, credential references, resource request and ownership. Record existing resources and the selected resource scope. Host headroom includes supervisors, gateway and other team members; a workload memory request is not proof of their combined cap. Report requested, admitted and observed resources separately. Do not infer enforced CPU/memory or isolation policy from accepted flags.
|
||||
|
||||
Persist a launch intent before a create operation. Use an attempt/retry identity where the provider supports it; otherwise reconcile the original operation before another create. A public binding receipt carries only nonsecret values. If the harness's native resume reference works like a bearer credential (anyone holding it can reopen the conversation), keep it in secret-held receipt data, never in the public receipt. A useful public receipt identifies:
|
||||
|
||||
- canonical seat, harness version and native resume-reference type;
|
||||
- provider/host/workspace, immutable environment ID and execution generation;
|
||||
- image/policy versions, requested resources and observed readiness;
|
||||
- inner working directory, private native-state location and projection manifest;
|
||||
- a public-safe native reference (never a bearer-like resume token), creator or attached-consumer disposition, and exact owned resource handles;
|
||||
- lifecycle operations supported, completion observations, pending effects and errors.
|
||||
|
||||
A resource name or IP can be reused. Reconcile immutable ID, workspace and any available generation/fingerprint before acting. A lost response is **unconfirmed**, not success or absence. Record provider failures without exposing credentials. Readiness should identify what is ready: provider reachable, filesystem projected, native process started, team identity authenticated, or task completed.
|
||||
|
||||
## Project files and credential references deliberately
|
||||
|
||||
Stage only declared inputs: role/startup material, selected skills, current work context, schemas and required native settings. Preserve unmanaged files and report conflicts. Review every path-bearing setting for its destination: host-only hook scripts, interpreters, status collectors and loopback URLs may not exist or be reachable in the guest. Missing activity or permission transport must remain unsupported rather than being inferred from the base harness name.
|
||||
|
||||
Distinguish three observations: host staging, destination delivery and native ingestion. A destination manifest can record relative path, byte count and hash; a useful task using the current context revision/nonce can show consumption. Neither observation substitutes for the other. Current working input may change between tasks while an earlier artifact stays immutable.
|
||||
|
||||
Retained native history is a different data class from ordinary project context. Preserve its exact bytes through the harness's supported resume path; do not edit records or synthesize a conversation. Preserve a complete checkpoint prefix with byte length/hash if the harness may append after the checkpoint. Record which native-state files are required and which remain unavailable. Never substitute a fresh conversation seeded with a report while calling it native resume.
|
||||
|
||||
Keep secret values out of RigSpec, preset YAML, bindings, argv and public receipts. The provider/credential owner supplies scoped references or native-supported credential helpers and controls revocation. A successful login, provider registration or model-list request is not evidence of usable inference credit or an authorized spend envelope. Do not infer subscription portability from a provider name or copy an authenticated owner home into a guest.
|
||||
|
||||
Authenticate guest coordination through the actual granted member route. A shared source IP is not a seat identity. Record the grant-bound principal and scope separately from resource placement; distinct principals do not imply per-seat row ACLs or isolation between co-located secrets.
|
||||
|
||||
## Own only the effects assigned to the integration
|
||||
|
||||
Record **created** versus **attached** for each launch receipt. The creator may stop/delete the exact resource it created under the selected policy. An attached consumer normally closes only its own client and any separately identified process it owns; it must preserve the external environment, native session, workspace and shared services. Attachment must not silently transfer provider custody. An explicit owner-authorized transfer is a separate recorded action.
|
||||
|
||||
Describe these effects independently:
|
||||
|
||||
| Operation | Required distinction |
|
||||
|---|---|
|
||||
| Cancel a turn | Ask the runner to stop current work and observe its result. Unsupported cancellation must not be replaced silently by killing compute. |
|
||||
| Close a viewer | Release that client/connection; report whether native execution continues or is unknown. |
|
||||
| Stop native execution | End the identified native process and observe its exit. This is separate from task success. |
|
||||
| Stop compute | Release the documented provider allocation; identify retained storage and actual provider/process state. |
|
||||
| Delete an owned environment | Remove the exact creator-owned resource and reconcile final absence, including provider-managed dependents. |
|
||||
|
||||
Use graceful shutdown first where supported, then only explicitly owned, identified handles for stronger termination. Never equate pane disappearance with process or resource release. Persistent/non-persistent lifetime describes when execution should stop; durability describes recovery. An explicit non-persistent completion condition must name its invocation/work scope. An unrelated queue closure or an empty queue is not permission to terminate a member, and no new lifecycle YAML syntax is specified here.
|
||||
|
||||
## Resume exactly, or return an honest disposition
|
||||
|
||||
A resume request identifies the seat, harness-qualified native reference, prior binding, expected environment boundary and retained state. Validate those before launching the native harness. Reject mismatched reference types, missing/changed history, wrong immutable resources, unavailable required files and duplicate active executions. Never use “latest” as a replacement for an exact requested conversation or silently start fresh on failure.
|
||||
|
||||
Separate the outcomes: same native conversation resumed; new conversation rebuilt from artifacts; fresh launch; unsupported; refused; or unconfirmed. Environment continuity has its own outcome: same retained environment, recreated compute with copied state, different host, or unknown. A matching UUID alone does not show that native history was consumed.
|
||||
|
||||
If a provider stops exposing file transfer after canonical-process exit, capture artifacts/history and publish any retained resume descriptor **while transfer is still available**. Verify destination bytes before shutdown. After completion, use host/provider observations instead of assuming guest exec still works. For recreated compute, explicitly transfer the retained checkpoint and required artifact into the successor environment and validate them before publishing its resume descriptor. A new environment ID is expected and must be recorded as a successor, not disguised as the old environment.
|
||||
|
||||
The measured private recipe demonstrated this latter boundary: the original sandbox was deleted; copied native history and task bytes were restored into new containers on the same physical host; the native harness launched with `--resume` and the retained UUID. Its first new reply recalled a private conversation-only marker before tools, with no marker in the new input and no native-history tool read. It then verified artifact 1, used changed task context, produced artifact 2 and returned it for independent coordinator verification. This establishes one bounded native-return/work result. It does **not** establish new-host migration, RAM restoration, same-container stop/start, cgroup enforcement, general portability or shipped platform support.
|
||||
|
||||
## Export results before teardown
|
||||
|
||||
A guest path/hash note means bytes exist in a guest; it is not delivery to the consumer. Identify the actor holding the provider transfer handle. In the measured recipe that actor was the host wrapper, not the guest worker.
|
||||
|
||||
The worker publishes its report once and records the task/path/hash on its existing child. The wrapper validates the binding, task, allowed path, file type, size and expected hash; transfers to a fresh host staging path; checks the returned bytes/schema/context; and publishes without overwriting an existing result. It records an actual consumer-accessible destination, hash, byte count and actor through the authenticated work route. The worker closes its child after the matching export receipt; the coordinator independently reads/verifies those bytes before closing its parent. An ambiguous transfer is reconciled by existing bytes/hash, not a new report or duplicate task.
|
||||
|
||||
Complete exports before resource deletion. If output cannot be returned, preserve the exact resource/owner and failure state. Cleanup then reconciles the exact owned workload, supervisor, volumes and provider records to the intended stopped/absent state. Keep reused engines, caches, authenticated environments and attached resources. Revoke scoped credentials/grants through their owners and record remaining state. “Delete requested” and “absence verified” are different receipts.
|
||||
|
||||
## Proposed generic platform seam — not implemented
|
||||
|
||||
The existing composition need not acquire another YAML format to describe these responsibilities. If a maintained platform seam is selected, the following **design pseudocode** names the smallest useful operations; it is not a loader or API available in this release:
|
||||
|
||||
```text
|
||||
prepare(placement, ownership, resourceRequest, credentialRefs) -> bindingReceipt
|
||||
project(binding, explicitFiles, nativeCheckpoint?) -> deliveryReceipt
|
||||
launch(binding, harnessLaunch, exactResumeRequest?) -> executionReceipt
|
||||
observe(binding, execution) -> observedState | unsupported | unconfirmed
|
||||
export(binding, artifactRequest) -> publicationReceipt
|
||||
shutdown(binding, execution, selectedEffect) -> effectReceipt
|
||||
```
|
||||
|
||||
The provider implementation interprets its image/network/compute configuration. The runner interprets native protocol and control. Core would retain typed provenance and route the selected effects; it must not reinterpret arbitrary provider keys, leak secrets or collapse ownership into a stop boolean. Each mutating operation needs an attempt identity and reconciliation route; partial receipts remain useful after failure. Capability reporting must expose unsupported projection, cancellation, resume or cleanup rather than letting method names imply support.
|
||||
|
||||
The most useful author evidence follows the real work path: exact resolved placement and native launch; delivered current context; authenticated work ownership; returned artifact bytes; exact resume boundary and consumed retained state; and final resource disposition. A parser pass, launcher success, viewer reconnect or same display name leaves the corresponding effect unconfirmed. These distinctions let an author implement and diagnose an integration; they do not create a new release gate or require another runtime campaign.
|
||||
Loaded 100 of 4302 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user