@bongos/core 1.21.86 → 1.21.88
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 +31 -16
- package/docs/design/project-startup-direction.md +58 -1
- package/docs/module-api-changelog.md +4 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +16 -0
- package/scripts/gds/claude-materialize.js +86 -4
- package/scripts/gds/retired-core-skills.json +101 -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/claude_materialize.mjs +82 -0
- 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.88",
|
|
6
|
+
"core_contract": "1.21.88",
|
|
7
|
+
"source_commit": "4bfba51c57c0f602ad9c166e149b64f3440e88b9",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-10-
|
|
9
|
+
"built_at": "2026-10-09T05:24:37.080Z",
|
|
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": 3001,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 3620,
|
|
20
|
+
"tree_sha256": "9c2cd52bac9f184d7097d5da786d1a69fe8403ddf698d36b914c4b39da21eeb7",
|
|
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": "ada2da687d2c0d7dbb14e027ab755e6fbd771ff026e48d62aeda44b5d14fa923"
|
|
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": "cfe8ee2b06d54070a847c417dc95352c7817ef56bf8a49e4ffe0d0bce8615c26"
|
|
10101
10101
|
},
|
|
10102
10102
|
{
|
|
10103
10103
|
"path": "package.json",
|
|
10104
10104
|
"mode": "0000644",
|
|
10105
|
-
"sha256": "
|
|
10105
|
+
"sha256": "50698c5b17011dbaf2098ae3bdcef8540a946ab36e2d5555c824666f778287ae"
|
|
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": "7d25584e81384714b841f849588e0ed95a630ed13d2adc5b106d9bc2940b5728"
|
|
10126
10126
|
},
|
|
10127
10127
|
{
|
|
10128
10128
|
"path": "scripts/cron/style_guide_audit.sh",
|
|
@@ -10427,7 +10427,7 @@
|
|
|
10427
10427
|
{
|
|
10428
10428
|
"path": "scripts/gds/claude-materialize.js",
|
|
10429
10429
|
"mode": "0000644",
|
|
10430
|
-
"sha256": "
|
|
10430
|
+
"sha256": "1d141943f8367b8814e5a455c0c5d68a3915ac802df6cae9c3550f0628dbc384"
|
|
10431
10431
|
},
|
|
10432
10432
|
{
|
|
10433
10433
|
"path": "scripts/gds/claude-md-template.js",
|
|
@@ -11304,6 +11304,11 @@
|
|
|
11304
11304
|
"mode": "0000644",
|
|
11305
11305
|
"sha256": "084fa5c4da1e755fa0edde5ec770e5151a9f66a247bca222bc7a2366ccf61748"
|
|
11306
11306
|
},
|
|
11307
|
+
{
|
|
11308
|
+
"path": "scripts/gds/retired-core-skills.json",
|
|
11309
|
+
"mode": "0000644",
|
|
11310
|
+
"sha256": "74dda013c4482bcd6a42bcfb36d0affa435044e7981a3c98c9e861f99ba3ade2"
|
|
11311
|
+
},
|
|
11307
11312
|
{
|
|
11308
11313
|
"path": "scripts/gds/review-sheet.js",
|
|
11309
11314
|
"mode": "0000644",
|
|
@@ -11734,10 +11739,15 @@
|
|
|
11734
11739
|
"mode": "0000644",
|
|
11735
11740
|
"sha256": "9f6ce80e5325f4cfe68cfa6cb82bec52d594af7e461b6734724819b23cbb0d37"
|
|
11736
11741
|
},
|
|
11742
|
+
{
|
|
11743
|
+
"path": "scripts/gds/start-goals.js",
|
|
11744
|
+
"mode": "0000644",
|
|
11745
|
+
"sha256": "8c4e1f1c7871c82b0fefd5402c93888831543dcd776a1e761e7b1238b3afafaa"
|
|
11746
|
+
},
|
|
11737
11747
|
{
|
|
11738
11748
|
"path": "scripts/gds/start.js",
|
|
11739
11749
|
"mode": "0000644",
|
|
11740
|
-
"sha256": "
|
|
11750
|
+
"sha256": "181f07b8d62f03ae001d08dd5e5b15a65b33f512fb548e55401b97f7f484f31f"
|
|
11741
11751
|
},
|
|
11742
11752
|
{
|
|
11743
11753
|
"path": "scripts/gds/status.js",
|
|
@@ -12372,7 +12382,7 @@
|
|
|
12372
12382
|
{
|
|
12373
12383
|
"path": "src/module-api.js",
|
|
12374
12384
|
"mode": "0000644",
|
|
12375
|
-
"sha256": "
|
|
12385
|
+
"sha256": "5408eebecc30a0da2527855d9aa1aff7b66658f32b504f4e71517938ad7dcb83"
|
|
12376
12386
|
},
|
|
12377
12387
|
{
|
|
12378
12388
|
"path": "src/module-loader/catalog.js",
|
|
@@ -13157,7 +13167,7 @@
|
|
|
13157
13167
|
{
|
|
13158
13168
|
"path": "tests/claude_materialize.mjs",
|
|
13159
13169
|
"mode": "0000644",
|
|
13160
|
-
"sha256": "
|
|
13170
|
+
"sha256": "da076e811015020cb8a7cc730dc8f403d1b9e29749871078998ebca64113b0e5"
|
|
13161
13171
|
},
|
|
13162
13172
|
{
|
|
13163
13173
|
"path": "tests/clawback_confirmed_backlog.mjs",
|
|
@@ -17529,6 +17539,11 @@
|
|
|
17529
17539
|
"mode": "0000644",
|
|
17530
17540
|
"sha256": "39e62a5fc50113e719cdaacf15e6fd054f38f8703fc64137eddee7901b374f16"
|
|
17531
17541
|
},
|
|
17542
|
+
{
|
|
17543
|
+
"path": "tests/start_goal_first.mjs",
|
|
17544
|
+
"mode": "0000644",
|
|
17545
|
+
"sha256": "a3addba1547bf0a9ce95f5d6c2ea676f63495669154e2f4c161cb8cc543b30bf"
|
|
17546
|
+
},
|
|
17532
17547
|
{
|
|
17533
17548
|
"path": "tests/start_inbox_count.mjs",
|
|
17534
17549
|
"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,9 @@ 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)
|
|
3009
|
+
1.21.88 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3010
|
+
landed since 1.21.87 with no explicit bump. run 37888371878. (task 1002620)
|
|
3007
3011
|
---------------------------------------------------------------------------
|
|
3008
3012
|
```
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.88",
|
|
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.88",
|
|
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.88",
|
|
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,21 @@
|
|
|
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
|
+
}
|
|
9355
|
+
],
|
|
9356
|
+
"1.21.88": [
|
|
9357
|
+
{
|
|
9358
|
+
"id": "1004482",
|
|
9359
|
+
"text": "Upgrading a project now clears out commands Bongos used to ship but has since retired (the old dev-box commands, the old /dev /ideate /paint playbooks, the pixel-art commands), so the Commands page and Claude's menu only list"
|
|
9360
|
+
}
|
|
9345
9361
|
]
|
|
9346
9362
|
}
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
const fs = require('node:fs');
|
|
31
31
|
const path = require('node:path');
|
|
32
32
|
const { spawnSync } = require('node:child_process');
|
|
33
|
+
const crypto = require('node:crypto');
|
|
33
34
|
const { VERBS } = require('../../bin/bongos.js'); // side-effect-free require (guarded main)
|
|
34
35
|
const ic = require('../../src/instance-config'); // neutral configDir/envPrefix fallbacks (never a founder literal)
|
|
35
36
|
const { isCorePackageName, CORE_PACKAGE_NAMES } = require('../../src/module-loader/catalog'); // one definition of "this checkout IS the core"
|
|
@@ -277,6 +278,59 @@ function moduleSkillSources(opts) {
|
|
|
277
278
|
return out;
|
|
278
279
|
}
|
|
279
280
|
|
|
281
|
+
// ---- retired core skills: the tombstone list (task 1004482) -----------------
|
|
282
|
+
// A skill the core USED to ship from its own .claude/skills/ and no longer does (the dev-box
|
|
283
|
+
// verbs, the old /dev /ideate /paint role playbooks, the pixel-art skills that moved into a
|
|
284
|
+
// default-off module). Every instance materialized before task 1004482 received them as core
|
|
285
|
+
// skills, and the provenance manifest then recorded only module-declared names — so step 1c
|
|
286
|
+
// could never prove it had put them there and they stayed in .claude/skills/ for the harness to
|
|
287
|
+
// load and the hall's Commands page to list, forever.
|
|
288
|
+
//
|
|
289
|
+
// From task 1004482 on the manifest records EVERY skill a materialize lands, so a skill the core
|
|
290
|
+
// drops later is withdrawn by step 1c's ordinary rule. This list closes the historical gap only,
|
|
291
|
+
// and is therefore a closed set: it should not need a new entry.
|
|
292
|
+
//
|
|
293
|
+
// "Never touch a skill the instance authored" is held by a fingerprint, not by the name: each
|
|
294
|
+
// entry lists sha256 digests (first 16 hex) of every description the core ever shipped under that
|
|
295
|
+
// name, as written and as bongosify() rewrote it. A dir whose SKILL.md description matches none of
|
|
296
|
+
// them is not ours and is left in place with a note — an owner who wrote their own /dev keeps it.
|
|
297
|
+
const RETIRED_CORE_SKILLS_FILE = path.join(__dirname, 'retired-core-skills.json');
|
|
298
|
+
|
|
299
|
+
// The `description:` value of a SKILL.md's frontmatter, whitespace-collapsed — a plain scalar,
|
|
300
|
+
// a quoted one, or a block scalar (`>` / `|`) with its indented continuation lines. null when
|
|
301
|
+
// the file has no frontmatter or no description. PURE + exported.
|
|
302
|
+
function skillDescription(text) {
|
|
303
|
+
const m = String(text == null ? '' : text).replace(/\r\n/g, '\n').match(/^---\n([\s\S]*?)\n---(?:\n|$)/);
|
|
304
|
+
if (!m) return null;
|
|
305
|
+
const lines = m[1].split('\n');
|
|
306
|
+
const i = lines.findIndex((l) => /^description:/.test(l));
|
|
307
|
+
if (i < 0) return null;
|
|
308
|
+
const parts = [lines[i].replace(/^description:\s*/, '')];
|
|
309
|
+
if (/^[>|][+-]?$/.test(parts[0])) parts[0] = '';
|
|
310
|
+
for (let j = i + 1; j < lines.length && /^\s+\S/.test(lines[j]); j += 1) parts.push(lines[j]);
|
|
311
|
+
const value = parts.join(' ').replace(/\s+/g, ' ').trim().replace(/^(["'])([\s\S]*)\1$/, '$2').trim();
|
|
312
|
+
return value || null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function descriptionDigest(desc) {
|
|
316
|
+
return crypto.createHash('sha256').update(String(desc)).digest('hex').slice(0, 16);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Does this instance skill dir carry a description the core shipped under `name`? Reads the
|
|
320
|
+
// tombstone's digests; an unreadable SKILL.md or a missing description answers "no" (keep it).
|
|
321
|
+
function isRetiredCoreCopy(dir, digests) {
|
|
322
|
+
if (!Array.isArray(digests) || !digests.length) return false;
|
|
323
|
+
let text;
|
|
324
|
+
try { text = fs.readFileSync(path.join(dir, 'SKILL.md'), 'utf8'); } catch { return false; }
|
|
325
|
+
const desc = skillDescription(text);
|
|
326
|
+
return desc != null && digests.includes(descriptionDigest(desc));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function readRetiredCoreSkills(file = RETIRED_CORE_SKILLS_FILE) {
|
|
330
|
+
const j = readJsonSafe(file);
|
|
331
|
+
return j && j.skills && typeof j.skills === 'object' ? j.skills : {};
|
|
332
|
+
}
|
|
333
|
+
|
|
280
334
|
// ---- I/O: copy a tree, transforming text files through bongosify ----------
|
|
281
335
|
|
|
282
336
|
const TEXT_EXT = new Set(['.md', '.txt', '.sh']); // command-bearing text we rewrite (skills are SKILL.md)
|
|
@@ -759,10 +813,38 @@ function materializeClaude({ coreRoot, instanceDir, dryRun = false, overwrite =
|
|
|
759
813
|
// exists to fix. Deliberately asked of the CORE source tree rather than the roster, so a
|
|
760
814
|
// name no enabled module declares any more is still recognised as legitimately sourced.
|
|
761
815
|
const coreShipsNow = (name) => fs.existsSync(path.join(skillsSrc, name)) && !skipSkills.has(name);
|
|
816
|
+
// Every CORE skill step 1 landed is recorded too (task 1004482), so a skill the core stops
|
|
817
|
+
// shipping in a later release is withdrawn by the rule below instead of lingering forever.
|
|
818
|
+
// When the core's .claude/skills/ could not be read at all, nothing was landed from it and
|
|
819
|
+
// nothing may be withdrawn on that account: the previous core entries are carried forward.
|
|
820
|
+
const coreTreeRead = skillsRes.present;
|
|
821
|
+
if (coreTreeRead) {
|
|
822
|
+
for (const name of fs.readdirSync(skillsSrc)) {
|
|
823
|
+
if (landedNow[name] || !SKILL_NAME_RE.test(name) || skillSkip(name)) continue;
|
|
824
|
+
if (!fs.statSync(path.join(skillsSrc, name)).isDirectory()) continue;
|
|
825
|
+
if (!dryRun && !fs.existsSync(path.join(skillsDest, name))) continue;
|
|
826
|
+
landedNow[name] = { source: `.claude/skills/${name}`, coreShipped: true };
|
|
827
|
+
}
|
|
828
|
+
} else {
|
|
829
|
+
for (const [name, rec] of Object.entries(landedPreviously)) if (rec && rec.coreShipped && !rec.module && !landedNow[name]) landedNow[name] = rec;
|
|
830
|
+
}
|
|
762
831
|
const withdrawable = Object.keys(landedPreviously).filter((n) => SKILL_NAME_RE.test(n) && !landedNow[n] && !coreShipsNow(n));
|
|
832
|
+
// The historical half (task 1004482): a skill the core retired BEFORE the manifest recorded
|
|
833
|
+
// core skills is withdrawn when its dir still carries a description the core shipped under
|
|
834
|
+
// that name. Anything else under a retired name is the instance's own and stays.
|
|
835
|
+
const retired = readRetiredCoreSkills();
|
|
836
|
+
const keptAsOwn = [];
|
|
837
|
+
for (const [name, entry] of Object.entries(retired)) {
|
|
838
|
+
if (!SKILL_NAME_RE.test(name) || landedNow[name] || coreShipsNow(name) || withdrawable.includes(name)) continue;
|
|
839
|
+
const dir = path.join(skillsDest, name);
|
|
840
|
+
if (!fs.existsSync(dir)) continue;
|
|
841
|
+
if (isRetiredCoreCopy(dir, entry && entry.descriptionDigests)) withdrawable.push(name);
|
|
842
|
+
else keptAsOwn.push(name);
|
|
843
|
+
}
|
|
844
|
+
if (keptAsOwn.length) summary.notes.push(`${keptAsOwn.length} skill dir(s) share a name the core once shipped but are not the core's copy (their description is not one it shipped), so they were left in place as the instance's own: ${keptAsOwn.sort().join(', ')}.`);
|
|
763
845
|
if (!overwrite) {
|
|
764
846
|
// adopt mode never clobbers what the owner already has; withdrawing is a clobber.
|
|
765
|
-
if (withdrawable.length) summary.notes.push(`${withdrawable.length} skill(s)
|
|
847
|
+
if (withdrawable.length) summary.notes.push(`${withdrawable.length} skill(s) the core no longer lands here were left in place (adopt mode does not withdraw): ${withdrawable.sort().join(', ')}.`);
|
|
766
848
|
} else {
|
|
767
849
|
for (const name of withdrawable.sort()) {
|
|
768
850
|
const dir = path.join(skillsDest, name);
|
|
@@ -774,7 +856,7 @@ function materializeClaude({ coreRoot, instanceDir, dryRun = false, overwrite =
|
|
|
774
856
|
if (!dryRun) fs.rmSync(dir, { recursive: true, force: true });
|
|
775
857
|
summary.withdrawnSkills.push(name);
|
|
776
858
|
}
|
|
777
|
-
if (summary.withdrawnSkills.length) log(` ${dryRun ? '[dry-run] would withdraw' : '✓ withdrew'} ${summary.withdrawnSkills.length} stale skill dir(s)
|
|
859
|
+
if (summary.withdrawnSkills.length) log(` ${dryRun ? '[dry-run] would withdraw' : '✓ withdrew'} ${summary.withdrawnSkills.length} stale skill dir(s) the core no longer ships here (retired, or its module is off): ${summary.withdrawnSkills.join(', ')}`);
|
|
778
860
|
}
|
|
779
861
|
if (!Object.keys(landedPreviously).length && summary.excludedSkills.length) {
|
|
780
862
|
summary.notes.push('no .instance-skills.json provenance in .claude/skills/ yet, so nothing was withdrawn — this run records it; a later run can withdraw the skills of a module that has since been disabled.');
|
|
@@ -788,7 +870,7 @@ function materializeClaude({ coreRoot, instanceDir, dryRun = false, overwrite =
|
|
|
788
870
|
// the pre-flight halts on and the real run then rewrites anyway.
|
|
789
871
|
if (overwrite) {
|
|
790
872
|
const names = Object.keys(landedNow).sort();
|
|
791
|
-
const manifest = { _doc: 'GENERATED by scripts/gds/claude-materialize.js (
|
|
873
|
+
const manifest = { _doc: 'GENERATED by scripts/gds/claude-materialize.js (tasks 1003632, 1004482): every skill a materialize landed in this instance, core and module alike. It is the provenance record that lets a later run WITHDRAW a skill the core stopped shipping or whose module has since been disabled — a name absent from here is never deleted on that account, so a hand-written skill is safe. Commit it; do not hand-edit.', skills: Object.fromEntries(names.map((n) => [n, landedNow[n]])) };
|
|
792
874
|
if (!dryRun) {
|
|
793
875
|
fs.mkdirSync(skillsDest, { recursive: true });
|
|
794
876
|
fs.writeFileSync(path.join(skillsDest, INSTANCE_SKILLS_MANIFEST), JSON.stringify(manifest, null, 2) + '\n');
|
|
@@ -1112,4 +1194,4 @@ if (require.main === module) {
|
|
|
1112
1194
|
process.exit(s.refused.length ? 1 : 0);
|
|
1113
1195
|
}
|
|
1114
1196
|
|
|
1115
|
-
module.exports = { scriptToVerb, bongosify, PROJECT_CLI, topUpNpxAllowTwins, transformSettings, readSettingsTransform, renderStarterSettings, STARTER_ALLOW, copyTreeTransformed, materializeClaude, materializeWillRewrite, materializeModuleSkillsIntoCore, isCoreCheckoutRoot, readModuleSkillsManifest, MODULE_SKILLS_MANIFEST, readInstanceSkillsManifest, INSTANCE_SKILLS_MANIFEST, GDS_INVOCATION_RE, buildRebrandRules, rebrandText, disabledModuleSkills, moduleSkillRoster, moduleSkillSources, vendoredSkillGate, SKILL_NAME_RE, VENDOR_DEFAULT_POLICY };
|
|
1197
|
+
module.exports = { skillDescription, descriptionDigest, isRetiredCoreCopy, readRetiredCoreSkills, RETIRED_CORE_SKILLS_FILE, scriptToVerb, bongosify, PROJECT_CLI, topUpNpxAllowTwins, transformSettings, readSettingsTransform, renderStarterSettings, STARTER_ALLOW, copyTreeTransformed, materializeClaude, materializeWillRewrite, materializeModuleSkillsIntoCore, isCoreCheckoutRoot, readModuleSkillsManifest, MODULE_SKILLS_MANIFEST, readInstanceSkillsManifest, INSTANCE_SKILLS_MANIFEST, GDS_INVOCATION_RE, buildRebrandRules, rebrandText, disabledModuleSkills, moduleSkillRoster, moduleSkillSources, vendoredSkillGate, SKILL_NAME_RE, VENDOR_DEFAULT_POLICY };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_doc": "Tombstone list (task 1004482): skills the core once shipped from its own .claude/skills/ and no longer does, from before materialize recorded core skills in an instance's .instance-skills.json. Each lists sha256 digests (first 16 hex) of every description the core ever shipped under that name, as written and as bongosify() rewrote it; claude-materialize.js withdraws an instance dir under one of these names ONLY when its SKILL.md description matches, so a skill the instance wrote itself is never touched. A closed historical set: a skill the core drops from now on is withdrawn through the manifest, with no entry here.",
|
|
3
|
+
"skills": {
|
|
4
|
+
"builder-box": {
|
|
5
|
+
"descriptionDigests": [
|
|
6
|
+
"14913228d8aefa2a",
|
|
7
|
+
"3a8458946af072fb",
|
|
8
|
+
"7695811a3bb4fa8a",
|
|
9
|
+
"d77e96d673b77f5b"
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
"builder-connect": {
|
|
13
|
+
"descriptionDigests": [
|
|
14
|
+
"27137471966e6a98",
|
|
15
|
+
"2bc0f02001bf7311",
|
|
16
|
+
"5bfb4795ceb3e77a",
|
|
17
|
+
"a9442b55f9588a06",
|
|
18
|
+
"d661c46a8076a1ea"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"builder-sync": {
|
|
22
|
+
"descriptionDigests": [
|
|
23
|
+
"3fad042c07a1d8e7",
|
|
24
|
+
"50c1a8505aead99b"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"close-box": {
|
|
28
|
+
"descriptionDigests": [
|
|
29
|
+
"03d839f2e5909448",
|
|
30
|
+
"4f30758956f46d88",
|
|
31
|
+
"66aef8299b4c39af",
|
|
32
|
+
"b3f6743fdb4a07fe",
|
|
33
|
+
"d119bb63ffaba1e8"
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
"dev": {
|
|
37
|
+
"descriptionDigests": [
|
|
38
|
+
"7836b393796e9ee0",
|
|
39
|
+
"8cf08a65be57d5f1"
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
"feedback": {
|
|
43
|
+
"descriptionDigests": [
|
|
44
|
+
"211c55ddd59ef937",
|
|
45
|
+
"2f58da0a75816b09",
|
|
46
|
+
"3b48dc4cf9e43692",
|
|
47
|
+
"7bca081fc8092577",
|
|
48
|
+
"85bb6d1200234a28",
|
|
49
|
+
"a309ad6d9ea56a5f",
|
|
50
|
+
"ab21803079a7530c",
|
|
51
|
+
"e090c86c838c0f97"
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
"ideate": {
|
|
55
|
+
"descriptionDigests": [
|
|
56
|
+
"466396868c0d2d37"
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
"otb-character-review": {
|
|
60
|
+
"descriptionDigests": [
|
|
61
|
+
"518356210d594d2e",
|
|
62
|
+
"af216311f26edffd",
|
|
63
|
+
"eb529abb98348cd1"
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
"otb-design-review": {
|
|
67
|
+
"descriptionDigests": [
|
|
68
|
+
"1b1375bc9ea12faa",
|
|
69
|
+
"9f97a32f15c805bc"
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
"otb-feedback-capture": {
|
|
73
|
+
"descriptionDigests": [
|
|
74
|
+
"6ba86ab812dd9495",
|
|
75
|
+
"c915f0de7ab1e8fd"
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
"otb-figma-sync": {
|
|
79
|
+
"descriptionDigests": [
|
|
80
|
+
"281987db3241a779",
|
|
81
|
+
"61132c40a0388319"
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
"otb-tile-generate": {
|
|
85
|
+
"descriptionDigests": [
|
|
86
|
+
"57981088137a89ee",
|
|
87
|
+
"b1db07959665a454"
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
"paint": {
|
|
91
|
+
"descriptionDigests": [
|
|
92
|
+
"944ae7795b04d726"
|
|
93
|
+
]
|
|
94
|
+
},
|
|
95
|
+
"tweak": {
|
|
96
|
+
"descriptionDigests": [
|
|
97
|
+
"a8d84a40f2e3d8a2"
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -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.88'; // 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');
|
|
@@ -19,6 +19,7 @@ import { strict as assert } from 'node:assert';
|
|
|
19
19
|
import { createRequire } from 'node:module';
|
|
20
20
|
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, readdirSync, rmSync } from 'node:fs';
|
|
21
21
|
import { tmpdir, homedir } from 'node:os';
|
|
22
|
+
import { execFileSync } from 'node:child_process';
|
|
22
23
|
import { join, dirname, relative, sep } from 'node:path';
|
|
23
24
|
import { fileURLToPath } from 'node:url';
|
|
24
25
|
|
|
@@ -420,6 +421,87 @@ t('materializeClaude: --dry-run reports the withdrawal without performing it; ad
|
|
|
420
421
|
for (const d of [core, inst]) rmSync(d, { recursive: true, force: true });
|
|
421
422
|
});
|
|
422
423
|
|
|
424
|
+
// ---- withdrawing skills the CORE stopped shipping (task 1004482) ----------------
|
|
425
|
+
// The bug: the manifest recorded only module-declared skills, so a skill the core shipped
|
|
426
|
+
// from its own .claude/skills/ and later dropped (the dev-box verbs, /dev /ideate /paint,
|
|
427
|
+
// the pixel-art skills) stayed in every upgraded instance, loaded by the harness and listed
|
|
428
|
+
// on the hall's Commands page. Two halves: the manifest now records core skills (future
|
|
429
|
+
// retirements), and a tombstone list fingerprinted by description covers the past ones.
|
|
430
|
+
|
|
431
|
+
// The /ideate description the core shipped until 2026-09-09, verbatim — a block scalar.
|
|
432
|
+
const IDEATE_SHIPPED = '---\nname: ideate\ndescription: >-\n The ideation session — the operating playbook for ideator-discipline work. Triggers when the user says "/ideate", "let\'s ideate", "I want to brainstorm", "help me think through an idea", "start an ideation session", or when a claimed task\'s discipline routes here (claim.js prints a directive to invoke /ideate for ideator tasks). Inverts the usual dynamic: the human ideator drives, Claude is the sounding board. Open to any rank for FILING ideas (Xenos+ can capture; only Metic+ can triage).\n---\nbody\n';
|
|
433
|
+
|
|
434
|
+
function coreSkillFixture() {
|
|
435
|
+
const core = mkdtempSync(join(tmpdir(), 'rt-core-'));
|
|
436
|
+
const inst = mkdtempSync(join(tmpdir(), 'rt-inst-'));
|
|
437
|
+
for (const n of ['keep', 'gamma']) {
|
|
438
|
+
mkdirSync(join(core, '.claude', 'skills', n), { recursive: true });
|
|
439
|
+
writeFileSync(join(core, '.claude', 'skills', n, 'SKILL.md'), `---\nname: ${n}\ndescription: ${n} skill\n---\nbody\n`);
|
|
440
|
+
}
|
|
441
|
+
return { core, inst };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
t('skillDescription: reads plain, quoted and block-scalar descriptions; null without one', () => {
|
|
445
|
+
assert.equal(m.skillDescription('---\nname: a\ndescription: one line\n---\n'), 'one line');
|
|
446
|
+
assert.equal(m.skillDescription('---\r\nname: a\r\ndescription: "quoted"\r\n---\r\n'), 'quoted');
|
|
447
|
+
assert.equal(m.skillDescription('---\ndescription: >-\n folded\n over two\nname: a\n---\n'), 'folded over two');
|
|
448
|
+
assert.equal(m.skillDescription('---\nname: a\n---\n'), null);
|
|
449
|
+
assert.equal(m.skillDescription('no frontmatter'), null);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
t('materializeClaude: a core skill the core STOPPED shipping is withdrawn on the next upgrade (task 1004482)', () => {
|
|
453
|
+
const { core, inst } = coreSkillFixture();
|
|
454
|
+
m.materializeClaude({ coreRoot: core, instanceDir: inst, dryRun: false });
|
|
455
|
+
const mani = m.readInstanceSkillsManifest(join(inst, '.claude', 'skills'));
|
|
456
|
+
assert.ok(mani.skills.gamma && mani.skills.gamma.coreShipped, 'core skills are recorded as landed now');
|
|
457
|
+
rmSync(join(core, '.claude', 'skills', 'gamma'), { recursive: true, force: true });
|
|
458
|
+
const res = m.materializeClaude({ coreRoot: core, instanceDir: inst, dryRun: false });
|
|
459
|
+
assert.deepEqual(res.withdrawnSkills, ['gamma']);
|
|
460
|
+
assert.ok(!existsSync(join(inst, '.claude', 'skills', 'gamma')), 'the retired core skill is gone');
|
|
461
|
+
assert.ok(existsSync(join(inst, '.claude', 'skills', 'keep')), 'a skill the core still ships stays');
|
|
462
|
+
assert.ok(res.writtenPaths.includes('.claude/skills/gamma/SKILL.md'), 'the deletion is reported so the pin commit carries it');
|
|
463
|
+
for (const d of [core, inst]) rmSync(d, { recursive: true, force: true });
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
t('materializeClaude: a core with no readable .claude/skills/ withdraws no core skill (task 1004482)', () => {
|
|
467
|
+
const { core, inst } = coreSkillFixture();
|
|
468
|
+
m.materializeClaude({ coreRoot: core, instanceDir: inst, dryRun: false });
|
|
469
|
+
rmSync(join(core, '.claude', 'skills'), { recursive: true, force: true });
|
|
470
|
+
const res = m.materializeClaude({ coreRoot: core, instanceDir: inst, dryRun: false });
|
|
471
|
+
assert.deepEqual(res.withdrawnSkills, [], 'nothing landed from the core, so nothing is withdrawn on that account');
|
|
472
|
+
assert.ok(existsSync(join(inst, '.claude', 'skills', 'gamma')));
|
|
473
|
+
assert.ok(m.readInstanceSkillsManifest(join(inst, '.claude', 'skills')).skills.gamma, 'and the provenance is carried forward');
|
|
474
|
+
for (const d of [core, inst]) rmSync(d, { recursive: true, force: true });
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
t('materializeClaude: a RETIRED core skill with the description the core shipped is withdrawn; one the instance wrote under that name is kept (task 1004482)', () => {
|
|
478
|
+
const { core, inst } = coreSkillFixture();
|
|
479
|
+
const skills = join(inst, '.claude', 'skills');
|
|
480
|
+
// an instance materialized before core skills were recorded: no manifest names either dir
|
|
481
|
+
mkdirSync(join(skills, 'ideate'), { recursive: true });
|
|
482
|
+
writeFileSync(join(skills, 'ideate', 'SKILL.md'), IDEATE_SHIPPED);
|
|
483
|
+
mkdirSync(join(skills, 'dev'), { recursive: true });
|
|
484
|
+
writeFileSync(join(skills, 'dev', 'SKILL.md'), '---\nname: dev\ndescription: our own dev loop\n---\nMINE\n');
|
|
485
|
+
const res = m.materializeClaude({ coreRoot: core, instanceDir: inst, dryRun: false });
|
|
486
|
+
assert.deepEqual(res.withdrawnSkills, ['ideate'], 'the core\'s old /ideate goes');
|
|
487
|
+
assert.ok(!existsSync(join(skills, 'ideate')));
|
|
488
|
+
assert.equal(readFileSync(join(skills, 'dev', 'SKILL.md'), 'utf8'), '---\nname: dev\ndescription: our own dev loop\n---\nMINE\n', 'the owner\'s /dev is untouched');
|
|
489
|
+
assert.ok(res.notes.some((n) => /left in place as the instance's own: dev/.test(n)), 'and the run says why it kept it');
|
|
490
|
+
for (const d of [core, inst]) rmSync(d, { recursive: true, force: true });
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
t('retired-core-skills.json: names the dev-box verbs, the old role playbooks and the pixel-art skills, none of which the core ships now', () => {
|
|
494
|
+
const retired = m.readRetiredCoreSkills();
|
|
495
|
+
for (const n of ['builder-box', 'builder-connect', 'builder-sync', 'close-box', 'dev', 'ideate', 'paint', 'otb-tile-generate', 'otb-design-review']) {
|
|
496
|
+
assert.ok(retired[n] && retired[n].descriptionDigests.length, `${n} is tombstoned with at least one digest`);
|
|
497
|
+
}
|
|
498
|
+
// A tombstone on a name the core still ships would be inert (step 1 lands it), but it would
|
|
499
|
+
// also mean the list was built wrong — tracked files only, since a checkout holds untracked
|
|
500
|
+
// module-skill copies (ADR 0224) under the same directory.
|
|
501
|
+
const tracked = execFileSync('git', ['-C', ROOT, 'ls-files', '.claude/skills'], { encoding: 'utf8' });
|
|
502
|
+
for (const n of Object.keys(retired)) assert.ok(!tracked.includes(`.claude/skills/${n}/`), `${n} is not a skill the core ships from .claude/skills/`);
|
|
503
|
+
});
|
|
504
|
+
|
|
423
505
|
// ---- module-owned skills (task 1003321 / ADR 0198) ---------------------------
|
|
424
506
|
// A module ships a skill from modules/<key>/skills/<name>/ (its own) or
|
|
425
507
|
// modules/<key>/skills/vendor/<name>/ (third-party + PROVENANCE.md); only a DECLARED
|
|
@@ -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();
|