@bongos/core 1.19.248 → 1.19.250

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,352 @@
1
+ // modules/hall-ui/public/copy-desk.js — the hall "Copy desk" tab
2
+ // (/builders/copy-desk). Task 1003113 / R02 of goal 1000074, the artist's loop.
3
+ //
4
+ // The surface half of the flag: R01 built the inventory of every string the
5
+ // product shows a person; the copy-desk module turned "this one is wrong" into a
6
+ // row; this page is where an artist reads those rows and where everyone else
7
+ // files them.
8
+ //
9
+ // FOUR PANELS, IN THE ORDER AN ARTIST WOULD WORK:
10
+ // 1. the counts — what is outstanding, and the fast pile
11
+ // 2. Flagged — the queue itself, newest first, open by default
12
+ // 3. Said more than once — the deterministic duplicate signal, minus what has
13
+ // already been flagged or dismissed
14
+ // 4. Find something to flag — search the whole inventory
15
+ //
16
+ // NO LIVE CMS. Nothing on this page edits a string. The composer collects a
17
+ // JUDGEMENT (kind + reason); the fix ships as a claimed, graded task. There is
18
+ // deliberately no "new text" field — see modules/copy-desk/tests/copy_no_cms.mjs.
19
+
20
+ (() => {
21
+ 'use strict';
22
+
23
+ const API = '/api/bongos';
24
+ const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
25
+ const { escapeHtml, $, toast, fmtInt } = window.OTB;
26
+
27
+ let viewer = null;
28
+ let queue = null;
29
+ // The composer's current target, set when a "Flag" button is pressed.
30
+ let target = null;
31
+ let closing = null;
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // rendering helpers
35
+ // ---------------------------------------------------------------------------
36
+
37
+ // A string shown as evidence is UNTRUSTED text from the product's own source
38
+ // tree, rendered into this page — so it is escaped like any other user data,
39
+ // and clipped so one runaway template literal cannot push the queue off screen.
40
+ function quote(text, max = 240) {
41
+ const s = String(text == null ? '' : text);
42
+ const clipped = s.length > max ? `${s.slice(0, max)}…` : s;
43
+ return `<q class="cd-quote">${escapeHtml(clipped)}</q>`;
44
+ }
45
+
46
+ function where(file, line) {
47
+ if (!file) return '';
48
+ return `<span class="cd-where">${escapeHtml(file)}${Number.isInteger(line) ? `:${line}` : ''}</span>`;
49
+ }
50
+
51
+ const KIND_LABEL = {
52
+ 'off-voice': 'off voice', slop: 'slop', unclear: 'unclear',
53
+ stale: 'stale', duplicate: 'duplicate', typo: 'typo', other: 'flagged',
54
+ };
55
+
56
+ // The three states a flag's target can be in (registry.js annotateFlag).
57
+ // 'gone' is the interesting one and is spelled out rather than iconified: it
58
+ // means the sentence was edited or deleted since the flag was filed, which is
59
+ // a strong hint the flag is resolvable — but a hint for a person to act on,
60
+ // never something the server closes on its own.
61
+ function targetState(f) {
62
+ if (f.target_state === 'gone') {
63
+ return '<span class="cd-tag cd-tag--gone" title="The string this flag names is no longer in the registry — it was edited or removed. Probably already fixed; confirm and close it.">target gone</span>';
64
+ }
65
+ if (f.moved) {
66
+ return '<span class="cd-tag cd-tag--moved" title="Same text, different place — it moved file or line since the flag was filed.">moved</span>';
67
+ }
68
+ return '';
69
+ }
70
+
71
+ function canClose(f) {
72
+ if (!viewer) return false;
73
+ if (String(f.flagged_by) === String(viewer.id)) return true;
74
+ return viewer.rank === 'metic' || viewer.rank === 'archon';
75
+ }
76
+
77
+ function flagRow(f) {
78
+ const who = escapeHtml(f.flagged_by_name || f.flagged_by_login || 'someone');
79
+ const when = f.flagged_at ? new Date(f.flagged_at).toLocaleDateString() : '';
80
+ // Show the live text when the string is still there, and the snapshot when it
81
+ // is not — labelled, so "what it says now" is never confused with "what it
82
+ // said when someone objected".
83
+ const body = f.current
84
+ ? `${quote(f.current.text)} ${where(f.current.file, f.current.line)}`
85
+ : `<span class="cd-snapshot-label">said, when flagged:</span> ${quote(f.text_at_flag)} ${where(f.file_at_flag, f.line_at_flag)}`;
86
+
87
+ const scopeLine = f.scope === 'surface'
88
+ ? `<span class="cd-tag">whole surface</span> <span class="cd-where">${escapeHtml(f.surface)}</span>`
89
+ : body;
90
+
91
+ const closed = f.status !== 'open'
92
+ ? `<p class="cd-closed"><strong>${escapeHtml(f.status)}</strong>${f.resolved_task_id ? ` in task ${escapeHtml(String(f.resolved_task_id))}` : ''} — ${escapeHtml(f.resolution_note || '')}</p>`
93
+ : '';
94
+
95
+ return `<article class="cd-card" data-flag="${escapeHtml(String(f.id))}">
96
+ <div class="cd-card__head">
97
+ <span class="cd-tag cd-tag--kind">${escapeHtml(KIND_LABEL[f.kind] || f.kind)}</span>
98
+ ${targetState(f)}
99
+ <span class="cd-byline">${who} · ${escapeHtml(when)}</span>
100
+ </div>
101
+ <div class="cd-card__target">${scopeLine}</div>
102
+ <p class="cd-reason">${escapeHtml(f.reason)}</p>
103
+ ${closed}
104
+ ${f.status === 'open' && canClose(f)
105
+ ? `<div class="cd-card__actions"><button class="btn btn--ghost cd-close-btn" data-flag="${escapeHtml(String(f.id))}">Close…</button></div>`
106
+ : ''}
107
+ </article>`;
108
+ }
109
+
110
+ function dupeRow(d) {
111
+ const state = d.flagged
112
+ ? '<span class="cd-tag cd-tag--flagged">already flagged</span>'
113
+ : d.settled
114
+ ? '<span class="cd-tag cd-tag--settled">looked at</span>'
115
+ : '<span class="cd-tag cd-tag--untouched">untouched</span>';
116
+ const places = (d.at || []).map((a) => `<li>${escapeHtml(a)}</li>`).join('');
117
+ return `<article class="cd-card">
118
+ <div class="cd-card__head">
119
+ ${state}
120
+ <span class="cd-byline">${escapeHtml(String(d.occurrences))} places · ${escapeHtml(d.surface)}</span>
121
+ </div>
122
+ <div class="cd-card__target">${quote(d.text)}</div>
123
+ <ul class="cd-places">${places}</ul>
124
+ ${d.flagged ? '' : `<div class="cd-card__actions"><button class="btn btn--ghost cd-flag-btn"
125
+ data-id="${escapeHtml(d.id)}" data-surface="${escapeHtml(d.surface)}" data-text="${escapeHtml(d.text)}">Flag this…</button></div>`}
126
+ </article>`;
127
+ }
128
+
129
+ function resultRow(e) {
130
+ return `<article class="cd-card cd-card--slim">
131
+ <div class="cd-card__target">${quote(e.text)} ${where(e.file, e.line)}</div>
132
+ <div class="cd-card__actions">
133
+ <span class="cd-byline">${escapeHtml(e.surface)} · ${escapeHtml(e.origin || '')}</span>
134
+ ${e.flagged
135
+ ? '<span class="cd-tag cd-tag--flagged">already flagged</span>'
136
+ : `<button class="btn btn--ghost cd-flag-btn" data-id="${escapeHtml(e.id)}" data-surface="${escapeHtml(e.surface)}" data-text="${escapeHtml(e.text)}">Flag this…</button>`}
137
+ </div>
138
+ </article>`;
139
+ }
140
+
141
+ // ---------------------------------------------------------------------------
142
+ // paint
143
+ // ---------------------------------------------------------------------------
144
+
145
+ function paintStats() {
146
+ const c = queue.counts;
147
+ const r = queue.registry || {};
148
+ const total = (r.counts && r.counts.strings) || 0;
149
+ $('#cd-stats').innerHTML = [
150
+ ['Open flags', c.flags_open],
151
+ ['Target since edited', c.flags_target_gone],
152
+ ['Duplicates untouched', c.duplicates_untouched],
153
+ ['Strings inventoried', total],
154
+ ].map(([label, n]) =>
155
+ `<div class="cd-stat"><span class="cd-stat__n">${escapeHtml(String(fmtInt ? fmtInt(n) : n))}</span><span class="cd-stat__l">${escapeHtml(label)}</span></div>`
156
+ ).join('');
157
+
158
+ // Say which artifact this page is describing, and how to check it is current.
159
+ // The registry has no timestamp by design (its generator is deterministic and
160
+ // carries no clock), so this page must not imply one — it names the file and
161
+ // the command instead of showing an age that would be the checkout's.
162
+ const blind = (r.blind_spots || []).length;
163
+ $('#cd-registry-note').innerHTML =
164
+ `Read from <code>${escapeHtml(r.at || 'the copy registry')}</code> as committed. `
165
+ + `It is generated, not live: run <code>${escapeHtml(r.freshness_check || 'node scripts/gds/copy-inventory.js --check')}</code> to see whether it still matches the tree.`
166
+ + (blind ? ` This inventory has ${blind} documented blind spot${blind === 1 ? '' : 's'} — server-side messages, skill prose and runtime-composed strings are not in it, so it is not every word the project says.` : '');
167
+ }
168
+
169
+ function paintFlags() {
170
+ const el = $('#cd-flags');
171
+ if (!queue.flags.length) {
172
+ el.innerHTML = '<div class="cd-empty">Nothing flagged here. That is either good news or an unread queue — the duplicates below are the cheapest place to find out.</div>';
173
+ return;
174
+ }
175
+ el.innerHTML = queue.flags.map(flagRow).join('');
176
+ }
177
+
178
+ function paintDupes() {
179
+ const el = $('#cd-dupes');
180
+ const rows = queue.duplicates || [];
181
+ if (!rows.length) { el.innerHTML = '<div class="cd-empty">No duplicated strings in the registry.</div>'; return; }
182
+ // Untouched first — it is the only part of this list that is work.
183
+ const ordered = [...rows].sort((a, b) =>
184
+ (Number(a.flagged || a.settled) - Number(b.flagged || b.settled)) || (b.occurrences - a.occurrences));
185
+ el.innerHTML = ordered.map(dupeRow).join('');
186
+ }
187
+
188
+ function paintSurfaces() {
189
+ const sel = $('#cd-surface');
190
+ const surfaces = Object.keys((queue.registry && queue.registry.surfaces) || {});
191
+ sel.innerHTML = '<option value="">All surfaces</option>'
192
+ + surfaces.map((s) => `<option value="${escapeHtml(s)}">${escapeHtml(s)}</option>`).join('');
193
+ }
194
+
195
+ // ---------------------------------------------------------------------------
196
+ // load
197
+ // ---------------------------------------------------------------------------
198
+
199
+ async function loadQueue() {
200
+ const status = $('#cd-status').value;
201
+ const surface = $('#cd-surface').value;
202
+ const qs = new URLSearchParams({ status });
203
+ if (surface) qs.set('surface', surface);
204
+
205
+ const res = await api.request('GET', `${API}/copy-desk/queue?${qs}`);
206
+ if (!res.ok) {
207
+ const code = res.data && res.data.error && res.data.error.code;
208
+ const detail = (res.data && res.data.error && res.data.error.details) || {};
209
+ $('#cd-sub').textContent = code === 'registry_missing'
210
+ ? `No copy registry on this instance yet. Run ${detail.run || 'node scripts/gds/copy-inventory.js'} and commit it.`
211
+ : 'Could not load the copy desk.';
212
+ $('#cd-flags').innerHTML = '<div class="cd-empty">—</div>';
213
+ $('#cd-dupes').innerHTML = '<div class="cd-empty">—</div>';
214
+ return;
215
+ }
216
+ queue = res.data;
217
+ if (!$('#cd-surface').value && $('#cd-surface').options.length <= 1) paintSurfaces();
218
+ $('#cd-sub').textContent = 'The words this project shows people — what has been flagged, and what nobody has looked at.';
219
+ paintStats();
220
+ paintFlags();
221
+ paintDupes();
222
+ }
223
+
224
+ let searchTimer = null;
225
+ async function runSearch() {
226
+ const q = $('#cd-search').value.trim();
227
+ const surface = $('#cd-surface').value;
228
+ if (!q) { $('#cd-results').innerHTML = ''; $('#cd-search-count').textContent = ''; return; }
229
+
230
+ const qs = new URLSearchParams({ q });
231
+ if (surface) qs.set('surface', surface);
232
+ const res = await api.request('GET', `${API}/copy-desk/strings?${qs}`);
233
+ if (!res.ok) { $('#cd-search-count').textContent = 'Search failed.'; return; }
234
+
235
+ const { entries, matched, truncated, limit } = res.data;
236
+ // Say what is NOT shown. A silently capped list reads as a complete one.
237
+ $('#cd-search-count').textContent = truncated
238
+ ? `${matched} matches — showing the first ${limit}. Narrow the search to see the rest.`
239
+ : `${matched} match${matched === 1 ? '' : 'es'}.`;
240
+ $('#cd-results').innerHTML = entries.map(resultRow).join('');
241
+ }
242
+
243
+ // ---------------------------------------------------------------------------
244
+ // the composer
245
+ // ---------------------------------------------------------------------------
246
+
247
+ function openComposer(t) {
248
+ target = t;
249
+ $('#cd-dialog-target').innerHTML = t.string_id
250
+ ? `${quote(t.text)}<br><span class="cd-where">${escapeHtml(t.surface)}</span>`
251
+ : `<span class="cd-tag">whole surface</span> <span class="cd-where">${escapeHtml(t.surface)}</span>`;
252
+ $('#cd-reason').value = '';
253
+ $('#cd-dialog-err').textContent = '';
254
+ $('#cd-dialog').showModal();
255
+ }
256
+
257
+ async function submitFlag() {
258
+ const reason = $('#cd-reason').value.trim();
259
+ // Mirror the server's floor locally so the common case answers instantly —
260
+ // but the server and the DB constraint are the real gates, not this.
261
+ if (reason.replace(/\s+/g, ' ').length < 10) {
262
+ $('#cd-dialog-err').textContent = 'A flag needs a reason an artist can act on. What is wrong with it, and what would better look like?';
263
+ return;
264
+ }
265
+ const body = {
266
+ scope: target.string_id ? 'string' : 'surface',
267
+ surface: target.surface,
268
+ reason,
269
+ kind: $('#cd-kind').value,
270
+ };
271
+ if (target.string_id) body.string_id = target.string_id;
272
+
273
+ const res = await api.request('POST', `${API}/copy-desk/flags`, { body });
274
+ if (!res.ok) {
275
+ const err = (res.data && res.data.error) || {};
276
+ const d = err.details || {};
277
+ $('#cd-dialog-err').textContent = d.what_this_means || d.why || err.message || 'Could not file the flag.';
278
+ return;
279
+ }
280
+ $('#cd-dialog').close();
281
+ toast('Flagged. It is in the queue.');
282
+ await loadQueue();
283
+ if ($('#cd-search').value.trim()) await runSearch();
284
+ }
285
+
286
+ function openCloser(id) {
287
+ closing = queue.flags.find((f) => String(f.id) === String(id));
288
+ if (!closing) return;
289
+ $('#cd-close-target').innerHTML = `${quote(closing.reason, 160)}`;
290
+ $('#cd-close-note').value = '';
291
+ $('#cd-close-task').value = '';
292
+ $('#cd-close-err').textContent = '';
293
+ $('#cd-close-dialog').showModal();
294
+ }
295
+
296
+ async function submitClose() {
297
+ const note = $('#cd-close-note').value.trim();
298
+ if (note.replace(/\s+/g, ' ').length < 10) {
299
+ $('#cd-close-err').textContent = 'Closing is a judgement too — say what you decided, so the next person does not re-litigate it.';
300
+ return;
301
+ }
302
+ const body = { status: $('#cd-close-status').value, resolution_note: note };
303
+ const taskId = $('#cd-close-task').value.trim();
304
+ if (taskId) body.resolved_task_id = Number(taskId);
305
+
306
+ const res = await api.request('POST', `${API}/copy-desk/flags/${encodeURIComponent(closing.id)}/close`, { body });
307
+ if (!res.ok) {
308
+ const err = (res.data && res.data.error) || {};
309
+ const d = err.details || {};
310
+ $('#cd-close-err').textContent = d.what_this_means || d.why || err.message || 'Could not close the flag.';
311
+ return;
312
+ }
313
+ $('#cd-close-dialog').close();
314
+ toast('Closed.');
315
+ await loadQueue();
316
+ }
317
+
318
+ // ---------------------------------------------------------------------------
319
+ // wire
320
+ // ---------------------------------------------------------------------------
321
+
322
+ document.addEventListener('click', (ev) => {
323
+ const flagBtn = ev.target.closest('.cd-flag-btn');
324
+ if (flagBtn) {
325
+ openComposer({
326
+ string_id: flagBtn.dataset.id,
327
+ surface: flagBtn.dataset.surface,
328
+ text: flagBtn.dataset.text,
329
+ });
330
+ return;
331
+ }
332
+ const closeBtn = ev.target.closest('.cd-close-btn');
333
+ if (closeBtn) openCloser(closeBtn.dataset.flag);
334
+ });
335
+
336
+ $('#cd-cancel').addEventListener('click', () => $('#cd-dialog').close());
337
+ $('#cd-submit').addEventListener('click', submitFlag);
338
+ $('#cd-close-cancel').addEventListener('click', () => $('#cd-close-dialog').close());
339
+ $('#cd-close-submit').addEventListener('click', submitClose);
340
+ $('#cd-status').addEventListener('change', loadQueue);
341
+ $('#cd-surface').addEventListener('change', () => { loadQueue(); if ($('#cd-search').value.trim()) runSearch(); });
342
+ $('#cd-search').addEventListener('input', () => {
343
+ clearTimeout(searchTimer);
344
+ searchTimer = setTimeout(runSearch, 200);
345
+ });
346
+
347
+ (async () => {
348
+ const me = await api.request('GET', `${API}/me`);
349
+ if (me.ok) viewer = me.data.builder || me.data;
350
+ await loadQueue();
351
+ })();
352
+ })();
@@ -122,6 +122,15 @@
122
122
  // active-nav detection (#nav-work), the 'work' gate case, and the server
123
123
  // page-gate all keep working — only the visible label and order change.
124
124
  { id: 'work', label: 'Tasks', icon: 'work', href: '/work', gate: 'signed' },
125
+ // task 1003113 (R02 of goal 1000074): the artist's queue over the words the
126
+ // product shows people. gate 'signed' and NOT a craft gate — filing a flag
127
+ // is open to every role on purpose (the engineer who wrote the toast and
128
+ // the newcomer who could not read the button are the two people most
129
+ // likely to notice bad copy, and neither is the artist), and an artist is a
130
+ // craft rather than a rank, so hiding this behind one would hide the
131
+ // artist's own queue from the artist. The module gate drops the tab on an
132
+ // instance that disables copy-desk.
133
+ { id: 'copy-desk', label: 'Copy desk', icon: 'docs', href: '/copy-desk', gate: 'signed', module: 'copy-desk' },
125
134
  ] },
126
135
  // Your own numbers. task 2174 folded Achievements into Standing and this task
127
136
  // retires its nav entry; VIEW_ALIASES still routes #achievements here, so old
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.248",
3
+ "version": "1.19.250",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.248",
9
+ "version": "1.19.250",
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.248",
3
+ "version": "1.19.250",
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",
@@ -73,7 +73,17 @@ const INTEGRATION = new Set([
73
73
  // platform-server — network + package build + a spawned server, run by its own
74
74
  // .github/workflows/consumer-layout-boot.yml, not this DB-free gate (task 2142).
75
75
  'consumer_layout_boot',
76
- 'claim_gate_open_blocker', 'claim_gate_rebase', 'clawback_confirmed_backlog', 'discord_autojoin', 'discord_oauth',
76
+ 'claim_gate_open_blocker', 'claim_gate_rebase', 'clawback_confirmed_backlog',
77
+ // task 1003113: the copy_desk_flags schema half. The module's own tests are
78
+ // DB-free and cover the pure layer; the rules that MATTER here live in the
79
+ // schema — a CHECK-constraint reason floor whose whole argument is that no
80
+ // future write path evades it, a PARTIAL unique index whose partiality is the
81
+ // design, and a close guarded on the open status so two closers cannot both
82
+ // win. None of those are testable without a real transaction engine.
83
+ // (Prose here avoids single-quoted tokens: the pairing guard below parses this
84
+ // set out of the source, so a quoted word in a comment reads as an entry.)
85
+ 'copy_desk_flags_db',
86
+ 'discord_autojoin', 'discord_oauth',
77
87
  // RENAMED by ADR 0174 (goal 1000068) — these entries said governance_* until task
78
88
  // 1003091. A curated set keyed by a stem that no longer exists silently stops
79
89
  // covering its file: both of these dropped OUT of the integration set and INTO the
@@ -216,6 +216,23 @@ const MODULE_GLOBS = {
216
216
  'modules/game/', 'src/preview-server.js',
217
217
  ],
218
218
  'art-pipeline': ['modules/art-pipeline/'],
219
+ // --- the COPY half of the design contract: the artist's loop over user-facing
220
+ // TEXT (goal 1000074). ADR 0081 gave design TOKENS a repo-owned source of
221
+ // truth; text had none. R01 built the inventory (the copy-inventory
222
+ // generator + its committed registry under docs/), R02 the flag + queue on
223
+ // top of it. The generator travels with the domain but stays under
224
+ // scripts/gds/ — the memory/ideas precedent (a command travels with its
225
+ // domain, design 02§1). The generated artifacts are listed as exact paths
226
+ // so a copy task's touches[] can name them without claiming all of docs/.
227
+ // KEY IS 'copy-desk', NOT 'copy': the branding contract already owns a
228
+ // `copy` block (branding.copy.tagline/hallName/notFound), so the bare word
229
+ // collides with kernel code and reds the "core carries no module code"
230
+ // fitness check on the English word alone.
231
+ 'copy-desk': [
232
+ 'modules/copy-desk/',
233
+ 'scripts/gds/copy-inventory.js',
234
+ 'docs/copy-registry.json', 'docs/copy-inventory.md',
235
+ ],
219
236
  // --- the declarative agent registry (goal 1000038, ADR 0083 default:false):
220
237
  // agent definitions reconciled at deploy into agents_definitions + a per-fire
221
238
  // agents_runs ledger. Carved to modules/agents/ at foundation time, so the
@@ -274,6 +291,27 @@ const MODULE_GLOBS = {
274
291
  'scripts/gds/box.js', 'scripts/gds/box-auth-check.js', 'scripts/gds/box-connect-lib.js',
275
292
  'scripts/gds/smoke-box.sh',
276
293
  ],
294
+ // --- the dev-hygiene enforcement surfaces: CI workflows, the agent permission/
295
+ // hook files, git hooks, root manifests, and the fitness/entropy tooling
296
+ // (task 1003124, goal 1000076). Before this key, NO key resolved these paths,
297
+ // so the continuous-red-team goal could not wall its own work (the
298
+ // 1002690/1002736 precedent). Most of these paths were ALREADY in the
299
+ // protected-surfaces registry (ADR 0043) — what was missing was ownership —
300
+ // so `protected` DERIVES true via the .github/ / package.json / .husky/ /
301
+ // .claude-hooks overlap. Boundaries: '.claude/scheduled-tasks/' stays with
302
+ // autonomy and '.claude/agents/' with agents (their source surfaces);
303
+ // '.claude/skills/' stays with whichever module ships the skill.
304
+ // '.npmrc' and '.gitleaks.toml' are pre-carved territory for goal 1000076's
305
+ // R-tasks (the carve precedes the feature — ADR 0086 §4).
306
+ devsecops: [
307
+ '.github/',
308
+ '.claude/settings.json', '.claude/hooks/',
309
+ '.husky/',
310
+ '.npmrc', '.gitleaks.toml',
311
+ 'package.json', 'package-lock.json',
312
+ 'scripts/gds/fitness.js', 'scripts/gds/docs-entropy.js',
313
+ 'scripts/gds/redteam-patrol.js',
314
+ ],
277
315
  };
278
316
 
279
317
  // A glob set is protected iff it overlaps the imported PROTECTED_GLOBS — the one
@@ -922,8 +922,18 @@ function mountInternalSurfaces(app) {
922
922
  // thetes redirected exactly that reader to /builders/not-ready. Owner's call
923
923
  // (task 1002710) — the RANK floor is gone; sign-in is unchanged.
924
924
  // The link-hiding in shell.js is only cosmetic; these are the real gates.
925
+ // task 1003113: copy-desk joins this tier rather than a higher one. Filing a
926
+ // flag is open to every rank by design (see the nav comment in shell.js), so
927
+ // the page's only real gate is sign-in — the writes behind it self-gate, and
928
+ // closing someone else's flag is checked server-side at metic-or-author.
929
+ //
930
+ // NB the assignment below must stay `const NAME_RE = /…/;` with NOTHING between
931
+ // the `=` and the literal: tests/hall_page_gate_map.mjs reconstructs the whole
932
+ // URL→gate map by parsing this file statically, and a comment in that gap drops
933
+ // the regex out of its map — which reads as "every page on this tier is
934
+ // ungated". It fails loudly by design; keep prose above the const.
925
935
  const SIGNEDIN_BUILDERS_PAGE_RE =
926
- /^\/builders\/(?:work|pair|settings|atlas|modules|goals|roadmap|primer(?:\.md)?|diagrams)(?:\.html)?\/?$/;
936
+ /^\/builders\/(?:work|pair|settings|atlas|modules|goals|roadmap|copy-desk|primer(?:\.md)?|diagrams)(?:\.html)?\/?$/;
927
937
  // The diagram image assets under /builders/diagrams/ ride the same tier as the
928
938
  // page that embeds them.
929
939
  const SIGNEDIN_BUILDERS_PREFIX_RE = /^\/builders\/diagrams\//;
package/src/module-api.js CHANGED
@@ -923,8 +923,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
923
923
  // landed since 1.19.246 with no explicit bump. run 32197488065. (task 1002620)
924
924
  // 1.19.248 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
925
925
  // landed since 1.19.247 with no explicit bump. run 32198208766. (task 1002620)
926
+ // 1.19.249 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
927
+ // landed since 1.19.248 with no explicit bump. run 32199293372. (task 1002620)
928
+ // 1.19.250 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
929
+ // landed since 1.19.249 with no explicit bump. run 32203357427. (task 1002620)
926
930
  // ---------------------------------------------------------------------------
927
- const CORE_VERSION = '1.19.248'; // CI auto-patch carrier (ADR 0161); see changelog above
931
+ const CORE_VERSION = '1.19.250'; // CI auto-patch carrier (ADR 0161); see changelog above
928
932
 
929
933
  // A namespaced logger so a module's log lines are attributable + consistent.
930
934
  // Usage: const log = api.logger('dev-box'); log.info('mounted');