@bongos/core 1.19.322 → 1.19.324

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.
@@ -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.324",
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.324",
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.324",
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",
@@ -32,6 +32,7 @@ const fs = require('node:fs');
32
32
  const path = require('node:path');
33
33
  const { execFileSync } = require('node:child_process');
34
34
  const { apiCall, cliClient, arg } = require('./cli-lib');
35
+ const { resolveDefaultVersion } = require('./version-select.js');
35
36
  const grader = require('../../modules/grading/grader');
36
37
  const llmCache = require('../../src/bongos/llm-cache'); // System 5 (task 1360): cache identical audits over an unchanged tree
37
38
  const { branding } = require('../../src/branding');
@@ -183,7 +184,12 @@ async function main() {
183
184
  const args = process.argv.slice(2);
184
185
  const scope = arg('--scope', args);
185
186
  const paths = arg('--paths', args);
186
- const version = arg('--version', args) || 'GDS-V3';
187
+ // task 1003166 (G02): was 'GDS-V3', long since shipped.
188
+ const version = arg('--version', args) || await resolveDefaultVersion();
189
+ if (!version) {
190
+ console.error('architect-audit: no version is building; pass --version <id>.');
191
+ process.exit(1);
192
+ }
187
193
  const dryRun = args.includes('--dry-run');
188
194
 
189
195
  if (!scope || !paths) {
@@ -21,6 +21,7 @@
21
21
 
22
22
  const { touchesOverlap } = require('../../src/bongos/path-match.js');
23
23
  const { cliClient } = require('./cli-lib');
24
+ const { resolveDefaultVersion } = require('./version-select.js');
24
25
  // task 1003153 — the DEAD-edge detector core, shared with the claim refusal so
25
26
  // "abandoned dependency" means one thing across the CLI, the route and this audit.
26
27
  const { findStrandedTasks } = require('../../modules/lifecycle/dead-deps.js');
@@ -128,7 +129,16 @@ async function main() {
128
129
  process.exitCode = await reportStranded({ asJson: argv.includes('--json') });
129
130
  return;
130
131
  }
131
- const ver = argv.includes('--version') ? argv[argv.indexOf('--version') + 1] : 'GDS-V3';
132
+ // task 1003166 (G02): the default was 'GDS-V3', which has shipped — so a bare
133
+ // `audit-deps` audited a closed version and reported nothing useful.
134
+ const ver = argv.includes('--version')
135
+ ? argv[argv.indexOf('--version') + 1]
136
+ : await resolveDefaultVersion();
137
+ if (!ver) {
138
+ console.error('audit-deps: no version is building; pass --version <id>.');
139
+ process.exitCode = 1;
140
+ return;
141
+ }
132
142
  const minRank = argv.includes('--min') ? argv[argv.indexOf('--min') + 1] : 'low';
133
143
  const asJson = argv.includes('--json');
134
144
  const order = { high: 3, medium: 2, low: 1 };
@@ -54,22 +54,10 @@ const readline = require('node:readline');
54
54
  const { cliClient, requireSession, arg, hasFlag } = require('./cli-lib');
55
55
 
56
56
 
57
- async function resolveDefaultVersion() {
58
- const api = await cliClient();
59
- const r = await api.versions.getVersions();
60
- if (!r.ok) return null;
61
- const versions = r.data?.versions ?? [];
62
- // Most recent building version. Fall back to any building version if
63
- // started_at is null on all of them.
64
- const building = versions.filter((v) => v.status === 'building');
65
- if (building.length === 0) return null;
66
- building.sort((a, b) => {
67
- const ta = a.started_at ? new Date(a.started_at).getTime() : 0;
68
- const tb = b.started_at ? new Date(b.started_at).getTime() : 0;
69
- return tb - ta;
70
- });
71
- return building[0].id;
72
- }
57
+ // task 1003166 (G02): the selection rule moved to version-select.js so the nine
58
+ // scripts that used to hardcode a version id share ONE implementation. Kept as a
59
+ // local name so every call site below reads unchanged.
60
+ const { resolveDefaultVersion } = require('./version-select.js');
73
61
 
74
62
  // Full JSON, not console.error's depth-2 inspect — a validation 400's nested
75
63
  // details otherwise print as "[ [Object] ]" and hide which field failed.
@@ -1910,7 +1910,7 @@ const CHECKS = [
1910
1910
  checkWriteRoutesValidated, // Check 18 — BV1 R12 / task 1999: every body-reading write route validates (ADR 0118)
1911
1911
  require('./fitness-ratchets.js').checkQualityRatchets, // Check 20 — task 1003129: quality budgets only tighten (mechanics + knip CI step live in fitness-ratchets.js)
1912
1912
  require('./doc-cli-guard.js').checkDocNamesRealCli, // Check 22 — task 1003165 / G01: a doc that names a CLI invocation must match the real CLI
1913
- require('./skill-preflight.js').checkOpsSkillsDeclareMachine, // Check 23 — task 1003167 / G03: an ops skill declares the machine it needs
1913
+ require('./skill-preflight.js').checkOpsSkillsDeclareMachine, require('./version-literal-guard.js').checkNoVersionLiterals, // Checks 23 (task 1003167/G03: a skill declares its machine) + 24 (task 1003166/G02: no script hardcodes a version id) — one line because fitness.js is 500 over the excess-lines ratchet and a new check must not cost a line (idea 1000810)
1914
1914
  require('./control-manifest.js').checkControlsExist, // Check 21 — task 1003131: goal-1000076 controls assert their own existence (the ADR 0022 assurance-of-assurance)
1915
1915
  ];
1916
1916
 
@@ -19,6 +19,7 @@
19
19
  // Pure helper (no I/O) — resolves the per-rank wandering default so the
20
20
  // off-task signal below only fires for the CONSTRAINING ranks (task 1268).
21
21
  const wanderingPrefs = require('../../modules/builder-settings/wandering-prefs'); // carved → modules/builder-settings/ (BV1.R80 / ADR 0093 §1)
22
+ const { resolveDefaultVersion } = require('./version-select.js');
22
23
 
23
24
  // ---- classification vocabularies -----------------------------------------
24
25
 
@@ -547,6 +548,9 @@ if (require.main === module) {
547
548
  : []);
548
549
  let filed = 0;
549
550
  let skipped = 0;
551
+ // task 1003166 (G02): resolved ONCE — it is invariant for the run, and this
552
+ // is a network round trip. The first version of this called it per finding.
553
+ const suggestedVersion = await resolveDefaultVersion();
550
554
  for (const s of systemic) {
551
555
  const tmpl = FINDING_TEMPLATES[s.type] || { title: s.type, body: '' };
552
556
  const title = `${PREFIX} ${tmpl.title}`.slice(0, 200);
@@ -561,7 +565,8 @@ if (require.main === module) {
561
565
  body: {
562
566
  title,
563
567
  body_md: bodyMd,
564
- suggested_version: 'GDS-V4',
568
+ // task 1003166 (G02): was 'GDS-V4', which has shipped.
569
+ suggested_version: suggestedVersion,
565
570
  },
566
571
  }).catch(() => null);
567
572
  if (r && r.ok) filed++;
@@ -20,6 +20,10 @@ const { autoLinkifyChangedDocs, mergeMainIntoBranch, preflight } = require('./sh
20
20
  const { applyBaseOverride, computeChangedFiles, computeGraderBaseline, runSmokeAsync } = require('./ship-grade.js');
21
21
  const { buildPriorRoundContext, computeGradeResult, gradeAndRecord, printCreditsAndAchievements, printGradeOutcome } = require('./ship-grade-steps.js');
22
22
  const { apiErrorLine, normalizeApiError } = require('./ship-io.js');
23
+ // task 1003205's carve left pushVia() used at regradeMain but never imported, so
24
+ // EVERY resume of a 'completed' task died with ReferenceError before resolving.
25
+ const { pushVia } = require('./ship-deploy-target.js');
26
+
23
27
  const { diffstatFor, finishShip } = require('./ship-finish.js');
24
28
  const { evaluateRegradeEligibility, evaluateResumeAction, resumeMerge } = require('./ship-resume.js');
25
29
  const { noteServerReward } = require('./ship-state.js');
@@ -24,6 +24,14 @@ source "$(dirname "$0")/lib.sh"
24
24
  API="$(gds_api_base)"
25
25
  TOKEN="$(gds_resolve_token)" || exit $?
26
26
 
27
+ # task 1003166 (G02): resolve the version at RUNTIME rather than naming one. A
28
+ # literal works until that version ships, then `version_closed` refuses every
29
+ # create and the smoke fails as if the code broke (idea 1000823).
30
+ VERSION_ID=$(node "$(dirname "$0")/version-select.js") || {
31
+ echo "FAIL: could not resolve a building version — see version-select.js" >&2
32
+ exit 1
33
+ }
34
+
27
35
  ts=$(date +%Y%m%d-%H%M%S)
28
36
 
29
37
  # Wrapper: METHOD PATH [BODY] → echoes "HTTP_CODE\nPAYLOAD". Never exits.
@@ -49,7 +57,7 @@ mk_task() {
49
57
  import json,sys
50
58
  t = '${touches_csv}'.split(',')
51
59
  print(json.dumps({
52
- 'version_id': 'BONGOS-V1',
60
+ 'version_id': '${VERSION_ID}',
53
61
  'title': '__smoke__ batch-claim ${ts}-${suffix}',
54
62
  'description': 'smoke',
55
63
  'status': 'ready',
@@ -31,6 +31,14 @@ source "$(dirname "$0")/lib.sh"
31
31
  API="$(gds_api_base)"
32
32
  TOKEN="$(gds_resolve_token)" || exit $?
33
33
 
34
+ # task 1003166 (G02): resolve the version at RUNTIME rather than naming one. A
35
+ # literal works until that version ships, then `version_closed` refuses every
36
+ # create and the smoke fails as if the code broke (idea 1000823).
37
+ VERSION_ID=$(node "$(dirname "$0")/version-select.js") || {
38
+ echo "FAIL: could not resolve a building version — see version-select.js" >&2
39
+ exit 1
40
+ }
41
+
34
42
  ts=$(date +%Y%m%d-%H%M%S)
35
43
 
36
44
  # Wrapper: METHOD PATH [BODY] → echoes "PAYLOAD\nHTTP_CODE". Never exits.
@@ -55,7 +63,7 @@ mk_task() {
55
63
  import json,sys
56
64
  t = '${touches_csv}'.split(',')
57
65
  print(json.dumps({
58
- 'version_id': 'BONGOS-V1',
66
+ 'version_id': '${VERSION_ID}',
59
67
  'title': '__smoke__ batch-release ${ts}-${suffix}',
60
68
  'description': 'smoke',
61
69
  'status': 'ready',
@@ -86,7 +86,7 @@ create_task() {
86
86
  body=$(python3 -c "
87
87
  import json
88
88
  print(json.dumps({
89
- 'version_id': 'PMS-V2',
89
+ 'version_id': '${VERSION_ID}',
90
90
  'title': '${title}',
91
91
  'description': 'Synthetic smoke task. Auto-cleaned by smoke-claim-consistency.sh.',
92
92
  'status': 'ready',
@@ -124,6 +124,15 @@ echo "API: ${API}"
124
124
  echo "Titles: ${SMOKE_TITLE_A} | ${SMOKE_TITLE_B}"
125
125
  echo
126
126
 
127
+ # task 1003166 (G02): resolve the version at RUNTIME. This file used to hardcode
128
+ # one; that version shipped, `version_closed` refused every create, and the smoke
129
+ # could not pass while /merge-mode still advertised it green (idea 1000823).
130
+ VERSION_ID=$(node "$(dirname "$0")/version-select.js") || {
131
+ echo "FAIL: could not resolve a building version — see version-select.js" >&2
132
+ exit 1
133
+ }
134
+ echo "Version: ${VERSION_ID}"
135
+
127
136
  echo "--- 1. create 2 smoke tasks ---"
128
137
  TASK_A=$(create_task "${SMOKE_TITLE_A}" "A")
129
138
  TASK_B=$(create_task "${SMOKE_TITLE_B}" "B")
@@ -58,6 +58,15 @@ echo "API: ${API}"
58
58
  echo "Title: ${SMOKE_TITLE}"
59
59
  echo
60
60
 
61
+ # task 1003166 (G02): resolve the version at RUNTIME. This file used to hardcode
62
+ # one; that version shipped, `version_closed` refused every create, and the smoke
63
+ # could not pass while /merge-mode still advertised it green (idea 1000823).
64
+ VERSION_ID=$(node "$(dirname "$0")/version-select.js") || {
65
+ echo "FAIL: could not resolve a building version — see version-select.js" >&2
66
+ exit 1
67
+ }
68
+ echo "Version: ${VERSION_ID}"
69
+
61
70
  echo "--- 0. snapshot progress before ---"
62
71
  PROGRESS_BEFORE=$(snapshot_progress)
63
72
  echo "${PROGRESS_BEFORE}"
@@ -67,7 +76,7 @@ echo "--- 1. create task ---"
67
76
  CREATE_BODY=$(python3 -c "
68
77
  import json
69
78
  print(json.dumps({
70
- 'version_id': 'PMS-V1',
79
+ 'version_id': '${VERSION_ID}',
71
80
  'title': '${SMOKE_TITLE}',
72
81
  'description': 'Synthetic smoke task. Auto-shipped by smoke-write.sh.',
73
82
  'status': 'ready',
@@ -38,6 +38,7 @@
38
38
  const readline = require('node:readline');
39
39
  const fs = require('node:fs');
40
40
  const { cliClient, requireSession } = require('./cli-lib');
41
+ const { resolveDefaultVersion } = require('./version-select.js');
41
42
 
42
43
  function prompt(rl, question) {
43
44
  return new Promise((resolve) => rl.question(question, (a) => resolve(a)));
@@ -384,7 +385,9 @@ async function main() {
384
385
 
385
386
  if (ans === 'p' || ans === 'promote') {
386
387
  // Promote: version, priority, title, goal — each defaulted from the idea.
387
- const defaultVersion = idea.suggested_version || 'PMS-V2';
388
+ // task 1003166 (G02): the fallback was 'PMS-V2', which has shipped —
389
+ // promoting into it would have been refused as version_closed.
390
+ const defaultVersion = idea.suggested_version || await resolveDefaultVersion();
388
391
  const versionAns = (await prompt(rl, ` version [${defaultVersion}]: `)).trim();
389
392
  const versionId = versionAns || defaultVersion;
390
393