@bongos/core 1.21.76 → 1.21.78

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.
@@ -31,7 +31,7 @@
31
31
  // below reads exactly as before; the single copy lives in task-lib.js.
32
32
  const {
33
33
  priorityRank, touchesConflict, rankTagHtml, deHashRefs,
34
- creditsPerMinute, buildPromptForTask, friendlyBatchReason,
34
+ creditsPerMinute, buildPromptForTask, buildPlanPromptForGoal, friendlyBatchReason,
35
35
  } = window.OTBTaskLib;
36
36
  // Shared component kit (hall-kit.js, task 2166): pager, filter bar, tabs,
37
37
  // empty/skeleton states — adopted here by task 2171.
@@ -404,6 +404,18 @@
404
404
  } catch (_e) { return false; }
405
405
  }
406
406
  }
407
+ // task 1004587 (ADR 0366 D4) — the optional "Plan with Claude" hand-off; every planning form works without it.
408
+ async function planWithClaude() {
409
+ const d = state.details.get(String(state.current));
410
+ if (!d) return;
411
+ // The spec task is found in the goal's task list, so wait for it: copying early would hand back the fallback prompt for a goal that has a spec task.
412
+ if (!state.workLoaded) return toast("Still loading this goal's tasks. Try again in a moment.");
413
+ const g = d.goal;
414
+ const ok = await copyText(buildPlanPromptForGoal(g, state.work.todo));
415
+ toast(ok ? 'Prompt copied. Paste it into a Claude Code session to plan this goal.' : 'Could not copy to the clipboard. Try again.');
416
+ }
417
+ // The button is drawn by renderActions (which re-draws it), so the click is delegated.
418
+ document.addEventListener('click', (e) => { if (e.target.closest && e.target.closest('#goal-plan-btn')) planWithClaude(); });
407
419
  async function copyPromptForTask(taskId, btn) {
408
420
  const t = state.work.todo.find((x) => String(x.id) === String(taskId));
409
421
  if (!t) return;
@@ -962,7 +962,7 @@
962
962
  if (canManage && g.status === 'open') {
963
963
  parts.push('<button class="btn-ghost" id="goal-archive-btn" type="button" title="Retire this goal — you will be asked what happens to any unfinished work">Archive goal</button>');
964
964
  }
965
- if (g.status === 'open') {
965
+ if (g.status === 'open') { parts.push('<button class="btn-ghost" id="goal-plan-btn" type="button" title="Copies a prompt to paste into a Claude Code session">Plan with Claude</button>'); // task 1004587
966
966
  if (joined) {
967
967
  parts.push('<button class="btn-ghost" id="goal-leave-btn" type="button">Leave goal</button>');
968
968
  } else if ((g.visibility === 'private' || g.scope_protected) && !isArchon()) {
@@ -497,7 +497,11 @@ body::after {
497
497
  z-index: var(--z-sidebar);
498
498
  display: flex;
499
499
  flex-direction: column;
500
- background: var(--glass);
500
+ /* Glass laid over a solid ground: the rail opens OVER the top bar and the
501
+ content, and --glass in light is a 5% tint with no blur, so bare glass let
502
+ the page title read straight through the brand ("Builders' Hall" on top of
503
+ "profile"). Same fix as the board's bar. */
504
+ background: linear-gradient(var(--glass), var(--glass)), var(--bg);
501
505
  border-right: 1px solid var(--glass-edge);
502
506
  backdrop-filter: blur(var(--glass-blur));
503
507
  -webkit-backdrop-filter: blur(var(--glass-blur));
@@ -109,10 +109,15 @@
109
109
  return c / m;
110
110
  }
111
111
 
112
+ // Where the hall lives: the builders. subdomain's own origin, else the apex /builders tree.
113
+ function hallBaseUrl() {
114
+ return location.hostname.startsWith('builders.') ? location.origin : `${location.origin}/builders`;
115
+ }
116
+
112
117
  // The paste-into-your-agent prompt for one task.
113
118
  function buildPromptForTask(t) {
114
119
  const desc = deHashRefs((t.description || '').trim());
115
- const hallBase = location.hostname.startsWith('builders.') ? location.origin : `${location.origin}/builders`;
120
+ const hallBase = hallBaseUrl();
116
121
  const hallUrl = `${hallBase}#/task/${t.id}`;
117
122
  return [
118
123
  `I am working on Bongos task ${t.id} (${t.title}) — ${hallUrl}`,
@@ -123,6 +128,20 @@
123
128
  ].join('\n');
124
129
  }
125
130
 
131
+ // task 1004587 (ADR 0366 D4) — the "Plan with Claude" prompt for one goal. When the
132
+ // goal has an unshipped spec task (source_ref goal-<id>-spec, made by the hall form)
133
+ // the prompt is that task's; otherwise it asks a session to run /goal-create against
134
+ // the goal. `todoTasks` is the goal's unshipped tasks. An extra, never a requirement.
135
+ function buildPlanPromptForGoal(goal, todoTasks) {
136
+ const spec = (todoTasks || []).find((t) => t.source_ref === `goal-${goal.id}-spec`);
137
+ if (spec) return buildPromptForTask(spec);
138
+ const hallBase = hallBaseUrl();
139
+ return [
140
+ `I want to plan Bongos goal ${goal.id} (${deHashRefs(goal.title || '')}) — ${hallBase}#/goal/${goal.id}`,
141
+ `Run /goal-create against this existing goal to add its seed tasks. Do not create a new goal.`,
142
+ ].join('\n');
143
+ }
144
+
126
145
  // Turn a batch-claim refusal into a sentence. THE DEFAULT MATTERS: an unmapped
127
146
  // code falls through to the raw string, which is how the missing MODULE_CONFLICT
128
147
  // case leaked a machine code into the Goals page UI. Adding a server-side reason
@@ -195,7 +214,7 @@
195
214
  focusVersionDefault,
196
215
  msOf, taskActivityMs, STALE_CHOICES, isStale,
197
216
  priorityRank, touchesConflict, conflictPeers, conflictChipHtml, rankTagHtml, deHashRefs,
198
- creditsPerMinute, buildPromptForTask, friendlyBatchReason,
217
+ creditsPerMinute, buildPromptForTask, buildPlanPromptForGoal, friendlyBatchReason,
199
218
  };
200
219
  if (typeof window !== 'undefined') window.OTBTaskLib = api;
201
220
  if (typeof module !== 'undefined' && module.exports) module.exports = api;
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.76",
3
+ "version": "1.21.78",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.76",
9
+ "version": "1.21.78",
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.76",
3
+ "version": "1.21.78",
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",
@@ -9266,5 +9266,17 @@
9266
9266
  "id": "1004499",
9267
9267
  "text": "Artists can now review the public website while looking at it. A signed-in builder on cloudbongos.com's public pages (home, projects, account, guilds, invitations, privacy, terms, contact) sees a Review button in the top bar,"
9268
9268
  }
9269
+ ],
9270
+ "1.21.77": [
9271
+ {
9272
+ "id": "1004638",
9273
+ "text": "The hall sidebar now has a solid background, so the page title no longer shows through the Builders' Hall name in light mode."
9274
+ }
9275
+ ],
9276
+ "1.21.78": [
9277
+ {
9278
+ "id": "1004587",
9279
+ "text": "Goal pages now have a Plan with Claude button that copies a ready-made planning prompt."
9280
+ }
9269
9281
  ]
9270
9282
  }
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.76'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.78'; // 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');
@@ -0,0 +1,41 @@
1
+ // tests/goal_plan_with_claude.mjs — the goal page's "Plan with Claude" hand-off (task 1004587, ADR 0366 D4).
2
+ //
3
+ // Run: node --test tests/goal_plan_with_claude.mjs
4
+
5
+ import assert from 'node:assert/strict';
6
+ import { test } from 'node:test';
7
+ import { readFileSync } from 'node:fs';
8
+ import { createRequire } from 'node:module';
9
+
10
+ const require = createRequire(import.meta.url);
11
+ globalThis.location = { hostname: 'builders.example.com', origin: 'https://builders.example.com' };
12
+ const { buildPlanPromptForGoal, buildPromptForTask } = require('../modules/hall-ui/public/task-lib.js');
13
+ const src = (rel) => readFileSync(new URL('../' + rel, import.meta.url), 'utf8');
14
+
15
+ const goal = { id: 42, title: 'Ship the thing' };
16
+ const spec = { id: 900, title: 'Spec goal 42: its scope', description: 'Do the interview.', source_ref: 'goal-42-spec' };
17
+ const other = { id: 901, title: 'Other', description: 'x', source_ref: null };
18
+
19
+ test('with an unshipped spec task the prompt is that task\'s', () => {
20
+ assert.equal(buildPlanPromptForGoal(goal, [other, spec]), buildPromptForTask(spec));
21
+ });
22
+
23
+ test('another goal\'s spec task is not this goal\'s', () => {
24
+ const p = buildPlanPromptForGoal(goal, [{ ...spec, source_ref: 'goal-43-spec' }]);
25
+ assert.match(p, /\/goal-create/);
26
+ });
27
+
28
+ test('without a spec task the prompt asks for /goal-create against the goal', () => {
29
+ const p = buildPlanPromptForGoal(goal, [other]);
30
+ assert.match(p, /\/goal-create/);
31
+ assert.match(p, /goal 42/);
32
+ assert.match(p, /#\/goal\/42/);
33
+ assert.equal(buildPlanPromptForGoal(goal, undefined), p);
34
+ });
35
+
36
+ test('the button is on the goal page and copies the goal prompt', () => {
37
+ const r = src('modules/hall-ui/public/goals-render.js');
38
+ const pg = src('modules/hall-ui/public/goals-page.js');
39
+ assert.match(r, /id="goal-plan-btn"/);
40
+ assert.ok(pg.includes('buildPlanPromptForGoal(g, state.work.todo)'));
41
+ });