@bongos/core 1.19.322 → 1.19.323

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.
package/.bongos-core.json CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.322",
6
- "core_contract": "1.19.322",
7
- "source_commit": "2fd56f3db9adacbb215ffdc56bba734266ef8e89",
5
+ "core_version": "1.19.323",
6
+ "core_contract": "1.19.323",
7
+ "source_commit": "6b1fb52bfb9b8d899ffc44c0b8a22fd08cab9379",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-24T01:23:28.434Z",
9
+ "built_at": "2026-08-24T01:33:26.263Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 324,
13
13
  "agent_docs_stubbed": 22,
14
- "functional_verbatim": 1455,
14
+ "functional_verbatim": 1457,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1801,
20
- "tree_sha256": "6cd259fb90abf1a7df74cc5e7dffead0cfa9c5d0f636032c51b94e715d21b31d",
19
+ "file_count": 1803,
20
+ "tree_sha256": "6534c0e9c341ea8d670e2f26984693052bc41041c70f99e671d5abd7ca3448dc",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -367,7 +367,7 @@
367
367
  {
368
368
  "path": "clients/bongos-client/index.d.ts",
369
369
  "mode": "0000644",
370
- "sha256": "85041b30fb2113ca5b3684ae961e67dc2bd5dfdb152064b151af3f0a93a29f31"
370
+ "sha256": "356ef6ee04671774868423f5312cedd2ae194f1f4abcdcaaac01828b044c1c14"
371
371
  },
372
372
  {
373
373
  "path": "clients/bongos-client/index.mjs",
@@ -1422,12 +1422,12 @@
1422
1422
  {
1423
1423
  "path": "docs/api-reference.md",
1424
1424
  "mode": "0000644",
1425
- "sha256": "a654d9def4780d3210c66031193198f58fd0d7d84f5088c4ce7e6d0a58682193"
1425
+ "sha256": "25a1978be80e817a5a9b54609aaa52dc0e172c941110183b9cfc93d193430f0b"
1426
1426
  },
1427
1427
  {
1428
1428
  "path": "docs/api/openapi.json",
1429
1429
  "mode": "0000644",
1430
- "sha256": "e0b531f19187a3c00a54917bff4e2827a75b250f1aaa761229660a974a4f6217"
1430
+ "sha256": "1cbc9af7fe828c5c81e6348a1a219b53ab484f9df67ec9ccb41e6f6cdef1693e"
1431
1431
  },
1432
1432
  {
1433
1433
  "path": "docs/architecture.md",
@@ -1452,12 +1452,12 @@
1452
1452
  {
1453
1453
  "path": "docs/copy-inventory.md",
1454
1454
  "mode": "0000644",
1455
- "sha256": "265f46593fe663dc38aa7980b0eacde8e6ab001b75301c5d0e38f1c1887d0e11"
1455
+ "sha256": "9b667d64d98c00a5a0d5991f3f69a783d82f8368719f50610d7514fff4d8ebe7"
1456
1456
  },
1457
1457
  {
1458
1458
  "path": "docs/copy-registry.json",
1459
1459
  "mode": "0000644",
1460
- "sha256": "e2e2af87ad6c8023e6b7399caac2372f08a87f41239873e83dbff39ac2fe1855"
1460
+ "sha256": "039628f57cb1bbda153b4450053cb9ca10e9eb7bad9c950854250139ac810c4a"
1461
1461
  },
1462
1462
  {
1463
1463
  "path": "docs/design-contract.md",
@@ -2459,6 +2459,11 @@
2459
2459
  "mode": "0000644",
2460
2460
  "sha256": "8fc38c0353811699e19d861321d4fca8fe8fd5cca1f66ff96312d7f44fb0290e"
2461
2461
  },
2462
+ {
2463
+ "path": "migrations/core_224_goal_subtitle.sql",
2464
+ "mode": "0000644",
2465
+ "sha256": "a379e9f926274559d8c57c9127024463e38b99e3f73962022b9f5d0f4e156a57"
2466
+ },
2462
2467
  {
2463
2468
  "path": "modules/autonomy/CLAUDE.md",
2464
2469
  "mode": "0000644",
@@ -3417,22 +3422,22 @@
3417
3422
  {
3418
3423
  "path": "modules/hall-ui/public/goals-lib.js",
3419
3424
  "mode": "0000644",
3420
- "sha256": "b899d1245f1138bf0e82115b80664eaa68ed2601c940bb0848ce2b784866d3e3"
3425
+ "sha256": "a7a0385c3c8026e6b21ac5793bdf5b48fa31968f176ebe891c43ad5c867df1c8"
3421
3426
  },
3422
3427
  {
3423
3428
  "path": "modules/hall-ui/public/goals-page.js",
3424
3429
  "mode": "0000644",
3425
- "sha256": "472483a2a5c8d5a2d033e630de2c24025dcbc7d7fc40017f64ea0d18f830e3a0"
3430
+ "sha256": "969aee56c6ec869a04d6e01752b24109114bd10e061c4a16fb8c7fa050671f47"
3426
3431
  },
3427
3432
  {
3428
3433
  "path": "modules/hall-ui/public/goals.css",
3429
3434
  "mode": "0000644",
3430
- "sha256": "ef69c360f46f508b1e987d8e9e523ed8708b7c74f48b6069103fae14ccc710f9"
3435
+ "sha256": "4ebae2d1b12a4a6a196e510dc034e2534ca0429e516292babdcb04917188c004"
3431
3436
  },
3432
3437
  {
3433
3438
  "path": "modules/hall-ui/public/goals.html",
3434
3439
  "mode": "0000644",
3435
- "sha256": "a9a6a23f60abba6072114db729691714819fefd316d42441462653e44eeab27b"
3440
+ "sha256": "f3656a4952d581c902c91c267a576193a91e60617b45745c879157e9cc8e702e"
3436
3441
  },
3437
3442
  {
3438
3443
  "path": "modules/hall-ui/public/goals.js",
@@ -3852,7 +3857,7 @@
3852
3857
  {
3853
3858
  "path": "modules/lifecycle/db-goals.js",
3854
3859
  "mode": "0000644",
3855
- "sha256": "29b950000d31618de168c6128d9b78c03daad7e68b9a9b6d3e83aa40abd3d9fb"
3860
+ "sha256": "b9d1768f8e271ad5c3595728f2433c27f44c841ab1c6be7d24086d1f6c8c7691"
3856
3861
  },
3857
3862
  {
3858
3863
  "path": "modules/lifecycle/db-grade.js",
@@ -3932,7 +3937,7 @@
3932
3937
  {
3933
3938
  "path": "modules/lifecycle/goal-edits.js",
3934
3939
  "mode": "0000644",
3935
- "sha256": "0367f88aefa890d8d7f2472dd847467d632150a93db8a96b85717b5fb1cc8c9e"
3940
+ "sha256": "7115a27272fe2594b65e2e56a796f5bf97600cb94ba301bc21599c6bc4e60175"
3936
3941
  },
3937
3942
  {
3938
3943
  "path": "modules/lifecycle/goal-suggest.js",
@@ -4007,7 +4012,7 @@
4007
4012
  {
4008
4013
  "path": "modules/lifecycle/routes/goals.js",
4009
4014
  "mode": "0000644",
4010
- "sha256": "3fcc0339927bcc7f426e93c7a5762b45496ecf4e9b841f905fb5f1c468b5f8c0"
4015
+ "sha256": "a58f01647120ce0b1143314f5edab2501bc18b8a748073dac788c92aaa0484ca"
4011
4016
  },
4012
4017
  {
4013
4018
  "path": "modules/lifecycle/routes/lifecycle.js",
@@ -4647,12 +4652,12 @@
4647
4652
  {
4648
4653
  "path": "package-lock.json",
4649
4654
  "mode": "0000644",
4650
- "sha256": "c9390686abc47c1954f212e7e4ebbce0320cfd21d9acc1fd2fe79d4f9667ab79"
4655
+ "sha256": "670e665f41fbb4d1c4154b2e2a08dcb64383583a8162188101dd0807ae849caa"
4651
4656
  },
4652
4657
  {
4653
4658
  "path": "package.json",
4654
4659
  "mode": "0000644",
4655
- "sha256": "290a425b20d5c5f87bc9a1eaeb154d02f8ca95e8147dd7a6f5835ea443d25ec4"
4660
+ "sha256": "27ab832e2bac7f7baac4016571d5536ac884b4b0f125a4b6f6a2dbae9b73112f"
4656
4661
  },
4657
4662
  {
4658
4663
  "path": "public-docs/index.html",
@@ -6422,7 +6427,7 @@
6422
6427
  {
6423
6428
  "path": "src/module-api.js",
6424
6429
  "mode": "0000644",
6425
- "sha256": "d808c8f826309876d6ce3bf99e47ca874b5afa0efe108fcbda629328c45d9b00"
6430
+ "sha256": "8624156a5587eaca3430325ef66420fb96b9567ed19af27dce361dc8af08611f"
6426
6431
  },
6427
6432
  {
6428
6433
  "path": "src/module-loader/catalog.js",
@@ -7404,6 +7409,11 @@
7404
7409
  "mode": "0000644",
7405
7410
  "sha256": "447e6376ae5f5e2bedf1a80b3355604544a5e349da87d110fc0135cc3cd98fac"
7406
7411
  },
7412
+ {
7413
+ "path": "tests/goal_subtitle.mjs",
7414
+ "mode": "0000644",
7415
+ "sha256": "4505dbeaafba7e7ca716d693864bea3246b4fe8767386bf0c7f3d48c41030db1"
7416
+ },
7407
7417
  {
7408
7418
  "path": "tests/goal_suggest.mjs",
7409
7419
  "mode": "0000644",
@@ -7412,7 +7422,7 @@
7412
7422
  {
7413
7423
  "path": "tests/goal_tier.mjs",
7414
7424
  "mode": "0000644",
7415
- "sha256": "00539cd1b188def53c67aab06301971132fbbe0bcd47818c19763ed760aaccda"
7425
+ "sha256": "e6a3114339989123d20f42f120f9fed52690fbc0230b10a99fa1fd0453acbfd2"
7416
7426
  },
7417
7427
  {
7418
7428
  "path": "tests/goal_wall_proof.mjs",
@@ -238,7 +238,7 @@ export interface PostGoalsIdTransferRequest { builder_id?: StringifiableId }
238
238
  export interface PostGoalsIdTransferResponse { ok: boolean; goal: unknown; changed: boolean }
239
239
  export interface PostGoalsReorderRequest { version_id: string; ordered_ids: number[] }
240
240
  export interface PostGoalsReorderResponse { ok: boolean; goals: unknown }
241
- export interface PostGoalsRequest { version_id: string; title: string; description?: string; scope_modules?: string[]; succeeds_goal_id?: number }
241
+ export interface PostGoalsRequest { version_id: string; title: string; subtitle?: string; description?: string; scope_modules?: string[]; succeeds_goal_id?: number }
242
242
  export interface PostGoalsResponse { ok: boolean; goal: unknown; succeeds_goal_id: unknown }
243
243
  export interface PostGovernmentAssignmentsRequest { builder_id: number; rank_key: string }
244
244
  export interface PostGovernmentBoardItemsItemIdVotesRequest { direction: string; reason_md?: string; section_key?: string }
@@ -4316,7 +4316,7 @@
4316
4316
  "goals"
4317
4317
  ],
4318
4318
  "summary": "PATCH /goals/:id",
4319
- "description": "PATCH /goals/:id — correct a goal's own title/description (task 1003102). Handler + rationale + rank note: ../goal-edits.js. Pinned in route-rank-check.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
4319
+ "description": "PATCH /goals/:id — correct a goal's own title/subtitle/description (tasks 1003102, 1003224). Handler + rationale + rank note: ../goal-edits.js. Pinned in route-rank-check.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
4320
4320
  "x-rank": "any-builder",
4321
4321
  "x-source": "modules/lifecycle/routes/goals.js",
4322
4322
  "parameters": [
@@ -16983,6 +16983,10 @@
16983
16983
  "minLength": 1,
16984
16984
  "maxLength": 200
16985
16985
  },
16986
+ "subtitle": {
16987
+ "type": "string",
16988
+ "maxLength": 200
16989
+ },
16986
16990
  "description": {
16987
16991
  "type": "string",
16988
16992
  "maxLength": 50000
@@ -202,9 +202,9 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
202
202
  | Method | Path | Rank | Body | Description |
203
203
  |---|---|---|---|---|
204
204
  | GET | `/api/bongos/goals` | `any-builder` | — | GET /goals?version=&status= — list goals (read). |
205
- | POST | `/api/bongos/goals` | `metic+archon` | `version_id`, `title`, `description`, `scope_modules`, `succeeds_goal_id` | POST /goals — create a goal. |
205
+ | POST | `/api/bongos/goals` | `metic+archon` | `version_id`, `title`, `subtitle`, `description`, `scope_modules`, `succeeds_goal_id` | POST /goals — create a goal. |
206
206
  | GET | `/api/bongos/goals/:id` | `any-builder` | — | GET /goals/:id — one goal with its members + linked criteria (read). |
207
- | PATCH | `/api/bongos/goals/:id` | `any-builder` | — | PATCH /goals/:id — correct a goal's own title/description (task 1003102). |
207
+ | PATCH | `/api/bongos/goals/:id` | `any-builder` | — | PATCH /goals/:id — correct a goal's own title/subtitle/description (tasks 1003102, 1003224). |
208
208
  | POST | `/api/bongos/goals/:id/archive` | `metic+archon` | — | POST /goals/:id/archive — retire a goal (open\|achieved → archived). |
209
209
  | GET | `/api/bongos/goals/:id/conflicts` | `any-builder` | — | GET /goals/:id/conflicts — the server-side task-conflict view (task 1736). |
210
210
  | 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… |
@@ -4,13 +4,13 @@
4
4
  > Deterministic on a given tree — regenerate and commit rather than hand-editing.
5
5
  > Machine-readable twin: [`copy-registry.json`](copy-registry.json).
6
6
 
7
- **1379 strings** across 3 surfaces · 1224 distinct pieces of copy · 1379 certain, 0 heuristic · 125 shown in more than one place.
7
+ **1380 strings** across 3 surfaces · 1225 distinct pieces of copy · 1380 certain, 0 heuristic · 125 shown in more than one place.
8
8
 
9
9
  ## Surfaces
10
10
 
11
11
  | surface | lives at | files | strings |
12
12
  |---|---|---|---|
13
- | `builders-hall` | `modules/hall-ui/public` | 65 | 1095 |
13
+ | `builders-hall` | `modules/hall-ui/public` | 65 | 1096 |
14
14
  | `status-dashboard` | `modules/status-ui/public` | 4 | 98 |
15
15
  | `landing` | `public-landing` | 2 | 186 |
16
16
 
@@ -52,7 +52,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
52
52
  | `df38941b25a6` | 3x | Sign in with GitHub | public-landing/projects.html:525, public-landing/projects.html:583, public-landing/projects.html:692 |
53
53
  | `b2b399fdd6a4` | 3x | in progress | modules/hall-ui/public/goals-page.js:423, modules/hall-ui/public/roadmap.js:108, modules/hall-ui/public/roadmap.js:194 |
54
54
  | `cc5106af4b35` | 3x | {…} of {…} criteria satisfied | modules/hall-ui/public/goals-lib.js:30, modules/hall-ui/public/goals-page.js:1016, modules/hall-ui/public/goals.js:45 |
55
- | `7926e30955d2` | 3x | ← Back to goal | modules/hall-ui/public/goals.html:152, modules/hall-ui/public/goals.html:163, modules/hall-ui/public/goals.html:175 |
55
+ | `7926e30955d2` | 3x | ← Back to goal | modules/hall-ui/public/goals.html:160, modules/hall-ui/public/goals.html:171, modules/hall-ui/public/goals.html:183 |
56
56
  | `a8813c34d20d` | 2x | &ldquo;Host denied&rdquo; | modules/hall-ui/public/box-panel.js:160, modules/hall-ui/public/box-panel.js:340 |
57
57
  | `66cea7cefa4e` | 2x | '; for (const row of bodyRows) { html += ' | modules/hall-ui/public/md-reader.js:188, modules/hall-ui/public/primer.js:278 |
58
58
  | `87fa099f5068` | 2x | '; } html += ' | modules/hall-ui/public/md-reader.js:192, modules/hall-ui/public/primer.js:282 |
@@ -64,8 +64,8 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
64
64
  | `490b73a0c6e8` | 2x | Approve your Dev Box app — {{worldName}} | modules/hall-ui/public/pair.html:6, modules/hall-ui/public/pair.html:6 |
65
65
  | `b71a26e6fb37` | 2x | Archons only | modules/hall-ui/public/sessions.html:28, modules/hall-ui/public/watch.html:31 |
66
66
  | `08afb1257b71` | 2x | Back to home | modules/hall-ui/public/primer.html:68, modules/hall-ui/public/primer.js:516 |
67
- | `563d29ed86d8` | 2x | Bulk actions | modules/hall-ui/public/goals.html:140, modules/hall-ui/public/work.html:118 |
68
- | `46b33550d52b` | 2x | Bulk claim | modules/hall-ui/public/goals.html:143, modules/hall-ui/public/work.html:121 |
67
+ | `563d29ed86d8` | 2x | Bulk actions | modules/hall-ui/public/goals.html:148, modules/hall-ui/public/work.html:118 |
68
+ | `46b33550d52b` | 2x | Bulk claim | modules/hall-ui/public/goals.html:151, modules/hall-ui/public/work.html:121 |
69
69
  | `c10806b50c61` | 2x | Claim task {…} | modules/hall-ui/public/goals-page.js:551, modules/hall-ui/public/work.js:356 |
70
70
  | `a832f32b5597` | 2x | Claimed {…} tasks. Open a terminal to start work. | modules/hall-ui/public/goals-page.js:647, modules/hall-ui/public/work.js:754 |
71
71
  | `cfe0f4c4fcc2` | 2x | Closing your box… (a few seconds) | modules/hall-ui/public/box-panel.js:265, modules/hall-ui/public/box-panel.js:370 |
@@ -78,7 +78,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
78
78
  | `7775fec0fa85` | 2x | Could not load sessions. | modules/hall-ui/public/sessions.js:168, modules/hall-ui/public/sessions.js:207 |
79
79
  | … | | _85 more in the JSON registry_ | |
80
80
 
81
- ## `builders-hall` — 1095 strings
81
+ ## `builders-hall` — 1096 strings
82
82
 
83
83
  | id | conf | text | where |
84
84
  |---|---|---|---|
@@ -184,9 +184,9 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
184
184
  | `a78477a24a48` | certain | Builders' Hall | `modules/hall-ui/public/index.html:12` |
185
185
  | `a78477a24a48` | certain | Builders' Hall | `modules/hall-ui/public/index.html:38` |
186
186
  | `b51edc826cbe` | certain | Built by this project | `modules/hall-ui/public/modules.html:47` |
187
- | `563d29ed86d8` | certain | Bulk actions | `modules/hall-ui/public/goals.html:140` |
187
+ | `563d29ed86d8` | certain | Bulk actions | `modules/hall-ui/public/goals.html:148` |
188
188
  | `563d29ed86d8` | certain | Bulk actions | `modules/hall-ui/public/work.html:118` |
189
- | `46b33550d52b` | certain | Bulk claim | `modules/hall-ui/public/goals.html:143` |
189
+ | `46b33550d52b` | certain | Bulk claim | `modules/hall-ui/public/goals.html:151` |
190
190
  | `46b33550d52b` | certain | Bulk claim | `modules/hall-ui/public/work.html:121` |
191
191
  | `6d6d626fc518` | certain | Bulk release | `modules/hall-ui/public/work.html:122` |
192
192
  | `181cfa50b127` | certain | By the numbers | `modules/hall-ui/public/profile.html:37` |
@@ -215,6 +215,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
215
215
  | `b9c64fe023a9` | certain | Claude Desktop app | `modules/hall-ui/public/builders.js:984` |
216
216
  | `9edebf179aa3` | certain | Claude Online | `modules/hall-ui/public/builders.js:984` |
217
217
  | `f2c734539bb8` | certain | Claude starts automatically when you open the terminal. | `modules/hall-ui/public/builders.js:984` |
218
+ | `5b3b75ee0596` | certain | Claude's notes on this goal | `modules/hall-ui/public/goals.html:67` |
218
219
  | `593dda3178eb` | certain | Click to copy | `modules/hall-ui/public/copy-on-click.js:137` |
219
220
  | `87789c3dc97c` | certain | Click to copy {…} | `modules/hall-ui/public/box-panel.js:137` |
220
221
  | `e712dd69a5f6` | certain | Clone the project to your machine, or open your hosted dev box (where it is already cloned). The build command | `modules/hall-ui/public/builders.js:1154` |
@@ -381,7 +382,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
381
382
  | `c15d1f0ddfb6` | certain | Estimation accuracy | `modules/hall-ui/public/watch.html:133` |
382
383
  | `44f928d13699` | certain | Event sounds | `modules/hall-ui/public/settings.html:282` |
383
384
  | `8690c6424585` | certain | Every builder in {{worldName}} — rank, {{currency}}, karma, and active claims. | `modules/hall-ui/public/watch.html:61` |
384
- | `2ae1eb35611e` | certain | Every done-when criterion for this goal, with its full detail. | `modules/hall-ui/public/goals.html:178` |
385
+ | `2ae1eb35611e` | certain | Every done-when criterion for this goal, with its full detail. | `modules/hall-ui/public/goals.html:186` |
385
386
  | `1e05b7fe42c8` | certain | Every project you build on across the platform. | `modules/hall-ui/public/my-projects.html:24` |
386
387
  | `efafe5ce29ed` | certain | Every task linked to this criterion has shipped — a Metic can confirm it | `modules/hall-ui/public/goals-page.js:1042` |
387
388
  | `ddebbccd4b37` | certain | Everyone building across hosted projects, ranked by track record. Descriptive only — cross-project numbers nev | `modules/hall-ui/public/people.html:23` |
@@ -419,7 +420,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
419
420
  | `13acaeb4b0bf` | certain | Goal #{…} | `modules/hall-ui/public/task.js:202` |
420
421
  | `9e70797b7e97` | certain | Goal id | `modules/hall-ui/public/goals-page.js:1213` |
421
422
  | `529460e82058` | certain | Goal invitations to you | `modules/hall-ui/public/goal-inbox.js:113` |
422
- | `e2a9e33abe60` | certain | Goal tasks | `modules/hall-ui/public/goals.html:104` |
423
+ | `e2a9e33abe60` | certain | Goal tasks | `modules/hall-ui/public/goals.html:112` |
423
424
  | `e0bc28d10785` | certain | Goals could not be loaded. Refresh to try again. | `modules/hall-ui/public/goals-page.js:1799` |
424
425
  | `4517f2253d0f` | certain | Goals couldn’t be loaded just now. Refresh to try again. | `modules/hall-ui/public/goals.js:112` |
425
426
  | `2ed2cb8fbffd` | certain | Goals page | `modules/hall-ui/public/goals.js:94` |
@@ -616,8 +617,8 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
616
617
  | `855277dff074` | certain | Open task #{…} | `modules/hall-ui/public/work.js:963` |
617
618
  | `e0810149de64` | certain | Open task details | `modules/hall-ui/public/goals-page.js:559` |
618
619
  | `e0810149de64` | certain | Open task details | `modules/hall-ui/public/work.js:341` |
619
- | `99c630009152` | certain | Open tasks whose declared files overlap — sequence these before two builders collide. | `modules/hall-ui/public/goals.html:90` |
620
- | `99c630009152` | certain | Open tasks whose declared files overlap — sequence these before two builders collide. | `modules/hall-ui/public/goals.html:155` |
620
+ | `99c630009152` | certain | Open tasks whose declared files overlap — sequence these before two builders collide. | `modules/hall-ui/public/goals.html:98` |
621
+ | `99c630009152` | certain | Open tasks whose declared files overlap — sequence these before two builders collide. | `modules/hall-ui/public/goals.html:163` |
621
622
  | `29ff278e96a8` | certain | Open the Work Board | `modules/hall-ui/public/task.js:258` |
622
623
  | `e706a4836ce8` | certain | Open the pairing page | `modules/hall-ui/public/settings.html:201` |
623
624
  | `2cad724171e6` | certain | Open your online environment. | `modules/hall-ui/public/builders.js:984` |
@@ -907,7 +908,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
907
908
  | `d3a8b2b5b93f` | certain | Where {{currency}} and karma come from and where they go. | `modules/hall-ui/public/diagrams.html:76` |
908
909
  | `8f5954c06daa` | certain | Whether your session cards — what | `modules/hall-ui/public/settings.html:229` |
909
910
  | `f9ab9eb870a6` | certain | While you wait: install the Claude desktop app at | `modules/hall-ui/public/builders.js:640` |
910
- | `045c629729b7` | certain | Who’s on this goal, and what they’ve contributed to it. | `modules/hall-ui/public/goals.html:166` |
911
+ | `045c629729b7` | certain | Who’s on this goal, and what they’ve contributed to it. | `modules/hall-ui/public/goals.html:174` |
911
912
  | `6e8a3151ec76` | certain | Why \u2014 the reason the author will act on | `modules/hall-ui/public/government.js:741` |
912
913
  | `a3ac116b499a` | certain | Why? | `modules/hall-ui/public/copy-desk.html:99` |
913
914
  | `6108c16edece` | certain | Window: last {…} days. | `modules/hall-ui/public/watch.js:628` |
@@ -1160,9 +1161,9 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1160
1161
  | `7d29dc9e0391` | certain | ← Back to Home | `modules/hall-ui/public/not-ready.html:35` |
1161
1162
  | `9e7f83400f9a` | certain | ← Back to Ideas | `modules/hall-ui/public/idea.html:27` |
1162
1163
  | `a71cd6fa8d57` | certain | ← Back to Tasks | `modules/hall-ui/public/task.html:55` |
1163
- | `7926e30955d2` | certain | ← Back to goal | `modules/hall-ui/public/goals.html:152` |
1164
- | `7926e30955d2` | certain | ← Back to goal | `modules/hall-ui/public/goals.html:163` |
1165
- | `7926e30955d2` | certain | ← Back to goal | `modules/hall-ui/public/goals.html:175` |
1164
+ | `7926e30955d2` | certain | ← Back to goal | `modules/hall-ui/public/goals.html:160` |
1165
+ | `7926e30955d2` | certain | ← Back to goal | `modules/hall-ui/public/goals.html:171` |
1166
+ | `7926e30955d2` | certain | ← Back to goal | `modules/hall-ui/public/goals.html:183` |
1166
1167
  | `1e7cf7b1e64e` | certain | ← Newer | `modules/hall-ui/public/sessions.html:56` |
1167
1168
  | `2180e0ecf138` | certain | → no idea # | `modules/hall-ui/public/task-detail.js:479` |
1168
1169
  | `d5d76f1c0dfe` | certain | ○ reserved | `modules/hall-ui/public/diagrams.html:53` |
@@ -3,9 +3,9 @@
3
3
  "generator": "scripts/gds/copy-inventory.js",
4
4
  "goal": 1000074,
5
5
  "counts": {
6
- "strings": 1379,
7
- "unique_ids": 1224,
8
- "certain": 1379,
6
+ "strings": 1380,
7
+ "unique_ids": 1225,
8
+ "certain": 1380,
9
9
  "heuristic": 0,
10
10
  "duplicate_ids": 125
11
11
  },
@@ -13,7 +13,7 @@
13
13
  "builders-hall": {
14
14
  "at": "modules/hall-ui/public",
15
15
  "files": 65,
16
- "strings": 1095
16
+ "strings": 1096
17
17
  },
18
18
  "status-dashboard": {
19
19
  "at": "modules/status-ui/public",
@@ -233,9 +233,9 @@
233
233
  "text": "← Back to goal",
234
234
  "occurrences": 3,
235
235
  "at": [
236
- "modules/hall-ui/public/goals.html:152",
237
- "modules/hall-ui/public/goals.html:163",
238
- "modules/hall-ui/public/goals.html:175"
236
+ "modules/hall-ui/public/goals.html:160",
237
+ "modules/hall-ui/public/goals.html:171",
238
+ "modules/hall-ui/public/goals.html:183"
239
239
  ]
240
240
  },
241
241
  {
@@ -354,7 +354,7 @@
354
354
  "text": "Bulk actions",
355
355
  "occurrences": 2,
356
356
  "at": [
357
- "modules/hall-ui/public/goals.html:140",
357
+ "modules/hall-ui/public/goals.html:148",
358
358
  "modules/hall-ui/public/work.html:118"
359
359
  ]
360
360
  },
@@ -364,7 +364,7 @@
364
364
  "text": "Bulk claim",
365
365
  "occurrences": 2,
366
366
  "at": [
367
- "modules/hall-ui/public/goals.html:143",
367
+ "modules/hall-ui/public/goals.html:151",
368
368
  "modules/hall-ui/public/work.html:121"
369
369
  ]
370
370
  },
@@ -834,8 +834,8 @@
834
834
  "text": "Open tasks whose declared files overlap — sequence these before two builders collide.",
835
835
  "occurrences": 2,
836
836
  "at": [
837
- "modules/hall-ui/public/goals.html:90",
838
- "modules/hall-ui/public/goals.html:155"
837
+ "modules/hall-ui/public/goals.html:98",
838
+ "modules/hall-ui/public/goals.html:163"
839
839
  ]
840
840
  },
841
841
  {
@@ -2243,7 +2243,7 @@
2243
2243
  "surface": "builders-hall",
2244
2244
  "text": "Bulk actions",
2245
2245
  "file": "modules/hall-ui/public/goals.html",
2246
- "line": 140,
2246
+ "line": 148,
2247
2247
  "origin": "html-attr:aria-label",
2248
2248
  "confidence": "certain"
2249
2249
  },
@@ -2261,7 +2261,7 @@
2261
2261
  "surface": "builders-hall",
2262
2262
  "text": "Bulk claim",
2263
2263
  "file": "modules/hall-ui/public/goals.html",
2264
- "line": 143,
2264
+ "line": 151,
2265
2265
  "origin": "html-text",
2266
2266
  "confidence": "certain"
2267
2267
  },
@@ -2517,6 +2517,15 @@
2517
2517
  "origin": "js-markup",
2518
2518
  "confidence": "certain"
2519
2519
  },
2520
+ {
2521
+ "id": "5b3b75ee0596",
2522
+ "surface": "builders-hall",
2523
+ "text": "Claude's notes on this goal",
2524
+ "file": "modules/hall-ui/public/goals.html",
2525
+ "line": 67,
2526
+ "origin": "html-text",
2527
+ "confidence": "certain"
2528
+ },
2520
2529
  {
2521
2530
  "id": "593dda3178eb",
2522
2531
  "surface": "builders-hall",
@@ -4016,7 +4025,7 @@
4016
4025
  "surface": "builders-hall",
4017
4026
  "text": "Every done-when criterion for this goal, with its full detail.",
4018
4027
  "file": "modules/hall-ui/public/goals.html",
4019
- "line": 178,
4028
+ "line": 186,
4020
4029
  "origin": "html-text",
4021
4030
  "confidence": "certain"
4022
4031
  },
@@ -4358,7 +4367,7 @@
4358
4367
  "surface": "builders-hall",
4359
4368
  "text": "Goal tasks",
4360
4369
  "file": "modules/hall-ui/public/goals.html",
4361
- "line": 104,
4370
+ "line": 112,
4362
4371
  "origin": "html-attr:aria-label",
4363
4372
  "confidence": "certain"
4364
4373
  },
@@ -6131,7 +6140,7 @@
6131
6140
  "surface": "builders-hall",
6132
6141
  "text": "Open tasks whose declared files overlap — sequence these before two builders collide.",
6133
6142
  "file": "modules/hall-ui/public/goals.html",
6134
- "line": 90,
6143
+ "line": 98,
6135
6144
  "origin": "html-text",
6136
6145
  "confidence": "certain"
6137
6146
  },
@@ -6140,7 +6149,7 @@
6140
6149
  "surface": "builders-hall",
6141
6150
  "text": "Open tasks whose declared files overlap — sequence these before two builders collide.",
6142
6151
  "file": "modules/hall-ui/public/goals.html",
6143
- "line": 155,
6152
+ "line": 163,
6144
6153
  "origin": "html-text",
6145
6154
  "confidence": "certain"
6146
6155
  },
@@ -8750,7 +8759,7 @@
8750
8759
  "surface": "builders-hall",
8751
8760
  "text": "Who’s on this goal, and what they’ve contributed to it.",
8752
8761
  "file": "modules/hall-ui/public/goals.html",
8753
- "line": 166,
8762
+ "line": 174,
8754
8763
  "origin": "html-text",
8755
8764
  "confidence": "certain"
8756
8765
  },
@@ -11027,7 +11036,7 @@
11027
11036
  "surface": "builders-hall",
11028
11037
  "text": "← Back to goal",
11029
11038
  "file": "modules/hall-ui/public/goals.html",
11030
- "line": 152,
11039
+ "line": 160,
11031
11040
  "origin": "html-text",
11032
11041
  "confidence": "certain"
11033
11042
  },
@@ -11036,7 +11045,7 @@
11036
11045
  "surface": "builders-hall",
11037
11046
  "text": "← Back to goal",
11038
11047
  "file": "modules/hall-ui/public/goals.html",
11039
- "line": 163,
11048
+ "line": 171,
11040
11049
  "origin": "html-text",
11041
11050
  "confidence": "certain"
11042
11051
  },
@@ -11045,7 +11054,7 @@
11045
11054
  "surface": "builders-hall",
11046
11055
  "text": "← Back to goal",
11047
11056
  "file": "modules/hall-ui/public/goals.html",
11048
- "line": 175,
11057
+ "line": 183,
11049
11058
  "origin": "html-text",
11050
11059
  "confidence": "certain"
11051
11060
  },
@@ -0,0 +1,35 @@
1
+ -- core_224_goal_subtitle.sql — a goal's explanation stops living inside its
2
+ -- name (task 1003224, goal 1000072, criterion legibility-goal-names-and-claude-text).
3
+ --
4
+ -- THE PROBLEM THIS FIXES IS IN THE DATA. 29 of 38 open goals carry their own
5
+ -- explanation appended to `title` after a dash — "Findability — the grep path",
6
+ -- "Idea routing — work that has a home lands in it". The goal board is the
7
+ -- primary work surface, so every list, card, pill and breadcrumb renders the
8
+ -- explanation at the same weight as the name, and nothing can show one without
9
+ -- the other. A name and a description are two fields; they were one.
10
+ --
11
+ -- ADDITIVE ONLY, and nullable with no default on purpose. Every existing row
12
+ -- keeps its title byte-for-byte: this migration creates somewhere for the
13
+ -- explanation to GO, and the separate renaming pass (task 1003225) moves it,
14
+ -- goal by goal, with judgment — three goals all head "Agents module" and would
15
+ -- become indistinguishable under a mechanical split. A DEFAULT '' would make
16
+ -- "never set" and "deliberately blank" the same state, and the UI needs to tell
17
+ -- them apart to know whether to render the affordance at all.
18
+ --
19
+ -- WHY THIS IS A CORE MIGRATION AND NOT A MODULE ONE. `goals` was created by
20
+ -- repo-root migrations/160_goal_tier.sql and is a CORE table — the lifecycle
21
+ -- module owns the queries against it, not the table itself (modules/lifecycle/
22
+ -- CLAUDE.md: "None owned"). Fitness check 11 enforces exactly that: a module
23
+ -- migration may only touch its own `<key>_`-prefixed tables, so an
24
+ -- `ALTER TABLE goals` from modules/lifecycle/migrations/ is refused however
25
+ -- lifecycle-ish it feels (ADR 0083 §Decision #5). The first draft of this file
26
+ -- lived there and the check caught it.
27
+
28
+ ALTER TABLE goals
29
+ ADD COLUMN IF NOT EXISTS subtitle text;
30
+
31
+ COMMENT ON COLUMN goals.subtitle IS
32
+ 'The goal''s explanation, held apart from its name. Rendered subordinate to title and on demand (task 1003224). NULL = never set, which is distinct from an empty string meaning deliberately blank.';
33
+
34
+ INSERT INTO schema_migrations (version) VALUES ('core_224_goal_subtitle')
35
+ ON CONFLICT (version) DO NOTHING;
@@ -90,10 +90,36 @@
90
90
  const msOf = (iso) => { const t = iso ? Date.parse(iso) : NaN; return Number.isNaN(t) ? 0 : t; };
91
91
  const taskActivityMs = (t) => Math.max(msOf(t.updated_at), msOf(t.shipped_at));
92
92
 
93
+ // task 1003224 — on a LIST, a goal's subtitle is a TOOLTIP, never a second
94
+ // visible line. The board's job there is to let someone find a goal by name;
95
+ // rendering every explanation inline is what led 29 of 38 titles to carry
96
+ // their own after a dash. Escaped because it lands inside an attribute.
97
+ function subtitleAttr(g) {
98
+ const t = (g && g.subtitle ? String(g.subtitle) : '').trim();
99
+ return t ? ` title="${window.OTB.escapeHtml(t)}"` : '';
100
+ }
101
+
102
+ // task 1003224 — the three-layer prose rules for a goal's detail head, kept
103
+ // here rather than in goals-page.js because that file is 500 lines over the
104
+ // excess-lines ratchet and this one exists for exactly that reason. It WRITES
105
+ // to elements the caller already looked up; it never reads the DOM or touches
106
+ // page state, so the file's purity contract holds.
107
+ // name -> the h2, rendered by the caller
108
+ // subtitle -> subordinate, and ABSENT when unset (NULL differs from blank)
109
+ // Claude's prose -> collapsed, and absent when there is nothing to reveal
110
+ function applyGoalProse(els, g) {
111
+ const subtitle = (g.subtitle || '').trim();
112
+ const description = g.description || '';
113
+ if (els.sub) { els.sub.textContent = subtitle; els.sub.hidden = !subtitle; }
114
+ if (els.desc) els.desc.textContent = description;
115
+ if (els.claude) { els.claude.hidden = !description.trim(); els.claude.open = false; }
116
+ }
117
+
93
118
  window.OTBGoalsLib = {
94
119
  DONE_STATUSES, CUT_STATUSES, isTodoStatus,
95
120
  ring, memberName,
96
121
  readinessOf, EMPTY_ROLLUP, goalRollLine,
97
122
  matchesTodoSearch, msOf, taskActivityMs,
123
+ subtitleAttr, applyGoalProse,
98
124
  };
99
125
  })();
@@ -40,7 +40,7 @@
40
40
  // so every call site below reads exactly as before.
41
41
  const { DONE_STATUSES, CUT_STATUSES, isTodoStatus, ring, memberName,
42
42
  readinessOf, EMPTY_ROLLUP, goalRollLine, matchesTodoSearch,
43
- msOf, taskActivityMs } = window.OTBGoalsLib;
43
+ msOf, taskActivityMs, subtitleAttr, applyGoalProse } = window.OTBGoalsLib;
44
44
  const API = '/api/bongos';
45
45
  // Generated typed client (task 1996 / R09), served as window.BongosClient by
46
46
  // /lib/bongos-client.js. baseUrl:'' passes the full same-origin paths this file
@@ -359,7 +359,7 @@
359
359
  return `<div class="gr-row" data-goal-id="${escapeHtml(String(g.id))}">
360
360
  <div class="gr-ringcol">${ringHtml}</div>
361
361
  <div class="gr-main">
362
- <a class="gr-title" href="${goalHref}">${escapeHtml(g.title || 'Untitled goal')}</a>
362
+ <a class="gr-title" href="${goalHref}"${subtitleAttr(g)}>${escapeHtml(g.title || 'Untitled goal')}</a>
363
363
  <div class="gr-meta">${metaHtml}</div>
364
364
  ${achieved ? '' : goalRollLine(r)}
365
365
  </div>
@@ -377,7 +377,7 @@
377
377
  return `<div class="goal-card" data-goal-id="${escapeHtml(String(g.id))}">
378
378
  <div class="goal-card__head">
379
379
  <div class="gr-ringcol">${ringHtml}</div>
380
- <a class="goal-card__title gr-title" href="${goalHref}">${escapeHtml(g.title || 'Untitled goal')}</a>
380
+ <a class="goal-card__title gr-title" href="${goalHref}"${subtitleAttr(g)}>${escapeHtml(g.title || 'Untitled goal')}</a>
381
381
  </div>
382
382
  <div class="goal-card__meta">${metaHtml}</div>
383
383
  ${achieved ? '' : goalRollLine(r)}
@@ -1211,8 +1211,8 @@
1211
1211
  // task 2163 — surface the numeric goal id alongside the title (escaped title,
1212
1212
  // so switching from textContent to innerHTML stays injection-safe).
1213
1213
  if (h) h.innerHTML = `<span class="goal-detail__id" title="Goal id">#${escapeHtml(String(g.id))}</span> ${escapeHtml(g.title || 'Untitled goal')}`;
1214
- const desc = $('#goal-detail-desc');
1215
- if (desc) desc.textContent = g.description || '';
1214
+ // task 1003224 — name / subtitle / Claude's prose at three weights; the rules live in goals-lib.
1215
+ applyGoalProse({ sub: $('#goal-detail-subtitle'), desc: $('#goal-detail-desc'), claude: $('#goal-detail-claude') }, g);
1216
1216
  const meta = $('#goal-detail-meta');
1217
1217
  if (meta) {
1218
1218
  const scope = Array.isArray(g.scope_modules) && g.scope_modules.length
@@ -259,3 +259,71 @@ a.goal-card:hover { text-decoration: none; border-color: var(--border-strong); b
259
259
  .gr-next { align-items: flex-start; }
260
260
  .gr-next__t { max-width: 100%; }
261
261
  }
262
+
263
+ /* ── task 1003224: name, subtitle, and Claude's text at three weights ────────
264
+ The goal board is the primary work surface, so what it shows by DEFAULT is
265
+ the decision this makes. The name reads first; the explanation is visibly
266
+ subordinate; the bulk prose Claude writes is behind a disclosure and absent
267
+ from the default reading path until someone asks for it.
268
+ Tokens only from style.css's real set (--ink-soft, --ink-faint, --rule,
269
+ --surface, --radius-sm) — inventing one renders as nothing, which is how
270
+ copy-desk.css ended up with invisible fallbacks (idea 1000821). */
271
+ .goal-detail__subtitle {
272
+ color: var(--ink-soft);
273
+ font-size: 13px;
274
+ font-weight: 400;
275
+ line-height: 1.45;
276
+ max-width: 72ch;
277
+ margin: 3px 0 0;
278
+ }
279
+
280
+ /* The disclosure is deliberately quiet: a row someone can ignore. */
281
+ .claude-text {
282
+ margin: 10px 0 0;
283
+ max-width: 72ch;
284
+ border-top: 1px solid var(--rule);
285
+ padding-top: 8px;
286
+ }
287
+
288
+ .claude-text__toggle {
289
+ color: var(--ink-faint);
290
+ font-size: 12px;
291
+ cursor: pointer;
292
+ list-style: none;
293
+ user-select: none;
294
+ display: inline-flex;
295
+ align-items: center;
296
+ gap: 6px;
297
+ }
298
+
299
+ /* Own marker, so the affordance reads the same in every browser. */
300
+ .claude-text__toggle::-webkit-details-marker { display: none; }
301
+ .claude-text__toggle::before {
302
+ content: '▸';
303
+ font-size: 10px;
304
+ transition: transform 120ms ease;
305
+ }
306
+ .claude-text[open] > .claude-text__toggle::before { transform: rotate(90deg); }
307
+ .claude-text__toggle:hover { color: var(--ink-soft); }
308
+ .claude-text__toggle:focus-visible {
309
+ outline: 2px solid var(--accent);
310
+ outline-offset: 2px;
311
+ border-radius: var(--radius-sm);
312
+ }
313
+
314
+ /* Set apart once opened, so it reads as a machine's notes rather than as the
315
+ goal's own definition. */
316
+ .claude-text__body {
317
+ margin: 8px 0 0;
318
+ padding: 8px 10px;
319
+ background: var(--surface);
320
+ border-radius: var(--radius-sm);
321
+ color: var(--ink-soft);
322
+ font-size: 13px;
323
+ line-height: 1.5;
324
+ white-space: pre-wrap;
325
+ }
326
+
327
+ @media (prefers-reduced-motion: reduce) {
328
+ .claude-text__toggle::before { transition: none; }
329
+ }
@@ -58,7 +58,15 @@
58
58
  <div class="goal-detail__head">
59
59
  <div style="flex:1;min-width:260px">
60
60
  <h2 class="goal-detail__title" id="goal-detail-h">…</h2>
61
- <p class="goal-detail__desc" id="goal-detail-desc"></p>
61
+ <!-- task 1003224: the goal's explanation, subordinate to its name. -->
62
+ <p class="goal-detail__subtitle" id="goal-detail-subtitle" hidden></p>
63
+ <!-- The bulk prose Claude writes. Collapsed by DEFAULT and marked as
64
+ Claude's text: the owner reads the name and the punched-in points,
65
+ and opens this only when they want the long form. -->
66
+ <details class="claude-text" id="goal-detail-claude" hidden>
67
+ <summary class="claude-text__toggle">Claude's notes on this goal</summary>
68
+ <p class="claude-text__body goal-detail__desc" id="goal-detail-desc"></p>
69
+ </details>
62
70
  <p class="goal-card__meta" id="goal-detail-meta" style="margin-top:10px"></p>
63
71
  </div>
64
72
  <div class="goal-detail__actions" id="goal-detail-actions"></div>
@@ -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, 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, 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, description = null, scopeModules = [], status = 'open', createdBy = null },
51
+ { versionId, title, subtitle = null, description = null, scopeModules = [], status = 'open', createdBy = 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, description, scope_modules, status, created_by, sort_order)
60
- SELECT $1, $2, $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)
60
+ SELECT $1, $2, $7, $3, $4, $5, $6, next_order.n 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]
67
+ [versionId, title, description, scopeModules, status, createdBy, subtitle]
68
68
  );
69
69
  return rows[0];
70
70
  }
@@ -91,24 +91,26 @@ function proseDelta(field, from, to) {
91
91
  async function updateGoalProse({ goalId, patch = {} }, deps = {}) {
92
92
  const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
93
93
  const { rows: before } = await activePool.query(
94
- `SELECT id, title, description FROM goals WHERE id = $1`,
94
+ `SELECT id, title, subtitle, description FROM goals WHERE id = $1`,
95
95
  [goalId]
96
96
  );
97
97
  if (before.length === 0) return null;
98
98
  const prev = before[0];
99
99
  const nextTitle = Object.prototype.hasOwnProperty.call(patch, 'title') ? patch.title : prev.title;
100
+ const nextSubtitle = Object.prototype.hasOwnProperty.call(patch, 'subtitle') ? patch.subtitle : prev.subtitle;
100
101
  const nextDesc = Object.prototype.hasOwnProperty.call(patch, 'description') ? patch.description : prev.description;
101
102
  const deltas = [
102
103
  proseDelta('title', prev.title, nextTitle),
104
+ proseDelta('subtitle', prev.subtitle, nextSubtitle),
103
105
  proseDelta('description', prev.description, nextDesc),
104
106
  ].filter(Boolean);
105
107
  if (deltas.length === 0) return { goal: prev, deltas: [], unchanged: true };
106
108
  const { rows } = await activePool.query(
107
109
  `UPDATE goals
108
- SET title = $2, description = $3, updated_at = now()
110
+ SET title = $2, description = $3, subtitle = $4, updated_at = now()
109
111
  WHERE id = $1
110
- RETURNING id, version_id, title, description, status, visibility, created_by, updated_at`,
111
- [goalId, nextTitle, nextDesc]
112
+ RETURNING id, version_id, title, subtitle, description, status, visibility, created_by, updated_at`,
113
+ [goalId, nextTitle, nextDesc, nextSubtitle]
112
114
  );
113
115
  return { goal: rows[0], deltas };
114
116
  }
@@ -219,13 +221,17 @@ function goalProseHandler({ db, authorizeGoalManagement, validateOrRespond, pars
219
221
  if (id === null) return;
220
222
  if (validateOrRespond(req, res, {
221
223
  title: { type: 'string', minLength: 1, maxLength: LIMITS.TITLE },
224
+ // No minLength: '' is a legal subtitle, meaning deliberately blank. The
225
+ // column's NULL (never set) is what the UI tells apart from that.
226
+ subtitle: { type: 'string', maxLength: LIMITS.TITLE },
222
227
  description: { type: 'string', maxLength: LIMITS.DESCRIPTION },
223
228
  })) return;
224
229
  const patch = {};
225
230
  if (Object.prototype.hasOwnProperty.call(req.body || {}, 'title')) patch.title = req.body.title;
231
+ if (Object.prototype.hasOwnProperty.call(req.body || {}, 'subtitle')) patch.subtitle = req.body.subtitle;
226
232
  if (Object.prototype.hasOwnProperty.call(req.body || {}, 'description')) patch.description = req.body.description;
227
233
  if (Object.keys(patch).length === 0) {
228
- return res.fail('nothing_to_update', 400, { editable: ['title', 'description'] });
234
+ return res.fail('nothing_to_update', 400, { editable: ['title', 'subtitle', 'description'] });
229
235
  }
230
236
  try {
231
237
  const goal = await db.getGoal(id);
@@ -438,7 +438,7 @@ module.exports = function buildGoalsRouter() {
438
438
  if (validateOrRespond(req, res, {
439
439
  version_id: { required: true, type: 'string', minLength: 1, maxLength: 64 },
440
440
  title: { required: true, type: 'string', minLength: 1, maxLength: LIMITS.TITLE },
441
- description: { type: 'string', maxLength: LIMITS.DESCRIPTION },
441
+ subtitle: { type: 'string', maxLength: LIMITS.TITLE }, description: { type: 'string', maxLength: LIMITS.DESCRIPTION },
442
442
  scope_modules: { type: 'array', maxItems: 64, itemsType: 'string' },
443
443
  succeeds_goal_id: { type: 'integer', min: 1 },
444
444
  })) return;
@@ -474,7 +474,7 @@ module.exports = function buildGoalsRouter() {
474
474
  const goal = await db.createGoal({
475
475
  versionId: req.body.version_id,
476
476
  title: req.body.title,
477
- description: (req.body.description || '').trim() || null,
477
+ subtitle: (req.body.subtitle || '').trim() || null, description: (req.body.description || '').trim() || null,
478
478
  scopeModules,
479
479
  createdBy: req.builder.id,
480
480
  });
@@ -640,7 +640,7 @@ module.exports = function buildGoalsRouter() {
640
640
  }
641
641
  });
642
642
 
643
- // PATCH /goals/:id — correct a goal's own title/description (task 1003102).
643
+ // PATCH /goals/:id — correct a goal's own title/subtitle/description (tasks 1003102, 1003224).
644
644
  // Handler + rationale + rank note: ../goal-edits.js. Pinned in route-rank-check.
645
645
  router.patch('/goals/:id', auth.requireBuilder, goalEdits.goalProseHandler({ db, authorizeGoalManagement, validateOrRespond, parseId, LIMITS }));
646
646
  // PATCH /goals/:id/visibility — make a goal public|private (task 1952 / ADR 0112).
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.322",
3
+ "version": "1.19.323",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.322",
9
+ "version": "1.19.323",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.322",
3
+ "version": "1.19.323",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
package/src/module-api.js CHANGED
@@ -1071,8 +1071,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
1071
1071
  // landed since 1.19.320 with no explicit bump. run 32679324013. (task 1002620)
1072
1072
  // 1.19.322 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1073
1073
  // landed since 1.19.321 with no explicit bump. run 32679604360. (task 1002620)
1074
+ // 1.19.323 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1075
+ // landed since 1.19.322 with no explicit bump. run 32680138776. (task 1002620)
1074
1076
  // ---------------------------------------------------------------------------
1075
- const CORE_VERSION = '1.19.322'; // CI auto-patch carrier (ADR 0161); see changelog above
1077
+ const CORE_VERSION = '1.19.323'; // CI auto-patch carrier (ADR 0161); see changelog above
1076
1078
 
1077
1079
  // A namespaced logger so a module's log lines are attributable + consistent.
1078
1080
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,237 @@
1
+ // tests/goal_subtitle.mjs — a goal's explanation stops living inside its name
2
+ // (task 1003224, goal 1000072, criterion legibility-goal-names-and-claude-text).
3
+ //
4
+ // Owner directive, three parts: the TITLE is a name; the SUBTITLE is a separate,
5
+ // subordinate, on-demand field; and the bulk prose Claude writes into the
6
+ // description is presented as Claude's text, absent from the default reading
7
+ // path until someone asks for it.
8
+ //
9
+ // These tests pin the parts a future edit could quietly undo: that subtitle is
10
+ // carried by the same prose-edit path as title (so it inherits the audit trail
11
+ // rather than getting a private one), that the disclosure is CLOSED by default,
12
+ // and that task creation did NOT gain a subtitle — the identical validator pair
13
+ // appears in POST /goals and POST /goals/:id/tasks, and only the first should
14
+ // have changed.
15
+ import { strict as assert } from 'node:assert';
16
+ import { test } from 'node:test';
17
+ import { createRequire } from 'node:module';
18
+ import fs from 'node:fs';
19
+ import path from 'node:path';
20
+ import { fileURLToPath } from 'node:url';
21
+
22
+ const require = createRequire(import.meta.url);
23
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
24
+ const goalEdits = require(path.join(ROOT, 'modules', 'lifecycle', 'goal-edits.js'));
25
+ const read = (...p) => fs.readFileSync(path.join(ROOT, ...p), 'utf8');
26
+
27
+ const fakeReqRes = ({ body }) => {
28
+ const res = { code: null, statusCode: null, payload: null,
29
+ fail(code, status, extra) { this.code = code; this.statusCode = typeof status === 'number' ? status : status?.status ?? 400; this.payload = { error: code, ...(extra || {}) }; return this; },
30
+ status(c) { this.statusCode = c; return this; },
31
+ json(p) { this.payload = p; return this; } };
32
+ return { req: { params: { id: '7' }, body, builder: { id: 1, rank: 'archon' } }, res };
33
+ };
34
+ const deps = {
35
+ db: { getGoal: async () => ({ id: 7, status: 'open', created_by: 1 }), getGoalMember: async () => null },
36
+ authorizeGoalManagement: () => ({ ok: true }),
37
+ validateOrRespond: () => false,
38
+ parseId: () => 7,
39
+ LIMITS: { TITLE: 200, DESCRIPTION: 50000 },
40
+ };
41
+
42
+ // ---- the field travels the existing prose path -----------------------------
43
+ test('a subtitle-only PATCH is accepted and recorded', () => {
44
+ // Not a private write path: subtitle rides title's handler so the prose-edit
45
+ // audit trail (deltas + record) covers it identically.
46
+ let patched = null; const recorded = [];
47
+ const handler = goalEdits.goalProseHandler({
48
+ ...deps,
49
+ update: async ({ patch }) => { patched = patch; return { goal: { id: 7, ...patch }, deltas: [goalEdits.proseDelta('subtitle', null, patch.subtitle)] }; },
50
+ record: async (...a) => { recorded.push(a); },
51
+ });
52
+ const { req, res } = fakeReqRes({ body: { subtitle: 'the grep path' } });
53
+ return handler(req, res).then(() => {
54
+ assert.equal(res.code, null, `must not fail — got ${res.code}`);
55
+ assert.deepEqual(patched, { subtitle: 'the grep path' }, 'only the named field is patched');
56
+ assert.equal(res.payload.deltas[0].field, 'subtitle');
57
+ assert.equal(recorded.length, 1, 'the edit is audit-recorded like any other prose edit');
58
+ });
59
+ });
60
+
61
+ test('an EMPTY subtitle is a legal value, not a no-op', () => {
62
+ // '' means deliberately blank; NULL means never set. The column has no
63
+ // default precisely so the UI can tell them apart, so '' must reach the patch.
64
+ let patched = null;
65
+ const handler = goalEdits.goalProseHandler({
66
+ ...deps,
67
+ update: async ({ patch }) => { patched = patch; return { goal: { id: 7 }, deltas: [] }; },
68
+ record: async () => {},
69
+ });
70
+ const { req, res } = fakeReqRes({ body: { subtitle: '' } });
71
+ return handler(req, res).then(() => {
72
+ assert.equal(res.code, null);
73
+ assert.ok(Object.prototype.hasOwnProperty.call(patched, 'subtitle'), 'empty string must still be a patch');
74
+ assert.equal(patched.subtitle, '');
75
+ });
76
+ });
77
+
78
+ test('nothing_to_update names subtitle as editable', () => {
79
+ const handler = goalEdits.goalProseHandler(deps);
80
+ const { req, res } = fakeReqRes({ body: { nope: 1 } });
81
+ return handler(req, res).then(() => {
82
+ assert.equal(res.code, 'nothing_to_update');
83
+ assert.deepEqual(res.payload.editable, ['title', 'subtitle', 'description']);
84
+ });
85
+ });
86
+
87
+ test('title and description still work — subtitle is additive', () => {
88
+ let patched = null;
89
+ const handler = goalEdits.goalProseHandler({
90
+ ...deps,
91
+ update: async ({ patch }) => { patched = patch; return { goal: { id: 7, ...patch }, deltas: [] }; },
92
+ record: async () => {},
93
+ });
94
+ const { req, res } = fakeReqRes({ body: { title: 'Findability', description: 'long prose' } });
95
+ return handler(req, res).then(() => {
96
+ assert.deepEqual(patched, { title: 'Findability', description: 'long prose' });
97
+ assert.ok(!Object.prototype.hasOwnProperty.call(patched, 'subtitle'), 'an unsent field is not patched to null');
98
+ });
99
+ });
100
+
101
+ // ---- the data layer --------------------------------------------------------
102
+ test('the migration is additive, core-numbered, and sets no default', () => {
103
+ const raw = read('migrations', 'core_224_goal_subtitle.sql');
104
+ assert.match(raw, /ADD COLUMN IF NOT EXISTS subtitle text/);
105
+ // Assert over the STATEMENTS, not the prose. The first version of this test
106
+ // matched /DEFAULT ''/ anywhere in the file and failed on the migration's own
107
+ // comment explaining why it does NOT set a default — the same over-broad-source
108
+ // -assertion mistake this session has made repeatedly (learning 1000196).
109
+ const ddl = raw.split('\n').filter((l) => !/^\s*--/.test(l)).join('\n');
110
+ assert.ok(!/\bDROP\b/i.test(ddl), 'no destructive DDL');
111
+ assert.ok(!/SET NOT NULL/i.test(ddl), 'stays nullable');
112
+ assert.ok(!/ADD COLUMN[^;]*DEFAULT/i.test(ddl), 'the added column takes no default');
113
+ assert.match(ddl, /schema_migrations/, 'records itself');
114
+ });
115
+
116
+ test('subtitle is in GOAL_COLS, so every read returns it', () => {
117
+ // One column list drives getGoal, the list query and createGoal's RETURNING —
118
+ // adding it anywhere else would be a second source that can drift.
119
+ const src = read('modules', 'lifecycle', 'db-goals.js');
120
+ const cols = /const GOAL_COLS\s*=\s*\n?\s*'([^']+)'/.exec(src);
121
+ assert.ok(cols, 'GOAL_COLS is still a single string literal');
122
+ assert.ok(cols[1].split(',').map((c) => c.trim()).includes('subtitle'), 'subtitle is selected');
123
+ });
124
+
125
+ test('POST /goals gained subtitle and POST /goals/:id/tasks did NOT', () => {
126
+ // The identical `title` + `description` validator pair appears in both routes.
127
+ // A blind replace-all would have given task creation a subtitle; this pins the
128
+ // boundary. (The first attempt at this edit tripped on exactly that ambiguity.)
129
+ const src = read('modules', 'lifecycle', 'routes', 'goals.js');
130
+ const goalRoute = src.slice(src.indexOf("router.post('/goals',"), src.indexOf("router.get('/goals',"));
131
+ assert.match(goalRoute, /subtitle: \{ type: 'string'/, 'the goal route takes a subtitle');
132
+ const taskRoute = src.slice(src.indexOf("router.post('/goals/:id/tasks',"));
133
+ const taskValidator = taskRoute.slice(0, taskRoute.indexOf('})) return;'));
134
+ assert.ok(!/subtitle/.test(taskValidator), 'task creation must NOT accept a subtitle');
135
+ });
136
+
137
+ // ---- the default reading path ---------------------------------------------
138
+ test("Claude's prose is a CLOSED disclosure, not visible body text", () => {
139
+ const html = read('modules', 'hall-ui', 'public', 'goals.html');
140
+ assert.match(html, /<details class="claude-text"[^>]*hidden/, 'a <details>, hidden until it has content');
141
+ assert.ok(!/<p class="goal-detail__desc" id="goal-detail-desc"><\/p>/.test(html),
142
+ 'the description is no longer a bare always-visible paragraph');
143
+ // the description element must now live INSIDE the disclosure
144
+ const det = html.slice(html.indexOf('<details class="claude-text"'));
145
+ const inner = det.slice(0, det.indexOf('</details>'));
146
+ assert.match(inner, /id="goal-detail-desc"/, 'the description renders inside the disclosure');
147
+ });
148
+
149
+ // applyGoalProse is pure enough to RUN, so these two assert behaviour rather
150
+ // than source text — a stronger pin, and the reason the rules were moved out of
151
+ // goals-page.js (which is 500 lines over the excess-lines ratchet) in the first
152
+ // place. Fake elements stand in for the DOM.
153
+ const el = () => ({ textContent: null, hidden: null, open: null });
154
+ function applyProse(goal) {
155
+ const lib = read('modules', 'hall-ui', 'public', 'goals-lib.js');
156
+ const els = { sub: el(), desc: el(), claude: el() };
157
+ const fn = new Function('window', `${lib}; return window.OTBGoalsLib.applyGoalProse;`)({ OTB: { escapeHtml: (x) => x } });
158
+ fn(els, goal);
159
+ return els;
160
+ }
161
+
162
+ test('the disclosure is CLOSED on render, never opened for the user', () => {
163
+ const els = applyProse({ description: 'Claude wrote a lot here', subtitle: 'sub' });
164
+ assert.equal(els.claude.open, false, 'closed');
165
+ assert.equal(els.claude.hidden, false, 'present, because there is prose to reveal');
166
+ assert.equal(els.desc.textContent, 'Claude wrote a lot here');
167
+ });
168
+
169
+ test('a goal with no description shows no empty disclosure', () => {
170
+ const els = applyProse({ description: ' ', subtitle: 'sub' });
171
+ assert.equal(els.claude.hidden, true, 'an empty disclosure invites a dead click');
172
+ });
173
+
174
+ test('an unset subtitle is hidden; a set one is shown trimmed', () => {
175
+ assert.equal(applyProse({}).sub.hidden, true, 'NULL subtitle renders nothing');
176
+ const els = applyProse({ subtitle: ' the grep path ' });
177
+ assert.equal(els.sub.hidden, false);
178
+ assert.equal(els.sub.textContent, 'the grep path');
179
+ });
180
+
181
+ test('on a LIST the subtitle is a tooltip, never a second visible line', () => {
182
+ const js = read('modules', 'hall-ui', 'public', 'goals-page.js');
183
+ // The helper lives in goals-lib.js — the file that exists because
184
+ // goals-page.js is over the excess-lines ratchet (task 1003135).
185
+ const lib = read('modules', 'hall-ui', 'public', 'goals-lib.js');
186
+ assert.match(lib, /function subtitleAttr/);
187
+ assert.match(lib, /escapeHtml\(t\)/, 'escaped, since it lands in an attribute');
188
+ for (const anchor of ['class="gr-title"', 'class="goal-card__title gr-title"']) {
189
+ const i = js.indexOf(anchor);
190
+ assert.ok(i > 0, `${anchor} still rendered`);
191
+ assert.match(js.slice(i, i + 160), /subtitleAttr\(g\)/, `${anchor} carries the tooltip`);
192
+ }
193
+ });
194
+
195
+ test('every CSS token the new styles use actually exists', () => {
196
+ // copy-desk.css shipped with --line and --panel, which style.css never
197
+ // defines, so its fallbacks rendered as nothing (idea 1000821).
198
+ const css = read('modules', 'hall-ui', 'public', 'goals.css');
199
+ const style = read('modules', 'hall-ui', 'public', 'style.css');
200
+ const block = css.slice(css.indexOf('task 1003224'));
201
+ const used = new Set([...block.matchAll(/var\((--[a-z-]+)\)/g)].map((m) => m[1]));
202
+ assert.ok(used.size >= 4, 'the new block does use tokens');
203
+ for (const t of used) {
204
+ assert.ok(new RegExp(`^\\s*${t}:`, 'm').test(style), `${t} must be defined in style.css`);
205
+ }
206
+ });
207
+
208
+ test('the subtitle style is subordinate to the title style', () => {
209
+ const css = read('modules', 'hall-ui', 'public', 'goals.css');
210
+ const size = (sel) => {
211
+ const i = css.indexOf(sel);
212
+ assert.ok(i > 0, `${sel} exists`);
213
+ const m = /font-size:\s*([\d.]+)px/.exec(css.slice(i, i + 300));
214
+ return m ? Number(m[1]) : null;
215
+ };
216
+ const title = size('.goal-detail__title');
217
+ const sub = size('.goal-detail__subtitle');
218
+ assert.ok(title && sub, 'both declare a size');
219
+ assert.ok(sub < title, `subtitle (${sub}px) must read smaller than the name (${title}px)`);
220
+ });
221
+
222
+ test("the PATCH route's own comment names every field it accepts", () => {
223
+ // The API reference is GENERATED from this comment, so the generator
224
+ // faithfully republishes a stale one: after subtitle was added the docs still
225
+ // advertised 'title/description', which the grader caught as a blocker. This
226
+ // pins comment and validator together so the next added field cannot drift.
227
+ const routes = read('modules', 'lifecycle', 'routes', 'goals.js');
228
+ const i = routes.indexOf("router.patch('/goals/:id',");
229
+ assert.ok(i > 0, 'the PATCH route still exists');
230
+ const comment = routes.slice(routes.lastIndexOf('// PATCH /goals/:id', i), i);
231
+ const edits = read('modules', 'lifecycle', 'goal-edits.js');
232
+ const validator = edits.slice(edits.indexOf('function goalProseHandler'));
233
+ for (const field of ['title', 'subtitle', 'description']) {
234
+ assert.match(validator, new RegExp(`${field}: \\{ `), `${field} is validated`);
235
+ assert.ok(comment.includes(field), `the route comment must name ${field} — the API reference is generated from it`);
236
+ }
237
+ });
@@ -65,8 +65,10 @@ await test('createGoal: INSERT binds all six fields in order and returns the row
65
65
  );
66
66
  assert.deepEqual(out, GOAL_ROW, 'returns the inserted row');
67
67
  const ins = find(pool.queries, /INSERT INTO goals/);
68
- assert.deepEqual(ins.params, ['BONGOS-V1', 'Goal hierarchy', 'desc', ['gds-core'], 'open', 42],
69
- 'version_id, title, description, scope_modules, status, created_by — in that order');
68
+ // task 1003224 appended `subtitle` as $7 rather than inserting it next to
69
+ // title, so $1–$6 keep the positions this test has always pinned.
70
+ assert.deepEqual(ins.params, ['BONGOS-V1', 'Goal hierarchy', 'desc', ['gds-core'], 'open', 42, null],
71
+ 'version_id, title, description, scope_modules, status, created_by, then subtitle — in that order');
70
72
  assert.match(ins.sql, /RETURNING/, 'returns the row, not just inserts');
71
73
  });
72
74
 
@@ -87,8 +89,8 @@ await test('createGoal: defaults — null description/createdBy, empty scope, op
87
89
  const pool = fakePool((s) => (/INSERT INTO goals/.test(s) ? { rows: [GOAL_ROW] } : null));
88
90
  await createGoal({ versionId: 'V1', title: 'general' }, { pool });
89
91
  const ins = find(pool.queries, /INSERT INTO goals/);
90
- assert.deepEqual(ins.params, ['V1', 'general', null, [], 'open', null],
91
- 'unspecified fields fall back to null / [] / open');
92
+ assert.deepEqual(ins.params, ['V1', 'general', null, [], 'open', null, null],
93
+ 'unspecified fields fall back to null / [] / open — subtitle ($7) defaults to null too');
92
94
  });
93
95
 
94
96
  // =========================================================================