@augurworks/augur 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/CANVAS.md +858 -0
  2. package/INSTALL.md +603 -0
  3. package/LICENSE +27 -0
  4. package/README.md +150 -0
  5. package/TRADEMARKS.md +72 -0
  6. package/agents/README.md +65 -0
  7. package/agents/canon-extract.md +117 -0
  8. package/agents/canon.md +151 -0
  9. package/agents/canvas.md +88 -0
  10. package/agents/currency.md +74 -0
  11. package/agents/drafts.md +92 -0
  12. package/agents/identity.md +148 -0
  13. package/agents/prototype-contract.md +69 -0
  14. package/agents/publishing.md +324 -0
  15. package/agents/review-feedback.md +76 -0
  16. package/agents/space-json.md +132 -0
  17. package/agents/ui-skill.md +101 -0
  18. package/agents/working-marks.md +86 -0
  19. package/brand/augur-eye.svg +3 -0
  20. package/brand/augur-icon.svg +41 -0
  21. package/brand/augur-mark.svg +5 -0
  22. package/brand/lentianova-bulky.otf +0 -0
  23. package/brand/lentianova-median.otf +0 -0
  24. package/brand/lentianova.woff2 +0 -0
  25. package/build.js +8409 -0
  26. package/changelog.md +133 -0
  27. package/fonts/inter-latin-wght-normal.woff2 +0 -0
  28. package/package.json +57 -0
  29. package/pitis/PETS.md +121 -0
  30. package/pitis/README.md +117 -0
  31. package/pitis/index.html +266 -0
  32. package/pitis/piti.build.js +81 -0
  33. package/pitis/piti.js +929 -0
  34. package/pitis/roast-agent.md +185 -0
  35. package/realtime/src/index.js +119 -0
  36. package/realtime/wrangler.example.toml +44 -0
  37. package/scripts/adopt.mjs +106 -0
  38. package/scripts/board-snapshot.mjs +197 -0
  39. package/scripts/build-seed-pack.mjs +33 -0
  40. package/scripts/bundle-rekey.mjs +112 -0
  41. package/scripts/bundle-tenancy-rehearsal.mjs +781 -0
  42. package/scripts/canon-extract.mjs +578 -0
  43. package/scripts/canon.mjs +599 -0
  44. package/scripts/canvas-screen.mjs +333 -0
  45. package/scripts/check-local.mjs +136 -0
  46. package/scripts/clawd-art.mjs +75 -0
  47. package/scripts/clawd-canvas.mjs +741 -0
  48. package/scripts/cli.mjs +100 -0
  49. package/scripts/clone.mjs +422 -0
  50. package/scripts/close.mjs +30 -0
  51. package/scripts/connect.mjs +103 -0
  52. package/scripts/deploy.mjs +157 -0
  53. package/scripts/dev.mjs +54 -0
  54. package/scripts/doc-lint.mjs +143 -0
  55. package/scripts/export.mjs +300 -0
  56. package/scripts/fork.mjs +64 -0
  57. package/scripts/freeze.mjs +74 -0
  58. package/scripts/frontdoor-parity.mjs +180 -0
  59. package/scripts/hook.mjs +67 -0
  60. package/scripts/identity-rekey.mjs +112 -0
  61. package/scripts/init.mjs +122 -0
  62. package/scripts/land.mjs +40 -0
  63. package/scripts/lib/adapters.mjs +156 -0
  64. package/scripts/lib/board-snapshot.mjs +511 -0
  65. package/scripts/lib/draft.mjs +385 -0
  66. package/scripts/lib/graduate.mjs +276 -0
  67. package/scripts/lib/instance.mjs +74 -0
  68. package/scripts/lib/marks.mjs +107 -0
  69. package/scripts/lib/materialize.mjs +138 -0
  70. package/scripts/lib/merge3.mjs +124 -0
  71. package/scripts/lib/offline-posture.mjs +120 -0
  72. package/scripts/lib/offline-respawn.mjs +18 -0
  73. package/scripts/lib/offline-wrangler.mjs +78 -0
  74. package/scripts/lib/publish-compose.mjs +12 -0
  75. package/scripts/lib/publish-conflict.mjs +78 -0
  76. package/scripts/lib/publish-evidence.mjs +144 -0
  77. package/scripts/lib/refine-compare.mjs +140 -0
  78. package/scripts/lib/refine-ledger.mjs +150 -0
  79. package/scripts/lib/refine-png.mjs +149 -0
  80. package/scripts/lib/seed-pack-build.mjs +141 -0
  81. package/scripts/lib/state-compare.mjs +68 -0
  82. package/scripts/lib/store.mjs +122 -0
  83. package/scripts/login.mjs +84 -0
  84. package/scripts/mark.mjs +112 -0
  85. package/scripts/migrate-board-keys.mjs +164 -0
  86. package/scripts/migrate.mjs +265 -0
  87. package/scripts/no-foreign-vocabulary.mjs +216 -0
  88. package/scripts/no-product-names.mjs +157 -0
  89. package/scripts/no-tenant-globals.mjs +1134 -0
  90. package/scripts/offline.mjs +247 -0
  91. package/scripts/og.mjs +170 -0
  92. package/scripts/one-tenant-resolver.mjs +146 -0
  93. package/scripts/open.mjs +63 -0
  94. package/scripts/publish.mjs +1041 -0
  95. package/scripts/read.mjs +31 -0
  96. package/scripts/refine.mjs +455 -0
  97. package/scripts/release-drift.mjs +138 -0
  98. package/scripts/restore.mjs +322 -0
  99. package/scripts/review.mjs +125 -0
  100. package/scripts/save.mjs +21 -0
  101. package/scripts/shell-lint.mjs +189 -0
  102. package/scripts/ship.mjs +460 -0
  103. package/scripts/shoot.mjs +264 -0
  104. package/scripts/smoke-invite.mjs +113 -0
  105. package/scripts/state-inventory.mjs +223 -0
  106. package/scripts/status.mjs +177 -0
  107. package/scripts/sync.mjs +27 -0
  108. package/scripts/tenant-do-rehearsal.mjs +1314 -0
  109. package/scripts/ui-copy-lint.mjs +202 -0
  110. package/scripts/watch.mjs +32 -0
  111. package/scripts/wrangler-preflight.mjs +246 -0
  112. package/seed/CANON.md +43 -0
  113. package/seed/README.md +119 -0
  114. package/seed/prototype-status.json +9 -0
  115. package/seed/registry.json +88 -0
  116. package/seed/skills/starter-ui/SKILL.md +52 -0
  117. package/seed/skills/starter-ui/skill.json +11 -0
  118. package/seed/skills/starter-ui/starter-tokens.css +73 -0
  119. package/seed/skills/starter-ui/starter-ui.css +318 -0
  120. package/seed/skills/starter-ui/starter-ui.js +50 -0
  121. package/seed/space.json +5 -0
  122. package/seed/start-here/prototypes/connect-your-terminal/index.html +428 -0
  123. package/seed/start-here/prototypes/connect-your-terminal/preview.webp +0 -0
  124. package/seed/start-here/prototypes/sample-with-comments/index.html +172 -0
  125. package/seed/start-here/prototypes/sample-with-comments/preview.webp +0 -0
  126. package/seed/start-here/prototypes/set-up-your-design-system/index.html +353 -0
  127. package/seed/start-here/prototypes/set-up-your-design-system/preview.webp +0 -0
  128. package/seed/threads.json +52 -0
  129. package/seed/worked-examples/prototypes/field-readings/index.html +273 -0
  130. package/seed/worked-examples/prototypes/field-readings/preview.webp +0 -0
  131. package/seed/worked-examples/prototypes/slide-deck/index.html +214 -0
  132. package/seed/worked-examples/prototypes/slide-deck/preview.webp +0 -0
  133. package/seed/worked-examples/prototypes/specimen-viewer/index.html +203 -0
  134. package/seed/worked-examples/prototypes/specimen-viewer/preview.webp +0 -0
  135. package/src/_worker.js +12668 -0
  136. package/src/board-key.mjs +57 -0
  137. package/src/board-room.mjs +881 -0
  138. package/src/bundle-keys.mjs +138 -0
  139. package/src/canon/collect-in-browser.js +215 -0
  140. package/src/canon/emit.mjs +232 -0
  141. package/src/canon/observe.mjs +260 -0
  142. package/src/canon/schema.mjs +451 -0
  143. package/src/canvas/DSEG-LICENSE.txt +95 -0
  144. package/src/canvas/DSEG7Classic-Bold.woff2 +0 -0
  145. package/src/canvas/canvas.css +885 -0
  146. package/src/canvas/canvas.js +5780 -0
  147. package/src/canvas/capture.js +485 -0
  148. package/src/chrome/appchrome.mjs +568 -0
  149. package/src/currency.mjs +203 -0
  150. package/src/delete-confirmation.mjs +254 -0
  151. package/src/drafts/drafts.js +326 -0
  152. package/src/entry.js +58 -0
  153. package/src/galleries.mjs +400 -0
  154. package/src/health-cron.mjs +173 -0
  155. package/src/identity.json +1 -0
  156. package/src/kv-codec.mjs +135 -0
  157. package/src/kv-identity.mjs +259 -0
  158. package/src/mail.mjs +599 -0
  159. package/src/provenance.mjs +74 -0
  160. package/src/publish-compose.mjs +363 -0
  161. package/src/publish-fork.mjs +221 -0
  162. package/src/publish-units.mjs +44 -0
  163. package/src/purge.mjs +81 -0
  164. package/src/review/COMMENTING-UX.md +306 -0
  165. package/src/review/cat.png +0 -0
  166. package/src/review/comment-cursor.svg +7 -0
  167. package/src/review/comments.js +1738 -0
  168. package/src/room-ticket.mjs +96 -0
  169. package/src/roster-chip.mjs +32 -0
  170. package/src/seed-pack.mjs +261 -0
  171. package/src/state-inventory.mjs +249 -0
  172. package/src/sw-logic.mjs +48 -0
  173. package/src/tenant-cache.mjs +141 -0
  174. package/src/tenant-context.mjs +386 -0
  175. package/src/tenant-do.js +2940 -0
  176. package/src/tenant-host.mjs +144 -0
  177. package/src/tenant-quotas.mjs +140 -0
  178. package/src/unit-core.mjs +103 -0
  179. package/src/unit-object.mjs +338 -0
  180. package/templates/README.md +67 -0
  181. package/templates/shell/deploy.yml +145 -0
  182. package/templates/shell/engine-bump.yml +142 -0
  183. package/templates/shell/health.yml +233 -0
  184. package/templates/shell/kv-backup.yml +201 -0
  185. package/templates/shell/roster-update.yml +110 -0
  186. package/templates/shell/space-preflight.yml +35 -0
  187. package/templates/shell/space-rebake.yml +79 -0
  188. package/templates/shell/store-backup.yml +152 -0
  189. package/templates/shell/wrangler.example.toml +266 -0
  190. package/templates/space/publish.yml +91 -0
package/INSTALL.md ADDED
@@ -0,0 +1,603 @@
1
+ # INSTALL — stand up an Augur instance
2
+
3
+ The complete recipe, start to finish. It is written to be **executed top to bottom by an
4
+ agent**: every step is a command with a check, and the handful of things only a person can
5
+ do are marked **HUMAN** — stop there, ask, continue.
6
+
7
+ Budget: about an hour, most of it waiting on DNS and CI.
8
+
9
+ If you only want to run the engine locally against some spaces, you do not need any of
10
+ this — see [README.md](./README.md).
11
+
12
+ ## What you are building
13
+
14
+ An instance is **three repos and one Cloudflare project**:
15
+
16
+ ```
17
+ <shell repo> private the deploy shell: engine pin, identity.json, deploy.config.json,
18
+ CI workflows, every secret. Ships the site's CODE.
19
+ <space repo> private the content: space.json + <project>/prototypes/**. One repo per
20
+ space; a design system is optional.
21
+ augur public this engine. Pinned as a submodule. Deploys nothing itself.
22
+
23
+ Cloudflare Pages project + KV namespace + R2 bucket (+ optional realtime worker)
24
+ ```
25
+
26
+ Two things move independently, and keeping them straight is the whole model:
27
+
28
+ | What | Ships by | Takes |
29
+ |---|---|---|
30
+ | Engine code + shared chrome | push to the shell (or an engine pin bump) | ~1 min, via CI |
31
+ | Space content | `augur publish` from a space clone | seconds, no CI |
32
+
33
+ **Rule zero: pin, don't fork.** Never patch the engine inside a shell or a space, never
34
+ run an instance off a private engine fork. A patched instance stops taking upstream
35
+ fixes. Engine gaps get fixed upstream (generic, no instance-specific words) and every
36
+ instance takes them by pin bump. See [CONTRIBUTING.md](./CONTRIBUTING.md).
37
+
38
+ **R2 is required.** CI builds the shell with `GV_ENGINE_ONLY=1` — no space is ever on
39
+ disk there — so the bundle store is the only path content has to the live site. An
40
+ instance without R2 serves chrome and nothing else.
41
+
42
+ ## Step 0 — fill this in
43
+
44
+ Decide these first and keep them at hand; every later step substitutes from this table.
45
+
46
+ | Value | Placeholder | Example |
47
+ |---|---|---|
48
+ | Instance slug | `<instance>` | `acme` |
49
+ | Shell repo | `<owner>/<shell>` | `acme-co/augur-deploy-acme` |
50
+ | Space repo | `<owner>/<space-repo>` | `acme-co/augur-space-acme` |
51
+ | Space id (from its `space.json`) | `<space-id>` | `acme` |
52
+ | Pages project | `<pages-project>` | `augur-acme` |
53
+ | KV namespace | `<kv-id>` | filled in at step 4 |
54
+ | R2 bucket | `<bucket>` | `augur-acme-bundles` |
55
+ | Site origin | `<site>` | `https://augur-acme.pages.dev` |
56
+ | Site host (origin without the scheme) | `<site-host>` | `augur-acme.pages.dev` |
57
+ | Admin email | `<admin-email>` | the first user, `role: "admin"` |
58
+
59
+ The space id is the mount name and the URL prefix; the repo name is a free label. The
60
+ **default** space owns the site root, every other space serves under `/<space-id>/`.
61
+
62
+ ## Step 1 — HUMAN: credentials
63
+
64
+ Three things an agent cannot obtain. Ask for them all at once.
65
+
66
+ 1. **Cloudflare API token** (dashboard → My Profile → API Tokens → Create). Account-scoped
67
+ permissions: `Cloudflare Pages: Edit`, `Workers KV Storage: Edit`,
68
+ `Workers R2 Storage: Edit`, and `Workers Scripts: Edit` if you want canvas multiplayer.
69
+ Add `Zone → DNS: Edit` on the zone if you want a custom domain automated.
70
+ 2. **Cloudflare account id** (dashboard sidebar, or the URL after `/accounts/`).
71
+ 3. **R2 enabled on the account** — a one-time dashboard click (R2 → Enable). The API
72
+ cannot do it and returns error code `10042` until it is done.
73
+ 4. **A GitHub PAT**, fine-grained, `Contents: Read and write` on the shell repo, and
74
+ `Contents: Read` on every space repo. One token covers every use below.
75
+
76
+ Export them for the rest of this run:
77
+
78
+ ```bash
79
+ export CF_TOKEN=… CF_ACCOUNT=… GH_TOKEN=…
80
+ export API="https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT"
81
+ ```
82
+
83
+ Check R2 before going further — this is the one gate that bites late:
84
+
85
+ ```bash
86
+ curl -s "$API/r2/buckets" -H "Authorization: Bearer $CF_TOKEN" | grep -q '"code":10042' \
87
+ && echo "R2 NOT ENABLED — ask the human to click Enable in the dashboard" || echo "R2 ok"
88
+ ```
89
+
90
+ ## Step 2 — the local workspace
91
+
92
+ Clone the three repos **side by side** in a parent folder that is not itself a repo. The
93
+ layout is load-bearing: local scripts find the shell by shape (a sibling holding an
94
+ `identity.json`), and offline mode picks up every sibling that has a `space.json`.
95
+
96
+ ```
97
+ <parent>/ # not a repo
98
+ ├── augur/ # this engine (public)
99
+ ├── <shell>/ # the deploy shell
100
+ └── <space-repo>/ # the space — where content is edited
101
+ ```
102
+
103
+ ```bash
104
+ mkdir -p <parent> && cd <parent>
105
+ git clone https://github.com/andratwiro/augur.git augur
106
+ ```
107
+
108
+ The engine has no runtime dependencies — `login`, `publish`, `build` and `status` run on
109
+ a bare clone with plain `node`. `npm install` is only needed for the Playwright-backed
110
+ screenshot scripts. The commands below call the scripts by path
111
+ (`node <parent>/augur/scripts/<name>.mjs`); `npm link` inside `augur/` gives you the
112
+ shorter `augur <cmd>` form if you want it.
113
+
114
+ Never nest one instance's parent inside another's — offline mode would serve both
115
+ instances' spaces at once.
116
+
117
+ ## Step 3 — the space repo
118
+
119
+ Content lives here, and this is the only repo a collaborator ever needs.
120
+
121
+ ```bash
122
+ mkdir -p <space-repo>/<project>/prototypes/hello
123
+ cd <space-repo>
124
+ cat > space.json <<'JSON'
125
+ {
126
+ "id": "<space-id>",
127
+ "name": "<Space name>",
128
+ "default": true,
129
+ "siteOrigin": "<site>"
130
+ }
131
+ JSON
132
+ echo '<!doctype html><meta charset=utf-8><title>Hello</title><h1>Hello</h1>' \
133
+ > <project>/prototypes/hello/index.html
134
+ git init -b main && git add . && git commit -m "space: initial"
135
+ gh repo create <owner>/<space-repo> --private --source=. --push
136
+ ```
137
+
138
+ - Only the contents of `prototypes/` folders are published — plus the three small
139
+ documents the build itself reads to compose the space (`registry.json`,
140
+ `prototype-status.json`, the skill's `skill.json`), so a workspace cloned back from
141
+ its publish builds again. Notes, research and anything else outside them stay private
142
+ by construction.
143
+ - A design system is optional. Plain self-contained HTML builds and ships fine. If you
144
+ do add one (`skills/<x>-ui/`), the space also needs a `registry.json` overlay catalog
145
+ at its root — the build refuses to ship an unlabeled overlay. See
146
+ [agents/ui-skill.md](./agents/ui-skill.md).
147
+ - `"default": true` puts the space at the site root. **An instance serves exactly one
148
+ space**, so exactly one space must have it — the old `/<id>/` mount for additional
149
+ spaces is retired and nothing routes to it. Run a second workspace as a second deploy.
150
+ - `"adminOnly"` is **inert** — it still parses, but it seals nothing. It only ever
151
+ applied to a non-default `/<id>/` mount, and those are gone. A workspace is private
152
+ because of who is a member of it, not because of a flag on its path.
153
+ - Every field, with semantics: [agents/space-json.md](./agents/space-json.md).
154
+
155
+ **No CI belongs in a space repo.** Content ships by publishing, not by pushing.
156
+
157
+ ## Step 4 — Cloudflare resources
158
+
159
+ ```bash
160
+ # KV — overlay state (comments, pins, statuses, sessions, publish tokens)
161
+ curl -s -X POST "$API/storage/kv/namespaces" -H "Authorization: Bearer $CF_TOKEN" \
162
+ -H "content-type: application/json" -d '{"title":"<instance>-kv"}'
163
+ # → note result.id as <kv-id>
164
+
165
+ # R2 — the bundle store (published content)
166
+ curl -s -X POST "$API/r2/buckets" -H "Authorization: Bearer $CF_TOKEN" \
167
+ -H "content-type: application/json" -d '{"name":"<bucket>"}'
168
+
169
+ # Pages project
170
+ curl -s -X POST "$API/pages/projects" -H "Authorization: Bearer $CF_TOKEN" \
171
+ -H "content-type: application/json" -d '{
172
+ "name":"<pages-project>","production_branch":"main",
173
+ "deployment_configs":{
174
+ "production":{"compatibility_date":"2026-06-14",
175
+ "kv_namespaces":{"COMMENTS":{"namespace_id":"<kv-id>"}}},
176
+ "preview": {"compatibility_date":"2026-06-14",
177
+ "kv_namespaces":{"COMMENTS":{"namespace_id":"<kv-id>"}}}}}'
178
+
179
+ # Bindings + runtime secrets on production. PATCH merges, so this keeps the KV binding.
180
+ curl -s -X PATCH "$API/pages/projects/<pages-project>" -H "Authorization: Bearer $CF_TOKEN" \
181
+ -H "content-type: application/json" -d "{\"deployment_configs\":{\"production\":{
182
+ \"r2_buckets\":{\"BUNDLES\":{\"name\":\"<bucket>\"}},
183
+ \"env_vars\":{\"SESSION_SECRET\":{\"type\":\"secret_text\",\"value\":\"$(openssl rand -hex 32)\"}}}}}"
184
+ ```
185
+
186
+ The binding **names** matter — the worker reads `env.COMMENTS`, `env.BUNDLES`. A missing
187
+ KV binding makes the overlay APIs answer `{"warning":"no-kv-binding"}` instead of
188
+ persisting; a missing R2 binding makes publishing impossible.
189
+
190
+ `SESSION_SECRET` is not optional in practice. Session cookies are HMACs keyed on it;
191
+ without it the worker falls back to an unkeyed digest and sessions are not
192
+ cryptographically bound to anything. Set it before the first login exists.
193
+
194
+ Do **not** set `GV_ASSET_SOURCE` yet. Serving flips to the store in step 7, after the
195
+ store has content — flipping an empty store serves a login page at every URL.
196
+
197
+ ## Step 5 — the shell repo
198
+
199
+ ```bash
200
+ cd <parent>/<shell> || { mkdir -p <parent>/<shell> && cd <parent>/<shell>; }
201
+ git init -b main
202
+ git submodule add https://github.com/andratwiro/augur.git engine
203
+ ```
204
+
205
+ `.gitmodules` URLs **must be HTTPS**. `actions/checkout` authenticates submodules by
206
+ rewriting HTTPS URLs with its token and cannot authenticate `git@github.com:` URLs — an
207
+ SSH URL here breaks every deploy at checkout.
208
+
209
+ `identity.json` — who exists. It is a roster, not a credential store: passwords live in
210
+ KV as PBKDF2 hashes, set by the person redeeming an invite. `[]` leaves the gate open to
211
+ anyone.
212
+
213
+ The one exception is the **first admin**, who has nobody to invite them. Seed that
214
+ account with a `passHash` — generate it with the engine's own hasher:
215
+
216
+ ```bash
217
+ cd <parent>/augur && node --input-type=module -e "
218
+ const W = (await import('./src/_worker.js')).__testables;
219
+ console.log(await W.hashPassword(process.argv[1]));" '<your-password>'
220
+ ```
221
+
222
+ ```json
223
+ [
224
+ { "email": "<admin-email>", "name": "<Name>", "initials": "XX",
225
+ "color": "#4a6cf7", "role": "admin", "passHash": "pbkdf2$100000$…$…" }
226
+ ]
227
+ ```
228
+
229
+ Use `passHash`, never a plaintext `pass`. A plaintext value still *resolves* as that
230
+ user's secret — so the account reads as active rather than pending — but nothing verifies
231
+ it, because password checking accepts only `pbkdf2$…` strings. The account would look
232
+ fine and be permanently unloginnable. Everyone after the first admin gets no password
233
+ field at all: invite them from the Admin panel and they choose their own.
234
+
235
+ `deploy.config.json` — instance knobs. Minimum viable:
236
+
237
+ ```json
238
+ {
239
+ "siteOrigin": "<site>",
240
+ "spaces": [{ "id": "<space-id>", "repo": "<owner>/<space-repo>" }],
241
+ "shellContract": 1
242
+ }
243
+ ```
244
+
245
+ `spaces` is a roster for repo-side automation (the health canary reads it), **not** a
246
+ build input — the shell mounts no space submodules. Optional keys: `realtimeOrigin`
247
+ (step 9), `sentinels`, `mcpHostSuffixes`, `mcpHostAllowlistUrl`, `vanityRedirects`,
248
+ `updateFeed`, `loginHint` (one line of plain text rendered under the
249
+ login form — how a demo instance surfaces its test credentials), `loginPrefill`
250
+ (`{email, password}` baked into the login form's `value=` attributes — how a demo
251
+ instance turns its throwaway account into a one-click login instead of a
252
+ copy-paste; leave unset and the form renders empty, as before), `devicePairing`
253
+ (boolean, default **`false`** — set it to `true` and this instance grows
254
+ `augur connect`: a terminal asks for a code, somebody types that code into a browser
255
+ already signed in here, and the token approval mints comes back to the terminal. No
256
+ password is typed into a shell, a history file or an agent transcript. It is off by
257
+ default because it adds an endpoint reachable WITHOUT credentials that ends in a
258
+ publish token, and only an explicit `true` enables it. The residual risk is
259
+ phishing — somebody talked into approving a code that is not theirs — so the code
260
+ lives five minutes, is typed rather than carried in a link, and the approval page
261
+ names who the terminal would publish as), `userImages`
262
+ (boolean, default `true` — set it to `false` and this instance stops accepting
263
+ user-supplied image BYTES at all: profile photos and canvas images are both refused
264
+ with a visible reason, while images already stored still render and a photo already
265
+ set can still be removed. Set it on any instance whose password is published or
266
+ shared with people who have agreed to nothing: the exposure is not abuse of your
267
+ data, it is your domain hosting somebody else's illegal image at a stable URL under
268
+ your name. Only an explicit `false` turns it off, so a typo cannot disable it),
269
+ `publishTokenTtlDays`
270
+ (number, default **30** — how long a publish token minted for a PERSON lives.
271
+ Both human doors read this one number: `augur login` and `augur connect`. When it
272
+ runs out the holder runs the same command again; nobody else has to do anything.
273
+ Tokens an admin mints by hand at `/__admin/tokens` for a machine — CI, a nightly
274
+ backup, an uptime probe — are NOT covered, because nothing re-runs a login for a
275
+ cron job and an expiry there is an outage at 4am with nobody to fix it; those end
276
+ on the revoke list. `0` turns expiry off. A value that is not a number falls back
277
+ to 30 rather than to "forever", so a typo cannot quietly restore immortal tokens.
278
+ Tokens minted before this existed carry no expiry and keep working — the check is
279
+ strictly additive, so taking this engine retires nothing anyone is using),
280
+ `runtimeChrome`
281
+ (boolean — compose current page chrome at serve time instead of trusting what
282
+ publishes baked in, so an engine deploy updates every page instantly; pages baked
283
+ by pre-marker engines still converge via the shell's space-rebake),
284
+ `tenantId` (the name this deployment answers to internally — leave it unset and
285
+ the build uses the id of the space it serves, which is what you want unless you
286
+ plan to rename that space and keep the identity), and `minClientProtocol`.
287
+
288
+ `minClientProtocol` is the oldest publish protocol this instance accepts a commit
289
+ from; a client below it is refused `426 cli-outdated` before anything is written.
290
+ Leave it unset on a single-operator instance — a floor nobody chose should never be
291
+ why a publish fails. Set it (to `3`) once several people or agents publish here from
292
+ clones you do not control, because an old client does not merely miss features: a
293
+ pre-3 client sends no `baseVersion`, so the store cannot tell whether its tree is
294
+ built on what is live, and a stale checkout can revert whoever published last with
295
+ neither of them seeing it.
296
+
297
+ `package.json` — so a local build reproduces CI's:
298
+
299
+ ```json
300
+ { "name": "<shell>", "private": true, "type": "module",
301
+ "scripts": { "build": "GV_ENGINE_ONLY=1 GV_IDENTITY_PATH=\"$PWD/identity.json\" GV_DEPLOY_CONFIG_PATH=\"$PWD/deploy.config.json\" node engine/build.js" } }
302
+ ```
303
+
304
+ Workflows — copy from [templates/shell/](./templates/shell/) into
305
+ `.github/workflows/`, then set the two instance values in `deploy.yml`
306
+ (`--project-name=<pages-project>` and `AUGUR_ORIGIN: <site>`) and the origin in
307
+ `health.yml`:
308
+
309
+ | File | Does |
310
+ |---|---|
311
+ | `deploy.yml` | build engine chrome → Pages → publish chrome to the store. Required. |
312
+ | `engine-bump.yml` | take engine updates on your schedule. Required in practice. |
313
+ | `health.yml` | canary: pushed-but-never-published drift, stale dirty publishes. |
314
+ | `store-backup.yml` | off-Cloudflare copies of the store, weekly + monthly. |
315
+ | `kv-backup.yml` | nightly copy of KV — comments, statuses, pins, canvases, identity. |
316
+ | `space-preflight.yml` | probe that CI's PAT can read a space repo before you add it. |
317
+ | `roster-update.yml` | commit Admin-panel invites/removals back to `identity.json`. |
318
+
319
+ Take **both** backups. `store-backup.yml` covers published content in R2;
320
+ `kv-backup.yml` covers the state the worker keeps beside it. Neither store has
321
+ point-in-time restore and neither backup covers the other. Because the KV export
322
+ includes password hashes and live publish tokens, keep the shell repo private —
323
+ that is the one workflow here that is unsafe on a public one.
324
+
325
+ Commit and create the repo:
326
+
327
+ ```bash
328
+ git add . && git commit -m "shell: initial" \
329
+ && gh repo create <owner>/<shell> --private --source=. --push
330
+ ```
331
+
332
+ ## Step 6 — secrets, then the first deploy
333
+
334
+ ```bash
335
+ gh secret set CLOUDFLARE_API_TOKEN -R <owner>/<shell> --body "$CF_TOKEN"
336
+ gh secret set CLOUDFLARE_ACCOUNT_ID -R <owner>/<shell> --body "$CF_ACCOUNT"
337
+ gh secret set SUBMODULE_PAT -R <owner>/<shell> --body "$GH_TOKEN"
338
+ gh secret set AUGUR_KV_NS -R <owner>/<shell> --body "<kv-id>" # kv-backup.yml
339
+ gh workflow run deploy.yml -R <owner>/<shell> && sleep 90 && curl -s <site>/_build.json
340
+ ```
341
+
342
+ `SUBMODULE_PAT` needs read on the **shell itself**, not just the spaces — checkout uses
343
+ one token for the host repo too. If the checkout step dies with
344
+ `Input required and not supplied: token`, the secret is missing; if it dies fetching a
345
+ space repo, the PAT's repository access does not cover it (run `space-preflight.yml` to
346
+ probe).
347
+
348
+ Verify: `<site>` serves the login page, and signing in as `<admin-email>` with the
349
+ password you hashed into `passHash` lands you inside. There is no content yet — that is correct. The workflow's
350
+ store-publish step reports `AUGUR_TOKEN not configured — skipping` and passes; that
351
+ secret arrives in the next step.
352
+
353
+ ## Step 7 — seed the store, then flip serving
354
+
355
+ The order matters. A flip before a seed serves a login page at every URL, because an
356
+ empty store means every path is unknown, and the gate answers unknown paths with the
357
+ login form.
358
+
359
+ ```bash
360
+ cd <parent>/augur
361
+
362
+ # 1. Mint a publish token from your own credentials. Admins get "*" — every space.
363
+ AUGUR_EMAIL=<admin-email> AUGUR_PASSWORD='<your-password>' node scripts/login.mjs --origin <site>
364
+
365
+ # 2. Seed: every sibling space + the engine chrome + the instance config.
366
+ node scripts/publish.mjs --all
367
+
368
+ # 3. Give CI the same token, so future engine deploys can publish the chrome.
369
+ gh secret set AUGUR_TOKEN -R <owner>/<shell> --body "$(python3 -c \
370
+ "import json,os;print(json.load(open(os.path.expanduser('~/.config/augur/tokens.json')))['<site-host>']['token'])")"
371
+
372
+ # 4. Confirm the store really has manifests BEFORE flipping.
373
+ curl -s "$API/r2/buckets/<bucket>/objects?prefix=spaces/&per_page=50" \
374
+ -H "Authorization: Bearer $CF_TOKEN" | grep -c 'manifest.json'
375
+
376
+ # 5. Flip serving to the store, then redeploy so the change takes effect.
377
+ curl -s -X PATCH "$API/pages/projects/<pages-project>" -H "Authorization: Bearer $CF_TOKEN" \
378
+ -H "content-type: application/json" \
379
+ -d '{"deployment_configs":{"production":{"env_vars":{"GV_ASSET_SOURCE":{"type":"plain_text","value":"r2"}}}}}'
380
+ gh workflow run deploy.yml -R <owner>/<shell>
381
+ ```
382
+
383
+ Notes on each:
384
+
385
+ - **Log in as the admin, not as anyone else.** An admin gets a `*`-scoped token; everyone
386
+ else gets a token scoped to the default space, and `/__publish/_instance/config`
387
+ rejects that with a 403. On a brand-new instance a non-admin login cannot even be
388
+ scoped yet, since no space has published.
389
+ - The token is saved to `~/.config/augur/tokens.json`, keyed by origin host, mode 0600;
390
+ publishing picks it up from there afterwards. Collaborators self-serve the same way
391
+ with `augur login` — never paste a password into a chat or a workflow file.
392
+ - `--all` discovers every sibling space next to the engine clone. From inside a single
393
+ space clone, plain `node <parent>/augur/scripts/publish.mjs` infers that one space.
394
+ If one parent folder holds spaces belonging to SEVERAL instances, never use `--all` —
395
+ it would publish every instance's spaces to this one origin. Publish each space from
396
+ its own clone instead.
397
+ - A Pages env var only applies to **new** deployments, which is why step 5 redeploys.
398
+
399
+ **The flip is one-way.** Unsetting `GV_ASSET_SOURCE` again does not roll anything back: CI
400
+ only ever uploads engine chrome to Pages, so assets mode on an instance built this way
401
+ serves a site with no content in it. To undo a bad **publish**, use the store's own
402
+ history — every version is kept and blobs are never collected, so any past publish is one
403
+ call away:
404
+
405
+ ```bash
406
+ TOKEN=$(python3 -c "import json,os;print(json.load(open(os.path.expanduser('~/.config/augur/tokens.json')))['<site-host>']['token'])")
407
+ curl -s <site>/__publish/<space-id>/versions -H "Authorization: Bearer $TOKEN"
408
+ curl -s -X POST <site>/__publish/<space-id>/rollback -H "Authorization: Bearer $TOKEN" \
409
+ -H "content-type: application/json" -d '{"version":<n>}'
410
+ ```
411
+
412
+ A rollback republishes the old content under a **new** version number rather than
413
+ repointing at the old one, so it is itself visible in the history and undone by another
414
+ rollback.
415
+
416
+ ## Step 8 — verify
417
+
418
+ ```bash
419
+ curl -s <site>/_build.json # builtAt fresh; spaces.<space-id>.sha == repo HEAD
420
+ curl -s -o /dev/null -w '%{http_code}\n' <site>/<project>/hello/ # 200 — public prototype
421
+ curl -s -o /dev/null -w '%{http_code}\n' <site>/__config/instance.json # 404 — config is sealed
422
+ curl -s <site>/__canvas/catalog.json | head -c 200 # synthesized, not stored
423
+ ```
424
+
425
+ Then, in a browser: the login page appears logged out; signing in works; a comment posted
426
+ on a page survives a reload (that is KV); the Admin panel lists your user.
427
+
428
+ `/_build.json` is the public deploy proof — a collaborator who cannot see your CI compares
429
+ its `spaces.<id>.sha` to their own `git rev-parse HEAD`. `augur status` does the same
430
+ comparison locally across every clone.
431
+
432
+ ## Step 9 — optional: custom domain
433
+
434
+ ```bash
435
+ curl -s -X POST "$API/pages/projects/<pages-project>/domains" -H "Authorization: Bearer $CF_TOKEN" \
436
+ -H "content-type: application/json" -d '{"name":"<host>"}'
437
+ ```
438
+
439
+ Then a **proxied CNAME `<host>` → `<pages-project>.pages.dev`** in the zone that owns the
440
+ domain. If the Pages token cannot see that zone — common — this is one dashboard click:
441
+ **HUMAN**. The domain flips to `active` on its own once the record resolves. Afterwards
442
+ update `siteOrigin` in `deploy.config.json`, `AUGUR_ORIGIN` in `deploy.yml` and
443
+ `health.yml`, and `siteOrigin` in each `space.json`.
444
+
445
+ ## Step 10 — optional: canvas multiplayer
446
+
447
+ Live boards need a second worker, **one per instance** — rooms are keyed by board path, so
448
+ two instances sharing a worker would share rooms and board storage.
449
+
450
+ 1. Copy `realtime/wrangler.example.toml` from the engine into the shell as
451
+ `realtime.wrangler.toml`; set `name` and point `BOARD_KV` at `<kv-id>`.
452
+ 2. `npx wrangler deploy -c realtime.wrangler.toml` from the shell root, with the engine
453
+ submodule checked out and the Cloudflare credentials in the environment.
454
+ 3. Add `"realtimeOrigin": "https://<worker>.<subdomain>.workers.dev"` to
455
+ `deploy.config.json`, push, redeploy.
456
+ 4. **Required:** set the same `RT_SHARED_SECRET` on BOTH workers — a secret on the
457
+ realtime worker (`wrangler secret put RT_SHARED_SECRET -c realtime.wrangler.toml`)
458
+ and a matching environment variable on the site project — then redeploy both. The
459
+ realtime worker sits on a public URL while the admin-only-space seal is enforced in
460
+ the site worker, so this is what stops anyone who learns the hostname from joining
461
+ any room directly. It is not optional and it fails closed: a realtime worker with no
462
+ secret answers **501** to every room request rather than serving them openly.
463
+
464
+ Verify with a websocket upgrade to `/__rt?path=/__test/x` — **101**. Paths under
465
+ `/__test/` never persist. Without any of this, `/__rt` answers 501 and boards run
466
+ single-user, persisting to KV through the Pages worker. A 501 from the realtime worker
467
+ after wiring `realtimeOrigin` means step 4 is missing or the two values disagree. A brand-new workers.dev worker
468
+ can 500 (`error code: 1104`) for its first minute — retry before debugging.
469
+
470
+ ## Step 11 — optional: email
471
+
472
+ Without this, an invite is a single-use link the admin copies out of the Admin panel and
473
+ sends however they like. That never stops working. Configuring a provider only means the
474
+ same link is *also* emailed to the person, and the panel tells you whether that worked.
475
+
476
+ Five runtime environment variables on the Pages project — `MAIL_API_KEY` as a secret, the
477
+ rest as plain values:
478
+
479
+ | variable | what it is |
480
+ | --- | --- |
481
+ | `MAIL_PROVIDER` | which driver: `scaleway` or `http`. Unset ⇒ email is off. |
482
+ | `MAIL_FROM` | the sending identity — `Name <noreply@example.org>`, or a bare address. |
483
+ | `MAIL_API_KEY` | the provider credential. **Secret**, never a plain value. |
484
+ | `MAIL_REGION` | `scaleway`: which region's endpoint to call. |
485
+ | `MAIL_PROJECT_ID` | `scaleway`: the project the sends are billed to. |
486
+ | `MAIL_API_URL` | `http`: where to POST. On any driver, overrides the derived endpoint. |
487
+
488
+ `http` is the escape hatch: a bearer-authenticated JSON POST of
489
+ `{from, fromName, to, subject, text, html, template}` to a URL you name. Put a dozen-line
490
+ relay in front of your own provider rather than forking the engine to add a driver.
491
+
492
+ **Send from a domain you control the DNS for, and not the one your prototypes are
493
+ published on.** Published content is arbitrary HTML and JavaScript, and the first
494
+ phishing page somebody publishes damages the reputation of the domain it was served
495
+ from — which must not be the reputation your invite mail depends on. Set SPF, DKIM and
496
+ DMARC on the sending domain, and check them with an external verifier before you trust
497
+ them: mail from an unauthenticated domain lands in spam, which looks exactly like an
498
+ invite that never arrived.
499
+
500
+ Verify by inviting yourself at a real address. Whatever happens, the panel shows the
501
+ link; underneath it says `Emailed to …`, or nothing at all if no provider is configured,
502
+ or what went wrong and that you should send the link yourself. A provider outage
503
+ degrades — it never blocks an invite.
504
+
505
+ ## Step 12 — optional: the platform MCP proxy
506
+
507
+ A prototype talking to an upstream API usually cannot call it from the browser, so
508
+ `/__mcp/<host>/<path>` forwards from the site's own origin. Which hosts it forwards comes
509
+ from three explicit sources, and nothing else:
510
+
511
+ - `"mcpHostSuffixes": ["example.com"]` in `deploy.config.json` — any subdomain of each
512
+ entry.
513
+ - `"mcpAllowlists": ["path/in/space/allowlist.json"]` in a `space.json` — each path names
514
+ a JSON document the space ships, shaped `{"hosts": ["a.example"]}`, matched as **exact
515
+ hosts**. For platforms on a vanity domain, where no suffix rule is safe. Mounting the
516
+ space is the trust act. A declared list that is missing or malformed fails the build.
517
+ - `"mcpHostAllowlistUrl"` in `deploy.config.json` — the same document shape fetched at
518
+ runtime, cached an hour, for a list that lives outside any space. Unreachable → the
519
+ other two still apply, so a broken URL never revokes working access.
520
+
521
+ Forwarding "anything that looks like the right kind of API" would make this an open proxy
522
+ for whatever is reachable from the edge. Hence three explicit lists.
523
+
524
+ ## Operating it
525
+
526
+ **Engine updates.** `engine-bump.yml` runs every 6 hours and on demand
527
+ (`gh workflow run engine-bump.yml -R <owner>/<shell>`), so an instance picks up fixes
528
+ without anyone watching it. It also updates the moment the engine moves if the engine
529
+ maintainer has your shell on their dispatch list; the schedule is what covers everyone
530
+ else. Detaching is deliberate — disable the workflow, or pin `TRACK` to a tag. On the default `release` track it
531
+ opens a PR moving the pin to the latest engine release with the notes in the body —
532
+ nothing lands unread. `TRACK: main` follows the bleeding edge instead. The pin is your
533
+ release valve: skip an update by not merging, roll back by reverting the pin commit.
534
+ Release mode needs one repo setting once:
535
+
536
+ ```bash
537
+ gh api -X PUT repos/<owner>/<shell>/actions/permissions/workflow \
538
+ -f default_workflow_permissions=write -F can_approve_pull_request_reviews=true
539
+ ```
540
+
541
+ **Backups.** The store is the only copy of published content and R2 has no
542
+ point-in-time restore. Most of it is reproducible from git at the sha `/_build.json`
543
+ reports — but a publish from an uncommitted working tree (`"dirty": true`) serves bytes
544
+ held in no repository. `store-backup.yml` takes weekly and monthly copies; on demand it is
545
+ `augur export --out <dir>`, and `augur restore <dir>` puts one back. Walkthrough:
546
+ `docs/store-recovery.md`.
547
+
548
+ ⚠️ **That is published content and nothing else.** Not the roster, the invites, the
549
+ publish tokens, the prototype statuses, the card names, the comment threads, the boards,
550
+ the pins or the images pasted onto a canvas. `augur export --out <dir> --full` adds all of
551
+ it and `augur restore <dir> --state` replays it; `--full` needs a star-scope token because
552
+ the answer carries the roster and the publish-token hashes. Password hashes are never in
553
+ it — a credential is account-level and the export route cannot reach one.
554
+
555
+ **Drift.** `health.yml` compares each space repo's `main` HEAD against the live stamp
556
+ every six hours and opens a single issue when work is pushed but never published, or when
557
+ a dirty publish outlives its grace window. It closes the issue itself when things go
558
+ green.
559
+
560
+ **Adding another space later.** Create the repo (step 3, without `"default": true`), grant
561
+ the PAT read access, verify the grant with
562
+ `gh workflow run space-preflight.yml -R <owner>/<shell> -f repo=<owner>/<repo>`, add it to
563
+ the `spaces` roster in `deploy.config.json`, and publish from its clone. It serves under
564
+ `/<space-id>/`. No submodule, no CI, no pin.
565
+
566
+ **Local preview.** `npm run offline` from the engine builds every sibling space, runs the
567
+ real worker locally with the shell's identity, and hot-reloads in about a second — see
568
+ [CLAUDE.md](./CLAUDE.md). A second instance on the same machine needs `OFFLINE_PORT`. Note
569
+ that if `augur/.env.deploy` holds real credentials, the offline worker reads and writes the
570
+ **production** KV; rename it for a local-only sandbox (the startup log says which mode it
571
+ is in).
572
+
573
+ ## Gotchas
574
+
575
+ - **Flipping `GV_ASSET_SOURCE` before the store is seeded** takes the whole site down to a
576
+ login page. Verify manifests exist first; roll back with the same PATCH set to `null`.
577
+ - **HTTPS in `.gitmodules`, always.** Checkout cannot authenticate SSH URLs.
578
+ - **`SUBMODULE_PAT` must cover the shell repo too**, not only the spaces.
579
+ - **`GITHUB_TOKEN` pushes never retrigger workflows**, by design. The bump workflows
580
+ therefore start `deploy.yml` explicitly, which needs `actions: write`. Anything that
581
+ must retrigger CI by pushing (`roster-update.yml`) needs a real PAT, `AUGUR_PIN_TOKEN`.
582
+ - **`/__publish/_instance/config` requires a `*`-scoped token.** A token scoped to one
583
+ space, or to `_engine`, gets a 403 there — this is the usual cause of a first CI publish
584
+ failing.
585
+ - **Publishing ships the working tree.** A publish from an uncommitted tree is flagged
586
+ `"dirty": true` in `/_build.json`; those exact bytes exist in no repository. `augur ship`
587
+ commits first for that reason.
588
+ - **A publish defines the whole space.** A clone missing a folder would remove its public
589
+ URLs for everyone, so `commit` refuses to drop public prefixes unless you pass
590
+ `--allow-unpublish`. Never work around it by passing the flag reflexively.
591
+ - **Never publish from a shallow clone.** Edit dates and contributor chips come from git
592
+ history; a `--depth 1` clone flattens both. Publish refuses, and tells you the unshallow
593
+ command.
594
+ - **A fresh Pages deploy serves mixed old and new assets for a minute or two** at some
595
+ edges. Poll until two consecutive fetches agree before diagnosing a "bug" reported
596
+ seconds after a ship.
597
+ - **Scheduled workflows quietly stop after ~60 idle days.** The manual dispatch always
598
+ works.
599
+ - **Do not unbind KV to escape a KV outage.** Authentication fails closed on a KV error on
600
+ purpose, but "no binding at all" is the offline-build case and falls through to whatever
601
+ `identity.json` seeds. Wait the outage out or fix the binding.
602
+ - **A plaintext `pass` in `identity.json` creates an unloginnable account** that reports
603
+ itself as active. Seed `passHash`, or nothing at all.
package/LICENSE ADDED
@@ -0,0 +1,27 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 the Augur contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ The MIT licence above covers the CODE. It does not grant rights in the "Augur" name, in
26
+ the marks under brand/, or in the third-party typefaces bundled there — those are not this
27
+ project's to sublicense. See TRADEMARKS.md.