@bongos/core 1.20.5 → 1.20.6

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 (55) hide show
  1. package/.bongos-core.json +102 -52
  2. package/.claude/skills/goal-review/SKILL.md +16 -24
  3. package/.claude/skills/goal-uat/SKILL.md +84 -0
  4. package/clients/bongos-client/README.md +1 -1
  5. package/clients/bongos-client/bongos-client.global.js +12 -0
  6. package/clients/bongos-client/index.cjs +12 -0
  7. package/clients/bongos-client/index.d.ts +18 -1
  8. package/clients/bongos-client/index.mjs +12 -0
  9. package/docs/adr/0183-criteria-close-themselves.md +1 -1
  10. package/docs/adr/0310-a-speciality-offers-skills-and-the-adopter-chooses-them.md +1 -1
  11. package/docs/adr/0351-a-criterion-closes-on-a-uat.md +73 -0
  12. package/docs/adr/README.md +28 -0
  13. package/docs/api/openapi.json +385 -5
  14. package/docs/api-reference.md +14 -4
  15. package/docs/architecture.md +7 -0
  16. package/docs/copy-inventory.md +22 -22
  17. package/docs/copy-registry.json +23 -23
  18. package/docs/file-map.md +2 -1
  19. package/docs/module-api-changelog.md +3 -1
  20. package/docs/page-readings.json +3 -3
  21. package/modules/hall-ui/public/goals-page.js +8 -3
  22. package/modules/hall-ui/public/tweak-editor.css +4 -7
  23. package/modules/hall-ui/public/tweak-editor.html +3 -3
  24. package/modules/hall-ui/public/tweak-editor.js +3 -0
  25. package/modules/lifecycle/criterion-uat-db.js +267 -0
  26. package/modules/lifecycle/criterion-uat.js +303 -0
  27. package/modules/lifecycle/db-goals.js +25 -12
  28. package/modules/lifecycle/done-when.js +84 -17
  29. package/modules/lifecycle/migrations/lifecycle_015_criterion_uat.sql +73 -0
  30. package/modules/lifecycle/module.json +1 -0
  31. package/modules/lifecycle/routes/criterion-uat.js +169 -0
  32. package/modules/lifecycle/routes/done-when.js +25 -1
  33. package/modules/npm-release/module.json +3 -1
  34. package/modules/npm-release/routes/task-where.js +18 -1
  35. package/modules/npm-release/work.js +38 -7
  36. package/modules/specialities/routes/specialities.js +8 -1
  37. package/modules/specialities/specialities.js +26 -1
  38. package/package-lock.json +2 -2
  39. package/package.json +1 -1
  40. package/release-notes.json +18 -0
  41. package/scripts/gds/cli-lib.js +4 -1
  42. package/scripts/gds/fitness-checks-write-validation.js +4 -0
  43. package/scripts/gds/status.js +14 -3
  44. package/scripts/gds/uat.js +120 -0
  45. package/src/bongos/route-rank-check.js +9 -0
  46. package/src/module-api.js +1 -1
  47. package/tests/auto_satisfy_criteria.mjs +4 -2
  48. package/tests/criterion_uat.mjs +467 -0
  49. package/tests/criterion_uat_routes.mjs +232 -0
  50. package/tests/fitness.mjs +3 -1
  51. package/tests/goal_achievement.mjs +4 -2
  52. package/tests/goal_routes.mjs +4 -2
  53. package/tests/ideator_full_idea_shapes_space_proof.mjs +3 -2
  54. package/tests/npm_release_where.mjs +18 -2
  55. package/tests/speciality_session_skills.mjs +150 -0
@@ -5,7 +5,7 @@ A **generated**, zero-dependency typed client for the Bongos API — produced fr
5
5
  by hand; it regenerates when the spec changes, so it can never drift from the routes.
6
6
 
7
7
  - API version: **v1** (served at `/api/bongos/v1`)
8
- - 455 operations across 69 resource groups
8
+ - 460 operations across 70 resource groups
9
9
 
10
10
  ## Use it from your project
11
11
 
@@ -332,6 +332,10 @@ function createClient(opts = {}) {
332
332
  // POST /cost — rank: any-builder — POST /cost
333
333
  postCost: (args) => request("POST", "/cost", { hasBody: true }, args),
334
334
  },
335
+ "criterionUats": {
336
+ // GET /criterion-uats/recordings/{name} — rank: any-builder — GET /criterion-uats/recordings/:name
337
+ getCriterionUatsRecordingsName: (args) => request("GET", "/criterion-uats/recordings/{name}", { hasBody: false }, args),
338
+ },
335
339
  "dependencies": {
336
340
  // DELETE /dependencies — rank: archon — DELETE /dependencies
337
341
  deleteDependencies: (args) => request("DELETE", "/dependencies", { hasBody: false }, args),
@@ -349,8 +353,16 @@ function createClient(opts = {}) {
349
353
  "doneWhen": {
350
354
  // PATCH /done-when/{criterionId} — rank: metic+archon — PATCH /done-when/:criterionId
351
355
  patchDoneWhenCriterionId: (args) => request("PATCH", "/done-when/{criterionId}", { hasBody: true }, args),
356
+ // PUT /done-when/{criterionId}/backend-only — rank: metic+archon — PUT /done-when/:criterionId/backend-only
357
+ putDoneWhenCriterionIdBackendOnly: (args) => request("PUT", "/done-when/{criterionId}/backend-only", { hasBody: true }, args),
352
358
  // POST /done-when/{criterionId}/satisfy — rank: metic+archon — POST /done-when/:criterionId/satisfy
353
359
  postDoneWhenCriterionIdSatisfy: (args) => request("POST", "/done-when/{criterionId}/satisfy", { hasBody: true }, args),
360
+ // GET /done-when/{criterionId}/uat — rank: any-builder — GET /done-when/:criterionId/uat
361
+ getDoneWhenCriterionIdUat: (args) => request("GET", "/done-when/{criterionId}/uat", { hasBody: false }, args),
362
+ // POST /done-when/{criterionId}/uat — rank: metic+archon — POST /done-when/:criterionId/uat
363
+ postDoneWhenCriterionIdUat: (args) => request("POST", "/done-when/{criterionId}/uat", { hasBody: true }, args),
364
+ // POST /done-when/{criterionId}/uat/recording — rank: metic+archon — POST /done-when/:criterionId/uat/recording
365
+ postDoneWhenCriterionIdUatRecording: (args) => request("POST", "/done-when/{criterionId}/uat/recording", { hasBody: true }, args),
354
366
  // POST /done-when/{criterionId}/unsatisfy — rank: metic+archon — POST /done-when/:criterionId/unsatisfy
355
367
  postDoneWhenCriterionIdUnsatisfy: (args) => request("POST", "/done-when/{criterionId}/unsatisfy", { hasBody: true }, args),
356
368
  // GET /done-when/pending-review — rank: metic+archon — GET /done-when/pending-review
@@ -331,6 +331,10 @@ function createClient(opts = {}) {
331
331
  // POST /cost — rank: any-builder — POST /cost
332
332
  postCost: (args) => request("POST", "/cost", { hasBody: true }, args),
333
333
  },
334
+ "criterionUats": {
335
+ // GET /criterion-uats/recordings/{name} — rank: any-builder — GET /criterion-uats/recordings/:name
336
+ getCriterionUatsRecordingsName: (args) => request("GET", "/criterion-uats/recordings/{name}", { hasBody: false }, args),
337
+ },
334
338
  "dependencies": {
335
339
  // DELETE /dependencies — rank: archon — DELETE /dependencies
336
340
  deleteDependencies: (args) => request("DELETE", "/dependencies", { hasBody: false }, args),
@@ -348,8 +352,16 @@ function createClient(opts = {}) {
348
352
  "doneWhen": {
349
353
  // PATCH /done-when/{criterionId} — rank: metic+archon — PATCH /done-when/:criterionId
350
354
  patchDoneWhenCriterionId: (args) => request("PATCH", "/done-when/{criterionId}", { hasBody: true }, args),
355
+ // PUT /done-when/{criterionId}/backend-only — rank: metic+archon — PUT /done-when/:criterionId/backend-only
356
+ putDoneWhenCriterionIdBackendOnly: (args) => request("PUT", "/done-when/{criterionId}/backend-only", { hasBody: true }, args),
351
357
  // POST /done-when/{criterionId}/satisfy — rank: metic+archon — POST /done-when/:criterionId/satisfy
352
358
  postDoneWhenCriterionIdSatisfy: (args) => request("POST", "/done-when/{criterionId}/satisfy", { hasBody: true }, args),
359
+ // GET /done-when/{criterionId}/uat — rank: any-builder — GET /done-when/:criterionId/uat
360
+ getDoneWhenCriterionIdUat: (args) => request("GET", "/done-when/{criterionId}/uat", { hasBody: false }, args),
361
+ // POST /done-when/{criterionId}/uat — rank: metic+archon — POST /done-when/:criterionId/uat
362
+ postDoneWhenCriterionIdUat: (args) => request("POST", "/done-when/{criterionId}/uat", { hasBody: true }, args),
363
+ // POST /done-when/{criterionId}/uat/recording — rank: metic+archon — POST /done-when/:criterionId/uat/recording
364
+ postDoneWhenCriterionIdUatRecording: (args) => request("POST", "/done-when/{criterionId}/uat/recording", { hasBody: true }, args),
353
365
  // POST /done-when/{criterionId}/unsatisfy — rank: metic+archon — POST /done-when/:criterionId/unsatisfy
354
366
  postDoneWhenCriterionIdUnsatisfy: (args) => request("POST", "/done-when/{criterionId}/unsatisfy", { hasBody: true }, args),
355
367
  // GET /done-when/pending-review — rank: metic+archon — GET /done-when/pending-review
@@ -286,7 +286,10 @@ export interface PostCostRequest { task_id?: number; amount_usd: number; categor
286
286
  export interface PostCostResponse { skipped?: boolean; reason?: unknown; id?: unknown; recorded_at?: unknown }
287
287
  export interface PostDependenciesResponse { ok: boolean; dependency: unknown }
288
288
  export interface PostDiscordChannelsReconcileResponse { ok: boolean; applied: unknown; failed: unknown; total: unknown; warnings: unknown }
289
- export interface PostDoneWhenCriterionIdSatisfyRequest { satisfied_by_task_id?: number }
289
+ export interface PostDoneWhenCriterionIdSatisfyRequest { satisfied_by_task_id?: number; override_reason?: string }
290
+ export interface PostDoneWhenCriterionIdUatRecordingResponse { ok: boolean; recording: unknown; bytes: unknown }
291
+ export interface PostDoneWhenCriterionIdUatRequest { kind: string; recording?: string; live_attested?: boolean; note?: string }
292
+ export interface PostDoneWhenCriterionIdUatResponse { ok: boolean; signoff: unknown; criterion_closed: unknown; achieved_goals: unknown; closed_versions: unknown }
290
293
  export interface PostDoneWhenCriterionIdUnsatisfyResponse { criterion: unknown }
291
294
  export interface PostGateApprovalsPrApproveResponse { ok: boolean; pr: unknown; already_clear?: boolean; message: unknown; head_sha?: unknown; status_set?: boolean; recheck?: unknown; held_for_artist?: unknown }
292
295
  export interface PostGithubRepoVisibilityRequest { repo: string; visibility: "public" | "private" }
@@ -480,6 +483,8 @@ export interface PostVersionsRequest { id: string; name: string; status?: string
480
483
  export interface PostVersionsResponse { version: unknown; goal: unknown; criteria: unknown }
481
484
  export interface PutCopyDeskPagesPageIdDraftRequest { reading_hash: string; lines: unknown[] }
482
485
  export interface PutCopyDeskPagesPageIdDraftResponse { ok: boolean; outcome: unknown; page_id: unknown; round: unknown; draft: unknown; counter: unknown }
486
+ export interface PutDoneWhenCriterionIdBackendOnlyRequest { backend_only: boolean }
487
+ export interface PutDoneWhenCriterionIdBackendOnlyResponse { ok: boolean; criterion_id: unknown; backend_only: unknown }
483
488
  export interface PutGovernmentRanksRankKeyPermissionsPermissionKeyResponse { ok: boolean; rank_key: unknown; permission_key: unknown; granted: unknown }
484
489
  export interface PutMingleOptInRequest { opted_in: boolean }
485
490
  export interface PutMingleOptInResponse { opted_in: unknown; updated_at: unknown }
@@ -752,6 +757,10 @@ export interface BongosClient {
752
757
  /** POST /cost — rank: any-builder */
753
758
  postCost(args: RequestArgs & { body: PostCostRequest }): Promise<PostCostResponse>;
754
759
  };
760
+ "criterionUats": {
761
+ /** GET /criterion-uats/recordings/{name} — rank: any-builder */
762
+ getCriterionUatsRecordingsName(args?: RequestArgs): Promise<ApiResponse>;
763
+ };
755
764
  "dependencies": {
756
765
  /** DELETE /dependencies — rank: archon */
757
766
  deleteDependencies(args?: RequestArgs): Promise<DeleteDependenciesResponse>;
@@ -769,8 +778,16 @@ export interface BongosClient {
769
778
  "doneWhen": {
770
779
  /** PATCH /done-when/{criterionId} — rank: metic+archon */
771
780
  patchDoneWhenCriterionId(args?: RequestArgs & { body?: PatchDoneWhenCriterionIdRequest }): Promise<PatchDoneWhenCriterionIdResponse>;
781
+ /** PUT /done-when/{criterionId}/backend-only — rank: metic+archon */
782
+ putDoneWhenCriterionIdBackendOnly(args: RequestArgs & { body: PutDoneWhenCriterionIdBackendOnlyRequest }): Promise<PutDoneWhenCriterionIdBackendOnlyResponse>;
772
783
  /** POST /done-when/{criterionId}/satisfy — rank: metic+archon */
773
784
  postDoneWhenCriterionIdSatisfy(args?: RequestArgs & { body?: PostDoneWhenCriterionIdSatisfyRequest }): Promise<ApiResponse>;
785
+ /** GET /done-when/{criterionId}/uat — rank: any-builder */
786
+ getDoneWhenCriterionIdUat(args?: RequestArgs): Promise<ApiResponse>;
787
+ /** POST /done-when/{criterionId}/uat — rank: metic+archon */
788
+ postDoneWhenCriterionIdUat(args: RequestArgs & { body: PostDoneWhenCriterionIdUatRequest }): Promise<PostDoneWhenCriterionIdUatResponse>;
789
+ /** POST /done-when/{criterionId}/uat/recording — rank: metic+archon */
790
+ postDoneWhenCriterionIdUatRecording(args?: RequestArgs): Promise<PostDoneWhenCriterionIdUatRecordingResponse>;
774
791
  /** POST /done-when/{criterionId}/unsatisfy — rank: metic+archon */
775
792
  postDoneWhenCriterionIdUnsatisfy(args?: RequestArgs): Promise<PostDoneWhenCriterionIdUnsatisfyResponse>;
776
793
  /** GET /done-when/pending-review — rank: metic+archon */
@@ -328,6 +328,10 @@ export function createClient(opts = {}) {
328
328
  // POST /cost — rank: any-builder — POST /cost
329
329
  postCost: (args) => request("POST", "/cost", { hasBody: true }, args),
330
330
  },
331
+ "criterionUats": {
332
+ // GET /criterion-uats/recordings/{name} — rank: any-builder — GET /criterion-uats/recordings/:name
333
+ getCriterionUatsRecordingsName: (args) => request("GET", "/criterion-uats/recordings/{name}", { hasBody: false }, args),
334
+ },
331
335
  "dependencies": {
332
336
  // DELETE /dependencies — rank: archon — DELETE /dependencies
333
337
  deleteDependencies: (args) => request("DELETE", "/dependencies", { hasBody: false }, args),
@@ -345,8 +349,16 @@ export function createClient(opts = {}) {
345
349
  "doneWhen": {
346
350
  // PATCH /done-when/{criterionId} — rank: metic+archon — PATCH /done-when/:criterionId
347
351
  patchDoneWhenCriterionId: (args) => request("PATCH", "/done-when/{criterionId}", { hasBody: true }, args),
352
+ // PUT /done-when/{criterionId}/backend-only — rank: metic+archon — PUT /done-when/:criterionId/backend-only
353
+ putDoneWhenCriterionIdBackendOnly: (args) => request("PUT", "/done-when/{criterionId}/backend-only", { hasBody: true }, args),
348
354
  // POST /done-when/{criterionId}/satisfy — rank: metic+archon — POST /done-when/:criterionId/satisfy
349
355
  postDoneWhenCriterionIdSatisfy: (args) => request("POST", "/done-when/{criterionId}/satisfy", { hasBody: true }, args),
356
+ // GET /done-when/{criterionId}/uat — rank: any-builder — GET /done-when/:criterionId/uat
357
+ getDoneWhenCriterionIdUat: (args) => request("GET", "/done-when/{criterionId}/uat", { hasBody: false }, args),
358
+ // POST /done-when/{criterionId}/uat — rank: metic+archon — POST /done-when/:criterionId/uat
359
+ postDoneWhenCriterionIdUat: (args) => request("POST", "/done-when/{criterionId}/uat", { hasBody: true }, args),
360
+ // POST /done-when/{criterionId}/uat/recording — rank: metic+archon — POST /done-when/:criterionId/uat/recording
361
+ postDoneWhenCriterionIdUatRecording: (args) => request("POST", "/done-when/{criterionId}/uat/recording", { hasBody: true }, args),
350
362
  // POST /done-when/{criterionId}/unsatisfy — rank: metic+archon — POST /done-when/:criterionId/unsatisfy
351
363
  postDoneWhenCriterionIdUnsatisfy: (args) => request("POST", "/done-when/{criterionId}/unsatisfy", { hasBody: true }, args),
352
364
  // GET /done-when/pending-review — rank: metic+archon — GET /done-when/pending-review
@@ -1,6 +1,6 @@
1
1
  # ADR 0183 — A done-when criterion closes itself; the review queue becomes a place to look
2
2
 
3
- - **Status:** accepted
3
+ - **Status:** superseded in part by [ADR 0351](0351-a-criterion-closes-on-a-uat.md) (2026-09-29): shipped linked tasks are now only the first of three checks, and a criterion closes on a UAT sign-off. The delivery guard below is kept.
4
4
  - **Date:** 2026-08-21
5
5
  - **Task:** 1002069 (owner request 2026-07-05, scope owner-confirmed)
6
6
  - **Supersedes part of:** [ADR 0086](0086-goal-scoped-work-hierarchy.md) §6 / BV1.R63 — the criterion auto-flag stays, but confirming it is no longer the only way a criterion closes.
@@ -76,6 +76,6 @@ Deliberately separate tasks, because the middle one is where the ruling lives an
76
76
 
77
77
  1. **Schema + validation** — the offered set on the catalog, the enabled set on the adoption, `validateDraft`, the routes. Refuses a skill the instance does not have. *Built in task 1004072 (`skills` / `enabled_skills`, specialities_003, `modules/specialities/skills.js`).*
78
78
  2. **The walkthrough** — the adoption flow that presents each skill and takes a decision. The ruling's real deliverable. *Built in task 1004073 (`GET /specialities/:id/skills`, `skills.explainSkill`, the hall settings panel). The consequence above came true: skill descriptions are model-facing, so a SKILL.md may now carry optional people-facing `plain:` / `reach-for:` / `cost:` lines, and the walkthrough says so when it had to derive the words instead.*
79
- 3. **Injection** — the enabled set reaches the session through the contract the Conductor already injects; a line naming the skills this builder leans on, per [ADR 0296](0296-a-role-speciality-is-data-over-three-existing-surfaces.md)'s pattern of using surfaces that exist.
79
+ 3. **Injection** — the enabled set reaches the session through the contract the Conductor already injects; a line naming the skills this builder leans on, per [ADR 0296](0296-a-role-speciality-is-data-over-three-existing-surfaces.md)'s pattern of using surfaces that exist. *Built in task 1004074: `describeForApi` appends one sentence naming the enabled skills — only those still offered and installed — after the provenance fence, since the list is the adopter's choice and not the author's prose. It says it is advice and not permission. Nothing enabled adds nothing. No new mechanism: the port, `GET /me` and the Conductor are unchanged in shape, and the Conductor's craft match scopes the line with the rest of the contract. Pinned end to end by `tests/speciality_session_skills.mjs`.*
80
80
 
81
81
  Nothing is built by this ADR. It is the decision task 1003978 asked to be made first.
@@ -0,0 +1,73 @@
1
+ # ADR 0351 — A criterion closes on a UAT, not on its linked tasks shipping
2
+
3
+ - **Status:** accepted
4
+ - **Date:** 2026-09-29
5
+ - **Task:** [task 1004392](https://cloudbongos.com/builders#/task/1004392) (goal 1000089 — working area 3, Human project management); the hall half is [task 1004402](https://cloudbongos.com/builders#/task/1004402)
6
+ - **Deciders:** the owner (Archon) decided every point below in chat on 2026-09-29, from options Claude laid out. Claude wrote the record.
7
+ - **Supersedes:** [ADR 0183](0183-criteria-close-themselves.md)'s rule that a criterion whose linked tasks all shipped is satisfied. Its delivery guard (at least one task actually shipped) is kept, as the first of three checks.
8
+
9
+ ## Context
10
+
11
+ ADR 0183 made a criterion close itself the moment every task linked to it shipped, because criteria waiting for a human confirm were leaving the board wrong. It fixed that, and it made a criterion something you could close without anyone reading it: link any tasks, ship them, done.
12
+
13
+ It happened. The owner asked why working area 7 (goal 1000111) was blocked, and on the way the criterion `wa7-government` showed satisfied. Its text asks for fully customizable ranks and recommended governments for different project types and sizes. The six tasks that closed it were Board Room navigation work. On the live site there were zero custom ranks, the government recommendation read headcount only and named a charter the page itself marks "documented for later", and a board amendment ratified on 25 Aug had never taken effect. Every check the system ran said done.
14
+
15
+ The owner's diagnosis: a criterion has to be achieved in the code **and** in what a person can actually do on the live site, and closing one is a bigger review step than a task shipping.
16
+
17
+ ## Decision
18
+
19
+ ### 1. Three checks, and only the first is automatic
20
+
21
+ | Check | Passes when |
22
+ |---|---|
23
+ | **Code** | every linked task is terminal and at least one shipped (ADR 0183's delivery guard) |
24
+ | **Live** | the shipped work is running on the deployed site |
25
+ | **UAT** | a signed-in person performed the criterion on the live site, and a recording of it is stored |
26
+
27
+ `autoSatisfyShippedCriteria` still runs from the ship transaction and the reconciler sweep, with its goal and version cascade, but a criterion is a candidate only when it also carries a **current** sign-off: a `uat` or `backend_signoff` row in `lifecycle_criterion_uats` created no earlier than the newest linked ship. Work linked and shipped after a sign-off is work nobody checked, so that criterion waits for a fresh UAT.
28
+
29
+ ### 2. "Awaiting UAT" is a state people can see
30
+
31
+ A criterion whose Code check passes and has no current sign-off reads **Awaiting UAT**, so the board shows the baseline is met (the owner asked for this explicitly). The states, derived in one place (`modules/lifecycle/criterion-uat.js` `uatState`) and carried on every criterion read (`/status`, the goal page, the review queue): Open · Awaiting UAT · Satisfied (UAT) · Satisfied (backend sign-off) · Satisfied (override) · Closed before UAT.
32
+
33
+ ### 3. The signer cannot have shipped the work, and one step is the sign-off
34
+
35
+ Whoever submits the recording is the sign-off; there is no second approver. The server refuses them if they shipped any linked task, with one exception, the **project owner**, so a solo project cannot deadlock on its own work. The permission is `criterion.review`, the review queue's own atom (Metic+), because the sign-off is that review now.
36
+
37
+ ### 4. The recording lives on the project's own server
38
+
39
+ An mp4 or webm, up to 100 MB, uploaded as a raw body, screened by type and magic bytes, stored under a server-minted name outside the git tree, and served only to signed-in builders. No third-party service, no new monthly cost.
40
+
41
+ ### 5. Backend-only is decided when the criterion is written
42
+
43
+ A criterion with no screen can be marked **backend-only**, and then takes a recording-free backend sign-off, still from a non-shipper. The flag is an authoring act (`criterion.create` plus the goal's authoring wall, ADR 0154) and the server refuses to change it once the criterion is Awaiting UAT or closed, so the person closing it cannot choose to skip the recording.
44
+
45
+ ### 6. Live is read where the site can tell, and attested where it cannot
46
+
47
+ On the Bongos hall the `npm-release` module already places every task in a deploy stage; it now provides that as the `deploy.taskWhere` port, and a sign-off is refused while any linked shipped task is not running on the site. Most instances have no such reading, and a reading that fails is treated the same way: the signer confirms they tested it on the live site, and the row records `live_basis = 'attested'`. An outage never blocks a person; it only moves the claim to their word, which is recorded as such.
48
+
49
+ ### 7. Criteria already closed stay closed
50
+
51
+ The owner chose not to reopen them: reopening every satisfied criterion would reopen finished goals across the project. They read "Closed before UAT" because they carry no sign-off row. `wa7-government` alone was reopened, by hand, with a task (1004400) linked to it so the sweep cannot close it again.
52
+
53
+ ### 8. The manual confirm becomes a recorded override
54
+
55
+ `POST /done-when/:id/satisfy` survives as the escape hatch (a criterion met some other way still needs one) but requires `override_reason`, writes an `override` row, and the criterion reads "Satisfied (override)" permanently.
56
+
57
+ ### 9. The walk is a skill
58
+
59
+ `/goal-uat` scripts the live-site test from the criterion's words, collects the recording and signs it off, through `scripts/gds/uat.js`. `/goal-review` keeps only the residue a UAT cannot test (all linked work abandoned, or none linked). The hall controls are task 1004402.
60
+
61
+ ## Consequences
62
+
63
+ - From the release that carries this, no criterion closes without a person. Goals whose last criterion is Awaiting UAT wait for one. That is the intended cost; ADR 0183's worry was a board that lies, and a board that says "Awaiting UAT" is telling the truth.
64
+ - The pile-up ADR 0183 was written against can come back as an Awaiting UAT backlog. `/goal-uat` and the hall controls exist to keep that queue fast to work; if it grows anyway, the fix is making UAT cheaper, not letting criteria close themselves again.
65
+ - Between this piece and task 1004402 the hall's Confirm button no longer closes a criterion; it explains that criteria close on a UAT.
66
+
67
+ ## Alternatives rejected
68
+
69
+ - **A human review of the task list instead of the live site.** That is what `/goal-review` was, and it checked the tasks, not the words.
70
+ - **Two-person sign-off** (a recorder and a separate approver). Heavier, and the non-shipper rule already puts a second person on the work.
71
+ - **A link to a recording on an outside service.** Links rot and leave the ledger; the owner chose the project's own server.
72
+ - **Refusing the sign-off when the deploy reading fails.** That blocks a person on an outage; attestation is recorded as attestation.
73
+ - **Reopening every satisfied criterion.** Rejected by the owner (decision 7).
@@ -59,6 +59,33 @@ When a later decision overrides an earlier one, **do not edit the old ADR**. Ins
59
59
 
60
60
  This keeps the decision history honest and traceable.
61
61
 
62
+ ## Citing an ADR, and the numbers two files share
63
+
64
+ **Cite an ADR by its filename, not by its number alone.** Write a link — `[ADR 0195](0195-adr-numbers-are-checked-like-migration-numbers.md)` — or the bare filename. A number by itself is ambiguous for the 20 numbers below, and a reader (human or agent) given only "ADR 0024" has two documents to choose from and no way to know which was meant. No tool resolves an ADR by number: the harm is to whoever reads the prose.
65
+
66
+ These 20 numbers are each shared by exactly two files. They are **accepted history, not a to-do**: they are not renumbered, because renaming a file breaks every existing citation to it, which is the harm this rule prevents. `scripts/gds/adr-namespace.js` (fitness Check 31, part of the required `unit` check; [ADR 0195](0195-adr-numbers-are-checked-like-migration-numbers.md)) freezes each pair by exact filename in `LEGACY_DUPLICATE_ADRS`, so a new collision, a third file on one of these numbers, or a rename of either half fails CI. The list may shrink and never grows. A new ADR takes its number from `node scripts/gds/next-number.js`.
67
+
68
+ - **0008** — [`0008-family-based-tile-generation.md`](0008-family-based-tile-generation.md) · [`0008-google-chat-oauth-user-auth.md`](0008-google-chat-oauth-user-auth.md)
69
+ - **0024** — [`0024-cloneable-repo-local-first-memory.md`](0024-cloneable-repo-local-first-memory.md) · [`0024-multi-agent-system-architecture.md`](0024-multi-agent-system-architecture.md)
70
+ - **0025** — [`0025-offsite-backup-vendor-digitalocean-spaces.md`](0025-offsite-backup-vendor-digitalocean-spaces.md) · [`0025-structured-criterion-task-link.md`](0025-structured-criterion-task-link.md)
71
+ - **0044** — [`0044-mediterranean-palette-replacement.md`](0044-mediterranean-palette-replacement.md) · [`0044-per-box-live-game-preview.md`](0044-per-box-live-game-preview.md)
72
+ - **0057** — [`0057-container-cost-ledger.md`](0057-container-cost-ledger.md) · [`0057-discord-archon-approval-channels.md`](0057-discord-archon-approval-channels.md)
73
+ - **0072** — [`0072-bongos-app-mac-signed-first-windows-deferred.md`](0072-bongos-app-mac-signed-first-windows-deferred.md) · [`0072-dev-box-code-staleness-visibility.md`](0072-dev-box-code-staleness-visibility.md)
74
+ - **0073** — [`0073-builder-needs-signal-and-byok-gemini-key.md`](0073-builder-needs-signal-and-byok-gemini-key.md) · [`0073-secrets-scan-exclude-uri-detector.md`](0073-secrets-scan-exclude-uri-detector.md)
75
+ - **0082** — [`0082-productized-key-storage-provisioning.md`](0082-productized-key-storage-provisioning.md) · [`0082-server-side-merge-conflict-auto-resolution.md`](0082-server-side-merge-conflict-auto-resolution.md)
76
+ - **0087** — [`0087-bongos-app-architecture-and-handoff-contract.md`](0087-bongos-app-architecture-and-handoff-contract.md) · [`0087-compete-on-governance-not-tooling-cursor.md`](0087-compete-on-governance-not-tooling-cursor.md)
77
+ - **0095** — [`0095-borrowed-memory-and-retrieval-concepts.md`](0095-borrowed-memory-and-retrieval-concepts.md) · [`0095-cross-agent-context-management.md`](0095-cross-agent-context-management.md)
78
+ - **0097** — [`0097-one-active-claim-per-session-and-worktree-binding.md`](0097-one-active-claim-per-session-and-worktree-binding.md) · [`0097-retroactive-reward-backfill.md`](0097-retroactive-reward-backfill.md)
79
+ - **0103** — [`0103-core-first-extraction-cloud-bongos-trunk.md`](0103-core-first-extraction-cloud-bongos-trunk.md) · [`0103-gdsv4-bongos-consolidation.md`](0103-gdsv4-bongos-consolidation.md)
80
+ - **0144** — [`0144-devbox-rehome-onto-cloudbongos-plane.md`](0144-devbox-rehome-onto-cloudbongos-plane.md) · [`0144-federated-single-logout-backchannel.md`](0144-federated-single-logout-backchannel.md)
81
+ - **0145** — [`0145-devbox-app-branding-driven-module.md`](0145-devbox-app-branding-driven-module.md) · [`0145-free-hosted-project-tier-isolation-and-domain-separation.md`](0145-free-hosted-project-tier-isolation-and-domain-separation.md)
82
+ - **0152** — [`0152-landing-gate-served-by-the-core-app.md`](0152-landing-gate-served-by-the-core-app.md) · [`0152-metic-task-abandonment.md`](0152-metic-task-abandonment.md)
83
+ - **0167** — [`0167-gate-trust-link-agpl-default.md`](0167-gate-trust-link-agpl-default.md) · [`0167-module-catalog-source-vs-provenance.md`](0167-module-catalog-source-vs-provenance.md)
84
+ - **0172** — [`0172-editable-rank-roles-substrate-axis.md`](0172-editable-rank-roles-substrate-axis.md) · [`0172-per-craft-compensation-ideator-credit-lane.md`](0172-per-craft-compensation-ideator-credit-lane.md)
85
+ - **0184** — [`0184-a-task-visual-inherits-its-summary-audience.md`](0184-a-task-visual-inherits-its-summary-audience.md) · [`0184-ship-requires-an-assurance.md`](0184-ship-requires-an-assurance.md)
86
+ - **0187** — [`0187-collab-four-decisions.md`](0187-collab-four-decisions.md) · [`0187-oauth-handshake-cookie-lifetime.md`](0187-oauth-handshake-cookie-lifetime.md)
87
+ - **0192** — [`0192-a-category-orients-and-authorises-nothing.md`](0192-a-category-orients-and-authorises-nothing.md) · [`0192-platform-visibility-member-door.md`](0192-platform-visibility-member-door.md)
88
+
62
89
  ## Index
63
90
 
64
91
  | # | Title | Topic |
@@ -442,3 +469,4 @@ This keeps the decision history honest and traceable.
442
469
  | 0348 | [**The web tier may look, read-only, at what an owner's Render key can see** ([task 1004352](https://cloudbongos.com/builders#/task/1004352), goal 1000106 — working area 1, owner Lars). Amends ADR 0111 §2. **D1 (owner):** one route, `POST /provisioning/render/lookup`, may call Render from the web tier — GET only (workspaces; web services in one named workspace), the owner's key held for that request and never stored, logged or returned, every service checked to carry the workspace asked about, capped per builder. Every write stays in the runner. **D2 (owner):** a key sent during a Create rides the project's own `provision` run; the runner sets the app up on Render straight after the project is up and clears the key either way.](0348-the-web-tier-may-look-read-only-at-what-an-owners-render-key-can-see.md) | provisioning / security / render |
443
470
  | 0349 | [**A version preview is a sandboxed child the web tier launches, reached by a cookie** ([task 1004300](https://cloudbongos.com/builders#/task/1004300), goal 1000090 — BONGOS-V2, owner Lars, approved 2026-09-29). Amends ADR 0293 D1 for this feature only. **D1 (owner):** route by a `bongos_preview` cookie through an optional `request.divert` seam, not a path; forward only with the cookie, off the sign-in and preview paths, with no Bearer, and with `core.pin.move` re-checked on every request. **D2 (owner):** the npm-release module launches the preview itself, with a deny-by-default environment, a dump-and-restore copy of the database (never a template copy), one at a time, 20-minute idle stop, published versions within 30 of the running one. **D3:** the same database role and no egress firewall are accepted for now.](0349-a-version-preview-is-a-sandboxed-child-the-web-tier-launches.md) | modules / npm-release / deploy |
444
471
  | 0350 | [**The hub holds a per-project WRITE deploy key, so a hosted project's upgraded pin always reaches its GitHub repo** ([task 1004291](https://cloudbongos.com/builders#/task/1004291), follow-up to task 1004065). Amends ADR 0176. The owner's token lasts one hour and the checkout's own key is read-only, so most hosted upgrades ended "GitHub was not updated". **D1:** a second deploy key per repo, registered `read_only: false` as `cloudbongos-pin-<slug>`, the pull key untouched. **D2:** kept outside every checkout in the runner's own directory (0700 dir, 0600 key, modes verified), unreadable by the project's account. **D3:** minted at standup and whenever a fresh owner token finds none, so reconnecting GitHub once is enough. **D4:** `pushUpgradePin` tries write key, then owner token, then origin, never forced. **D5:** revoked on teardown and disconnect. **D6:** a project on the shared app user is refused a key: "needs its own account first". **D7:** /deploy says reconnecting sets up lasting access.](0350-the-hub-holds-a-per-project-write-deploy-key-so-a-hosted-upgrade-reaches-github.md) | provisioning / security |
472
+ | 0351 | [**A criterion closes on a UAT, not on its linked tasks shipping** ([task 1004392](https://cloudbongos.com/builders#/task/1004392), goal 1000089 — working area 3). Supersedes ADR 0183's close rule. `wa7-government` had closed on six Board Room navigation tasks while nothing it describes was on the live site. **D1:** three checks — Code (linked tasks done, ≥1 shipped; automatic), Live, UAT — and the sweep closes a criterion only with a CURRENT sign-off (no older than the newest linked ship). **D2 (owner):** "Awaiting UAT" is a visible state. **D3 (owner):** one step; the signer is refused if they shipped linked work, the project owner excepted. **D4 (owner):** an mp4/webm up to 100 MB on the project's own server. **D5 (owner):** backend-only is set while a criterion is open and takes a recording-free sign-off. **D6 (owner):** Live is read via the `deploy.taskWhere` port where the site can tell, attested otherwise. **D7 (owner):** already-closed criteria stay closed ("Closed before UAT"). **D8:** `/satisfy` is a recorded override that needs a reason. **D9:** `/goal-uat` + `scripts/gds/uat.js`; the hall is task 1004402.](0351-a-criterion-closes-on-a-uat.md) | lifecycle / criteria / review |