@bongos/core 1.19.365 → 1.19.367

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 (34) hide show
  1. package/.bongos-core.json +93 -28
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +8 -0
  4. package/clients/bongos-client/index.cjs +8 -0
  5. package/clients/bongos-client/index.d.ts +9 -1
  6. package/clients/bongos-client/index.mjs +8 -0
  7. package/docs/adr/0192-a-category-orients-and-authorises-nothing.md +97 -0
  8. package/docs/adr/README.md +1 -0
  9. package/docs/api/openapi.json +117 -1
  10. package/docs/api-reference.md +11 -3
  11. package/docs/file-map.md +13 -1
  12. package/migrations/core_226_work_categories.sql +100 -0
  13. package/modules/builder-settings/event-sound-prefs.js +14 -4
  14. package/modules/dev-box/app/src/vendor/bongos-client.cjs +8 -0
  15. package/modules/hall-ui/public/alert.wav +0 -0
  16. package/modules/hall-ui/public/fanfare.wav +0 -0
  17. package/modules/hall-ui/public/ship.wav +0 -0
  18. package/modules/hall-ui/public/triumph.wav +0 -0
  19. package/modules/lifecycle/db-goals.js +5 -5
  20. package/modules/lifecycle/goal-category.js +273 -0
  21. package/modules/lifecycle/routes/goals.js +38 -2
  22. package/package-lock.json +2 -2
  23. package/package.json +1 -1
  24. package/scripts/gds/category-advisory-guard.js +153 -0
  25. package/scripts/gds/fitness.js +6 -1
  26. package/scripts/gds/gen-sounds.js +176 -0
  27. package/scripts/gds/ship-io.js +16 -2
  28. package/scripts/gds/turn-end-sound.js +74 -0
  29. package/src/bongos/route-rank-check.js +11 -0
  30. package/src/module-api.js +5 -1
  31. package/tests/category_advisory_guard.mjs +135 -0
  32. package/tests/goal_category.mjs +199 -0
  33. package/tests/goal_tier.mjs +7 -5
  34. package/tests/sound_player.mjs +345 -0
@@ -224,6 +224,10 @@
224
224
  {
225
225
  "name": "versions",
226
226
  "description": "Endpoints under `/api/bongos/versions`."
227
+ },
228
+ {
229
+ "name": "work-categories",
230
+ "description": "Endpoints under `/api/bongos/work-categories`."
227
231
  }
228
232
  ],
229
233
  "paths": {
@@ -4553,6 +4557,51 @@
4553
4557
  ]
4554
4558
  }
4555
4559
  },
4560
+ "/goals/{id}/category": {
4561
+ "patch": {
4562
+ "operationId": "patch_goals_id_category",
4563
+ "tags": [
4564
+ "goals"
4565
+ ],
4566
+ "summary": "PATCH /goals/:id/category",
4567
+ "description": "PATCH /goals/:id/category — body { category_id } (null clears it). Separate from PATCH /goals/:id because that route is the PROSE edit with its write-once posture and before/after audit delta; a label is neither. rank: any-builder (requireBuilder) + the in-handler owner|manager|Archon gate.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
4568
+ "x-rank": "any-builder",
4569
+ "x-source": "modules/lifecycle/routes/goals.js",
4570
+ "parameters": [
4571
+ {
4572
+ "name": "id",
4573
+ "in": "path",
4574
+ "required": true,
4575
+ "schema": {
4576
+ "type": "string"
4577
+ },
4578
+ "description": "Path parameter `id`."
4579
+ }
4580
+ ],
4581
+ "responses": {
4582
+ "200": {
4583
+ "description": "Success."
4584
+ },
4585
+ "400": {
4586
+ "$ref": "#/components/responses/BadRequest"
4587
+ },
4588
+ "401": {
4589
+ "$ref": "#/components/responses/Unauthorized"
4590
+ },
4591
+ "403": {
4592
+ "$ref": "#/components/responses/Forbidden"
4593
+ },
4594
+ "404": {
4595
+ "$ref": "#/components/responses/NotFound"
4596
+ }
4597
+ },
4598
+ "security": [
4599
+ {
4600
+ "builderSession": []
4601
+ }
4602
+ ]
4603
+ }
4604
+ },
4556
4605
  "/goals/{id}/conflicts": {
4557
4606
  "get": {
4558
4607
  "operationId": "get_goals_id_conflicts",
@@ -14324,6 +14373,69 @@
14324
14373
  },
14325
14374
  "security": []
14326
14375
  }
14376
+ },
14377
+ "/work-categories": {
14378
+ "get": {
14379
+ "operationId": "get_work_categories",
14380
+ "tags": [
14381
+ "work-categories"
14382
+ ],
14383
+ "summary": "GET /work-categories",
14384
+ "description": "---- the ADVISORY human category (task 1003275, criterion legibility-categories) ---- A category ORIENTS a person and steers Claude; it authorises nothing. Nothing in this file, in claim eligibility, or in the claimable feed may branch on it — scripts/gds/category-advisory-guard.js fails the build if that changes (ADR 0192). GET /work-categories — the project's set. rank: any-builder; it is a vocabulary, and a client that cannot read it hard-codes the labels instead.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
14385
+ "x-rank": "any-builder",
14386
+ "x-source": "modules/lifecycle/routes/goals.js",
14387
+ "responses": {
14388
+ "200": {
14389
+ "description": "Success."
14390
+ },
14391
+ "400": {
14392
+ "$ref": "#/components/responses/BadRequest"
14393
+ },
14394
+ "401": {
14395
+ "$ref": "#/components/responses/Unauthorized"
14396
+ },
14397
+ "403": {
14398
+ "$ref": "#/components/responses/Forbidden"
14399
+ }
14400
+ },
14401
+ "security": [
14402
+ {
14403
+ "builderSession": []
14404
+ }
14405
+ ]
14406
+ },
14407
+ "post": {
14408
+ "operationId": "post_work_categories",
14409
+ "tags": [
14410
+ "work-categories"
14411
+ ],
14412
+ "summary": "POST /work-categories",
14413
+ "description": "POST /work-categories — the endpoint the \"Claude could not place this goal\" prompt writes into. Same permission as authoring a goal: extending the project's vocabulary is a planning act. A duplicate slug returns the existing row (200), so two people prompted at once converge instead of one being stranded.\n\n**Rank:** `metic+archon` — Metic or Archon rank (review/triage powers).\n\n**Permissions:** `goal.create` (all required).",
14414
+ "x-rank": "metic+archon",
14415
+ "x-source": "modules/lifecycle/routes/goals.js",
14416
+ "x-permissions": [
14417
+ "goal.create"
14418
+ ],
14419
+ "responses": {
14420
+ "200": {
14421
+ "description": "Success."
14422
+ },
14423
+ "400": {
14424
+ "$ref": "#/components/responses/BadRequest"
14425
+ },
14426
+ "401": {
14427
+ "$ref": "#/components/responses/Unauthorized"
14428
+ },
14429
+ "403": {
14430
+ "$ref": "#/components/responses/Forbidden"
14431
+ }
14432
+ },
14433
+ "security": [
14434
+ {
14435
+ "builderSession": []
14436
+ }
14437
+ ]
14438
+ }
14327
14439
  }
14328
14440
  },
14329
14441
  "components": {
@@ -17888,6 +18000,10 @@
17888
18000
  "succeeds_goal_id": {
17889
18001
  "type": "integer",
17890
18002
  "minimum": 1
18003
+ },
18004
+ "category_id": {
18005
+ "type": "integer",
18006
+ "minimum": 1
17891
18007
  }
17892
18008
  },
17893
18009
  "required": [
@@ -19961,7 +20077,7 @@
19961
20077
  "description": "A required dependency/feature is not configured or is temporarily down."
19962
20078
  }
19963
20079
  },
19964
- "x-endpoint-count": 315,
20080
+ "x-endpoint-count": 318,
19965
20081
  "x-schema-count": 353,
19966
20082
  "x-undocumented-bodies": 8,
19967
20083
  "x-response-schemas": 237,
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Bongos API reference
4
4
 
5
- > **Generated from the live route files** — the route file is authoritative. 315 endpoints across 52 route files.
5
+ > **Generated from the live route files** — the route file is authoritative. 318 endpoints across 52 route files.
6
6
  > Machine-readable spec: [`docs/api/openapi.json`](api/openapi.json) (OpenAPI 3.1). Rendered docs site: **`/docs`** (e.g. `cloudbongos.com/docs`).
7
7
 
8
8
  Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>.md)): `public` < `any-builder` < `metic+archon` < `archon`.
@@ -205,15 +205,16 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
205
205
  | GET | `/api/bongos/github/repos` | `any-builder` | — | acts only on the caller's OWN GitHub identity (the token they granted). |
206
206
  | POST | `/api/bongos/github/repos` | `any-builder` | `name` | creates a repo in the caller's OWN account. |
207
207
 
208
- ## `goals` (26)
208
+ ## `goals` (27)
209
209
 
210
210
  | Method | Path | Rank | Body | Description |
211
211
  |---|---|---|---|---|
212
212
  | GET | `/api/bongos/goals` | `any-builder` | — | GET /goals?version=&status= — list goals (read). |
213
- | POST | `/api/bongos/goals` | `metic+archon` | `version_id`, `title`, `subtitle`, `description`, `scope_modules`, `succeeds_goal_id` | POST /goals — create a goal. |
213
+ | POST | `/api/bongos/goals` | `metic+archon` | `version_id`, `title`, `subtitle`, `description`, `scope_modules`, `succeeds_goal_id`, `category_id` | POST /goals — create a goal. |
214
214
  | GET | `/api/bongos/goals/:id` | `any-builder` | — | GET /goals/:id — one goal with its members + linked criteria (read). |
215
215
  | PATCH | `/api/bongos/goals/:id` | `any-builder` | — | PATCH /goals/:id — correct a goal's own title/subtitle/description (tasks 1003102, 1003224). |
216
216
  | POST | `/api/bongos/goals/:id/archive` | `metic+archon` | — | POST /goals/:id/archive — retire a goal (open\|achieved → archived). |
217
+ | PATCH | `/api/bongos/goals/:id/category` | `any-builder` | — | PATCH /goals/:id/category — body { category_id } (null clears it). |
217
218
  | GET | `/api/bongos/goals/:id/conflicts` | `any-builder` | — | GET /goals/:id/conflicts — the server-side task-conflict view (task 1736). |
218
219
  | POST | `/api/bongos/goals/:id/conflicts/resolve` | `any-builder` | `action`, `note`, `touches`, `first_task_id`, `then_task_id`, `task_id` | POST /goals/:id/conflicts/resolve — the owner/manager resolution actions for a detected conflict (task 1736; authority model ADR 0106): s… |
219
220
  | POST | `/api/bongos/goals/:id/invitations` | `any-builder` | `note`, `builder_id` | POST /goals/:id/invitations { builder_id, note? } — an owner\|manager\|Archon invites a builder (task 1952). |
@@ -577,6 +578,13 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
577
578
  | GET | `/api/bongos/versions/close-suggestions` | `public` | — | Close-flow gate suggestions for every version still 'building'. |
578
579
  | GET | `/api/bongos/versions/progress` | `public` | — | read-only progress aggregate; mirrors /public/progress. |
579
580
 
581
+ ## `work-categories` (2)
582
+
583
+ | Method | Path | Rank | Body | Description |
584
+ |---|---|---|---|---|
585
+ | GET | `/api/bongos/work-categories` | `any-builder` | — | ---- the ADVISORY human category (task 1003275, criterion legibility-categories) ---- A category ORIENTS a person and steers Claude; it a… |
586
+ | POST | `/api/bongos/work-categories` | `metic+archon` | — | POST /work-categories — the endpoint the "Claude could not place this goal" prompt writes into. |
587
+
580
588
  ---
581
589
 
582
590
  > 8 write endpoint(s) take a JSON body with no declared validator schema — their fields are not yet machine-documented. Adding a `validateOrRespond` schema (`src/bongos/routes/_helpers.js`) surfaces them here automatically.
package/docs/file-map.md CHANGED
@@ -412,12 +412,24 @@ To install on a new machine, symlink each routine's directory under `~/.claude/s
412
412
  ├── lifecycle-card.js ← PostToolUse: deterministic delivery of MUTATING lifecycle cards (claim/ship/release) — the write-side mirror of session-cards.js (task 1288)
413
413
  ├── pre-compact.js ← PreCompact: thin reader around eviction-policy.js — emits an eviction directive (blocks to drop + one-line recovery stubs) at the compaction seam (task 1349)
414
414
  ├── eviction-policy.js ← pure deterministic in-session compaction classifier (transcript turns → eviction set + replacement stubs); no I/O, unit-tested in tests/eviction_policy.mjs; consumed by pre-compact.js (task 1348)
415
- ├── stop-sound.js ← Stop: completion sound on macOS (afplay; Glass / Hero on /tmp/otb-celebrate); silent no-op on other OSes
415
+ ├── stop-sound.js ← ⚠ ABSENT from this repo (see the note below). Superseded anyway: the turn-end sound is now `scripts/gds/turn-end-sound.js`, wired as the Stop hook in `.claude/settings.json` and cross-platform rather than macOS-only (task 1003272)
416
416
  └── stop-refcheck.js ← Stop: deterministic backstop that catches a bare `#NNN` Bongos ref in live session text (autolinks to a 404 GitHub issue) and asks for the clickable form (follow-on to task 929)
417
417
  ```
418
418
 
419
+ > **⚠ THIS TREE IS STALE: 12 of the 13 files listed above are NOT in this repo** — only `secret-gate.js` exists on disk, and `.claude/settings.json` wires just `SessionStart` (three `scripts/gds/*` probes + the sound-prefs mirror), `PreToolUse` (secret-gate) and `Stop` (`scripts/gds/turn-end-sound.js`). The hook tree went the same way as `.claude/sounds/` — absent from every commit, lost when core was re-seeded to 1.18.0 (task 1003272 found and fixed the sound half of that). Do not assume a hook named here runs; check `.claude/settings.json`. Auditing and either restoring or deleting the other twelve is its own task, not a doc edit.
420
+
419
421
  (Before [#671](https://example.com/builders#/task/671) these ran via `bash` shims — `sync-env-local.sh`, `conductor.sh`, `onboarding-ship-logs.sh`, `rank-change-greeting.sh`, `pull-memory.sh` — which failed on Windows without Git Bash. The Node entries above replaced them.)
420
422
 
423
+ **Notification sounds** at `.claude/sounds/` + `modules/hall-ui/public/*.wav` (task 1003272 — the local half of the 5-event sound system, which had never been committed):
424
+
425
+ ```
426
+ .claude/sounds/
427
+ └── play.js ← the local player. playSound(<wavName>) — fire-and-forget, never throws, spawns the platform player detached (afplay / paplay / aplay / ffplay / PowerShell SoundPlayer); scales the WAV itself for the master volume because aplay and SoundPlayer have no volume flag. Reads the prefs mirror at instance-config.configPath('sound-prefs.json') with the pre-rename ~/.config/otb/ as fallback. Required (guarded) by scripts/gds/ship-state.js; runnable directly: `node .claude/sounds/play.js ship`
428
+ ```
429
+
430
+ The five base assets live ONCE, in `modules/hall-ui/public/` — served at `/builders/<name>.wav` for the Settings previews and read from the same directory by the player:
431
+ `chime.wav` (pre-existing) plus `alert.wav`, `ship.wav`, `triumph.wav`, `fanfare.wav`, all generated by [`scripts/gds/gen-sounds.js`](../scripts/gds/gen-sounds.js) (`--check` verifies freshness; it never touches `chime.wav`).
432
+
421
433
  **On the droplet (`REDACTED_IP`):**
422
434
 
423
435
  ```
@@ -0,0 +1,100 @@
1
+ -- core_226_work_categories.sql — a human CATEGORY for the work, that gates
2
+ -- nothing (task 1003275, goal 1000072, criterion legibility-categories).
3
+ --
4
+ -- WHAT THIS IS FOR. The project is legible to Claude and illegible to a person.
5
+ -- A human scanning 38 open goals cannot tell at a glance which are about money,
6
+ -- which are about the walls, and which are about the hall — the goal's own words
7
+ -- carry that, but only if you read all of them. A category is one word that
8
+ -- answers it, and the owner named the set: Builders Hall, Context Management,
9
+ -- Government (Decision Making), Modules, Security, Economics, Legal, and
10
+ -- User/ABI Experience.
11
+ --
12
+ -- THE CONSTRAINT THAT MATTERS MORE THAN THE FEATURE — THIS IS ADVISORY ONLY.
13
+ -- The owner said it twice: this organisation "has no hierarchical purpose" and
14
+ -- these categories have "no effect on the creation and agentic management of
15
+ -- these elements". The system ALREADY has a real hierarchy (Project → Version →
16
+ -- Goal → Criteria → Task, ADR 0086) with real enforcement — the scope_modules
17
+ -- wall, per-task rank floors, goal membership. A second thing that LOOKS like a
18
+ -- hierarchy will be mistaken for one within a release, and then something will
19
+ -- be refused because of a label. So: no claim gate, no route guard, no rank
20
+ -- check, no claimable-feed filter and no dependency rule may read a category.
21
+ -- That is enforced in code by the fitness check
22
+ -- `scripts/gds/category-advisory-guard.js`, not by this comment (ADR 0192).
23
+ --
24
+ -- WHY A TABLE AND NOT AN ENUM OR A text COLUMN. The criterion requires that when
25
+ -- Claude cannot fit a goal into the set, "the human is prompted to create a new
26
+ -- category — that failure is a designed prompt, not an error". A category set
27
+ -- that a person extends at runtime cannot be a CHECK constraint or a pg enum
28
+ -- without a migration per addition, and it must not be a free-text column or the
29
+ -- set is not a set (two spellings of "Security" and the label has stopped
30
+ -- orienting anyone).
31
+ --
32
+ -- WHY A TASK HAS NO COLUMN HERE. The criterion says "every goal (and task, BY
33
+ -- INHERITANCE)". A task's category is its goal's, read through tasks.goal_id —
34
+ -- storing a copy creates two answers to one question and no way to know which is
35
+ -- right. Inheritance is literal: there is nothing to keep in sync.
36
+ --
37
+ -- WHY CORE AND NOT modules/lifecycle/migrations/. `goals` was created by
38
+ -- migrations/160_goal_tier.sql and is a CORE table; the lifecycle module owns the
39
+ -- QUERIES against it, not the table (modules/lifecycle/CLAUDE.md: "None owned").
40
+ -- Fitness check 11 refuses an `ALTER TABLE goals` from a module migration however
41
+ -- lifecycle-ish it feels (ADR 0083 §Decision #5) — the same trap core_224 hit.
42
+ --
43
+ -- Additive, replay-safe, forward-safe: nullable with no default (NULL = never
44
+ -- categorised, which is a legal state and a prompt, never a rejection), and every
45
+ -- existing reader SELECTs an explicit column list so old code on the new schema
46
+ -- neither sees nor needs it (tests/migrations_forward_safe.mjs).
47
+
48
+ BEGIN;
49
+
50
+ -- 1. The set itself. `slug` is the stable machine key (a client filter, a skill's
51
+ -- inference); `label` is what a person reads and may be re-worded without
52
+ -- breaking anything that referenced it. created_by NULL = shipped with the
53
+ -- project (the eight below); a real id = a human created it at the moment
54
+ -- Claude could not place a goal. A migration must never hard-code a builder
55
+ -- id, so the seeded rows are system-attributed (learning 1238).
56
+ -- `active` retires a category without deleting it — a goal already carrying it
57
+ -- keeps reading correctly, which a DELETE would break.
58
+ CREATE TABLE IF NOT EXISTS work_categories (
59
+ id bigserial PRIMARY KEY,
60
+ slug text NOT NULL UNIQUE,
61
+ label text NOT NULL,
62
+ sort_order integer NOT NULL DEFAULT 0,
63
+ active boolean NOT NULL DEFAULT true,
64
+ created_by bigint REFERENCES builders(id), -- NULL = shipped with the project
65
+ created_at timestamptz NOT NULL DEFAULT now(),
66
+ updated_at timestamptz NOT NULL DEFAULT now()
67
+ );
68
+
69
+ COMMENT ON TABLE work_categories IS
70
+ 'The human category set a project owns (task 1003275). ADVISORY ONLY: no gate, route guard, rank check, feed filter or dependency rule may read it — enforced by scripts/gds/category-advisory-guard.js. The real hierarchy is ADR 0086.';
71
+
72
+ -- 2. The owner's default set. ON CONFLICT DO NOTHING so a replay is a no-op and
73
+ -- a project that has re-worded a label does not have it reverted.
74
+ INSERT INTO work_categories (slug, label, sort_order) VALUES
75
+ ('builders-hall', 'Builders Hall', 10),
76
+ ('context-management', 'Context Management', 20),
77
+ ('government', 'Government (Decision Making)', 30),
78
+ ('modules', 'Modules', 40),
79
+ ('security', 'Security', 50),
80
+ ('economics', 'Economics', 60),
81
+ ('legal', 'Legal', 70),
82
+ ('user-experience', 'User/ABI Experience', 80)
83
+ ON CONFLICT (slug) DO NOTHING;
84
+
85
+ -- 3. The goal's label. ON DELETE SET NULL rather than RESTRICT: retiring a
86
+ -- category is `active = false`, but if one is ever genuinely deleted an
87
+ -- advisory label must not be able to block it — a goal falling back to
88
+ -- uncategorised is exactly the designed prompt.
89
+ ALTER TABLE goals
90
+ ADD COLUMN IF NOT EXISTS category_id bigint REFERENCES work_categories(id) ON DELETE SET NULL;
91
+
92
+ COMMENT ON COLUMN goals.category_id IS
93
+ 'The goal''s human category (task 1003275). ADVISORY ONLY — orients a person and steers Claude, authorises nothing. NULL = not yet categorised, a legal state that prompts rather than refuses. A task inherits this through tasks.goal_id and stores no copy.';
94
+
95
+ CREATE INDEX IF NOT EXISTS idx_goals_category ON goals (category_id);
96
+
97
+ COMMIT;
98
+
99
+ INSERT INTO schema_migrations (version) VALUES ('core_226_work_categories')
100
+ ON CONFLICT (version) DO NOTHING;
@@ -2,12 +2,22 @@
2
2
  // src/bongos/event-sound-prefs.js — per-builder per-event sound variant selection
3
3
  // (task 582). Extends the mute-toggle system (sound-prefs.js) with variant
4
4
  // picking: for each of 5 build events a builder can choose which world-themed
5
- // sound variant plays. Each variant maps to an EXISTING .wav in .claude/sounds/;
6
- // the dedicated world-themed audio assets (lyre-pluck.wav etc.) ship separately.
5
+ // sound variant plays. Each variant maps to an EXISTING base .wav; the dedicated
6
+ // per-variant audio (lyre-pluck.wav etc.) would ship separately.
7
+ //
8
+ // WHERE THE BASE WAVS ACTUALLY LIVE (corrected, task 1003272): modules/hall-ui/
9
+ // public/, served at /builders/<name>.wav and read from that same directory by
10
+ // .claude/sounds/play.js. This comment used to say .claude/sounds/, which was
11
+ // true of no commit in this repo's history — for months FOUR of the five base
12
+ // wavs did not exist anywhere, so every variant below except the two backed by
13
+ // `chime` resolved to a 404 and played nothing. All five exist now, and
14
+ // tests/sound_player.mjs asserts that every `wav` named here has a real file.
7
15
 
8
16
  // 5 build events, ordered for the UI. Each has a world-themed default variant
9
- // plus alternates. `wav` is the existing .claude/sounds/<name>.wav that backs
10
- // the variant until dedicated audio ships.
17
+ // plus alternates. `wav` is the existing base sound that backs the variant until
18
+ // dedicated audio ships. Each event's two variants deliberately name DIFFERENT
19
+ // base wavs, so switching the picker audibly changes something — a picker whose
20
+ // options all sounded identical would be a worse lie than no picker.
11
21
  const EVENTS = Object.freeze([
12
22
  {
13
23
  id: 'user-input',
@@ -322,6 +322,8 @@ function createClient(opts = {}) {
322
322
  patchGoalsId: (args) => request("PATCH", "/goals/{id}", { hasBody: true }, args),
323
323
  // POST /goals/{id}/archive — rank: metic+archon — POST /goals/:id/archive
324
324
  postGoalsIdArchive: (args) => request("POST", "/goals/{id}/archive", { hasBody: true }, args),
325
+ // PATCH /goals/{id}/category — rank: any-builder — PATCH /goals/:id/category
326
+ patchGoalsIdCategory: (args) => request("PATCH", "/goals/{id}/category", { hasBody: true }, args),
325
327
  // GET /goals/{id}/conflicts — rank: any-builder — GET /goals/:id/conflicts
326
328
  getGoalsIdConflicts: (args) => request("GET", "/goals/{id}/conflicts", { hasBody: false }, args),
327
329
  // POST /goals/{id}/conflicts/resolve — rank: any-builder — POST /goals/:id/conflicts/resolve
@@ -815,6 +817,12 @@ function createClient(opts = {}) {
815
817
  // GET /versions/progress — rank: public — GET /versions/progress
816
818
  getVersionsProgress: (args) => request("GET", "/versions/progress", { hasBody: false }, args),
817
819
  },
820
+ "workCategories": {
821
+ // GET /work-categories — rank: any-builder — GET /work-categories
822
+ getWorkCategories: (args) => request("GET", "/work-categories", { hasBody: false }, args),
823
+ // POST /work-categories — rank: metic+archon — POST /work-categories
824
+ postWorkCategories: (args) => request("POST", "/work-categories", { hasBody: true }, args),
825
+ },
818
826
  };
819
827
  }
820
828
 
Binary file
Binary file
@@ -32,7 +32,7 @@ const { deriveRequiredRank, highestRank } = require('./db-rank-authz.js');
32
32
  // Shared column list so every goal read/return shapes a row identically.
33
33
 
34
34
  const GOAL_COLS =
35
- 'id, version_id, title, subtitle, description, scope_modules, status, visibility, created_by, succeeded_by_goal_id, sort_order, created_at, updated_at';
35
+ 'id, version_id, title, subtitle, description, scope_modules, status, visibility, created_by, succeeded_by_goal_id, sort_order, category_id, created_at, updated_at';
36
36
 
37
37
  // Shared column list for a goal_membership_requests row (task 1951 / ADR 0112) —
38
38
  // the invite / join-request queue behind private goals. Every helper below shapes
@@ -48,7 +48,7 @@ const MEMBERSHIP_REQUEST_COLS =
48
48
  // goals) inserts no member — those goals have no owner; Archons act as one.
49
49
 
50
50
  async function createGoal(
51
- { versionId, title, subtitle = null, description = null, scopeModules = [], status = 'open', createdBy = null },
51
+ { versionId, title, subtitle = null, description = null, scopeModules = [], status = 'open', createdBy = null, categoryId = null },
52
52
  deps = {}
53
53
  ) {
54
54
  const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
@@ -56,15 +56,15 @@ async function createGoal(
56
56
  `WITH next_order AS (
57
57
  SELECT COALESCE(MAX(sort_order) + 1, 0) AS n FROM goals WHERE version_id = $1
58
58
  ), g AS (
59
- INSERT INTO goals (version_id, title, subtitle, description, scope_modules, status, created_by, sort_order)
60
- SELECT $1, $2, $7, $3, $4, $5, $6, next_order.n FROM next_order
59
+ INSERT INTO goals (version_id, title, subtitle, description, scope_modules, status, created_by, sort_order, category_id)
60
+ SELECT $1, $2, $7, $3, $4, $5, $6, next_order.n, $8 FROM next_order
61
61
  RETURNING ${GOAL_COLS}
62
62
  ), first_lead AS (
63
63
  INSERT INTO goal_members (goal_id, builder_id, membership_kind)
64
64
  SELECT g.id, $6::bigint, 'lead' FROM g WHERE $6::bigint IS NOT NULL
65
65
  )
66
66
  SELECT * FROM g`,
67
- [versionId, title, description, scopeModules, status, createdBy, subtitle]
67
+ [versionId, title, description, scopeModules, status, createdBy, subtitle, categoryId]
68
68
  );
69
69
  return rows[0];
70
70
  }