@bongos/core 1.21.47 → 1.21.49

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.
Files changed (39) hide show
  1. package/.bongos-core.json +50 -60
  2. package/.claude/skills/bongos-feedback/SKILL.md +5 -4
  3. package/bin/bongos.js +0 -0
  4. package/docs/adr/0087-bongos-app-architecture-and-handoff-contract.md +2 -2
  5. package/docs/copy-inventory.md +33 -31
  6. package/docs/copy-registry.json +103 -75
  7. package/docs/file-map.md +0 -1
  8. package/docs/module-api-changelog.md +4 -0
  9. package/docs/onboarding/primer.md +23 -16
  10. package/docs/page-readings.json +154 -145
  11. package/modules/hall-ui/public/commands-lib.js +1 -1
  12. package/modules/hall-ui/public/hall-render.js +13 -11
  13. package/modules/hall-ui/public/settings.html +4 -4
  14. package/modules/hall-ui/public/settings.js +1 -1
  15. package/modules/hall-ui/public/style.css +9 -0
  16. package/modules/onboarding/onboarding-state.js +5 -5
  17. package/package-lock.json +2 -2
  18. package/package.json +1 -1
  19. package/release-notes.json +16 -0
  20. package/scripts/gds/claude-materialize.js +3 -0
  21. package/scripts/gds/pack-env-prefix.js +73 -0
  22. package/scripts/gds/provision-repo.js +13 -0
  23. package/scripts/gds/provision-units.js +6 -17
  24. package/scripts/gds/provision.js +8 -2
  25. package/src/bongos/routes/auth.js +6 -5
  26. package/src/instance-config.js +28 -1
  27. package/src/module-api.js +1 -1
  28. package/tests/bongos_cli.mjs +2 -1
  29. package/tests/cli_allowlist_permissions.mjs +4 -0
  30. package/tests/cli_token_reissue.mjs +5 -3
  31. package/tests/hall_onboarding_bongos_setup.mjs +101 -0
  32. package/tests/instance_config.mjs +53 -0
  33. package/tests/instance_feedback.mjs +12 -0
  34. package/tests/provision.mjs +103 -0
  35. package/tests/skill_menu_ruling.mjs +1 -1
  36. package/.claude/skills/feedback/SKILL.md +0 -39
  37. package/scripts/bongos-mcp.js +0 -28
  38. package/scripts/gds/feedback-latest.js +0 -224
  39. package/tests/feedback_latest.mjs +0 -122
@@ -0,0 +1,101 @@
1
+ // task 1004574 — every builders-hall onboarding surface names ONE sign-in
2
+ // command, `npx bongos setup`, the same on every OS. Bare `bongos` is not on a
3
+ // fresh clone's PATH; `npx` resolves the repo's own bin after `npm install`.
4
+ // The old three (`/builder-setup`, `./scripts/setup-builder.sh`,
5
+ // `node scripts/setup-builder.js`) must not come back on these surfaces.
6
+ import { test } from 'node:test';
7
+ import assert from 'node:assert/strict';
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { execFileSync } from 'node:child_process';
12
+
13
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
14
+ const read = (p) => fs.readFileSync(path.join(ROOT, p), 'utf8');
15
+ const OLD = /\/builder-setup|setup-builder\.(sh|js)/;
16
+
17
+ // The welcome panel's markup, from the setupSteps literal to the panel's innerHTML.
18
+ function welcomeMarkup() {
19
+ const src = read('modules/hall-ui/public/hall-render.js');
20
+ const start = src.indexOf('const setupSteps = `');
21
+ const end = src.indexOf('body.innerHTML = `', start);
22
+ assert.ok(start > 0 && end > start, 'welcome panel markup located');
23
+ return src.slice(start, end).split('\n').filter((l) => !/^\s*\/\//.test(l)).join('\n');
24
+ }
25
+
26
+ test('welcome panel: the steps and the clone block say npx bongos setup', () => {
27
+ const w = welcomeMarkup();
28
+ assert.match(w, /Run <code>npx bongos setup<\/code>/);
29
+ assert.match(w, /\\nnpm install\\nnpx bongos setup`/, 'clone block ends npm install + npx bongos setup');
30
+ assert.doesNotMatch(w, OLD, 'no legacy setup command on the welcome panel');
31
+ assert.doesNotMatch(w, /On Windows, run/, 'one command on every OS — no Windows-only line');
32
+ assert.match(w, /nodejs\.org/, 'the tooling list names Node, which npm install needs');
33
+ });
34
+
35
+ test('onboarding checklist: the cli_setup step names npx bongos setup', () => {
36
+ const src = read('modules/onboarding/onboarding-state.js');
37
+ const labels = src.slice(src.indexOf('const DISPLAY_LABELS'), src.indexOf('function nextStage'));
38
+ assert.match(labels, /cli_setup:\s+'Run npx bongos setup'/);
39
+ assert.match(labels, /cli_setup:\s+'[^']*npx bongos setup[^']*'/);
40
+ assert.doesNotMatch(labels, OLD);
41
+ });
42
+
43
+ test('primer: both routes sign in with npx bongos setup', () => {
44
+ const md = read('docs/onboarding/primer.md');
45
+ assert.match(md, /```sh\nnpm install\nnpx bongos setup\n```/);
46
+ // Not inside a `>` quote: the hall's reader garbles a fence there (task 1004575).
47
+ assert.doesNotMatch(md, /^> ```/m);
48
+ assert.match(md, /run `npx bongos setup` again/);
49
+ assert.doesNotMatch(md, OLD);
50
+ });
51
+
52
+ test('the get-the-code block keeps one command per line (screen and copy)', () => {
53
+ const css = read('modules/hall-ui/public/style.css');
54
+ const m = /\.setup__getcode \.welcome__paste\s*\{([^}]*)\}/.exec(css);
55
+ assert.ok(m, 'scoped rule present');
56
+ assert.match(m[1], /white-space:\s*pre/, 'copy-on-click reads innerText, so pre is what keeps the line breaks');
57
+ assert.match(m[1], /overflow-y:\s*hidden/, 'a one-axis strip names both axes');
58
+ });
59
+
60
+ // task 1004575 — the audit of the above, across the rest of the hall.
61
+
62
+ test('bin/bongos.js is committed executable, so npx leaves a fresh clone clean', () => {
63
+ // Committed 100644, `npx bongos` chmods it on first run: the clone then reads as
64
+ // changed, and a reverted mode makes the next run fail "Permission denied".
65
+ const entry = execFileSync('git', ['ls-files', '-s', 'bin/bongos.js'], { cwd: ROOT, encoding: 'utf8' });
66
+ assert.match(entry, /^100755 /, `expected mode 100755, got: ${entry.trim()}`);
67
+ });
68
+
69
+ test('welcome intro and checklist lead ask only for what a builder needs', () => {
70
+ const src = read('modules/hall-ui/public/hall-render.js');
71
+ assert.doesNotMatch(src, /run the platform locally/, 'a builder does not run the platform');
72
+ assert.match(src, /Clone the project to your machine and run <code>npm install<\/code> in it\./);
73
+ const st = read('modules/onboarding/onboarding-state.js');
74
+ assert.doesNotMatch(st, /Device Flow or the web sign-in/);
75
+ assert.match(st, /auth_complete:\s+'[^']*npx bongos setup[^']*'/);
76
+ });
77
+
78
+ test('primer no-terminal route installs dependencies (and Node) before signing in', () => {
79
+ const md = read('docs/onboarding/primer.md');
80
+ assert.match(md, /Node\*\* \(version 22 or newer\)/);
81
+ assert.match(md, /send this one message — \*Install this project's dependencies, then run npx bongos setup\.\* —/);
82
+ assert.match(md, /have four things ready/);
83
+ });
84
+
85
+ test('Settings CLI access says where to paste the npx command', () => {
86
+ const html = read('modules/hall-ui/public/settings.html');
87
+ const js = read('modules/hall-ui/public/settings.js');
88
+ assert.match(html, /into a terminal in your project folder/);
89
+ assert.doesNotMatch(html, /bypasses Device Flow/);
90
+ assert.match(js, /Paste this into a terminal in your project folder:/);
91
+ });
92
+
93
+ test('the npx paste command Settings hands out is pre-approved for a Claude session', () => {
94
+ // Both lists: the core's own settings (Bash + PowerShell twins) and the starter
95
+ // list materialized into every instance. Without them the copied command prompts.
96
+ const settings = JSON.parse(read('.claude/settings.json'));
97
+ for (const tool of ['Bash', 'PowerShell']) {
98
+ assert.ok(settings.permissions.allow.includes(`${tool}(npx bongos paste-token:*)`), `${tool} twin`);
99
+ }
100
+ assert.match(read('scripts/gds/claude-materialize.js'), /^\s*'npx bongos paste-token:\*',$/m);
101
+ });
@@ -235,3 +235,56 @@ test('resolveApiBase: session:null does not read the session file from disk', ()
235
235
  // and callers that already know there is no session get a deterministic answer.
236
236
  assert.equal(ic.resolveApiBase({ session: null, env: {}, b: BRAND_B }), 'https://instance-b.example');
237
237
  });
238
+
239
+ // ── task 1004573: the platform's BONGOS_* spelling is always heard ────────────
240
+ // The platform writes every hosted project's web.env as BONGOS_*. Mercury's adopted
241
+ // config/branding.json said envPrefix CLOUDBONGOS, so it read none of it — not its
242
+ // sign-in credentials, not its address, not one setting (live, 2026-10-04).
243
+
244
+ test('resolveEnv: a pack naming another prefix still reads the platform\'s BONGOS_* (task 1004573)', () => {
245
+ const env = { BONGOS_GITHUB_CLIENT_ID: 'Iv23-platform' };
246
+ assert.equal(resolveEnv('GITHUB_CLIENT_ID', { prefix: 'CLOUDBONGOS', env, warn: () => {}, warnPack: () => {} }), 'Iv23-platform');
247
+ assert.equal(resolveEnv('GITHUB_CLIENT_ID', { prefix: 'OTB', env, warn: () => {}, warnPack: () => {} }), 'Iv23-platform');
248
+ });
249
+
250
+ test('resolveEnv: the pack\'s own spelling still WINS over the platform\'s (task 1004573)', () => {
251
+ const env = { CLOUDBONGOS_PUBLIC_ORIGIN: 'https://own.example', BONGOS_PUBLIC_ORIGIN: 'https://platform.example' };
252
+ assert.equal(resolveEnv('PUBLIC_ORIGIN', { prefix: 'CLOUDBONGOS', env, warn: () => {}, warnPack: () => {} }), 'https://own.example');
253
+ });
254
+
255
+ test('resolveEnv: BONGOS_* is read BEFORE the legacy list, and is never called deprecated (task 1004573)', () => {
256
+ const legacyWarnings = [];
257
+ const v = resolveEnv('JOINABILITY', { prefix: 'OTB', env: { BONGOS_JOINABILITY: 'open', PMS_JOINABILITY: 'closed' }, warn: (w) => legacyWarnings.push(w), warnPack: () => {} });
258
+ assert.equal(v, 'open', 'the canonical platform spelling beats a legacy one');
259
+ assert.ok(!legacyWarnings.some((w) => /^BONGOS_/.test(w.legacyName)), 'BONGOS_ is the canonical name — no deprecation warning names it');
260
+ });
261
+
262
+ test('resolveEnv: when it has to read BONGOS_*, it says to fix the pack — and only then (task 1004573)', () => {
263
+ const calls = [];
264
+ const warnPack = (w) => calls.push(w);
265
+ resolveEnv('LOOK', { prefix: 'CLOUDBONGOS', env: { BONGOS_LOOK: 'own' }, warn: () => {}, warnPack });
266
+ assert.deepEqual(calls, [{ prefix: 'CLOUDBONGOS', name: 'BONGOS_LOOK' }], 'names the pack prefix and the name it fell back to');
267
+ resolveEnv('LOOK', { prefix: 'CLOUDBONGOS', env: { CLOUDBONGOS_LOOK: 'x', BONGOS_LOOK: 'own' }, warn: () => {}, warnPack });
268
+ resolveEnv('LOOK', { prefix: 'BONGOS', env: { BONGOS_LOOK: 'own' }, warn: () => {}, warnPack });
269
+ assert.equal(calls.length, 1, 'silent when the pack\'s own spelling won, and when the pack is already BONGOS');
270
+ });
271
+
272
+ test('resolveEnv: the default pack warning names the fix, once per process (task 1004573)', () => {
273
+ const seen = [];
274
+ const orig = console.warn;
275
+ console.warn = (m) => seen.push(String(m));
276
+ try {
277
+ // the default warnPack — the real wiring, not a hand-called helper
278
+ resolveEnv('ONE', { prefix: 'CLOUDBONGOS', env: { BONGOS_ONE: '1' }, warn: () => {} });
279
+ resolveEnv('TWO', { prefix: 'CLOUDBONGOS', env: { BONGOS_TWO: '2' }, warn: () => {} });
280
+ } finally { console.warn = orig; }
281
+ const lines = seen.filter((m) => /envPrefix "CLOUDBONGOS"/.test(m));
282
+ assert.equal(lines.length, 1, 'one line, not one per setting');
283
+ assert.match(lines[0], /BONGOS_ONE/);
284
+ assert.match(lines[0], /Set envPrefix to "BONGOS"/);
285
+ });
286
+
287
+ test('resolveEnv: a pack already on BONGOS is unchanged (task 1004573)', () => {
288
+ assert.equal(resolveEnv('X', { prefix: 'BONGOS', env: { BONGOS_X: 'a', PMS_X: 'b' }, warn: () => {}, warnPack: () => {} }), 'a');
289
+ assert.equal(resolveEnv('X', { prefix: 'BONGOS', env: {}, warn: () => {}, warnPack: () => {} }), undefined);
290
+ });
@@ -312,6 +312,18 @@ test('the skill files unasked on a Bongos rant, and the kernel tells every sessi
312
312
  assert.match(kernel, /vents that Bongos itself is broken.*\/bongos-feedback/);
313
313
  });
314
314
 
315
+ // task 1004576: "Bongos feedback: …" (a calm wish, not a rant) fired no skill — the
316
+ // listing only described venting, and a since-deleted /feedback skill owned the phrase.
317
+ test('a labelled message or a wish about Bongos triggers the skill too, and nothing else claims the phrase', () => {
318
+ const skill = fs.readFileSync(path.join(ROOT, '.claude/skills/bongos-feedback/SKILL.md'), 'utf8');
319
+ const description = skill.split('\n---')[0];
320
+ assert.match(description, /Bongos feedback:/, 'the literal label is a trigger');
321
+ assert.match(description, /Bongos should/, 'a wish is a trigger');
322
+ assert.match(skill, /Suggestions count/);
323
+ assert.match(skill, /\*\*is\*\* the go/, 'a labelled message is sent without a second ask');
324
+ assert.ok(!fs.existsSync(path.join(ROOT, '.claude/skills/feedback')), 'no second skill competes for "feedback"');
325
+ });
326
+
315
327
  test('every report is secret-scrubbed before it leaves the machine, and never sent unscrubbed', async () => {
316
328
  const { scrubReport } = require_(path.join(ROOT, 'scripts/gds/feedback-send.js'));
317
329
  const token = 'ghp_' + 'a1B2c3D4e5F6g7H8i9J0k1L2m3N4o5P6q7R8';
@@ -1330,6 +1330,59 @@ await ta('scaffoldStandaloneRepo(adopt): does NOT clobber an existing .gitignore
1330
1330
  assert.match(String(gi.body), /node_modules\//, 'and the entries `npm ci` + the deploy require are added');
1331
1331
  });
1332
1332
 
1333
+ // Task 1004573: adopt layers config only where ABSENT, so an owner's config/branding.json
1334
+ // kept whatever envPrefix it carried while web.env was written under the platform's —
1335
+ // mercury's said CLOUDBONGOS and read none of its credentials or settings.
1336
+ async function adoptWithPack(packText, mode = 'adopt') {
1337
+ const base = mkdtempSync(join(tmpdir(), `pack-${mode}-`));
1338
+ const origBase = P.CONFIG.standaloneBase;
1339
+ P.CONFIG.standaloneBase = base;
1340
+ try {
1341
+ mkdirSync(join(base, 'demo', 'config'), { recursive: true });
1342
+ writeFileSync(join(base, 'demo', 'config', 'branding.json'), packText);
1343
+ const { deps } = recorder({ apply: true });
1344
+ const seq = [];
1345
+ deps.exec = (cmd) => { seq.push(`exec ${cmd}`); return { ok: true, stdout: '' }; };
1346
+ deps.writeFile = (p, body) => { seq.push(`write ${p}`); seq.writes = (seq.writes || []).concat([{ p, body }]); return { ok: true }; };
1347
+ deps.getGithubToken = async () => ({ token: 'gho_TOK', scope: 'repo' });
1348
+ await P.scaffoldStandaloneRepo(
1349
+ { slug: 'demo', hosting_shape: 'cloud-host', target_ref: 'example-owner/demo', owner_builder_id: 7, onboard_mode: mode },
1350
+ { ...deps, instanceRoot: base },
1351
+ );
1352
+ return { seq, writes: seq.writes || [] };
1353
+ } finally {
1354
+ P.CONFIG.standaloneBase = origBase;
1355
+ rmSync(base, { recursive: true, force: true });
1356
+ }
1357
+ }
1358
+ const U2 = require('../scripts/gds/provision-units.js');
1359
+
1360
+ await ta('scaffoldStandaloneRepo(adopt): an owner pack on another envPrefix is corrected IN the adopt commit (task 1004573)', async () => {
1361
+ const owner = '{\n "identity": { "productName": "mercury" },\n "envPrefix": "CLOUDBONGOS",\n "configDir": "cloudbongos"\n}\n';
1362
+ const { seq, writes } = await adoptWithPack(owner);
1363
+ const w = writes.find((x) => /config\/branding\.json$/.test(x.p));
1364
+ assert.ok(w, 'the pack was rewritten');
1365
+ assert.equal(JSON.parse(w.body).envPrefix, U2.provisionedEnvPrefix(), 'to the prefix web.env is written under');
1366
+ assert.equal(w.body, owner.replace('"envPrefix": "CLOUDBONGOS"', `"envPrefix": "${U2.provisionedEnvPrefix()}"`), 'and NOTHING else in the owner\'s file moved');
1367
+ const wrote = seq.findIndex((s) => /^write .*config\/branding\.json$/.test(s));
1368
+ const added = seq.findIndex((s) => /git -C \S+ add -A/.test(s));
1369
+ assert.ok(wrote >= 0 && added > wrote, 'written before `git add -A`, so it rides the adopt commit to the owner\'s repo');
1370
+ });
1371
+
1372
+ await ta('scaffoldStandaloneRepo: a pack that already agrees, and a greenfield scaffold, are left alone (task 1004573)', async () => {
1373
+ const agree = await adoptWithPack(JSON.stringify({ envPrefix: U2.provisionedEnvPrefix() }));
1374
+ assert.ok(!agree.writes.some((x) => /config\/branding\.json$/.test(x.p)), 'agreeing pack untouched');
1375
+ const green = await adoptWithPack(JSON.stringify({ envPrefix: 'CLOUDBONGOS' }), 'greenfield');
1376
+ assert.ok(!green.writes.some((x) => /config\/branding\.json$/.test(x.p)), 'greenfield owns its config through init; no rewrite here');
1377
+ });
1378
+
1379
+ await ta('normalizePackEnvPrefix: rewrites only the value, refuses what it cannot rewrite safely (task 1004573)', async () => {
1380
+ assert.deepEqual(U2.normalizePackEnvPrefix('{"envPrefix":"OTB","x":1}', 'BONGOS'), { text: '{"envPrefix":"BONGOS","x":1}', from: 'OTB' });
1381
+ assert.equal(U2.normalizePackEnvPrefix('{"envPrefix":"BONGOS"}', 'BONGOS'), null);
1382
+ assert.equal(U2.normalizePackEnvPrefix('{"identity":{}}', 'BONGOS'), null, 'no envPrefix → nothing to correct');
1383
+ assert.equal(U2.normalizePackEnvPrefix('not json', 'BONGOS'), null);
1384
+ });
1385
+
1333
1386
  await ta('scaffoldStandaloneRepo(adopt): a default branch that is not `main` fails LOUDLY, not silently', async () => {
1334
1387
  // The rest of provisioning is main-only (git pull --ff-only origin main; the box deploy
1335
1388
  // does git reset --hard origin/main), so layering onto another branch would push somewhere
@@ -1776,6 +1829,56 @@ await ta('a failure BEFORE GitHub handed over credentials claims no App (task 10
1776
1829
  assert.ok(resolves.every((x) => !('app_slug' in x.patch)), 'no App is named when none was handed over');
1777
1830
  });
1778
1831
 
1832
+ // Task 1004573: an adopted repo brings its own config/branding.json. Mercury's said
1833
+ // envPrefix CLOUDBONGOS while the platform wrote BONGOS_*, so the project never saw
1834
+ // its credentials — and the only thing anyone was told was "did not report sign-in
1835
+ // ready". When the check fails, the runner now reads the pack and names a mismatch.
1836
+ const U = require('../scripts/gds/provision-units.js');
1837
+
1838
+ await ta('envPrefixMismatch names a pack that reads other names, and nothing else (task 1004573)', async () => {
1839
+ const m = U.envPrefixMismatch(JSON.stringify({ envPrefix: 'CLOUDBONGOS' }), 'BONGOS');
1840
+ assert.match(m, /reads settings named CLOUDBONGOS_\*, but the platform wrote BONGOS_\*/);
1841
+ assert.match(m, /set its envPrefix to "BONGOS"/);
1842
+ assert.equal(U.envPrefixMismatch(JSON.stringify({ envPrefix: 'BONGOS' }), 'BONGOS'), null, 'agreeing names say nothing');
1843
+ assert.equal(U.envPrefixMismatch(JSON.stringify({ identity: {} }), 'BONGOS'), null, 'a pack with no envPrefix claims nothing');
1844
+ assert.equal(U.envPrefixMismatch('not json', 'BONGOS'), null, 'an unreadable pack claims nothing');
1845
+ assert.equal(U.envPrefixMismatch(null, 'BONGOS'), null);
1846
+ });
1847
+
1848
+ await ta('a sign-in check that fails on a mismatched pack SAYS so (task 1004573)', async () => {
1849
+ const inst = { ...coTenant, hosting_shape: 'cloud-host', slug: 'mercury', domain: 'mercury.example.com' };
1850
+ const cmds = [], reads = [];
1851
+ const deps = {
1852
+ apply: true, privileged: false, log: () => {}, sleep: async () => {},
1853
+ convertManifestCode: async () => ({ clientId: 'Iv1.abcdef0123456789', clientSecret: 'z'.repeat(40), appId: 1, appSlug: 's' }),
1854
+ exec: (cmd) => { cmds.push(cmd); return { ok: true, stdout: '{"ok":true,"auth_configured":false}' }; },
1855
+ readFile: (p) => { reads.push(p); return JSON.stringify({ envPrefix: 'CLOUDBONGOS' }); },
1856
+ writeFile: () => ({ ok: true }),
1857
+ };
1858
+ const r = await P.placeManifestCreds(inst, { github_code: 'C', attempts: 1 }, deps);
1859
+ assert.equal(r.ok, false);
1860
+ assert.match(r.error, /credentials are placed/, 'still owns what DID happen');
1861
+ assert.match(r.error, /reads settings named CLOUDBONGOS_\*, but the platform wrote/, 'and names the cause');
1862
+ assert.deepEqual(reads, [join(P.standaloneRoot(inst), 'config', 'branding.json')], 'reads the project\'s OWN pack, from its checkout');
1863
+ assert.ok(!cmds.some((c) => /branding\.json/.test(c)), 'a plain file read — no shell command carries the path');
1864
+ });
1865
+
1866
+ await ta('a sign-in check that fails with agreeing names keeps the plain message (task 1004573)', async () => {
1867
+ const inst = { ...coTenant, hosting_shape: 'cloud-host', domain: 'mercury.example.com' };
1868
+ const deps = {
1869
+ apply: true, privileged: false, log: () => {}, sleep: async () => {},
1870
+ convertManifestCode: async () => ({ clientId: 'Iv1.abcdef0123456789', clientSecret: 'z'.repeat(40), appId: 1, appSlug: 's' }),
1871
+ exec: () => ({ ok: true, stdout: '{"ok":true,"auth_configured":false}' }),
1872
+ readFile: () => JSON.stringify({ envPrefix: U.provisionedEnvPrefix() }),
1873
+ writeFile: () => ({ ok: true }),
1874
+ };
1875
+ const r = await P.placeManifestCreds(inst, { github_code: 'C', attempts: 1 }, deps);
1876
+ assert.equal(r.error, 'the credentials are placed, but the project did not report sign-in ready in time');
1877
+ // an unreadable pack names nothing either
1878
+ const r2 = await P.placeManifestCreds(inst, { github_code: 'C', attempts: 1 }, { ...deps, readFile: () => { throw new Error('ENOENT'); } });
1879
+ assert.equal(r2.error, 'the credentials are placed, but the project did not report sign-in ready in time');
1880
+ });
1881
+
1779
1882
  await ta('a PRE-conversion failure stays retryable — the narrowing spared the retry path (F23)', async () => {
1780
1883
  let claims = 0; const resolves = [];
1781
1884
  const deps = {
@@ -19,7 +19,7 @@ const read = (n) => fs.readFileSync(byName.get(n), 'utf8');
19
19
  const info = (n) => lint.lintSkillFile(read(n), { file: byName.get(n) });
20
20
 
21
21
  // The ruling, as written into the task.
22
- const LISTED_TRIMMED = ['grade-recover', 'collab-review', 'session-handoff', 'builder-claim', 'builder-sequence', 'blocker-solve', 'design', 'goal-create', 'blocker-review', 'builder-reauth', 'read-session-export', 'recall', 'worktree-clean', 'merge-mode', 'builder-setup', 'builder-key', 'bongos-feedback', 'feedback', 'builder-redteam', 'scan-before-install', 'owner-review'];
22
+ const LISTED_TRIMMED = ['grade-recover', 'collab-review', 'session-handoff', 'builder-claim', 'builder-sequence', 'blocker-solve', 'design', 'goal-create', 'blocker-review', 'builder-reauth', 'read-session-export', 'recall', 'worktree-clean', 'merge-mode', 'builder-setup', 'builder-key', 'bongos-feedback', 'builder-redteam', 'scan-before-install', 'owner-review'];
23
23
  const LISTED_AS_IS = ['strand-fix', 'builder-ship', 'builder-start', 'fix-task', 'ask-for-help', 'ship-check', 'status', 'builder-end', 'builder-release', 'builder-stage'];
24
24
  const HIDDEN = ['planning-session', 'goal-close', 'grade-sweep', 'builder-exit', 'builder-backup', 'builder-cost', 'new-project', 'design-sync', 'figma-design-sync', 'tweak'];
25
25
  const ALIASES = ['backlog-review', 'bug-triage', 'idea-triage', 'priority-session', 'goal-uat', 'goal-review', 'grade-audit', 'grader-health'];
@@ -1,39 +0,0 @@
1
- ---
2
- name: feedback
3
- description: >-
4
- Pull the latest Bongos feedback bundle (walkthrough transcript, screenshots) into this session; read-only. Triggers: "/feedback", "pull in my latest feedback", "load my bongos feedback", "I just recorded a walkthrough", "act on my screen recording".
5
- plain: >-
6
- Loads your most recent recorded walkthrough, what you said and the screenshots, into this conversation.
7
- reach-for: >-
8
- Right after you have recorded feedback on the project and want your assistant to act on it.
9
- cost: >-
10
- Free, and it changes nothing. It only reads files on your computer.
11
- ---
12
-
13
- You are pulling in the **latest Bongos feedback bundle** — a self-contained capture the user just recorded with the Bongos desktop app (BV1.R13 / [ADR 0087](../../../docs/adr/0087-bongos-app-architecture-and-handoff-contract.md)). The app turns a spoken screen walkthrough into a model-agnostic `prompt.md` (a timestamped transcript + an ask) plus a handful of screenshots, all under `~/CloudBongos/feedback/`. This skill loads that bundle so you can act on it directly — no copy-paste, no clipboard-image fiddling.
14
-
15
- ## Steps
16
-
17
- 1. **Resolve + print the latest bundle:**
18
-
19
- ```bash
20
- node scripts/gds/feedback-latest.js
21
- ```
22
-
23
- This prints the full `prompt.md` body followed by an `attached frames` list — one **absolute image path** per line. (Add `--json` if you want structured output instead.)
24
-
25
- 2. **If it says "No feedback bundle found"** — tell the user there's nothing to load yet: record a walkthrough in the Bongos app and stop the capture (that writes the bundle), then run `/feedback` again. Stop here.
26
-
27
- 3. **Read every listed frame.** For each absolute path under `attached frames`, call the **Read** tool on it so the screenshots enter your context as images. The paths are real files on disk — Read them directly; do not ask the user to paste anything.
28
-
29
- 4. **Act on the bundle.** Treat the transcript in `prompt.md` as the request and the screenshots as ground truth for what the user was looking at (the timestamps line the words up with the images). Work out what they want changed or fixed and do it. Ask a question only if you are genuinely blocked.
30
-
31
- ## Notes
32
-
33
- - **Read-only + local.** The helper only reads files under `~/CloudBongos/feedback/`; it needs no Bongos session and no network.
34
- - **"Latest" = most recently produced.** The Bongos app updates a `latest` pointer every time a capture is stopped, so this always loads the newest walkthrough. To act on an older one, point the user at the specific dir under `~/CloudBongos/feedback/`.
35
- - **The frames are file paths, never base64** — that's the whole point of the bundle format (it keeps the text prompt tiny and lets your Read tool load the images). See [ADR 0087](../../../docs/adr/0087-bongos-app-architecture-and-handoff-contract.md) for the full bundle contract.
36
-
37
- ## Files this skill touches
38
-
39
- - Runs: `node scripts/gds/feedback-latest.js` (resolves the ADR 0087 `latest` pointer itself — the app source is off-repo, ADR 0098).
@@ -1,28 +0,0 @@
1
- #!/usr/bin/env node
2
- // scripts/bongos-mcp.js — R14 launcher for the Bongos feedback MCP server.
3
- //
4
- // This is the entry point a Claude MCP config points at. The server itself
5
- // lives in bongos-app/src/mcp-server.js (bundled with the app); this thin
6
- // launcher lets it run from a repo checkout / a stable path without depending
7
- // on the app's process.
8
- //
9
- // Register it with Claude Code:
10
- // claude mcp add bongos-feedback -- node /ABSOLUTE/PATH/scripts/bongos-mcp.js
11
- //
12
- // Or in a Claude Desktop / Code MCP config:
13
- // { "mcpServers": { "bongos-feedback": {
14
- // "command": "node",
15
- // "args": ["/ABSOLUTE/PATH/scripts/bongos-mcp.js"] } } }
16
- //
17
- // Then call the get_latest_feedback tool to pull the latest bundle (prompt text
18
- // + screenshot file paths). See bongos-app/README.md for the full contract.
19
-
20
- 'use strict';
21
-
22
- const path = require('node:path');
23
- const { main } = require(path.join(__dirname, '..', 'bongos-app', 'src', 'mcp-server.js'));
24
-
25
- main().catch((err) => {
26
- process.stderr.write(`bongos-mcp: ${err.message}\n`);
27
- process.exit(1);
28
- });
@@ -1,224 +0,0 @@
1
- #!/usr/bin/env node
2
- // scripts/gds/feedback-latest.js — R13 (BV1.R13): the engine behind the
3
- // /feedback Claude Code slash command.
4
- //
5
- // Resolves the LATEST feedback bundle the Bongos app wrote to
6
- // ~/CloudBongos/feedback/ (via the shared `latest` pointer, ADR 0087) and
7
- // prints it for a Claude Code session:
8
- //
9
- // 1. the full prompt.md body (the model-agnostic transcript + ask, R11), and
10
- // 2. the ABSOLUTE path of every attached frame, on its own line, so Claude's
11
- // Read tool can load each image directly.
12
- //
13
- // This sidesteps the CLI's unreliable clipboard-image paste: Claude reads the
14
- // frames from disk by path instead of from a pasted blob.
15
- //
16
- // Usage:
17
- // node scripts/gds/feedback-latest.js # human/agent-readable dump
18
- // node scripts/gds/feedback-latest.js --json # { id, dir, promptMarkdown, imagePaths }
19
- // node scripts/gds/feedback-latest.js --base DIR # override bundle base dir (tests)
20
- //
21
- // Local + read-only: it reads files under your home dir. No GDS session, no
22
- // network — so it works on any machine the Bongos app runs on.
23
- //
24
- // resolveLatest + feedbackBaseDir are ported here from the app's
25
- // bongos-app/src/handoff.js, which no longer lives in this repo (the desktop
26
- // apps publish off-repo, ADR 0098 — task 1004356). The contract is ADR 0087's:
27
- // `latest.json` is authoritative, the `latest/` symlink is a convenience that
28
- // may be absent, and with neither the reader self-heals by scanning for the
29
- // newest bundle dir holding a prompt.md.
30
-
31
- 'use strict';
32
-
33
- const fs = require('node:fs/promises');
34
- const os = require('node:os');
35
- const path = require('node:path');
36
-
37
- const IMAGE_EXT = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif']);
38
-
39
- /** Where the Bongos app writes bundles: ~/CloudBongos/feedback. */
40
- function feedbackBaseDir() {
41
- return path.join(os.homedir(), 'CloudBongos', 'feedback');
42
- }
43
-
44
- async function isBundleDir(dir) {
45
- try {
46
- return (await fs.stat(path.join(dir, 'prompt.md'))).isFile();
47
- } catch {
48
- return false;
49
- }
50
- }
51
-
52
- // The dir `latest.json` names, or null if it is missing, unreadable or stale.
53
- // Accepts the pointer by `dir` (absolute) or by `id` (a child of baseDir).
54
- async function fromPointer(baseDir) {
55
- let ptr;
56
- try {
57
- ptr = JSON.parse(await fs.readFile(path.join(baseDir, 'latest.json'), 'utf8'));
58
- } catch {
59
- return null;
60
- }
61
- if (!ptr || typeof ptr !== 'object') return null;
62
- const candidates = [];
63
- if (typeof ptr.dir === 'string' && ptr.dir) candidates.push(path.resolve(baseDir, ptr.dir));
64
- if (typeof ptr.id === 'string' && ptr.id && !/[\\/]/.test(ptr.id)) candidates.push(path.join(baseDir, ptr.id));
65
- for (const dir of candidates) if (await isBundleDir(dir)) return dir;
66
- return null;
67
- }
68
-
69
- async function fromSymlink(baseDir) {
70
- const link = path.join(baseDir, 'latest');
71
- if (!(await isBundleDir(link))) return null;
72
- try {
73
- return await fs.realpath(link);
74
- } catch {
75
- return null;
76
- }
77
- }
78
-
79
- // Self-heal: the newest child dir (by prompt.md mtime, then name) with a prompt.md.
80
- async function fromScan(baseDir) {
81
- let entries;
82
- try {
83
- entries = await fs.readdir(baseDir, { withFileTypes: true });
84
- } catch {
85
- return null;
86
- }
87
- let best = null;
88
- for (const e of entries) {
89
- if (!e.isDirectory() || e.name === 'latest') continue;
90
- const dir = path.join(baseDir, e.name);
91
- let st;
92
- try {
93
- st = await fs.stat(path.join(dir, 'prompt.md'));
94
- } catch {
95
- continue;
96
- }
97
- if (!st.isFile()) continue;
98
- if (!best || st.mtimeMs > best.mtimeMs || (st.mtimeMs === best.mtimeMs && e.name > best.name)) {
99
- best = { dir, name: e.name, mtimeMs: st.mtimeMs };
100
- }
101
- }
102
- return best && best.dir;
103
- }
104
-
105
- // The bundle's frames as absolute paths: those prompt.md references (in the
106
- // order the walkthrough cites them) that exist, then any other image in the dir.
107
- async function collectImages(dir, promptMarkdown) {
108
- const out = [];
109
- const seen = new Set();
110
- const add = (p) => {
111
- const key = process.platform === 'win32' ? p.toLowerCase() : p;
112
- if (!seen.has(key)) { seen.add(key); out.push(p); }
113
- };
114
- for (const m of promptMarkdown.matchAll(/!\[[^\]]*\]\(\s*<?([^)>]+?)>?\s*\)/g)) {
115
- const p = path.resolve(dir, m[1]);
116
- if (!IMAGE_EXT.has(path.extname(p).toLowerCase())) continue;
117
- try {
118
- if ((await fs.stat(p)).isFile()) add(p);
119
- } catch { /* referenced but absent — skip */ }
120
- }
121
- const walk = async (d, depth) => {
122
- let entries;
123
- try {
124
- entries = await fs.readdir(d, { withFileTypes: true });
125
- } catch {
126
- return;
127
- }
128
- entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
129
- for (const e of entries) {
130
- const p = path.join(d, e.name);
131
- if (e.isDirectory() && depth > 0) await walk(p, depth - 1);
132
- else if (e.isFile() && IMAGE_EXT.has(path.extname(e.name).toLowerCase())) add(p);
133
- }
134
- };
135
- await walk(dir, 1);
136
- return out;
137
- }
138
-
139
- /**
140
- * The newest feedback bundle under baseDir, or null if there is none.
141
- * @returns {Promise<null|{id:string,dir:string,promptPath:string,promptMarkdown:string,imagePaths:string[]}>}
142
- */
143
- async function resolveLatest(baseDir = feedbackBaseDir()) {
144
- const dir = (await fromPointer(baseDir)) || (await fromSymlink(baseDir)) || (await fromScan(baseDir));
145
- if (!dir) return null;
146
- const promptPath = path.join(dir, 'prompt.md');
147
- const promptMarkdown = await fs.readFile(promptPath, 'utf8');
148
- return {
149
- id: path.basename(dir),
150
- dir,
151
- promptPath,
152
- promptMarkdown,
153
- imagePaths: await collectImages(dir, promptMarkdown),
154
- };
155
- }
156
-
157
- function parseArgs(argv) {
158
- const out = { json: false, base: undefined };
159
- for (let i = 0; i < argv.length; i++) {
160
- const a = argv[i];
161
- if (a === '--json') out.json = true;
162
- else if (a === '--base') out.base = argv[++i];
163
- else if (a.startsWith('--base=')) out.base = a.slice('--base='.length);
164
- }
165
- return out;
166
- }
167
-
168
- async function main() {
169
- const args = parseArgs(process.argv.slice(2));
170
- const baseDir = args.base || feedbackBaseDir();
171
-
172
- const latest = await resolveLatest(baseDir);
173
-
174
- if (!latest) {
175
- if (args.json) {
176
- process.stdout.write(`${JSON.stringify({ ok: false, reason: 'no_bundle', baseDir })}\n`);
177
- } else {
178
- process.stdout.write(
179
- `No feedback bundle found under ${baseDir}.\n`
180
- + 'Record a walkthrough in the Bongos app and stop the capture — that '
181
- + 'writes a bundle here — then run /feedback again.\n',
182
- );
183
- }
184
- return;
185
- }
186
-
187
- if (args.json) {
188
- process.stdout.write(`${JSON.stringify({
189
- ok: true,
190
- id: latest.id,
191
- dir: latest.dir,
192
- promptPath: latest.promptPath,
193
- imagePaths: latest.imagePaths,
194
- promptMarkdown: latest.promptMarkdown,
195
- }, null, 2)}\n`);
196
- return;
197
- }
198
-
199
- const lines = [];
200
- lines.push(`# Latest Bongos feedback bundle — ${latest.id}`);
201
- lines.push(`# dir: ${latest.dir}`);
202
- lines.push('');
203
- lines.push('---- prompt.md ----');
204
- lines.push('');
205
- lines.push(latest.promptMarkdown.trimEnd());
206
- lines.push('');
207
- lines.push('---- attached frames (Read each path to see the screenshots) ----');
208
- if (latest.imagePaths.length === 0) {
209
- lines.push('(none — this session had no salient frames or markup)');
210
- } else {
211
- for (const p of latest.imagePaths) lines.push(p);
212
- }
213
- lines.push('');
214
- process.stdout.write(`${lines.join('\n')}\n`);
215
- }
216
-
217
- module.exports = { resolveLatest, feedbackBaseDir };
218
-
219
- if (require.main === module) {
220
- main().catch((err) => {
221
- process.stderr.write(`feedback-latest: ${err.message}\n`);
222
- process.exitCode = 1;
223
- });
224
- }