@bongos/core 1.21.148 → 1.21.149

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,11 +2,11 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.21.148",
6
- "core_contract": "1.21.148",
7
- "source_commit": "96de3db7b01db9d1d1ea6de0020e2863549ace48",
5
+ "core_version": "1.21.149",
6
+ "core_contract": "1.21.149",
7
+ "source_commit": "59efe78dbbc530e6743f98d50abc605e2184adcc",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-10-19T16:38:35.780Z",
9
+ "built_at": "2026-10-19T21:44:07.784Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 608,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 3805,
20
- "tree_sha256": "9aa795e140a79d60c77adddcf814eb815324f8778aa3e839f2eed0d221aba834",
20
+ "tree_sha256": "2fb626947478be776e71613c82d973ebf073298ea8d6e5561f4e604e1438d661",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -42,7 +42,7 @@
42
42
  {
43
43
  "path": ".claude/skills/bongos-feedback/SKILL.md",
44
44
  "mode": "0000644",
45
- "sha256": "14b448ca3b42daa4b97d7d699099536047163d50f778f079daa6616cd9b0dd51"
45
+ "sha256": "8b295d76d7cc4c9e0f2549db0f1ac857aa8e76a92b3e00898f95820a32c855ba"
46
46
  },
47
47
  {
48
48
  "path": ".claude/skills/bug-triage/SKILL.md",
@@ -2927,7 +2927,7 @@
2927
2927
  {
2928
2928
  "path": "docs/module-api-changelog.md",
2929
2929
  "mode": "0000644",
2930
- "sha256": "8f019f26dca4f3a5b624cbabd6d28976deef91faab61279479708ab9585b3fc9"
2930
+ "sha256": "af2424a88f8836e7035c00a52637fbbf137bf1b716130a360868fa50bab54b2d"
2931
2931
  },
2932
2932
  {
2933
2933
  "path": "docs/modules-contract.md",
@@ -10552,12 +10552,12 @@
10552
10552
  {
10553
10553
  "path": "package-lock.json",
10554
10554
  "mode": "0000644",
10555
- "sha256": "c5456eee2c0d7c56f4a86b537ad4a056d7ee25178017ce8b2c4a6d584a700b91"
10555
+ "sha256": "d03c2a175c60400695aecccbe5c9ef87b085521c6bf92176c6d2ed08e514c8a6"
10556
10556
  },
10557
10557
  {
10558
10558
  "path": "package.json",
10559
10559
  "mode": "0000644",
10560
- "sha256": "eeee21b5c79606fce2ec960818bbca2bda46e8487e821e086db727725dd34483"
10560
+ "sha256": "9d6713f305cdfe492805906778e171c75c42e4fd8dff5935ad9245d185b6ce22"
10561
10561
  },
10562
10562
  {
10563
10563
  "path": "public-docs/index.html",
@@ -10577,7 +10577,7 @@
10577
10577
  {
10578
10578
  "path": "release-notes.json",
10579
10579
  "mode": "0000644",
10580
- "sha256": "e75ce28a56ae19d8e908bca8b07e539fcb4066b72fb7e6f282e8dff5803f22b4"
10580
+ "sha256": "9394c3c784137020e70389b269a68f36f8f2d0757b306a1f64622b8b5886f368"
10581
10581
  },
10582
10582
  {
10583
10583
  "path": "scripts/cron/style_guide_audit.sh",
@@ -11102,7 +11102,7 @@
11102
11102
  {
11103
11103
  "path": "scripts/gds/feedback-send.js",
11104
11104
  "mode": "0000644",
11105
- "sha256": "2ea43986744a893b0f44954926447a4c817fae8af8ef4956d149b31ea51e96c2"
11105
+ "sha256": "e3c82d95c9412126d0c064d40267c7ae7befbc3cebdba99c496c884dceb4a750"
11106
11106
  },
11107
11107
  {
11108
11108
  "path": "scripts/gds/fetch-art-key.js",
@@ -12957,7 +12957,7 @@
12957
12957
  {
12958
12958
  "path": "src/module-api.js",
12959
12959
  "mode": "0000644",
12960
- "sha256": "2b61d02ebd3b94b723ccec869d7d6432ce9c035c5135259cf6e709759622f0af"
12960
+ "sha256": "b89f1cd4820d3d4257eabf44a666b44c722c9e159dd561bbced94784cceaf8ea"
12961
12961
  },
12962
12962
  {
12963
12963
  "path": "src/module-loader/catalog.js",
@@ -16097,7 +16097,7 @@
16097
16097
  {
16098
16098
  "path": "tests/instance_feedback.mjs",
16099
16099
  "mode": "0000644",
16100
- "sha256": "704a506419ff49032b9604dc039113eaa63db68ac7a57fa09e09162ce7acfae5"
16100
+ "sha256": "a0a558b9d9e0698fa4b6078a2f2e4e2d9679e517149876809c1478743db0f6ff"
16101
16101
  },
16102
16102
  {
16103
16103
  "path": "tests/instance_manifest.mjs",
@@ -14,7 +14,7 @@ Sends a note about Bongos — the tool — straight to its maintainers (tasks 10
14
14
 
15
15
  **When.** File when the builder (a) labels it — "Bongos feedback: …", "tell the Bongos team" — that **is** the go; (b) wishes — "Bongos should…" (**Suggestions count**: send with `--idea`, even if you also fix it in their project); or (c) vents that Bongos is broken — **File it without being asked**, don't argue them out of it. Only about **Bongos itself** (a `bongos`/`/builder-*` command, claim, ship, grade, deploy, upgrade, hall, Settings, sign-in, credits, how it organises work). Not their own project's code, Claude, their laptop or GitHub, nor a rule working as designed. **One filing per distinct problem per session.**
16
16
 
17
- **Write it yourself — Never paste the rant.** First line = what went wrong, in at most twelve words (it becomes the title); then what they did, what they expected, what happened, the exact error line. A wish (`--idea`) asks the maintainers to decide something, so it opens with the card `node scripts/gds/ask.js check <file>` prints, and the rest follows as detail (agents ask like humans, task 1004704). No code, file contents, secrets or personal data.
17
+ **Write it yourself — Never paste the rant.** First line = what went wrong, in at most twelve words (it becomes the title); then what they did, what they expected, what happened, the exact error line. A wish (`--idea`) asks the maintainers to decide something, so open it as an ask (task 1004704): one question of at most twelve words, two or three answers with what each leads to, your pick and why; the rest follows as detail. Write that shape yourself — run no helper first: a project on an older core may not have one, and a failed command before the send is noise (task 1004759). No code, file contents, secrets or personal data.
18
18
 
19
19
  **Send** (stdin, so nothing in the text is mangled by the shell):
20
20
 
@@ -24,7 +24,7 @@ bongos feedback --message-file - <<'EOF'
24
24
  EOF
25
25
  ```
26
26
 
27
- Add `--idea` for a wish. The command prints one line — relay it, then carry on. Proactive filing: one short sentence, e.g. *"I've sent that to the Bongos team."* If it says the project runs only on this computer, tell the builder in one line; never retry.
27
+ Add `--idea` for a wish. The command prints one line — relay it, then carry on. Proactive filing: one short sentence, e.g. *"I've sent that to the Bongos team."* If it says "Not sent", relay that in one line and never retry: "runs only on your computer" and "older Bongos" both mean this project cannot send yet, and the second names the file the note is saved in — send it with `--message-file <that file>` after the project is upgraded. Don't send the builder to GitHub or "the Bongos team" elsewhere instead, and don't call it sent.
28
28
 
29
29
  ## Files this skill touches
30
30
 
@@ -3133,5 +3133,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
3133
3133
  landed since 1.21.146 with no explicit bump. run 39274828902. (task 1002620)
3134
3134
  1.21.148 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3135
3135
  landed since 1.21.147 with no explicit bump. run 39286989989. (task 1002620)
3136
+ 1.21.149 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3137
+ landed since 1.21.148 with no explicit bump. run 39329687123. (task 1002620)
3136
3138
  ---------------------------------------------------------------------------
3137
3139
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.148",
3
+ "version": "1.21.149",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.148",
9
+ "version": "1.21.149",
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.21.148",
3
+ "version": "1.21.149",
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",
@@ -9902,5 +9902,11 @@
9902
9902
  "id": "1004732",
9903
9903
  "text": "When the hub upgrades a project's core, your laptop now notices at the next session start or ship, says so in one line, and on a clean main catches up and reinstalls the new core by itself."
9904
9904
  }
9905
+ ],
9906
+ "1.21.149": [
9907
+ {
9908
+ "id": "1004759",
9909
+ "text": "When a project's site is too old to send Bongos feedback, the command now says 'Not sent' and keeps the note, instead of claiming it was sent."
9910
+ }
9905
9911
  ]
9906
9912
  }
@@ -14,8 +14,12 @@
14
14
  // signed-in builder does nothing extra.
15
15
  //
16
16
  // Fast and quiet by design (the owner, 2026-10-06): the command prints one line
17
- // and returns at once; a detached background process does the network. No task
18
- // number comes back. Only the typed text (secret-scrubbed) and version stamps travel.
17
+ // and returns at once; a detached background process sends to the hub. The one
18
+ // call made in the foreground is the receipt from the builder's own project, so
19
+ // "Sent" is never printed for a report the project could not vouch for (task
20
+ // 1004759: a project on an older core has no receipt door, and the background
21
+ // 404 was invisible). No task number comes back. Only the typed text
22
+ // (secret-scrubbed) and version stamps travel.
19
23
 
20
24
  'use strict';
21
25
 
@@ -47,6 +51,8 @@ const REFUSALS = {
47
51
  too_long: 'That is too long to send — keep it under 4,000 characters, or split it into two.',
48
52
  local_only: 'This project runs only on your computer, so Cloud Bongos cannot confirm who sent the report. Nothing was sent.',
49
53
  no_scrubber: 'Feedback was not sent: the secret filter that checks every report is missing from this install.',
54
+ project_too_old: "Not sent: this project's live site runs an older Bongos that cannot send feedback yet. It will work once the project is upgraded.",
55
+ receipt_failed: "Not sent: this project's live site did not confirm who sent it",
50
56
  };
51
57
 
52
58
  function stateDir() { return path.join(os.homedir(), '.config', ic.FALLBACK_DIR || 'cloudbongos'); }
@@ -98,12 +104,15 @@ function repeatKey(message) {
98
104
  const first = String(message).split('\n').map((l) => l.trim().toLowerCase()).find(Boolean) || '';
99
105
  return crypto.createHash('sha256').update(first).digest('hex').slice(0, 16);
100
106
  }
101
- function checkAndRecordRepeat(message, { file = path.join(stateDir(), 'feedback-sent.json'), now = Date.now() } = {}) {
107
+ // `record: false` only looks: the CLI records a report once its project has
108
+ // vouched for it, so a report that never left is not "already sent" (task 1004759).
109
+ function checkAndRecordRepeat(message, { file = path.join(stateDir(), 'feedback-sent.json'), now = Date.now(), record = true } = {}) {
102
110
  let seen = {};
103
111
  try { seen = JSON.parse(fs.readFileSync(file, 'utf8')) || {}; } catch { /* first report */ }
104
112
  for (const [k, t] of Object.entries(seen)) if (!(now - t < REPEAT_WINDOW_MS)) delete seen[k];
105
113
  const key = repeatKey(message);
106
114
  if (Object.prototype.hasOwnProperty.call(seen, key)) return true;
115
+ if (!record) return false;
107
116
  seen[key] = now;
108
117
  try {
109
118
  fs.mkdirSync(path.dirname(file), { recursive: true });
@@ -112,22 +121,29 @@ function checkAndRecordRepeat(message, { file = path.join(stateDir(), 'feedback-
112
121
  return false;
113
122
  }
114
123
 
115
- // deliver — the network half: receipt from the project, then the report to the
116
- // hub. Returns an outcome object; never prints. Runs in the background child.
117
- async function deliver({ message, kind }, deps = {}) {
124
+ // requestReceipt — the project vouches for this exact text. Foreground: one call
125
+ // to the builder's own project. A 404 means the project's core predates the
126
+ // receipt door (task 1004591), which no retry fixes until it is upgraded.
127
+ async function requestReceipt({ message, kind }, deps = {}) {
118
128
  const api = deps.api || await cliClient();
119
129
  const base = deps.base || ic.resolveApiBase({ session: loadSessionSync() });
120
- const doFetch = deps.fetchImpl || fetch;
121
130
  const sha256 = crypto.createHash('sha256').update(message, 'utf8').digest('hex');
122
131
  const r = await api.request('POST', '/feedback/receipt', { body: { sha256, kind } });
123
- if (!r.ok) return { ok: false, step: 'receipt', code: errCode(r.data) || `http_${r.status}` };
132
+ if (!r.ok) return { ok: false, step: 'receipt', code: r.status === 404 ? 'project_too_old' : (errCode(r.data) || `http_${r.status}`) };
124
133
  const origin = new URL(base).origin;
125
134
  const hub = String((r.data && r.data.hub) || origin).replace(/\/+$/, '');
135
+ return { ok: true, receipt: r.data.receipt, origin, hub };
136
+ }
137
+
138
+ // sendToHub — the vouched report straight to the hub. Returns an outcome
139
+ // object; never prints. Runs in the background child.
140
+ async function sendToHub({ message, kind, receipt, origin, hub }, deps = {}) {
141
+ const doFetch = deps.fetchImpl || fetch;
126
142
  const res = await doFetch(`${hub}/api/bongos/sso/feedback/direct`, {
127
143
  method: 'POST',
128
144
  headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
129
145
  body: JSON.stringify({
130
- origin, receipt: r.data.receipt, message, kind,
146
+ origin, receipt, message, kind,
131
147
  context: { cli_version: cliVersion(), os: `${os.platform()} ${os.release()}` },
132
148
  }),
133
149
  signal: AbortSignal.timeout(15000),
@@ -138,13 +154,31 @@ async function deliver({ message, kind }, deps = {}) {
138
154
  return { ok: false, step: 'hub', code: errCode(body) || `http_${res.status}` };
139
155
  }
140
156
 
157
+ // deliver — both halves in one go.
158
+ async function deliver(job, deps = {}) {
159
+ const r = await requestReceipt(job, deps);
160
+ if (!r.ok) return r;
161
+ return sendToHub({ ...job, receipt: r.receipt, origin: r.origin, hub: r.hub }, deps);
162
+ }
163
+
164
+ // A report the project could not vouch for is kept, so it can be sent after the
165
+ // upgrade (`--message-file <it>`) instead of being written again.
166
+ function keepUnsent(message, { dir = stateDir(), now = new Date() } = {}) {
167
+ try {
168
+ fs.mkdirSync(dir, { recursive: true });
169
+ const file = path.join(dir, `feedback-unsent-${now.toISOString().replace(/[:.]/g, '-')}.md`);
170
+ fs.writeFileSync(file, message, { mode: 0o600 });
171
+ return file;
172
+ } catch { return null; }
173
+ }
174
+
141
175
  // The background child: read the queued report, deliver it, record the outcome
142
176
  // where a curious builder (or a maintainer helping them) can look.
143
177
  async function runDeliverChild(file) {
144
178
  let job;
145
179
  try { job = JSON.parse(fs.readFileSync(file, 'utf8')); } finally { fs.rmSync(path.dirname(file), { recursive: true, force: true }); }
146
180
  let outcome;
147
- try { outcome = await deliver(job); } catch (e) { outcome = { ok: false, step: 'network', code: String(e && e.message || e).slice(0, 200) }; }
181
+ try { outcome = job.receipt ? await sendToHub(job) : await deliver(job); } catch (e) { outcome = { ok: false, step: 'network', code: String(e && e.message || e).slice(0, 200) }; }
148
182
  try {
149
183
  fs.writeFileSync(path.join(stateDir(), 'feedback-last.json'), JSON.stringify({ at: new Date().toISOString(), ...outcome }), { mode: 0o600 });
150
184
  } catch { /* nowhere to record — the report itself is what mattered */ }
@@ -177,16 +211,27 @@ async function main(argv = process.argv.slice(2), deps = {}) {
177
211
  if (isLocalBase(base)) { console.error(REFUSALS.local_only); return 1; }
178
212
 
179
213
  const job = { message: scrubbed.text, kind };
180
- if ((deps.checkRepeat || checkAndRecordRepeat)(job.message)) {
214
+ const checkRepeat = deps.checkRepeat || checkAndRecordRepeat;
215
+ if (checkRepeat(job.message, { record: false })) {
181
216
  console.log('Already sent — the Bongos team has this one.');
182
217
  return 0;
183
218
  }
219
+ // The receipt in the foreground: "Sent" only once the project has vouched.
220
+ const r = await (deps.requestReceipt || requestReceipt)(job, { base });
221
+ if (!r.ok) {
222
+ const kept = (deps.keepUnsent || keepUnsent)(job.message);
223
+ const why = r.code === 'project_too_old' ? REFUSALS.project_too_old : `${REFUSALS.receipt_failed} (${r.code}).`;
224
+ console.error(kept ? `${why} Your note is saved in ${kept}` : why);
225
+ return 1;
226
+ }
227
+ checkRepeat(job.message);
228
+ const vouched = { ...job, receipt: r.receipt, origin: r.origin, hub: r.hub };
184
229
  if (hasFlag('--wait', argv)) {
185
- const outcome = await (deps.deliver || deliver)(job);
230
+ const outcome = await (deps.sendToHub || sendToHub)(vouched);
186
231
  console.log(outcome.ok ? SENT_LINE : `Not sent (${outcome.step}: ${outcome.code}).`);
187
232
  return outcome.ok ? 0 : 1;
188
233
  }
189
- (deps.spawnDelivery || spawnDelivery)(job);
234
+ (deps.spawnDelivery || spawnDelivery)(vouched);
190
235
  console.log(SENT_LINE);
191
236
  return 0;
192
237
  }
@@ -201,4 +246,4 @@ if (require.main === module) {
201
246
  });
202
247
  }
203
248
 
204
- module.exports = { main, deliver, readMessage, REFUSALS, scrubReport, isLocalBase, repeatKey, checkAndRecordRepeat, SENT_LINE };
249
+ module.exports = { main, deliver, requestReceipt, sendToHub, keepUnsent, readMessage, REFUSALS, scrubReport, isLocalBase, repeatKey, checkAndRecordRepeat, SENT_LINE };
package/src/module-api.js CHANGED
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
75
75
  // MAJOR (see allowBoxScope below): passes the request through untouched.
76
76
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
77
77
 
78
- const CORE_VERSION = '1.21.148'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.149'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
79
79
 
80
80
  // A namespaced logger so a module's log lines are attributable + consistent.
81
81
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -326,6 +326,8 @@ async function runCli(argv, extra = {}) {
326
326
  requireSession: async () => ({ token: 't' }),
327
327
  base: 'https://mercury.cloudbongos.com',
328
328
  checkRepeat: () => false,
329
+ requestReceipt: async () => ({ ok: true, receipt: 'R'.repeat(24), origin: 'https://mercury.cloudbongos.com', hub: 'https://cloudbongos.com' }),
330
+ keepUnsent: () => null,
329
331
  spawnDelivery: (job) => spawned.push(job),
330
332
  ...extra,
331
333
  });
@@ -337,7 +339,49 @@ test('the CLI prints one line and hands the network to the background', async ()
337
339
  const r = await runCli(['claim', 'hung']);
338
340
  assert.equal(r.code, 0);
339
341
  assert.deepEqual(r.out, ['Sent to the Bongos team.']);
340
- assert.deepEqual(r.spawned, [{ message: 'claim hung', kind: 'bug' }]);
342
+ assert.deepEqual(r.spawned, [{ message: 'claim hung', kind: 'bug', receipt: 'R'.repeat(24), origin: 'https://mercury.cloudbongos.com', hub: 'https://cloudbongos.com' }]);
343
+ });
344
+
345
+ // Task 1004759 (toast, core 1.21.20): the project's live site had no receipt
346
+ // door, the background child got a 404, and the builder had been told "Sent".
347
+ test('a project too old to vouch: "Not sent" in the foreground, the note kept, nothing recorded as sent', async () => {
348
+ const kept = []; const recorded = [];
349
+ const r = await runCli(['--idea', 'one', 'skill', 'for', 'project', 'purpose'], {
350
+ requestReceipt: async () => ({ ok: false, step: 'receipt', code: 'project_too_old' }),
351
+ keepUnsent: (m) => { kept.push(m); return '/home/x/.config/cloudbongos/feedback-unsent-1.md'; },
352
+ checkRepeat: (m, o = {}) => { if (o.record !== false) recorded.push(m); return false; },
353
+ });
354
+ assert.equal(r.code, 1);
355
+ assert.deepEqual(r.out, [], 'never says Sent');
356
+ assert.equal(r.spawned.length, 0);
357
+ assert.match(r.err.join('\n'), /older Bongos/);
358
+ assert.match(r.err.join('\n'), /feedback-unsent-1\.md/, 'says where the note is');
359
+ assert.deepEqual(kept, ['one skill for project purpose']);
360
+ assert.deepEqual(recorded, [], 'a report that never left is not "already sent"');
361
+ const other = await runCli(['x'], { requestReceipt: async () => ({ ok: false, step: 'receipt', code: 'not_signed_in' }) });
362
+ assert.equal(other.code, 1);
363
+ assert.match(other.err.join('\n'), /did not confirm.*not_signed_in/);
364
+ });
365
+
366
+ test('the repeat guard records only after the project vouched', async () => {
367
+ const calls = [];
368
+ await runCli(['x'], { checkRepeat: (m, o = {}) => { calls.push(o.record === false ? 'look' : 'record'); return false; } });
369
+ assert.deepEqual(calls, ['look', 'record']);
370
+ });
371
+
372
+ test('requestReceipt: a 404 from the project reads as project_too_old', async () => {
373
+ const { requestReceipt } = require_(path.join(ROOT, 'scripts/gds/feedback-send.js'));
374
+ const api = { request: async () => ({ ok: false, status: 404, data: { error: 'Not found' } }) };
375
+ const r = await requestReceipt({ message: 'x', kind: 'idea' }, { api, base: 'https://toast.cloudbongos.com' });
376
+ assert.deepEqual(r, { ok: false, step: 'receipt', code: 'project_too_old' });
377
+ });
378
+
379
+ test('keepUnsent writes the note where the builder can send it again', () => {
380
+ const { keepUnsent } = require_(path.join(ROOT, 'scripts/gds/feedback-send.js'));
381
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'fb-unsent-'));
382
+ const file = keepUnsent('the note', { dir, now: new Date('2026-10-19T10:00:00Z') });
383
+ assert.equal(fs.readFileSync(file, 'utf8'), 'the note');
384
+ assert.match(path.basename(file), /^feedback-unsent-2026-10-19T10-00-00-000Z\.md$/);
341
385
  });
342
386
 
343
387
  test('the CLI refuses a project only this computer can reach, and sends nothing', async () => {