@bongos/core 1.20.4 → 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 (56) hide show
  1. package/.bongos-core.json +103 -53
  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/0015-task-dependencies-and-auto-promotion.md +2 -0
  10. package/docs/adr/0183-criteria-close-themselves.md +1 -1
  11. package/docs/adr/0310-a-speciality-offers-skills-and-the-adopter-chooses-them.md +1 -1
  12. package/docs/adr/0351-a-criterion-closes-on-a-uat.md +73 -0
  13. package/docs/adr/README.md +28 -0
  14. package/docs/api/openapi.json +385 -5
  15. package/docs/api-reference.md +14 -4
  16. package/docs/architecture.md +7 -0
  17. package/docs/copy-inventory.md +22 -22
  18. package/docs/copy-registry.json +23 -23
  19. package/docs/file-map.md +2 -1
  20. package/docs/module-api-changelog.md +4 -0
  21. package/docs/page-readings.json +3 -3
  22. package/modules/hall-ui/public/goals-page.js +8 -3
  23. package/modules/hall-ui/public/tweak-editor.css +4 -7
  24. package/modules/hall-ui/public/tweak-editor.html +3 -3
  25. package/modules/hall-ui/public/tweak-editor.js +3 -0
  26. package/modules/lifecycle/criterion-uat-db.js +267 -0
  27. package/modules/lifecycle/criterion-uat.js +303 -0
  28. package/modules/lifecycle/db-goals.js +25 -12
  29. package/modules/lifecycle/done-when.js +84 -17
  30. package/modules/lifecycle/migrations/lifecycle_015_criterion_uat.sql +73 -0
  31. package/modules/lifecycle/module.json +1 -0
  32. package/modules/lifecycle/routes/criterion-uat.js +169 -0
  33. package/modules/lifecycle/routes/done-when.js +25 -1
  34. package/modules/npm-release/module.json +3 -1
  35. package/modules/npm-release/routes/task-where.js +18 -1
  36. package/modules/npm-release/work.js +38 -7
  37. package/modules/specialities/routes/specialities.js +8 -1
  38. package/modules/specialities/specialities.js +26 -1
  39. package/package-lock.json +2 -2
  40. package/package.json +1 -1
  41. package/release-notes.json +24 -0
  42. package/scripts/gds/cli-lib.js +4 -1
  43. package/scripts/gds/fitness-checks-write-validation.js +4 -0
  44. package/scripts/gds/status.js +14 -3
  45. package/scripts/gds/uat.js +120 -0
  46. package/src/bongos/route-rank-check.js +9 -0
  47. package/src/module-api.js +1 -1
  48. package/tests/auto_satisfy_criteria.mjs +4 -2
  49. package/tests/criterion_uat.mjs +467 -0
  50. package/tests/criterion_uat_routes.mjs +232 -0
  51. package/tests/fitness.mjs +3 -1
  52. package/tests/goal_achievement.mjs +4 -2
  53. package/tests/goal_routes.mjs +4 -2
  54. package/tests/ideator_full_idea_shapes_space_proof.mjs +3 -2
  55. package/tests/npm_release_where.mjs +18 -2
  56. package/tests/speciality_session_skills.mjs +150 -0
@@ -0,0 +1,84 @@
1
+ ---
2
+ name: goal-uat
3
+ description: >-
4
+ Test criteria awaiting UAT on the live site, then sign off. Metic+. Triggers: "/goal-uat", "what's awaiting UAT".
5
+ ---
6
+
7
+ You are running the **goal-uat** session: the closing step of a goal. A criterion no longer closes because its linked tasks shipped ([ADR 0351](../../../docs/adr/0351-a-criterion-closes-on-a-uat.md), task 1004392). Once its work has shipped it reads **Awaiting UAT**, and it closes only when a person who did **not** ship that work does what the criterion describes **on the live site** and signs it off.
8
+
9
+ This is the owner's rule because criteria were being closed by any tasks that happened to be linked to them, whether or not the thing the criterion describes was real. A UAT is the check against the criterion's **words**, not against the task list. Hold it to that.
10
+
11
+ ## Rank gate: Metic+ only
12
+
13
+ Signing off rides `criterion.review` (Metic+); the server enforces it. Check first so a Xenos is not walked into a wall of 403s: `node scripts/gds/api.js GET /api/bongos/me` and read `builder.rank`. If it lowercases to `xenos` or `thetes`, stop and say: *"Signing off a UAT is a Metic+ step. Ask an Archon to promote you, or ask a Metic to run it."*
14
+
15
+ ## The three checks (what you are verifying)
16
+
17
+ | Check | Passes when | Who decides |
18
+ |---|---|---|
19
+ | **Code** | every linked task is done and at least one shipped | automatic |
20
+ | **Live** | the shipped work is running on the deployed site | read automatically where the site can tell; otherwise the signer confirms it |
21
+ | **UAT** | a signed-in person performed the criterion on the live site; a recording is stored | the signer, who must not have shipped any linked task (the project owner excepted) |
22
+
23
+ A criterion marked **backend-only** (set when it was written, visible on the criterion) has no screen to record, so it takes a recording-free **backend sign-off** instead. It still needs a non-shipper to sign it.
24
+
25
+ ## Step 1: the queue
26
+
27
+ ```
28
+ node scripts/gds/uat.js --queue
29
+ ```
30
+
31
+ (Add `--version <id>` to scope it.) If nothing is awaiting UAT, say so and stop. If the builder named a goal, keep only that goal's rows.
32
+
33
+ ## Step 2: one criterion at a time
34
+
35
+ For each, run `node scripts/gds/uat.js <criterion-id>` and present, in plain words:
36
+
37
+ - the criterion's text, in full;
38
+ - its goal, and whether it is the goal's **last** open criterion (then signing it off achieves the goal);
39
+ - the Live line (running, not yet, or "this site cannot tell");
40
+ - whether it is backend-only.
41
+
42
+ Then tell the tester exactly what to do **on the live site**, derived from the criterion's words: which page, what to click, what they should see. Write it as a short numbered script. If the criterion's words describe something the tester cannot find on the live site, that is the answer: **it is not met.** Say so and go to Step 4 (reject) rather than looking for a reading under which it passes.
43
+
44
+ ## Step 3: record and sign off
45
+
46
+ The tester records their screen doing the script (any screen recorder that saves **mp4 or webm**, up to **100 MB**; Windows: Win+Alt+R with Xbox Game Bar, or the Snipping Tool's record button; macOS: Cmd+Shift+5). Then:
47
+
48
+ ```
49
+ node scripts/gds/uat.js <criterion-id> --recording <path-to-file> --note "<one line: what was checked>"
50
+ ```
51
+
52
+ For a backend-only criterion, after the tester has checked it by whatever means proves it (a log line, an API read, a test run against live):
53
+
54
+ ```
55
+ node scripts/gds/uat.js <criterion-id> --backend-signoff --note "<how it was checked>"
56
+ ```
57
+
58
+ Add `--live-attested` only when the server says this site cannot tell whether the work is live, and only if the tester really did it on the live site.
59
+
60
+ **Refusals, in the words to give the builder:**
61
+
62
+ | Refusal | Meaning |
63
+ |---|---|
64
+ | `signer_shipped_this_work` | they shipped linked work; someone else signs this one |
65
+ | `work_not_live` | shipped but not deployed yet; sign off after the deploy |
66
+ | `criterion_not_awaiting_uat` | linked work still open (or none linked, or all abandoned: that is `/goal-review`) |
67
+ | `criterion_needs_recording` / `criterion_is_backend_only` | wrong kind of sign-off for this criterion |
68
+ | `live_attestation_required` | confirm it was tested on the live site (`--live-attested`) |
69
+
70
+ A sign-off on a goal's last open criterion reports the goal achieved; say so.
71
+
72
+ ## Step 4: when it is NOT met
73
+
74
+ The shipped work does not do what the criterion says. Do not sign off. File the missing work as a task linked to the criterion (`POST /api/bongos/tasks`, then `POST /api/bongos/tasks/<id>/criteria` with `{"criterion_id": <criterion-id>}`) so the criterion drops back to Open until that ships. That is what happened to `wa7-government` (task 1004400).
75
+
76
+ ## What this skill does NOT do
77
+
78
+ - It never uses `POST /done-when/:id/satisfy`. That is the **override** now: it requires a written `override_reason` and the criterion reads "Satisfied (override)" for good. Use it only when the owner asks, and never to get past a refusal above.
79
+ - It never marks a criterion backend-only to avoid recording one. Backend-only is set while a criterion is still open (`uat.js <id> --backend-only on`), and the server refuses it once the criterion is awaiting UAT.
80
+ - The all-abandoned and no-linked-task residue is `/goal-review`'s, not this skill's.
81
+
82
+ ## Summary to give at the end
83
+
84
+ Criteria walked · signed off (ids) · goals achieved · rejected, with the follow-up task filed for each · left waiting (not live yet, or no tester available).
@@ -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
@@ -58,6 +58,8 @@ The function fires when `NEW.status='shipped' AND OLD.status<>'shipped'`. It sca
58
58
 
59
59
  `claimTasksBatch` does the same check per-task but treats batch-internal deps as "OK" (the batch is worked serially).
60
60
 
61
+ **The batch caller owns the ordering (task 1004391).** "Worked serially" is a promise the caller makes, not one the system keeps. `claimTasksBatch` (now in [modules/lifecycle/db-claims.js](../../modules/lifecycle/db-claims.js), with its dry-run twin in the same file) only waives the claim-time check for a dependency that is claimed in the same batch; **nothing at ship time checks that the dependency shipped first**. Once both are claimed, a dependent can pass `completed`, `confirmed` and land on `main` before the task it depends on. Whoever claims a batch — a builder, `/builder-sequence`, the autonomous runner — must ship each in-batch dependency before its dependent. The owner chose to write this rule down rather than add a ship-time gate (which would change the protected ship pipeline); if dependents landing out of order ever causes real damage, the gate is the follow-up.
62
+
61
63
  ### API surface
62
64
 
63
65
  | Verb | Path | Body / returns |
@@ -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 |