@bongos/core 1.21.46 → 1.21.48
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 +37 -52
- package/.claude/skills/bongos-feedback/SKILL.md +5 -4
- package/docs/adr/0087-bongos-app-architecture-and-handoff-contract.md +2 -2
- package/docs/file-map.md +0 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +242 -243
- package/modules/hall-ui/public/commands-lib.js +1 -1
- package/modules/hall-ui/public/street-card.css +8 -2
- package/modules/hall-ui/public/street-desk.js +78 -9
- package/modules/hall-ui/public/street.css +4 -1
- package/modules/hall-ui/public/street.js +9 -6
- package/modules/hall-ui/records/governor-street.md +14 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +16 -0
- package/scripts/gds/pack-env-prefix.js +73 -0
- package/scripts/gds/provision-repo.js +13 -0
- package/scripts/gds/provision-units.js +6 -17
- package/scripts/gds/provision.js +8 -2
- package/src/instance-config.js +28 -1
- package/src/module-api.js +1 -1
- package/tests/hall_street_desk.mjs +64 -2
- package/tests/instance_config.mjs +53 -0
- package/tests/instance_feedback.mjs +12 -0
- package/tests/provision.mjs +103 -0
- package/tests/skill_menu_ruling.mjs +1 -1
- package/.claude/skills/feedback/SKILL.md +0 -39
- package/scripts/bongos-mcp.js +0 -28
- package/scripts/gds/feedback-latest.js +0 -224
- package/tests/feedback_latest.mjs +0 -122
|
@@ -327,6 +327,7 @@ test('the page: the desk acts in place — the row clears, the floor lights with
|
|
|
327
327
|
assert.deepEqual(p.sent, [{ method: 'PATCH', url: '/api/bongos/access-requests/11', body: { status: 'invited' } }], 'the card\'s own route, nothing else');
|
|
328
328
|
assert.match(p.byId.get('streetToast').textContent, /Ana Ruiz is invited in/);
|
|
329
329
|
assert.equal(p.byId.get('streetToast').hidden, false);
|
|
330
|
+
assert.match(p.byId.get('streetToast').className, /street-toast--desk/, "the desk's own toast, as on the board (task 1004562)");
|
|
330
331
|
assert.equal(p.byId.get('streetTodayN').textContent, '1 decided');
|
|
331
332
|
assert.equal(p.byId.get('streetTodayPips').children.filter((x) => x.className.includes('--on')).length, 1, 'one pip per decision');
|
|
332
333
|
assert.equal(JSON.parse(p.store.get('gov-desk-today')).n, 1, 'the day keeps its count');
|
|
@@ -624,6 +625,12 @@ test('Release: the core update is checked first; take is offered only after a pa
|
|
|
624
625
|
assert.equal(C.request(passed.card, 'take', {}), null, 'no version chip, no move');
|
|
625
626
|
assert.deepEqual(coreRoute.coreDeskRows({ ...PREVIEW, open_intent: { state: 'running' } }), [], 'a run in progress offers nothing');
|
|
626
627
|
assert.deepEqual(coreRoute.coreDeskRows({ ...PREVIEW, behind: false }), []);
|
|
628
|
+
// the stand-in keeps what it stands for: the core update needs you, so the
|
|
629
|
+
// desk says so in orange, as the floor's sign and the street's count do (task 1004562)
|
|
630
|
+
const desk = D.deskModel({ key: 'release', floor: 3, docket: moreDesks({ preflight: null }), goals: GOALS, now: NOW, viewerId: '95' });
|
|
631
|
+
assert.equal(desk.hot, true);
|
|
632
|
+
assert.match(desk.status, /^1 update/);
|
|
633
|
+
assert.deepEqual(D.breaks(desk), []);
|
|
627
634
|
});
|
|
628
635
|
|
|
629
636
|
test('Rules: the rot timer as day chips, work categories as a link, modules on in one tap and off with a yes', () => {
|
|
@@ -747,10 +754,11 @@ test('Ideas to ratify: a promise ratifies in one tap; a carried Full Idea become
|
|
|
747
754
|
const dk = planningDocket();
|
|
748
755
|
const rows = D.rowsFor('planning', 1, dk, { now: NOW, goals: GOALS });
|
|
749
756
|
const promise = rows.find((r) => r.name === 'Calm mode promise');
|
|
750
|
-
assert.deepEqual(promise.actions.map((a) => a.label), ['ratify', '
|
|
757
|
+
assert.deepEqual(promise.actions.map((a) => a.label), ['ratify'], 'a row that acts here keeps its detail in place: no link out (task 1004562)');
|
|
758
|
+
assert.ok(promise.facts.length >= 1, 'its detail opens in place');
|
|
751
759
|
assert.deepEqual(j(C.request(promise.card, 'ratify')), { method: 'POST', url: '/api/bongos/inbox/5/ratify', body: { notes_md: 'Ratified on the floor desk.' } });
|
|
752
760
|
const full = rows.find((r) => r.name === 'Objection goes back');
|
|
753
|
-
assert.deepEqual(full.actions.map((a) => a.label), ['make goal', '
|
|
761
|
+
assert.deepEqual(full.actions.map((a) => a.label), ['make goal'], 'no link out beside an act');
|
|
754
762
|
assert.deepEqual(full.choices.ratify_goal.options.map((o) => o.label), ['V2', 'V3'], 'the target version is a chip');
|
|
755
763
|
assert.deepEqual(j(C.request(full.card, 'ratify_goal', full.choices.ratify_goal.options[1].input)), { method: 'POST', url: '/api/bongos/inbox/6/ratify-goal', body: { target_version: 'V3', notes_md: 'Ratified on the floor desk.' } });
|
|
756
764
|
assert.ok(!rows.some((r) => r.name === 'Still sitting'), 'a Full Idea the board has not carried is not ratified from here');
|
|
@@ -832,3 +840,57 @@ test('the page: one tap on its option decides a blocker, its options standing op
|
|
|
832
840
|
assert.deepEqual(p.sent, [{ method: 'POST', url: '/api/bongos/blockers/12/resolve', body: { resolution_note: 'Decided on the floor desk: Paddle.' } }]);
|
|
833
841
|
assert.match(p.byId.get('streetToast').textContent, /Decided/);
|
|
834
842
|
});
|
|
843
|
+
|
|
844
|
+
// ── the live walkthrough's differences (task 1004562) ─────────────────────────
|
|
845
|
+
function floorRow(key, count, line) {
|
|
846
|
+
return { key, building: key.split('.')[0], status: 'live', count, line, lines: [], hot: false, reason: null };
|
|
847
|
+
}
|
|
848
|
+
test('walkthrough: a quiet floor\'s tile says what its number counts, in two words that read as a label', () => {
|
|
849
|
+
const dk = docket([], { history: [], floors: [
|
|
850
|
+
floorRow('people.testers', '3', 'Lu, Bo and Ines'),
|
|
851
|
+
floorRow('people.council', '5', '5 seats · no term yet'),
|
|
852
|
+
floorRow('people.newcomers', '2', '2 newcomer tasks being built'),
|
|
853
|
+
floorRow('release.live_month', '1', '1 piece of work went live'),
|
|
854
|
+
floorRow('review.signed_off', '2', '2 signed off this month'),
|
|
855
|
+
] });
|
|
856
|
+
const tile = (key, floor) => D.deskModel({ key, floor, docket: dk, now: NOW }).tiles.map((t) => t.v + ' ' + t.l);
|
|
857
|
+
assert.deepEqual(tile('people', 1), ['3 testers'], 'a line that is not a count names nothing: the floor does');
|
|
858
|
+
assert.deepEqual(tile('people', 0), ['5 seats'], 'the line\'s own noun when it follows the same number');
|
|
859
|
+
assert.deepEqual(tile('people', 3), ['2 newcomers'], 'a long line: the floor\'s name');
|
|
860
|
+
assert.deepEqual(tile('release', 4), ['1 went live'], 'a long line under a long name: its last two words');
|
|
861
|
+
assert.deepEqual(tile('review', 2), ['2 signed off']);
|
|
862
|
+
});
|
|
863
|
+
test('walkthrough: a cleared floor shows no count of the work just done, and says its own clear line', () => {
|
|
864
|
+
const m = matter({ id: 'removal_proposals:61', source: 'removal_proposals', building: 'disputes', state: 'needs', title: 'Old search page', ref: { id: 61 } });
|
|
865
|
+
const dk = docket([m], { history: [], floors: [floorRow('disputes.removals', '2', '1 needs you')] });
|
|
866
|
+
const desk = D.deskModel({ key: 'disputes', floor: 1, docket: dk, now: NOW, done: { 'removal_proposals:61': true }, cleared: true, today: 2 });
|
|
867
|
+
assert.equal(desk.cleared, true);
|
|
868
|
+
assert.deepEqual(desk.tiles, [], 'no "2 needs you" tile on a floor you just cleared');
|
|
869
|
+
assert.deepEqual([desk.status, desk.clearLine, desk.clearSub], ['floor clear', 'No removals asked', 'lit up · 2 decided today']);
|
|
870
|
+
assert.deepEqual(D.breaks(desk), []);
|
|
871
|
+
// with a tool left on the floor (the invite), the status does not repeat the stale count
|
|
872
|
+
const join = matter({ id: 'access_requests:7', source: 'access_requests', building: 'people', state: 'needs', proposer: { name: 'Builder 4' }, ref: { id: 7 } });
|
|
873
|
+
const invite = { key: 'people.invite', status: 'live', rows: [{ id: 'people.invite:new', desk: 'people.invite', building: 'people', name: 'Invite someone', fact: 'by their GitHub name', facts: [], needs: false, tool: true, ref: { type: 'invite', id: 'new' }, acts: [{ key: 'invite', field: 'login' }] }] };
|
|
874
|
+
const left = D.deskModel({ key: 'people', floor: 4, docket: docket([join], { history: [], desks: [invite], floors: [floorRow('people.join', '1', '1 needs you')] }), now: NOW, done: { 'access_requests:7': true } });
|
|
875
|
+
assert.deepEqual(left.rows.map((r) => r.name), ['Invite someone']);
|
|
876
|
+
assert.equal(left.status, 'nothing needs you');
|
|
877
|
+
assert.equal(left.hot, false);
|
|
878
|
+
});
|
|
879
|
+
test('walkthrough: a count agrees with its noun, in the status and on the tile', () => {
|
|
880
|
+
const votes = [1, 2].map((n) => matter({ id: 'board:' + n, source: 'board', building: 'charter', state: 'needs', title: 'Vote ' + n }));
|
|
881
|
+
const two = D.deskModel({ key: 'charter', floor: 0, docket: docket(votes, { history: [] }), now: NOW });
|
|
882
|
+
assert.match(two.status, /^2 open votes · oldest/);
|
|
883
|
+
assert.deepEqual(two.tiles.map((t) => t.v + ' ' + t.l), ['2 open votes']);
|
|
884
|
+
const one = D.deskModel({ key: 'charter', floor: 0, docket: docket(votes.slice(0, 1), { history: [] }), now: NOW });
|
|
885
|
+
assert.match(one.status, /^1 open vote · oldest/);
|
|
886
|
+
});
|
|
887
|
+
test('walkthrough: a record item keeps its age, and what you did keeps its "now"', () => {
|
|
888
|
+
const hist = [
|
|
889
|
+
{ id: 'a', building: 'release', title: 'The new sign-up', outcome: 'live', at: '2026-10-01T12:00:00.000Z' },
|
|
890
|
+
{ id: 'b', building: 'release', title: 'Mira holds the Treasurer seat', outcome: 'granted', at: '2026-09-12T12:00:00.000Z' },
|
|
891
|
+
{ id: 'c', building: 'release', title: 'Today it went out', outcome: '', at: '2026-10-03T09:00:00.000Z' },
|
|
892
|
+
];
|
|
893
|
+
const desk = D.deskModel({ key: 'release', floor: 2, docket: docket([], { history: hist }), now: NOW, mine: ['voted yes A returned idea carries it back'] });
|
|
894
|
+
assert.deepEqual(desk.record.map((x) => x.text), ['voted yes A returned id… · now', 'The new sign-up · live · 2d', 'Mira holds the Treasurer… · 3w', 'Today it went out · today']);
|
|
895
|
+
assert.ok(desk.record.every((x) => x.text.length <= D.LIMITS.recordLen));
|
|
896
|
+
});
|
|
@@ -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';
|
package/tests/provision.mjs
CHANGED
|
@@ -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', '
|
|
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).
|
package/scripts/bongos-mcp.js
DELETED
|
@@ -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
|
-
}
|