@bongos/core 1.21.86 → 1.21.87
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 +24 -14
- package/docs/design/project-startup-direction.md +58 -1
- package/docs/module-api-changelog.md +2 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +10 -0
- package/scripts/gds/start-goals.js +102 -0
- package/scripts/gds/start.js +47 -11
- package/src/module-api.js +1 -1
- package/tests/start_goal_first.mjs +90 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.21.
|
|
6
|
-
"core_contract": "1.21.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.21.87",
|
|
6
|
+
"core_contract": "1.21.87",
|
|
7
|
+
"source_commit": "3a000da66e4b7ed910b59eb0e1843e81a7e3f7fd",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-10-
|
|
9
|
+
"built_at": "2026-10-09T05:06:35.756Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 590,
|
|
13
13
|
"agent_docs_stubbed": 28,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 3000,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 3619,
|
|
20
|
+
"tree_sha256": "94cb33a270055b9c3e008a1a26ad2e66038572aae76148fccb2f9b3bf2bbecf9",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2432,7 +2432,7 @@
|
|
|
2432
2432
|
{
|
|
2433
2433
|
"path": "docs/design/project-startup-direction.md",
|
|
2434
2434
|
"mode": "0000644",
|
|
2435
|
-
"sha256": "
|
|
2435
|
+
"sha256": "8cac194bfa4e7e0cfa6785ff72fd141980eb2abed6497ce56e42585c2f4a918c"
|
|
2436
2436
|
},
|
|
2437
2437
|
{
|
|
2438
2438
|
"path": "docs/design/projects-hub-direction-v2.md",
|
|
@@ -2862,7 +2862,7 @@
|
|
|
2862
2862
|
{
|
|
2863
2863
|
"path": "docs/module-api-changelog.md",
|
|
2864
2864
|
"mode": "0000644",
|
|
2865
|
-
"sha256": "
|
|
2865
|
+
"sha256": "39126044930a9015689ba736100232d99d1d13d43d723ec95a15dbe505c4390d"
|
|
2866
2866
|
},
|
|
2867
2867
|
{
|
|
2868
2868
|
"path": "docs/modules-contract.md",
|
|
@@ -10097,12 +10097,12 @@
|
|
|
10097
10097
|
{
|
|
10098
10098
|
"path": "package-lock.json",
|
|
10099
10099
|
"mode": "0000644",
|
|
10100
|
-
"sha256": "
|
|
10100
|
+
"sha256": "6953a80922eac656bc533d17e921f49228e939cacb64da8e56d1a90fa5f358db"
|
|
10101
10101
|
},
|
|
10102
10102
|
{
|
|
10103
10103
|
"path": "package.json",
|
|
10104
10104
|
"mode": "0000644",
|
|
10105
|
-
"sha256": "
|
|
10105
|
+
"sha256": "be88b7b575574dc07cbf071c639c419a4365cd89e7ed0b7bba058de98982b443"
|
|
10106
10106
|
},
|
|
10107
10107
|
{
|
|
10108
10108
|
"path": "public-docs/index.html",
|
|
@@ -10122,7 +10122,7 @@
|
|
|
10122
10122
|
{
|
|
10123
10123
|
"path": "release-notes.json",
|
|
10124
10124
|
"mode": "0000644",
|
|
10125
|
-
"sha256": "
|
|
10125
|
+
"sha256": "ba7ae503bc8f34ef8244b2fb989462283e4128b6ea03ac066d050acfb35edbc5"
|
|
10126
10126
|
},
|
|
10127
10127
|
{
|
|
10128
10128
|
"path": "scripts/cron/style_guide_audit.sh",
|
|
@@ -11734,10 +11734,15 @@
|
|
|
11734
11734
|
"mode": "0000644",
|
|
11735
11735
|
"sha256": "9f6ce80e5325f4cfe68cfa6cb82bec52d594af7e461b6734724819b23cbb0d37"
|
|
11736
11736
|
},
|
|
11737
|
+
{
|
|
11738
|
+
"path": "scripts/gds/start-goals.js",
|
|
11739
|
+
"mode": "0000644",
|
|
11740
|
+
"sha256": "8c4e1f1c7871c82b0fefd5402c93888831543dcd776a1e761e7b1238b3afafaa"
|
|
11741
|
+
},
|
|
11737
11742
|
{
|
|
11738
11743
|
"path": "scripts/gds/start.js",
|
|
11739
11744
|
"mode": "0000644",
|
|
11740
|
-
"sha256": "
|
|
11745
|
+
"sha256": "181f07b8d62f03ae001d08dd5e5b15a65b33f512fb548e55401b97f7f484f31f"
|
|
11741
11746
|
},
|
|
11742
11747
|
{
|
|
11743
11748
|
"path": "scripts/gds/status.js",
|
|
@@ -12372,7 +12377,7 @@
|
|
|
12372
12377
|
{
|
|
12373
12378
|
"path": "src/module-api.js",
|
|
12374
12379
|
"mode": "0000644",
|
|
12375
|
-
"sha256": "
|
|
12380
|
+
"sha256": "3ba47cbdc5c1c0dc86af2cf1302a6ffc3c9966a7646f88bd2a004f5dd67b0ded"
|
|
12376
12381
|
},
|
|
12377
12382
|
{
|
|
12378
12383
|
"path": "src/module-loader/catalog.js",
|
|
@@ -17529,6 +17534,11 @@
|
|
|
17529
17534
|
"mode": "0000644",
|
|
17530
17535
|
"sha256": "39e62a5fc50113e719cdaacf15e6fd054f38f8703fc64137eddee7901b374f16"
|
|
17531
17536
|
},
|
|
17537
|
+
{
|
|
17538
|
+
"path": "tests/start_goal_first.mjs",
|
|
17539
|
+
"mode": "0000644",
|
|
17540
|
+
"sha256": "a3addba1547bf0a9ce95f5d6c2ea676f63495669154e2f4c161cb8cc543b30bf"
|
|
17541
|
+
},
|
|
17532
17542
|
{
|
|
17533
17543
|
"path": "tests/start_inbox_count.mjs",
|
|
17534
17544
|
"mode": "0000644",
|
|
@@ -10,7 +10,64 @@ This document is what every build task in goal 1000121 cites. Name a screen by i
|
|
|
10
10
|
- **what the new design ADDS**;
|
|
11
11
|
- the decisions that govern both.
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## One-page flow — supersedes the blended six screens (owner, 2026-10-08)
|
|
14
|
+
|
|
15
|
+
> *Summary wins, do it in the night-room look, if it's day time let's make a day view, and likewise, same for the artist page.* Then: *approved.* — the owner, 2026-10-08.
|
|
16
|
+
|
|
17
|
+
The owner found the six-screen flow still overcomplicated, and the GitHub steps after Create still failing. The mockup in [`mocks/day-night-rooms/`](mocks/day-night-rooms/README.md) is now the design of record for **every hub-side creation screen and for the artist studio's room**. Where it disagrees with the blended flow below, the mockup wins. Genesis (part 2) and the blend rule are unchanged.
|
|
18
|
+
|
|
19
|
+
### The owner's decisions
|
|
20
|
+
|
|
21
|
+
1. **One page, three parts.** Sign-in stays the gate in front. Then one page:
|
|
22
|
+
- **01 Your project:** name, kind, the optional sentence, and the four quick checks (country, anyone under 18, where builders will be, who's behind it). The screening stop still fires before anything is created.
|
|
23
|
+
- **02 How it's run:** a summary to skim. Each row (code, address, team, extras, rewards, hosting, we run, look) has a **Change** link that opens the real control in place.
|
|
24
|
+
- **03 Connect GitHub:** opens once the project exists, below **Create my project**.
|
|
25
|
+
2. **The summary wins.** No row needs confirming and Create is not gated on the rows. This **replaces** the blended flow's "the review must not be skippable" rule. The terms stay a required checkbox above Create, and the terms version stamp still records the acceptance.
|
|
26
|
+
3. **GitHub in one trip (option A).** One click: approve the project's app on GitHub, go straight on to installing it on the repo, and land back on the page. A checklist ticks itself from what the server has verified (approve the app · install it on your repo · builders can sign in · command-line sign-in is on). Coming back works from any tab or device. The founder chooses a personal or an organisation account. A failure says what stopped, and "Pick up where it stopped" resumes without making a second app.
|
|
27
|
+
4. **Day room and night room, by the viewer's own clock.** Day from 07:00 to 18:59 local time, night otherwise. The day room is the same drawing in daylight: a sunny sky with clouds, a sun for the moon, lighter towers with unlit windows, a warm wall and floor, the lamp off and a patch of sun on the floor. Its palette is the mockup's `[data-sky="day"]` tokens. **The artist studio gets the same day room**, and its greeting follows the time of day.
|
|
28
|
+
5. **The demo is a small link** under the heading, not its own screen.
|
|
29
|
+
|
|
30
|
+
### Nothing the live wizard does is lost
|
|
31
|
+
|
|
32
|
+
This table replaces the blended flow's table below for every hub-side capability. The mockup draws the simple path; each row's capability is a reveal or sits behind a summary row's **Change**.
|
|
33
|
+
|
|
34
|
+
| Today | In the one-page flow |
|
|
35
|
+
|---|---|
|
|
36
|
+
| Q1 new or existing, and what already exists | Part 02's Code row: "A repo I already have" reveals what already exists and the adopt path. |
|
|
37
|
+
| Adopt: GitHub repo picker, manual fallback, conflict pre-flight, backlog import | Behind the Code row's Change, unchanged. |
|
|
38
|
+
| Q2 geography | Part 01's "Where are you?", with state or province revealed beside the country for the countries that need it, and "Where will builders be?". |
|
|
39
|
+
| Q3 under-18s and the age tiers | Part 01; "Yes" reveals the tiers and the guardian rules in place. |
|
|
40
|
+
| Q4 entity, new intent or existing legal type | Part 01's "Who's behind it?"; a company or nonprofit reveals the full entity list. |
|
|
41
|
+
| The screening stop | Unchanged: an answer the platform cannot serve stops the page before Create, with the reason in place. |
|
|
42
|
+
| Q5 one or many, count, growth guess | Part 02's Team row; Change opens the count, the growth guess and how people join. |
|
|
43
|
+
| Q6 rewards | Part 02's Rewards row (pick several). |
|
|
44
|
+
| Q7 framing A–D and the hybrid suite | Part 02's **We run** row ("Everything, for you" by default); "Some of it" reveals the service checklist. |
|
|
45
|
+
| Auto-fill from my account | Kept, as an offer at the top of part 01 when the account has a last project. |
|
|
46
|
+
| Resume a saved draft | Kept, as an offer at the top of the page (there is no fork screen any more). |
|
|
47
|
+
| Type and About | Part 01's Kind and "In a sentence". |
|
|
48
|
+
| Modules picker and search | Part 02's Extras row; Change opens the searchable picker. |
|
|
49
|
+
| Address: handle, own domain, no address | Part 02's Address row (PS19: shown on every path, the demo included). |
|
|
50
|
+
| App: Render key check, workspace, runtime, commands, plan, database | Part 02's Hosting row; Render opens today's settings in place. |
|
|
51
|
+
| Look, logo, night tint | Part 02's Look row; Change opens the look picker, the logo and the night tint. |
|
|
52
|
+
| Terms acknowledgement | The required checkbox above Create; the terms version stamp still records it. |
|
|
53
|
+
| Review | Part 02 itself, plus the one-line recap beside Create. Nothing is gated on confirming rows. |
|
|
54
|
+
| Done: standup strip, the owner-only steps, invite | Part 03 on the same page: the GitHub checklist replaces the paged owner-only steps, then "is alive" with the invite box (once the hall is live, PS15) and the genesis door. |
|
|
55
|
+
| Demo: fit check, settings, teammates, 30-day notice | The "Try a small demo instead" link opens the demo's people, hours, idea and teammates in place at the top of part 01, marks the page "demo", and shows the fit sentence; the rest of the page is unchanged. |
|
|
56
|
+
|
|
57
|
+
### Who builds it
|
|
58
|
+
|
|
59
|
+
| Task | Builds |
|
|
60
|
+
|---|---|
|
|
61
|
+
| task 1004647 | This record and the mockup. |
|
|
62
|
+
| task 1004648 | The one page, the day room on it, and removing the old panels. |
|
|
63
|
+
| task 1004649 | GitHub in one trip, with the self-ticking checklist. |
|
|
64
|
+
| task 1004650 | The hand-back from any tab or device, and checking while the page is hidden. |
|
|
65
|
+
| task 1004651 | A personal or organisation account for the app. |
|
|
66
|
+
| task 1004652 | Resuming a stopped step, and a fixing button for every failure. |
|
|
67
|
+
| task 1004653 | The end-to-end walk, day and night, desktop and phone. |
|
|
68
|
+
| task 1004654 | The artist studio's day room and time-of-day greeting. |
|
|
69
|
+
|
|
70
|
+
## The blended flow — superseded by the one-page flow above (owner, 2026-10-01)
|
|
14
71
|
|
|
15
72
|
> *Go for this as the design of record.* — the owner, approving the blended mockup (task 1004515).
|
|
16
73
|
|
|
@@ -3004,5 +3004,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
3004
3004
|
landed since 1.21.84 with no explicit bump. run 37884200093. (task 1002620)
|
|
3005
3005
|
1.21.86 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3006
3006
|
landed since 1.21.85 with no explicit bump. run 37885516955. (task 1002620)
|
|
3007
|
+
1.21.87 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3008
|
+
landed since 1.21.86 with no explicit bump. run 37886940909. (task 1002620)
|
|
3007
3009
|
---------------------------------------------------------------------------
|
|
3008
3010
|
```
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.87",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.87",
|
|
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.
|
|
3
|
+
"version": "1.21.87",
|
|
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",
|
package/release-notes.json
CHANGED
|
@@ -9342,5 +9342,15 @@
|
|
|
9342
9342
|
"id": "1004399",
|
|
9343
9343
|
"text": "Published module packages are now kept in the project's own data folder instead of inside the installed core, so a core upgrade no longer deletes them; any already stored in the old place are moved over when the server starts."
|
|
9344
9344
|
}
|
|
9345
|
+
],
|
|
9346
|
+
"1.21.87": [
|
|
9347
|
+
{
|
|
9348
|
+
"id": "1004647",
|
|
9349
|
+
"text": "The new project creation design is now on record for every builder: one page in three parts instead of seven screens, a skimmable summary instead of confirming every row, GitHub in a single trip with a checklist that ticks its"
|
|
9350
|
+
},
|
|
9351
|
+
{
|
|
9352
|
+
"id": "1002852",
|
|
9353
|
+
"text": "The task list a builder sees at the start of a session now opens with the goals they have joined and how much work in each is ready, and groups the tasks under their goals, with the builder's own goals first and anything witho"
|
|
9354
|
+
}
|
|
9345
9355
|
]
|
|
9346
9356
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// scripts/gds/start-goals.js — /builder-start, goal-first (task 1002852).
|
|
2
|
+
//
|
|
3
|
+
// WHY. Work is organised in goals, but /builder-start opened on a flat list of
|
|
4
|
+
// tasks, so a builder saw "task 1002852" and never which goal it served or which of
|
|
5
|
+
// their own goals had work waiting. This leads with the goals the builder has
|
|
6
|
+
// JOINED, then shows the same claimable table grouped under each task's goal.
|
|
7
|
+
//
|
|
8
|
+
// The owner's two rules (2026-10-08, WA6 chain kickoff):
|
|
9
|
+
// - the goals sit ABOVE the claimable table and never replace it, so nothing
|
|
10
|
+
// claimable is hidden from a builder who has joined no goal;
|
|
11
|
+
// - a task with no goal is listed under "Other work". Nothing is backfilled.
|
|
12
|
+
//
|
|
13
|
+
// PURE: no I/O. start.js fetches GET /goals (best-effort: a failed read groups by
|
|
14
|
+
// goal id with a generic name and lists no joined goals) and hands both lists in.
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const OTHER_WORK = 'Other work';
|
|
19
|
+
const JOINED_SHOWN = 5;
|
|
20
|
+
|
|
21
|
+
// The open goals this builder is a member of, in the board's own order (version,
|
|
22
|
+
// sort order). `viewer_is_member` is the server's answer, never re-derived here.
|
|
23
|
+
function joinedGoals(goals) {
|
|
24
|
+
return (Array.isArray(goals) ? goals : [])
|
|
25
|
+
.filter((g) => g && g.viewer_is_member === true && g.status === 'open');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function goalName(id, byId) {
|
|
29
|
+
const g = byId.get(String(id));
|
|
30
|
+
return (g && g.title) || `Goal ${id}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Group claimable rows by goal, keeping each row's place in the server's order
|
|
34
|
+
// (priority, then estimate) inside its group. Groups run: the builder's joined
|
|
35
|
+
// goals first (board order), then every other goal by its first row, then
|
|
36
|
+
// "Other work" last. Every row lands in exactly one group, so the grouped table
|
|
37
|
+
// lists precisely the rows the flat one did.
|
|
38
|
+
function groupByGoal(rows, goals) {
|
|
39
|
+
const list = Array.isArray(rows) ? rows : [];
|
|
40
|
+
const byId = new Map((Array.isArray(goals) ? goals : []).map((g) => [String(g.id), g]));
|
|
41
|
+
const joinedOrder = joinedGoals(goals).map((g) => String(g.id));
|
|
42
|
+
const groups = new Map();
|
|
43
|
+
for (const t of list) {
|
|
44
|
+
const key = t.goal_id == null ? null : String(t.goal_id);
|
|
45
|
+
if (!groups.has(key)) {
|
|
46
|
+
groups.set(key, {
|
|
47
|
+
goal_id: key,
|
|
48
|
+
title: key == null ? OTHER_WORK : goalName(key, byId),
|
|
49
|
+
joined: key != null && joinedOrder.includes(key),
|
|
50
|
+
rows: [],
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
groups.get(key).rows.push(t);
|
|
54
|
+
}
|
|
55
|
+
const all = [...groups.values()];
|
|
56
|
+
const joined = joinedOrder.map((id) => groups.get(id)).filter(Boolean);
|
|
57
|
+
const others = all.filter((g) => g.goal_id != null && !g.joined);
|
|
58
|
+
const other = groups.get(null);
|
|
59
|
+
return [...joined, ...others, ...(other ? [other] : [])];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The "Your goals" lines above the table: each joined goal and how many tasks in
|
|
63
|
+
// it are claimable for this builder right now (from the FULL filtered set, not
|
|
64
|
+
// the capped card rows, so a count never under-reports). Empty when the builder
|
|
65
|
+
// has joined no open goal; the table below still lists everything.
|
|
66
|
+
function yourGoals(goals, claimable) {
|
|
67
|
+
const counts = new Map();
|
|
68
|
+
for (const t of Array.isArray(claimable) ? claimable : []) {
|
|
69
|
+
if (t.goal_id == null) continue;
|
|
70
|
+
const k = String(t.goal_id);
|
|
71
|
+
counts.set(k, (counts.get(k) || 0) + 1);
|
|
72
|
+
}
|
|
73
|
+
const joined = joinedGoals(goals);
|
|
74
|
+
return {
|
|
75
|
+
goals: joined.slice(0, JOINED_SHOWN).map((g) => ({
|
|
76
|
+
id: String(g.id),
|
|
77
|
+
title: g.title || `Goal ${g.id}`,
|
|
78
|
+
claimable: counts.get(String(g.id)) || 0,
|
|
79
|
+
})),
|
|
80
|
+
more: Math.max(0, joined.length - JOINED_SHOWN),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Goal-first ordering for the capped card: rows in a goal the builder joined come
|
|
85
|
+
// first, everything else after, each half keeping the server's order. Without it
|
|
86
|
+
// the card's top rows were the board's top rows and a builder's own goals could
|
|
87
|
+
// show "5 to claim" above a table holding none of them. A stable partition, so it
|
|
88
|
+
// only moves rows, never drops one.
|
|
89
|
+
function joinedFirst(rows, goals) {
|
|
90
|
+
const list = Array.isArray(rows) ? rows : [];
|
|
91
|
+
const joined = new Set(joinedGoals(goals).map((g) => String(g.id)));
|
|
92
|
+
const mine = [];
|
|
93
|
+
const rest = [];
|
|
94
|
+
for (const t of list) (t.goal_id != null && joined.has(String(t.goal_id)) ? mine : rest).push(t);
|
|
95
|
+
return mine.concat(rest);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function claimableWord(n) {
|
|
99
|
+
return n === 0 ? 'nothing to claim right now' : `${n} to claim`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = { OTHER_WORK, joinedGoals, joinedFirst, groupByGoal, yourGoals, claimableWord };
|
package/scripts/gds/start.js
CHANGED
|
@@ -233,23 +233,30 @@ function whatItIs(t) {
|
|
|
233
233
|
// Ids are emitted as hall links (renders as a clean #N) — a bare "#NNN" in a
|
|
234
234
|
// relayed chat message autolinks to GitHub and 404s (CLAUDE.md ref rule).
|
|
235
235
|
const surface = require('./surface');
|
|
236
|
+
const startGoals = require('./start-goals');
|
|
236
237
|
const HALL_TASK = `${hallBase()}#/task/`;
|
|
237
|
-
function claimableTable(rows, rewardFromServer) {
|
|
238
|
+
function claimableTable(rows, rewardFromServer, goals) {
|
|
238
239
|
// On a cost-plus-only instance the Reward column is an advertised estimate that
|
|
239
240
|
// is never paid — say so in the header so the column can't read as a promise,
|
|
240
241
|
// and footnote what IS paid below the table (task 1002469).
|
|
241
242
|
const perTask = rewardPolicy(rewardFromServer).perTaskCredit;
|
|
242
243
|
const rewardHead = perTask ? 'Reward' : 'Reward (est.)';
|
|
243
244
|
const headers = ['#', 'Version', ROLE_AXIS_LABEL, rewardHead, 'Est.', 'What it is'];
|
|
244
|
-
const
|
|
245
|
+
const rowCells = (t) => [
|
|
245
246
|
surface.taskRef(t.id, HALL_TASK),
|
|
246
247
|
cell(t.version_id || '?'),
|
|
247
248
|
cell(roleLabel(t) || '—'),
|
|
248
249
|
`${t.credits_reward ?? 0}c`,
|
|
249
250
|
estimateShort(t),
|
|
250
251
|
cell(whatItIs(t)),
|
|
251
|
-
]
|
|
252
|
-
|
|
252
|
+
];
|
|
253
|
+
// task 1002852: one table per goal, each under its goal's name; goal-less work
|
|
254
|
+
// last under "Other work". An empty goal list (a failed read) still groups by id.
|
|
255
|
+
const out = [];
|
|
256
|
+
for (const g of startGoals.groupByGoal(rows, goals)) {
|
|
257
|
+
if (out.length) out.push('');
|
|
258
|
+
out.push(surface.strong(g.title) + (g.joined ? ' · your goal' : ''), surface.table(headers, g.rows.map(rowCells)));
|
|
259
|
+
}
|
|
253
260
|
const cpNote = costPlusOnlyNote(rewardFromServer);
|
|
254
261
|
if (cpNote) out.push('', surface.em(cpNote));
|
|
255
262
|
return out.join('\n');
|
|
@@ -374,6 +381,7 @@ const WIDGET_STYLE = `<style>
|
|
|
374
381
|
.bs-thead{text-align:left;color:var(--color-text-tertiary);font-size:12px;}
|
|
375
382
|
.bs-th{padding:0 6px 4px;font-weight:400;}
|
|
376
383
|
.bs-tr{cursor:pointer;}
|
|
384
|
+
.bs-group td{padding:10px 0 4px;font-size:12px;font-weight:500;color:var(--color-text-secondary);}
|
|
377
385
|
.bs-td{padding:7px 6px;border-top:0.5px solid var(--color-border-tertiary);}
|
|
378
386
|
.bs-id{color:var(--color-text-secondary);font-family:var(--font-mono);font-size:12px;}
|
|
379
387
|
.bs-nowrap{white-space:nowrap;}
|
|
@@ -384,7 +392,7 @@ const WIDGET_STYLE = `<style>
|
|
|
384
392
|
.bs-foot{font-size:13px;color:var(--color-text-tertiary);margin-top:12px;}
|
|
385
393
|
</style>`;
|
|
386
394
|
|
|
387
|
-
function renderWidget({ builder, claims, claimable, total, streak, recommendations, header, routingByPreference, preferred, filters, readyToClose, needsRebaseTasks }) {
|
|
395
|
+
function renderWidget({ builder, claims, claimable, total, streak, recommendations, header, routingByPreference, preferred, filters, readyToClose, needsRebaseTasks, goals, yourGoals }) {
|
|
388
396
|
const b = builder || {};
|
|
389
397
|
// ADR 0120 part 5: the owed-rebase gate warning, rendered at the top of the
|
|
390
398
|
// card (before status/holdings) since it blocks new claims. '' when none owed.
|
|
@@ -443,7 +451,7 @@ function renderWidget({ builder, claims, claimable, total, streak, recommendatio
|
|
|
443
451
|
|
|
444
452
|
// rows — click to claim. Columns: # / Version / Type / Reward /
|
|
445
453
|
// Est. / What it is (the work lane is its own column, not baked into the title).
|
|
446
|
-
const
|
|
454
|
+
const taskRow = (t) => {
|
|
447
455
|
const claimMsg = jsq(`Claim task ${t.id}`);
|
|
448
456
|
const titleAttr = hesc(`Claim #${t.id}: ${t.title || ''}`);
|
|
449
457
|
const d = t.discipline;
|
|
@@ -458,7 +466,16 @@ function renderWidget({ builder, claims, claimable, total, streak, recommendatio
|
|
|
458
466
|
+ TD(hesc(estimateShort(t)), 'bs-sub')
|
|
459
467
|
+ TD(hesc(t.title || ''), 'bs-ellip')
|
|
460
468
|
+ `</tr>`;
|
|
461
|
-
}
|
|
469
|
+
};
|
|
470
|
+
// task 1002852: the same rows, grouped under each task's goal ("Other work" last).
|
|
471
|
+
const rows = startGoals.groupByGoal(claimable, goals).map((g) =>
|
|
472
|
+
`<tr class="bs-group"><td colspan="6">${hesc(g.title)}${g.joined ? '<span class="bs-sub"> · your goal</span>' : ''}</td></tr>`
|
|
473
|
+
+ g.rows.map(taskRow).join('')).join('');
|
|
474
|
+
// task 1002852: the goals this builder joined, ABOVE the table (never instead of it).
|
|
475
|
+
const goalsBlock = yourGoals.goals.length
|
|
476
|
+
? `<div class="bs-menuh">Your goals</div>${yourGoals.goals.map((g) =>
|
|
477
|
+
`<div class="bs-line"><i class="ti ti-target" aria-hidden="true"></i>${hesc(clip(g.title, 60))}<span class="bs-dot"> · </span><span class="bs-sub">${hesc(startGoals.claimableWord(g.claimable))}</span></div>`).join('')}${yourGoals.more ? `<div class="bs-line bs-sub">and ${yourGoals.more} more</div>` : ''}`
|
|
478
|
+
: '';
|
|
462
479
|
|
|
463
480
|
const menu = claimable.length
|
|
464
481
|
? `<table class="bs-table">
|
|
@@ -495,6 +512,7 @@ function renderWidget({ builder, claims, claimable, total, streak, recommendatio
|
|
|
495
512
|
${statusRow}
|
|
496
513
|
${holdings}
|
|
497
514
|
${closeBanner}
|
|
515
|
+
${goalsBlock}
|
|
498
516
|
<div class="bs-menuh">Claim one to start — ${total || claimable.length} ready for you right now${matched}${fNote}</div>
|
|
499
517
|
${menu}
|
|
500
518
|
<div class="bs-foot">Click a task to claim it${moreLink}${crossLane}</div>
|
|
@@ -679,9 +697,9 @@ async function main() {
|
|
|
679
697
|
// endpoint fails, that widget degrades to "N/A" rather than killing
|
|
680
698
|
// /builder-start.
|
|
681
699
|
const api = await cliClient();
|
|
682
|
-
let me, r, closeRes, progressRes, costRes, recentRes, inboxRes, blockersRes;
|
|
700
|
+
let me, r, closeRes, progressRes, costRes, recentRes, inboxRes, blockersRes, goalsRes;
|
|
683
701
|
try {
|
|
684
|
-
[me, r, closeRes, progressRes, costRes, recentRes, inboxRes, blockersRes] = await Promise.all([
|
|
702
|
+
[me, r, closeRes, progressRes, costRes, recentRes, inboxRes, blockersRes, goalsRes] = await Promise.all([
|
|
685
703
|
api.me.getMe(),
|
|
686
704
|
api.tasks.getTasksClaimable({ query: claimableQuery }),
|
|
687
705
|
api.versions.getVersionsCloseSuggestions(),
|
|
@@ -690,6 +708,8 @@ async function main() {
|
|
|
690
708
|
api.public.getPublicRecentShipped(),
|
|
691
709
|
api.inbox.getInbox({ query: { status: 'open' } }),
|
|
692
710
|
api.blockers.getBlockers(),
|
|
711
|
+
// task 1002852: best-effort — a failed goals read leaves the table flat-grouped by id.
|
|
712
|
+
api.goals.getGoals({ query: { status: 'open' } }).catch(() => ({ ok: false })),
|
|
693
713
|
]);
|
|
694
714
|
} catch (err) {
|
|
695
715
|
// A fresh-clone instance that was never brought up refuses the connection.
|
|
@@ -741,7 +761,10 @@ async function main() {
|
|
|
741
761
|
const filtered = routingByPreference
|
|
742
762
|
? rawClaimable.filter((t) => preferred.includes(t.discipline))
|
|
743
763
|
: rawClaimable;
|
|
744
|
-
|
|
764
|
+
// task 1002852: goal-first. The open goals (with the server's viewer_is_member)
|
|
765
|
+
// group the table and put the builder's joined goals' rows ahead of the cap.
|
|
766
|
+
const goals = goalsRes && goalsRes.ok && Array.isArray(goalsRes.data?.goals) ? goalsRes.data.goals : [];
|
|
767
|
+
const claimable = startGoals.joinedFirst(filtered, goals).slice(0, limit);
|
|
745
768
|
|
|
746
769
|
// "You might enjoy" — up to 3 claimable tasks outside the builder's lanes.
|
|
747
770
|
// Computed from the FULL set so it can reach across disciplines. Suppressed
|
|
@@ -758,6 +781,7 @@ async function main() {
|
|
|
758
781
|
? closeRes.data.suggestions
|
|
759
782
|
: [];
|
|
760
783
|
const readyToClose = closeSuggestions.filter((s) => s.ready);
|
|
784
|
+
const yourGoals = startGoals.yourGoals(goals, filtered);
|
|
761
785
|
|
|
762
786
|
// Header widget data (each independently optional).
|
|
763
787
|
const progress = progressRes.ok && Array.isArray(progressRes.data?.progress) ? progressRes.data.progress : null;
|
|
@@ -781,6 +805,7 @@ async function main() {
|
|
|
781
805
|
me: me.data,
|
|
782
806
|
claimable,
|
|
783
807
|
recommendations,
|
|
808
|
+
your_goals: yourGoals.goals,
|
|
784
809
|
discipline_routing: {
|
|
785
810
|
preferred_disciplines: preferred,
|
|
786
811
|
applied: routingByPreference,
|
|
@@ -836,6 +861,8 @@ async function main() {
|
|
|
836
861
|
filters: { version, discipline, fit },
|
|
837
862
|
readyToClose,
|
|
838
863
|
needsRebaseTasks,
|
|
864
|
+
goals,
|
|
865
|
+
yourGoals,
|
|
839
866
|
}) + '\n');
|
|
840
867
|
return;
|
|
841
868
|
}
|
|
@@ -951,11 +978,18 @@ async function main() {
|
|
|
951
978
|
fit && `fit=${fit}`,
|
|
952
979
|
].filter(Boolean);
|
|
953
980
|
const filterNote = filterBits.length ? ` (${filterBits.join(', ')})` : '';
|
|
981
|
+
// task 1002852: the goals this builder joined, above the table (never instead of it).
|
|
982
|
+
if (yourGoals.goals.length) {
|
|
983
|
+
console.log(surface.strong('Your goals'));
|
|
984
|
+
for (const g of yourGoals.goals) console.log(` ${clip(g.title, 60)} · ${startGoals.claimableWord(g.claimable)}`);
|
|
985
|
+
if (yourGoals.more) console.log(` and ${yourGoals.more} more`);
|
|
986
|
+
console.log('');
|
|
987
|
+
}
|
|
954
988
|
console.log(`${surface.strong('Claim one to start')} — ${filtered.length} ready for you right now${matched}${filterNote}:`);
|
|
955
989
|
console.log('');
|
|
956
990
|
if (claimable.length) {
|
|
957
991
|
const cardRows = claimable.slice(0, CARD_ROWS);
|
|
958
|
-
console.log(claimableTable(cardRows, me.ok ? me.data?.reward : null));
|
|
992
|
+
console.log(claimableTable(cardRows, me.ok ? me.data?.reward : null, goals));
|
|
959
993
|
const moreN = filtered.length - cardRows.length;
|
|
960
994
|
if (moreN > 0) {
|
|
961
995
|
console.log('');
|
|
@@ -1012,6 +1046,8 @@ async function main() {
|
|
|
1012
1046
|
filters: { version, discipline, fit },
|
|
1013
1047
|
readyToClose,
|
|
1014
1048
|
needsRebaseTasks,
|
|
1049
|
+
goals,
|
|
1050
|
+
yourGoals,
|
|
1015
1051
|
})
|
|
1016
1052
|
: null;
|
|
1017
1053
|
const markdown = captureLog(emitMarkdownCard).join('\n');
|
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.
|
|
78
|
+
const CORE_VERSION = '1.21.87'; // 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,90 @@
|
|
|
1
|
+
// tests/start_goal_first.mjs — /builder-start leads with the builder's goals
|
|
2
|
+
// (task 1002852).
|
|
3
|
+
//
|
|
4
|
+
// The owner's rules (2026-10-08): the joined goals sit ABOVE the claimable table
|
|
5
|
+
// and never replace it, and a task with no goal is listed under "Other work".
|
|
6
|
+
// These drive the pure helpers in scripts/gds/start-goals.js and the real table
|
|
7
|
+
// renderer start.js uses, so the property that matters — every claimable row is
|
|
8
|
+
// still listed, exactly once — is checked on what the builder actually sees.
|
|
9
|
+
|
|
10
|
+
import { strict as assert } from 'node:assert';
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
import { makeRunner } from './helpers.mjs';
|
|
13
|
+
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
const { test, summary } = makeRunner();
|
|
16
|
+
const sg = require('../scripts/gds/start-goals.js');
|
|
17
|
+
|
|
18
|
+
const GOALS = [
|
|
19
|
+
{ id: 10, title: 'Alpha', status: 'open', viewer_is_member: false },
|
|
20
|
+
{ id: 20, title: 'Mine', status: 'open', viewer_is_member: true },
|
|
21
|
+
{ id: 30, title: 'Done one', status: 'achieved', viewer_is_member: true },
|
|
22
|
+
{ id: 40, title: 'Also mine', status: 'open', viewer_is_member: true },
|
|
23
|
+
];
|
|
24
|
+
const ROWS = [
|
|
25
|
+
{ id: 1, goal_id: 10, title: 'a1' },
|
|
26
|
+
{ id: 2, goal_id: null, title: 'loose' },
|
|
27
|
+
{ id: 3, goal_id: 20, title: 'm1' },
|
|
28
|
+
{ id: 4, goal_id: 99, title: 'unknown goal' },
|
|
29
|
+
{ id: 5, goal_id: 20, title: 'm2' },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
await test('joined goals are the open ones the server says the viewer is a member of', () => {
|
|
33
|
+
assert.deepEqual(sg.joinedGoals(GOALS).map((g) => g.id), [20, 40]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
await test('groups: joined goals first, then other goals, then "Other work" last; every row exactly once', () => {
|
|
37
|
+
const groups = sg.groupByGoal(ROWS, GOALS);
|
|
38
|
+
assert.deepEqual(groups.map((g) => g.title), ['Mine', 'Alpha', 'Goal 99', sg.OTHER_WORK]);
|
|
39
|
+
assert.deepEqual(groups[0].rows.map((r) => r.id), [3, 5], 'server order kept inside a group');
|
|
40
|
+
assert.equal(groups[0].joined, true);
|
|
41
|
+
assert.equal(groups[3].goal_id, null);
|
|
42
|
+
const ids = groups.flatMap((g) => g.rows.map((r) => r.id)).sort();
|
|
43
|
+
assert.deepEqual(ids, [1, 2, 3, 4, 5]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await test('with no goal list (a failed read) nothing is hidden: goals by id, goal-less under Other work', () => {
|
|
47
|
+
const groups = sg.groupByGoal(ROWS, []);
|
|
48
|
+
assert.equal(groups.flatMap((g) => g.rows).length, ROWS.length);
|
|
49
|
+
assert.equal(groups.at(-1).title, sg.OTHER_WORK);
|
|
50
|
+
assert.ok(groups.every((g) => !g.joined));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await test('joinedFirst moves the joined goals\' rows ahead of the cap and drops nothing', () => {
|
|
54
|
+
const out = sg.joinedFirst(ROWS, GOALS);
|
|
55
|
+
assert.deepEqual(out.map((r) => r.id), [3, 5, 1, 2, 4]);
|
|
56
|
+
assert.deepEqual(sg.joinedFirst(ROWS, []).map((r) => r.id), [1, 2, 3, 4, 5], 'no joined goal: order unchanged');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
await test('"Your goals" counts claimable work per joined goal from the full set', () => {
|
|
60
|
+
const y = sg.yourGoals(GOALS, ROWS);
|
|
61
|
+
assert.deepEqual(y.goals, [
|
|
62
|
+
{ id: '20', title: 'Mine', claimable: 2 },
|
|
63
|
+
{ id: '40', title: 'Also mine', claimable: 0 },
|
|
64
|
+
]);
|
|
65
|
+
assert.equal(y.more, 0);
|
|
66
|
+
assert.equal(sg.claimableWord(0), 'nothing to claim right now');
|
|
67
|
+
assert.equal(sg.claimableWord(2), '2 to claim');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await test('a builder in no goal gets no goals list, and the table still lists everything', () => {
|
|
71
|
+
const none = GOALS.map((g) => ({ ...g, viewer_is_member: false }));
|
|
72
|
+
assert.deepEqual(sg.yourGoals(none, ROWS).goals, []);
|
|
73
|
+
assert.equal(sg.groupByGoal(ROWS, none).flatMap((g) => g.rows).length, ROWS.length);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await test('start.js renders "Your goals" ABOVE the table and groups the table by goal (the wiring)', () => {
|
|
77
|
+
const fs = require('node:fs');
|
|
78
|
+
const src = fs.readFileSync(require.resolve('../scripts/gds/start.js'), 'utf8');
|
|
79
|
+
const goalsAt = src.indexOf("surface.strong('Your goals')");
|
|
80
|
+
const tableAt = src.indexOf("surface.strong('Claim one to start')");
|
|
81
|
+
assert.ok(goalsAt > 0 && goalsAt < tableAt, 'markdown: goals before the table');
|
|
82
|
+
const wGoals = src.indexOf('${goalsBlock}');
|
|
83
|
+
const wMenu = src.indexOf('<div class="bs-menuh">Claim one to start');
|
|
84
|
+
assert.ok(wGoals > 0 && wGoals < wMenu, 'widget: goals before the table');
|
|
85
|
+
assert.match(src, /claimableTable\(cardRows, [^)]*, goals\)/, 'the markdown table is grouped');
|
|
86
|
+
assert.equal((src.match(/\n\s+goals,\n\s+yourGoals,\n/g) || []).length, 2, 'both widget calls carry the goals');
|
|
87
|
+
assert.match(src, /startGoals\.joinedFirst\(filtered, goals\)\.slice\(0, limit\)/);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
summary();
|