@bongos/core 1.21.124 → 1.21.125
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 +33 -13
- package/bin/bongos.js +9 -0
- package/docs/module-api-changelog.md +2 -0
- package/modules/onboarding/howto-guide.js +259 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +10 -0
- package/scripts/gds/core-pin-sync.js +233 -0
- package/src/module-api.js +1 -1
- package/tests/core_pin_sync.mjs +237 -0
- package/tests/howto_guide.mjs +96 -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.125",
|
|
6
|
+
"core_contract": "1.21.125",
|
|
7
|
+
"source_commit": "6537f85e10dcb60107037c138237fe4e5d855500",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-10-
|
|
9
|
+
"built_at": "2026-10-15T15:06:45.205Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 605,
|
|
13
13
|
"agent_docs_stubbed": 28,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 3128,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 3762,
|
|
20
|
+
"tree_sha256": "7fb72ed794b2cdaa7f0fe66ffdccb7b56f5c18a9d4d66f18b4767254072e1827",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -312,7 +312,7 @@
|
|
|
312
312
|
{
|
|
313
313
|
"path": "bin/bongos.js",
|
|
314
314
|
"mode": "0000755",
|
|
315
|
-
"sha256": "
|
|
315
|
+
"sha256": "f2a64dc742bb1f13938840c9f294ea7a60f1895dc9366200e33d53891e5e0fe5"
|
|
316
316
|
},
|
|
317
317
|
{
|
|
318
318
|
"path": "clients/bongos-client/README.md",
|
|
@@ -2922,7 +2922,7 @@
|
|
|
2922
2922
|
{
|
|
2923
2923
|
"path": "docs/module-api-changelog.md",
|
|
2924
2924
|
"mode": "0000644",
|
|
2925
|
-
"sha256": "
|
|
2925
|
+
"sha256": "a1ec4ace0bc91b34031c59bb4fd08263af542f5ddd1284204bfd821cf843b527"
|
|
2926
2926
|
},
|
|
2927
2927
|
{
|
|
2928
2928
|
"path": "docs/modules-contract.md",
|
|
@@ -7929,6 +7929,11 @@
|
|
|
7929
7929
|
"mode": "0000644",
|
|
7930
7930
|
"sha256": "22904d8c06daabb634b55f240b895c906e90ba4c6ed81e7a41cdbbb78635ace8"
|
|
7931
7931
|
},
|
|
7932
|
+
{
|
|
7933
|
+
"path": "modules/onboarding/howto-guide.js",
|
|
7934
|
+
"mode": "0000644",
|
|
7935
|
+
"sha256": "18984c946d746da66ff7678379e9f49c9cfdcdb8f70d2acd852ab29c820baad0"
|
|
7936
|
+
},
|
|
7932
7937
|
{
|
|
7933
7938
|
"path": "modules/onboarding/module.json",
|
|
7934
7939
|
"mode": "0000644",
|
|
@@ -10472,12 +10477,12 @@
|
|
|
10472
10477
|
{
|
|
10473
10478
|
"path": "package-lock.json",
|
|
10474
10479
|
"mode": "0000644",
|
|
10475
|
-
"sha256": "
|
|
10480
|
+
"sha256": "cb1fe1a974c6063f91ae16893f1daddb028647374dc13d9df725ea4be5074d9a"
|
|
10476
10481
|
},
|
|
10477
10482
|
{
|
|
10478
10483
|
"path": "package.json",
|
|
10479
10484
|
"mode": "0000644",
|
|
10480
|
-
"sha256": "
|
|
10485
|
+
"sha256": "e92ae06c5b33aa14e3ec516f009526f335ebec59b5e03b4e774a4a48c252d087"
|
|
10481
10486
|
},
|
|
10482
10487
|
{
|
|
10483
10488
|
"path": "public-docs/index.html",
|
|
@@ -10497,7 +10502,7 @@
|
|
|
10497
10502
|
{
|
|
10498
10503
|
"path": "release-notes.json",
|
|
10499
10504
|
"mode": "0000644",
|
|
10500
|
-
"sha256": "
|
|
10505
|
+
"sha256": "415b376c73ce06c6803897093c02aa9157378d55487a0f84dd8387dc749a75a0"
|
|
10501
10506
|
},
|
|
10502
10507
|
{
|
|
10503
10508
|
"path": "scripts/cron/style_guide_audit.sh",
|
|
@@ -10874,6 +10879,11 @@
|
|
|
10874
10879
|
"mode": "0000644",
|
|
10875
10880
|
"sha256": "9dd80d2c37602c59cb75db0be02cfadc737fc64891a81d03f5be5f0ea5e35a27"
|
|
10876
10881
|
},
|
|
10882
|
+
{
|
|
10883
|
+
"path": "scripts/gds/core-pin-sync.js",
|
|
10884
|
+
"mode": "0000644",
|
|
10885
|
+
"sha256": "f8a3a0ee2c0f4dbc2583603cd6e7b547140a5f8dc286b7c5f0b6dadd6096a002"
|
|
10886
|
+
},
|
|
10877
10887
|
{
|
|
10878
10888
|
"path": "scripts/gds/coreB-carve-proof.sh",
|
|
10879
10889
|
"mode": "0000644",
|
|
@@ -12817,7 +12827,7 @@
|
|
|
12817
12827
|
{
|
|
12818
12828
|
"path": "src/module-api.js",
|
|
12819
12829
|
"mode": "0000644",
|
|
12820
|
-
"sha256": "
|
|
12830
|
+
"sha256": "81694f072dbbb4e64a286a4f5157d07a8e1105ae8a94d800f09eba00f30848e6"
|
|
12821
12831
|
},
|
|
12822
12832
|
{
|
|
12823
12833
|
"path": "src/module-loader/catalog.js",
|
|
@@ -13829,6 +13839,11 @@
|
|
|
13829
13839
|
"mode": "0000644",
|
|
13830
13840
|
"sha256": "a3cf352d5dc340b453b22513c28d8c0a439526862535f51f84b7aea2161c734f"
|
|
13831
13841
|
},
|
|
13842
|
+
{
|
|
13843
|
+
"path": "tests/core_pin_sync.mjs",
|
|
13844
|
+
"mode": "0000644",
|
|
13845
|
+
"sha256": "ac3714b1b089ba2450f1125c74cbe5491deb4ddec1eec77c4c83a8a635a84d93"
|
|
13846
|
+
},
|
|
13832
13847
|
{
|
|
13833
13848
|
"path": "tests/core_update_banner.mjs",
|
|
13834
13849
|
"mode": "0000644",
|
|
@@ -15584,6 +15599,11 @@
|
|
|
15584
15599
|
"mode": "0000644",
|
|
15585
15600
|
"sha256": "7cd031d48b41424a654ddc66404cf112a21ecc7d558256ac27ec9357401f112c"
|
|
15586
15601
|
},
|
|
15602
|
+
{
|
|
15603
|
+
"path": "tests/howto_guide.mjs",
|
|
15604
|
+
"mode": "0000644",
|
|
15605
|
+
"sha256": "8e9b206fdde738f289bc11e204852595e9128b523cfc20cd67e3e83fb60a2fac"
|
|
15606
|
+
},
|
|
15587
15607
|
{
|
|
15588
15608
|
"path": "tests/html_comment_nesting.mjs",
|
|
15589
15609
|
"mode": "0000644",
|
package/bin/bongos.js
CHANGED
|
@@ -190,6 +190,15 @@ function main(argv) {
|
|
|
190
190
|
return 0;
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
// Run the core the checkout PINS (task 1004632). A task worktree has no node_modules
|
|
194
|
+
// of its own, so after a core update lands it resolves the main checkout's older core
|
|
195
|
+
// and the ship fails on a bug the pin already fixed. `ship` installs the pinned core
|
|
196
|
+
// into the checkout and re-runs under it; scripts/gds/core-pin-sync.js.
|
|
197
|
+
if (!COMPILED && verb === 'ship') {
|
|
198
|
+
const synced = require('../scripts/gds/core-pin-sync.js').ensurePinnedCore({ coreRoot: REPO_ROOT, argv });
|
|
199
|
+
if (synced != null) return synced;
|
|
200
|
+
}
|
|
201
|
+
|
|
193
202
|
// `bongos exec <relpath> [args]` — run a core script by its package-relative path (see above).
|
|
194
203
|
if (verb === 'exec') {
|
|
195
204
|
if (COMPILED) {
|
|
@@ -3085,5 +3085,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
3085
3085
|
landed since 1.21.122 with no explicit bump. run 38624070870. (task 1002620)
|
|
3086
3086
|
1.21.124 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3087
3087
|
landed since 1.21.123 with no explicit bump. run 38626749089. (task 1002620)
|
|
3088
|
+
1.21.125 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3089
|
+
landed since 1.21.124 with no explicit bump. run 38757326466. (task 1002620)
|
|
3088
3090
|
---------------------------------------------------------------------------
|
|
3089
3091
|
```
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// modules/onboarding/howto-guide.js — the in-session how-to guide: ~25 short steps an AI walks a person through (task 1004627)
|
|
2
|
+
//
|
|
3
|
+
// WHY. The owner opened Bongos on their own project and did not know where to begin:
|
|
4
|
+
// nothing told them `/builder-start` exists, what a goal is for, or what happens on a
|
|
5
|
+
// first ship. This guide is the CONTENT an AI assistant walks a person through inside
|
|
6
|
+
// their own session, one short step at a time, at the moment it is useful — so someone
|
|
7
|
+
// who has never heard of a Bongos command is never lost.
|
|
8
|
+
//
|
|
9
|
+
// It is the single source of truth for the three tasks that build on it:
|
|
10
|
+
// • task 1004634 stores each person's read/unread state per step id;
|
|
11
|
+
// • task 1004635 shows the right unread step when its moment arrives, then marks it read;
|
|
12
|
+
// • task 1004636 lets a person reopen the guide, see progress, or skip it.
|
|
13
|
+
// So the step ids are STABLE: a stored "read" mark names one. Never rename or reuse an
|
|
14
|
+
// id — retire a step by deleting it and add a new id for new content
|
|
15
|
+
// (tests/howto_guide.mjs pins the shape and the plain-language rule).
|
|
16
|
+
//
|
|
17
|
+
// Each step is: what the feature is FOR (`what`), WHEN to reach for it (`when`), and the
|
|
18
|
+
// exact words or command to TRY (`try`). Plain language throughout — the reader may not
|
|
19
|
+
// be technical; the test bans the accessibility scorer's jargon list. Commands are the
|
|
20
|
+
// ones an instance actually gets (core skills + `npx bongos` verbs); a step names a
|
|
21
|
+
// slash command AND the plain words that reach it, because typed-only commands do not
|
|
22
|
+
// answer to plain words.
|
|
23
|
+
//
|
|
24
|
+
// Read it in a terminal: `node modules/onboarding/howto-guide.js` (add `--moment <id>`).
|
|
25
|
+
|
|
26
|
+
'use strict';
|
|
27
|
+
|
|
28
|
+
// The moments, in the order a new project meets them. `trigger` says, for task 1004635,
|
|
29
|
+
// what event opens the moment; it is descriptive here, not wiring.
|
|
30
|
+
const MOMENTS = Object.freeze([
|
|
31
|
+
{ id: 'first_open', title: 'Opening the project for the first time', trigger: 'the first prompt of the first session in a project folder' },
|
|
32
|
+
{ id: 'setup', title: 'Setting up the project', trigger: 'the project has no goal yet, or setup is unfinished' },
|
|
33
|
+
{ id: 'goal', title: 'Creating a goal', trigger: 'the person asks to plan work, or the project has no open goal' },
|
|
34
|
+
{ id: 'task', title: 'Creating a task', trigger: 'the person describes a change they want, or a goal has no tasks' },
|
|
35
|
+
{ id: 'find_work', title: 'Finding work', trigger: 'the person asks what to do next, or starts a session with nothing claimed' },
|
|
36
|
+
{ id: 'first_claim', title: 'Claiming your first task', trigger: 'the person claims a task for the first time' },
|
|
37
|
+
{ id: 'first_ship', title: 'Your first ship', trigger: 'the person finishes the work on their first claimed task' },
|
|
38
|
+
{ id: 'after_ship', title: 'After a ship', trigger: 'a task reaches shipped for the first time' },
|
|
39
|
+
{ id: 'invite', title: 'Inviting people', trigger: 'the person mentions a teammate, or asks how others can help' },
|
|
40
|
+
{ id: 'settings', title: 'Settings', trigger: 'the person asks to change how Bongos behaves for them or the project' },
|
|
41
|
+
{ id: 'stuck', title: 'When something goes wrong', trigger: 'a command fails, or the person says they are stuck or confused' },
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
const STEPS = Object.freeze([
|
|
45
|
+
// first_open
|
|
46
|
+
{
|
|
47
|
+
id: 'welcome', moment: 'first_open', title: 'What Bongos is',
|
|
48
|
+
what: 'Bongos keeps the list of everything your project needs, and the AI does the building. You decide what gets made; the AI does the work and records it.',
|
|
49
|
+
when: 'The first time you open this project with an AI assistant.',
|
|
50
|
+
try: 'Ask: "What is Bongos and what can it do for me?"',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'where_to_start', moment: 'first_open', title: 'Where to start',
|
|
54
|
+
what: 'There are three ways in: finish setting up the project, make a goal (a bigger outcome you want), or make a task (one piece of work).',
|
|
55
|
+
when: 'Any time you are not sure what to do first.',
|
|
56
|
+
try: 'Say: "Where should I start?" The AI will look at the project and suggest one of the three.',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 'ask_in_plain_words', moment: 'first_open', title: 'Just ask in plain words',
|
|
60
|
+
what: 'You never need to memorise commands. Describe what you want and the AI picks the right one, then tells you which it used.',
|
|
61
|
+
when: 'Whenever you want something done and do not know the command.',
|
|
62
|
+
try: 'Say: "I want to add a contact form to the website."',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: 'the_hall', moment: 'first_open', title: 'The hall: your project in a browser',
|
|
66
|
+
what: 'The hall is your project\'s web page. It shows goals, tasks, who is working on what, and what has shipped.',
|
|
67
|
+
when: 'When you want to see everything at a glance instead of asking.',
|
|
68
|
+
try: 'Ask: "Open the hall for this project" or "What is the hall link?"',
|
|
69
|
+
},
|
|
70
|
+
// setup
|
|
71
|
+
{
|
|
72
|
+
id: 'sign_in', moment: 'setup', title: 'Sign in once',
|
|
73
|
+
what: 'Signing in links this computer to your project, so the AI can claim and ship work in your name.',
|
|
74
|
+
when: 'The first time on a new computer, or when the AI says your sign-in has expired.',
|
|
75
|
+
try: 'Type /builder-setup, or in a terminal run `npx bongos login`.',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 'check_setup', moment: 'setup', title: 'Check everything is ready',
|
|
79
|
+
what: 'A quick health check of this computer and your sign-in, with a plain fix for anything missing.',
|
|
80
|
+
when: 'Before your first task, or when something that used to work stops working.',
|
|
81
|
+
try: 'Say: "Check my setup", or run `npx bongos doctor`.',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: 'project_status', moment: 'setup', title: 'See where the project stands',
|
|
85
|
+
what: 'A summary of the current version: what is done, what is left, and what is blocking.',
|
|
86
|
+
when: 'At the start of a working session, or before planning.',
|
|
87
|
+
try: 'Type /status, or say "How far along is the project?"',
|
|
88
|
+
},
|
|
89
|
+
// goal
|
|
90
|
+
{
|
|
91
|
+
id: 'what_is_a_goal', moment: 'goal', title: 'What a goal is',
|
|
92
|
+
what: 'A goal is a bigger outcome, like "people can sign up" — a home for the tasks that get you there, with a clear "done when".',
|
|
93
|
+
when: 'When what you want is too big to finish in one sitting.',
|
|
94
|
+
try: 'Ask: "Should this be a goal or a single task?"',
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: 'create_goal', moment: 'goal', title: 'Create a goal',
|
|
98
|
+
what: 'The AI asks what the goal is for and what "done" looks like, then writes it up with its first few tasks.',
|
|
99
|
+
when: 'When you have decided on a new outcome to work towards.',
|
|
100
|
+
try: 'Type /goal-create, or say "Make a goal for letting people sign up."',
|
|
101
|
+
},
|
|
102
|
+
// task
|
|
103
|
+
{
|
|
104
|
+
id: 'what_is_a_task', moment: 'task', title: 'What a task is',
|
|
105
|
+
what: 'A task is one piece of work small enough to finish and check in one go. Every change to the project belongs to a task, so there is always a record of it.',
|
|
106
|
+
when: 'Whenever you want something changed.',
|
|
107
|
+
try: 'Ask: "What makes a good task?"',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: 'create_task', moment: 'task', title: 'Create a task',
|
|
111
|
+
what: 'Describe the change and how you will know it is done. The AI writes it up and files it under the right goal.',
|
|
112
|
+
when: 'When you know a specific change you want.',
|
|
113
|
+
try: 'Say: "Make a task: the contact form should email me each message."',
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: 'capture_an_idea', moment: 'task', title: 'Save an idea for later',
|
|
117
|
+
what: 'Not ready to commit to it? Save it as an idea. Ideas wait in a list until you decide to turn them into tasks or drop them.',
|
|
118
|
+
when: 'When a thought comes up in the middle of other work.',
|
|
119
|
+
try: 'Say: "Save an idea: a dark mode for the website."',
|
|
120
|
+
},
|
|
121
|
+
// find_work
|
|
122
|
+
{
|
|
123
|
+
id: 'whats_claimable', moment: 'find_work', title: 'See what you can work on',
|
|
124
|
+
what: 'A list of the tasks that are ready right now and safe to start without clashing with anyone else\'s work.',
|
|
125
|
+
when: 'At the start of a session, or when you finish a task.',
|
|
126
|
+
try: 'Type /builder-start, or say "What can I work on?"',
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: 'work_a_goal', moment: 'find_work', title: 'Work through a whole goal',
|
|
130
|
+
what: 'The AI takes the goal\'s tasks one at a time — start, build, ship, next — and stops to ask you only when it needs a decision.',
|
|
131
|
+
when: 'When a goal has several tasks that follow on from each other.',
|
|
132
|
+
try: 'Say: "Work through goal 12 end to end", or type /builder-sequence.',
|
|
133
|
+
},
|
|
134
|
+
// first_claim
|
|
135
|
+
{
|
|
136
|
+
id: 'claim_a_task', moment: 'first_claim', title: 'Claiming a task',
|
|
137
|
+
what: 'Claiming says "I am on this one", so nobody else starts the same work. The AI then works on it in its own separate copy of the project, so your main folder stays tidy.',
|
|
138
|
+
when: 'When you pick a task to work on.',
|
|
139
|
+
try: 'Type /builder-claim followed by the task number, or say "Claim task 42."',
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: 'give_a_task_back', moment: 'first_claim', title: 'Give a task back',
|
|
143
|
+
what: 'Changed your mind? Giving it back makes the task available again. Nothing is lost and nothing is charged.',
|
|
144
|
+
when: 'When you will not finish a task you claimed.',
|
|
145
|
+
try: 'Type /builder-release, or say "Give this task back."',
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: 'preview_before_ship', moment: 'first_claim', title: 'See a change before it ships',
|
|
149
|
+
what: 'For changes you can see, the AI can show you the page as it will look, before anything goes live.',
|
|
150
|
+
when: 'When the task changes something on screen.',
|
|
151
|
+
try: 'Say: "Let me see it before you ship", or type /builder-stage.',
|
|
152
|
+
},
|
|
153
|
+
// first_ship
|
|
154
|
+
{
|
|
155
|
+
id: 'what_ship_does', moment: 'first_ship', title: 'What shipping does',
|
|
156
|
+
what: 'Shipping finishes a task: automatic reviewers check the work, then it is added to the project. Going live for your users can wait for a person to say yes.',
|
|
157
|
+
when: 'When the work on your task is finished.',
|
|
158
|
+
try: 'Type /builder-ship, or say "Ship it."',
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: 'review_findings', moment: 'first_ship', title: 'When the reviewers ask for fixes',
|
|
162
|
+
what: 'If the reviewers find a problem, the task is held, not lost. The AI fixes what they listed and asks for another review.',
|
|
163
|
+
when: 'When a ship says it needs fixes.',
|
|
164
|
+
try: 'Say: "Fix what the reviewers found and try again."',
|
|
165
|
+
},
|
|
166
|
+
// after_ship
|
|
167
|
+
{
|
|
168
|
+
id: 'what_happens_after', moment: 'after_ship', title: 'What happens after a ship',
|
|
169
|
+
what: 'The task is marked shipped, anything that was waiting on it becomes ready, and the record of the work is kept for the next person.',
|
|
170
|
+
when: 'Right after your first task ships.',
|
|
171
|
+
try: 'Ask: "What did that ship unlock?"',
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
id: 'fresh_session', moment: 'after_ship', title: 'Start fresh after a few tasks',
|
|
175
|
+
what: 'A long conversation gets slower and costlier. After a few tasks, the AI can write a short note so a fresh conversation picks up exactly where you left off.',
|
|
176
|
+
when: 'After three or so tasks in one conversation.',
|
|
177
|
+
try: 'Say: "Hand this off to a fresh session", or type /session-handoff.',
|
|
178
|
+
},
|
|
179
|
+
// invite
|
|
180
|
+
{
|
|
181
|
+
id: 'invite_people', moment: 'invite', title: 'Invite people',
|
|
182
|
+
what: 'Bring someone onto the project by their GitHub name. You choose whether anyone can join, people must ask first, or only invited people can sign in.',
|
|
183
|
+
when: 'When you want someone else to help build.',
|
|
184
|
+
try: 'In the hall, use "Invite someone" and enter their GitHub name, or ask "How do I invite someone?"',
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
id: 'ask_for_help', moment: 'invite', title: 'Ask a teammate for help',
|
|
188
|
+
what: 'Send a named person, or anyone with the right skills, a request for help on a task. It shows up in their own list.',
|
|
189
|
+
when: 'When you are stuck on a task and someone else knows the area.',
|
|
190
|
+
try: 'Say: "Ask Sam to help with task 42", or type /ask-for-help.',
|
|
191
|
+
},
|
|
192
|
+
// settings
|
|
193
|
+
{
|
|
194
|
+
id: 'ship_mode', moment: 'settings', title: 'Ship mode: who starts a ship',
|
|
195
|
+
what: 'In Auto, the AI ships a finished task by itself. In Manual, it stops and waits for you to say "ship it".',
|
|
196
|
+
when: 'When you want more, or less, control over when work is added to the project.',
|
|
197
|
+
try: 'In the hall\'s project settings pick Auto or Manual, or run `npx bongos ship-mode` to see the current one.',
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
id: 'how_the_ai_talks', moment: 'settings', title: 'How the AI talks to you',
|
|
201
|
+
what: 'Tell Bongos how technical you are and how long you like answers. Every AI session on this project follows it.',
|
|
202
|
+
when: 'When answers feel too technical, too long, or too short.',
|
|
203
|
+
try: 'Open your settings in the hall and look for how the AI talks to you, or say "Keep your answers shorter."',
|
|
204
|
+
},
|
|
205
|
+
// stuck
|
|
206
|
+
{
|
|
207
|
+
id: 'when_stuck', moment: 'stuck', title: 'When something goes wrong',
|
|
208
|
+
what: 'Every refusal comes with a plain explanation: what happened, what the AI will do, and whether it needs you. Most fix themselves.',
|
|
209
|
+
when: 'When a command fails or you do not understand what happened.',
|
|
210
|
+
try: 'Say: "Explain that in plain words and fix it."',
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
id: 'tell_the_bongos_team', moment: 'stuck', title: 'Tell the Bongos team',
|
|
214
|
+
what: 'If Bongos itself gets in your way, say so. Your words go to the people who build Bongos, and each complaint becomes a fix.',
|
|
215
|
+
when: 'Whenever Bongos is confusing, slow, or broken.',
|
|
216
|
+
try: 'Say: "Bongos feedback: …" followed by what went wrong.',
|
|
217
|
+
},
|
|
218
|
+
]);
|
|
219
|
+
|
|
220
|
+
const MOMENT_IDS = new Set(MOMENTS.map((m) => m.id));
|
|
221
|
+
|
|
222
|
+
function stepById(id) { return STEPS.find((s) => s.id === id) || null; }
|
|
223
|
+
|
|
224
|
+
// The steps for one moment, in guide order. Unknown moment → [].
|
|
225
|
+
function stepsForMoment(momentId) { return STEPS.filter((s) => s.moment === momentId); }
|
|
226
|
+
|
|
227
|
+
// The first step not in `readIds` (an iterable of step ids), in guide order — what
|
|
228
|
+
// "continue the tutorial" means. null when everything is read.
|
|
229
|
+
function nextUnread(readIds) {
|
|
230
|
+
const read = new Set(readIds || []);
|
|
231
|
+
return STEPS.find((s) => !read.has(s.id)) || null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function formatStep(step) {
|
|
235
|
+
return [`${step.title} [${step.id}]`, ` What it is for: ${step.what}`, ` When: ${step.when}`, ` Try: ${step.try}`].join('\n');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function formatGuide({ moment = null } = {}) {
|
|
239
|
+
const out = [];
|
|
240
|
+
for (const m of MOMENTS) {
|
|
241
|
+
if (moment && m.id !== moment) continue;
|
|
242
|
+
out.push(`## ${m.title}`);
|
|
243
|
+
for (const s of stepsForMoment(m.id)) out.push(formatStep(s), '');
|
|
244
|
+
}
|
|
245
|
+
return out.join('\n').trimEnd() + '\n';
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (require.main === module) {
|
|
249
|
+
const i = process.argv.indexOf('--moment');
|
|
250
|
+
const moment = i > -1 ? process.argv[i + 1] : null;
|
|
251
|
+
if (moment && !MOMENT_IDS.has(moment)) {
|
|
252
|
+
process.stderr.write(`unknown moment "${moment}" — one of: ${[...MOMENT_IDS].join(', ')}\n`);
|
|
253
|
+
process.exitCode = 2;
|
|
254
|
+
} else {
|
|
255
|
+
process.stdout.write(formatGuide({ moment }));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
module.exports = { MOMENTS, STEPS, MOMENT_IDS, stepById, stepsForMoment, nextUnread, formatStep, formatGuide };
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.125",
|
|
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.125",
|
|
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.125",
|
|
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
|
@@ -9726,5 +9726,15 @@
|
|
|
9726
9726
|
"id": "1004633",
|
|
9727
9727
|
"text": "Found why a merged task on your own project stayed at 'confirmed': with no GitHub key set up on the project's server, Bongos could not prove the merge happened, and its background checker switched itself off. It now confirms t"
|
|
9728
9728
|
}
|
|
9729
|
+
],
|
|
9730
|
+
"1.21.125": [
|
|
9731
|
+
{
|
|
9732
|
+
"id": "1004632",
|
|
9733
|
+
"text": "When a project updates its Bongos core, the next task's ship now uses the new core automatically instead of the old one, so it no longer fails for no visible reason."
|
|
9734
|
+
},
|
|
9735
|
+
{
|
|
9736
|
+
"id": "1004627",
|
|
9737
|
+
"text": "Bongos now has a 27-step plain-language how-to guide that an AI can walk a new person through inside their session, from where to start to their first ship."
|
|
9738
|
+
}
|
|
9729
9739
|
]
|
|
9730
9740
|
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// scripts/gds/core-pin-sync.js — make `bongos ship` run the core the checkout PINS (task 1004632)
|
|
2
|
+
//
|
|
3
|
+
// WHY. An instance's task worktrees (`.claude/worktrees/task-N`) have no `node_modules`
|
|
4
|
+
// of their own: Node and `npx` walk up to the MAIN checkout's, and so does every
|
|
5
|
+
// `bongos ship`. After a core-update task lands, the next task's branch pins the new
|
|
6
|
+
// core in package.json — but its worktree still resolves the main checkout's install,
|
|
7
|
+
// which is whatever version was there before. On the owner's project that meant a
|
|
8
|
+
// ship ran 1.21.20 against a branch pinned to 1.21.71 and failed on a bug 1.21.71 had
|
|
9
|
+
// already fixed; the AI had to `npm ci` by hand in both places.
|
|
10
|
+
//
|
|
11
|
+
// THE FIX. Before `bongos ship` runs, the dispatcher asks `planCoreSync()` whether the
|
|
12
|
+
// core it is running is the one the checkout it stands in pins. When it is not, it
|
|
13
|
+
// installs the pinned core INTO that checkout (`npm ci`, or `npm install` with no
|
|
14
|
+
// lockfile) and re-runs the same command under it. A worktree that gets its own
|
|
15
|
+
// install keeps resolving it from then on. If the install fails, the builder gets one
|
|
16
|
+
// line that names the command to run — never a silent run on the wrong core. Only
|
|
17
|
+
// `ship` checks: it is the verb that runs the core's land/smoke code against the
|
|
18
|
+
// branch, and it is slow anyway — the many quick verbs (status, recall, exec) pay no
|
|
19
|
+
// git spawn for it.
|
|
20
|
+
//
|
|
21
|
+
// Safety. The pin comes from the branch, and installing it then RUNS it — so a branch
|
|
22
|
+
// must never be able to choose what gets installed. Two gates, both required:
|
|
23
|
+
// 1. the spec is a shape the core itself writes — a vendored
|
|
24
|
+
// `file:vendor/bongos-core-<v>.tgz` or an exact registry semver;
|
|
25
|
+
// 2. the pin has already LANDED: package.json, package-lock.json and the vendored
|
|
26
|
+
// tarball in this checkout are byte-identical (same git blob, no local edits) to
|
|
27
|
+
// the ones on `main` / `origin/main`. That is exactly the case this fixes — a
|
|
28
|
+
// core update merged, the next task's branch carries it — and a branch that pins
|
|
29
|
+
// something main does not get the remedy line, never an automatic install.
|
|
30
|
+
// The install also runs with `--ignore-scripts` (the core's dependencies need no
|
|
31
|
+
// install hook), through npm's own JS entry under this node where it can be found, so
|
|
32
|
+
// no shell is involved; only a Windows box without it falls back to `npm.cmd` + shell,
|
|
33
|
+
// with fixed arguments. A `node_modules` that
|
|
34
|
+
// is a LINK (the junction worktree.js gives an outside-repo worktree) is unlinked
|
|
35
|
+
// first — installing through it would rewrite the MAIN checkout's install. It is never
|
|
36
|
+
// followed and a real directory is never removed. The re-run sets BONGOS_CORE_SYNCED
|
|
37
|
+
// so a pin the install still cannot satisfy cannot loop.
|
|
38
|
+
//
|
|
39
|
+
// It does nothing in the core repo itself (the running package is not `@bongos/core`),
|
|
40
|
+
// outside git, or when the checkout pins no core. Pure planner + injectable runner, so
|
|
41
|
+
// tests/core_pin_sync.mjs drives every branch without npm or a network.
|
|
42
|
+
|
|
43
|
+
'use strict';
|
|
44
|
+
|
|
45
|
+
const fs = require('node:fs');
|
|
46
|
+
const path = require('node:path');
|
|
47
|
+
const { spawnSync } = require('node:child_process');
|
|
48
|
+
|
|
49
|
+
const CORE_PKG = '@bongos/core';
|
|
50
|
+
const SYNCED_ENV = 'BONGOS_CORE_SYNCED';
|
|
51
|
+
|
|
52
|
+
function readJson(file, fsImpl = fs) {
|
|
53
|
+
try { return JSON.parse(String(fsImpl.readFileSync(file, 'utf8')).replace(/^/, '')); } catch { return null; }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// { version, safe } for a dependency spec. `version` reads the same shapes upgrade.js
|
|
57
|
+
// does (kept local so the dispatcher never loads upgrade.js's tree); `safe` is true
|
|
58
|
+
// only for the two shapes the core writes itself — a tarball under vendor/ named for
|
|
59
|
+
// its version, or an EXACT registry semver.
|
|
60
|
+
function parseSpec(spec) {
|
|
61
|
+
const s = String(spec || '').trim();
|
|
62
|
+
const tgz = s.match(/bongos-core-(\d+\.\d+\.\d+[^/\\]*)\.tgz$/);
|
|
63
|
+
if (tgz) return { version: tgz[1], safe: /^file:(\.\/)?vendor\/bongos-core-[^/\\]+\.tgz$/.test(s) };
|
|
64
|
+
if (/^file:/.test(s)) return { version: null, safe: false };
|
|
65
|
+
const sem = s.match(/^[\^~>=<\s]*v?(\d+\.\d+\.\d+[A-Za-z0-9.+-]*)$/);
|
|
66
|
+
if (!sem) return { version: null, safe: false };
|
|
67
|
+
return { version: sem[1], safe: /^\d+\.\d+\.\d+(-[A-Za-z0-9.]+)?$/.test(s) };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function versionFromSpec(spec) { return parseSpec(spec).version; }
|
|
71
|
+
|
|
72
|
+
function coreSpec(root, fsImpl = fs) {
|
|
73
|
+
const pkg = readJson(path.join(root, 'package.json'), fsImpl);
|
|
74
|
+
return pkg ? ((pkg.dependencies || {})[CORE_PKG] || (pkg.devDependencies || {})[CORE_PKG] || null) : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// What the checkout at `root` pins: the lockfile's resolved version first (exact), then
|
|
78
|
+
// the package.json spec.
|
|
79
|
+
function pinnedCoreVersion(root, fsImpl = fs) {
|
|
80
|
+
const lock = readJson(path.join(root, 'package-lock.json'), fsImpl);
|
|
81
|
+
const locked = lock && lock.packages && lock.packages[`node_modules/${CORE_PKG}`];
|
|
82
|
+
if (locked && locked.version) return locked.version;
|
|
83
|
+
return versionFromSpec(coreSpec(root, fsImpl));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// The refs that count as LANDED for gate 2: a pin is trusted only once it is on one of them.
|
|
87
|
+
const LANDED_REFS = ['main', 'origin/main'];
|
|
88
|
+
|
|
89
|
+
function gitOut(args, cwd, run) {
|
|
90
|
+
try {
|
|
91
|
+
const r = run('git', args, { cwd, encoding: 'utf8' });
|
|
92
|
+
return r && r.status === 0 ? String(r.stdout || '').trim() : null;
|
|
93
|
+
} catch { return null; }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Gate 2: every file that decides what `npm ci` installs — package.json, the lockfile,
|
|
97
|
+
// the project .npmrc (it can redirect the registry), and the vendored tarball the spec
|
|
98
|
+
// names — is in the SAME state as on a landed ref: the same blob as it sits on disk (so
|
|
99
|
+
// an uncommitted edit counts), or absent on both. Deleting a file the landed ref has
|
|
100
|
+
// is a difference too. True when one landed ref matches all of them. The lockfile is
|
|
101
|
+
// also required outright: the install is `npm ci`, whose integrity check is the point.
|
|
102
|
+
const PIN_FILES = ['package.json', 'package-lock.json', '.npmrc'];
|
|
103
|
+
|
|
104
|
+
function pinLanded(root, { run = spawnSync, fsImpl = fs } = {}) {
|
|
105
|
+
if (!fsImpl.existsSync(path.join(root, 'package-lock.json'))) return false;
|
|
106
|
+
const spec = coreSpec(root, fsImpl);
|
|
107
|
+
const files = [...PIN_FILES];
|
|
108
|
+
const vendored = String(spec || '').match(/^file:(?:\.\/)?(vendor\/bongos-core-[^/\\]+\.tgz)$/);
|
|
109
|
+
if (vendored) files.push(vendored[1]);
|
|
110
|
+
const local = files.map((f) => (fsImpl.existsSync(path.join(root, f)) ? (gitOut(['hash-object', '--', f], root, run) || 'unreadable') : null));
|
|
111
|
+
return LANDED_REFS.some((ref) => files.every((f, i) => gitOut(['rev-parse', '--verify', '--quiet', `${ref}:${f}`], root, run) === local[i]));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function gitTopLevel(cwd, run = spawnSync) {
|
|
115
|
+
try {
|
|
116
|
+
const r = run('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8' });
|
|
117
|
+
if (r.status !== 0) return null;
|
|
118
|
+
const top = String(r.stdout || '').trim();
|
|
119
|
+
return top ? path.resolve(top) : null;
|
|
120
|
+
} catch { return null; }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Pure decision. Inputs are facts; output says whether to sync and why.
|
|
124
|
+
// runningPkg the running core's package.json ({ name, version })
|
|
125
|
+
// root the git top-level of the cwd (null outside git)
|
|
126
|
+
// pinned what that checkout pins (null when it pins no core)
|
|
127
|
+
// specSafe gate 1: the spec is a shape the core writes itself (parseSpec)
|
|
128
|
+
// landed gate 2: the pin is byte-identical to a landed ref's (pinLanded)
|
|
129
|
+
// env process env (the re-run guard)
|
|
130
|
+
function planCoreSync({ runningPkg, root, pinned, specSafe = false, landed = false, env = process.env }) {
|
|
131
|
+
if (!runningPkg || runningPkg.name !== CORE_PKG) return { action: 'none', reason: 'not-installed-core' };
|
|
132
|
+
if (!root) return { action: 'none', reason: 'no-git' };
|
|
133
|
+
if (!pinned) return { action: 'none', reason: 'no-pin' };
|
|
134
|
+
const running = runningPkg.version || null;
|
|
135
|
+
if (running === pinned) return { action: 'none', reason: 'in-sync', running, pinned };
|
|
136
|
+
if (env && env[SYNCED_ENV]) return { action: 'refuse', reason: 'still-mismatched', running, pinned, root };
|
|
137
|
+
if (!specSafe) return { action: 'refuse', reason: 'unrecognised-spec', running, pinned, root };
|
|
138
|
+
if (!landed) return { action: 'refuse', reason: 'pin-not-landed', running, pinned, root };
|
|
139
|
+
return { action: 'sync', reason: 'mismatch', running, pinned, root };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function installArgs(hasLock) {
|
|
143
|
+
return [hasLock ? 'ci' : 'install', '--ignore-scripts', '--no-audit', '--no-fund'];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// npm's own JS entry beside this node (Windows: <dir>/node_modules/npm; POSIX:
|
|
147
|
+
// <prefix>/lib/node_modules/npm), so the install needs no shell. null when absent.
|
|
148
|
+
function npmCliPath(execPath = process.execPath, fsImpl = fs) {
|
|
149
|
+
const dir = path.dirname(execPath);
|
|
150
|
+
const candidates = [
|
|
151
|
+
path.join(dir, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
|
152
|
+
path.join(dir, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
|
153
|
+
];
|
|
154
|
+
return candidates.find((c) => fsImpl.existsSync(c)) || null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function remedyLine({ root, pinned, running, hasLock = true }) {
|
|
158
|
+
return `bongos: this checkout pins core ${pinned} but ${running || 'another version'} is running. ` +
|
|
159
|
+
`Run \`npm ${hasLock ? 'ci' : 'install'}\` in ${root}, then run the command again.`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Remove a node_modules LINK without following it — the same move as worktree.js's
|
|
163
|
+
// unlinkNodeModulesLink, kept here rather than required: bin/bongos.js is in the
|
|
164
|
+
// executed closure (tests/executed_closure.mjs), and worktree.js would pull its
|
|
165
|
+
// computed require in with it. unlink takes a symlink and (via libuv) a junction; a
|
|
166
|
+
// Windows reparse point unlink refuses falls back to rmdir, which removes the link only.
|
|
167
|
+
function unlinkLink(linkPath, { platform = process.platform, fsImpl = fs } = {}) {
|
|
168
|
+
if (!fsImpl.lstatSync(linkPath).isSymbolicLink()) throw new Error(`${linkPath} is not a link`);
|
|
169
|
+
try { fsImpl.unlinkSync(linkPath); } catch (e) {
|
|
170
|
+
if (platform !== 'win32' || (e.code !== 'EPERM' && e.code !== 'EISDIR')) throw e;
|
|
171
|
+
fsImpl.rmdirSync(linkPath);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Install the pinned core into `root` and re-run the same bongos command under it.
|
|
176
|
+
// Returns the exit code to use.
|
|
177
|
+
function syncAndRerun(plan, { argv, run = spawnSync, fsImpl = fs, log = (s) => process.stderr.write(s + '\n'), env = process.env, platform = process.platform, npmCli = npmCliPath(process.execPath, fsImpl) } = {}) {
|
|
178
|
+
const { root, pinned, running } = plan;
|
|
179
|
+
const hasLock = fsImpl.existsSync(path.join(root, 'package-lock.json'));
|
|
180
|
+
const remedy = () => log(remedyLine({ root, pinned, running, hasLock }));
|
|
181
|
+
log(`bongos: this checkout pins core ${pinned}, but ${running} is running — installing ${pinned} here first.`);
|
|
182
|
+
const nm = path.join(root, 'node_modules');
|
|
183
|
+
let st = null;
|
|
184
|
+
try { st = fsImpl.lstatSync(nm); } catch { /* absent */ }
|
|
185
|
+
if (st && st.isSymbolicLink()) {
|
|
186
|
+
try { unlinkLink(nm, { platform, fsImpl }); } catch (e) {
|
|
187
|
+
log(`bongos: could not detach the shared node_modules link (${e.message}).`);
|
|
188
|
+
remedy();
|
|
189
|
+
return 1;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// npm's JS entry under this node — no shell. Fallback: `npm`, which on win32 is
|
|
193
|
+
// npm.cmd and needs a shell (Node refuses a .cmd without one); the args are fixed
|
|
194
|
+
// strings, never branch data.
|
|
195
|
+
const cli = npmCli;
|
|
196
|
+
const inst = cli
|
|
197
|
+
? run(process.execPath, [cli, ...installArgs(hasLock)], { cwd: root, stdio: 'inherit', env })
|
|
198
|
+
: run('npm', installArgs(hasLock), { cwd: root, stdio: 'inherit', env, ...(platform === 'win32' ? { shell: true } : {}) });
|
|
199
|
+
if (!inst || inst.error || inst.status !== 0) {
|
|
200
|
+
log(`bongos: installing core ${pinned} failed.`);
|
|
201
|
+
remedy();
|
|
202
|
+
return 1;
|
|
203
|
+
}
|
|
204
|
+
const bin = path.join(nm, '@bongos', 'core', 'bin', 'bongos.js');
|
|
205
|
+
if (!fsImpl.existsSync(bin)) { remedy(); return 1; }
|
|
206
|
+
const res = run(process.execPath, [bin, ...argv.slice(2)], { stdio: 'inherit', env: { ...env, [SYNCED_ENV]: '1' } });
|
|
207
|
+
if (!res || res.error) return 1;
|
|
208
|
+
return res.status == null ? 1 : res.status;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// The dispatcher's one call. Returns an exit code when it handled the command (synced
|
|
212
|
+
// and re-ran, or refused), or null to run the verb on the current core.
|
|
213
|
+
function ensurePinnedCore({ coreRoot, argv, cwd = process.cwd(), env = process.env, run = spawnSync, fsImpl = fs, log } = {}) {
|
|
214
|
+
if (env && env.BONGOS_SKIP_CORE_SYNC) return null;
|
|
215
|
+
const runningPkg = readJson(path.join(coreRoot, 'package.json'), fsImpl);
|
|
216
|
+
if (!runningPkg || runningPkg.name !== CORE_PKG) return null; // fast path: the core repo itself
|
|
217
|
+
const root = gitTopLevel(cwd, run);
|
|
218
|
+
const pinned = root ? pinnedCoreVersion(root, fsImpl) : null;
|
|
219
|
+
const specSafe = root ? parseSpec(coreSpec(root, fsImpl)).safe : false;
|
|
220
|
+
const out = { runningPkg, root, pinned, specSafe, env };
|
|
221
|
+
// gate 2 costs git spawns — only pay it when there is a mismatch to act on
|
|
222
|
+
const pre = planCoreSync({ ...out, landed: true });
|
|
223
|
+
const plan = pre.action === 'sync' ? planCoreSync({ ...out, landed: pinLanded(root, { run, fsImpl }) }) : pre;
|
|
224
|
+
if (plan.action === 'none') return null;
|
|
225
|
+
const say = log || ((s) => process.stderr.write(s + '\n'));
|
|
226
|
+
if (plan.action === 'refuse') {
|
|
227
|
+
say(remedyLine({ ...plan, hasLock: fsImpl.existsSync(path.join(root, 'package-lock.json')) }));
|
|
228
|
+
return 1;
|
|
229
|
+
}
|
|
230
|
+
return syncAndRerun(plan, { argv, run, fsImpl, log: say, env });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
module.exports = { CORE_PKG, SYNCED_ENV, LANDED_REFS, pinLanded, npmCliPath, parseSpec, versionFromSpec, installArgs, pinnedCoreVersion, planCoreSync, remedyLine, syncAndRerun, ensurePinnedCore };
|
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.125'; // 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,237 @@
|
|
|
1
|
+
// tests/core_pin_sync.mjs — `bongos <verb>` runs the core the checkout pins (task 1004632)
|
|
2
|
+
//
|
|
3
|
+
// The bug: an instance's task worktree has no node_modules of its own, so after a
|
|
4
|
+
// core-update task lands the next task's `bongos ship` resolved the MAIN checkout's
|
|
5
|
+
// older core and failed on a bug the new pin had already fixed. These cases pin the
|
|
6
|
+
// decision (planCoreSync), the pin reader, and the install-then-re-run path with an
|
|
7
|
+
// injected runner — no npm, no network. The last case is end to end on a real temp
|
|
8
|
+
// git repo with a fake installed core.
|
|
9
|
+
//
|
|
10
|
+
// Run: node tests/core_pin_sync.mjs
|
|
11
|
+
|
|
12
|
+
import { strict as assert } from 'node:assert';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import os from 'node:os';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { spawnSync } from 'node:child_process';
|
|
18
|
+
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const sync = require('../scripts/gds/core-pin-sync.js');
|
|
21
|
+
|
|
22
|
+
let passed = 0;
|
|
23
|
+
let failed = 0;
|
|
24
|
+
function test(name, fn) {
|
|
25
|
+
try { fn(); passed++; console.log(` ok ${name}`); }
|
|
26
|
+
catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.stack || err.message}`); }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const CORE = { name: '@bongos/core', version: '1.21.20' };
|
|
30
|
+
|
|
31
|
+
test('versionFromSpec reads a vendored tarball and a registry semver', () => {
|
|
32
|
+
assert.equal(sync.versionFromSpec('file:vendor/bongos-core-1.21.71.tgz'), '1.21.71');
|
|
33
|
+
assert.equal(sync.versionFromSpec('^1.21.71'), '1.21.71');
|
|
34
|
+
assert.equal(sync.versionFromSpec('1.21.71'), '1.21.71');
|
|
35
|
+
assert.equal(sync.versionFromSpec('file:../somewhere'), null);
|
|
36
|
+
assert.equal(sync.versionFromSpec(undefined), null);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('parseSpec: only the shapes the core writes itself are safe to auto-install', () => {
|
|
40
|
+
assert.equal(sync.parseSpec('file:vendor/bongos-core-1.21.71.tgz').safe, true);
|
|
41
|
+
assert.equal(sync.parseSpec('1.21.71').safe, true);
|
|
42
|
+
assert.equal(sync.parseSpec('^1.21.71').safe, false, 'a range is not an exact pin');
|
|
43
|
+
assert.equal(sync.parseSpec('file:../evil/bongos-core-1.21.71.tgz').safe, false);
|
|
44
|
+
assert.equal(sync.parseSpec('git+https://x/y.git').safe, false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('planCoreSync: a pin that has not landed on main refuses — a branch cannot choose what gets installed', () => {
|
|
48
|
+
const p = sync.planCoreSync({ runningPkg: CORE, root: '/r', pinned: '9.9.9', specSafe: true, landed: false, env: {} });
|
|
49
|
+
assert.equal(p.action, 'refuse');
|
|
50
|
+
assert.equal(p.reason, 'pin-not-landed');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('planCoreSync: an unrecognised spec refuses with the remedy, never installs', () => {
|
|
54
|
+
const p = sync.planCoreSync({ runningPkg: CORE, root: '/r', pinned: '1.21.71', specSafe: false, env: {} });
|
|
55
|
+
assert.equal(p.action, 'refuse');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('installArgs: never runs install scripts; ci with a lockfile, install without', () => {
|
|
59
|
+
assert.deepEqual(sync.installArgs(true).slice(0, 2), ['ci', '--ignore-scripts']);
|
|
60
|
+
assert.deepEqual(sync.installArgs(false).slice(0, 2), ['install', '--ignore-scripts']);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('planCoreSync: the core repo itself is never touched', () => {
|
|
64
|
+
const p = sync.planCoreSync({ runningPkg: { name: 'cloud-bongos', version: '0.0.2' }, root: '/r', pinned: '1.21.71', env: {} });
|
|
65
|
+
assert.equal(p.action, 'none');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('planCoreSync: in sync, no git, no pin → carry on', () => {
|
|
69
|
+
assert.equal(sync.planCoreSync({ runningPkg: { ...CORE, version: '1.21.71' }, root: '/r', pinned: '1.21.71', env: {} }).action, 'none');
|
|
70
|
+
assert.equal(sync.planCoreSync({ runningPkg: CORE, root: null, pinned: '1.21.71', env: {} }).action, 'none');
|
|
71
|
+
assert.equal(sync.planCoreSync({ runningPkg: CORE, root: '/r', pinned: null, env: {} }).action, 'none');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('planCoreSync: the task-49 shape (pinned 1.21.71, running 1.21.20) syncs', () => {
|
|
75
|
+
const p = sync.planCoreSync({ runningPkg: CORE, root: '/r', pinned: '1.21.71', specSafe: true, landed: true, env: {} });
|
|
76
|
+
assert.equal(p.action, 'sync');
|
|
77
|
+
assert.equal(p.pinned, '1.21.71');
|
|
78
|
+
assert.equal(p.running, '1.21.20');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('planCoreSync: a re-run that is STILL mismatched refuses instead of looping', () => {
|
|
82
|
+
const p = sync.planCoreSync({ runningPkg: CORE, root: '/r', pinned: '1.21.71', env: { [sync.SYNCED_ENV]: '1' } });
|
|
83
|
+
assert.equal(p.action, 'refuse');
|
|
84
|
+
assert.match(sync.remedyLine(p), /npm ci/);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
function tmpCheckout({ pinSpec, lockVersion }) {
|
|
88
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'core-pin-sync-'));
|
|
89
|
+
fs.writeFileSync(path.join(dir, 'package.json'), '' + JSON.stringify({ name: 'inst', dependencies: { '@bongos/core': pinSpec } }));
|
|
90
|
+
if (lockVersion) {
|
|
91
|
+
fs.writeFileSync(path.join(dir, 'package-lock.json'), JSON.stringify({ packages: { 'node_modules/@bongos/core': { version: lockVersion } } }));
|
|
92
|
+
}
|
|
93
|
+
return dir;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
test('pinnedCoreVersion prefers the lockfile, falls back to package.json (BOM tolerated)', () => {
|
|
97
|
+
const a = tmpCheckout({ pinSpec: 'file:vendor/bongos-core-1.21.71.tgz', lockVersion: '1.21.72' });
|
|
98
|
+
assert.equal(sync.pinnedCoreVersion(a), '1.21.72');
|
|
99
|
+
const b = tmpCheckout({ pinSpec: 'file:vendor/bongos-core-1.21.71.tgz' });
|
|
100
|
+
assert.equal(fs.readFileSync(path.join(b, 'package.json'), 'utf8').charCodeAt(0), 0xFEFF, 'fixture really carries a BOM');
|
|
101
|
+
assert.equal(sync.pinnedCoreVersion(b), '1.21.71');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('syncAndRerun: npm ci in the checkout, then re-run the same argv under its own core with the loop guard', () => {
|
|
105
|
+
const root = tmpCheckout({ pinSpec: 'file:vendor/bongos-core-1.21.71.tgz', lockVersion: '1.21.71' });
|
|
106
|
+
const calls = [];
|
|
107
|
+
const run = (cmd, args, opts) => {
|
|
108
|
+
calls.push({ cmd, args, opts });
|
|
109
|
+
if (cmd === 'npm') {
|
|
110
|
+
const d = path.join(root, 'node_modules', '@bongos', 'core', 'bin');
|
|
111
|
+
fs.mkdirSync(d, { recursive: true });
|
|
112
|
+
fs.writeFileSync(path.join(d, 'bongos.js'), '');
|
|
113
|
+
return { status: 0 };
|
|
114
|
+
}
|
|
115
|
+
return { status: 7 };
|
|
116
|
+
};
|
|
117
|
+
const code = sync.syncAndRerun({ root, pinned: '1.21.71', running: '1.21.20' }, { argv: ['node', 'bongos', 'ship', '49'], run, log: () => {}, env: {}, platform: 'linux', npmCli: null });
|
|
118
|
+
assert.equal(code, 7, 'the re-run exit code is passed through');
|
|
119
|
+
assert.equal(calls[0].cmd, 'npm');
|
|
120
|
+
assert.equal(calls[0].args[0], 'ci');
|
|
121
|
+
assert.equal(path.resolve(calls[0].opts.cwd), path.resolve(root));
|
|
122
|
+
assert.equal(calls[1].cmd, process.execPath);
|
|
123
|
+
assert.deepEqual(calls[1].args.slice(1), ['ship', '49']);
|
|
124
|
+
assert.ok(calls[1].args[0].includes(path.join('node_modules', '@bongos', 'core', 'bin', 'bongos.js')));
|
|
125
|
+
assert.equal(calls[1].opts.env[sync.SYNCED_ENV], '1');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('syncAndRerun: a failed install exits 1 with the one-line remedy, and never re-runs', () => {
|
|
129
|
+
const root = tmpCheckout({ pinSpec: '1.21.71' });
|
|
130
|
+
const logs = [];
|
|
131
|
+
const calls = [];
|
|
132
|
+
const run = (cmd, args) => { calls.push(cmd); return { status: 1 }; };
|
|
133
|
+
const code = sync.syncAndRerun({ root, pinned: '1.21.71', running: '1.21.20' }, { argv: ['node', 'bongos', 'ship'], run, log: (s) => logs.push(s), env: {}, platform: 'linux', npmCli: null });
|
|
134
|
+
assert.equal(code, 1);
|
|
135
|
+
assert.deepEqual(calls, ['npm']);
|
|
136
|
+
assert.equal(calls.length, 1);
|
|
137
|
+
assert.ok(logs.some((l) => /Run `npm install` in /.test(l)), 'no lockfile → remedy names npm install: ' + logs.join('\n'));
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('syncAndRerun: a node_modules LINK is detached first, never installed through', () => {
|
|
141
|
+
const root = tmpCheckout({ pinSpec: '1.21.71', lockVersion: '1.21.71' });
|
|
142
|
+
const shared = fs.mkdtempSync(path.join(os.tmpdir(), 'core-pin-shared-'));
|
|
143
|
+
fs.writeFileSync(path.join(shared, 'keep.txt'), 'shared');
|
|
144
|
+
fs.symlinkSync(shared, path.join(root, 'node_modules'), 'junction');
|
|
145
|
+
const run = (cmd) => {
|
|
146
|
+
if (cmd === 'npm') {
|
|
147
|
+
assert.ok(!fs.existsSync(path.join(root, 'node_modules')), 'link must be gone before npm runs');
|
|
148
|
+
return { status: 1 };
|
|
149
|
+
}
|
|
150
|
+
return { status: 0 };
|
|
151
|
+
};
|
|
152
|
+
sync.syncAndRerun({ root, pinned: '1.21.71', running: '1.21.20' }, { argv: ['node', 'bongos', 'ship'], run, log: () => {}, env: {}, npmCli: null });
|
|
153
|
+
assert.equal(fs.readFileSync(path.join(shared, 'keep.txt'), 'utf8'), 'shared', 'the shared install is untouched');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
function git(cwd, ...args) {
|
|
157
|
+
const r = spawnSync('git', ['-c', 'user.email=t@t', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', ...args], { cwd, encoding: 'utf8' });
|
|
158
|
+
assert.equal(r.status, 0, r.stderr);
|
|
159
|
+
return r.stdout;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// A real repo: main carries the 1.21.71 pin (the core update landed); the checkout is
|
|
163
|
+
// on a task branch. The running core is the older 1.21.20.
|
|
164
|
+
function landedRepo() {
|
|
165
|
+
const root = tmpCheckout({ pinSpec: 'file:vendor/bongos-core-1.21.71.tgz', lockVersion: '1.21.71' });
|
|
166
|
+
fs.mkdirSync(path.join(root, 'vendor'));
|
|
167
|
+
fs.writeFileSync(path.join(root, 'vendor', 'bongos-core-1.21.71.tgz'), 'tarball-bytes');
|
|
168
|
+
git(root, 'init', '-q', '-b', 'main');
|
|
169
|
+
git(root, 'add', '-A');
|
|
170
|
+
git(root, 'commit', '-qm', 'pin 1.21.71');
|
|
171
|
+
git(root, 'checkout', '-qb', 'task-49');
|
|
172
|
+
const coreRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'core-pin-running-'));
|
|
173
|
+
fs.writeFileSync(path.join(coreRoot, 'package.json'), JSON.stringify(CORE));
|
|
174
|
+
const installs = [];
|
|
175
|
+
const run = (cmd, args, opts) => {
|
|
176
|
+
if (cmd === 'git') return spawnSync(cmd, args, opts);
|
|
177
|
+
if (cmd === 'npm' || String(args[0] || '').endsWith('npm-cli.js')) installs.push(args);
|
|
178
|
+
return { status: 1 };
|
|
179
|
+
};
|
|
180
|
+
return { root, coreRoot, installs, run };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
test('ensurePinnedCore end to end: a pin that LANDED on main gets installed', () => {
|
|
184
|
+
const { root, coreRoot, installs, run } = landedRepo();
|
|
185
|
+
assert.equal(sync.pinLanded(root, { run }), true);
|
|
186
|
+
const code = sync.ensurePinnedCore({ coreRoot, argv: ['node', 'bongos', 'ship'], cwd: root, env: {}, run, log: () => {} });
|
|
187
|
+
assert.equal(code, 1, 'the fake install fails, so the remedy exit');
|
|
188
|
+
assert.equal(installs.length, 1, 'npm ran once');
|
|
189
|
+
// in sync → null (carry on), no install
|
|
190
|
+
fs.writeFileSync(path.join(coreRoot, 'package.json'), JSON.stringify({ ...CORE, version: '1.21.71' }));
|
|
191
|
+
installs.length = 0;
|
|
192
|
+
assert.equal(sync.ensurePinnedCore({ coreRoot, argv: ['node', 'bongos', 'ship'], cwd: root, env: {}, run, log: () => {} }), null);
|
|
193
|
+
assert.equal(installs.length, 0);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('ensurePinnedCore end to end: a branch-swapped tarball (committed or not) is refused, never installed', () => {
|
|
197
|
+
const { root, coreRoot, installs, run } = landedRepo();
|
|
198
|
+
fs.writeFileSync(path.join(root, 'vendor', 'bongos-core-1.21.71.tgz'), 'attacker-bytes');
|
|
199
|
+
assert.equal(sync.pinLanded(root, { run }), false, 'uncommitted swap');
|
|
200
|
+
git(root, 'commit', '-qam', 'swap');
|
|
201
|
+
assert.equal(sync.pinLanded(root, { run }), false, 'committed on the branch only');
|
|
202
|
+
const logs = [];
|
|
203
|
+
const code = sync.ensurePinnedCore({ coreRoot, argv: ['node', 'bongos', 'ship'], cwd: root, env: {}, run, log: (l) => logs.push(l) });
|
|
204
|
+
assert.equal(code, 1);
|
|
205
|
+
assert.equal(installs.length, 0);
|
|
206
|
+
assert.ok(logs.some((l) => /Run `npm ci`/.test(l)), logs.join('\n'));
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('ensurePinnedCore end to end: a branch-only version bump is refused, never installed', () => {
|
|
210
|
+
const { root, coreRoot, installs, run } = landedRepo();
|
|
211
|
+
fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: 'inst', dependencies: { '@bongos/core': '9.9.9' } }));
|
|
212
|
+
fs.rmSync(path.join(root, 'package-lock.json'));
|
|
213
|
+
git(root, 'commit', '-qam', 'bump');
|
|
214
|
+
assert.equal(sync.ensurePinnedCore({ coreRoot, argv: ['node', 'bongos', 'ship'], cwd: root, env: {}, run, log: () => {} }), 1);
|
|
215
|
+
assert.equal(installs.length, 0);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('ensurePinnedCore end to end: deleting the lockfile or adding an .npmrc on the branch is refused', () => {
|
|
219
|
+
for (const tamper of [
|
|
220
|
+
(root) => fs.rmSync(path.join(root, 'package-lock.json')),
|
|
221
|
+
(root) => fs.writeFileSync(path.join(root, '.npmrc'), 'registry=https://evil.example/\n'),
|
|
222
|
+
]) {
|
|
223
|
+
const { root, coreRoot, installs, run } = landedRepo();
|
|
224
|
+
tamper(root);
|
|
225
|
+
assert.equal(sync.pinLanded(root, { run }), false);
|
|
226
|
+
assert.equal(sync.ensurePinnedCore({ coreRoot, argv: ['node', 'bongos', 'ship'], cwd: root, env: {}, run, log: () => {} }), 1);
|
|
227
|
+
assert.equal(installs.length, 0);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('the dispatcher runs the check for ship only', () => {
|
|
232
|
+
const src = fs.readFileSync(new URL('../bin/bongos.js', import.meta.url), 'utf8');
|
|
233
|
+
assert.match(src, /if \(!COMPILED && verb === 'ship'\) \{\s*const synced = require\('\.\.\/scripts\/gds\/core-pin-sync\.js'\)\.ensurePinnedCore/);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
237
|
+
if (failed) process.exit(1);
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// tests/howto_guide.mjs — the in-session how-to guide's shape + plain-language rule (task 1004627)
|
|
2
|
+
//
|
|
3
|
+
// The guide's step ids are stored as per-person read marks (task 1004634) and matched to
|
|
4
|
+
// moments (task 1004635), so they must be unique, stable-shaped, and every step must
|
|
5
|
+
// belong to a known moment and carry all three parts (what / when / try). It is written
|
|
6
|
+
// for people who may not be technical, so no step may use the accessibility scorer's
|
|
7
|
+
// jargon list (the same list tests/plain_cards.mjs holds the failure cards to).
|
|
8
|
+
//
|
|
9
|
+
// Run: node tests/howto_guide.mjs
|
|
10
|
+
|
|
11
|
+
import { strict as assert } from 'node:assert';
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
const guide = require('../modules/onboarding/howto-guide.js');
|
|
16
|
+
const { JARGON_TERMS } = require('../modules/sessions/accessibility.js');
|
|
17
|
+
|
|
18
|
+
let passed = 0;
|
|
19
|
+
let failed = 0;
|
|
20
|
+
function test(name, fn) {
|
|
21
|
+
try { fn(); passed++; console.log(` ok ${name}`); }
|
|
22
|
+
catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.message}`); }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('about 25 steps, each with a unique snake_case id', () => {
|
|
26
|
+
assert.ok(guide.STEPS.length >= 20 && guide.STEPS.length <= 32, `got ${guide.STEPS.length}`);
|
|
27
|
+
const ids = guide.STEPS.map((s) => s.id);
|
|
28
|
+
assert.equal(new Set(ids).size, ids.length, 'duplicate step id');
|
|
29
|
+
for (const id of ids) assert.match(id, /^[a-z][a-z0-9_]{2,39}$/, id);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('every step has a known moment and all three parts', () => {
|
|
33
|
+
for (const s of guide.STEPS) {
|
|
34
|
+
assert.ok(guide.MOMENT_IDS.has(s.moment), `${s.id}: unknown moment ${s.moment}`);
|
|
35
|
+
for (const f of ['title', 'what', 'when', 'try']) assert.ok(String(s[f] || '').trim().length > 5, `${s.id}.${f} empty`);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('every moment has at least one step and a trigger', () => {
|
|
40
|
+
for (const m of guide.MOMENTS) {
|
|
41
|
+
assert.ok(guide.stepsForMoment(m.id).length > 0, `${m.id} has no steps`);
|
|
42
|
+
assert.ok(m.trigger, `${m.id} has no trigger`);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('the moments the owner named are covered', () => {
|
|
47
|
+
for (const m of ['first_open', 'setup', 'goal', 'task', 'find_work', 'first_ship', 'invite', 'settings']) {
|
|
48
|
+
assert.ok(guide.MOMENT_IDS.has(m), m);
|
|
49
|
+
}
|
|
50
|
+
assert.ok(guide.stepById('where_to_start'), 'the "where to start" step exists');
|
|
51
|
+
assert.ok(guide.stepById('ship_mode'), 'settings covers the ship mode');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('steps are short and in plain language (no jargon)', () => {
|
|
55
|
+
const hits = [];
|
|
56
|
+
for (const s of guide.STEPS) {
|
|
57
|
+
for (const f of ['title', 'what', 'when', 'try']) {
|
|
58
|
+
const text = s[f];
|
|
59
|
+
if (f !== 'try') assert.ok(text.length <= 240, `${s.id}.${f} is ${text.length} chars — keep a step short`);
|
|
60
|
+
for (const term of JARGON_TERMS) {
|
|
61
|
+
const re = new RegExp(`(^|[^A-Za-z])${term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^A-Za-z]|$)`, /^[A-Z+-]+$/.test(term) ? '' : 'i');
|
|
62
|
+
if (re.test(text)) hits.push(`${s.id}.${f}: "${term}"`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
assert.deepEqual(hits, []);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('every slash command a step names is a core skill an instance gets', async () => {
|
|
70
|
+
const fs = await import('node:fs');
|
|
71
|
+
const { isPublishable } = require('../scripts/gds/publish-manifest.js');
|
|
72
|
+
for (const s of guide.STEPS) {
|
|
73
|
+
for (const m of s.try.matchAll(/\/([a-z][a-z-]+)/g)) {
|
|
74
|
+
const skill = `.claude/skills/${m[1]}/SKILL.md`;
|
|
75
|
+
assert.ok(fs.existsSync(new URL(`../${skill}`, import.meta.url)), `${s.id}: /${m[1]} is not a skill`);
|
|
76
|
+
assert.ok(isPublishable(skill), `${s.id}: /${m[1]} does not ship to an instance`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('nextUnread walks the guide in order and ends at null', () => {
|
|
82
|
+
assert.equal(guide.nextUnread([]).id, guide.STEPS[0].id);
|
|
83
|
+
assert.equal(guide.nextUnread([guide.STEPS[0].id]).id, guide.STEPS[1].id);
|
|
84
|
+
assert.equal(guide.nextUnread(guide.STEPS.map((s) => s.id)), null);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('formatGuide renders every step, and one moment on request', () => {
|
|
88
|
+
const all = guide.formatGuide();
|
|
89
|
+
for (const s of guide.STEPS) assert.ok(all.includes(`[${s.id}]`), s.id);
|
|
90
|
+
const one = guide.formatGuide({ moment: 'first_ship' });
|
|
91
|
+
assert.ok(one.includes('[what_ship_does]'));
|
|
92
|
+
assert.ok(!one.includes('[welcome]'));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
96
|
+
if (failed) process.exit(1);
|