@bongos/core 1.19.321 → 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.
@@ -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.321",
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.321",
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.321",
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
@@ -1069,8 +1069,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
1069
1069
  // landed since 1.19.319 with no explicit bump. run 32678147546. (task 1002620)
1070
1070
  // 1.19.321 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1071
1071
  // landed since 1.19.320 with no explicit bump. run 32679324013. (task 1002620)
1072
+ // 1.19.322 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
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)
1072
1076
  // ---------------------------------------------------------------------------
1073
- const CORE_VERSION = '1.19.321'; // 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
1074
1078
 
1075
1079
  // A namespaced logger so a module's log lines are attributable + consistent.
1076
1080
  // Usage: const log = api.logger('dev-box'); log.info('mounted');